- 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.
11 KiB
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:
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_ptrfor owned pointers to ensure proper RAII - Add weak
parentpointer for easier navigation - This ensures memory safety without manual management
# 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 child = nullptr; // next in current timeline std::unique_ptr 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 root; // first edit ever UndoNode* current = nullptr; // current state of buffer UndoNode* saved = nullptr; // points to node matching last save std::unique_ptr pending = nullptr; // in-progress batch };
### 1.4 UndoSystem class (UndoSystem.h)
cpp class UndoSystem { private: std::unique_ptr 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 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:
- Memory Safety: Uses
std::unique_ptrfor automatic memory management - Clear Implementation Steps: Breaks down into logical phases
- Integration Points: Clearly identifies where to hook into existing code
- Edge Case Handling: Addresses buffer lifecycle and error conditions
- Testing Strategy: Ensures robust validation
The core design remains faithful to your emacs-style undo tree vision while being practical for implementation by Junie.