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.
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.

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.
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)
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)

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -197,17 +197,38 @@ 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 {
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.