Files
kte/docs/undo.md

4.7 KiB

This is a design for a non-linear undo/redo system for kte. The design must be identical in behavior and correctness to the proven kte editor undo system.

Core Requirements

  1. Each open buffer has its own completely independent undo tree.
  2. Undo and redo must be non-linear: typing after undo creates a branch; old redo branches are discarded.
  3. Typing, backspacing, and pasting are batched into word-level undo steps.
  4. Undo/redo must never create new undo nodes while applying an undo/redo (silent, low-level apply).
  5. The system must be memory-safe and leak-proof even if the user types and immediately closes the buffer.

Data Structures

enum class UndoType : uint8_t {
    Insert,
    Delete,
    Paste,        // optional, can reuse Insert
    Newline,
    DeleteRow,
    // future: IndentRegion, KillRegion, etc.
};

struct UndoNode {
    UndoType type;
    int row;           // original cursor row
    int col;           // original cursor column (updated during batch)
    std::string text;  // the inserted or deleted text (full batch)
    UndoNode* child = nullptr;   // next in current timeline
    UndoNode* next  = nullptr;   // redo branch (rarely used)
    // no parent pointer needed — we walk from root
};

struct UndoTree {
    UndoNode* root    = nullptr;  // first edit ever
    UndoNode* current = nullptr;  // current state of buffer
    UndoNode* saved   = nullptr;  // points to node matching last save (for dirty flag)
    UndoNode* pending = nullptr;  // in-progress batch (detached)
};

Each Buffer owns one std::unique_ptr<UndoTree>.

Core API (must implement exactly)

class UndoSystem {
public:
    void Begin(UndoType type);
    void Append(char ch);
    void Append(std::string_view text);
    void commit();                     // called on cursor move, commands, etc.

    void undo();                       // Ctrl+Z
    void redo();                       // Ctrl+Y or Ctrl+Shift+Z

    void mark_saved();                 // after successful save
    void discard_pending();            // before closing buffer or loading new file
    void clear();                      // new file / reset

private:
    void apply(const UndoNode* node, int direction);  // +1 = redo, -1 = undo
    void free_node(UndoNode* node);
    void free_branch(UndoNode* node);  // frees redo siblings only
};

Critical Invariants and Rules

  1. begin() must reuse pending if:

    • same type
    • same row
    • pending->col + pending->text.size() == current_cursor_col → otherwise commit() old and create new
  2. pending is detached — never linked until commit()

  3. commit():

    • discards redo branches (current->child)
    • attaches pending as current->child
    • advances current
    • clears pending
    • if diverged from saved, null it
  4. apply() must use low-level buffer operations:

    • Never call public insert/delete/newline
    • Use raw buffer.insert_text(row, col, text) and buffer.delete_text(row, col, len)
    • These must not trigger undo
  5. undo():

    • move current to parent
    • apply(current, -1)
  6. redo():

    • move current to child
    • apply(current, +1)
  7. discard_pending() must be called in:

    • buffer close
    • file reload
    • new file
    • any destructive operation

Example Flow: Typing "hello"

begin(Insert) → pending = new node, col=0
append('h') → pending->text = "h", pending->col = 1
append('e') → "he", col = 2
...
commit() on arrow key → pending becomes current->child, current advances

One undo step removes all of "hello".

Required Helper in Buffer Class

class Buffer {
    void insert_text(int row, int col, std::string_view text);   // raw, no undo
    void delete_text(int row, int col, size_t len);              // raw, no undo
    void split_line(int row, int col);                           // raw newline
    void join_lines(int row);                                    // raw join
    void insert_row(int row, std::string_view text);             // raw
    void delete_row(int row);                                    // raw
};

Tasks for Agent

  1. Implement UndoNode, UndoTree, and UndoSystem class exactly as specified.
  2. Add std::unique_ptr<UndoTree> undo; to Buffer.
  3. Modify insert_char, delete_char, paste, newline to use undo.begin()/append()/commit().
  4. Add undo.commit() at start of all cursor movement and command functions.
  5. Implement apply() using only Buffer's raw methods.
  6. Add undo.discard_pending() in all buffer reset/close paths.
  7. Add Ctrl+Zbuffer.undo(), Ctrl+Ybuffer.redo().

This design is used in production editors and is considered the gold standard for small, correct, non-linear undo in C/C++. Implement it faithfully.