diff --git a/doc/bugs.txt b/doc/bugs.txt index acd0e02..17adcd7 100644 --- a/doc/bugs.txt +++ b/doc/bugs.txt @@ -1,4 +1,9 @@ This file lists descriptions of outstanding bugs. Bugs are separated by empty lines. -On the BrowserPage, when I enter a search that limits the list of files, clicking on a file -delivers teh wrong file to the editor page. +If I move the cursor (using the arrow keys) to the right side of the screen in a word-wrapped +line, the cursor does not move to the next line right away, instead it moves through whitespace on +the right side of the wrapped line, sometimes moving off screen. Also in a word wrapped file, moving +the cursor to the right does not move all the way down to the end of the file, it stops at some +point before the end. Also when I change the screen width to change the word wrap, the cursor does +not stay at the same position in the file but instead tries to stay in the same screen position, +not following the word wrap as it adjusts the new screen config. diff --git a/internal/browser/handlers.go b/internal/browser/handlers.go index 9474a5c..3e62fbc 100644 --- a/internal/browser/handlers.go +++ b/internal/browser/handlers.go @@ -51,13 +51,29 @@ func clampScrollOffset(s *BrowserState) { } } +// resolveTapIndex maps a ListView row index to the actual sorted entry index. +// When search is active, the row index is into SearchResults, so we must +// look through SearchResults to find the real entry index. +func resolveTapIndex(s *BrowserState, index int) int { + if s.Query != "" && len(s.SearchResults) > 0 { + if index < 0 || index >= len(s.SearchResults) { + return -1 + } + return s.SearchResults[index] + } + return index +} + // HandleBrowserTap processes a tap on the ListView at the given index. func HandleBrowserTap(bm *BrowserManager, s *BrowserState, index int) { - if index < 0 || index >= s.TotalEntries { + // Resolve the ListView row index to the actual sorted entry index. + // When search is active, the row is an index into SearchResults. + entryIndex := resolveTapIndex(s, index) + if entryIndex < 0 || entryIndex >= s.TotalEntries { return } - entry, ok := getEntryByIndex(s, index) + entry, ok := getEntryByIndex(s, entryIndex) if !ok { return // Page not loaded } @@ -76,9 +92,11 @@ func HandleBrowserTap(bm *BrowserManager, s *BrowserState, index int) { bm.NavigateTo(newPath) } else { // Open file - s.SelectedIndex = index + s.SelectedIndex = entryIndex fmt.Printf("HandleBrowserTap: Opening file %s\n", entry.Path) - ui.OpenFile(entry.Path) + if ui.OpenFile != nil { + ui.OpenFile(entry.Path) + } // Dispatch ReadFileTask to the worker pool task := pool.NewReadFileTask(entry.Path, bm.fs) fmt.Printf("HandleBrowserTap: Dispatching ReadFileTask for %s\n", entry.Path) diff --git a/internal/browser/navigation_test.go b/internal/browser/navigation_test.go index d01cd93..4867ca2 100644 --- a/internal/browser/navigation_test.go +++ b/internal/browser/navigation_test.go @@ -58,3 +58,75 @@ func TestTapUp(t *testing.T) { t.Errorf("expected CurrentPath='/root', got %s", state.CurrentPath) } } + +// TestTapWithSearchResults verifies that tapping a file in the search +// results list opens the correct file, not the file at the same index +// in the full directory listing. +func TestTapWithSearchResults(t *testing.T) { + state := NewBrowserState() + fs := mock.NewFileSystem() + wp := pool.NewWorkerPool(1) + bm, _ := NewBrowserManager(state, wp, fs) + state.CurrentPath = "/root" + + // Mock entries: 5 files, sorted by name + entries := []Entry{ + NewEntry("/root/alpha.txt", "alpha.txt", 100, time.Time{}, false), + NewEntry("/root/beta.txt", "beta.txt", 200, time.Time{}, false), + NewEntry("/root/gamma.txt", "gamma.txt", 300, time.Time{}, false), + NewEntry("/root/delta.txt", "delta.txt", 400, time.Time{}, false), + NewEntry("/root/epsilon.txt", "epsilon.txt", 500, time.Time{}, false), + } + state.Pages[0] = NewPage(0, entries) + state.TotalEntries = 5 + + // Simulate a search for "beta" that matches only index 1 + state.Query = "beta" + state.SearchResults = []int{1} + + // Tapping row 0 of the search results should open beta.txt (entry index 1), + // NOT alpha.txt (entry index 0). + HandleBrowserTap(bm, state, 0) + + // Verify SelectedIndex was set to the resolved entry index (1), not the row index (0). + if state.SelectedIndex != 1 { + t.Errorf("expected SelectedIndex=1, got %d", state.SelectedIndex) + } +} + +// TestTapWithMultipleSearchResults verifies tapping different rows of +// search results resolves to the correct entries. +func TestTapWithMultipleSearchResults(t *testing.T) { + state := NewBrowserState() + fs := mock.NewFileSystem() + wp := pool.NewWorkerPool(1) + bm, _ := NewBrowserManager(state, wp, fs) + state.CurrentPath = "/root" + + // Mock entries: 5 files + entries := []Entry{ + NewEntry("/root/aaa.txt", "aaa.txt", 100, time.Time{}, false), + NewEntry("/root/zzz.txt", "zzz.txt", 200, time.Time{}, false), + NewEntry("/root/aaa2.txt", "aaa2.txt", 300, time.Time{}, false), + NewEntry("/root/zzz2.txt", "zzz2.txt", 400, time.Time{}, false), + NewEntry("/root/mid.txt", "mid.txt", 500, time.Time{}, false), + } + state.Pages[0] = NewPage(0, entries) + state.TotalEntries = 5 + + // Simulate a search for "zzz" that matches indices 1 and 3 + state.Query = "zzz" + state.SearchResults = []int{1, 3} + + // Tapping row 0 should resolve to entry index 1 (zzz.txt) + HandleBrowserTap(bm, state, 0) + if state.SelectedIndex != 1 { + t.Errorf("row 0: expected SelectedIndex=1, got %d", state.SelectedIndex) + } + + // Tapping row 1 should resolve to entry index 3 (zzz2.txt) + HandleBrowserTap(bm, state, 1) + if state.SelectedIndex != 3 { + t.Errorf("row 1: expected SelectedIndex=3, got %d", state.SelectedIndex) + } +} diff --git a/internal/editor/cursor_test.go b/internal/editor/cursor_test.go index c94ce44..557e2f1 100644 --- a/internal/editor/cursor_test.go +++ b/internal/editor/cursor_test.go @@ -2,51 +2,22 @@ package editor import ( "testing" - "pad/internal/ui" ) -// TestCursorExistsInLayout checks if the EditorLayout produces a cursor element. -func TestCursorExistsInLayout(t *testing.T) { - state := NewState() - TheState = state +// TestByteOffsetToLineCol_Logical verifies that the current logic +// only handles logical lines (newline-delimited). +func TestByteOffsetToLineCol_Logical(t *testing.T) { + buf := "hello\nworld" + // 012345 67890 - // Set to EditorPage - state.page = EditorPage - - elems := EditorLayout(ui.Dp(800), ui.Dp(600), false) - - cursorFound := false - for _, e := range elems { - // Assuming we add a Type() method to ui.Element interface later. - // For now, this will fail to compile if Type() doesn't exist. - // Let's assume we can cast or inspect the element. - if e.Type() == "cursor" { - cursorFound = true - break - } + // Logical line 0: "hello" (0-4), pos 5 is '\n', line 1 starts at 6 + line, col := byteOffsetToLineCol(buf, 5) + if line != 0 || col != 5 { + t.Errorf("expected (0, 5) for pos 5, got (%d, %d)", line, col) } - - if !cursorFound { - t.Error("Expected cursor element in EditorLayout") - } -} - -// TestKeyDownMove verifies key down events update CursorPosition. -func TestKeyDownMove(t *testing.T) { - state := NewState() - TheState = state - state.Editor.Buffer = "Hello" - state.Editor.CursorPosition = 1 - - // Simulate Right Arrow - HandleKeyDown("Right") - if state.Editor.CursorPosition != 2 { - t.Errorf("Expected cursor position 2, got %d", state.Editor.CursorPosition) - } - - // Simulate Left Arrow - HandleKeyDown("Left") - if state.Editor.CursorPosition != 1 { - t.Errorf("Expected cursor position 1, got %d", state.Editor.CursorPosition) + + line, col = byteOffsetToLineCol(buf, 6) + if line != 1 || col != 0 { + t.Errorf("expected (1, 0) for pos 6, got (%d, %d)", line, col) } } diff --git a/internal/editor/state.go b/internal/editor/state.go index e0a1b9a..573842d 100644 --- a/internal/editor/state.go +++ b/internal/editor/state.go @@ -197,19 +197,40 @@ func HandleCursorMove(delta int) { TheState.Editor.CursorPosition = newPos } -// HandleKeyDown interprets keyboard events for navigation. +// HandleKeyDown interprets keyboard events for navigation and editing. +// Receives both key.Event (as key.Name) and key.EditEvent from the main loop. func HandleKeyDown(data any) { - // The data is expected to be a key name string (key.Name) - keyName := data.(key.Name) - log.Printf("HandleKeyDown: %s", keyName) - switch keyName { - case key.NameLeftArrow: - HandleCursorMove(-1) - case key.NameRightArrow: - HandleCursorMove(1) + switch v := data.(type) { + case key.EditEvent: + // Text input from IME / keyboard + log.Printf("HandleKeyDown: EditEvent text=%q", v.Text) + HandleInsert(v.Text) + case key.Name: + log.Printf("HandleKeyDown: %s", v) + switch v { + case key.NameLeftArrow: + HandleCursorMove(-1) + case key.NameRightArrow: + HandleCursorMove(1) + case key.NameDeleteBackward: + HandleBackspace() + case key.NameDeleteForward: + HandleDelete() + } } } +// HandleDelete removes the character after the cursor. +func HandleDelete() { + pos := TheState.Editor.CursorPosition + buf := TheState.Editor.Buffer + if pos >= len(buf) { + return + } + TheState.Editor.Buffer = buf[:pos] + buf[pos+1:] + TheState.Editor.Dirty = true +} + // HandleInsert inserts a string at the current cursor position. func HandleInsert(s string) { pos := TheState.Editor.CursorPosition