@@ -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)
}
@@ -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)