Skip to content

Commit 435a835

Browse files
authored
Use substring filtering instead of fuzzy filtering by default (#3376)
By default we now search for substrings; you can search for multiple substrings by separating them with spaces. Add a config option `gui.filterMode` that can be set to 'fuzzy' to switch back to the previous behavior. Addresses #3373.
2 parents 2cf8e7b + 4f2bebe commit 435a835

22 files changed

+177
-84
lines changed

docs/Config.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ gui:
8686
border: 'rounded' # one of 'single' | 'double' | 'rounded' | 'hidden'
8787
animateExplosion: true # shows an explosion animation when nuking the working tree
8888
portraitMode: 'auto' # one of 'auto' | 'never' | 'always'
89+
filterMode: 'substring' # one of 'substring' | 'fuzzy'; see 'Filtering' section below
8990
git:
9091
paging:
9192
colorArg: always
@@ -374,6 +375,12 @@ That's the behavior when `gui.scrollOffBehavior` is set to "margin" (the default
374375

375376
This setting applies both to all list views (e.g. commits and branches etc), and to the staging view.
376377

378+
## Filtering
379+
380+
We have two ways to filter things, substring matching (the default) and fuzzy searching. With substring matching, the text you enter gets searched for verbatim (usually case-insensitive, except when your filter string contains uppercase letters, in which case we search case-sensitively). You can search for multiple non-contiguous substrings by separating them with spaces; for example, "int test" will match "integration-testing". All substrings have to match, but not necessarily in the given order.
381+
382+
Fuzzy searching is smarter in that it allows every letter of the filter string to match anywhere in the text (only in order though), assigning a weight to the quality of the match and sorting by that order. This has the advantage that it allows typing "clt" to match "commit_loader_test" (letters at the beginning of subwords get more weight); but it has the disadvantage that it tends to return lots of irrelevant results, especially with short filter strings.
383+
377384
## Color Attributes
378385

379386
For color attributes you can choose an array of attributes (with max one color attribute)

pkg/config/user_config.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,13 @@ type GuiConfig struct {
142142
// Whether to stack UI components on top of each other.
143143
// One of 'auto' (default) | 'always' | 'never'
144144
PortraitMode string `yaml:"portraitMode"`
145+
// How things are filtered when typing '/'.
146+
// One of 'substring' (default) | 'fuzzy'
147+
FilterMode string `yaml:"filterMode" jsonschema:"enum=substring,enum=fuzzy"`
148+
}
149+
150+
func (c *GuiConfig) UseFuzzySearch() bool {
151+
return c.FilterMode == "fuzzy"
145152
}
146153

147154
type ThemeConfig struct {
@@ -660,6 +667,7 @@ func GetDefaultConfig() *UserConfig {
660667
Border: "rounded",
661668
AnimateExplosion: true,
662669
PortraitMode: "auto",
670+
FilterMode: "substring",
663671
},
664672
Git: GitConfig{
665673
Paging: PagingConfig{

pkg/gui/context/branches_context.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ func NewBranchesContext(c *ContextCommon) *BranchesContext {
2222
func(branch *models.Branch) []string {
2323
return []string{branch.Name}
2424
},
25-
func() bool { return c.AppState.LocalBranchSortOrder != "alphabetical" },
2625
)
2726

2827
getDisplayStrings := func(_ int, _ int) [][]string {

pkg/gui/context/filtered_list.go

Lines changed: 11 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package context
22

33
import (
4-
"slices"
54
"strings"
65

76
"github.com/jesseduffield/lazygit/pkg/utils"
@@ -17,40 +16,33 @@ type FilteredList[T any] struct {
1716
getFilterFields func(T) []string
1817
filter string
1918

20-
// Normally, filtered items are presented sorted by best match. If this
21-
// function returns true, they retain their original sort order instead;
22-
// this is useful for lists that show items sorted by date, for example.
23-
// Leaving this nil is equivalent to returning false.
24-
shouldRetainSortOrder func() bool
25-
2619
mutex *deadlock.Mutex
2720
}
2821

29-
func NewFilteredList[T any](getList func() []T, getFilterFields func(T) []string, shouldRetainSortOrder func() bool) *FilteredList[T] {
22+
func NewFilteredList[T any](getList func() []T, getFilterFields func(T) []string) *FilteredList[T] {
3023
return &FilteredList[T]{
31-
getList: getList,
32-
getFilterFields: getFilterFields,
33-
shouldRetainSortOrder: shouldRetainSortOrder,
34-
mutex: &deadlock.Mutex{},
24+
getList: getList,
25+
getFilterFields: getFilterFields,
26+
mutex: &deadlock.Mutex{},
3527
}
3628
}
3729

3830
func (self *FilteredList[T]) GetFilter() string {
3931
return self.filter
4032
}
4133

42-
func (self *FilteredList[T]) SetFilter(filter string) {
34+
func (self *FilteredList[T]) SetFilter(filter string, useFuzzySearch bool) {
4335
self.filter = filter
4436

45-
self.applyFilter()
37+
self.applyFilter(useFuzzySearch)
4638
}
4739

4840
func (self *FilteredList[T]) ClearFilter() {
49-
self.SetFilter("")
41+
self.SetFilter("", false)
5042
}
5143

52-
func (self *FilteredList[T]) ReApplyFilter() {
53-
self.applyFilter()
44+
func (self *FilteredList[T]) ReApplyFilter(useFuzzySearch bool) {
45+
self.applyFilter(useFuzzySearch)
5446
}
5547

5648
func (self *FilteredList[T]) IsFiltering() bool {
@@ -84,7 +76,7 @@ func (self *fuzzySource[T]) Len() int {
8476
return len(self.list)
8577
}
8678

87-
func (self *FilteredList[T]) applyFilter() {
79+
func (self *FilteredList[T]) applyFilter(useFuzzySearch bool) {
8880
self.mutex.Lock()
8981
defer self.mutex.Unlock()
9082

@@ -96,13 +88,10 @@ func (self *FilteredList[T]) applyFilter() {
9688
getFilterFields: self.getFilterFields,
9789
}
9890

99-
matches := fuzzy.FindFrom(self.filter, source)
91+
matches := utils.FindFrom(self.filter, source, useFuzzySearch)
10092
self.filteredIndices = lo.Map(matches, func(match fuzzy.Match, _ int) int {
10193
return match.Index
10294
})
103-
if self.shouldRetainSortOrder != nil && self.shouldRetainSortOrder() {
104-
slices.Sort(self.filteredIndices)
105-
}
10695
}
10796
}
10897

pkg/gui/context/filtered_list_view_model.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ type FilteredListViewModel[T HasID] struct {
66
*SearchHistory
77
}
88

9-
func NewFilteredListViewModel[T HasID](getList func() []T, getFilterFields func(T) []string, shouldRetainSortOrder func() bool) *FilteredListViewModel[T] {
10-
filteredList := NewFilteredList(getList, getFilterFields, shouldRetainSortOrder)
9+
func NewFilteredListViewModel[T HasID](getList func() []T, getFilterFields func(T) []string) *FilteredListViewModel[T] {
10+
filteredList := NewFilteredList(getList, getFilterFields)
1111

1212
self := &FilteredListViewModel[T]{
1313
FilteredList: filteredList,

pkg/gui/context/menu_context.go

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -61,10 +61,6 @@ func NewMenuViewModel(c *ContextCommon) *MenuViewModel {
6161
self.FilteredListViewModel = NewFilteredListViewModel(
6262
func() []*types.MenuItem { return self.menuItems },
6363
func(item *types.MenuItem) []string { return item.LabelColumns },
64-
// The only menu that the user is likely to filter in is the keybindings
65-
// menu; retain the sort order in that one because this allows us to
66-
// keep the section headers while filtering:
67-
func() bool { return true },
6864
)
6965

7066
return self
@@ -99,6 +95,13 @@ func (self *MenuViewModel) GetDisplayStrings(_ int, _ int) [][]string {
9995
}
10096

10197
func (self *MenuViewModel) GetNonModelItems() []*NonModelItem {
98+
// Don't display section headers when we are filtering, and the filter mode
99+
// is fuzzy. The reason is that filtering changes the order of the items
100+
// (they are sorted by best match), so all the sections would be messed up.
101+
if self.FilteredListViewModel.IsFiltering() && self.c.UserConfig.Gui.UseFuzzySearch() {
102+
return []*NonModelItem{}
103+
}
104+
102105
result := []*NonModelItem{}
103106
menuItems := self.FilteredListViewModel.GetItems()
104107
var prevSection *types.MenuSection = nil

pkg/gui/context/reflog_commits_context.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ func NewReflogCommitsContext(c *ContextCommon) *ReflogCommitsContext {
2424
func(commit *models.Commit) []string {
2525
return []string{commit.ShortSha(), commit.Name}
2626
},
27-
func() bool { return true },
2827
)
2928

3029
getDisplayStrings := func(_ int, _ int) [][]string {

pkg/gui/context/remote_branches_context.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ func NewRemoteBranchesContext(
2626
func(remoteBranch *models.RemoteBranch) []string {
2727
return []string{remoteBranch.Name}
2828
},
29-
func() bool { return c.AppState.RemoteBranchSortOrder != "alphabetical" },
3029
)
3130

3231
getDisplayStrings := func(_ int, _ int) [][]string {

pkg/gui/context/remotes_context.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ func NewRemotesContext(c *ContextCommon) *RemotesContext {
2222
func(remote *models.Remote) []string {
2323
return []string{remote.Name}
2424
},
25-
nil,
2625
)
2726

2827
getDisplayStrings := func(_ int, _ int) [][]string {

pkg/gui/context/stash_context.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ func NewStashContext(
2424
func(stashEntry *models.StashEntry) []string {
2525
return []string{stashEntry.Name}
2626
},
27-
func() bool { return true },
2827
)
2928

3029
getDisplayStrings := func(_ int, _ int) [][]string {

0 commit comments

Comments
 (0)