Duplicated text editing logic lacks consistency
I've created a comprehensive refactoring plan at docs/feature/text-refactoring-amp.md.
Summary
The document (~2600 lines) covers:
Analysis (Chapters 1-2):
- Current state of 4 text input areas (Main Editor, Command Palette, Go-to-Line, CSV Cell)
- Problem statement: code duplication, inconsistent behavior, missing features
Core Design (Chapters 3-6):
TextBuffertrait abstracting overRopeandStringbackendsTextInputField<B>component with configurable cursor/selection/undo- Unified word boundary detection using
CharType InputEditfor optional undo/redo
Integration (Chapters 7-9):
- Unified
TextInputMsgenum replacing fragmented message types - Shared rendering pipeline via
render_text_input() - Context-specific restrictions via
InputFieldConfigandInputContext
Implementation (Chapters 10-12):
- 8-phase migration strategy over ~6 weeks
- File-by-file modification guide
- Comprehensive test strategy
Appendices:
- Current code inventory with file locations
- Message type mappings (old → new)
- Keyboard shortcut matrix
I'll analyze both plans and provide a comparison.
Comparison: My Plan vs Claude Code's Plan
Structural Differences
| Aspect | My Plan (text-refactoring-amp.md) | Claude Code (unified-text-editing.md) |
|---|---|---|
| Module Location | src/model/text_buffer.rs, src/model/text_input.rs |
src/editable/ (dedicated directory) |
| Main Abstraction | TextInputField<B: TextBuffer> |
EditableState + TextBuffer trait |
| Buffer Approach | Generic over buffer type | Separate TextBuffer / TextBufferMut traits |
| Message System | TextInputMsg enum |
TextEditMsg + EditContext + MoveTarget |
| Rendering | render_text_input() function |
TextFieldRenderer struct with TextFieldContent trait |
Key Architectural Differences
1. Module Organization
| My Plan | Claude Code's Plan |
|---|---|
Spreads across existing src/model/ |
Creates dedicated src/editable/ module |
| Fewer new files, more modification | More new files, cleaner separation |
Winner: Claude Code's approach is cleaner. A dedicated module makes it easier to find, test, and maintain.
2. Buffer Trait Design
// My Plan - Single trait with all methods
pub trait TextBuffer {
fn insert(&mut self, offset: usize, text: &str);
fn remove(&mut self, range: Range<usize>);
// ...
}
// Claude Code - Split read/write traits
pub trait TextBuffer {
fn line_count(&self) -> usize;
fn char_at(&self, line: usize, column: usize) -> Option<char>;
// Read-only methods
}
pub trait TextBufferMut: TextBuffer {
fn insert(&mut self, offset: usize, text: &str);
fn remove(&mut self, range: std::ops::Range<usize>);
}
Winner: Claude Code's split is better - it allows read-only access to buffers when mutation isn't needed (e.g., rendering).
3. Cursor/Selection Model
// My Plan - Byte offset based
pub struct InputCursor {
pub offset: usize,
pub desired_column: Option<usize>,
}
// Claude Code - Line/column based (matching existing editor)
pub struct Cursor {
pub line: usize,
pub column: usize,
pub desired_column: Option<usize>,
}
Winner: Claude Code's approach. Using line/column directly avoids constant offset↔position conversions and matches the existing EditorState model.
4. Message System
// My Plan - Flat enum
pub enum TextInputMsg {
CursorLeft,
CursorRight,
CursorWordLeft,
SelectLeft,
SelectWordLeft,
// ... 40+ variants
}
// Claude Code - Structured with MoveTarget
pub enum TextEditMsg {
Move(MoveTarget),
MoveWithSelection(MoveTarget),
InsertChar(char),
// ...
}
pub enum MoveTarget {
Left, Right, Up, Down,
WordLeft, WordRight,
LineStart, LineEnd,
// ...
}
Winner: Claude Code's approach. The MoveTarget abstraction reduces duplication (movement vs selection-movement share the same targets).
5. Context Handling
// My Plan - Config struct with flags
pub struct InputFieldConfig {
pub multiline: bool,
pub multi_cursor: bool,
pub allow_selection: bool,
pub enable_undo: bool,
pub char_filter: Option<fn(char) -> bool>,
}
// Claude Code - Explicit context enum + constraints
pub enum EditContext {
Editor(GroupId),
CommandPalette,
GotoLine,
// ...
}
pub struct EditConstraints {
pub allow_multiline: bool,
pub allow_multi_cursor: bool,
pub char_filter: Option<fn(char) -> bool>,
// ...
}
Winner: Claude Code's approach. Separating context identity from constraints is cleaner. The EditContext enum makes message routing explicit.
What My Plan Does Better
Detailed Rendering Code: My plan has complete rendering implementation with
TextInputRenderConfig, while Claude Code's is more abstract.UTF-8 Handling Detail: My plan explicitly handles byte offset ↔ character position conversions with examples.
Comprehensive Appendices: Keyboard shortcut matrix, message mappings, and file locations are more complete.
Incremental Undo Design: My
InputEditis simpler and standalone, while Claude Code reuses the existingEditOperationwhich couples it more tightly.
What Claude Code's Plan Does Better
EditContextRouting: HavingMsg::TextEdit(EditContext, TextEditMsg)makes the message routing very clean:Msg::TextEdit(EditContext::CommandPalette, msg) => { update_text_edit(&mut model.ui.palette.editable, msg) }Invariants Documentation: Appendix A explicitly lists invariants (cursor-selection parallel, sorted cursors, etc.)
Bridge Migration Strategy: The main editor migration uses a "bridge phase" that maps
TextEditMsgto existingEditorMsg/DocumentMsg- safer incremental migration.first_non_whitespace_column/last_non_whitespace_column: Useful methods I missed for smart home/end behavior.Explicit Manual Testing Checklist: Very practical for QA.
Overlapping Ideas
Both plans agree on:
TextBuffertrait abstracting overRopeandString- Unified cursor/selection model
- Configurable constraints per context
- Phased migration (CSV → Modals → Editor)
- 6-8 phase timeline
- Shared rendering code
Recommendations: Best of Both
I recommend merging these improvements from Claude Code into our plan:
- Adopt
src/editable/module structure - cleaner organization - Split
TextBuffer/TextBufferMuttraits - allows read-only access - Use line/column cursors instead of byte offsets - matches existing model
- Use
MoveTargetenum - reduces message variant duplication - Add
EditContextenum for explicit routing - Add the invariants appendix - crucial for implementation correctness
- Use bridge migration for main editor - safer incremental approach
Revised Module Structure (Merged)
src/editable/
├── mod.rs # Module exports
├── buffer.rs # TextBuffer, TextBufferMut traits + RopeBuffer, StringBuffer
├── cursor.rs # Position, Cursor (line/column based)
├── selection.rs # Selection (anchor/head positions)
├── history.rs # EditOperation, EditHistory
├── constraints.rs # EditConstraints, CharFilter
├── state.rs # EditableState<B: TextBuffer>
├── messages.rs # TextEditMsg, MoveTarget
└── context.rs # EditContext enum
src/update/text_edit.rs # update_text_edit() handler
src/view/text_field.rs # TextFieldRenderer + TextFieldContent trait
Summary
| Category | Winner |
|---|---|
| Module organization | Claude Code |
| Trait design | Claude Code |
| Cursor model | Claude Code |
| Message structure | Claude Code |
| Rendering detail | My Plan |
| UTF-8 handling | My Plan |
| Migration safety | Claude Code |
| Appendices | Tie (combine both) |
Overall: Claude Code's architectural choices are more principled. My plan has more implementation detail. The best approach is to use Claude Code's architecture with my implementation details and appendices merged in.
I'll create a merged final plan that combines the best of both approaches. This will be a comprehensive document.
Now I'll add the migration strategy, implementation order, and all appendices.
The comprehensive merged plan is complete at docs/feature/unified-text-editing-final.md.
Summary
The final document (~3,700 lines) combines the best of both plans:
From Claude Code's Plan (Architecture)
src/editable/module structure - dedicated, well-organized module- Split
TextBuffer/TextBufferMuttraits - read-only access for rendering - Line/column-based cursors - matches existing model, avoids conversions
MoveTargetenum - reduces message duplicationEditContextenum - explicit routing withMsg::TextEdit(context, msg)- Bridge migration strategy - safer incremental approach for main editor
- Key invariants appendix - crucial for correctness
From My Original Plan (Implementation Details)
- Complete Rust code for all structs and implementations
- Detailed UTF-8 handling in buffer implementations
- Full rendering code with
TextFieldRenderer - Keyboard shortcut matrix for macOS and Windows/Linux
- Message type mappings (old → new)
- File locations reference with line numbers
- Manual testing checklist for QA
Document Structure
- Executive Summary - Problem, solution, requirements
- Current State Analysis - 5 implementations, feature matrix
- Target Architecture - Module structure, data flow diagram
- Phase 1: Core Data Model - Complete implementations (~1,500 lines of Rust)
- Phase 2: Message System -
TextEditMsg,EditContext, routing - Phase 3: Rendering -
TextFieldRenderer, usage examples - Phase 4: Migration Strategy - 7 phases over 6 weeks
- Implementation Order - 8 milestones with checkboxes
- File Change Summary - New/modified/deprecated files
- Testing Strategy - Unit, integration, regression tests
- Appendices A-F - Invariants, word boundaries, shortcuts, mappings, files, testing checklist