mirror of
https://github.com/jesseduffield/lazygit.git
synced 2025-07-30 03:23:08 +03:00
Use fuzzy search when filtering a view
This adds fuzzy filtering instead of exact match filtering, which is more forgiving of typos and allows more efficiency.
This commit is contained in:
@ -1,7 +1,11 @@
|
|||||||
package context
|
package context
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||||
|
"github.com/sahilm/fuzzy"
|
||||||
|
"github.com/samber/lo"
|
||||||
"github.com/sasha-s/go-deadlock"
|
"github.com/sasha-s/go-deadlock"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -53,6 +57,21 @@ func (self *FilteredList[T]) UnfilteredLen() int {
|
|||||||
return len(self.getList())
|
return len(self.getList())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type fuzzySource[T any] struct {
|
||||||
|
list []T
|
||||||
|
getFilterFields func(T) []string
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ fuzzy.Source = &fuzzySource[string]{}
|
||||||
|
|
||||||
|
func (self *fuzzySource[T]) String(i int) string {
|
||||||
|
return strings.Join(self.getFilterFields(self.list[i]), " ")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *fuzzySource[T]) Len() int {
|
||||||
|
return len(self.list)
|
||||||
|
}
|
||||||
|
|
||||||
func (self *FilteredList[T]) applyFilter() {
|
func (self *FilteredList[T]) applyFilter() {
|
||||||
self.mutex.Lock()
|
self.mutex.Lock()
|
||||||
defer self.mutex.Unlock()
|
defer self.mutex.Unlock()
|
||||||
@ -60,20 +79,16 @@ func (self *FilteredList[T]) applyFilter() {
|
|||||||
if self.filter == "" {
|
if self.filter == "" {
|
||||||
self.filteredIndices = nil
|
self.filteredIndices = nil
|
||||||
} else {
|
} else {
|
||||||
self.filteredIndices = []int{}
|
source := &fuzzySource[T]{
|
||||||
for i, item := range self.getList() {
|
list: self.getList(),
|
||||||
for _, field := range self.getFilterFields(item) {
|
getFilterFields: self.getFilterFields,
|
||||||
if self.match(field, self.filter) {
|
|
||||||
self.filteredIndices = append(self.filteredIndices, i)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (self *FilteredList[T]) match(haystack string, needle string) bool {
|
matches := fuzzy.FindFrom(self.filter, source)
|
||||||
return utils.CaseAwareContains(haystack, needle)
|
self.filteredIndices = lo.Map(matches, func(match fuzzy.Match, _ int) int {
|
||||||
|
return match.Index
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (self *FilteredList[T]) UnfilteredIndex(index int) int {
|
func (self *FilteredList[T]) UnfilteredIndex(index int) int {
|
||||||
|
@ -539,6 +539,7 @@ func (self *ViewDriver) FilterOrSearch(text string) *ViewDriver {
|
|||||||
self.Press(self.t.keys.Universal.StartSearch).
|
self.Press(self.t.keys.Universal.StartSearch).
|
||||||
Tap(func() {
|
Tap(func() {
|
||||||
self.t.ExpectSearch().
|
self.t.ExpectSearch().
|
||||||
|
Clear().
|
||||||
Type(text).
|
Type(text).
|
||||||
Confirm()
|
Confirm()
|
||||||
|
|
||||||
|
35
pkg/integration/tests/filter_and_search/filter_fuzzy.go
Normal file
35
pkg/integration/tests/filter_and_search/filter_fuzzy.go
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
package filter_and_search
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/jesseduffield/lazygit/pkg/config"
|
||||||
|
. "github.com/jesseduffield/lazygit/pkg/integration/components"
|
||||||
|
)
|
||||||
|
|
||||||
|
var FilterFuzzy = NewIntegrationTest(NewIntegrationTestArgs{
|
||||||
|
Description: "Verify that fuzzy filtering works (not just exact matches)",
|
||||||
|
ExtraCmdArgs: []string{},
|
||||||
|
Skip: false,
|
||||||
|
SetupConfig: func(config *config.AppConfig) {},
|
||||||
|
SetupRepo: func(shell *Shell) {
|
||||||
|
shell.NewBranch("this-is-my-branch")
|
||||||
|
shell.EmptyCommit("first commit")
|
||||||
|
shell.NewBranch("other-branch")
|
||||||
|
},
|
||||||
|
Run: func(t *TestDriver, keys config.KeybindingConfig) {
|
||||||
|
t.Views().Branches().
|
||||||
|
Focus().
|
||||||
|
Lines(
|
||||||
|
Contains(`other-branch`).IsSelected(),
|
||||||
|
Contains(`this-is-my-branch`),
|
||||||
|
).
|
||||||
|
FilterOrSearch("timb"). // using first letters of words
|
||||||
|
Lines(
|
||||||
|
Contains(`this-is-my-branch`).IsSelected(),
|
||||||
|
).
|
||||||
|
FilterOrSearch("brnch"). // allows missing letter
|
||||||
|
Lines(
|
||||||
|
Contains(`other-branch`).IsSelected(),
|
||||||
|
Contains(`this-is-my-branch`),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
})
|
@ -98,6 +98,7 @@ var tests = []*components.IntegrationTest{
|
|||||||
file.RememberCommitMessageAfterFail,
|
file.RememberCommitMessageAfterFail,
|
||||||
filter_and_search.FilterCommitFiles,
|
filter_and_search.FilterCommitFiles,
|
||||||
filter_and_search.FilterFiles,
|
filter_and_search.FilterFiles,
|
||||||
|
filter_and_search.FilterFuzzy,
|
||||||
filter_and_search.FilterMenu,
|
filter_and_search.FilterMenu,
|
||||||
filter_and_search.FilterRemoteBranches,
|
filter_and_search.FilterRemoteBranches,
|
||||||
filter_and_search.NestedFilter,
|
filter_and_search.NestedFilter,
|
||||||
|
Reference in New Issue
Block a user