1
0
mirror of https://github.com/jesseduffield/lazygit.git synced 2025-08-24 19:39:16 +02:00

Allow filtering the keybindings menu by keybinding (#4821)

- **PR Description**

When filtering the keybindings menu using `/`, typing `@` filters by
keybinding.

Closes #4739.
This commit is contained in:
Stefan Haller
2025-08-14 15:04:56 +02:00
committed by GitHub
13 changed files with 122 additions and 26 deletions

View File

@@ -14,6 +14,7 @@ type FilteredList[T any] struct {
getList func() []T getList func() []T
getFilterFields func(T) []string getFilterFields func(T) []string
preprocessFilter func(string) string
filter string filter string
mutex deadlock.Mutex 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 { func (self *FilteredList[T]) GetFilter() string {
return self.filter return self.filter
} }
@@ -79,7 +84,12 @@ func (self *FilteredList[T]) applyFilter(useFuzzySearch bool) {
self.mutex.Lock() self.mutex.Lock()
defer self.mutex.Unlock() defer self.mutex.Unlock()
if self.filter == "" { filter := self.filter
if self.preprocessFilter != nil {
filter = self.preprocessFilter(filter)
}
if filter == "" {
self.filteredIndices = nil self.filteredIndices = nil
} else { } else {
source := &fuzzySource[T]{ source := &fuzzySource[T]{
@@ -87,7 +97,7 @@ func (self *FilteredList[T]) applyFilter(useFuzzySearch bool) {
getFilterFields: self.getFilterFields, 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 { self.filteredIndices = lo.Map(matches, func(match fuzzy.Match, _ int) int {
return match.Index return match.Index
}) })

View File

@@ -1,5 +1,7 @@
package context package context
import "github.com/jesseduffield/lazygit/pkg/i18n"
type FilteredListViewModel[T HasID] struct { type FilteredListViewModel[T HasID] struct {
*FilteredList[T] *FilteredList[T]
*ListViewModel[T] *ListViewModel[T]
@@ -33,3 +35,8 @@ func (self *FilteredListViewModel[T]) ClearFilter() {
self.SetSelection(unfilteredIndex) 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
}

View File

@@ -2,10 +2,12 @@ package context
import ( import (
"errors" "errors"
"strings"
"github.com/jesseduffield/lazygit/pkg/gui/keybindings" "github.com/jesseduffield/lazygit/pkg/gui/keybindings"
"github.com/jesseduffield/lazygit/pkg/gui/style" "github.com/jesseduffield/lazygit/pkg/gui/style"
"github.com/jesseduffield/lazygit/pkg/gui/types" "github.com/jesseduffield/lazygit/pkg/gui/types"
"github.com/jesseduffield/lazygit/pkg/i18n"
"github.com/jesseduffield/lazygit/pkg/utils" "github.com/jesseduffield/lazygit/pkg/utils"
"github.com/samber/lo" "github.com/samber/lo"
) )
@@ -53,6 +55,7 @@ type MenuViewModel struct {
prompt string prompt string
promptLines []string promptLines []string
columnAlignment []utils.Alignment columnAlignment []utils.Alignment
allowFilteringKeybindings bool
*FilteredListViewModel[*types.MenuItem] *FilteredListViewModel[*types.MenuItem]
} }
@@ -62,11 +65,29 @@ func NewMenuViewModel(c *ContextCommon) *MenuViewModel {
c: c, c: c,
} }
filterKeybindings := false
self.FilteredListViewModel = NewFilteredListViewModel( self.FilteredListViewModel = NewFilteredListViewModel(
func() []*types.MenuItem { return self.menuItems }, 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 return self
} }
@@ -92,6 +113,10 @@ func (self *MenuViewModel) SetPromptLines(promptLines []string) {
self.promptLines = promptLines self.promptLines = promptLines
} }
func (self *MenuViewModel) SetAllowFilteringKeybindings(allow bool) {
self.allowFilteringKeybindings = allow
}
// TODO: move into presentation package // TODO: move into presentation package
func (self *MenuViewModel) GetDisplayStrings(_ int, _ int) [][]string { func (self *MenuViewModel) GetDisplayStrings(_ int, _ int) [][]string {
menuItems := self.FilteredListViewModel.GetItems() menuItems := self.FilteredListViewModel.GetItems()
@@ -214,3 +239,11 @@ func (self *MenuContext) OnMenuPress(selectedItem *types.MenuItem) error {
func (self *MenuContext) RangeSelectEnabled() bool { func (self *MenuContext) RangeSelectEnabled() bool {
return false return false
} }
func (self *MenuContext) FilterPrefix(tr *i18n.TranslationSet) string {
if self.allowFilteringKeybindings {
return tr.FilterPrefixMenu
}
return self.FilteredListViewModel.FilterPrefix(tr)
}

View File

@@ -35,7 +35,7 @@ func (self *SearchHelper) OpenFilterPrompt(context types.IFilterableContext) err
state.Context = context state.Context = context
self.searchPrefixView().SetContent(self.c.Tr.FilterPrefix) self.searchPrefixView().SetContent(context.FilterPrefix(self.c.Tr))
promptView := self.promptView() promptView := self.promptView()
promptView.ClearTextArea() promptView.ClearTextArea()
self.OnPromptContentChanged("") self.OnPromptContentChanged("")
@@ -69,7 +69,7 @@ func (self *SearchHelper) DisplayFilterStatus(context types.IFilterableContext)
state.Context = context state.Context = context
searchString := context.GetFilter() searchString := context.GetFilter()
self.searchPrefixView().SetContent(self.c.Tr.FilterPrefix) self.searchPrefixView().SetContent(context.FilterPrefix(self.c.Tr))
promptView := self.promptView() promptView := self.promptView()
keybindingConfig := self.c.UserConfig().Keybinding keybindingConfig := self.c.UserConfig().Keybinding

View File

@@ -79,10 +79,10 @@ func (self *WindowArrangementHelper) GetWindowDimensions(informationStr string,
repoState := self.c.State().GetRepoState() repoState := self.c.State().GetRepoState()
var searchPrefix string var searchPrefix string
if repoState.GetSearchState().SearchType() == types.SearchTypeSearch { if filterableContext, ok := repoState.GetSearchState().Context.(types.IFilterableContext); ok {
searchPrefix = self.c.Tr.SearchPrefix searchPrefix = filterableContext.FilterPrefix(self.c.Tr)
} else { } else {
searchPrefix = self.c.Tr.FilterPrefix searchPrefix = self.c.Tr.SearchPrefix
} }
args := WindowArrangementArgs{ args := WindowArrangementArgs{

View File

@@ -50,6 +50,7 @@ func (self *OptionsMenuAction) Call() error {
Items: menuItems, Items: menuItems,
HideCancel: true, HideCancel: true,
ColumnAlignment: []utils.Alignment{utils.AlignRight, utils.AlignLeft}, ColumnAlignment: []utils.Alignment{utils.AlignRight, utils.AlignLeft},
AllowFilteringKeybindings: true,
}) })
} }

View File

@@ -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.SetMenuItems(opts.Items, opts.ColumnAlignment)
gui.State.Contexts.Menu.SetPrompt(opts.Prompt) gui.State.Contexts.Menu.SetPrompt(opts.Prompt)
gui.State.Contexts.Menu.SetAllowFilteringKeybindings(opts.AllowFilteringKeybindings)
gui.State.Contexts.Menu.SetSelection(0) gui.State.Contexts.Menu.SetSelection(0)
gui.Views.Menu.Title = opts.Title gui.Views.Menu.Title = opts.Title

View File

@@ -152,6 +152,7 @@ type CreateMenuOptions struct {
Items []*MenuItem Items []*MenuItem
HideCancel bool HideCancel bool
ColumnAlignment []utils.Alignment ColumnAlignment []utils.Alignment
AllowFilteringKeybindings bool
} }
type CreatePopupPanelOpts struct { type CreatePopupPanelOpts struct {

View File

@@ -4,6 +4,7 @@ import (
"github.com/jesseduffield/gocui" "github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/config" "github.com/jesseduffield/lazygit/pkg/config"
"github.com/jesseduffield/lazygit/pkg/gui/patch_exploring" "github.com/jesseduffield/lazygit/pkg/gui/patch_exploring"
"github.com/jesseduffield/lazygit/pkg/i18n"
"github.com/jesseduffield/lazygit/pkg/utils" "github.com/jesseduffield/lazygit/pkg/utils"
"github.com/sasha-s/go-deadlock" "github.com/sasha-s/go-deadlock"
) )
@@ -130,6 +131,7 @@ type IFilterableContext interface {
ReApplyFilter(bool) ReApplyFilter(bool)
IsFiltering() bool IsFiltering() bool
IsFilterableContext() IsFilterableContext()
FilterPrefix(tr *i18n.TranslationSet) string
} }
type ISearchableContext interface { type ISearchableContext interface {

View File

@@ -816,6 +816,7 @@ type TranslationSet struct {
SearchKeybindings string SearchKeybindings string
SearchPrefix string SearchPrefix string
FilterPrefix string FilterPrefix string
FilterPrefixMenu string
ExitSearchMode string ExitSearchMode string
ExitTextFilterMode string ExitTextFilterMode string
Switch string Switch string
@@ -1881,6 +1882,7 @@ func EnglishTranslationSet() *TranslationSet {
SearchKeybindings: "%s: Next match, %s: Previous match, %s: Exit search mode", SearchKeybindings: "%s: Next match, %s: Previous match, %s: Exit search mode",
SearchPrefix: "Search: ", SearchPrefix: "Search: ",
FilterPrefix: "Filter: ", FilterPrefix: "Filter: ",
FilterPrefixMenu: "Filter (prepend '@' to filter keybindings): ",
WorktreesTitle: "Worktrees", WorktreesTitle: "Worktrees",
WorktreeTitle: "Worktree", WorktreeTitle: "Worktree",
Switch: "Switch", Switch: "Switch",

View File

@@ -48,7 +48,7 @@ var FilterMenu = NewIntegrationTest(NewIntegrationTestArgs{
Tap(func() { Tap(func() {
t.ExpectPopup().Menu(). t.ExpectPopup().Menu().
Title(Equals("Keybindings")). Title(Equals("Keybindings")).
LineCount(GreaterThan(1)) LineCount(GreaterThan(2))
}) })
}, },
}) })

View File

@@ -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))
})
},
})

View File

@@ -220,6 +220,7 @@ var tests = []*components.IntegrationTest{
filter_and_search.FilterFiles, filter_and_search.FilterFiles,
filter_and_search.FilterFuzzy, filter_and_search.FilterFuzzy,
filter_and_search.FilterMenu, filter_and_search.FilterMenu,
filter_and_search.FilterMenuByKeybinding,
filter_and_search.FilterMenuCancelFilterWithEscape, filter_and_search.FilterMenuCancelFilterWithEscape,
filter_and_search.FilterMenuWithNoKeybindings, filter_and_search.FilterMenuWithNoKeybindings,
filter_and_search.FilterRemoteBranches, filter_and_search.FilterRemoteBranches,