// Scintilla source code edit control /** @file UndoHistory.cxx ** Manages undo for the document. **/ // Copyright 1998-2024 by Neil Hodgson // The License.txt file describes the conditions under which this software may be distributed. #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "ScintillaTypes.h" #include "Debugging.h" #include "Position.h" #include "SplitVector.h" #include "Partitioning.h" #include "RunStyles.h" #include "SparseVector.h" #include "ChangeHistory.h" #include "CellBuffer.h" #include "UndoHistory.h" namespace Scintilla::Internal { UndoActionType::UndoActionType() noexcept : at(ActionType::insert), mayCoalesce(false) { } void UndoActions::resize(size_t length) { types.resize(length); positions.resize(length); lengths.resize(length); } size_t UndoActions::size() const noexcept { return types.size(); } void UndoActions::Create(size_t index, ActionType at_, Sci::Position position_, Sci::Position lenData_, bool mayCoalesce_) noexcept { types[index].at = at_; types[index].mayCoalesce = mayCoalesce_; positions[index] = position_; lengths[index] = lenData_; } const char *ScrapStack::Push(const char *text, size_t length) { if (current < stack.length()) { stack.resize(current); } stack.append(text, length); current = stack.length(); return stack.data() + current - length; } void ScrapStack::SetCurrent(size_t position) noexcept { current = position; } void ScrapStack::MoveForward(size_t length) noexcept { if ((current + length) <= stack.length()) { current += length; } } void ScrapStack::MoveBack(size_t length) noexcept { if (current >= length) { current -= length; } } const char *ScrapStack::CurrentText() const noexcept { return stack.data() + current; } const char *ScrapStack::TextAt(size_t position) const noexcept { return stack.data() + position; } // The undo history stores a sequence of user operations that represent the user's view of the // commands executed on the text. // Each user operation contains a sequence of text insertion and text deletion actions. // All the user operations are stored in a list of individual actions with 'start' actions used // as delimiters between user operations. // Initially there is one start action in the history. // As each action is performed, it is recorded in the history. The action may either become // part of the current user operation or may start a new user operation. If it is to be part of the // current operation, then it overwrites the current last action. If it is to be part of a new // operation, it is appended after the current last action. // After writing the new action, a new start action is appended at the end of the history. // The decision of whether to start a new user operation is based upon two factors. If a // compound operation has been explicitly started by calling BeginUndoAction and no matching // EndUndoAction (these calls nest) has been called, then the action is coalesced into the current // operation. If there is no outstanding BeginUndoAction call then a new operation is started // unless it looks as if the new action is caused by the user typing or deleting a stream of text. // Sequences that look like typing or deletion are coalesced into a single user operation. UndoHistory::UndoHistory() { actions.resize(3); scraps = std::make_unique(); actions.Create(currentAction, ActionType::start); } UndoHistory::~UndoHistory() noexcept = default; void UndoHistory::EnsureUndoRoom() { // Have to test that there is room for 2 more actions in the array // as two actions may be created by the calling function if (static_cast(currentAction) >= (actions.size() - 2)) { // Run out of undo nodes so extend the array actions.resize(actions.size() * 2); } } const char *UndoHistory::AppendAction(ActionType at, Sci::Position position, const char *data, Sci::Position lengthData, bool &startSequence, bool mayCoalesce) { EnsureUndoRoom(); //Platform::DebugPrintf("%% %d action %d %d %d\n", at, position, lengthData, currentAction); //Platform::DebugPrintf("^ %d action %d %d\n", actions[currentAction - 1].at, // actions[currentAction - 1].position, actions[currentAction - 1].lenData); if (currentAction < savePoint) { savePoint = -1; if (!detach) { detach = currentAction; } } else if (detach && (*detach > currentAction)) { detach = currentAction; } const int oldCurrentAction = currentAction; if (currentAction >= 1) { if (0 == undoSequenceDepth) { // Top level actions may not always be coalesced ptrdiff_t targetAct = currentAction - 1; // Container actions may forward the coalesce state of Scintilla Actions. while ((targetAct > 0) && (actions.types[targetAct].at == ActionType::container) && actions.types[targetAct].mayCoalesce) { targetAct--; } // See if current action can be coalesced into previous action // Will work if both are inserts or deletes and position is same if ((currentAction == savePoint) || (currentAction == tentativePoint)) { currentAction++; } else if (!actions.types[currentAction].mayCoalesce) { // Not allowed to coalesce if this set currentAction++; } else if (!mayCoalesce || !actions.types[targetAct].mayCoalesce) { currentAction++; } else if (at == ActionType::container || actions.types[currentAction].at == ActionType::container) { ; // A coalescible containerAction } else if ((at != actions.types[targetAct].at) && (actions.types[targetAct].at != ActionType::start)) { currentAction++; } else if ((at == ActionType::insert) && (position != (actions.positions[targetAct] + actions.lengths[targetAct]))) { // Insertions must be immediately after to coalesce currentAction++; } else if (at == ActionType::remove) { if ((lengthData == 1) || (lengthData == 2)) { if ((position + lengthData) == actions.positions[targetAct]) { ; // Backspace -> OK } else if (position == actions.positions[targetAct]) { ; // Delete -> OK } else { // Removals must be at same position to coalesce currentAction++; } } else { // Removals must be of one character to coalesce currentAction++; } } else { // Action coalesced. } } else { // Actions not at top level are always coalesced unless this is after return to top level if (!actions.types[currentAction].mayCoalesce) currentAction++; } } else { currentAction++; } startSequence = oldCurrentAction != currentAction; const char *dataNew = lengthData ? scraps->Push(data, lengthData) : nullptr; actions.Create(currentAction, at, position, lengthData, mayCoalesce); currentAction++; actions.Create(currentAction, ActionType::start); maxAction = currentAction; return dataNew; } void UndoHistory::BeginUndoAction() { EnsureUndoRoom(); if (undoSequenceDepth == 0) { if (actions.types[currentAction].at != ActionType::start) { currentAction++; actions.Create(currentAction, ActionType::start); maxAction = currentAction; } actions.types[currentAction].mayCoalesce = false; } undoSequenceDepth++; } void UndoHistory::EndUndoAction() { PLATFORM_ASSERT(undoSequenceDepth > 0); EnsureUndoRoom(); undoSequenceDepth--; if (0 == undoSequenceDepth) { if (actions.types[currentAction].at != ActionType::start) { currentAction++; actions.Create(currentAction, ActionType::start); maxAction = currentAction; } actions.types[currentAction].mayCoalesce = false; } } void UndoHistory::DropUndoSequence() noexcept { undoSequenceDepth = 0; } void UndoHistory::DeleteUndoHistory() noexcept { for (int i = 1; i < maxAction; i++) { actions.lengths[i] = 0; } maxAction = 0; currentAction = 0; actions.Create(currentAction, ActionType::start); savePoint = 0; tentativePoint = -1; } void UndoHistory::SetSavePoint() noexcept { savePoint = currentAction; detach.reset(); } bool UndoHistory::IsSavePoint() const noexcept { return savePoint == currentAction; } bool UndoHistory::BeforeSavePoint() const noexcept { return (savePoint < 0) || (savePoint > currentAction); } bool UndoHistory::BeforeReachableSavePoint() const noexcept { return (savePoint >= 0) && !detach && (savePoint > currentAction); } bool UndoHistory::AfterSavePoint() const noexcept { return (savePoint >= 0) && (savePoint <= currentAction); } bool UndoHistory::AfterDetachPoint() const noexcept { return detach && (*detach < currentAction); } void UndoHistory::TentativeStart() noexcept { tentativePoint = currentAction; } void UndoHistory::TentativeCommit() noexcept { tentativePoint = -1; // Truncate undo history maxAction = currentAction; } bool UndoHistory::TentativeActive() const noexcept { return tentativePoint >= 0; } int UndoHistory::TentativeSteps() noexcept { // Drop any trailing startAction if (actions.types[currentAction].at == ActionType::start && currentAction > 0) currentAction--; if (tentativePoint >= 0) return currentAction - tentativePoint; return -1; } bool UndoHistory::CanUndo() const noexcept { return (currentAction > 0) && (maxAction > 0); } int UndoHistory::StartUndo() noexcept { // Drop any trailing startAction if (actions.types[currentAction].at == ActionType::start && currentAction > 0) currentAction--; // Count the steps in this action int act = currentAction; while (actions.types[act].at != ActionType::start && act > 0) { act--; } return currentAction - act; } Action UndoHistory::GetUndoStep() const noexcept { Action acta { actions.types[currentAction].at, actions.types[currentAction].mayCoalesce, actions.positions[currentAction], nullptr, actions.lengths[currentAction] }; if (acta.lenData) { acta.data = scraps->CurrentText() - acta.lenData; } return acta; } void UndoHistory::CompletedUndoStep() noexcept { scraps->MoveBack(actions.lengths[currentAction]); currentAction--; } bool UndoHistory::CanRedo() const noexcept { return maxAction > currentAction; } int UndoHistory::StartRedo() noexcept { // Drop any leading startAction if (currentAction < maxAction && actions.types[currentAction].at == ActionType::start) currentAction++; // Count the steps in this action int act = currentAction; while (act < maxAction && actions.types[act].at != ActionType::start) { act++; } return act - currentAction; } Action UndoHistory::GetRedoStep() const noexcept { Action acta{ actions.types[currentAction].at, actions.types[currentAction].mayCoalesce, actions.positions[currentAction], nullptr, actions.lengths[currentAction] }; if (acta.lenData) { acta.data = scraps->CurrentText(); } return acta; } void UndoHistory::CompletedRedoStep() noexcept { scraps->MoveForward(actions.lengths[currentAction]); currentAction++; } }