diff --git a/fuzzylist.go b/fuzzylist.go index dad9591..ae9039c 100644 --- a/fuzzylist.go +++ b/fuzzylist.go @@ -40,10 +40,11 @@ type scoredItem struct { // separators, and rendering live here once (and stay reusable for future // pickers). type fuzzyList struct { - input textinput.Model - items []listItem - filtered []scoredItem - cursor int + input textinput.Model + items []listItem + filtered []scoredItem + cursor int + lastQuery string } // newFuzzyList builds a list over items with a focused, empty query box. @@ -65,6 +66,10 @@ func newFuzzyList(placeholder string, items []listItem) fuzzyList { // matches that land inside the name. func (l *fuzzyList) filter() { q := strings.TrimSpace(l.input.Value()) + if q != l.lastQuery { + l.cursor = 0 + l.lastQuery = q + } l.filtered = l.filtered[:0] if q == "" { diff --git a/fuzzylist_test.go b/fuzzylist_test.go index b8ea117..d35057d 100644 --- a/fuzzylist_test.go +++ b/fuzzylist_test.go @@ -159,3 +159,49 @@ func TestFuzzyListClickRow(t *testing.T) { t.Fatalf("a missed click moved the cursor: ref = %d, want 1", got) } } + +// TestFuzzyListQueryChangeResetsCursor verifies that when the query changes, +// the cursor resets to 0 (the top matched item), preventing the issue where +// starting with a separator (which defaults the initial cursor to index 1) +// causes the second matching item (index 1 of filtered) to be highlighted +// instead of the first matched item (index 0 of filtered). +func TestFuzzyListQueryChangeResetsCursor(t *testing.T) { + items := []listItem{ + {name: "Group Heading", selectable: false}, + {name: "alpha", selectable: true, ref: 1}, + {name: "bravo", selectable: true, ref: 2}, + } + l := newFuzzyList("", items) + + // Since index 0 is a heading, the initial cursor must skip it and land on index 1 ("alpha"). + if got := l.cursor; got != 1 { + t.Fatalf("initial cursor = %d, want 1", got) + } + if got := l.selectedIndex(); got != 1 { + t.Fatalf("initial selectedIndex = %d, want 1 (alpha)", got) + } + + // Change query to "a" so "alpha" and "bravo" both match. + l.input.SetValue("a") + l.filter() + + // Since the query changed, the cursor should have been reset to 0, which points to "alpha". + if got := l.cursor; got != 0 { + t.Fatalf("cursor after typing 'a' = %d, want 0", got) + } + if got := l.selectedIndex(); got != 1 { + t.Fatalf("selectedIndex after typing 'a' = %d, want 1 (alpha)", got) + } + + // Clear query back to "" + l.input.SetValue("") + l.filter() + + // Cursor should go back to 1 (skipping heading). + if got := l.cursor; got != 1 { + t.Fatalf("cursor after clearing query = %d, want 1", got) + } + if got := l.selectedIndex(); got != 1 { + t.Fatalf("selectedIndex after clearing query = %d, want 1 (alpha)", got) + } +}