1 // Scintilla source code edit control
2 /** @file UndoHistory.cxx
3 ** Manages undo for the document.
5 // Copyright 1998-2024 by Neil Hodgson <neilh@scintilla.org>
6 // The License.txt file describes the conditions under which this software may be distributed.
19 #include <string_view>
25 #include "ScintillaTypes.h"
27 #include "Debugging.h"
30 #include "SplitVector.h"
31 #include "Partitioning.h"
32 #include "RunStyles.h"
33 #include "SparseVector.h"
34 #include "ChangeHistory.h"
35 #include "CellBuffer.h"
36 #include "UndoHistory.h"
38 namespace Scintilla::Internal
{
41 void VectorTruncate(std::vector
<T
> &v
, size_t length
) noexcept
{
42 v
.erase(v
.begin() + length
, v
.end());
45 constexpr size_t byteMask
= UINT8_MAX
;
46 constexpr size_t byteBits
= 8;
48 size_t ReadValue(const uint8_t *bytes
, size_t length
) noexcept
{
50 for (size_t i
= 0; i
< length
; i
++) {
51 value
= (value
<< byteBits
) + bytes
[i
];
56 void WriteValue(uint8_t *bytes
, size_t length
, size_t value
) noexcept
{
59 bytes
[length
] = value
& byteMask
;
60 value
= value
>> byteBits
;
64 size_t ScaledVector::Size() const noexcept
{
65 return bytes
.size() / element
.size
;
68 size_t ScaledVector::ValueAt(size_t index
) const noexcept
{
69 return ReadValue(bytes
.data() + index
* element
.size
, element
.size
);
72 intptr_t ScaledVector::SignedValueAt(size_t index
) const noexcept
{
73 return ReadValue(bytes
.data() + index
* element
.size
, element
.size
);
76 constexpr SizeMax
ElementForValue(size_t value
) noexcept
{
77 size_t maxN
= byteMask
;
79 while (value
> byteMask
) {
82 maxN
= (maxN
<< byteBits
) + byteMask
;
87 void ScaledVector::SetValueAt(size_t index
, size_t value
) {
88 // Check if value fits, if not then expand
89 if (value
> element
.maxValue
) {
90 const SizeMax elementForValue
= ElementForValue(value
);
91 const size_t length
= bytes
.size() / element
.size
;
92 std::vector
<uint8_t> bytesNew(elementForValue
.size
* length
);
93 for (size_t i
= 0; i
< length
; i
++) {
94 const uint8_t *source
= bytes
.data() + i
* element
.size
;
95 uint8_t *destination
= bytesNew
.data() + (i
+1) * elementForValue
.size
- element
.size
;
96 memcpy(destination
, source
, element
.size
);
98 std::swap(bytes
, bytesNew
);
99 element
= elementForValue
;
101 WriteValue(bytes
.data() + index
* element
.size
, element
.size
, value
);
104 void ScaledVector::ClearValueAt(size_t index
) noexcept
{
105 // 0 fits in any size element so no expansion needed so no exceptions
106 WriteValue(bytes
.data() + index
* element
.size
, element
.size
, 0);
109 void ScaledVector::Clear() noexcept
{
113 void ScaledVector::Truncate(size_t length
) noexcept
{
114 VectorTruncate(bytes
, length
* element
.size
);
115 assert(bytes
.size() == length
* element
.size
);
118 void ScaledVector::ReSize(size_t length
) {
119 bytes
.resize(length
* element
.size
);
122 void ScaledVector::PushBack() {
123 bytes
.resize(bytes
.size() + element
.size
);
126 size_t ScaledVector::SizeInBytes() const noexcept
{
130 UndoActionType::UndoActionType() noexcept
: at(ActionType::insert
), mayCoalesce(false) {
133 UndoActions::UndoActions() noexcept
= default;
135 void UndoActions::Truncate(size_t length
) noexcept
{
136 VectorTruncate(types
, length
);
137 assert(types
.size() == length
);
138 positions
.Truncate(length
);
139 lengths
.Truncate(length
);
142 void UndoActions::PushBack() {
143 types
.emplace_back();
144 positions
.PushBack();
148 void UndoActions::Clear() noexcept
{
154 intptr_t UndoActions::SSize() const noexcept
{
158 void UndoActions::Create(size_t index
, ActionType at_
, Sci::Position position_
, Sci::Position lenData_
, bool mayCoalesce_
) {
159 types
[index
].at
= at_
;
160 types
[index
].mayCoalesce
= mayCoalesce_
;
161 positions
.SetValueAt(index
, position_
);
162 lengths
.SetValueAt(index
, lenData_
);
165 bool UndoActions::AtStart(size_t index
) const noexcept
{
169 return !types
[index
-1].mayCoalesce
;
172 size_t UndoActions::LengthTo(size_t index
) const noexcept
{
174 for (size_t act
= 0; act
< index
; act
++) {
175 sum
+= lengths
.ValueAt(act
);
180 Sci::Position
UndoActions::Position(int action
) const noexcept
{
181 return positions
.SignedValueAt(action
);
183 Sci::Position
UndoActions::Length(int action
) const noexcept
{
184 return lengths
.SignedValueAt(action
);
187 void ScrapStack::Clear() noexcept
{
192 const char *ScrapStack::Push(const char *text
, size_t length
) {
193 if (current
< stack
.length()) {
194 stack
.resize(current
);
196 stack
.append(text
, length
);
197 current
= stack
.length();
198 return stack
.data() + current
- length
;
201 void ScrapStack::SetCurrent(size_t position
) noexcept
{
205 void ScrapStack::MoveForward(size_t length
) noexcept
{
206 if ((current
+ length
) <= stack
.length()) {
211 void ScrapStack::MoveBack(size_t length
) noexcept
{
212 if (current
>= length
) {
217 const char *ScrapStack::CurrentText() const noexcept
{
218 return stack
.data() + current
;
221 const char *ScrapStack::TextAt(size_t position
) const noexcept
{
222 return stack
.data() + position
;
225 // The undo history stores a sequence of user operations that represent the user's view of the
226 // commands executed on the text.
227 // Each user operation contains a sequence of text insertion and text deletion actions.
228 // All the user operations are stored in a list of individual actions.
229 // A false 'mayCoalesce' flag acts as an end to a user operation.
230 // Initially there are no actions in the history.
231 // As each action is performed, it is recorded in the history. The action may either become
232 // part of the current user operation or may start a new user operation. If it is to be part of the
233 // current operation, then 'mayCoalesce' is true. If it is to be part of a new operation, the
234 // 'mayCoalesce' flag of the previous action is set false.
235 // The decision of whether to start a new user operation is based upon two factors. If a
236 // compound operation has been explicitly started by calling BeginUndoAction and no matching
237 // EndUndoAction (these calls nest) has been called, then the action is coalesced into the current
238 // operation. If there is no outstanding BeginUndoAction call then a new operation is started
239 // unless it looks as if the new action is caused by the user typing or deleting a stream of text.
240 // Sequences that look like typing or deletion are coalesced into a single user operation.
242 int UndoHistory::PreviousAction() const noexcept
{
243 return currentAction
- 1;
246 UndoHistory::UndoHistory() {
247 scraps
= std::make_unique
<ScrapStack
>();
250 UndoHistory::~UndoHistory() noexcept
= default;
252 const char *UndoHistory::AppendAction(ActionType at
, Sci::Position position
, const char *data
, Sci::Position lengthData
,
253 bool &startSequence
, bool mayCoalesce
) {
254 //Platform::DebugPrintf("%% %d action %d %d %d\n", at, position, lengthData, currentAction);
255 //Platform::DebugPrintf("^ %d action %d %d\n", actions[currentAction - 1].at,
256 // actions[currentAction - 1].position, actions[currentAction - 1].lenData);
257 if (currentAction
< savePoint
) {
260 detach
= currentAction
;
262 } else if (detach
&& (*detach
> currentAction
)) {
263 detach
= currentAction
;
265 if (undoSequenceDepth
> 0) {
266 // Actions not at top level are always coalesced unless this is after return to top level
269 bool coalesce
= true;
270 if (currentAction
>= 1) {
271 int targetAct
= currentAction
- 1;
272 if (0 == undoSequenceDepth
) {
273 // Top level actions may not always be coalesced
274 // Container actions may forward the coalesce state of Scintilla Actions.
275 while ((targetAct
> 0) && (actions
.types
[targetAct
].at
== ActionType::container
) && actions
.types
[targetAct
].mayCoalesce
) {
278 // See if current action can be coalesced into previous action
279 // Will work if both are inserts or deletes and position is same
280 if ((currentAction
== savePoint
) || (currentAction
== tentativePoint
)) {
282 } else if (!mayCoalesce
|| !actions
.types
[targetAct
].mayCoalesce
) {
284 } else if (at
== ActionType::container
|| actions
.types
[targetAct
].at
== ActionType::container
) {
285 ; // A coalescible containerAction
286 } else if ((at
!= actions
.types
[targetAct
].at
)) { // } && (!actions.AtStart(targetAct))) {
288 } else if ((at
== ActionType::insert
) &&
289 (position
!= (actions
.Position(targetAct
) + actions
.Length(targetAct
)))) {
290 // Insertions must be immediately after to coalesce
292 } else if (at
== ActionType::remove
) {
293 if ((lengthData
== 1) || (lengthData
== 2)) {
294 if ((position
+ lengthData
) == actions
.Position(targetAct
)) {
296 } else if (position
== actions
.Position(targetAct
)) {
299 // Removals must be at same position to coalesce
303 // Removals must be of one character to coalesce
311 // Actions not at top level are always coalesced unless this is after return to top level
312 if (!actions
.types
[targetAct
].mayCoalesce
)
318 startSequence
= !coalesce
;
319 // Maybe close previous action
320 if ((currentAction
> 0) && startSequence
) {
321 actions
.types
[PreviousAction()].mayCoalesce
= false;
323 const char *dataNew
= lengthData
? scraps
->Push(data
, lengthData
) : nullptr;
324 if (currentAction
>= actions
.SSize()) {
327 actions
.Truncate(currentAction
+1);
329 actions
.Create(currentAction
, at
, position
, lengthData
, mayCoalesce
);
334 void UndoHistory::BeginUndoAction(bool mayCoalesce
) noexcept
{
335 if (undoSequenceDepth
== 0) {
336 if (currentAction
> 0) {
337 actions
.types
[PreviousAction()].mayCoalesce
= mayCoalesce
;
343 void UndoHistory::EndUndoAction() noexcept
{
344 PLATFORM_ASSERT(undoSequenceDepth
> 0);
346 if (0 == undoSequenceDepth
) {
347 if (currentAction
> 0) {
348 actions
.types
[PreviousAction()].mayCoalesce
= false;
353 int UndoHistory::UndoSequenceDepth() const noexcept
{
354 return undoSequenceDepth
;
357 void UndoHistory::DropUndoSequence() noexcept
{
358 undoSequenceDepth
= 0;
361 void UndoHistory::DeleteUndoHistory() noexcept
{
370 int UndoHistory::Actions() const noexcept
{
371 return static_cast<int>(actions
.SSize());
374 void UndoHistory::SetSavePoint(int action
) noexcept
{
378 int UndoHistory::SavePoint() const noexcept
{
382 void UndoHistory::SetSavePoint() noexcept
{
383 savePoint
= currentAction
;
387 bool UndoHistory::IsSavePoint() const noexcept
{
388 return savePoint
== currentAction
;
391 bool UndoHistory::BeforeSavePoint() const noexcept
{
392 return (savePoint
< 0) || (savePoint
> currentAction
);
395 bool UndoHistory::PreviousBeforeSavePoint() const noexcept
{
396 return (savePoint
< 0) || (savePoint
>= currentAction
);
399 bool UndoHistory::BeforeReachableSavePoint() const noexcept
{
400 return (savePoint
> 0) && (savePoint
> currentAction
);
403 bool UndoHistory::AfterSavePoint() const noexcept
{
404 return (savePoint
>= 0) && (savePoint
<= currentAction
);
407 void UndoHistory::SetDetachPoint(int action
) noexcept
{
415 int UndoHistory::DetachPoint() const noexcept
{
416 return detach
.value_or(-1);
419 bool UndoHistory::AfterDetachPoint() const noexcept
{
420 return detach
&& (*detach
< currentAction
);
423 bool UndoHistory::AfterOrAtDetachPoint() const noexcept
{
424 return detach
&& (*detach
<= currentAction
);
427 intptr_t UndoHistory::Delta(int action
) const noexcept
{
428 intptr_t sizeChange
= 0;
429 for (int act
= 0; act
< action
; act
++) {
430 const intptr_t lengthChange
= actions
.Length(act
);
431 sizeChange
+= (actions
.types
[act
].at
== ActionType::insert
) ? lengthChange
: -lengthChange
;
436 bool UndoHistory::Validate(intptr_t lengthDocument
) const noexcept
{
437 // Check history for validity
438 const intptr_t sizeChange
= Delta(currentAction
);
439 if (sizeChange
> lengthDocument
) {
440 // Current document size too small for changes made in undo history.
443 const intptr_t lengthOriginal
= lengthDocument
- sizeChange
;
444 intptr_t lengthCurrent
= lengthOriginal
;
445 for (int act
= 0; act
< actions
.SSize(); act
++) {
446 const intptr_t lengthChange
= actions
.Length(act
);
447 if (actions
.Position(act
) > lengthCurrent
) {
448 // Change outside document.
451 lengthCurrent
+= (actions
.types
[act
].at
== ActionType::insert
) ? lengthChange
: -lengthChange
;
452 if (lengthCurrent
< 0) {
459 void UndoHistory::SetCurrent(int action
, intptr_t lengthDocument
) {
460 // Find position in scraps for action
462 const size_t lengthSum
= actions
.LengthTo(action
);
463 scraps
->SetCurrent(lengthSum
);
464 currentAction
= action
;
465 if (!Validate(lengthDocument
)) {
468 throw std::runtime_error("UndoHistory::SetCurrent: invalid undo history.");
472 int UndoHistory::Current() const noexcept
{
473 return currentAction
;
476 int UndoHistory::Type(int action
) const noexcept
{
477 const int baseType
= static_cast<int>(actions
.types
[action
].at
);
478 const int open
= actions
.types
[action
].mayCoalesce
? coalesceFlag
: 0;
479 return baseType
| open
;
482 Sci::Position
UndoHistory::Position(int action
) const noexcept
{
483 return actions
.Position(action
);
486 Sci::Position
UndoHistory::Length(int action
) const noexcept
{
487 return actions
.Length(action
);
490 std::string_view
UndoHistory::Text(int action
) noexcept
{
491 // Assumes first call after any changes is for action 0.
492 // TODO: may need to invalidate memory in other circumstances
498 if (memory
&& memory
->act
<= action
) {
500 position
= memory
->position
;
502 for (; act
< action
; act
++) {
503 position
+= actions
.Length(act
);
505 const size_t length
= actions
.Length(action
);
506 const char *scrap
= scraps
->TextAt(position
);
507 memory
= {action
, position
};
508 return {scrap
, length
};
511 void UndoHistory::PushUndoActionType(int type
, Sci::Position position
) {
513 actions
.Create(actions
.SSize()-1, static_cast<ActionType
>(type
& byteMask
),
514 position
, 0, type
& coalesceFlag
);
517 void UndoHistory::ChangeLastUndoActionText(size_t length
, const char *text
) {
518 assert(actions
.lengths
.ValueAt(actions
.SSize()-1) == 0);
519 actions
.lengths
.SetValueAt(actions
.SSize()-1, length
);
520 scraps
->Push(text
, length
);
523 void UndoHistory::SetTentative(int action
) noexcept
{
524 tentativePoint
= action
;
527 int UndoHistory::TentativePoint() const noexcept
{
528 return tentativePoint
;
531 void UndoHistory::TentativeStart() noexcept
{
532 tentativePoint
= currentAction
;
535 void UndoHistory::TentativeCommit() noexcept
{
537 // Truncate undo history
538 actions
.Truncate(currentAction
);
541 bool UndoHistory::TentativeActive() const noexcept
{
542 return tentativePoint
>= 0;
545 int UndoHistory::TentativeSteps() const noexcept
{
546 // Drop any trailing startAction
547 if (tentativePoint
>= 0)
548 return currentAction
- tentativePoint
;
552 bool UndoHistory::CanUndo() const noexcept
{
553 return (currentAction
> 0) && (actions
.SSize() != 0);
556 int UndoHistory::StartUndo() const noexcept
{
557 assert(currentAction
>= 0);
559 // Count the steps in this action
560 if (currentAction
== 0) {
564 int act
= currentAction
- 1;
566 while (act
> 0 && !actions
.AtStart(act
)) {
569 return currentAction
- act
;
572 Action
UndoHistory::GetUndoStep() const noexcept
{
573 const int previousAction
= PreviousAction();
575 actions
.types
[previousAction
].at
,
576 actions
.types
[previousAction
].mayCoalesce
,
577 actions
.Position(previousAction
),
579 actions
.Length(previousAction
)
582 acta
.data
= scraps
->CurrentText() - acta
.lenData
;
587 void UndoHistory::CompletedUndoStep() noexcept
{
588 scraps
->MoveBack(actions
.Length(PreviousAction()));
592 bool UndoHistory::CanRedo() const noexcept
{
593 return actions
.SSize() > currentAction
;
596 int UndoHistory::StartRedo() const noexcept
{
597 // Count the steps in this action
599 if (currentAction
>= actions
.SSize()) {
600 // Already at end so can't redo
604 // Slightly unusual logic handles case where last action still has mayCoalesce.
605 // Could set mayCoalesce of last action to false in StartUndo but this state is
606 // visible to applications so should not be changed.
607 const int maxAction
= Actions() - 1;
608 int act
= currentAction
;
609 while (act
<= maxAction
&& actions
.types
[act
].mayCoalesce
) {
612 act
= std::min(act
, maxAction
);
613 return act
- currentAction
+ 1;
616 Action
UndoHistory::GetRedoStep() const noexcept
{
618 actions
.types
[currentAction
].at
,
619 actions
.types
[currentAction
].mayCoalesce
,
620 actions
.Position(currentAction
),
622 actions
.Length(currentAction
)
625 acta
.data
= scraps
->CurrentText();
630 void UndoHistory::CompletedRedoStep() noexcept
{
631 scraps
->MoveForward(actions
.Length(currentAction
));