From f515c4ec3b873d750484aca7399943854456b3a7 Mon Sep 17 00:00:00 2001 From: Greg Pomerantz Date: Wed, 3 Jun 2026 12:01:25 -0400 Subject: [PATCH] Documentation updates. --- doc/editor_implementation_plan.md | 169 +++++++++++++++++++++++------- 1 file changed, 132 insertions(+), 37 deletions(-) diff --git a/doc/editor_implementation_plan.md b/doc/editor_implementation_plan.md index c1d598a..c0db20e 100644 --- a/doc/editor_implementation_plan.md +++ b/doc/editor_implementation_plan.md @@ -9,43 +9,47 @@ ## 2. Core State Structure -The `State` struct in `internal/editor/state.go` will be extended to track active editing session data: +The `State` struct in `internal/editor/state.go` is extended to track active editing session data: ```go type EditorState struct { - Buffer string // Document content (or gap buffer) - CursorPosition int // Byte offset - SelectionStart int // -1 if no selection - SelectionEnd int // -1 if no selection - UndoStack []EditCommand // For undo/redo - Dirty bool // Needs save + Buffer string // Document content (plain string for now) + CursorPosition int // Byte offset + SelectionStart int // -1 if no selection (field exists, unused) + SelectionEnd int // -1 if no selection (field exists, unused) + Dirty bool // Needs save + CursorVisible bool // Blink state (field exists, no timer yet) + // UndoStack []EditCommand // Planned for Phase 4 } ``` +**Current state** (as of June 2026): `EditorState` is defined in `internal/editor/state.go` with the fields above. `UndoStack` is deferred. `SelectionStart`/`SelectionEnd` are present but not yet wired into any UI or input handler. + ## 3. Implementation Phases ### Phase 1: Basic Buffer Management & Cursor -- [ ] Define `EditorState` (Cursor, Selection, Dirty flag, GlyphLayout). -- [ ] Implement `GlyphLayout` capture in `drawWrappedText` and feedback via `layoutChan`. -- [ ] Implement cursor navigation (Arrow keys: Left, Right, Up, Down) using GlyphLayout. -- [ ] Implement basic buffer updates (Insert character, Delete/Backspace). -- [ ] Implement cursor display and blink animation. -- [ ] Update `EditorLayout` to position cursor from GlyphLayout (no fixed-width approximation). +- [x] Define `EditorState` (Cursor, Selection, Dirty flag). — **DONE** (`internal/editor/state.go`) +- [ ] Implement `GlyphLayout` capture in `drawWrappedText` and feedback via `layoutChan`. — **NOT STARTED** (see §8) +- [x] Implement cursor navigation (Arrow keys: Left, Right). — **DONE** (byte-based via `HandleCursorMove`; Up/Down not yet implemented) +- [x] Implement basic buffer updates (Insert character, Delete/Backspace). — **DONE** (`HandleInsert`, `HandleBackspace`, `HandleDelete`) +- [x] Implement cursor display. — **DONE** (`Cursor` element in `internal/ui/element.go`, rendered in `EditorLayout`) +- [ ] Implement cursor blink animation. — **NOT STARTED** (`CursorVisible` field exists but no blink timer) +- [ ] Update `EditorLayout` to position cursor from GlyphLayout (no fixed-width approximation). — **NOT STARTED** (currently uses `byteOffsetToLineCol` with `EditorFontSize * 0.6` fixed-width approximation) ### Phase 2: File IO Integration -- [ ] Implement `SaveFile` handler: Dispatch `WriteFileTask` to worker pool. -- [ ] Implement `LoadFile` handler: Dispatch `ReadFileTask` to worker pool (existing in `logic.go`). -- [ ] Status bar integration: Show "Saving..." indicator, "Modified" status. +- [ ] Implement `SaveFile` handler: Dispatch `WriteFileTask` to worker pool. — **NOT STARTED** +- [x] Implement `LoadFile` handler: Dispatch `ReadFileTask` to worker pool. — **DONE** (`openFileChan` in `logic.go` dispatches `ReadFileTask`; `handleWorkerResult` loads content into `Editor.Buffer`) +- [ ] Status bar integration: Show "Saving..." indicator, "Modified" status. — **NOT STARTED** (status bar exists with static labels; no dynamic save/modified state) ### Phase 3: Text Editing Operations -- [ ] Implement Cut/Copy/Paste interactions. -- [ ] Implement multi-line text navigation (Home/End, PageUp/PageDown). -- [ ] Implement text selection display and mouse interaction. -- [ ] Implement soft-wrap toggle (already exists, need to verify logic). +- [ ] Implement Cut/Copy/Paste interactions. — **NOT STARTED** (icon elements exist but handlers are `nil`) +- [ ] Implement multi-line text navigation (Home/End, PageUp/PageDown). — **NOT STARTED** +- [ ] Implement text selection display and mouse interaction. — **NOT STARTED** (`SelectionStart`/`SelectionEnd` fields exist in `EditorState` but are unused) +- [x] Implement soft-wrap toggle. — **DONE** (`ToggleWordWrap` in `state.go`, wired to bottom bar "Wrap" label tap) ### Phase 4: Polish & Advanced Features -- [ ] Undo/Redo stack implementation. -- [ ] Performance testing with large files (>1MB). +- [ ] Undo/Redo stack implementation. — **NOT STARTED** (`UndoStack` field is commented out in `EditorState`) +- [ ] Performance testing with large files (>1MB). — **NOT STARTED** --- @@ -122,7 +126,9 @@ Storing all X advances is not expensive — a single page of text is a tiny amou ### 8.4 Capture Point -`Renderer.drawWrappedText` already iterates every glyph via `r.shp.NextGlyph()`. It captures layout data during that loop and returns it: +**Current state (as of June 2026):** `drawWrappedText` iterates every glyph via `r.shp.NextGlyph()` but does **not** capture per-glyph layout data. It tracks only `lastLineY` (the baseline Y of the last drawn glyph) and `displayLineCount`. These are exposed via `Renderer.LastLineY()` and `Renderer.DisplayLineCount()`. + +**Planned change:** `drawWrappedText` will be extended to capture layout data during the glyph iteration loop and return a `GlyphLayout`: ```go func (r *Renderer) drawWrappedText(...) GlyphLayout { @@ -148,21 +154,59 @@ func (r *Renderer) drawWrappedText(...) GlyphLayout { } ``` -`TextField.Draw` stores the returned layout on the renderer for the main loop to pick up. +`TextField.Draw` will store the returned layout on the renderer for the main loop to pick up. ### 8.5 Feedback Path -A new channel carries the full layout from renderer to logic: +**Current state (as of June 2026):** The main loop sends `lastLineY` (as `int`) on `DisplayLineChan()` after each frame. The logic goroutine stores it in `State.LastLineY`. This is the **only** layout feedback path currently in use. ```go -layoutChan chan GlyphLayout // in Logic struct +// Current: main.go +lastLineY := int(renderer.LastLineY()) +logic.DisplayLineChan() <- lastLineY + +// Current: logic.go +case y := <-l.lastLineYChan: + if ui.Dp(y) != l.state.LastLineY { + l.state.LastLineY = ui.Dp(y) + l.frameChan <- l.state.layout(l.browserManager) + } ``` -Main loop sends `r.GlyphLayout()` after each draw frame. Logic stores it in `State.Editor.GlyphLayout`. The old `lastLineYChan` is removed — `LastLineY` is derived from `GlyphLayout.Y[len-1]`. +**Planned change:** A new `layoutChan` will carry the full `GlyphLayout` from renderer to logic, replacing `lastLineYChan`. `LastLineY` will be derived from `GlyphLayout.Y[len-1]`. + +```go +layoutChan chan GlyphLayout // in Logic struct (replaces lastLineYChan) +``` ### 8.6 Editor State Consumption -`EditorState` gains the layout field: +**Current state (as of June 2026):** `EditorState` does **not** yet contain a `GlyphLayout` field. Cursor positioning uses a fixed-width approximation: + +```go +// Current: state.go — EditorLayout() +line, col := byteOffsetToLineCol(TheState.Editor.Buffer, TheState.Editor.CursorPosition) +charWidth := ui.Dp(float32(EditorFontSize) * 0.6) +lineHeight := EditorLineHeight() +cursorX := editorRegion.X + ui.Dp(col)*charWidth +cursorY := editorRegion.Y + ui.Dp(line)*lineHeight - TheState.ScrollOffset +``` + +Cursor movement (`HandleCursorMove`) is byte-based (±1 per arrow press), not glyph-based: + +```go +// Current: state.go +func HandleCursorMove(delta int) { + newPos := TheState.Editor.CursorPosition + delta + if newPos < 0 { newPos = 0 } + if newPos > len(TheState.Editor.Buffer) { newPos = len(TheState.Editor.Buffer) } + TheState.Editor.CursorPosition = newPos +} +``` + +This works for monospace-like navigation but is inaccurate for proportional fonts and does not respect visual (wrapped) line boundaries. + +**Planned change:** `EditorState` gains the layout field and cursor logic switches to GlyphLayout-based positioning: ```go type EditorState struct { @@ -173,7 +217,7 @@ type EditorState struct { } ``` -**Cursor rendering** in `EditorLayout`: +**Cursor rendering** in `EditorLayout` (planned): ```go func EditorLayout(...) []ui.Element { layout := TheState.Editor.GlyphLayout @@ -196,7 +240,7 @@ func EditorLayout(...) []ui.Element { } ``` -**Cursor movement** (`HandleCursorMove`): +**Cursor movement** (`HandleCursorMove`, planned): ```go func HandleCursorMove(delta int) { layout := TheState.Editor.GlyphLayout @@ -232,11 +276,62 @@ func HandleCursorMove(delta int) { ### 8.8 Summary of Changes -| File | Change | +| File | Current State (June 2026) | Planned Change | +|---|---|---| +| `internal/ui/unit.go` | No `GlyphLayout` type defined | Add `GlyphLayout` struct | +| `internal/ui/render.go` | `drawWrappedText` tracks `lastLineY` and `displayLineCount` only | Return `GlyphLayout`; derive `lastLineY` from it; expose via `r.GlyphLayout()` | +| `internal/ui/element.go` | `TextField.Draw` calls `drawWrappedText` but ignores any layout return value | Store returned `GlyphLayout` on renderer | +| `internal/editor/logic.go` | Uses `lastLineYChan` for feedback | Add `layoutChan`; handle it in `Run()` loop; remove `lastLineYChan` | +| `internal/editor/state.go` | `EditorLayout` uses fixed-width approximation (`EditorFontSize * 0.6`) and `byteOffsetToLineCol`; `HandleCursorMove` is byte-based (±1) | `EditorState.GlyphLayout` field; `EditorLayout` uses it for cursor positioning; `HandleCursorMove` uses it for navigation | +| `cmd/pad/main.go` | Sends `int(renderer.LastLineY())` on `DisplayLineChan()` after each frame | Send `r.GlyphLayout()` on `layoutChan` after draw; remove `lastLineYChan` usage | + +--- + +## 9. Current Implementation Status (as of June 2026) + +This section documents what has been implemented beyond the original plan, and what gaps remain. + +### 9.1 Implemented Components + +| Component | Status | Notes | +|---|---|---| +| `EditorState` struct | **Done** | `Buffer`, `CursorPosition`, `SelectionStart/End` (unused), `Dirty`, `CursorVisible` | +| `HandleInsert` | **Done** | String concatenation at cursor position, advances cursor, sets `Dirty` | +| `HandleBackspace` | **Done** | Removes byte before cursor, decrements cursor, sets `Dirty` | +| `HandleDelete` | **Done** | Removes byte after cursor, sets `Dirty` | +| `HandleCursorMove` | **Done (basic)** | Byte-based ±1 movement, bounded by buffer length | +| `HandleKeyDown` | **Done** | Dispatches `key.Name` (arrows, delete) and `key.EditEvent` (text input) | +| `EditorLayout` | **Done** | Full layout: status bar, text field, cursor element, bottom bar | +| `Cursor` element | **Done** | Renders as 2dp × 18dp vertical bar at computed position | +| `byteOffsetToLineCol` | **Done** | Converts byte offset to (line, col) for cursor positioning | +| File open flow | **Done** | `OpenFile` → `openFileChan` → `ReadFileTask` → worker pool → `handleWorkerResult` → `Editor.Buffer` | +| Soft-wrap toggle | **Done** | `ToggleWordWrap` wired to bottom bar label | +| Page navigation | **Done** | Browser ↔ Editor page switching via `GoToBrowser`/`GoToEditor` | +| Keyboard focus | **Done** | `FocusedElementID` + `key.FocusCmd` + `key.Filter`/`key.FocusFilter` routing | +| Scroll handling | **Done** | `HandleScroll` clamped to `[0, MaxScroll]` derived from `LastLineY` | +| Mock filesystem | **Done** | `populateMockFileSystem` with realistic directory structure | +| Worker pool | **Done** | 4-goroutine pool with `ReadFileTask`, `BuildIndexTask`, etc. | + +### 9.2 Test Coverage + +| Test File | Coverage | |---|---| -| `internal/ui/unit.go` | Add `GlyphLayout` struct | -| `internal/ui/render.go` | `drawWrappedText` returns `GlyphLayout`; derive `lastLineY` from it; expose via `r.GlyphLayout()` | -| `internal/ui/element.go` | `TextField.Draw` stores returned layout on renderer | -| `internal/editor/logic.go` | Add `layoutChan`; handle it in `Run()` loop; remove `lastLineYChan` | -| `internal/editor/state.go` | `EditorState.GlyphLayout` field; `EditorLayout` uses it for cursor positioning; `HandleCursorMove` uses it for navigation | -| `cmd/pad/main.go` | Main loop sends `r.GlyphLayout()` on `layoutChan` after draw; remove `lastLineYChan` usage | +| `buffer_test.go` | `TestInsertChar` — verifies insert at end of buffer | +| `buffer_test.go` | `TestBackspace` — verifies backspace at end of buffer | +| `cursor_test.go` | `TestByteOffsetToLineCol_Logical` — verifies logical line/column conversion | +| `integration_test.go` | `TestOpenFileIntegration` — verifies full file open flow through worker pool | + +### 9.3 Known Gaps + +| Gap | Description | +|---|---| +| GlyphLayout capture | Not yet implemented — cursor uses fixed-width approximation | +| Cursor blink | `CursorVisible` field exists but no blink timer is driven | +| Up/Down arrow | `HandleKeyDown` does not handle `NameUpArrow`/`NameDownArrow` | +| Home/End/PageUp/PageDown | No handlers for these keys | +| Cut/Copy/Paste | Icon elements exist with `nil` handlers | +| Text selection | `SelectionStart`/`SelectionEnd` fields present but unused | +| Save file | No `SaveFile` handler or `WriteFileTask` integration | +| Undo/Redo | Commented out in `EditorState`, no implementation | +| Status bar dynamics | Filename, line/col, byte count are static strings | +| `Dirty` flag UI | `Dirty` is set on edits but not displayed in status bar |