// lesson: undo-and-redo
Undo and Redo
An editor without undo is a threat, not a tool. Two classic designs:
- Snapshots (the memento pattern): before each change, save the whole buffer; undo = restore. Trivially correct โ undo can never disagree with what happened, because it is what happened โ and memory-hungry in proportion to buffer size, not edit size. For source-file-sized buffers, honestly fine, and the final challenge uses it for exactly that reason.
- Inverse operations (the command pattern): record each edit as a small object that knows how to reverse itself. An insert's inverse is "erase those characters"; an erase's inverse is "put this text back" โ note that the erase record must therefore carry the erased text; the operation alone isn't invertible without it. Memory scales with the edits, which is why every serious editor lives here. The cost is a new invariant to defend: the records must replay against exactly the document states they were recorded against, in exactly reverse order. One position off and undo corrupts the file it was supposed to protect.
This lesson builds the second kind. Two stacks: undo holds done
things; redo holds undone things. undo pops, reverses, pushes to
redo. redo pops, re-applies, pushes back. And the rule everyone knows
from using editors without knowing they know it: a fresh edit clears
the redo stack โ history is a line, not a tree, and editing after an
undo abandons the future you undid. (vim actually keeps the abandoned
branches โ :help undo-tree โ one of its deepest features, built on
exactly the representation you're about to write.)
Granularity: coalescing
The subtle design decision is not how to undo but how much. Type
hello โ five insert operations. Should undo peel back o, then l,
then l...? Every real editor says no: one undo removes the word. vi's
rule is that the whole insert-mode session โ from i to Esc โ is one
undo unit. The mechanism is coalescing: when a new insert arrives,
check whether it extends the unit on top of the undo stack, and if
so, merge instead of push. Extends means:
- the top unit is an insert (you can't merge typing into a deletion),
- nothing has broken the run since (leaving insert mode calls
break_run()โ that's the i-to-Esc boundary made explicit; cursor motions in a real editor would too), - the new text lands exactly at the end of the top unit's text
(
pos == top.pos + top.text.size()โ type in the middle of a word after moving the cursor and positions won't line up, correctly forcing a new unit), - and the new text isn't a newline โ we cut units at line breaks, so undoing a paragraph of typing goes line by line rather than vaporizing all of it. (vim keeps whole sessions; our dialect is line-grained. Both are defensible; ours makes the tests nicer and the behavior less destructive.)
Deletions could coalesce too (backspace-backspace-backspace as one unit โ note the positions decrease), but that's symmetric bookkeeping you can add later; here, each erase is its own unit.
The document in this challenge is a plain std::string with byte
positions โ undo logic is completely independent of the 2-D row/column
structure, and testing it in 1-D removes every distraction. In your
editor, the same class runs against the flattened buffer, or against
Pos-keyed edits; the algebra is identical.
โบ The Undo History
20 ptsImplement UndoHistory against std::string documents:
record_insert(pos, text)โ the editor just insertedtextatpos. Coalesce per the four conditions above; otherwise push a new unit. Clears redo.record_erase(pos, text)โ the editor just removedtextfrompos. Always a new unit. Clears redo.break_run()โ end the current coalescing run (Esc pressed). No other effect; harmless when the stack is empty.undo(doc)โ reverse the top unit againstdoc(erase what the unit inserted / re-insert what it erased), move it to the redo stack, returntrue. Empty stack: returnfalse, touch nothing.redo(doc)โ re-apply the most recently undone unit, move it back to the undo stack, returntrue;falseif no redo available.can_undo()/can_redo().
The tests drive a consistent document alongside the history โ exactly the discipline your editor must keep: record precisely what you did, when you did it.
Log in to submit a solution and earn points.