mirror of
https://github.com/jesseduffield/lazygit.git
synced 2026-09-10 07:36:27 -04:00
Filter the keybindings menu as you type
Looking up a keybinding is a search, so the menu that lists them is the one that most wants this. Its items do have keys, but only as a reminder of what they do outside the menu, so nothing is lost by not binding them. The prompt in front of the input field says what '@' does. It only ever showed up while the user was typing in the search prompt, so it could afford to be wordy; on a row that is on screen for as long as the menu is, it can't. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
68355862c1
commit
fb761892aa
|
|
@ -64,6 +64,7 @@ func (self *OptionsMenuAction) Call() error {
|
|||
ColumnAlignment: []utils.Alignment{utils.AlignRight, utils.AlignLeft},
|
||||
AllowFilteringKeybindings: true,
|
||||
KeepConflictingKeybindings: true,
|
||||
FilterAsYouType: true,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2040,7 +2040,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): ",
|
||||
FilterPrefixMenu: "Filter ('@' for keybindings): ",
|
||||
MenuFilterHint: "(Type to filter)",
|
||||
WorktreesTitle: "Worktrees",
|
||||
WorktreeTitle: "Worktree",
|
||||
|
|
|
|||
|
|
@ -56,8 +56,12 @@ func (self *MenuDriver) ContainsLines(matchers ...*TextMatcher) *MenuDriver {
|
|||
return self
|
||||
}
|
||||
|
||||
// types the text into the menu's filter row. Only for menus that filter as you
|
||||
// type; other menus are filtered through the search prompt.
|
||||
func (self *MenuDriver) Filter(text string) *MenuDriver {
|
||||
self.getViewDriver().FilterOrSearch(text)
|
||||
self.getViewDriver().IsFocused()
|
||||
self.t.typeContent(text)
|
||||
self.t.Views().MenuFilter().IsVisible()
|
||||
|
||||
return self
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,94 @@
|
|||
package filter_and_search
|
||||
|
||||
import (
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
. "github.com/jesseduffield/lazygit/pkg/integration/components"
|
||||
)
|
||||
|
||||
var FilterMenuAsYouType = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
Description: "Filtering a menu by typing into the filter row that appears as you type",
|
||||
ExtraCmdArgs: []string{},
|
||||
Skip: false,
|
||||
SetupConfig: func(config *config.AppConfig) {},
|
||||
SetupRepo: func(shell *Shell) {},
|
||||
Run: func(t *TestDriver, keys config.KeybindingConfig) {
|
||||
t.Views().Files().IsFocused().Press(keys.Universal.OptionMenu)
|
||||
|
||||
// The menu offers the filter, but stays as it is until we take it up on it
|
||||
t.Views().Menu().
|
||||
IsFocused().
|
||||
Subtitle(Equals("(Type to filter)")).
|
||||
Footer(Contains(" of "))
|
||||
t.Views().MenuFilter().IsInvisible()
|
||||
t.Views().MenuFilterFrame().IsInvisible()
|
||||
t.CursorIsHidden()
|
||||
|
||||
t.ExpectPopup().Menu().Filter("whitespace")
|
||||
|
||||
t.Views().Menu().
|
||||
Lines(
|
||||
Contains("─── Global"),
|
||||
Contains("Toggle whitespace").IsSelected(),
|
||||
).
|
||||
// the row covers the border the footer was on, so it moves there
|
||||
Subtitle(Equals("")).
|
||||
Footer(Equals(""))
|
||||
t.Views().MenuFilterFrame().
|
||||
IsVisible().
|
||||
Content(Equals("Filter ('@' for keybindings): ")).
|
||||
Footer(Equals("1 of 1")).
|
||||
SharesTopBorderWithBottomOf(t.Views().Menu())
|
||||
t.Views().MenuFilter().IsVisible().Content(Equals("whitespace"))
|
||||
t.Views().Tooltip().
|
||||
IsVisible().
|
||||
Content(Contains("Toggle whether or not whitespace changes are shown")).
|
||||
IsImmediatelyBelow(t.Views().MenuFilterFrame())
|
||||
t.CursorIsVisible()
|
||||
|
||||
// Emptying the filter shows all the items again, and keeps the row
|
||||
t.GlobalPress(config.Keybinding{"<c-u>"})
|
||||
t.Views().MenuFilter().IsVisible().Content(Equals(""))
|
||||
t.Views().Menu().LineCount(GreaterThan(2))
|
||||
t.CursorIsVisible()
|
||||
|
||||
// Moving the text cursor within the filter leaves the menu's selection alone
|
||||
t.ExpectPopup().Menu().Filter("co")
|
||||
t.Views().Menu().LineCount(GreaterThan(2))
|
||||
t.GlobalPress(config.Keybinding{"<down>"})
|
||||
t.Views().Menu().SelectedLineIdxAtLeast(2)
|
||||
t.GlobalPress(config.Keybinding{"<left>"})
|
||||
t.Views().Menu().SelectedLineIdxAtLeast(2)
|
||||
t.GlobalPress(config.Keybinding{"<right>"})
|
||||
|
||||
// Clicking an item selects it and leaves the filter where it is
|
||||
t.Views().Menu().Click(0, 1).SelectedLineIdx(1)
|
||||
t.GlobalPress(config.Keybinding{"m"})
|
||||
t.Views().MenuFilter().Content(Equals("com"))
|
||||
|
||||
t.GlobalPress(config.Keybinding{"<c-u>"})
|
||||
|
||||
// Escape gives up the filter, keeping the item that was selected
|
||||
t.ExpectPopup().Menu().Filter("whitespace")
|
||||
t.Views().Menu().SelectedLine(Contains("Toggle whitespace"))
|
||||
t.GlobalPress(keys.Universal.Return)
|
||||
t.Views().Menu().
|
||||
IsFocused().
|
||||
SelectedLine(Contains("Toggle whitespace")).
|
||||
Subtitle(Equals("(Type to filter)")).
|
||||
Footer(Contains(" of "))
|
||||
t.Views().MenuFilter().IsInvisible()
|
||||
t.Views().MenuFilterFrame().IsInvisible()
|
||||
t.CursorIsHidden()
|
||||
|
||||
// The next escape closes the menu
|
||||
t.GlobalPress(keys.Universal.Return)
|
||||
t.Views().Files().IsFocused()
|
||||
|
||||
// A menu opened afterwards starts with no filter
|
||||
t.Views().Files().Press(keys.Universal.OptionMenu)
|
||||
t.ExpectPopup().Menu().
|
||||
Title(Equals("Keybindings")).
|
||||
LineCount(GreaterThan(2)).
|
||||
Cancel()
|
||||
},
|
||||
})
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
package filter_and_search
|
||||
|
||||
import (
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
. "github.com/jesseduffield/lazygit/pkg/integration/components"
|
||||
)
|
||||
|
||||
var FilterMenuKeyHandling = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
Description: "Which keys drive a menu that filters as you type, and which ones are filter text",
|
||||
ExtraCmdArgs: []string{},
|
||||
Skip: false,
|
||||
SetupConfig: func(cfg *config.AppConfig) {
|
||||
// so that quitting is observable instead of ending the test
|
||||
cfg.GetUserConfig().ConfirmOnQuit = true
|
||||
},
|
||||
SetupRepo: func(shell *Shell) {},
|
||||
Run: func(t *TestDriver, keys config.KeybindingConfig) {
|
||||
// presses a key that is expected to move the selection away from the first
|
||||
// item, and one that is expected to bring it back
|
||||
navigates := func(forward string, back string) {
|
||||
t.GlobalPress(config.Keybinding{forward})
|
||||
t.Views().Menu().SelectedLineIdxAtLeast(2)
|
||||
t.GlobalPress(config.Keybinding{back})
|
||||
t.Views().Menu().SelectedLineIdx(1)
|
||||
}
|
||||
|
||||
t.Views().Files().IsFocused().Press(keys.Universal.OptionMenu)
|
||||
t.Views().Menu().IsFocused().SelectedLineIdx(1)
|
||||
|
||||
// Until there is a filter, the configured navigation keys drive the menu,
|
||||
// printable or not
|
||||
navigates("<down>", "<up>")
|
||||
navigates("j", "k")
|
||||
navigates(".", ",")
|
||||
navigates(">", "<")
|
||||
t.Views().MenuFilter().IsInvisible()
|
||||
|
||||
// A menu item's own key is filter text; it doesn't execute the item. 'c'
|
||||
// commits when the files view has the focus.
|
||||
t.ExpectPopup().Menu().Filter("c")
|
||||
t.Views().Menu().IsFocused()
|
||||
t.Views().MenuFilter().Content(Equals("c"))
|
||||
|
||||
// So is the key that filters other lists
|
||||
t.GlobalPress(keys.Universal.StartSearch)
|
||||
t.Views().MenuFilter().Content(Equals("c/"))
|
||||
t.Views().Search().IsInvisible()
|
||||
|
||||
// And so are the printable navigation keys, now that there is somewhere for
|
||||
// them to go
|
||||
t.GlobalPress(config.Keybinding{"<c-u>"})
|
||||
t.GlobalPress(config.Keybinding{"j"})
|
||||
t.GlobalPress(config.Keybinding{"."})
|
||||
t.GlobalPress(config.Keybinding{">"})
|
||||
t.Views().MenuFilter().Content(Equals("j.>"))
|
||||
|
||||
// The keys that can't be typed keep driving the menu
|
||||
t.GlobalPress(config.Keybinding{"<c-u>"})
|
||||
navigates("<down>", "<up>")
|
||||
navigates("<pgdown>", "<pgup>")
|
||||
navigates("<end>", "<home>")
|
||||
|
||||
// Keys that the filter doesn't take and the menu doesn't handle reach the
|
||||
// global keybindings
|
||||
t.GlobalPress(config.Keybinding{"<c-c>"})
|
||||
t.ExpectPopup().Confirmation().
|
||||
Title(Equals("")).
|
||||
Content(Contains("Are you sure you want to quit?")).
|
||||
Confirm()
|
||||
},
|
||||
})
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
package filter_and_search
|
||||
|
||||
import (
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
. "github.com/jesseduffield/lazygit/pkg/integration/components"
|
||||
)
|
||||
|
||||
var FilterMenuWithPrintableKeybindings = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
Description: "Driving a menu that filters as you type when every key configured for it is printable",
|
||||
ExtraCmdArgs: []string{},
|
||||
Skip: false,
|
||||
SetupConfig: func(cfg *config.AppConfig) {
|
||||
cfg.GetUserConfig().Keybinding.Universal.ConfirmMenu = config.Keybinding{"x"}
|
||||
cfg.GetUserConfig().Keybinding.Universal.Return = config.Keybinding{"q"}
|
||||
cfg.GetUserConfig().Keybinding.Universal.PrevItem = config.Keybinding{"k"}
|
||||
cfg.GetUserConfig().Keybinding.Universal.NextItem = config.Keybinding{"j"}
|
||||
cfg.GetUserConfig().Keybinding.Universal.PrevPage = config.Keybinding{"u"}
|
||||
cfg.GetUserConfig().Keybinding.Universal.NextPage = config.Keybinding{"d"}
|
||||
cfg.GetUserConfig().Keybinding.Universal.GotoTop = config.Keybinding{"g"}
|
||||
cfg.GetUserConfig().Keybinding.Universal.GotoBottom = config.Keybinding{"G"}
|
||||
},
|
||||
SetupRepo: func(shell *Shell) {},
|
||||
Run: func(t *TestDriver, keys config.KeybindingConfig) {
|
||||
navigates := func(forward string, back string) {
|
||||
t.GlobalPress(config.Keybinding{forward})
|
||||
t.Views().Menu().SelectedLineIdxAtLeast(2)
|
||||
t.GlobalPress(config.Keybinding{back})
|
||||
t.Views().Menu().SelectedLineIdx(1)
|
||||
}
|
||||
|
||||
t.Views().Files().IsFocused().Press(keys.Universal.OptionMenu)
|
||||
t.Views().Menu().IsFocused().SelectedLineIdx(1)
|
||||
|
||||
// Until there is a filter, the configured keys drive the menu
|
||||
navigates("j", "k")
|
||||
navigates("d", "u")
|
||||
navigates("G", "g")
|
||||
|
||||
// Once there is one, they are all filter text. It takes a key that isn't a
|
||||
// navigation key to get there.
|
||||
t.ExpectPopup().Menu().Filter("a")
|
||||
t.GlobalPress(config.Keybinding{"j"})
|
||||
t.GlobalPress(config.Keybinding{"k"})
|
||||
t.GlobalPress(config.Keybinding{"d"})
|
||||
t.GlobalPress(config.Keybinding{"u"})
|
||||
t.Views().MenuFilter().Content(Equals("ajkdu"))
|
||||
t.GlobalPress(config.Keybinding{"<c-u>"})
|
||||
|
||||
// The menu is still navigable, because the physical keys drive it whatever
|
||||
// the configuration says
|
||||
navigates("<down>", "<up>")
|
||||
navigates("<pgdown>", "<pgup>")
|
||||
navigates("<end>", "<home>")
|
||||
|
||||
// And so are confirming and cancelling. Escape gives up the filter first.
|
||||
t.ExpectPopup().Menu().Filter("Toggle whitespace")
|
||||
t.GlobalPress(config.Keybinding{"<esc>"})
|
||||
t.Views().MenuFilter().IsInvisible()
|
||||
t.Views().Menu().IsFocused().SelectedLine(Contains("Toggle whitespace"))
|
||||
|
||||
t.ExpectPopup().Menu().Filter("Toggle whitespace")
|
||||
t.Views().Menu().SelectedLine(Contains("Toggle whitespace"))
|
||||
t.GlobalPress(config.Keybinding{"<enter>"})
|
||||
t.Views().Files().IsFocused()
|
||||
},
|
||||
})
|
||||
|
|
@ -262,9 +262,12 @@ var tests = []*components.IntegrationTest{
|
|||
filter_and_search.FilterFilesStageDirectory,
|
||||
filter_and_search.FilterFuzzy,
|
||||
filter_and_search.FilterMenu,
|
||||
filter_and_search.FilterMenuAsYouType,
|
||||
filter_and_search.FilterMenuByKeybinding,
|
||||
filter_and_search.FilterMenuCancelFilterWithEscape,
|
||||
filter_and_search.FilterMenuKeyHandling,
|
||||
filter_and_search.FilterMenuWithNoKeybindings,
|
||||
filter_and_search.FilterMenuWithPrintableKeybindings,
|
||||
filter_and_search.FilterPreservesSelectionOnModelChange,
|
||||
filter_and_search.FilterRemoteBranches,
|
||||
filter_and_search.FilterRemotes,
|
||||
|
|
|
|||
|
|
@ -17,16 +17,19 @@ var EmptyMenu = NewIntegrationTest(NewIntegrationTestArgs{
|
|||
IsFocused().
|
||||
Press(keys.Universal.OptionMenu)
|
||||
|
||||
t.ExpectPopup().Menu().
|
||||
// a string that filters everything out
|
||||
Filter("ljasldkjaslkdjalskdjalsdjaslkd")
|
||||
|
||||
t.Views().Menu().
|
||||
IsFocused().
|
||||
// a string that filters everything out
|
||||
FilterOrSearch("ljasldkjaslkdjalskdjalsdjaslkd").
|
||||
IsEmpty().
|
||||
Press(keys.Universal.Select).
|
||||
// space is filter text in this menu, so we confirm with enter
|
||||
Press(keys.Universal.ConfirmMenu).
|
||||
Tap(func() {
|
||||
t.ExpectToast(Equals("Disabled: No item selected"))
|
||||
}).
|
||||
// escape the search
|
||||
// escape the filter
|
||||
PressEscape().
|
||||
// escape the view
|
||||
PressEscape()
|
||||
|
|
|
|||
Loading…
Reference in a new issue