Files
kte/docs/undo-roadmap.md
Kyle Isom cbbde43dc2 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.
2025-12-03 15:12:28 -08:00

391 lines
11 KiB
Markdown

## Updated Undo System Plan for kte/kge
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:
### Refined Data Structures
The proposed data structures are sound but need some refinements:
```c++
enum class UndoType : uint8_t {
Insert,
Delete,
Paste,
Newline,
DeleteRow,
// Future: IndentRegion, KillRegion, etc.
};
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
};
struct UndoTree {
std::unique_ptr<UndoNode> root;
UndoNode* current = nullptr;
UndoNode* saved = nullptr;
std::unique_ptr<UndoNode> pending = nullptr;
};
```
Key changes:
- 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
---
```markdown
# Undo System Implementation Roadmap for kte/kge
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.
## Overview
The goal is to implement a robust, memory-safe undo system where:
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
## Phase 1: Core Data Structures
### 1.1 UndoType enum (UndoNode.h)
```
cpp enum class UndoType : uint8_t { Insert, Delete, Paste, // can
reuse Insert if preferred Newline, DeleteRow, // Future extensions:
IndentRegion, KillRegion };
```
### 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 };
```
### 1.3 UndoTree struct (UndoTree.h)
```
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)
```
cpp class UndoSystem { private: std::unique_ptr<UndoTree> tree;
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();
// Undo/Redo operations
void undo(class Buffer& buffer);
void redo(class Buffer& buffer);
// State management
void mark_saved();
void discard_pending();
void clear();
// Query methods
bool can_undo() const;
bool can_redo() const;
bool is_dirty() const;
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(); };
```
## Phase 2: Buffer Integration
### 2.1 Add undo system to Buffer class (Buffer.h)
Add to Buffer class:
```
cpp private: std::unique_ptr<UndoSystem> undo_system; bool
applying_undo = false; // prevent recursive undo during apply
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);
// Undo/Redo public API
void undo();
void redo();
bool can_undo() const;
bool can_redo() const;
void mark_saved();
bool is_dirty() const;
```
### 2.2 Modify existing Buffer operations (Buffer.cc)
For each user-facing operation (`insert_char`, `delete_char`, etc.):
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
Example pattern:
```
cpp void Buffer::insert_char(char ch) { if (applying_undo) return; //
silent during undo application
// Auto-commit if cursor moved significantly or type changed
if (should_commit_before_insert()) {
undo_system->commit();
}
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++;
}
```
### 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
### 3.1 Core batching logic (UndoSystem.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();
}
// Create new pending node
pending = std::make_unique<UndoNode>();
pending->type = type;
pending->row = row;
pending->col = col;
pending->text.clear();
}
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;
// For Insert: check if we're continuing at the right position
if (type == UndoType::Insert) {
return (pending->col + pending->text.size()) == col;
}
// For Delete: check if we're continuing from the same position
if (type == UndoType::Delete) {
return pending->col == col;
}
return false;
}
```
### 3.2 Commit logic
```
cpp void UndoSystem::commit() { if (!pending || pending->text.empty())
{ pending.reset(); return; }
// Discard any redo branches from current position
discard_redo_branches();
// Attach pending as child of current
attach_pending_to_current();
// 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;
}
}
}
```
### 3.3 Apply operations
```
cpp void UndoSystem::apply_node(Buffer& buffer, const UndoNode* node,
int direction) { if (!node) return;
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.