Skip to content

Commit c37a5f9

Browse files
committed
implement fuzzy search
1 parent cfeae2c commit c37a5f9

File tree

7 files changed

+65
-16
lines changed

7 files changed

+65
-16
lines changed

fzf.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ func New(opts ...Option) *FZF {
1818
}
1919

2020
func (fzf *FZF) Find(items Items) (int, error) {
21-
m := newModel(fzf, items)
21+
m := newModel(fzf, newItems(items))
2222

2323
p := tea.NewProgram(m)
2424
if _, err := p.Run(); err != nil {

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ require (
66
github.com/charmbracelet/bubbles v0.15.0
77
github.com/charmbracelet/bubbletea v0.23.2
88
github.com/charmbracelet/lipgloss v0.6.0
9+
github.com/sahilm/fuzzy v0.1.0
910
github.com/spf13/cobra v1.6.1
1011
)
1112

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkX
1616
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
1717
github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc=
1818
github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
19+
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
1920
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
2021
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
2122
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
@@ -45,6 +46,7 @@ github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJ
4546
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
4647
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
4748
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
49+
github.com/sahilm/fuzzy v0.1.0 h1:FzWGaw2Opqyu+794ZQ9SYifWv2EIXpwP4q8dY1kDAwI=
4850
github.com/sahilm/fuzzy v0.1.0/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=
4951
github.com/spf13/cobra v1.6.1 h1:o94oiPyS4KD1mPy2fmcYYHHfCxLqYjJOhGsCHFZtEzA=
5052
github.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY=

item.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,19 @@ type Items interface {
44
ItemString(int) string
55
Len() int
66
}
7+
8+
type items struct {
9+
items Items
10+
}
11+
12+
func newItems(is Items) items {
13+
return items{is}
14+
}
15+
16+
func (is items) String(i int) string {
17+
return is.items.ItemString(i)
18+
}
19+
20+
func (is items) Len() int {
21+
return is.items.Len()
22+
}

model.go

Lines changed: 39 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"github.com/charmbracelet/bubbles/textinput"
99
tea "github.com/charmbracelet/bubbletea"
1010
"github.com/charmbracelet/lipgloss"
11+
"github.com/sahilm/fuzzy"
1112
)
1213

1314
var (
@@ -16,11 +17,12 @@ var (
1617

1718
type model struct {
1819
fzf *FZF
19-
items Items
20+
items items
2021

2122
// state
2223
abort bool
2324
cursor int
25+
matches fuzzy.Matches
2426
choices []int
2527

2628
// window
@@ -32,7 +34,7 @@ type model struct {
3234
input textinput.Model
3335
}
3436

35-
func newModel(fzf *FZF, items Items) *model {
37+
func newModel(fzf *FZF, items items) *model {
3638
input := textinput.New()
3739
input.Prompt = fzf.option.prompt
3840
input.Placeholder = fzf.option.inputPlaceholder
@@ -44,6 +46,7 @@ func newModel(fzf *FZF, items Items) *model {
4446
// state
4547
abort: false,
4648
cursor: 0,
49+
matches: fuzzy.Matches{},
4750
choices: []int{},
4851
// window
4952
windowWidth: 0,
@@ -79,7 +82,7 @@ func (m *model) itemsView() string {
7982
headerHeight := lipgloss.Height(m.headerView())
8083
cursorLen := stringLen(m.fzf.option.cursor)
8184

82-
for i := 0; i < m.items.Len(); i++ {
85+
for i, match := range m.matches {
8386
if i < m.windowYPosition {
8487
continue
8588
}
@@ -92,15 +95,17 @@ func (m *model) itemsView() string {
9295
_, _ = v.WriteString(cursor)
9396

9497
// write item
95-
itemstring := m.items.ItemString(i)
9698
var itemv strings.Builder
97-
for _, c := range itemstring {
98-
// cursor line
99+
for ci, c := range match.Str {
100+
// matches
101+
style := lipgloss.NewStyle()
102+
if intContains(match.MatchedIndexes, ci) {
103+
style = style.Inherit(m.fzf.option.styles.Matches)
104+
}
99105
if i == m.cursor {
100-
_, _ = itemv.WriteString(m.fzf.option.styles.CursorLine.Render(string(c)))
101-
} else {
102-
itemv.WriteString(string(c))
106+
style = style.Inherit(m.fzf.option.styles.CursorLine)
103107
}
108+
_, _ = itemv.WriteString(style.Render(string(c)))
104109
}
105110
_, _ = v.WriteString(itemv.String())
106111

@@ -151,6 +156,7 @@ func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
151156
cmds = append(cmds, cmd)
152157
}
153158

159+
m.filter()
154160
m.fixYPosition()
155161
m.fixCursor()
156162

@@ -164,7 +170,7 @@ func (m *model) choice() {
164170
}
165171

166172
if len(m.choices) == 0 && m.cursor >= 0 {
167-
m.choices = append(m.choices, m.cursor)
173+
m.choices = append(m.choices, m.matches[m.cursor].Index)
168174
}
169175
}
170176

@@ -175,27 +181,45 @@ func (m *model) cursorUp() {
175181
}
176182

177183
func (m *model) cursorDown() {
178-
if m.cursor+1 < m.items.Len() {
184+
if m.cursor+1 < len(m.matches) {
179185
m.cursor++
180186
}
181187
}
182188

189+
func (m *model) filter() {
190+
var matches fuzzy.Matches
191+
192+
s := m.input.Value()
193+
if s == "" {
194+
for i := 0; i < m.items.Len(); i++ {
195+
matches = append(matches, fuzzy.Match{
196+
Str: m.items.String(i),
197+
Index: i,
198+
})
199+
}
200+
m.matches = matches
201+
return
202+
}
203+
204+
m.matches = fuzzy.FindFrom(s, m.items)
205+
}
206+
183207
func (m *model) fixCursor() {
184-
if m.cursor < 0 && m.items.Len() > 0 {
208+
if m.cursor < 0 && len(m.matches) > 0 {
185209
m.cursor = 0
186210
return
187211
}
188212

189-
if m.cursor+1 > m.items.Len() {
190-
m.cursor = m.items.Len() - 1
213+
if m.cursor+1 > len(m.matches) {
214+
m.cursor = len(m.matches) - 1
191215
return
192216
}
193217
}
194218

195219
func (m *model) fixYPosition() {
196220
headerHeight := lipgloss.Height(m.headerView())
197221

198-
if m.windowHeight-headerHeight > m.items.Len() {
222+
if m.windowHeight-headerHeight > len(m.matches) {
199223
m.windowYPosition = 0
200224
return
201225
}

option.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ var defaultOption = option{
1111
inputPlaceholder: "Filter...",
1212
styles: &styles{
1313
CursorLine: lipgloss.NewStyle().Bold(true),
14+
Matches: lipgloss.NewStyle().Foreground(lipgloss.Color("#00ADD8")),
1415
},
1516
keymap: &keymap{
1617
Up: key.NewBinding(key.WithKeys("up", "ctrl+p")),
@@ -48,6 +49,9 @@ func WithStyles(ss *Styles) Option {
4849
if ss.CursorLine != nil {
4950
o.styles.CursorLine = ss.CursorLine.lipgloss()
5051
}
52+
if ss.Matches != nil {
53+
o.styles.Matches = ss.Matches.lipgloss()
54+
}
5155
}
5256
}
5357

styles.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,12 @@ type Style struct {
1010

1111
type Styles struct {
1212
CursorLine *Style
13+
Matches *Style
1314
}
1415

1516
type styles struct {
1617
CursorLine lipgloss.Style
18+
Matches lipgloss.Style
1719
}
1820

1921
func (s *Style) lipgloss() lipgloss.Style {

0 commit comments

Comments
 (0)