From a82e26d11e4c937c8ca2fdef92bb75ae912e7b44 Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Thu, 14 Mar 2024 08:43:44 +0100 Subject: [PATCH 1/5] Don't sort the results of fuzzy.Find It sorts them already, so it's unnecessary. In the next commit we use this same code for substring searching too, and in that case we don't want to sort because sorting is by Score, but we don't even fill in the score for substring searching. --- pkg/utils/search.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/pkg/utils/search.go b/pkg/utils/search.go index c204506f0..ab780c5dd 100644 --- a/pkg/utils/search.go +++ b/pkg/utils/search.go @@ -1,7 +1,6 @@ package utils import ( - "sort" "strings" "github.com/sahilm/fuzzy" @@ -14,7 +13,6 @@ func FuzzySearch(needle string, haystack []string) []string { } matches := fuzzy.Find(needle, haystack) - sort.Sort(matches) return lo.Map(matches, func(match fuzzy.Match, _ int) string { return match.Str From a8797c72617b3ace7ce7c1c0eab469497e40b774 Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Sun, 10 Mar 2024 10:58:19 +0100 Subject: [PATCH 2/5] Default to substring filtering, add option to go back to fuzzy filtering 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. --- docs/Config.md | 7 +++ pkg/config/user_config.go | 8 +++ pkg/gui/context/filtered_list.go | 16 ++--- pkg/gui/controllers/custom_command_action.go | 2 +- pkg/gui/controllers/helpers/search_helper.go | 4 +- .../controllers/helpers/suggestions_helper.go | 42 ++++++++----- pkg/gui/types/context.go | 4 +- pkg/i18n/english.go | 4 ++ .../tests/filter_and_search/filter_fuzzy.go | 4 +- pkg/utils/search.go | 53 +++++++++++++++- pkg/utils/search_test.go | 63 ++++++++++++------- schema/config.json | 9 +++ 12 files changed, 164 insertions(+), 52 deletions(-) diff --git a/docs/Config.md b/docs/Config.md index 6998b2296..4e2486784 100644 --- a/docs/Config.md +++ b/docs/Config.md @@ -86,6 +86,7 @@ gui: border: 'rounded' # one of 'single' | 'double' | 'rounded' | 'hidden' animateExplosion: true # shows an explosion animation when nuking the working tree portraitMode: 'auto' # one of 'auto' | 'never' | 'always' + filterMode: 'substring' # one of 'substring' | 'fuzzy'; see 'Filtering' section below git: paging: colorArg: always @@ -374,6 +375,12 @@ That's the behavior when `gui.scrollOffBehavior` is set to "margin" (the default This setting applies both to all list views (e.g. commits and branches etc), and to the staging view. +## Filtering + +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. + +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. + ## Color Attributes For color attributes you can choose an array of attributes (with max one color attribute) diff --git a/pkg/config/user_config.go b/pkg/config/user_config.go index 2cbb15ce4..588fe2638 100644 --- a/pkg/config/user_config.go +++ b/pkg/config/user_config.go @@ -142,6 +142,13 @@ type GuiConfig struct { // Whether to stack UI components on top of each other. // One of 'auto' (default) | 'always' | 'never' PortraitMode string `yaml:"portraitMode"` + // How things are filtered when typing '/'. + // One of 'substring' (default) | 'fuzzy' + FilterMode string `yaml:"filterMode" jsonschema:"enum=substring,enum=fuzzy"` +} + +func (c *GuiConfig) UseFuzzySearch() bool { + return c.FilterMode == "fuzzy" } type ThemeConfig struct { @@ -660,6 +667,7 @@ func GetDefaultConfig() *UserConfig { Border: "rounded", AnimateExplosion: true, PortraitMode: "auto", + FilterMode: "substring", }, Git: GitConfig{ Paging: PagingConfig{ diff --git a/pkg/gui/context/filtered_list.go b/pkg/gui/context/filtered_list.go index 13b9c166a..8ac912cd0 100644 --- a/pkg/gui/context/filtered_list.go +++ b/pkg/gui/context/filtered_list.go @@ -39,18 +39,18 @@ func (self *FilteredList[T]) GetFilter() string { return self.filter } -func (self *FilteredList[T]) SetFilter(filter string) { +func (self *FilteredList[T]) SetFilter(filter string, useFuzzySearch bool) { self.filter = filter - self.applyFilter() + self.applyFilter(useFuzzySearch) } func (self *FilteredList[T]) ClearFilter() { - self.SetFilter("") + self.SetFilter("", false) } -func (self *FilteredList[T]) ReApplyFilter() { - self.applyFilter() +func (self *FilteredList[T]) ReApplyFilter(useFuzzySearch bool) { + self.applyFilter(useFuzzySearch) } func (self *FilteredList[T]) IsFiltering() bool { @@ -84,7 +84,7 @@ func (self *fuzzySource[T]) Len() int { return len(self.list) } -func (self *FilteredList[T]) applyFilter() { +func (self *FilteredList[T]) applyFilter(useFuzzySearch bool) { self.mutex.Lock() defer self.mutex.Unlock() @@ -96,11 +96,11 @@ func (self *FilteredList[T]) applyFilter() { getFilterFields: self.getFilterFields, } - matches := fuzzy.FindFrom(self.filter, source) + matches := utils.FindFrom(self.filter, source, useFuzzySearch) self.filteredIndices = lo.Map(matches, func(match fuzzy.Match, _ int) int { return match.Index }) - if self.shouldRetainSortOrder != nil && self.shouldRetainSortOrder() { + if useFuzzySearch && self.shouldRetainSortOrder != nil && self.shouldRetainSortOrder() { slices.Sort(self.filteredIndices) } } diff --git a/pkg/gui/controllers/custom_command_action.go b/pkg/gui/controllers/custom_command_action.go index bc595934d..16d811aff 100644 --- a/pkg/gui/controllers/custom_command_action.go +++ b/pkg/gui/controllers/custom_command_action.go @@ -38,7 +38,7 @@ func (self *CustomCommandAction) Call() error { func (self *CustomCommandAction) GetCustomCommandsHistorySuggestionsFunc() func(string) []*types.Suggestion { history := self.c.GetAppState().CustomCommandsHistory - return helpers.FuzzySearchFunc(history) + return helpers.FuzzySearchFunc(history, self.c.UserConfig.Gui.UseFuzzySearch()) } // this mimics the shell functionality `ignorespace` diff --git a/pkg/gui/controllers/helpers/search_helper.go b/pkg/gui/controllers/helpers/search_helper.go index 9ceea2f90..c31720949 100644 --- a/pkg/gui/controllers/helpers/search_helper.go +++ b/pkg/gui/controllers/helpers/search_helper.go @@ -218,7 +218,7 @@ func (self *SearchHelper) OnPromptContentChanged(searchString string) { case types.IFilterableContext: context.SetSelection(0) _ = context.GetView().SetOriginY(0) - context.SetFilter(searchString) + context.SetFilter(searchString, self.c.UserConfig.Gui.UseFuzzySearch()) _ = self.c.PostRefreshUpdate(context) case types.ISearchableContext: // do nothing @@ -234,7 +234,7 @@ func (self *SearchHelper) ReApplyFilter(context types.Context) { if ok { filterableContext.SetSelection(0) _ = filterableContext.GetView().SetOriginY(0) - filterableContext.ReApplyFilter() + filterableContext.ReApplyFilter(self.c.UserConfig.Gui.UseFuzzySearch()) } } } diff --git a/pkg/gui/controllers/helpers/suggestions_helper.go b/pkg/gui/controllers/helpers/suggestions_helper.go index 2ae9d2158..91f42df8c 100644 --- a/pkg/gui/controllers/helpers/suggestions_helper.go +++ b/pkg/gui/controllers/helpers/suggestions_helper.go @@ -3,6 +3,7 @@ package helpers import ( "fmt" "os" + "strings" "github.com/jesseduffield/gocui" "github.com/jesseduffield/lazygit/pkg/commands/models" @@ -65,7 +66,7 @@ func matchesToSuggestions(matches []string) []*types.Suggestion { func (self *SuggestionsHelper) GetRemoteSuggestionsFunc() func(string) []*types.Suggestion { remoteNames := self.getRemoteNames() - return FuzzySearchFunc(remoteNames) + return FuzzySearchFunc(remoteNames, self.c.UserConfig.Gui.UseFuzzySearch()) } func (self *SuggestionsHelper) getBranchNames() []string { @@ -82,7 +83,7 @@ func (self *SuggestionsHelper) GetBranchNameSuggestionsFunc() func(string) []*ty if input == "" { matchingBranchNames = branchNames } else { - matchingBranchNames = utils.FuzzySearch(input, branchNames) + matchingBranchNames = utils.FuzzySearch(input, branchNames, self.c.UserConfig.Gui.UseFuzzySearch()) } return lo.Map(matchingBranchNames, func(branchName string, _ int) *types.Suggestion { @@ -128,13 +129,26 @@ func (self *SuggestionsHelper) GetFilePathSuggestionsFunc() func(string) []*type return func(input string) []*types.Suggestion { matchingNames := []string{} - _ = self.c.Model().FilesTrie.VisitFuzzy(patricia.Prefix(input), true, func(prefix patricia.Prefix, item patricia.Item, skipped int) error { - matchingNames = append(matchingNames, item.(string)) - return nil - }) + if self.c.UserConfig.Gui.UseFuzzySearch() { + _ = self.c.Model().FilesTrie.VisitFuzzy(patricia.Prefix(input), true, func(prefix patricia.Prefix, item patricia.Item, skipped int) error { + matchingNames = append(matchingNames, item.(string)) + return nil + }) - // doing another fuzzy search for good measure - matchingNames = utils.FuzzySearch(input, matchingNames) + // doing another fuzzy search for good measure + matchingNames = utils.FuzzySearch(input, matchingNames, true) + } else { + substrings := strings.Fields(input) + _ = self.c.Model().FilesTrie.Visit(func(prefix patricia.Prefix, item patricia.Item) error { + for _, sub := range substrings { + if !utils.CaseAwareContains(item.(string), sub) { + return nil + } + } + matchingNames = append(matchingNames, item.(string)) + return nil + }) + } return matchesToSuggestions(matchingNames) } @@ -149,7 +163,7 @@ func (self *SuggestionsHelper) getRemoteBranchNames(separator string) []string { } func (self *SuggestionsHelper) GetRemoteBranchesSuggestionsFunc(separator string) func(string) []*types.Suggestion { - return FuzzySearchFunc(self.getRemoteBranchNames(separator)) + return FuzzySearchFunc(self.getRemoteBranchNames(separator), self.c.UserConfig.Gui.UseFuzzySearch()) } func (self *SuggestionsHelper) getTagNames() []string { @@ -161,7 +175,7 @@ func (self *SuggestionsHelper) getTagNames() []string { func (self *SuggestionsHelper) GetTagsSuggestionsFunc() func(string) []*types.Suggestion { tagNames := self.getTagNames() - return FuzzySearchFunc(tagNames) + return FuzzySearchFunc(tagNames, self.c.UserConfig.Gui.UseFuzzySearch()) } func (self *SuggestionsHelper) GetRefsSuggestionsFunc() func(string) []*types.Suggestion { @@ -172,7 +186,7 @@ func (self *SuggestionsHelper) GetRefsSuggestionsFunc() func(string) []*types.Su refNames := append(append(append(remoteBranchNames, localBranchNames...), tagNames...), additionalRefNames...) - return FuzzySearchFunc(refNames) + return FuzzySearchFunc(refNames, self.c.UserConfig.Gui.UseFuzzySearch()) } func (self *SuggestionsHelper) GetAuthorsSuggestionsFunc() func(string) []*types.Suggestion { @@ -182,16 +196,16 @@ func (self *SuggestionsHelper) GetAuthorsSuggestionsFunc() func(string) []*types slices.Sort(authors) - return FuzzySearchFunc(authors) + return FuzzySearchFunc(authors, self.c.UserConfig.Gui.UseFuzzySearch()) } -func FuzzySearchFunc(options []string) func(string) []*types.Suggestion { +func FuzzySearchFunc(options []string, useFuzzySearch bool) func(string) []*types.Suggestion { return func(input string) []*types.Suggestion { var matches []string if input == "" { matches = options } else { - matches = utils.FuzzySearch(input, options) + matches = utils.FuzzySearch(input, options, useFuzzySearch) } return matchesToSuggestions(matches) diff --git a/pkg/gui/types/context.go b/pkg/gui/types/context.go index 92b07a729..bb57375f9 100644 --- a/pkg/gui/types/context.go +++ b/pkg/gui/types/context.go @@ -102,10 +102,10 @@ type IFilterableContext interface { IListPanelState ISearchHistoryContext - SetFilter(string) + SetFilter(string, bool) GetFilter() string ClearFilter() - ReApplyFilter() + ReApplyFilter(bool) IsFiltering() bool IsFilterableContext() } diff --git a/pkg/i18n/english.go b/pkg/i18n/english.go index f475ec756..29b78f601 100644 --- a/pkg/i18n/english.go +++ b/pkg/i18n/english.go @@ -1906,6 +1906,10 @@ keybinding: - Push/pull/fetch loading statuses are now shown against the branch rather than in a popup. This allows you to e.g. fetch multiple branches in parallel and see the status for each branch. - The git log graph in the commits view is now always shown by default (previously it was only shown when the view was maximised). If you find this too noisy, you can change it back via ctrl+L -> 'Show git graph' -> 'when maximised' - Pressing space on a remote branch used to show a prompt for entering a name for a new local branch to check out from the remote branch. Now it just checks out the remote branch directly, letting you choose between a new local branch with the same name, or a detached head. The old behavior is still available via the 'n' keybinding. +- Filtering (e.g. when pressing '/') is less fuzzy by default; it only matches substrings now. Multiple substrings can be matched by separating them with spaces. If you want to revert to the old behavior, set the following in your config: + +gui: + filterMode: 'fuzzy' `, }, } diff --git a/pkg/integration/tests/filter_and_search/filter_fuzzy.go b/pkg/integration/tests/filter_and_search/filter_fuzzy.go index 198020afa..63df903d8 100644 --- a/pkg/integration/tests/filter_and_search/filter_fuzzy.go +++ b/pkg/integration/tests/filter_and_search/filter_fuzzy.go @@ -9,7 +9,9 @@ var FilterFuzzy = NewIntegrationTest(NewIntegrationTestArgs{ Description: "Verify that fuzzy filtering works (not just exact matches)", ExtraCmdArgs: []string{}, Skip: false, - SetupConfig: func(config *config.AppConfig) {}, + SetupConfig: func(config *config.AppConfig) { + config.UserConfig.Gui.FilterMode = "fuzzy" + }, SetupRepo: func(shell *Shell) { shell.NewBranch("this-is-my-branch") shell.EmptyCommit("first commit") diff --git a/pkg/utils/search.go b/pkg/utils/search.go index ab780c5dd..7abe8c85d 100644 --- a/pkg/utils/search.go +++ b/pkg/utils/search.go @@ -7,18 +7,67 @@ import ( "github.com/samber/lo" ) -func FuzzySearch(needle string, haystack []string) []string { +func FuzzySearch(needle string, haystack []string, useFuzzySearch bool) []string { if needle == "" { return []string{} } - matches := fuzzy.Find(needle, haystack) + matches := Find(needle, haystack, useFuzzySearch) return lo.Map(matches, func(match fuzzy.Match, _ int) string { return match.Str }) } +// Duplicated from the fuzzy package because it's private there +type stringSource []string + +func (ss stringSource) String(i int) string { + return ss[i] +} + +func (ss stringSource) Len() int { return len(ss) } + +// Drop-in replacement for fuzzy.Find (except that it doesn't fill out +// MatchedIndexes or Score, but we are not using these) +func FindSubstrings(pattern string, data []string) fuzzy.Matches { + return FindSubstringsFrom(pattern, stringSource(data)) +} + +// Drop-in replacement for fuzzy.FindFrom (except that it doesn't fill out +// MatchedIndexes or Score, but we are not using these) +func FindSubstringsFrom(pattern string, data fuzzy.Source) fuzzy.Matches { + substrings := strings.Fields(pattern) + result := fuzzy.Matches{} + +outer: + for i := 0; i < data.Len(); i++ { + s := data.String(i) + for _, sub := range substrings { + if !CaseAwareContains(s, sub) { + continue outer + } + } + result = append(result, fuzzy.Match{Str: s, Index: i}) + } + + return result +} + +func Find(pattern string, data []string, useFuzzySearch bool) fuzzy.Matches { + if useFuzzySearch { + return fuzzy.Find(pattern, data) + } + return FindSubstrings(pattern, data) +} + +func FindFrom(pattern string, data fuzzy.Source, useFuzzySearch bool) fuzzy.Matches { + if useFuzzySearch { + return fuzzy.FindFrom(pattern, data) + } + return FindSubstringsFrom(pattern, data) +} + func CaseAwareContains(haystack, needle string) bool { // if needle contains an uppercase letter, we'll do a case sensitive search if ContainsUppercase(needle) { diff --git a/pkg/utils/search_test.go b/pkg/utils/search_test.go index 79668c0f5..fef10ed59 100644 --- a/pkg/utils/search_test.go +++ b/pkg/utils/search_test.go @@ -10,46 +10,65 @@ import ( // TestFuzzySearch is a function. func TestFuzzySearch(t *testing.T) { type scenario struct { - needle string - haystack []string - expected []string + needle string + haystack []string + useFuzzySearch bool + expected []string } scenarios := []scenario{ { - needle: "", - haystack: []string{"test"}, - expected: []string{}, + needle: "", + haystack: []string{"test"}, + useFuzzySearch: true, + expected: []string{}, }, { - needle: "test", - haystack: []string{"test"}, - expected: []string{"test"}, + needle: "test", + haystack: []string{"test"}, + useFuzzySearch: true, + expected: []string{"test"}, }, { - needle: "o", - haystack: []string{"a", "o", "e"}, - expected: []string{"o"}, + needle: "o", + haystack: []string{"a", "o", "e"}, + useFuzzySearch: true, + expected: []string{"o"}, }, { - needle: "mybranch", - haystack: []string{"my_branch", "mybranch", "branch", "this is my branch"}, - expected: []string{"mybranch", "my_branch", "this is my branch"}, + needle: "mybranch", + haystack: []string{"my_branch", "mybranch", "branch", "this is my branch"}, + useFuzzySearch: true, + expected: []string{"mybranch", "my_branch", "this is my branch"}, }, { - needle: "test", - haystack: []string{"not a good match", "this 'test' is a good match", "test"}, - expected: []string{"test", "this 'test' is a good match"}, + needle: "test", + haystack: []string{"not a good match", "this 'test' is a good match", "test"}, + useFuzzySearch: true, + expected: []string{"test", "this 'test' is a good match"}, }, { - needle: "test", - haystack: []string{"Test"}, - expected: []string{"Test"}, + needle: "test", + haystack: []string{"Test"}, + useFuzzySearch: true, + expected: []string{"Test"}, + }, + { + needle: "test", + haystack: []string{"integration-testing", "t_e_s_t"}, + useFuzzySearch: false, + expected: []string{"integration-testing"}, + }, + { + needle: "integr test", + haystack: []string{"integration-testing", "testing-integration"}, + useFuzzySearch: false, + expected: []string{"integration-testing", "testing-integration"}, }, } for _, s := range scenarios { - assert.EqualValues(t, s.expected, FuzzySearch(s.needle, s.haystack)) + assert.EqualValues(t, s.expected, FuzzySearch(s.needle, s.haystack, s.useFuzzySearch)) } } diff --git a/schema/config.json b/schema/config.json index 3816dcea6..65383cd9f 100644 --- a/schema/config.json +++ b/schema/config.json @@ -357,6 +357,15 @@ "type": "string", "description": "Whether to stack UI components on top of each other.\nOne of 'auto' (default) | 'always' | 'never'", "default": "auto" + }, + "filterMode": { + "type": "string", + "enum": [ + "substring", + "fuzzy" + ], + "description": "How things are filtered when typing '/'.\nOne of 'substring' (default) | 'fuzzy'", + "default": "substring" } }, "additionalProperties": false, From 561afa990186ce6bdf7f62ce6e0114e6430eda8b Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Sun, 17 Mar 2024 09:11:00 +0100 Subject: [PATCH 3/5] Rename FuzzySearch to FilterStrings It isn't necessarily fuzzy any more. --- pkg/gui/controllers/helpers/suggestions_helper.go | 6 +++--- pkg/utils/search.go | 2 +- pkg/utils/search_test.go | 5 ++--- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/pkg/gui/controllers/helpers/suggestions_helper.go b/pkg/gui/controllers/helpers/suggestions_helper.go index 91f42df8c..b31a19f7a 100644 --- a/pkg/gui/controllers/helpers/suggestions_helper.go +++ b/pkg/gui/controllers/helpers/suggestions_helper.go @@ -83,7 +83,7 @@ func (self *SuggestionsHelper) GetBranchNameSuggestionsFunc() func(string) []*ty if input == "" { matchingBranchNames = branchNames } else { - matchingBranchNames = utils.FuzzySearch(input, branchNames, self.c.UserConfig.Gui.UseFuzzySearch()) + matchingBranchNames = utils.FilterStrings(input, branchNames, self.c.UserConfig.Gui.UseFuzzySearch()) } return lo.Map(matchingBranchNames, func(branchName string, _ int) *types.Suggestion { @@ -136,7 +136,7 @@ func (self *SuggestionsHelper) GetFilePathSuggestionsFunc() func(string) []*type }) // doing another fuzzy search for good measure - matchingNames = utils.FuzzySearch(input, matchingNames, true) + matchingNames = utils.FilterStrings(input, matchingNames, true) } else { substrings := strings.Fields(input) _ = self.c.Model().FilesTrie.Visit(func(prefix patricia.Prefix, item patricia.Item) error { @@ -205,7 +205,7 @@ func FuzzySearchFunc(options []string, useFuzzySearch bool) func(string) []*type if input == "" { matches = options } else { - matches = utils.FuzzySearch(input, options, useFuzzySearch) + matches = utils.FilterStrings(input, options, useFuzzySearch) } return matchesToSuggestions(matches) diff --git a/pkg/utils/search.go b/pkg/utils/search.go index 7abe8c85d..4ec26bc22 100644 --- a/pkg/utils/search.go +++ b/pkg/utils/search.go @@ -7,7 +7,7 @@ import ( "github.com/samber/lo" ) -func FuzzySearch(needle string, haystack []string, useFuzzySearch bool) []string { +func FilterStrings(needle string, haystack []string, useFuzzySearch bool) []string { if needle == "" { return []string{} } diff --git a/pkg/utils/search_test.go b/pkg/utils/search_test.go index fef10ed59..ad5dd1225 100644 --- a/pkg/utils/search_test.go +++ b/pkg/utils/search_test.go @@ -7,8 +7,7 @@ import ( "github.com/stretchr/testify/assert" ) -// TestFuzzySearch is a function. -func TestFuzzySearch(t *testing.T) { +func TestFilterStrings(t *testing.T) { type scenario struct { needle string haystack []string @@ -68,7 +67,7 @@ func TestFuzzySearch(t *testing.T) { } for _, s := range scenarios { - assert.EqualValues(t, s.expected, FuzzySearch(s.needle, s.haystack, s.useFuzzySearch)) + assert.EqualValues(t, s.expected, FilterStrings(s.needle, s.haystack, s.useFuzzySearch)) } } From 7d2163d63297063364a18a4d62077badc1c1f47e Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Sun, 17 Mar 2024 09:12:19 +0100 Subject: [PATCH 4/5] Rename FuzzySearchFunc to FilterFunc It isn't necessarily fuzzy any more. --- pkg/gui/controllers/custom_command_action.go | 2 +- pkg/gui/controllers/helpers/suggestions_helper.go | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/pkg/gui/controllers/custom_command_action.go b/pkg/gui/controllers/custom_command_action.go index 16d811aff..f4de3218e 100644 --- a/pkg/gui/controllers/custom_command_action.go +++ b/pkg/gui/controllers/custom_command_action.go @@ -38,7 +38,7 @@ func (self *CustomCommandAction) Call() error { func (self *CustomCommandAction) GetCustomCommandsHistorySuggestionsFunc() func(string) []*types.Suggestion { history := self.c.GetAppState().CustomCommandsHistory - return helpers.FuzzySearchFunc(history, self.c.UserConfig.Gui.UseFuzzySearch()) + return helpers.FilterFunc(history, self.c.UserConfig.Gui.UseFuzzySearch()) } // this mimics the shell functionality `ignorespace` diff --git a/pkg/gui/controllers/helpers/suggestions_helper.go b/pkg/gui/controllers/helpers/suggestions_helper.go index b31a19f7a..ff8aeea71 100644 --- a/pkg/gui/controllers/helpers/suggestions_helper.go +++ b/pkg/gui/controllers/helpers/suggestions_helper.go @@ -66,7 +66,7 @@ func matchesToSuggestions(matches []string) []*types.Suggestion { func (self *SuggestionsHelper) GetRemoteSuggestionsFunc() func(string) []*types.Suggestion { remoteNames := self.getRemoteNames() - return FuzzySearchFunc(remoteNames, self.c.UserConfig.Gui.UseFuzzySearch()) + return FilterFunc(remoteNames, self.c.UserConfig.Gui.UseFuzzySearch()) } func (self *SuggestionsHelper) getBranchNames() []string { @@ -163,7 +163,7 @@ func (self *SuggestionsHelper) getRemoteBranchNames(separator string) []string { } func (self *SuggestionsHelper) GetRemoteBranchesSuggestionsFunc(separator string) func(string) []*types.Suggestion { - return FuzzySearchFunc(self.getRemoteBranchNames(separator), self.c.UserConfig.Gui.UseFuzzySearch()) + return FilterFunc(self.getRemoteBranchNames(separator), self.c.UserConfig.Gui.UseFuzzySearch()) } func (self *SuggestionsHelper) getTagNames() []string { @@ -175,7 +175,7 @@ func (self *SuggestionsHelper) getTagNames() []string { func (self *SuggestionsHelper) GetTagsSuggestionsFunc() func(string) []*types.Suggestion { tagNames := self.getTagNames() - return FuzzySearchFunc(tagNames, self.c.UserConfig.Gui.UseFuzzySearch()) + return FilterFunc(tagNames, self.c.UserConfig.Gui.UseFuzzySearch()) } func (self *SuggestionsHelper) GetRefsSuggestionsFunc() func(string) []*types.Suggestion { @@ -186,7 +186,7 @@ func (self *SuggestionsHelper) GetRefsSuggestionsFunc() func(string) []*types.Su refNames := append(append(append(remoteBranchNames, localBranchNames...), tagNames...), additionalRefNames...) - return FuzzySearchFunc(refNames, self.c.UserConfig.Gui.UseFuzzySearch()) + return FilterFunc(refNames, self.c.UserConfig.Gui.UseFuzzySearch()) } func (self *SuggestionsHelper) GetAuthorsSuggestionsFunc() func(string) []*types.Suggestion { @@ -196,10 +196,10 @@ func (self *SuggestionsHelper) GetAuthorsSuggestionsFunc() func(string) []*types slices.Sort(authors) - return FuzzySearchFunc(authors, self.c.UserConfig.Gui.UseFuzzySearch()) + return FilterFunc(authors, self.c.UserConfig.Gui.UseFuzzySearch()) } -func FuzzySearchFunc(options []string, useFuzzySearch bool) func(string) []*types.Suggestion { +func FilterFunc(options []string, useFuzzySearch bool) func(string) []*types.Suggestion { return func(input string) []*types.Suggestion { var matches []string if input == "" { From 4f2bebe453c42ac9fd9896eb32cf044eb3af1697 Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Sun, 17 Mar 2024 12:11:24 +0100 Subject: [PATCH 5/5] Get rid of the retain-sort-order-when-filtering logic again For die-hard fuzzy-searching fans it's probably in the way, so taking it out makes fuzzy filtering work better. For substring filtering it always retains the sort order anyway. --- pkg/gui/context/branches_context.go | 1 - pkg/gui/context/filtered_list.go | 19 ++++--------------- pkg/gui/context/filtered_list_view_model.go | 4 ++-- pkg/gui/context/menu_context.go | 11 +++++++---- pkg/gui/context/reflog_commits_context.go | 1 - pkg/gui/context/remote_branches_context.go | 1 - pkg/gui/context/remotes_context.go | 1 - pkg/gui/context/stash_context.go | 1 - pkg/gui/context/submodules_context.go | 1 - pkg/gui/context/tags_context.go | 1 - pkg/gui/context/worktrees_context.go | 1 - 11 files changed, 13 insertions(+), 29 deletions(-) diff --git a/pkg/gui/context/branches_context.go b/pkg/gui/context/branches_context.go index 6317a60b2..d2647ef84 100644 --- a/pkg/gui/context/branches_context.go +++ b/pkg/gui/context/branches_context.go @@ -22,7 +22,6 @@ func NewBranchesContext(c *ContextCommon) *BranchesContext { func(branch *models.Branch) []string { return []string{branch.Name} }, - func() bool { return c.AppState.LocalBranchSortOrder != "alphabetical" }, ) getDisplayStrings := func(_ int, _ int) [][]string { diff --git a/pkg/gui/context/filtered_list.go b/pkg/gui/context/filtered_list.go index 8ac912cd0..0724ecb3b 100644 --- a/pkg/gui/context/filtered_list.go +++ b/pkg/gui/context/filtered_list.go @@ -1,7 +1,6 @@ package context import ( - "slices" "strings" "github.com/jesseduffield/lazygit/pkg/utils" @@ -17,21 +16,14 @@ type FilteredList[T any] struct { getFilterFields func(T) []string filter string - // Normally, filtered items are presented sorted by best match. If this - // function returns true, they retain their original sort order instead; - // this is useful for lists that show items sorted by date, for example. - // Leaving this nil is equivalent to returning false. - shouldRetainSortOrder func() bool - mutex *deadlock.Mutex } -func NewFilteredList[T any](getList func() []T, getFilterFields func(T) []string, shouldRetainSortOrder func() bool) *FilteredList[T] { +func NewFilteredList[T any](getList func() []T, getFilterFields func(T) []string) *FilteredList[T] { return &FilteredList[T]{ - getList: getList, - getFilterFields: getFilterFields, - shouldRetainSortOrder: shouldRetainSortOrder, - mutex: &deadlock.Mutex{}, + getList: getList, + getFilterFields: getFilterFields, + mutex: &deadlock.Mutex{}, } } @@ -100,9 +92,6 @@ func (self *FilteredList[T]) applyFilter(useFuzzySearch bool) { self.filteredIndices = lo.Map(matches, func(match fuzzy.Match, _ int) int { return match.Index }) - if useFuzzySearch && self.shouldRetainSortOrder != nil && self.shouldRetainSortOrder() { - slices.Sort(self.filteredIndices) - } } } diff --git a/pkg/gui/context/filtered_list_view_model.go b/pkg/gui/context/filtered_list_view_model.go index b52fcbc0a..2c2841964 100644 --- a/pkg/gui/context/filtered_list_view_model.go +++ b/pkg/gui/context/filtered_list_view_model.go @@ -6,8 +6,8 @@ type FilteredListViewModel[T HasID] struct { *SearchHistory } -func NewFilteredListViewModel[T HasID](getList func() []T, getFilterFields func(T) []string, shouldRetainSortOrder func() bool) *FilteredListViewModel[T] { - filteredList := NewFilteredList(getList, getFilterFields, shouldRetainSortOrder) +func NewFilteredListViewModel[T HasID](getList func() []T, getFilterFields func(T) []string) *FilteredListViewModel[T] { + filteredList := NewFilteredList(getList, getFilterFields) self := &FilteredListViewModel[T]{ FilteredList: filteredList, diff --git a/pkg/gui/context/menu_context.go b/pkg/gui/context/menu_context.go index 2158d5c7a..9db09b74f 100644 --- a/pkg/gui/context/menu_context.go +++ b/pkg/gui/context/menu_context.go @@ -61,10 +61,6 @@ func NewMenuViewModel(c *ContextCommon) *MenuViewModel { self.FilteredListViewModel = NewFilteredListViewModel( func() []*types.MenuItem { return self.menuItems }, func(item *types.MenuItem) []string { return item.LabelColumns }, - // The only menu that the user is likely to filter in is the keybindings - // menu; retain the sort order in that one because this allows us to - // keep the section headers while filtering: - func() bool { return true }, ) return self @@ -99,6 +95,13 @@ func (self *MenuViewModel) GetDisplayStrings(_ int, _ int) [][]string { } func (self *MenuViewModel) GetNonModelItems() []*NonModelItem { + // Don't display section headers when we are filtering, and the filter mode + // is fuzzy. The reason is that filtering changes the order of the items + // (they are sorted by best match), so all the sections would be messed up. + if self.FilteredListViewModel.IsFiltering() && self.c.UserConfig.Gui.UseFuzzySearch() { + return []*NonModelItem{} + } + result := []*NonModelItem{} menuItems := self.FilteredListViewModel.GetItems() var prevSection *types.MenuSection = nil diff --git a/pkg/gui/context/reflog_commits_context.go b/pkg/gui/context/reflog_commits_context.go index 6791932ba..65137d633 100644 --- a/pkg/gui/context/reflog_commits_context.go +++ b/pkg/gui/context/reflog_commits_context.go @@ -24,7 +24,6 @@ func NewReflogCommitsContext(c *ContextCommon) *ReflogCommitsContext { func(commit *models.Commit) []string { return []string{commit.ShortSha(), commit.Name} }, - func() bool { return true }, ) getDisplayStrings := func(_ int, _ int) [][]string { diff --git a/pkg/gui/context/remote_branches_context.go b/pkg/gui/context/remote_branches_context.go index 9de792f27..884d3debb 100644 --- a/pkg/gui/context/remote_branches_context.go +++ b/pkg/gui/context/remote_branches_context.go @@ -26,7 +26,6 @@ func NewRemoteBranchesContext( func(remoteBranch *models.RemoteBranch) []string { return []string{remoteBranch.Name} }, - func() bool { return c.AppState.RemoteBranchSortOrder != "alphabetical" }, ) getDisplayStrings := func(_ int, _ int) [][]string { diff --git a/pkg/gui/context/remotes_context.go b/pkg/gui/context/remotes_context.go index 51fc1c036..73ea428aa 100644 --- a/pkg/gui/context/remotes_context.go +++ b/pkg/gui/context/remotes_context.go @@ -22,7 +22,6 @@ func NewRemotesContext(c *ContextCommon) *RemotesContext { func(remote *models.Remote) []string { return []string{remote.Name} }, - nil, ) getDisplayStrings := func(_ int, _ int) [][]string { diff --git a/pkg/gui/context/stash_context.go b/pkg/gui/context/stash_context.go index c832f85ff..c8d487688 100644 --- a/pkg/gui/context/stash_context.go +++ b/pkg/gui/context/stash_context.go @@ -24,7 +24,6 @@ func NewStashContext( func(stashEntry *models.StashEntry) []string { return []string{stashEntry.Name} }, - func() bool { return true }, ) getDisplayStrings := func(_ int, _ int) [][]string { diff --git a/pkg/gui/context/submodules_context.go b/pkg/gui/context/submodules_context.go index dbd12077a..5428da044 100644 --- a/pkg/gui/context/submodules_context.go +++ b/pkg/gui/context/submodules_context.go @@ -19,7 +19,6 @@ func NewSubmodulesContext(c *ContextCommon) *SubmodulesContext { func(submodule *models.SubmoduleConfig) []string { return []string{submodule.FullName()} }, - nil, ) getDisplayStrings := func(_ int, _ int) [][]string { diff --git a/pkg/gui/context/tags_context.go b/pkg/gui/context/tags_context.go index c5ae2ccd5..d827564dd 100644 --- a/pkg/gui/context/tags_context.go +++ b/pkg/gui/context/tags_context.go @@ -24,7 +24,6 @@ func NewTagsContext( func(tag *models.Tag) []string { return []string{tag.Name, tag.Message} }, - nil, ) getDisplayStrings := func(_ int, _ int) [][]string { diff --git a/pkg/gui/context/worktrees_context.go b/pkg/gui/context/worktrees_context.go index 55618de85..3e45f2d45 100644 --- a/pkg/gui/context/worktrees_context.go +++ b/pkg/gui/context/worktrees_context.go @@ -19,7 +19,6 @@ func NewWorktreesContext(c *ContextCommon) *WorktreesContext { func(Worktree *models.Worktree) []string { return []string{Worktree.Name} }, - nil, ) getDisplayStrings := func(_ int, _ int) [][]string {