From 3f53836adec08c7b188a80d12a9a6eeb426656c2 Mon Sep 17 00:00:00 2001 From: Greg Pomerantz Date: Wed, 3 Jun 2026 19:49:27 -0400 Subject: [PATCH] Implement vertical cursor movement and reset cursor on file open --- internal/editor/state.go | 90 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) diff --git a/internal/editor/state.go b/internal/editor/state.go index b276ebd..16c1237 100644 --- a/internal/editor/state.go +++ b/internal/editor/state.go @@ -167,6 +167,7 @@ func GoToEditor(data any) { // data is the filename string from the browser list. func OpenFile(data any) { TheState.Editor.Buffer = "Select a file to edit..." // Placeholder, should be loaded from file + TheState.Editor.CursorPosition = 0 // Reset cursor to top TheState.ScrollOffset = 0 // Reset editor scroll to top when opening a new file TheState.page = EditorPage TheState.FocusedElementID = "editor_text" // Set focus to editor @@ -214,6 +215,10 @@ func HandleKeyDown(data any) { HandleCursorMove(-1) case key.NameRightArrow: HandleCursorMove(1) + case key.NameUpArrow: + HandleVerticalCursorMove(true) + case key.NameDownArrow: + HandleVerticalCursorMove(false) case key.NameDeleteBackward: HandleBackspace() case key.NameDeleteForward: @@ -222,6 +227,91 @@ func HandleKeyDown(data any) { } } +// HandleVerticalCursorMove updates the cursor position to the previous or next visual line. +func HandleVerticalCursorMove(up bool) { + layout := TheState.Editor.GlyphLayout + if len(layout.ByteOffsets) == 0 { + return + } + + pos := TheState.Editor.CursorPosition + // Find current glyph index. + idx := sort.Search(len(layout.ByteOffsets), func(i int) bool { + return layout.ByteOffsets[i] >= pos + }) + // If idx == len, we are at the end. Use the last glyph. + if idx == len(layout.ByteOffsets) { + idx = len(layout.ByteOffsets) - 1 + } + + currentX := layout.X[idx] + currentY := layout.Y[idx] + + var targetIdx int = idx + + if up { + // Scan backwards to find the previous line's Y. + prevY := currentY + for i := idx; i >= 0; i-- { + if layout.Y[i] < prevY { + prevY = layout.Y[i] + break + } + } + + if prevY == currentY { + // Already at the top line? + return + } + + // Now find the closest X in prevY. + bestDiff := float32(1e9) + for i := 0; i < len(layout.Y); i++ { + if layout.Y[i] == prevY { + diff := float32(layout.X[i] - currentX) + if diff < 0 { + diff = -diff + } + if diff < bestDiff { + bestDiff = diff + targetIdx = i + } + } + } + } else { + // Scan forwards to find the next line's Y. + nextY := currentY + for i := idx; i < len(layout.Y); i++ { + if layout.Y[i] > nextY { + nextY = layout.Y[i] + break + } + } + + if nextY == currentY { + // Already at the bottom line? + return + } + + // Now find the closest X in nextY. + bestDiff := float32(1e9) + for i := 0; i < len(layout.Y); i++ { + if layout.Y[i] == nextY { + diff := float32(layout.X[i] - currentX) + if diff < 0 { + diff = -diff + } + if diff < bestDiff { + bestDiff = diff + targetIdx = i + } + } + } + } + + TheState.Editor.CursorPosition = layout.ByteOffsets[targetIdx] +} + // HandleDelete removes the character after the cursor. func HandleDelete() { pos := TheState.Editor.CursorPosition