diff --git a/pkg/gui/context/filtered_list.go b/pkg/gui/context/filtered_list.go index 8164cfa16..e8112f95b 100644 --- a/pkg/gui/context/filtered_list.go +++ b/pkg/gui/context/filtered_list.go @@ -12,9 +12,10 @@ import ( type FilteredList[T any] struct { filteredIndices []int // if nil, we are not filtering - getList func() []T - getFilterFields func(T) []string - filter string + getList func() []T + getFilterFields func(T) []string + preprocessFilter func(string) string + filter string mutex deadlock.Mutex } @@ -26,6 +27,10 @@ func NewFilteredList[T any](getList func() []T, getFilterFields func(T) []string } } +func (self *FilteredList[T]) SetPreprocessFilterFunc(preprocessFilter func(string) string) { + self.preprocessFilter = preprocessFilter +} + func (self *FilteredList[T]) GetFilter() string { return self.filter } @@ -79,7 +84,12 @@ func (self *FilteredList[T]) applyFilter(useFuzzySearch bool) { self.mutex.Lock() defer self.mutex.Unlock() - if self.filter == "" { + filter := self.filter + if self.preprocessFilter != nil { + filter = self.preprocessFilter(filter) + } + + if filter == "" { self.filteredIndices = nil } else { source := &fuzzySource[T]{ @@ -87,7 +97,7 @@ func (self *FilteredList[T]) applyFilter(useFuzzySearch bool) { getFilterFields: self.getFilterFields, } - matches := utils.FindFrom(self.filter, source, useFuzzySearch) + matches := utils.FindFrom(filter, source, useFuzzySearch) self.filteredIndices = lo.Map(matches, func(match fuzzy.Match, _ int) int { return match.Index }) diff --git a/pkg/gui/context/filtered_list_view_model.go b/pkg/gui/context/filtered_list_view_model.go index 2c2841964..ce2f8ac36 100644 --- a/pkg/gui/context/filtered_list_view_model.go +++ b/pkg/gui/context/filtered_list_view_model.go @@ -1,5 +1,7 @@ package context +import "github.com/jesseduffield/lazygit/pkg/i18n" + type FilteredListViewModel[T HasID] struct { *FilteredList[T] *ListViewModel[T] @@ -33,3 +35,8 @@ func (self *FilteredListViewModel[T]) ClearFilter() { self.SetSelection(unfilteredIndex) } + +// Default implementation of most filterable contexts. Can be overridden if needed. +func (self *FilteredListViewModel[T]) FilterPrefix(tr *i18n.TranslationSet) string { + return tr.FilterPrefix +} diff --git a/pkg/gui/context/menu_context.go b/pkg/gui/context/menu_context.go index 3d5937a9d..0d59f1fa9 100644 --- a/pkg/gui/context/menu_context.go +++ b/pkg/gui/context/menu_context.go @@ -2,10 +2,12 @@ package context import ( "errors" + "strings" "github.com/jesseduffield/lazygit/pkg/gui/keybindings" "github.com/jesseduffield/lazygit/pkg/gui/style" "github.com/jesseduffield/lazygit/pkg/gui/types" + "github.com/jesseduffield/lazygit/pkg/i18n" "github.com/jesseduffield/lazygit/pkg/utils" "github.com/samber/lo" ) @@ -48,11 +50,12 @@ func NewMenuContext( } type MenuViewModel struct { - c *ContextCommon - menuItems []*types.MenuItem - prompt string - promptLines []string - columnAlignment []utils.Alignment + c *ContextCommon + menuItems []*types.MenuItem + prompt string + promptLines []string + columnAlignment []utils.Alignment + allowFilteringKeybindings bool *FilteredListViewModel[*types.MenuItem] } @@ -62,11 +65,29 @@ func NewMenuViewModel(c *ContextCommon) *MenuViewModel { c: c, } + filterKeybindings := false + self.FilteredListViewModel = NewFilteredListViewModel( func() []*types.MenuItem { return self.menuItems }, - func(item *types.MenuItem) []string { return item.LabelColumns }, + func(item *types.MenuItem) []string { + if filterKeybindings { + return []string{keybindings.LabelFromKey(item.Key)} + } + + return item.LabelColumns + }, ) + self.FilteredListViewModel.SetPreprocessFilterFunc(func(filter string) string { + if self.allowFilteringKeybindings && strings.HasPrefix(filter, "@") { + filterKeybindings = true + return filter[1:] + } + + filterKeybindings = false + return filter + }) + return self } @@ -92,6 +113,10 @@ func (self *MenuViewModel) SetPromptLines(promptLines []string) { self.promptLines = promptLines } +func (self *MenuViewModel) SetAllowFilteringKeybindings(allow bool) { + self.allowFilteringKeybindings = allow +} + // TODO: move into presentation package func (self *MenuViewModel) GetDisplayStrings(_ int, _ int) [][]string { menuItems := self.FilteredListViewModel.GetItems() @@ -214,3 +239,11 @@ func (self *MenuContext) OnMenuPress(selectedItem *types.MenuItem) error { func (self *MenuContext) RangeSelectEnabled() bool { return false } + +func (self *MenuContext) FilterPrefix(tr *i18n.TranslationSet) string { + if self.allowFilteringKeybindings { + return tr.FilterPrefixMenu + } + + return self.FilteredListViewModel.FilterPrefix(tr) +} diff --git a/pkg/gui/controllers/helpers/search_helper.go b/pkg/gui/controllers/helpers/search_helper.go index cc9f22b22..67af0695b 100644 --- a/pkg/gui/controllers/helpers/search_helper.go +++ b/pkg/gui/controllers/helpers/search_helper.go @@ -35,7 +35,7 @@ func (self *SearchHelper) OpenFilterPrompt(context types.IFilterableContext) err state.Context = context - self.searchPrefixView().SetContent(self.c.Tr.FilterPrefix) + self.searchPrefixView().SetContent(context.FilterPrefix(self.c.Tr)) promptView := self.promptView() promptView.ClearTextArea() self.OnPromptContentChanged("") @@ -69,7 +69,7 @@ func (self *SearchHelper) DisplayFilterStatus(context types.IFilterableContext) state.Context = context searchString := context.GetFilter() - self.searchPrefixView().SetContent(self.c.Tr.FilterPrefix) + self.searchPrefixView().SetContent(context.FilterPrefix(self.c.Tr)) promptView := self.promptView() keybindingConfig := self.c.UserConfig().Keybinding diff --git a/pkg/gui/controllers/helpers/window_arrangement_helper.go b/pkg/gui/controllers/helpers/window_arrangement_helper.go index 51c5cf37a..accd61adc 100644 --- a/pkg/gui/controllers/helpers/window_arrangement_helper.go +++ b/pkg/gui/controllers/helpers/window_arrangement_helper.go @@ -79,10 +79,10 @@ func (self *WindowArrangementHelper) GetWindowDimensions(informationStr string, repoState := self.c.State().GetRepoState() var searchPrefix string - if repoState.GetSearchState().SearchType() == types.SearchTypeSearch { - searchPrefix = self.c.Tr.SearchPrefix + if filterableContext, ok := repoState.GetSearchState().Context.(types.IFilterableContext); ok { + searchPrefix = filterableContext.FilterPrefix(self.c.Tr) } else { - searchPrefix = self.c.Tr.FilterPrefix + searchPrefix = self.c.Tr.SearchPrefix } args := WindowArrangementArgs{ diff --git a/pkg/gui/controllers/options_menu_action.go b/pkg/gui/controllers/options_menu_action.go index f295a19f4..01d4f1bb6 100644 --- a/pkg/gui/controllers/options_menu_action.go +++ b/pkg/gui/controllers/options_menu_action.go @@ -46,10 +46,11 @@ func (self *OptionsMenuAction) Call() error { appendBindings(navigation, &types.MenuSection{Title: self.c.Tr.KeybindingsMenuSectionNavigation, Column: 1}) return self.c.Menu(types.CreateMenuOptions{ - Title: self.c.Tr.Keybindings, - Items: menuItems, - HideCancel: true, - ColumnAlignment: []utils.Alignment{utils.AlignRight, utils.AlignLeft}, + Title: self.c.Tr.Keybindings, + Items: menuItems, + HideCancel: true, + ColumnAlignment: []utils.Alignment{utils.AlignRight, utils.AlignLeft}, + AllowFilteringKeybindings: true, }) } diff --git a/pkg/gui/menu_panel.go b/pkg/gui/menu_panel.go index 75fd6700d..b0e69cfe0 100644 --- a/pkg/gui/menu_panel.go +++ b/pkg/gui/menu_panel.go @@ -43,6 +43,7 @@ func (gui *Gui) createMenu(opts types.CreateMenuOptions) error { gui.State.Contexts.Menu.SetMenuItems(opts.Items, opts.ColumnAlignment) gui.State.Contexts.Menu.SetPrompt(opts.Prompt) + gui.State.Contexts.Menu.SetAllowFilteringKeybindings(opts.AllowFilteringKeybindings) gui.State.Contexts.Menu.SetSelection(0) gui.Views.Menu.Title = opts.Title diff --git a/pkg/gui/types/common.go b/pkg/gui/types/common.go index 7d8528ee8..be8c10a15 100644 --- a/pkg/gui/types/common.go +++ b/pkg/gui/types/common.go @@ -147,11 +147,12 @@ const ( ) type CreateMenuOptions struct { - Title string - Prompt string // a message that will be displayed above the menu options - Items []*MenuItem - HideCancel bool - ColumnAlignment []utils.Alignment + Title string + Prompt string // a message that will be displayed above the menu options + Items []*MenuItem + HideCancel bool + ColumnAlignment []utils.Alignment + AllowFilteringKeybindings bool } type CreatePopupPanelOpts struct { diff --git a/pkg/gui/types/context.go b/pkg/gui/types/context.go index 6d5f5d573..917342776 100644 --- a/pkg/gui/types/context.go +++ b/pkg/gui/types/context.go @@ -4,6 +4,7 @@ import ( "github.com/jesseduffield/gocui" "github.com/jesseduffield/lazygit/pkg/config" "github.com/jesseduffield/lazygit/pkg/gui/patch_exploring" + "github.com/jesseduffield/lazygit/pkg/i18n" "github.com/jesseduffield/lazygit/pkg/utils" "github.com/sasha-s/go-deadlock" ) @@ -130,6 +131,7 @@ type IFilterableContext interface { ReApplyFilter(bool) IsFiltering() bool IsFilterableContext() + FilterPrefix(tr *i18n.TranslationSet) string } type ISearchableContext interface { diff --git a/pkg/i18n/english.go b/pkg/i18n/english.go index 3a8823b8b..39227d25d 100644 --- a/pkg/i18n/english.go +++ b/pkg/i18n/english.go @@ -816,6 +816,7 @@ type TranslationSet struct { SearchKeybindings string SearchPrefix string FilterPrefix string + FilterPrefixMenu string ExitSearchMode string ExitTextFilterMode string Switch string @@ -1881,6 +1882,7 @@ func EnglishTranslationSet() *TranslationSet { SearchKeybindings: "%s: Next match, %s: Previous match, %s: Exit search mode", SearchPrefix: "Search: ", FilterPrefix: "Filter: ", + FilterPrefixMenu: "Filter (prepend '@' to filter keybindings): ", WorktreesTitle: "Worktrees", WorktreeTitle: "Worktree", Switch: "Switch", diff --git a/pkg/integration/tests/filter_and_search/filter_menu.go b/pkg/integration/tests/filter_and_search/filter_menu.go index 59c47fb71..e5b6b216e 100644 --- a/pkg/integration/tests/filter_and_search/filter_menu.go +++ b/pkg/integration/tests/filter_and_search/filter_menu.go @@ -48,7 +48,7 @@ var FilterMenu = NewIntegrationTest(NewIntegrationTestArgs{ Tap(func() { t.ExpectPopup().Menu(). Title(Equals("Keybindings")). - LineCount(GreaterThan(1)) + LineCount(GreaterThan(2)) }) }, }) diff --git a/pkg/integration/tests/filter_and_search/filter_menu_by_keybinding.go b/pkg/integration/tests/filter_and_search/filter_menu_by_keybinding.go new file mode 100644 index 000000000..aee4b907a --- /dev/null +++ b/pkg/integration/tests/filter_and_search/filter_menu_by_keybinding.go @@ -0,0 +1,38 @@ +package filter_and_search + +import ( + "github.com/jesseduffield/lazygit/pkg/config" + . "github.com/jesseduffield/lazygit/pkg/integration/components" +) + +var FilterMenuByKeybinding = NewIntegrationTest(NewIntegrationTestArgs{ + Description: "Filtering the keybindings menu by keybinding", + ExtraCmdArgs: []string{}, + Skip: false, + SetupConfig: func(config *config.AppConfig) {}, + SetupRepo: func(shell *Shell) { + }, + Run: func(t *TestDriver, keys config.KeybindingConfig) { + t.Views().Files(). + Press(keys.Universal.OptionMenu). + Tap(func() { + t.ExpectPopup().Menu(). + Title(Equals("Keybindings")). + Filter("@+"). + Lines( + // menu has filtered down to the one item that matches the filter + Contains("--- Global ---"), + Contains("+ Next screen mode").IsSelected(), + ). + Confirm() + }). + + // Upon opening the menu again, the filter should have been reset + Press(keys.Universal.OptionMenu). + Tap(func() { + t.ExpectPopup().Menu(). + Title(Equals("Keybindings")). + LineCount(GreaterThan(1)) + }) + }, +}) diff --git a/pkg/integration/tests/test_list.go b/pkg/integration/tests/test_list.go index 3e94d39ea..000b21339 100644 --- a/pkg/integration/tests/test_list.go +++ b/pkg/integration/tests/test_list.go @@ -220,6 +220,7 @@ var tests = []*components.IntegrationTest{ filter_and_search.FilterFiles, filter_and_search.FilterFuzzy, filter_and_search.FilterMenu, + filter_and_search.FilterMenuByKeybinding, filter_and_search.FilterMenuCancelFilterWithEscape, filter_and_search.FilterMenuWithNoKeybindings, filter_and_search.FilterRemoteBranches,