@@ -180,7 +180,12 @@ func (f *filterableGroupList[T]) inputHeight() int {
return lipgloss.Height(f.inputStyle.Render(f.input.View()))
}
-func (f *filterableGroupList[T]) Filter(query string) tea.Cmd {
+type groupMatch[T FilterableItem] struct {
+ group Group[T]
+ score int
+}
+
+func (f *filterableGroupList[T]) clearItemState() []tea.Cmd {
var cmds []tea.Cmd
for _, item := range slices.Collect(f.items.Seq()) {
if i, ok := any(item).(layout.Focusable); ok {
@@ -190,67 +195,135 @@ func (f *filterableGroupList[T]) Filter(query string) tea.Cmd {
i.MatchIndexes(make([]int, 0))
}
}
+ return cmds
+}
- f.selectedItem = ""
+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]) findMatchingGroups(firstWord string) []groupMatch[T] {
+ var matchedGroups []groupMatch[T]
+ for _, g := range f.groups {
+ groupName := f.getGroupName(g)
+ matches := fuzzy.Find(firstWord, []string{groupName})
+ if len(matches) > 0 && matches[0].Score > 0 {
+ matchedGroups = append(matchedGroups, groupMatch[T]{
+ group: g,
+ score: matches[0].Score,
+ })
+ }
+ }
+ // Sort by score (higher scores first - exact matches will have higher scores)
+ sort.SliceStable(matchedGroups, func(i, j int) bool {
+ return matchedGroups[i].score > matchedGroups[j].score
+ })
+ return matchedGroups
+}
+
+func (f *filterableGroupList[T]) filterItemsInGroup(group Group[T], query string) []T {
if query == "" {
- return f.groupedList.SetGroups(f.groups)
+ // 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
}
+ // Build search words
+ words := make([]string, len(group.Items))
+ for i, item := range group.Items {
+ words[i] = strings.ToLower(item.FilterValue())
+ }
+
+ // Perform fuzzy search
+ matches := fuzzy.Find(query, words)
+ sort.SliceStable(matches, func(i, j int) bool {
+ return matches[i].Score > matches[j].Score
+ })
+
+ if len(matches) > 0 {
+ // Found matches, return only those with highlights
+ var matchedItems []T
+ for _, match := range matches {
+ item := group.Items[match.Index]
+ f.setMatchIndexes(item, match.MatchedIndexes)
+ matchedItems = append(matchedItems, item)
+ }
+ return matchedItems
+ }
+
+ // No matches, return all items without highlights
+ var allItems []T
+ for _, item := range group.Items {
+ f.setMatchIndexes(item, make([]int, 0))
+ allItems = append(allItems, item)
+ }
+ return allItems
+}
+
+func (f *filterableGroupList[T]) searchAllGroups(query string) []Group[T] {
var newGroups []Group[T]
for _, g := range f.groups {
- // Check if group name matches the query
- // Extract the group name from the section - we'll use the section's view content
- // as a fallback since ItemSection doesn't implement FilterableItem
- var groupName string
- if section, ok := g.Section.(*itemSectionModel); ok {
- groupName = strings.ToLower(section.title)
- } else {
- // Fallback to using the section's ID or view content
- groupName = strings.ToLower(g.Section.ID())
- }
- groupMatches := fuzzy.Find(query, []string{groupName})
-
- if len(groupMatches) > 0 && groupMatches[0].Score > 0 {
- // If group name matches, include all items from this group
- // Clear any existing match indexes for items since the group matched
- for _, item := range g.Items {
- if i, ok := any(item).(HasMatchIndexes); ok {
- i.MatchIndexes(make([]int, 0))
- }
- }
+ matchedItems := f.filterItemsInGroup(g, query)
+ if len(matchedItems) > 0 {
newGroups = append(newGroups, Group[T]{
Section: g.Section,
- Items: g.Items,
+ Items: matchedItems,
})
- } else {
- // Group name doesn't match, check individual items
- words := make([]string, len(g.Items))
- for i, item := range g.Items {
- words[i] = strings.ToLower(item.FilterValue())
- }
+ }
+ }
+ return newGroups
+}
- matches := fuzzy.Find(query, words)
+func (f *filterableGroupList[T]) Filter(query string) tea.Cmd {
+ cmds := f.clearItemState()
+ f.selectedItem = ""
- sort.SliceStable(matches, func(i, j int) bool {
- return matches[i].Score > matches[j].Score
- })
+ if query == "" {
+ return f.groupedList.SetGroups(f.groups)
+ }
- var matchedItems []T
- for _, match := range matches {
- item := g.Items[match.Index]
- if i, ok := any(item).(HasMatchIndexes); ok {
- i.MatchIndexes(match.MatchedIndexes)
- }
- matchedItems = append(matchedItems, item)
- }
+ lowerQuery := strings.ToLower(query)
+ queryWords := strings.Fields(lowerQuery)
+ firstWord := queryWords[0]
+
+ // Find groups that match the first word
+ matchedGroups := f.findMatchingGroups(firstWord)
+
+ var newGroups []Group[T]
+ if len(matchedGroups) > 0 {
+ // Filter within matched groups using remaining words
+ remainingQuery := ""
+ if len(queryWords) > 1 {
+ remainingQuery = strings.Join(queryWords[1:], " ")
+ }
+
+ for _, matchedGroup := range matchedGroups {
+ matchedItems := f.filterItemsInGroup(matchedGroup.group, remainingQuery)
if len(matchedItems) > 0 {
newGroups = append(newGroups, Group[T]{
- Section: g.Section,
+ Section: matchedGroup.group.Section,
Items: matchedItems,
})
}
}
+ } else {
+ // No group matches, search all groups
+ newGroups = f.searchAllGroups(lowerQuery)
}
+
cmds = append(cmds, f.groupedList.SetGroups(newGroups))
return tea.Batch(cmds...)
}