Stub out previous undo implementation; update docs.

- Remove outdated `undo-state.md`
- Add two code quality/optimization reports that were used to guide previous work:
  - `code-report.md` (optimization)
  - `code-report-quality.md` (stability and code health)
- Add `themes.md`.
- Update undo system docs and roadmap.
This commit is contained in:
2025-12-03 15:12:28 -08:00
parent 45b2b88623
commit cbbde43dc2
12 changed files with 1746 additions and 664 deletions

View File

@@ -1,279 +1,390 @@
Undo System Overhaul Roadmap (emacs-style undo-tree)
## Updated Undo System Plan for kte/kge
Context: macOS, C++17 project, ncurses terminal and SDL2/ImGui GUI frontends. Date: 2025-12-01.
After reviewing the existing codebase and your undo plan, I propose
the following refined approach that preserves your goals while making
it more suitable for implementation:
Purpose
### Refined Data Structures
- Define a clear, incremental plan to implement a robust, non-linear undo system inspired by emacs' undo-tree.
- Align implementation with docs/undo.md and fix gaps observed in docs/undo-state.md.
- Provide test cases and acceptance criteria so a junior engineer or agentic coding system can execute the plan safely.
The proposed data structures are sound but need some refinements:
References
```c++
enum class UndoType : uint8_t {
Insert,
Delete,
Paste,
Newline,
DeleteRow,
// Future: IndentRegion, KillRegion, etc.
};
- Specification: docs/undo.md (API, invariants, batching rules, raw buffer ops)
- Current snapshot and recent fix: docs/undo-state.md (GUI mapping notes; Begin/Append ordering fix)
- Code: UndoSystem.{h,cc}, UndoTree.{h,cc}, UndoNode.{h,cc}, Buffer.{h,cc}, Command.{h,cc}, GUI/Terminal InputHandlers,
KKeymap.
struct UndoNode {
UndoType type;
int row;
int col;
std::string text;
std::unique_ptr<UndoNode> child = nullptr; // next in timeline
std::unique_ptr<UndoNode> next = nullptr; // redo branch
UndoNode* parent = nullptr; // weak pointer for navigation
};
Instrumentation (KTE_UNDO_DEBUG)
struct UndoTree {
std::unique_ptr<UndoNode> root;
UndoNode* current = nullptr;
UndoNode* saved = nullptr;
std::unique_ptr<UndoNode> pending = nullptr;
};
```
- How to enable
- Build with the CMake option `-DKTE_UNDO_DEBUG=ON` to enable concise instrumentation logs from `UndoSystem`.
- The following targets receive the `KTE_UNDO_DEBUG` compile definition when ON:
- `kte` (terminal), `kge` (GUI), and `test_undo` (tests).
- Examples:
```sh
# Terminal build with tests and instrumentation ON
cmake -S . -B cmake-build-term -DBUILD_TESTS=ON -DBUILD_GUI=OFF -DKTE_UNDO_DEBUG=ON
cmake --build cmake-build-term --target test_undo -j
./cmake-build-term/test_undo 2> undo.log
Key changes:
# GUI build (requires SDL2/OpenGL/Freetype toolchain) with instrumentation ON
cmake -S . -B cmake-build-gui -DBUILD_GUI=ON -DKTE_UNDO_DEBUG=ON
cmake --build cmake-build-gui --target kge -j
# Run kge and perform actions; logs go to stderr
```
- Use `std::unique_ptr` for owned pointers to ensure proper RAII
- Add weak `parent` pointer for easier navigation
- This ensures memory safety without manual management
- What it logs
- Each Begin/Append/commit/undo/redo operation prints a single `[UNDO]` line with:
- current cursor `(row,col)`, pointer to `pending`, its type/row/col/text-size, and pointers to `current`/`saved`.
- Example fields: `[UNDO] Begin cur=(0,0) pending=0x... t=Insert r=0 c=0 nlen=2 current=0x... saved=0x...`
---
- Example trace snippets
- Typing a contiguous word ("Hello") batches into a single Insert node; one commit occurs before the subsequent undo:
```text
[UNDO] Begin cur=(0,0) pending=0x0 t=- r=-1 c=-1 nlen=0 current=0x0 saved=0x0
[UNDO] commit:enter cur=(0,0) pending=0x0 t=- r=-1 c=-1 nlen=0 current=0x0 saved=0x0
[UNDO] Begin:new cur=(0,0) pending=0x... t=Insert r=0 c=0 nlen=0 current=0x0 saved=0x0
[UNDO] Append:sv cur=(0,0) pending=0x... t=Insert r=0 c=0 nlen=1 current=0x0 saved=0x0
... (more Append as characters are typed) ...
[UNDO] commit:enter cur=(0,5) pending=0x... t=Insert r=0 c=0 nlen=5 current=0x0 saved=0x0
[UNDO] commit:done cur=(0,5) pending=0x0 t=- r=-1 c=-1 nlen=0 current=0x... saved=0x0
```
```markdown
# Undo System Implementation Roadmap for kte/kge
- Undo then Redo across that batch:
```text
[UNDO] commit:enter cur=(0,5) pending=0x0 t=- r=-1 c=-1 nlen=0 current=0x... saved=0x0
[UNDO] undo cur=(0,5) pending=0x0 t=- r=-1 c=-1 nlen=0 current=0x0 saved=0x0
[UNDO] commit:enter cur=(0,5) pending=0x0 t=- r=-1 c=-1 nlen=0 current=0x0 saved=0x0
[UNDO] redo cur=(0,5) pending=0x0 t=- r=-1 c=-1 nlen=0 current=0x... saved=0x0
```
This is the complete implementation plan for the non-linear undo/redo
system for kte. This document serves as a detailed
specification for Junie to implement an undo system similar to emacs'
undo-tree.
- Newline and backspace/delete traces follow the same pattern with `t=Newline` or `t=Delete` and immediate commit for newline.
Capture by running `kge`/`kte` with `KTE_UNDO_DEBUG=ON` and performing the actions; append representative 36 line snippets to docs.
## Overview
Notes
The goal is to implement a robust, memory-safe undo system where:
- Pointer values and exact cursor positions in the logs depend on the runtime and actions; this is expected.
- Keep `KTE_UNDO_DEBUG` OFF by default in CI/release to avoid noisy logs and any performance impact.
1. Each buffer has its own independent undo tree
2. Undo and redo are non-linear - typing after undo creates a branch
3. Operations are batched into word-level undo steps
4. The system is leak-proof and handles buffer closure gracefully
̄1) Current State Summary (from docs/undo-state.md)
## Phase 1: Core Data Structures
- Terminal (kte): Keybindings and UndoSystem integration have been stable.
- GUI (kge): Previously, C-k u/U mapping and SDL TEXTINPUT suppression had issues on macOS; these were debugged. The
core root cause of “status shows Undone but no change” was fixed by moving UndoSystem::Begin/Append/commit to occur
after buffer modifications/cursor updates so batching conditions see the correct cursor.
- Undo core exists with tree invariants, saved marker/dirty flag mirroring, batching for Insert/Delete, and Newline as a
single-step undo.
### 1.1 UndoType enum (UndoNode.h)
```
Gaps/Risks
cpp enum class UndoType : uint8_t { Insert, Delete, Paste, // can
reuse Insert if preferred Newline, DeleteRow, // Future extensions:
IndentRegion, KillRegion };
- Event-path unification between KEYDOWN and TEXTINPUT across platforms (macOS specifics).
- Comprehensive tests for branching, GC/limits, multi-line operations, and UTF-8 text input.
- Advanced/compound command grouping and future region operations.
```
### 1.2 UndoNode struct (UndoNode.h)
```
cpp 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)
std::unique_ptr<UndoNode> child = nullptr; // next in current timeline
std::unique_ptr<UndoNode> next = nullptr; // redo branch (rarely used)
UndoNode* parent = nullptr; // weak pointer for navigation };
2) Design Goals (emacs-like undo-tree)
```
### 1.3 UndoTree struct (UndoTree.h)
```
- Per-buffer, non-linear undo tree: new edits after undo create a branch; existing redo branches are discarded.
- Batching: insert/backspace/paste/newline grouped into sensible units to match user expectations.
- Silent apply during undo/redo (no re-recording), using raw Buffer methods only.
- Correct saved/dirty tracking and robust pending node lifecycle (detached until commit).
- Efficient memory behavior; optional pruning limits similar to emacs (undo-limit, undo-strong-limit).
- Deterministic behavior across terminal and GUI frontends.
cpp struct UndoTree { std::unique_ptr<UndoNode> root; // first edit
ever UndoNode* current = nullptr; // current state of buffer UndoNode*
saved = nullptr; // points to node matching last save
std::unique_ptr<UndoNode> pending = nullptr; // in-progress batch };
```
### 1.4 UndoSystem class (UndoSystem.h)
```
3) Invariants and API (must align with docs/undo.md)
cpp class UndoSystem { private: std::unique_ptr<UndoTree> tree;
- UndoTree holds root/current/saved/pending; pending is detached and only linked on commit.
- Begin(type) reuses pending only if: same type, same row, and pending->col + pending->text.size() == current cursor
col (or prepend rules for backspace sequences); otherwise it commits and starts a new node.
- commit(): frees redo siblings from current, attaches pending as current->child, advances current, clears pending;
nullifies saved marker if diverged.
- undo()/redo(): move current and apply the node using low-level Buffer APIs that do not trigger undo recording.
- mark_saved(): updates saved pointer and dirty flag (dirty ⇔ current != saved).
- discard_pending()/clear(): lifecycle for buffer close/reset/new file.
public: UndoSystem(); ~UndoSystem() = default;
// Core batching API
void begin(UndoType type, int row, int col);
void append(char ch);
void append(std::string_view text);
void commit();
4) Phased Roadmap
// Undo/Redo operations
void undo(class Buffer& buffer);
void redo(class Buffer& buffer);
Phase 0 — Baseline & Instrumentation (1 day)
// State management
void mark_saved();
void discard_pending();
void clear();
- Audit UndoSystem against docs/undo.md invariants; ensure apply() uses only raw Buffer ops.
- Verify Begin/Append ordering across all edit commands: insert, backspace, delete, newline, paste.
- Add a temporary debug toggle (compile-time or editor flag) to log Begin/Append/commit/undo/redo, cursor(row,col), node
sizes, and pending state. Include assertions for: pending detached, commit clears pending, redo branch freed on new
commit, and correct batching preconditions.
- Deliverables: Short log from typing/undo/redo scenarios; instrumentation behind a macro so it can be removed.
// Query methods
bool can_undo() const;
bool can_redo() const;
bool is_dirty() const;
Phase 1 — Input Path Unification & Batching Rules (12 days)
private: void apply_node(Buffer& buffer, const UndoNode* node, int
direction); bool should_batch_with_pending(UndoType type, int row, int
col) const; void attach_pending_to_current(); void
discard_redo_branches(); };
- Ensure all printable text insertion (terminal and GUI) flows through CommandId::InsertText and reaches UndoSystem
Begin/Append. On SDL, handle KEYDOWN vs TEXTINPUT consistently; always suppress trailing TEXTINPUT after k-prefix
suffix commands.
- Commit boundaries: at k-prefix entry, before Undo/Redo, on cursor movement, on focus/file ops, and before any
non-editing command that should separate undo units.
- Batching heuristics:
- Insert: same row, contiguous columns; Append(std::string_view) handles multi-character text (pastes, IME).
- Backspace: prepend batching in increasing column order (store deleted text in forward order).
- Delete (forward): contiguous at same row/col.
- Newline: record as UndoType::Newline and immediately commit (single-step undo for line splits/joins).
- Deliverables: Manual tests pass for typing/backspace/delete/newline/paste; GUI C-k u/U work as expected on macOS.
```
## Phase 2: Buffer Integration
Phase 2 — Tree Limits & GC (1 day)
### 2.1 Add undo system to Buffer class (Buffer.h)
Add to Buffer class:
```
- Add configurable memory/size limits for undo data (soft and strong limits like emacs). Implement pruning of oldest
ancestors or deep redo branches while preserving recent edits. Provide stats (node count, bytes in text storage).
- Deliverables: Config hooks, tests demonstrating pruning without violating apply/undo invariants.
cpp private: std::unique_ptr<UndoSystem> undo_system; bool
applying_undo = false; // prevent recursive undo during apply
Phase 3 — Compound Commands & Region Ops (23 days)
public: // Raw operations (don't trigger undo) void
raw_insert_text(int row, int col, std::string_view text); void
raw_delete_text(int row, int col, size_t len); void raw_split_line(int
row, int col); void raw_join_lines(int row); void raw_insert_row(int
row, std::string_view text); void raw_delete_row(int row);
- Introduce an optional RAII-style UndoTransaction to group multi-step commands (indent region, kill region, rectangle
ops) into a single undo step. Internally this just sequences Begin/Append and ensures commit even on early returns.
- Support row operations (InsertRow/DeleteRow) with proper raw Buffer calls. Ensure join_lines/split_line are handled by
Newline nodes or dedicated types if necessary.
- Deliverables: Commands updated to use transactions when appropriate; tests for region delete/indent and multi-line
paste.
// Undo/Redo public API
void undo();
void redo();
bool can_undo() const;
bool can_redo() const;
void mark_saved();
bool is_dirty() const;
Phase 4 — Developer UX & Diagnostics (1 day)
```
### 2.2 Modify existing Buffer operations (Buffer.cc)
For each user-facing operation (`insert_char`, `delete_char`, etc.):
- Add a dev command to dump the undo tree (preorder) with markers for current/saved and pending (detached). For GUI,
optionally expose a simple ImGui debug window (behind a compile flag) that visualizes the current branch.
- Editor status improvements: show short status codes for undo/redo and when a new branch was created or redo discarded.
- Deliverables: Tree dump command; example output in docs.
1. **Before performing operation**: Call `undo_system->commit()` if cursor moved
2. **Begin batching**: Call `undo_system->begin(type, row, col)`
3. **Record change**: Call `undo_system->append()` with the affected text
4. **Perform operation**: Execute the actual buffer modification
5. **Auto-commit conditions**: Commit on cursor movement, command execution
Phase 5 — Comprehensive Tests & Property Checks (23 days)
Example pattern:
```
- Unit tests (extend test_undo.cc):
- Insert batching: type "Hello" then one undo removes all; redo restores.
- Backspace batching: type "Hello", backspace 3×, undo → restores the 3; redo → re-applies deletion.
- Delete batching (forward delete) with cursor not moving.
- Newline: split a line and undo to join; join a line (via backspace at col 0) and undo to split.
- Branching: type "abc", undo twice, type "X" → redo history discarded; ensure redo no longer restores 'b'/'c'.
- Saved/dirty: mark_saved after typing; ensure dirty flag toggles correctly after undo/redo; saved marker tracks the
node.
- discard_pending: create pending by typing, then move cursor or invoke commit boundary; ensure pending is attached;
also ensure discard on buffer close clears pending.
- clear(): resets state with no leaks; tree pointers null.
- UTF-8 input: insert multi-byte characters via InsertText with multi-char std::string; ensure counts/col tracking
behave (text stored as bytes; editor col policy consistent within kte).
- Integration tests (TestFrontend):
- Both TerminalFrontend and GUIFrontend: simulate text input and commands, including k-prefix C-k u/U.
- Paste scenarios: multi-character insertions batched as one.
- Property tests (optional but recommended):
- Generate random sequences of edits; record them; then apply undo until root and redo back to the end → buffer
contents match at each step; no crashes; dirty flag transitions consistent. Seed-based to reproduce failures.
- Redo-branch discard property: any new edit after undo must eliminate redo path; redoing should be impossible
afterward.
- Deliverables: Tests merged and passing on CI for both frontends; failures block changes to undo core.
cpp void Buffer::insert_char(char ch) { if (applying_undo) return; //
silent during undo application
Phase 6 — Performance & Stress (0.51 day)
// Auto-commit if cursor moved significantly or type changed
if (should_commit_before_insert()) {
undo_system->commit();
}
- Stress test with large files and long edit sequences. Target: smooth typing at 10k+ ops/minute on commodity hardware;
memory growth bounded when GC limits enabled.
- Deliverables: Basic perf notes; optional lightweight benchmarks.
undo_system->begin(UndoType::Insert, cursor_row, cursor_col);
undo_system->append(ch);
// Perform actual insertion
raw_insert_text(cursor_row, cursor_col, std::string(1, ch));
cursor_col++;
5) Acceptance Criteria
}
- Conformance to docs/undo.md invariants and API surface (including raw Buffer operations for apply()).
- Repro checklist passes:
- Type text; single-step undo/redo works and respects batching.
- Backspace/delete batching works.
- Newline split/join are single-step undo/redo.
- Branching works: undo, then type → redo path is discarded; no ghost redo.
- Saved/dirty flags accurate across undo/redo and diverge/rejoin paths.
- No pending nodes leaked on buffer close/reload; no re-recording during undo/redo.
- Behavior identical across terminal and GUI input paths.
- Tests added for all above; CI green.
```
### 2.3 Commit triggers
Auto-commit `pending` operations when:
- Cursor moves (arrow keys, mouse click)
- Any command starts executing
- Buffer switching
- Before undo/redo operations
- Before file save/close
## Phase 3: UndoSystem Implementation
6) Concrete Work Items by File
### 3.1 Core batching logic (UndoSystem.cc)
```
- UndoSystem.h/cc:
- Re-verify Begin/Append ordering; enforce batching invariants; prepend logic for backspace; immediate commit for
newline.
- Implement/verify apply() uses only Buffer raw methods: insert_text/delete_text/split_line/join_lines/row ops.
- Add limits (configurable) and stats; add discard_pending safety paths.
- Buffer.h/cc:
- Ensure raw methods exist and do not trigger undo; ensure UpdateBufferReference is correctly used when
replacing/renaming the underlying buffer.
- Call undo.commit() on cursor movement and non-editing commands (via Command layer integration).
- Command.cc:
- Ensure all edit commands drive UndoSystem correctly; commit at k-prefix entry and before Undo/Redo.
- Introduce UndoTransaction for compound commands when needed.
- GUIInputHandler.cc / TerminalInputHandler.cc / KKeymap.cc:
- Ensure unified InsertText path; suppress SDL_TEXTINPUT when a k-prefix suffix produced a command; preserve case
mapping.
- Tests: test_undo.cc (extend) + new tests (e.g., test_undo_branching.cc, test_undo_multiline.cc).
cpp void UndoSystem::begin(UndoType type, int row, int col) { if
(should_batch_with_pending(type, row, col)) { // Continue existing
batch return; }
// Commit any existing pending operation
if (pending) {
commit();
}
7) Example Test Cases (sketches)
// Create new pending node
pending = std::make_unique<UndoNode>();
pending->type = type;
pending->row = row;
pending->col = col;
pending->text.clear();
- Branch discard after undo:
1) InsertText("abc"); Undo(); Undo(); InsertText("X"); Redo();
Expected: Redo is a no-op (or status indicates no redo), buffer is "aX".
}
- Newline split/join:
1) InsertText("ab"); Newline(); InsertText("c"); Undo();
Expected: single undo joins lines → buffer "abc" on one line at original join point; Redo() splits again.
bool UndoSystem::should_batch_with_pending(UndoType type, int row, int
col) const { if (!pending) return false; if (pending->type != type)
return false; if (pending->row != row) return false;
- Backspace batching:
1) InsertText("hello"); Backspace×3; Undo();
Expected: restores "hello".
// For Insert: check if we're continuing at the right position
if (type == UndoType::Insert) {
return (pending->col + pending->text.size()) == col;
}
- UTF-8 insertion:
1) InsertText("😀汉"); Undo(); Redo();
Expected: content unchanged across cycles; no crashes.
// For Delete: check if we're continuing from the same position
if (type == UndoType::Delete) {
return pending->col == col;
}
- Saved/dirty transitions:
1) InsertText("hi"); mark_saved(); InsertText("!"); Undo(); Redo();
Expected: dirty false after mark_saved; dirty true after InsertText("!"); dirty returns to false after Undo();
true again after Redo().
return false;
}
8) Risks & Mitigations
```
### 3.2 Commit logic
```
- SDL/macOS event ordering (KEYDOWN vs TEXTINPUT, IME): Mitigate by suppressing TEXTINPUT on mapped k-prefix suffixes;
optionally temporarily disable SDL text input during k-prefix suffix mapping; add targeted diagnostics.
- UTF-8 width vs byte-length: Store bytes in UndoNode::text; keep column logic consistent with existing Buffer
semantics.
- Memory growth: Add GC/limits and provide a way to clear/reduce history for huge sessions.
- Re-entrancy during apply(): Prevent public edit paths from being called; use only raw operations.
cpp void UndoSystem::commit() { if (!pending || pending->text.empty())
{ pending.reset(); return; }
// Discard any redo branches from current position
discard_redo_branches();
9) Nice-to-Have (post-MVP)
// Attach pending as child of current
attach_pending_to_current();
- Visual undo-tree navigation (emacs-like time travel and branch selection), at least as a debug tool initially.
- Persistent undo across saves (opt-in; likely out-of-scope initially).
- Time-based batching threshold (e.g., break batches after >500ms pause in typing).
// Move current forward
current = pending.release();
if (current->parent) {
current->parent->child.reset(current);
}
// Update saved pointer if we diverged
if (saved && saved != current) {
// Check if saved is still reachable from current
if (!is_ancestor_of(current, saved)) {
saved = nullptr;
}
}
10) Execution Notes for a Junior Engineer/Agentic System
}
- Start from Phase 0; do not skip instrumentation—assertions will catch subtle batching bugs early.
- Change one surface at a time; when adjusting Begin/Append/commit positions, re-run unit tests immediately.
- Always ensure commit boundaries before invoking commands that move the cursor/state.
- When unsure about apply(), read docs/undo.md and mirror exactly: only raw Buffer methods, never the public editing
APIs.
- Keep diffs small and localized; add tests alongside behavior changes.
```
### 3.3 Apply operations
```
Appendix A — Minimal Developer Checklist
cpp void UndoSystem::apply_node(Buffer& buffer, const UndoNode* node,
int direction) { if (!node) return;
- [ ] Begin/Append occur after buffer mutation and cursor updates for all edit commands.
- [ ] Pending detached until commit; freed/cleared on commit/discard/clear.
- [ ] Redo branches freed on new commit after undo.
- [ ] mark_saved updates both saved pointer and dirty flag; dirty mirrors current != saved.
- [ ] apply() uses only raw Buffer methods; no recording during apply.
- [ ] Terminal and GUI both route printable input to InsertText; k-prefix mapping suppresses trailing TEXTINPUT.
- [ ] Unit and integration tests cover batching, branching, newline, saved/dirty, and UTF-8 cases.
switch (node->type) {
case UndoType::Insert:
if (direction > 0) { // redo
buffer.raw_insert_text(node->row, node->col, node->text);
} else { // undo
buffer.raw_delete_text(node->row, node->col, node->text.size());
}
break;
case UndoType::Delete:
if (direction > 0) { // redo
buffer.raw_delete_text(node->row, node->col, node->text.size());
} else { // undo
buffer.raw_insert_text(node->row, node->col, node->text);
}
break;
case UndoType::Newline:
if (direction > 0) { // redo
buffer.raw_split_line(node->row, node->col);
} else { // undo
buffer.raw_join_lines(node->row);
}
break;
// Handle other types...
}
}
```
## Phase 4: Command Integration
### 4.1 Add undo/redo commands (Command.cc)
Register the undo/redo commands in the command system:
```
cpp // In InstallDefaultCommands() CommandRegistry::Register({
CommandId::Undo, "undo", "Undo the last change", [](CommandContext&
ctx) { auto& editor = ctx.editor; auto* buffer =
editor.current_buffer(); if (buffer && buffer->can_undo()) {
buffer->undo(); return true; } return false; }, false // not public
command });
CommandRegistry::Register({ CommandId::Redo, "redo", "Redo the last
undone change", [](CommandContext& ctx) { auto& editor = ctx.editor;
auto* buffer = editor.current_buffer(); if (buffer &&
buffer->can_redo()) { buffer->redo(); return true; } return false; },
false // not public command });
```
### 4.2 Update keybinding handlers
Ensure the input handlers map `C-k u` to `CommandId::Undo` and `C-k r`
to `CommandId::Redo`.
## Phase 5: Memory Management and Edge Cases
### 5.1 Buffer lifecycle management
- **Constructor**: Initialize `undo_system = std::make_unique<UndoSystem>()`
- **Destructor**: `undo_system.reset()` (automatic)
- **File reload**: Call `undo_system->clear()` before loading
- **New file**: Call `undo_system->clear()`
- **Close buffer**: Call `undo_system->discard_pending()` then let destructor handle cleanup
### 5.2 Save state tracking
- **After successful save**: Call `buffer->mark_saved()`
- **For dirty flag**: Use `buffer->is_dirty()`
### 5.3 Edge case handling
- Prevent undo during undo application (`applying_undo` flag)
- Handle empty operations gracefully
- Ensure cursor positioning after undo/redo
- Test memory leaks with rapid typing + buffer close
## Phase 6: Testing
### 6.1 Unit tests (test_undo.cc)
Create comprehensive tests covering:
- Basic typing and undo
- Word-level batching
- Non-linear undo (type, undo, type different text)
- Memory leak testing
- Save state tracking
- Edge cases (empty buffers, large operations)
### 6.2 Integration tests
- Test with all buffer implementations (GapBuffer, PieceTable)
- Test with GUI and Terminal frontends
- Test rapid typing + immediate buffer close
- Test file reload during pending operations
## Implementation Priority
1. **Phase 1**: Implement core data structures
2. **Phase 2**: Add Buffer integration points
3. **Phase 3**: Implement UndoSystem methods
4. **Phase 4**: Wire up commands and keybindings
5. **Phase 5**: Handle edge cases and memory management
6. **Phase 6**: Comprehensive testing
## Critical Success Criteria
- ✅ No memory leaks even with rapid typing + buffer close
- ✅ Batching works correctly (word-level undo steps)
- ✅ Non-linear undo creates branches correctly
- ✅ Save state tracking works properly
- ✅ Silent operations during undo application
- ✅ Clean integration with existing Buffer operations
This roadmap provides Junie with a complete, step-by-step implementation plan that preserves the original design goals while ensuring robust, memory-safe implementation.
```
This roadmap refines your original plan by:
1. **Memory Safety**: Uses `std::unique_ptr` for automatic memory
management
2. **Clear Implementation Steps**: Breaks down into logical phases
3. **Integration Points**: Clearly identifies where to hook into
existing code
4. **Edge Case Handling**: Addresses buffer lifecycle and error
conditions
5. **Testing Strategy**: Ensures robust validation
The core design remains faithful to your emacs-style undo tree vision
while being practical for implementation by Junie.