From a292b299e74ba280154a8eac5b5fb660bf4ff124 Mon Sep 17 00:00:00 2001 From: James Trew <66286082+jamestrew@users.noreply.github.com> Date: Wed, 22 Oct 2025 14:18:01 -0400 Subject: [PATCH] perf(list): optimize filter performance and limit results (#1193) --- internal/tui/components/chat/editor/editor.go | 2 + .../tui/components/completions/completions.go | 2 + internal/tui/exp/list/filterable.go | 47 +++++++++++-------- 3 files changed, 32 insertions(+), 19 deletions(-) diff --git a/internal/tui/components/chat/editor/editor.go b/internal/tui/components/chat/editor/editor.go index f70a0a3dbe63a9473f552efa233e03bd4efc0ee1..9ccd453d3f6f200c43012b61a7545fb3c08a4e6a 100644 --- a/internal/tui/components/chat/editor/editor.go +++ b/internal/tui/components/chat/editor/editor.go @@ -86,6 +86,7 @@ var DeleteKeyMaps = DeleteAttachmentKeyMaps{ const ( maxAttachments = 5 + maxFileResults = 25 ) type OpenEditorMsg struct { @@ -500,6 +501,7 @@ func (m *editorCmp) startCompletions() tea.Msg { Completions: completionItems, X: x, Y: y, + MaxResults: maxFileResults, } } diff --git a/internal/tui/components/completions/completions.go b/internal/tui/components/completions/completions.go index ae3c233e4f21b089f59b7effb88ddc3300277d16..1d8a0a854197d3b7ba9e26426bde8a2679f79573 100644 --- a/internal/tui/components/completions/completions.go +++ b/internal/tui/components/completions/completions.go @@ -22,6 +22,7 @@ type OpenCompletionsMsg struct { Completions []Completion X int // X position for the completions popup Y int // Y position for the completions popup + MaxResults int // Maximum number of results to render, 0 for no limit } type FilterCompletionsMsg struct { @@ -192,6 +193,7 @@ func (c *completionsCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } c.width = width c.height = max(min(maxCompletionsHeight, len(items)), 1) // Ensure at least 1 item height + c.list.SetResultsSize(msg.MaxResults) return c, tea.Batch( c.list.SetItems(items), c.list.SetSize(c.width, c.height), diff --git a/internal/tui/exp/list/filterable.go b/internal/tui/exp/list/filterable.go index e639786db5777aaeda237e959dffe36d9c6a7583..d5c47b01083cdc1becaed9aac4fb8a5d3e9f3b47 100644 --- a/internal/tui/exp/list/filterable.go +++ b/internal/tui/exp/list/filterable.go @@ -3,8 +3,6 @@ package list import ( "regexp" "slices" - "sort" - "strings" "github.com/charmbracelet/bubbles/v2/key" "github.com/charmbracelet/bubbles/v2/textinput" @@ -28,7 +26,9 @@ type FilterableList[T FilterableItem] interface { Cursor() *tea.Cursor SetInputWidth(int) SetInputPlaceholder(string) + SetResultsSize(int) Filter(q string) tea.Cmd + fuzzy.Source } type HasMatchIndexes interface { @@ -47,10 +47,11 @@ type filterableList[T FilterableItem] struct { *filterableOptions width, height int // stores all available items - items []T - input textinput.Model - inputWidth int - query string + items []T + resultsSize int + input textinput.Model + inputWidth int + query string } type filterableListOption func(*filterableOptions) @@ -246,22 +247,18 @@ func (f *filterableList[T]) Filter(query string) tea.Cmd { return f.list.SetItems(f.items) } - words := make([]string, len(f.items)) - for i, item := range f.items { - words[i] = strings.ToLower(item.FilterValue()) - } - - matches := fuzzy.Find(query, words) - - sort.SliceStable(matches, func(i, j int) bool { - return matches[i].Score > matches[j].Score - }) + matches := fuzzy.FindFrom(query, f) var matchedItems []T - for _, match := range matches { + resultSize := len(matches) + if f.resultsSize > 0 && resultSize > f.resultsSize { + resultSize = f.resultsSize + } + for i := range resultSize { + match := matches[i] item := f.items[match.Index] - if i, ok := any(item).(HasMatchIndexes); ok { - i.MatchIndexes(match.MatchedIndexes) + if it, ok := any(item).(HasMatchIndexes); ok { + it.MatchIndexes(match.MatchedIndexes) } matchedItems = append(matchedItems, item) } @@ -307,3 +304,15 @@ func (f *filterableList[T]) SetInputWidth(w int) { func (f *filterableList[T]) SetInputPlaceholder(ph string) { f.placeholder = ph } + +func (f *filterableList[T]) SetResultsSize(size int) { + f.resultsSize = size +} + +func (f *filterableList[T]) String(i int) string { + return f.items[i].FilterValue() +} + +func (f *filterableList[T]) Len() int { + return len(f.items) +}