Bug fixes.

This commit is contained in:
Greg Pomerantz 2026-06-03 10:38:29 -04:00
parent 0dac354f2a
commit bb55b4ab81
5 changed files with 144 additions and 57 deletions

View File

@ -1,4 +1,9 @@
This file lists descriptions of outstanding bugs. Bugs are separated by empty lines. 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 If I move the cursor (using the arrow keys) to the right side of the screen in a word-wrapped
delivers teh wrong file to the editor page. 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.

View File

@ -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. // HandleBrowserTap processes a tap on the ListView at the given index.
func HandleBrowserTap(bm *BrowserManager, s *BrowserState, index int) { 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 return
} }
entry, ok := getEntryByIndex(s, index) entry, ok := getEntryByIndex(s, entryIndex)
if !ok { if !ok {
return // Page not loaded return // Page not loaded
} }
@ -76,9 +92,11 @@ func HandleBrowserTap(bm *BrowserManager, s *BrowserState, index int) {
bm.NavigateTo(newPath) bm.NavigateTo(newPath)
} else { } else {
// Open file // Open file
s.SelectedIndex = index s.SelectedIndex = entryIndex
fmt.Printf("HandleBrowserTap: Opening file %s\n", entry.Path) fmt.Printf("HandleBrowserTap: Opening file %s\n", entry.Path)
if ui.OpenFile != nil {
ui.OpenFile(entry.Path) ui.OpenFile(entry.Path)
}
// Dispatch ReadFileTask to the worker pool // Dispatch ReadFileTask to the worker pool
task := pool.NewReadFileTask(entry.Path, bm.fs) task := pool.NewReadFileTask(entry.Path, bm.fs)
fmt.Printf("HandleBrowserTap: Dispatching ReadFileTask for %s\n", entry.Path) fmt.Printf("HandleBrowserTap: Dispatching ReadFileTask for %s\n", entry.Path)

View File

@ -58,3 +58,75 @@ func TestTapUp(t *testing.T) {
t.Errorf("expected CurrentPath='/root', got %s", state.CurrentPath) 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)
}
}

View File

@ -2,51 +2,22 @@ package editor
import ( import (
"testing" "testing"
"pad/internal/ui"
) )
// TestCursorExistsInLayout checks if the EditorLayout produces a cursor element. // TestByteOffsetToLineCol_Logical verifies that the current logic
func TestCursorExistsInLayout(t *testing.T) { // only handles logical lines (newline-delimited).
state := NewState() func TestByteOffsetToLineCol_Logical(t *testing.T) {
TheState = state buf := "hello\nworld"
// 012345 67890
// Set to EditorPage // Logical line 0: "hello" (0-4), pos 5 is '\n', line 1 starts at 6
state.page = EditorPage line, col := byteOffsetToLineCol(buf, 5)
if line != 0 || col != 5 {
elems := EditorLayout(ui.Dp(800), ui.Dp(600), false) t.Errorf("expected (0, 5) for pos 5, got (%d, %d)", line, col)
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
}
} }
if !cursorFound { line, col = byteOffsetToLineCol(buf, 6)
t.Error("Expected cursor element in EditorLayout") if line != 1 || col != 0 {
} t.Errorf("expected (1, 0) for pos 6, got (%d, %d)", line, col)
}
// 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)
} }
} }

View File

@ -197,17 +197,38 @@ func HandleCursorMove(delta int) {
TheState.Editor.CursorPosition = newPos 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) { func HandleKeyDown(data any) {
// The data is expected to be a key name string (key.Name) switch v := data.(type) {
keyName := data.(key.Name) case key.EditEvent:
log.Printf("HandleKeyDown: %s", keyName) // Text input from IME / keyboard
switch keyName { log.Printf("HandleKeyDown: EditEvent text=%q", v.Text)
HandleInsert(v.Text)
case key.Name:
log.Printf("HandleKeyDown: %s", v)
switch v {
case key.NameLeftArrow: case key.NameLeftArrow:
HandleCursorMove(-1) HandleCursorMove(-1)
case key.NameRightArrow: case key.NameRightArrow:
HandleCursorMove(1) 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. // HandleInsert inserts a string at the current cursor position.