Engineering Atlas › Low-Level Design

Code a text editor with undo and redo

16 min read · last reviewed 2026-10-17

Open in the interactive app: practice, quiz, mock interview →

In one minute

Requirements

Functional:

Non-functional:

Out of scope: rich text and styles, multiple cursors, collaborative editing, persistence of the history across sessions, Unicode grapheme handling. Mention them, park them in Extensibility, and move on.

Entities and relationships

Relationships: the editor is composed of one buffer and two stacks; a step aggregates edits; an edit references no object other than the buffer it is applied to, which is what makes it replayable.

Class diagram

classDiagram
    class Editor {
      -buf: Buffer
      -cur: int
      -anchor: int
      -undoStack: Step[]
      -redoStack: Step[]
      +insert(s)
      +backspace()
      +move(pos)
      +select(a, b)
      +beginGroup()
      +endGroup()
      +undo() bool
      +redo() bool
    }
    class Buffer {
      <<interface>>
      +insert(pos, s)
      +delete(pos, n)
      +slice(a, b) string
      +text() string
    }
    class GapBuffer
    class PieceTable
    class Step {
      +edits: Edit[]
      +grouped: bool
    }
    class Edit {
      +pos: int
      +removed: string
      +inserted: string
      +selBefore: pair
      +cursorAfter: int
      +time: int
      +apply(buf)
      +revert(buf)
    }
    class Clock {
      <<interface>>
      +now() int
    }
    Buffer <|.. GapBuffer
    Buffer <|.. PieceTable
    Editor *-- Buffer
    Editor o-- Step
    Step o-- Edit
    Editor ..> Clock

Key flows

Typing three letters quickly, pausing, then undoing twice shows the coalescing rule and the stack movement.

sequenceDiagram
    participant U as User
    participant E as Editor
    participant B as Buffer
    participant S as UndoStack
    U->>E: insert "a" (t=0)
    E->>B: insert(0, "a")
    E->>S: push Edit("a")
    U->>E: insert "b" (t=400)
    E->>B: insert(1, "b")
    E->>S: merge into top: "ab"
    U->>E: insert "c" (t=5000)
    E->>B: insert(2, "c")
    E->>S: push new Edit("c")
    U->>E: undo
    E->>S: pop Edit("c")
    E->>B: delete(2, 1)
    E->>E: push to redo stack

Undo pops a step, reverts its edits in reverse order, restores the selection that existed before the step, and pushes the step on the redo stack. Redo does the mirror image. Any fresh edit empties the redo stack, because the future it described no longer follows from the present.

Design patterns used and why

Command versus Memento, decided. Command stores what changed and needs both directions of every edit. It is small and fast, but every new kind of mutation must supply a correct inverse. Memento stores what the state was and needs nothing from the mutation, but costs memory per step and is slow for large state. A good rule is to use commands when edits are small and well understood, and snapshots when the state is small, immutable or the inverse is hard to write. Some editors do both: commands for typing, a snapshot for rare "transform the whole file" actions.

Concurrency and edge cases

In the wild

Down the stack

Layer 1The contract

The editor promises five things you can test. After insert(s) the text contains s at the cursor and the cursor sits after it. undo() returns true and restores the exact previous text, cursor or selection, or returns false with no change when there is nothing to undo. redo() after undo() restores the text that was undone. Any new edit clears the redo stack. A step created inside a group reverts as a unit. Everything else, including which edits merge, is policy that the contract states explicitly so users are not surprised.

Layer 2The mechanism

A gap buffer is an array with a hole in it. Text before the cursor lives at the start, text after lives at the end, and the gap sits at the cursor. Inserting writes into the gap and advances its start pointer. Deleting backwards moves the start pointer back. Moving the cursor slides the gap by copying characters across it, which costs the distance moved. When the gap fills, the array is reallocated, usually by doubling. Emacs is the best-known user of this structure (Wikipedia: gap buffer).

A piece table never moves text. It keeps the original file buffer and an append-only "added" buffer, plus a list of pieces, each pointing at a range in one of the two buffers. An insert in the middle splits one piece into two and adds a piece pointing at the new text. Deleting trims or removes pieces. Because nothing is overwritten, old piece lists are a natural way to represent history (Wikipedia: piece table). VS Code stores pieces in a balanced tree to make offset and line lookups logarithmic.

On top of the buffer, the history is two stacks of steps. Each Edit records pos, removed, inserted, the selection before and the cursor after, so apply deletes len(removed) characters at pos and inserts inserted, and revert does the opposite with the roles swapped. Vim calls its equivalent of a group an undo block, and offers :undojoin to merge a change into the previous block (Vim documentation). Vim also keeps undo branches, so undoing and then editing does not throw the abandoned future away; this lesson simply discards it.

Layer 3The machine

A gap buffer moves bytes with memmove, a sequential copy that runs near memory bandwidth, on the order of 10 GB/s on a modern core. Sliding the gap 1 MB costs around 100 microseconds, while a jump across a 100 MB file costs around 10 milliseconds, roughly a frame; typing at the gap is a pointer bump with no copy at all. Appending to a piece table's added buffer is also sequential, and its pieces are small structs (buffer index, start, length: 12 to 24 bytes each), so ten thousand edits in a session cost a few hundred kilobytes of metadata. The expensive part is locality: reading a line across many pieces touches scattered memory and misses the CPU cache, which is why VS Code caches line-break offsets per piece.

History memory is the sum of removed and inserted strings plus a fixed header per edit, about 64 to 100 bytes in a managed language. Coalescing matters here too: 100 characters typed in one burst is one edit and one string, not 100 objects with 100 headers.

Why it's built this way

  1. People type and delete near one spot, so the gap buffer makes that spot cheap and pays only when the cursor jumps.
  2. Whole-file snapshots per step cost memory proportional to the document, so history stores the changed span only.
  3. A span can only be applied to the text it was recorded against, so the history must be strictly last-in first-out and the redo stack must be cleared on a new edit.
  4. Humans think in words and operations, not keystrokes, so adjacent edits are merged by time and contiguity and callers can open a group.
  5. Large files make a contiguous array costly to open and to hold, so piece tables point at the original bytes instead of copying them.

Extensibility

Leadership lens

Check yourself

  1. Why must the redo stack be cleared when the user makes a new edit after an undo?

    Show answer

    Each edit stores absolute positions that are valid only for the text it was recorded against. After a new edit the text differs, so replaying the old redo steps would modify the wrong characters.

  2. When would you pick snapshots (Memento) over commands for undo?

    Show answer

    When state is small or immutable so copying is cheap, or when writing a correct inverse for a mutation is hard. For large documents with small edits, commands that store only the changed span use far less memory.

  3. A user types "hello", clicks elsewhere in the text, and types "x". How many undo steps exist, and why?

    Show answer

    Two. Moving the cursor seals the first group, and the next insertion is not contiguous with the previous one, so it starts a new step.

  4. A gap buffer holds a 100 MB file and the user jumps from the start to the end. What does it cost, and how would you reduce it?

    Show answer

    The gap must slide across the whole file, a copy of about 100 MB, around ten milliseconds or more. A piece table avoids the copy because it changes only its piece list; so would a rope or a tree of chunks.

Watch

Sources and further study

Primary

Systems docs

Books

Other curricula

Free resources

Open in the interactive app: practice, quiz, mock interview →