fix(tui): clear @-picker filter when browsing into/out of a directory

In flat (non-recursive) mode, typing a filter to locate a directory and
then opening it with Right re-applied that same filter inside the
directory. Typing "@eda" then Right to open eda/ showed nothing,
because no child of eda/ matches "eda". The filter the user typed
selected the directory at the current level; it has no meaning one
level deeper.

Clear the text after the last "@" (keeping the bare "@" so the picker
stays open) whenever Right or Left successfully changes the browse
level. The filter was scoped to the level just left, so dropping it
shows the new directory's full contents.

Adds a regression test that opens eda/ after an "@eda" filter and
asserts the directory's contents are listed while the stale filter
would have matched nothing.
This commit is contained in:
Raymond Gasper 2026-06-10 09:41:35 -04:00
parent 1a3e0a572e
commit 4a8d2ed68e
2 changed files with 73 additions and 4 deletions

View file

@ -176,6 +176,54 @@ func TestFileSuggesterFuzzyMatch(t *testing.T) {
}
}
// TestFileSuggesterFlatBrowseIntoDirIgnoresStaleFilter reproduces the
// reported bug: in flat (non-recursive) mode, typing "@eda" to find a
// directory then opening it with Right must show that directory's
// contents, not re-apply the "eda" filter inside it (which matches
// nothing). The interactive layer clears the @-query when descending,
// so here we model that by browsing with Right and then matching an
// empty query against the new level.
func TestFileSuggesterFlatBrowseIntoDirIgnoresStaleFilter(t *testing.T) {
tmp := t.TempDir()
// eda/rjg/enk-1150 with a file inside, plus a sibling so the filter
// is meaningful at the top level.
if err := os.MkdirAll(filepath.Join(tmp, "eda", "rjg", "enk-1150"), 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(tmp, "eda", "rjg", "enk-1150", "pipeline.py"), []byte("x"), 0o644); err != nil {
t.Fatal(err)
}
if err := os.MkdirAll(filepath.Join(tmp, "unrelated"), 0o755); err != nil {
t.Fatal(err)
}
s := newFileSuggester()
s.SetCWD(tmp) // flat mode
// Top level: "@eda" highlights the eda/ directory. Render populates
// lastMatches, which Right/Left act on (it runs every frame before
// key handling in the live UI).
s.lastMatches = s.matches("@eda")
if !containsEntry(s.lastMatches, "eda", true) {
t.Fatalf("@eda did not match eda/: %#v", s.lastMatches)
}
s.cursor = 0 // eda/ is the (only) match, selected.
// Open it. After the interactive layer clears the query, the picker
// is browsing eda/ with an empty filter and must show rjg/.
if !s.Right() {
t.Fatal("Right() did not open eda/")
}
if got := s.matches("@"); !containsEntry(got, "rjg", true) {
t.Fatalf("after opening eda/, empty filter did not show rjg/: %#v", got)
}
// The stale filter would have shown nothing: confirm that's the
// behavior the fix avoids.
if got := s.matches("@eda"); len(got) != 0 {
t.Fatalf("stale @eda filter inside eda/ unexpectedly matched: %#v", got)
}
}
// TestFileSuggesterRecursiveMatch verifies recursive mode flattens the
// tree and matches against the cwd-relative path, so a pattern can
// span directory boundaries.

View file

@ -1605,6 +1605,18 @@ func (i *Interactive) ctrlCExitArmed() bool {
return !t.IsZero() && time.Since(t) <= ctrlCExitWindow
}
// clearFileSuggestQuery strips the filter the user typed after the
// last "@", leaving the bare "@" so the picker stays open. Called when
// navigating between directory levels (Right/Left): the filter applied
// to the level the user was on, not the one being entered, so carrying
// it forward would wrongly hide the new directory's contents.
func (i *Interactive) clearFileSuggestQuery() {
val := i.ed.Value()
if idx := strings.LastIndex(val, "@"); idx >= 0 {
i.ed.SetValue(val[:idx+1])
}
}
func (i *Interactive) handleKey(ctx context.Context, k tui.Key) (done bool) {
// Any key that isn't ctrl+c invalidates an armed ctrl+c-exit, so
// pressing ctrl+c then typing then ctrl+c much later doesn't quit
@ -2083,12 +2095,21 @@ func (i *Interactive) handleKey(ctx context.Context, k tui.Key) (done bool) {
i.fileSuggest.Down()
return false
case tui.KeyRight:
// Open selected directory.
i.fileSuggest.Right()
// Open selected directory. The filter the user typed picked
// that directory at the current level; once we descend it no
// longer applies to the directory's contents, so clear it.
// Otherwise typing "@eda" then right would re-filter inside
// eda/ by "eda" and show nothing.
if i.fileSuggest.Right() {
i.clearFileSuggestQuery()
}
return false
case tui.KeyLeft:
// Go back to parent directory.
i.fileSuggest.Left()
// Go back to parent directory. Clear the filter for the same
// reason as Right: it was scoped to the level we just left.
if i.fileSuggest.Left() {
i.clearFileSuggestQuery()
}
return false
case tui.KeyEnter:
if entry, ok := i.fileSuggest.SelectedEntry(i.ed.Value()); ok {