diff --git a/internal/tui/exp/list/filterable_group.go b/internal/tui/exp/list/filterable_group.go index 1abda8d4cf97e1ef5a1842f3c3eb62fa244920d4..c130034726b4ccca7eb2293ec6869a105b5a92c3 100644 --- a/internal/tui/exp/list/filterable_group.go +++ b/internal/tui/exp/list/filterable_group.go @@ -193,6 +193,66 @@ func (f *filterableGroupList[T]) clearItemState() []tea.Cmd { return cmds } +func (f *filterableGroupList[T]) getGroupName(g Group[T]) string { + if section, ok := g.Section.(*itemSectionModel); ok { + return strings.ToLower(section.title) + } + return strings.ToLower(g.Section.ID()) +} + +func (f *filterableGroupList[T]) setMatchIndexes(item T, indexes []int) { + if i, ok := any(item).(HasMatchIndexes); ok { + i.MatchIndexes(indexes) + } +} + +func (f *filterableGroupList[T]) filterItemsInGroup(group Group[T], query string) []T { + if query == "" { + // No query, return all items with cleared match indexes + var items []T + for _, item := range group.Items { + f.setMatchIndexes(item, make([]int, 0)) + items = append(items, item) + } + return items + } + + name := f.getGroupName(group) + " " + + names := make([]string, len(group.Items)) + for i, item := range group.Items { + names[i] = strings.ToLower(name + item.FilterValue()) + } + + matches := fuzzy.Find(query, names) + sort.SliceStable(matches, func(i, j int) bool { + return matches[i].Score > matches[j].Score + }) + + if len(matches) > 0 { + var matchedItems []T + for _, match := range matches { + item := group.Items[match.Index] + var idxs []int + for _, idx := range match.MatchedIndexes { + // adjusts removing group name highlights + if idx < len(name) { + continue + } + idxs = append(idxs, idx-len(name)) + } + f.setMatchIndexes(item, idxs) + matchedItems = append(matchedItems, item) + } + return matchedItems + } + + return []T{} +} + +func (f *filterableGroupList[T]) Filter(query string) tea.Cmd { + cmds := f.clearItemState() + if query == "" { return f.groupedList.SetGroups(f.groups) } diff --git a/internal/tui/exp/list/list.go b/internal/tui/exp/list/list.go index 9429ccec4cd6d6d54af90bdac4a2f42006dadeae..a86562efdae2cc1aebb335f18c85cd3bb15bcc7d 100644 --- a/internal/tui/exp/list/list.go +++ b/internal/tui/exp/list/list.go @@ -15,7 +15,6 @@ import ( "github.com/charmbracelet/lipgloss/v2" uv "github.com/charmbracelet/ultraviolet" "github.com/charmbracelet/x/ansi" - "github.com/charmbracelet/x/exp/ordered" "github.com/rivo/uniseg" ) @@ -556,7 +555,7 @@ func (l *list[T]) viewPosition() (int, int) { // Ensure offset doesn't exceed the maximum valid offset maxOffset := max(0, l.virtualHeight-l.height) actualOffset := min(l.offset, maxOffset) - + start = actualOffset if l.virtualHeight > 0 { end = min(actualOffset+l.height-1, l.virtualHeight-1) @@ -569,7 +568,7 @@ func (l *list[T]) viewPosition() (int, int) { // Ensure offset doesn't exceed the maximum valid offset maxOffset := max(0, l.virtualHeight-l.height) actualOffset := min(l.offset, maxOffset) - + end = l.virtualHeight - actualOffset - 1 start = max(0, end-l.height+1) } else { @@ -1031,10 +1030,10 @@ func (l *list[T]) renderVirtualScrolling() string { linesAdded := 0 maxLinesToAdd := len(itemLines) - startLine for i := 0; i < maxLinesToAdd && len(lines) < l.height; i++ { - lines = append(lines, itemLines[startLine + i]) + lines = append(lines, itemLines[startLine+i]) linesAdded++ } - + // Update currentLine to track our position in virtual space if vis.pos.start < viewStart { // Item started before viewport, we're now at viewStart + linesAdded @@ -1047,7 +1046,7 @@ func (l *list[T]) renderVirtualScrolling() string { // For content that fits entirely in viewport, don't pad with empty lines // Only pad if we have scrolled or if content is larger than viewport - + if l.virtualHeight > l.height || l.offset > 0 { // Fill remaining viewport with empty lines if needed initialLen := len(lines)