diff options
author | Neil <nyamatongwe@gmail.com> | 2025-01-22 21:34:54 +1100 |
---|---|---|
committer | Neil <nyamatongwe@gmail.com> | 2025-01-22 21:34:54 +1100 |
commit | 3de9d37c7b8f4501558d309ada718dc52533e94c (patch) | |
tree | ce87afaff43a86c26c562fefcf0c9a5ea409ef6b /src | |
parent | bac32d7fde0b1d052ac9e926c6b3c96afe39bcfd (diff) | |
download | scintilla-mirror-3de9d37c7b8f4501558d309ada718dc52533e94c.tar.gz |
Bug [#1224]. Remember selection in undo history. SCI_SETSELECTIONUNDOHISTORY.
Diffstat (limited to 'src')
-rw-r--r-- | src/CellBuffer.cxx | 4 | ||||
-rw-r--r-- | src/CellBuffer.h | 1 | ||||
-rw-r--r-- | src/Document.cxx | 48 | ||||
-rw-r--r-- | src/Document.h | 27 | ||||
-rw-r--r-- | src/EditModel.cxx | 59 | ||||
-rw-r--r-- | src/EditModel.h | 41 | ||||
-rw-r--r-- | src/Editor.cxx | 98 | ||||
-rw-r--r-- | src/Editor.h | 7 | ||||
-rw-r--r-- | src/Selection.cxx | 7 | ||||
-rw-r--r-- | src/Selection.h | 2 | ||||
-rw-r--r-- | src/UndoHistory.cxx | 8 | ||||
-rw-r--r-- | src/UndoHistory.h | 1 |
12 files changed, 295 insertions, 8 deletions
diff --git a/src/CellBuffer.cxx b/src/CellBuffer.cxx index 643929352..04486d4c6 100644 --- a/src/CellBuffer.cxx +++ b/src/CellBuffer.cxx @@ -1075,6 +1075,10 @@ int CellBuffer::UndoSequenceDepth() const noexcept { return uh->UndoSequenceDepth(); } +bool CellBuffer::AfterUndoSequenceStart() const noexcept { + return uh->AfterUndoSequenceStart(); +} + void CellBuffer::AddUndoAction(Sci::Position token, bool mayCoalesce) { bool startSequence = false; uh->AppendAction(ActionType::container, token, nullptr, 0, startSequence, mayCoalesce); diff --git a/src/CellBuffer.h b/src/CellBuffer.h index d5d9a0467..1b035597a 100644 --- a/src/CellBuffer.h +++ b/src/CellBuffer.h @@ -167,6 +167,7 @@ public: void BeginUndoAction(bool mayCoalesce=false) noexcept; void EndUndoAction() noexcept; int UndoSequenceDepth() const noexcept; + bool AfterUndoSequenceStart() const noexcept; void AddUndoAction(Sci::Position token, bool mayCoalesce); void DeleteUndoHistory() noexcept; diff --git a/src/Document.cxx b/src/Document.cxx index ac398fcd3..379a88786 100644 --- a/src/Document.cxx +++ b/src/Document.cxx @@ -18,6 +18,7 @@ #include <string_view> #include <vector> #include <array> +#include <map> #include <forward_list> #include <optional> #include <algorithm> @@ -1320,6 +1321,10 @@ bool Document::DeleteChars(Sci::Position pos, Sci::Position len) { } else { enteredModification++; if (!cb.IsReadOnly()) { + if (cb.IsCollectingUndo() && cb.CanRedo()) { + // Abandoning some undo actions so truncate any later selections + TruncateUndoComments(cb.UndoCurrent()); + } NotifyModified( DocModification( ModificationFlags::BeforeDelete | ModificationFlags::User, @@ -1373,6 +1378,10 @@ Sci::Position Document::InsertString(Sci::Position position, const char *s, Sci: s = insertion.c_str(); insertLength = insertion.length(); } + if (cb.IsCollectingUndo() && cb.CanRedo()) { + // Abandoning some undo actions so truncate any later selections + TruncateUndoComments(cb.UndoCurrent()); + } NotifyModified( DocModification( ModificationFlags::BeforeInsert | ModificationFlags::User, @@ -1556,6 +1565,20 @@ Sci::Position Document::Redo() { return newPos; } +void Document::EndUndoAction() noexcept { + cb.EndUndoAction(); + if (UndoSequenceDepth() == 0) { + // Broadcast notification to views to allow end of group processing. + // NotifyGroupCompleted may throw (for memory exhaustion) but this method + // may not as it is called in UndoGroup destructor so ignore exception. + try { + NotifyGroupCompleted(); + } catch (...) { + // Ignore any exception + } + } +} + int Document::UndoSequenceDepth() const noexcept { return cb.UndoSequenceDepth(); } @@ -2504,6 +2527,25 @@ void Document::SetLexInterface(std::unique_ptr<LexInterface> pLexInterface) noex pli = std::move(pLexInterface); } +void Document::SetViewState(void *view, ViewStateShared pVSS) { + viewData[view] = pVSS; +} + +ViewStateShared Document::GetViewState(void *view) const noexcept { + const std::map<void *, ViewStateShared>::const_iterator it = viewData.find(view); + + if (it != viewData.end()) { + return it->second; + } + return {}; +} + +void Document::TruncateUndoComments(int action) { + for (auto &[key, value] : viewData) { + value->TruncateUndo(action); + } +} + int SCI_METHOD Document::SetLineState(Sci_Position line, int state) { const int statePrevious = States()->SetLineState(line, state, LinesTotal()); if (state != statePrevious) { @@ -2700,6 +2742,12 @@ void Document::NotifySavePoint(bool atSavePoint) { } } +void Document::NotifyGroupCompleted() noexcept { + for (const WatcherWithUserData &watcher : watchers) { + watcher.watcher->NotifyGroupCompleted(this, watcher.userData); + } +} + void Document::NotifyModified(DocModification mh) { if (FlagSet(mh.modificationType, ModificationFlags::InsertText)) { decorations->InsertSpace(mh.position, mh.length); diff --git a/src/Document.h b/src/Document.h index cecf02899..4e18c42d1 100644 --- a/src/Document.h +++ b/src/Document.h @@ -172,6 +172,22 @@ public: bool isEnabled; }; +// Base class for view state that can be held and transferred without understanding the contents. +// Declared here but real implementation subclass declared in EditModel +struct ViewState { + ViewState() noexcept = default; + // Deleted so ViewState objects can not be copied + ViewState(const ViewState &) = delete; + ViewState(ViewState &&) = delete; + ViewState &operator=(const ViewState &) = delete; + ViewState &operator=(ViewState &&) = delete; + virtual ~ViewState() noexcept = default; + + virtual void TruncateUndo(int index)=0; +}; + +using ViewStateShared = std::shared_ptr<ViewState>; + struct LexerReleaser { // Called by unique_ptr to destroy/free the Resource void operator()(Scintilla::ILexer5 *pLexer) noexcept { @@ -304,6 +320,8 @@ private: std::unique_ptr<RegexSearchBase> regex; std::unique_ptr<LexInterface> pli; + std::map<void *, ViewStateShared>viewData; + public: Scintilla::EndOfLine eolMode; @@ -396,8 +414,9 @@ public: } bool IsCollectingUndo() const noexcept { return cb.IsCollectingUndo(); } void BeginUndoAction(bool coalesceWithPrior=false) noexcept { cb.BeginUndoAction(coalesceWithPrior); } - void EndUndoAction() noexcept { cb.EndUndoAction(); } + void EndUndoAction() noexcept; int UndoSequenceDepth() const noexcept; + bool AfterUndoSequenceStart() const noexcept { return cb.AfterUndoSequenceStart(); } void AddUndoAction(Sci::Position token, bool mayCoalesce) { cb.AddUndoAction(token, mayCoalesce); } void SetSavePoint(); bool IsSavePoint() const noexcept { return cb.IsSavePoint(); } @@ -534,6 +553,10 @@ public: LexInterface *GetLexInterface() const noexcept; void SetLexInterface(std::unique_ptr<LexInterface> pLexInterface) noexcept; + void SetViewState(void *view, ViewStateShared pVSS); + ViewStateShared GetViewState(void *view) const noexcept; + void TruncateUndoComments(int action); + int SCI_METHOD SetLineState(Sci_Position line, int state) override; int SCI_METHOD GetLineState(Sci_Position line) const override; Sci::Line GetMaxLineState() const noexcept; @@ -574,6 +597,7 @@ public: private: void NotifyModifyAttempt(); void NotifySavePoint(bool atSavePoint); + void NotifyGroupCompleted() noexcept; void NotifyModified(DocModification mh); }; @@ -664,6 +688,7 @@ public: virtual void NotifyDeleted(Document *doc, void *userData) noexcept = 0; virtual void NotifyStyleNeeded(Document *doc, void *userData, Sci::Position endPos) = 0; virtual void NotifyErrorOccurred(Document *doc, void *userData, Scintilla::Status status) = 0; + virtual void NotifyGroupCompleted(Document *doc, void *userData) noexcept = 0; }; } diff --git a/src/EditModel.cxx b/src/EditModel.cxx index 13b3da5b0..60848e6dc 100644 --- a/src/EditModel.cxx +++ b/src/EditModel.cxx @@ -58,6 +58,54 @@ using namespace Scintilla::Internal; Caret::Caret() noexcept : active(false), on(false), period(500) {} +SelectionSimple::SelectionSimple(const Selection &sel) { + selType = sel.selType; + if (sel.IsRectangular()) { + // rectangular or thin + // Could be large so don't remember each range, just the rectangular bounds then reconstitute when undone + rangeRectangular = sel.RectangularCopy(); + } else { + ranges = sel.RangesCopy(); + } + mainRange = sel.Main(); +} + +void ModelState::RememberSelectionForUndo(int index, const Selection &sel) { + historyForUndo.indexCurrent = index; + historyForUndo.ssCurrent = SelectionSimple(sel); +} + +void ModelState::ForgetSelectionForUndo() noexcept { + historyForUndo.indexCurrent = -1; +} + +void ModelState::RememberSelectionOntoStack(int index) { + if ((historyForUndo.indexCurrent >= 0) && (index == historyForUndo.indexCurrent + 1)) { + // Don't overwrite initial selection save if most recent action was coalesced + historyForUndo.stack[index] = historyForUndo.ssCurrent; + } +} + +void ModelState::RememberSelectionForRedoOntoStack(int index, const Selection &sel) { + historyForRedo.stack[index] = SelectionSimple(sel); +} + +const SelectionSimple *ModelState::SelectionFromStack(int index, UndoRedo history) const { + const SelectionHistory &sh = history == UndoRedo::undo ? historyForUndo : historyForRedo; + std::map<int, SelectionSimple>::const_iterator it = sh.stack.find(index); + if (it != sh.stack.end()) { + return &it->second; + } + return {}; +} + +void ModelState::TruncateUndo(int index) { + std::map<int, SelectionSimple>::iterator itUndo = historyForUndo.stack.find(index); + historyForUndo.stack.erase(itUndo, historyForUndo.stack.end()); + std::map<int, SelectionSimple>::iterator itRedo = historyForRedo.stack.find(index); + historyForRedo.stack.erase(itRedo, historyForRedo.stack.end()); +} + EditModel::EditModel() : braces{} { inOverstrike = false; xOffset = 0; @@ -131,3 +179,14 @@ InSelection EditModel::LineEndInSelection(Sci::Line lineDoc) const { int EditModel::GetMark(Sci::Line line) const { return pdoc->GetMark(line, FlagSet(changeHistoryOption, ChangeHistoryOption::Markers)); } + +void EditModel::EnsureModelState() { + if (!modelState && rememberingSelectionForUndo) { + if (ViewStateShared vss = pdoc->GetViewState(this)) { + modelState = std::dynamic_pointer_cast<ModelState>(vss); + } else { + modelState = std::make_shared<ModelState>(); + pdoc->SetViewState(this, std::static_pointer_cast<ViewState>(modelState)); + } + } +} diff --git a/src/EditModel.h b/src/EditModel.h index e36d26fb1..716fd5788 100644 --- a/src/EditModel.h +++ b/src/EditModel.h @@ -21,6 +21,41 @@ public: Caret() noexcept; }; +// Simplified version of selection which won't contain rectangular selection realized +// into ranges as too much data. +// Just a type and single range for now. + +struct SelectionSimple { + std::vector<SelectionRange> ranges; + SelectionRange rangeRectangular; + size_t mainRange = 0; + Selection::SelTypes selType = Selection::SelTypes::stream; + + SelectionSimple() = default; + explicit SelectionSimple(const Selection &sel); +}; + +enum class UndoRedo { undo, redo }; + +struct SelectionHistory { + int indexCurrent = 0; + SelectionSimple ssCurrent; + std::map<int, SelectionSimple> stack; +}; + +struct ModelState : ViewState { + SelectionHistory historyForUndo; + SelectionHistory historyForRedo; + void RememberSelectionForUndo(int index, const Selection &sel); + void ForgetSelectionForUndo() noexcept; + void RememberSelectionOntoStack(int index); + void RememberSelectionForRedoOntoStack(int index, const Selection &sel); + const SelectionSimple *SelectionFromStack(int index, UndoRedo history) const; + virtual void TruncateUndo(int index) final; +}; + +using ModelStateShared = std::shared_ptr<ModelState>; + class EditModel { public: bool inOverstrike; @@ -57,6 +92,10 @@ public: Document *pdoc; + bool rememberingSelectionForUndo = false; + bool needRedoRemembered = false; + ModelStateShared modelState; + EditModel(); // Deleted so EditModel objects can not be copied. EditModel(const EditModel &) = delete; @@ -75,6 +114,8 @@ public: const char *GetFoldDisplayText(Sci::Line lineDoc) const noexcept; InSelection LineEndInSelection(Sci::Line lineDoc) const; [[nodiscard]] int GetMark(Sci::Line line) const; + + void EnsureModelState(); }; } diff --git a/src/Editor.cxx b/src/Editor.cxx index b60d96643..8a7b15719 100644 --- a/src/Editor.cxx +++ b/src/Editor.cxx @@ -926,6 +926,37 @@ void Editor::SetLastXChosen() { lastXChosen = static_cast<int>(pt.x) + xOffset; } +void Editor::RememberSelectionForUndo(int index) { + EnsureModelState(); + if (modelState) { + modelState->RememberSelectionForUndo(index, sel); + needRedoRemembered = true; + // Remember selection at end of processing current message + } +} + +void Editor::RememberSelectionOntoStack(int index) { + EnsureModelState(); + if (modelState) { + // Is undo currently inside a group? + if (!pdoc->AfterUndoSequenceStart()) { + // Don't remember selections inside a grouped sequence as can only + // unto or redo to the start and end of the group. + modelState->RememberSelectionOntoStack(index); + } + } +} + +void Editor::RememberCurrentSelectionForRedoOntoStack() { + if (needRedoRemembered && (pdoc->UndoSequenceDepth() == 0)) { + EnsureModelState(); + if (modelState) { + modelState->RememberSelectionForRedoOntoStack(pdoc->UndoCurrent(), sel); + needRedoRemembered = false; + } + } +} + void Editor::ScrollTo(Sci::Line line, bool moveThumb) { const Sci::Line topLineNew = std::clamp<Sci::Line>(line, 0, MaxScrollPos()); if (topLineNew != topLine) { @@ -2085,13 +2116,13 @@ void Editor::InsertCharacter(std::string_view sv, CharacterSource charSource) { } } } + ThinRectangularRange(); } if (wrapOccurred) { SetScrollBars(); SetVerticalScrollPos(); Redraw(); } - ThinRectangularRange(); // If in wrap mode rewrap current line so EnsureCaretVisible has accurate information EnsureCaretVisible(); // Avoid blinking during rapid typing: @@ -2367,22 +2398,43 @@ void Editor::SelectAll() { Redraw(); } +void Editor::RestoreSelection(Sci::Position newPos, UndoRedo history) { + if (rememberingSelectionForUndo && modelState) { + // Undo wants the element after the current as it just undid it + const int index = pdoc->UndoCurrent() + (history == UndoRedo::undo ? 1 : 0); + const SelectionSimple *pss = modelState->SelectionFromStack(index, history); + if (pss) { + sel.selType = pss->selType; + if (sel.IsRectangular()) { + sel.Rectangular() = pss->rangeRectangular; + // Reconstitute ranges from rectangular range + SetRectangularRange(); + } else { + sel.SetRanges(pss->ranges); + } + // Unsure if this is safe with SetMain potentially failing if document doesn't appear the same. + // Maybe this can occur if the user changes font or wrap mode? + sel.SetMain(pss->mainRange); + newPos = -1; // Used selection from stack so don't use position returned from undo/redo. + } + } + if (newPos >= 0) + SetEmptySelection(newPos); + EnsureCaretVisible(); +} + void Editor::Undo() { if (pdoc->CanUndo()) { InvalidateCaret(); const Sci::Position newPos = pdoc->Undo(); - if (newPos >= 0) - SetEmptySelection(newPos); - EnsureCaretVisible(); + RestoreSelection(newPos, UndoRedo::undo); } } void Editor::Redo() { if (pdoc->CanRedo()) { const Sci::Position newPos = pdoc->Redo(); - if (newPos >= 0) - SetEmptySelection(newPos); - EnsureCaretVisible(); + RestoreSelection(newPos, UndoRedo::redo); } } @@ -2458,6 +2510,17 @@ void Editor::NotifyErrorOccurred(Document *, void *, Status status) { errorStatus = status; } +void Editor::NotifyGroupCompleted(Document *, void *) noexcept { + // RememberCurrentSelectionForRedoOntoStack may throw (for memory exhaustion) + // but this method may not as it is called in UndoGroup destructor so ignore + // exception. + try { + RememberCurrentSelectionForRedoOntoStack(); + } catch (...) { + // Ignore any exception + } +} + void Editor::NotifyChar(int ch, CharacterSource charSource) { NotificationData scn = {}; scn.nmhdr.code = Notification::CharAdded; @@ -2726,6 +2789,14 @@ void Editor::NotifyModified(Document *, DocModification mh, void *) { view.llc.Invalidate(LineLayout::ValidLevel::checkTextAndStyle); } } else { + if (rememberingSelectionForUndo && FlagSet(mh.modificationType, ModificationFlags::User)) { + if (FlagSet(mh.modificationType, ModificationFlags::BeforeInsert | ModificationFlags::BeforeDelete)) { + RememberSelectionForUndo(pdoc->UndoCurrent()); + } + if (FlagSet(mh.modificationType, ModificationFlags::InsertText | ModificationFlags::DeleteText)) { + RememberSelectionOntoStack(pdoc->UndoCurrent()); + } + } // Move selection and brace highlights if (FlagSet(mh.modificationType, ModificationFlags::InsertText)) { sel.MovePositions(true, mh.position, mh.length); @@ -5460,6 +5531,7 @@ void Editor::SetDocPointer(Document *document) { pdoc = document; } pdoc->AddRef(); + modelState.reset(); pcs = ContractionStateCreate(pdoc->IsLarge()); // Ensure all positions within document @@ -8615,6 +8687,13 @@ sptr_t Editor::WndProc(Message iMessage, uptr_t wParam, sptr_t lParam) { case Message::GetChangeHistory: return static_cast<sptr_t>(changeHistoryOption); + case Message::SetSelectionUndoHistory: + rememberingSelectionForUndo = wParam; + break; + + case Message::GetSelectionUndoHistory: + return rememberingSelectionForUndo; + case Message::SetExtraAscent: vs.extraAscent = static_cast<int>(wParam); InvalidateStyleRedraw(); @@ -9024,6 +9103,11 @@ sptr_t Editor::WndProc(Message iMessage, uptr_t wParam, sptr_t lParam) { default: return DefWndProc(iMessage, wParam, lParam); } + + // If there was a change that needs its selection saved and it wasn't explicity saved + // then do that here. + RememberCurrentSelectionForRedoOntoStack(); + //Platform::DebugPrintf("end wnd proc\n"); return 0; } diff --git a/src/Editor.h b/src/Editor.h index d95f552ad..655184a31 100644 --- a/src/Editor.h +++ b/src/Editor.h @@ -178,6 +178,8 @@ constexpr XYScrollOptions operator|(XYScrollOptions a, XYScrollOptions b) noexce return static_cast<XYScrollOptions>(static_cast<int>(a) | static_cast<int>(b)); } +struct SelectionStack; + /** */ class Editor : public EditModel, public DocWatcher { @@ -367,6 +369,9 @@ protected: // ScintillaBase subclass needs access to much of Editor SelectionPosition MovePositionSoVisible(Sci::Position pos, int moveDir); Point PointMainCaret(); void SetLastXChosen(); + void RememberSelectionForUndo(int index); + void RememberSelectionOntoStack(int index); + void RememberCurrentSelectionForRedoOntoStack(); void ScrollTo(Sci::Line line, bool moveThumb=true); virtual void ScrollText(Sci::Line linesToMove); @@ -443,6 +448,7 @@ protected: // ScintillaBase subclass needs access to much of Editor virtual void Paste() = 0; void Clear(); virtual void SelectAll(); + void RestoreSelection(Sci::Position newPos, UndoRedo history); virtual void Undo(); virtual void Redo(); void DelCharBack(bool allowLineStartDeletion); @@ -477,6 +483,7 @@ protected: // ScintillaBase subclass needs access to much of Editor void NotifyDeleted(Document *document, void *userData) noexcept override; void NotifyStyleNeeded(Document *doc, void *userData, Sci::Position endStyleNeeded) override; void NotifyErrorOccurred(Document *doc, void *userData, Scintilla::Status status) override; + void NotifyGroupCompleted(Document *, void *) noexcept override; void NotifyMacroRecord(Scintilla::Message iMessage, Scintilla::uptr_t wParam, Scintilla::sptr_t lParam); void ContainerNeedsUpdate(Scintilla::Update flags) noexcept; diff --git a/src/Selection.cxx b/src/Selection.cxx index 82614b040..a4531d65b 100644 --- a/src/Selection.cxx +++ b/src/Selection.cxx @@ -217,6 +217,10 @@ SelectionRange &Selection::Rectangular() noexcept { return rangeRectangular; } +SelectionRange Selection::RectangularCopy() const noexcept { + return rangeRectangular; +} + SelectionSegment Selection::Limits() const noexcept { PLATFORM_ASSERT(!ranges.empty()); SelectionSegment sr(ranges[0].anchor, ranges[0].caret); @@ -456,3 +460,6 @@ void Selection::RotateMain() noexcept { mainRange = (mainRange + 1) % ranges.size(); } +void Selection::SetRanges(const Ranges &rangesToSet) { + ranges = rangesToSet; +} diff --git a/src/Selection.h b/src/Selection.h index 28db77445..c0d785870 100644 --- a/src/Selection.h +++ b/src/Selection.h @@ -163,6 +163,7 @@ public: Sci::Position MainCaret() const noexcept; Sci::Position MainAnchor() const noexcept; SelectionRange &Rectangular() noexcept; + SelectionRange RectangularCopy() const noexcept; SelectionSegment Limits() const noexcept; // This is for when you want to move the caret in response to a // user direction command - for rectangular selections, use the range @@ -202,6 +203,7 @@ public: Ranges RangesCopy() const { return ranges; } + void SetRanges(const Ranges &rangesToSet); }; } diff --git a/src/UndoHistory.cxx b/src/UndoHistory.cxx index c0d7ba5f2..29a80c6d1 100644 --- a/src/UndoHistory.cxx +++ b/src/UndoHistory.cxx @@ -354,6 +354,14 @@ int UndoHistory::UndoSequenceDepth() const noexcept { return undoSequenceDepth; } +bool UndoHistory::AfterUndoSequenceStart() const noexcept { + if (currentAction == 0) { + return false; + } + // Count back to last sequence start? + return !actions.AtStart(currentAction-1); +} + void UndoHistory::DropUndoSequence() noexcept { undoSequenceDepth = 0; } diff --git a/src/UndoHistory.h b/src/UndoHistory.h index be639c541..299e28f58 100644 --- a/src/UndoHistory.h +++ b/src/UndoHistory.h @@ -102,6 +102,7 @@ public: void BeginUndoAction(bool mayCoalesce=false) noexcept; void EndUndoAction() noexcept; int UndoSequenceDepth() const noexcept; + bool AfterUndoSequenceStart() const noexcept; void DropUndoSequence() noexcept; void DeleteUndoHistory() noexcept; |