mirror of
https://github.com/jesseduffield/lazygit.git
synced 2026-06-20 01:19:23 +02:00
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.
This commit is contained in:
@@ -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{
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -102,10 +102,10 @@ type IFilterableContext interface {
|
||||
IListPanelState
|
||||
ISearchHistoryContext
|
||||
|
||||
SetFilter(string)
|
||||
SetFilter(string, bool)
|
||||
GetFilter() string
|
||||
ClearFilter()
|
||||
ReApplyFilter()
|
||||
ReApplyFilter(bool)
|
||||
IsFiltering() bool
|
||||
IsFilterableContext()
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
`,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
|
||||
+51
-2
@@ -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) {
|
||||
|
||||
+41
-22
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user