@@ -2,16 +2,21 @@ package list
22
33import (
44 "slices"
5+ "sort"
56 "strings"
67
78 "github.com/charmbracelet/bubbles/v2/help"
89 "github.com/charmbracelet/bubbles/v2/key"
910 "github.com/charmbracelet/bubbles/v2/spinner"
11+ "github.com/charmbracelet/bubbles/v2/textinput"
1012 tea "github.com/charmbracelet/bubbletea/v2"
1113 "github.com/charmbracelet/lipgloss/v2"
1214 "github.com/opencode-ai/opencode/internal/tui/components/anim"
1315 "github.com/opencode-ai/opencode/internal/tui/layout"
16+ "github.com/opencode-ai/opencode/internal/tui/styles"
17+ "github.com/opencode-ai/opencode/internal/tui/theme"
1418 "github.com/opencode-ai/opencode/internal/tui/util"
19+ "github.com/sahilm/fuzzy"
1520)
1621
1722// Constants for special index values and defaults
@@ -43,6 +48,18 @@ type HasAnim interface {
4348 Spinning() bool // Returns true if the item is currently animating
4449}
4550
51+// HasFilterValue interface allows items to provide a filter value for searching.
52+type HasFilterValue interface {
53+ util.Model
54+ FilterValue() string // Returns a string value used for filtering/searching
55+}
56+
57+// HasMatchIndexes interface allows items to set matched character indexes.
58+type HasMatchIndexes interface {
59+ util.Model
60+ MatchIndexes([]int) // Sets the indexes of matched characters in the item's content
61+}
62+
4663// renderedItem represents a cached rendered item with its position and content.
4764type renderedItem struct {
4865 lines []string // The rendered lines of text for this item
@@ -105,10 +122,15 @@ type model struct {
105122 renderState *renderState // Rendering cache and state
106123 selectionState selectionState // Item selection state
107124 help help.Model // Help system for keyboard shortcuts
108− keymap KeyMap // Key bindings for navigation
109− items []util.Model // The actual list items
125+ keyMap KeyMap // Key bindings for navigation
126+ allItems []util.Model // The actual list items
110127 gapSize int // Number of empty lines between items
111128 padding []int // Padding around the list content
129+
130+ filterable bool // Whether items can be filtered
131+ filteredItems []util.Model // Filtered items based on current search
132+ input textinput.Model // Input field for filtering items
133+ currentSearch string // Current search term for filtering
112134}
113135
114136// listOptions is a function type for configuring list options.
@@ -117,7 +139,7 @@ type listOptions func(*model)
117139// WithKeyMap sets custom key bindings for the list.
118140func WithKeyMap(k KeyMap) listOptions {
119141 return func(m *model) {
120− m.keymap = k
142+ m.keyMap = k
121143 }
122144}
123145
@@ -147,7 +169,15 @@ func WithPadding(padding ...int) listOptions {
147169// WithItems sets the initial items for the list.
148170func WithItems(items []util.Model) listOptions {
149171 return func(m *model) {
150− m.items = items
172+ m.allItems = items
173+ m.filteredItems = items // Initially, all items are visible
174+ }
175+}
176+
177+// WithFilterable enables filtering of items based on their FilterValue.
178+func WithFilterable(filterable bool) listOptions {
179+ return func(m *model) {
180+ m.filterable = filterable
151181 }
152182}
153183
@@ -157,8 +187,9 @@ func WithItems(items []util.Model) listOptions {
157187func New(opts ...listOptions) ListModel {
158188 m := &model{
159189 help: help.New(),
160− keymap: DefaultKeymap(),
161− items: []util.Model{},
190+ keyMap: DefaultKeyMap(),
191+ allItems: []util.Model{},
192+ filteredItems: []util.Model{},
162193 renderState: newRenderState(),
163194 gapSize: DefaultGapSize,
164195 padding: []int{},
@@ -167,13 +198,25 @@ func New(opts ...listOptions) ListModel {
167198 for _, opt := range opts {
168199 opt(m)
169200 }
201+
202+ if m.filterable {
203+ ti := textinput.New()
204+ ti.Placeholder = "Type to filter..."
205+ ti.SetVirtualCursor(false)
206+ ti.Focus()
207+ m.input = ti
208+
209+ // disable j,k movements
210+ m.keyMap.NDown.SetEnabled(false)
211+ m.keyMap.NUp.SetEnabled(false)
212+ }
170213 return m
171214}
172215
173216// Init initializes the list component and sets up the initial items.
174217// This is called automatically by the Bubble Tea framework.
175218func (m *model) Init() tea.Cmd {
176− return m.SetItems(m.items)
219+ return m.SetItems(m.filteredItems)
177220}
178221
179222// Update handles incoming messages and updates the list state accordingly.
@@ -186,34 +229,78 @@ func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
186229 case anim.ColorCycleMsg, anim.StepCharsMsg, spinner.TickMsg:
187230 return m.handleAnimationMsg(msg)
188231 }
189−
190− if m.selectionState.isValidIndex(len(m.items)) {
232+ if m.selectionState.isValidIndex(len(m.filteredItems)) {
191233 return m.updateSelectedItem(msg)
192234 }
193235
194236 return m, nil
195237}
196238
239+// View renders the list to a string for display.
240+// Returns empty string if the list has no dimensions.
241+// Triggers re-rendering if needed before returning content.
242+func (m *model) View() tea.View {
243+ if m.viewState.height == 0 || m.viewState.width == 0 {
244+ return tea.NewView("") // No content to display
245+ }
246+ if m.renderState.needsRerender {
247+ m.renderVisible()
248+ }
249+
250+ content := lipgloss.NewStyle().
251+ Padding(m.padding...).
252+ Height(m.viewState.height).
253+ Render(m.viewState.content)
254+
255+ if m.filterable {
256+ content = lipgloss.JoinVertical(
257+ lipgloss.Left,
258+ m.inputStyle().Render(m.input.View()),
259+ content,
260+ )
261+ }
262+ view := tea.NewView(content)
263+ if m.filterable {
264+ view.SetCursor(m.input.Cursor())
265+ }
266+ return view
267+}
268+
197269// handleKeyPress processes keyboard input for list navigation.
198270// Supports scrolling, item selection, and navigation to top/bottom.
199271func (m *model) handleKeyPress(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) {
200272 switch {
201− case key.Matches(msg, m.keymap.Down) || key.Matches(msg, m.keymap.NDown):
273+ case key.Matches(msg, m.keyMap.Down) || key.Matches(msg, m.keyMap.NDown):
202274 m.scrollDown(1)
203− case key.Matches(msg, m.keymap.Up) || key.Matches(msg, m.keymap.NUp):
275+ case key.Matches(msg, m.keyMap.Up) || key.Matches(msg, m.keyMap.NUp):
204276 m.scrollUp(1)
205− case key.Matches(msg, m.keymap.DownOneItem):
277+ case key.Matches(msg, m.keyMap.DownOneItem):
206278 return m, m.selectNextItem()
207− case key.Matches(msg, m.keymap.UpOneItem):
279+ case key.Matches(msg, m.keyMap.UpOneItem):
208280 return m, m.selectPreviousItem()
209− case key.Matches(msg, m.keymap.HalfPageDown):
281+ case key.Matches(msg, m.keyMap.HalfPageDown):
210282 m.scrollDown(m.listHeight() / 2)
211− case key.Matches(msg, m.keymap.HalfPageUp):
283+ case key.Matches(msg, m.keyMap.HalfPageUp):
212284 m.scrollUp(m.listHeight() / 2)
213− case key.Matches(msg, m.keymap.Home):
285+ case key.Matches(msg, m.keyMap.Home):
214286 return m, m.goToTop()
215− case key.Matches(msg, m.keymap.End):
287+ case key.Matches(msg, m.keyMap.End):
216288 return m, m.goToBottom()
289+ default:
290+ if !m.filterable {
291+ return m, nil // Ignore other keys if not filterable
292+ }
293+ var cmds []tea.Cmd
294+ u, cmd := m.input.Update(msg)
295+ m.input = u
296+ cmds = append(cmds, cmd)
297+ if m.currentSearch != m.input.Value() {
298+ cmd = m.filter(m.input.Value())
299+ cmds = append(cmds, cmd)
300+ }
301+ m.currentSearch = m.input.Value()
302+ return m, tea.Batch(cmds...)
303+
217304 }
218305 return m, nil
219306}
@@ -222,7 +309,7 @@ func (m *model) handleKeyPress(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) {
222309// Only items implementing HasAnim and currently spinning receive these messages.
223310func (m *model) handleAnimationMsg(msg tea.Msg) (tea.Model, tea.Cmd) {
224311 var cmds []tea.Cmd
225− for inx, item := range m.items {
312+ for inx, item := range m.filteredItems {
226313 if i, ok := item.(HasAnim); ok && i.Spinning() {
227314 updated, cmd := i.Update(msg)
228315 cmds = append(cmds, cmd)
@@ -238,7 +325,7 @@ func (m *model) handleAnimationMsg(msg tea.Msg) (tea.Model, tea.Cmd) {
238325// This allows the selected item to handle its own input and state changes.
239326func (m *model) updateSelectedItem(msg tea.Msg) (tea.Model, tea.Cmd) {
240327 var cmds []tea.Cmd
241− u, cmd := m.items[m.selectionState.selectedIndex].Update(msg)
328+ u, cmd := m.filteredItems[m.selectionState.selectedIndex].Update(msg)
242329 cmds = append(cmds, cmd)
243330 if updated, ok := u.(util.Model); ok {
244331 m.UpdateItem(m.selectionState.selectedIndex, updated)
@@ -266,27 +353,9 @@ func (m *model) scrollUp(amount int) {
266353 }
267354}
268355
269−// View renders the list to a string for display.
270−// Returns empty string if the list has no dimensions.
271−// Triggers re-rendering if needed before returning content.
272−func (m *model) View() tea.View {
273− if m.viewState.height == 0 || m.viewState.width == 0 {
274− return tea.NewView("") // No content to display
275− }
276− if m.renderState.needsRerender {
277− m.renderVisible()
278− }
279− return tea.NewView(
280− lipgloss.NewStyle().
281− Padding(m.padding...).
282− Height(m.viewState.height).
283− Render(m.viewState.content),
284− )
285−}
286−
287356// Items returns a copy of all items in the list.
288357func (m *model) Items() []util.Model {
289− return m.items
358+ return m.filteredItems
290359}
291360
292361// renderVisible determines which rendering strategy to use and triggers rendering.
@@ -306,12 +375,12 @@ func (m *model) renderVisibleForward() {
306375 model: m,
307376 start: 0,
308377 cutoff: m.viewState.offset + m.listHeight(),
309− items: m.items,
378+ items: m.filteredItems,
310379 realIdx: m.renderState.lastIndex,
311380 }
312381
313382 if m.renderState.lastIndex > NotRendered {
314− renderer.items = m.items[m.renderState.lastIndex+1:]
383+ renderer.items = m.filteredItems[m.renderState.lastIndex+1:]
315384 renderer.start = len(m.renderState.lines)
316385 }
317386
@@ -326,16 +395,16 @@ func (m *model) renderVisibleReverse() {
326395 model: m,
327396 start: 0,
328397 cutoff: m.viewState.offset + m.listHeight(),
329− items: m.items,
398+ items: m.filteredItems,
330399 realIdx: m.renderState.lastIndex,
331400 }
332401
333402 if m.renderState.lastIndex > NotRendered {
334− renderer.items = m.items[:m.renderState.lastIndex]
403+ renderer.items = m.filteredItems[:m.renderState.lastIndex]
335404 renderer.start = len(m.renderState.lines)
336405 } else {
337− m.renderState.lastIndex = len(m.items)
338− renderer.realIdx = len(m.items)
406+ m.renderState.lastIndex = len(m.filteredItems)
407+ renderer.realIdx = len(m.filteredItems)
339408 }
340409
341410 renderer.render()
@@ -389,7 +458,7 @@ func (r *forwardRenderer) render() {
389458 }
390459
391460 itemLines := r.getOrRenderItem(item)
392− if r.realIdx == len(r.model.items)-1 {
461+ if r.realIdx == len(r.model.filteredItems)-1 {
393462 r.model.renderState.finalHeight = max(0, r.start+len(itemLines)-r.model.listHeight())
394463 }
395464
@@ -485,7 +554,7 @@ func (m *model) selectPreviousItem() tea.Cmd {
485554// selectNextItem moves selection to the next item in the list.
486555// Handles focus management and ensures the selected item remains visible.
487556func (m *model) selectNextItem() tea.Cmd {
488− if m.selectionState.selectedIndex >= len(m.items)-1 || m.selectionState.selectedIndex < 0 {
557+ if m.selectionState.selectedIndex >= len(m.filteredItems)-1 || m.selectionState.selectedIndex < 0 {
489558 return nil
490559 }
491560
@@ -543,7 +612,7 @@ func (m *model) ensureVisibleForward(cachedItem renderedItem) {
543612// Handles both large items (taller than viewport) and normal items.
544613func (m *model) ensureVisibleReverse(cachedItem renderedItem) {
545614 if cachedItem.height >= m.listHeight() {
546− if m.selectionState.selectedIndex < len(m.items)-1 {
615+ if m.selectionState.selectedIndex < len(m.filteredItems)-1 {
547616 changeNeeded := m.viewState.offset - (cachedItem.start + cachedItem.height - m.listHeight())
548617 m.decreaseOffset(changeNeeded)
549618 } else {
@@ -567,7 +636,7 @@ func (m *model) ensureVisibleReverse(cachedItem renderedItem) {
567636func (m *model) goToBottom() tea.Cmd {
568637 cmds := []tea.Cmd{m.blurSelected()}
569638 m.viewState.reverse = true
570− m.selectionState.selectedIndex = len(m.items) - 1
639+ m.selectionState.selectedIndex = len(m.filteredItems) - 1
571640 cmds = append(cmds, m.focusSelected())
572641 m.ResetView()
573642 return tea.Batch(cmds...)
@@ -578,7 +647,7 @@ func (m *model) goToBottom() tea.Cmd {
578647func (m *model) goToTop() tea.Cmd {
579648 cmds := []tea.Cmd{m.blurSelected()}
580649 m.viewState.reverse = false
581− if len(m.items) > 0 {
650+ if len(m.filteredItems) > 0 {
582651 m.selectionState.selectedIndex = 0
583652 }
584653 cmds = append(cmds, m.focusSelected())
@@ -596,10 +665,10 @@ func (m *model) ResetView() {
596665// focusSelected gives focus to the currently selected item if it supports focus.
597666// Triggers a re-render of the item to show its focused state.
598667func (m *model) focusSelected() tea.Cmd {
599− if !m.selectionState.isValidIndex(len(m.items)) {
668+ if !m.selectionState.isValidIndex(len(m.filteredItems)) {
600669 return nil
601670 }
602− if i, ok := m.items[m.selectionState.selectedIndex].(layout.Focusable); ok {
671+ if i, ok := m.filteredItems[m.selectionState.selectedIndex].(layout.Focusable); ok {
603672 cmd := i.Focus()
604673 m.rerenderItem(m.selectionState.selectedIndex)
605674 return cmd
@@ -610,10 +679,10 @@ func (m *model) focusSelected() tea.Cmd {
610679// blurSelected removes focus from the currently selected item if it supports focus.
611680// Triggers a re-render of the item to show its unfocused state.
612681func (m *model) blurSelected() tea.Cmd {
613− if !m.selectionState.isValidIndex(len(m.items)) {
682+ if !m.selectionState.isValidIndex(len(m.filteredItems)) {
614683 return nil
615684 }
616− if i, ok := m.items[m.selectionState.selectedIndex].(layout.Focusable); ok {
685+ if i, ok := m.filteredItems[m.selectionState.selectedIndex].(layout.Focusable); ok {
617686 cmd := i.Blur()
618687 m.rerenderItem(m.selectionState.selectedIndex)
619688 return cmd
@@ -625,7 +694,7 @@ func (m *model) blurSelected() tea.Cmd {
625694// This is called when an item's state changes (e.g., focus/blur) and needs to be re-displayed.
626695// It efficiently updates only the changed item and adjusts positions of subsequent items if needed.
627696func (m *model) rerenderItem(inx int) {
628− if inx < 0 || inx >= len(m.items) || len(m.renderState.lines) == 0 {
697+ if inx < 0 || inx >= len(m.filteredItems) || len(m.renderState.lines) == 0 {
629698 return
630699 }
631700
@@ -634,7 +703,7 @@ func (m *model) rerenderItem(inx int) {
634703 return
635704 }
636705
637− rerenderedLines := m.getItemLines(m.items[inx])
706+ rerenderedLines := m.getItemLines(m.filteredItems[inx])
638707 if slices.Equal(cachedItem.lines, rerenderedLines) {
639708 return
640709 }
@@ -687,7 +756,7 @@ func (m *model) updateItemPositions(inx int, cachedItem renderedItem, newHeight
687756 return
688757 }
689758
690− if inx == len(m.items)-1 {
759+ if inx == len(m.filteredItems)-1 {
691760 m.renderState.finalHeight = max(0, cachedItem.start+newHeight-m.listHeight())
692761 }
693762
@@ -701,7 +770,7 @@ func (m *model) updateItemPositions(inx int, cachedItem renderedItem, newHeight
701770
702771// updatePositionsForward updates positions for items after the changed item in forward mode.
703772func (m *model) updatePositionsForward(inx int, currentStart int) {
704− for i := inx + 1; i < len(m.items); i++ {
773+ for i := inx + 1; i < len(m.filteredItems); i++ {
705774 if existing, ok := m.renderState.items[i]; ok {
706775 existing.start = currentStart
707776 currentStart += existing.height
@@ -766,12 +835,12 @@ func (m *model) decreaseOffset(n int) {
766835// UpdateItem replaces an item at the specified index with a new item.
767836// Handles focus management and triggers re-rendering as needed.
768837func (m *model) UpdateItem(inx int, item util.Model) {
769− if inx < 0 || inx >= len(m.items) {
838+ if inx < 0 || inx >= len(m.filteredItems) {
770839 return
771840 }
772− m.items[inx] = item
841+ m.filteredItems[inx] = item
773842 if m.selectionState.selectedIndex == inx {
774− if i, ok := m.items[m.selectionState.selectedIndex].(layout.Focusable); ok {
843+ if i, ok := m.filteredItems[m.selectionState.selectedIndex].(layout.Focusable); ok {
775844 i.Focus()
776845 }
777846 }
@@ -788,6 +857,10 @@ func (m *model) GetSize() (int, int) {
788857// SetSize updates the list dimensions and triggers a complete re-render.
789858// Also updates the size of all items that support sizing.
790859func (m *model) SetSize(width int, height int) tea.Cmd {
860+ if m.filterable {
861+ height -= 2 // adjust for input field height and border
862+ }
863+
791864 if m.viewState.width == width && m.viewState.height == height {
792865 return nil
793866 }
@@ -797,11 +870,14 @@ func (m *model) SetSize(width int, height int) tea.Cmd {
797870 }
798871 m.viewState.width = width
799872 m.ResetView()
873+ if m.filterable {
874+ m.input.SetWidth(m.getItemWidth() - 3)
875+ }
800876 return m.setAllItemsSize()
801877}
802878
803−// getItemSize calculates the available width for items, accounting for padding.
804−func (m *model) getItemSize() int {
879+// getItemWidth calculates the available width for items, accounting for padding.
880+func (m *model) getItemWidth() int {
805881 width := m.viewState.width
806882 switch len(m.padding) {
807883 case 1:
@@ -816,11 +892,11 @@ func (m *model) getItemSize() int {
816892
817893// setItemSize updates the size of a specific item if it supports sizing.
818894func (m *model) setItemSize(inx int) tea.Cmd {
819− if inx < 0 || inx >= len(m.items) {
895+ if inx < 0 || inx >= len(m.filteredItems) {
820896 return nil
821897 }
822− if i, ok := m.items[inx].(layout.Sizeable); ok {
823− return i.SetSize(m.getItemSize(), 0)
898+ if i, ok := m.filteredItems[inx].(layout.Sizeable); ok {
899+ return i.SetSize(m.getItemWidth(), 0)
824900 }
825901 return nil
826902}
@@ -828,7 +904,7 @@ func (m *model) setItemSize(inx int) tea.Cmd {
828904// setAllItemsSize updates the size of all items that support sizing.
829905func (m *model) setAllItemsSize() tea.Cmd {
830906 var cmds []tea.Cmd
831− for i := range m.items {
907+ for i := range m.filteredItems {
832908 if cmd := m.setItemSize(i); cmd != nil {
833909 cmds = append(cmds, cmd)
834910 }
@@ -856,8 +932,9 @@ func (m *model) AppendItem(item util.Model) tea.Cmd {
856932 cmds := []tea.Cmd{
857933 item.Init(),
858934 }
859− m.items = append(m.items, item)
860− cmds = append(cmds, m.setItemSize(len(m.items)-1))
935+ m.allItems = append(m.allItems, item)
936+ m.filteredItems = m.allItems
937+ cmds = append(cmds, m.setItemSize(len(m.filteredItems)-1))
861938 cmds = append(cmds, m.goToBottom())
862939 m.renderState.needsRerender = true
863940 return tea.Batch(cmds...)
@@ -866,11 +943,12 @@ func (m *model) AppendItem(item util.Model) tea.Cmd {
866943// DeleteItem removes an item at the specified index.
867944// Adjusts selection if necessary and triggers a complete re-render.
868945func (m *model) DeleteItem(i int) {
869− if i < 0 || i >= len(m.items) {
946+ if i < 0 || i >= len(m.filteredItems) {
870947 return
871948 }
872− m.items = slices.Delete(m.items, i, i+1)
949+ m.allItems = slices.Delete(m.allItems, i, i+1)
873950 delete(m.renderState.items, i)
951+ m.filteredItems = m.allItems
874952
875953 if m.selectionState.selectedIndex == i && m.selectionState.selectedIndex > 0 {
876954 m.selectionState.selectedIndex--
@@ -886,7 +964,8 @@ func (m *model) DeleteItem(i int) {
886964// Adjusts cached positions and selection index, then switches to forward mode.
887965func (m *model) PrependItem(item util.Model) tea.Cmd {
888966 cmds := []tea.Cmd{item.Init()}
889− m.items = append([]util.Model{item}, m.items...)
967+ m.allItems = append([]util.Model{item}, m.allItems...)
968+ m.filteredItems = m.allItems
890969
891970 // Shift all cached item indices by 1
892971 newItems := make(map[int]renderedItem, len(m.renderState.items))
@@ -917,16 +996,78 @@ func (m *model) setReverse(reverse bool) {
917996// SetItems replaces all items in the list with a new set.
918997// Initializes all items, sets their sizes, and establishes initial selection.
919998func (m *model) SetItems(items []util.Model) tea.Cmd {
920− m.items = items
999+ m.allItems = items
1000+ m.filteredItems = items
9211001 cmds := []tea.Cmd{m.setAllItemsSize()}
9221002
923− for _, item := range m.items {
1003+ for _, item := range m.filteredItems {
9241004 cmds = append(cmds, item.Init())
9251005 }
9261006
927− if len(m.items) > 0 {
1007+ if len(m.filteredItems) > 0 {
1008+ if m.viewState.reverse {
1009+ m.selectionState.selectedIndex = len(m.filteredItems) - 1
1010+ } else {
1011+ m.selectionState.selectedIndex = 0
1012+ }
1013+ if cmd := m.focusSelected(); cmd != nil {
1014+ cmds = append(cmds, cmd)
1015+ }
1016+ } else {
1017+ m.selectionState.selectedIndex = NoSelection
1018+ }
1019+
1020+ m.ResetView()
1021+ return tea.Batch(cmds...)
1022+}
1023+
1024+func (c *model) inputStyle() lipgloss.Style {
1025+ t := theme.CurrentTheme()
1026+ return styles.BaseStyle().
1027+ BorderStyle(lipgloss.NormalBorder()).
1028+ BorderForeground(t.TextMuted()).
1029+ BorderBackground(t.Background()).
1030+ BorderBottom(true)
1031+}
1032+
1033+func (m *model) filter(search string) tea.Cmd {
1034+ var cmds []tea.Cmd
1035+ search = strings.TrimSpace(search)
1036+ search = strings.ToLower(search)
1037+ for _, item := range m.allItems {
1038+ if i, ok := item.(layout.Focusable); ok {
1039+ cmds = append(cmds, i.Blur())
1040+ }
1041+ if i, ok := item.(HasMatchIndexes); ok {
1042+ i.MatchIndexes(make([]int, 0))
1043+ }
1044+ }
1045+ if search == "" {
1046+ cmds = append(cmds, m.SetItems(m.allItems)) // Reset to all items if search is empty
1047+ return tea.Batch(cmds...)
1048+ }
1049+ words := make([]string, 0, len(m.allItems))
1050+ for _, cmd := range m.allItems {
1051+ if f, ok := cmd.(HasFilterValue); ok {
1052+ words = append(words, strings.ToLower(f.FilterValue()))
1053+ } else {
1054+ words = append(words, strings.ToLower(""))
1055+ }
1056+ }
1057+ matches := fuzzy.Find(search, words)
1058+ sort.Sort(matches)
1059+ filteredItems := make([]util.Model, 0, len(matches))
1060+ for _, match := range matches {
1061+ item := m.allItems[match.Index]
1062+ if i, ok := item.(HasMatchIndexes); ok {
1063+ i.MatchIndexes(match.MatchedIndexes)
1064+ }
1065+ filteredItems = append(filteredItems, item)
1066+ }
1067+ m.filteredItems = filteredItems
1068+ if len(filteredItems) > 0 {
9281069 if m.viewState.reverse {
929− m.selectionState.selectedIndex = len(m.items) - 1
1070+ m.selectionState.selectedIndex = len(filteredItems) - 1
9301071 } else {
9311072 m.selectionState.selectedIndex = 0
9321073 }