mirror of
https://github.com/jesseduffield/lazygit.git
synced 2026-09-10 07:36:27 -04:00
Filter a menu by typing into it
The filter input is where the keyboard points for as long as such a menu is open, so that the first printable key can go straight into it. The menu still gets every key the input doesn't take, because the input view is embedded in the menu view, and the two are drawn as one focused panel. Which keys the input takes changes once there is a filter: until then printable keys still drive the menu, so that the configured navigation keys work as usual, and afterwards they are all filter text. A menu item's own keys are never bound in such a menu, because typing one has to reach the filter rather than execute the item. Escape gives up the filter and leaves the menu open; the next one closes it. The filter prompt behind '/' is gone from these menus: the row already does that job, and a second filter would only be confusing. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
1fb5f87d05
commit
fe9b990c4c
|
|
@ -179,11 +179,12 @@ func (self *ContextMgr) Activate(c types.Context, opts types.OnFocusOpts) {
|
|||
self.gui.helpers.Window.SetWindowContext(c)
|
||||
|
||||
self.gui.helpers.Window.MoveToTopOfWindow(c)
|
||||
inputViewName := c.GetInputViewName()
|
||||
oldView := self.gui.c.GocuiGui().CurrentView()
|
||||
if oldView != nil && oldView.Name() != viewName {
|
||||
if oldView != nil && oldView.Name() != inputViewName {
|
||||
oldView.HighlightInactive = true
|
||||
}
|
||||
if _, err := self.gui.c.GocuiGui().SetCurrentView(viewName); err != nil {
|
||||
if _, err := self.gui.c.GocuiGui().SetCurrentView(inputViewName); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -102,6 +102,10 @@ func (self *BaseContext) GetViewName() string {
|
|||
return self.view.Name()
|
||||
}
|
||||
|
||||
func (self *BaseContext) GetInputViewName() string {
|
||||
return self.GetViewName()
|
||||
}
|
||||
|
||||
func (self *BaseContext) GetView() *gocui.View {
|
||||
return self.view
|
||||
}
|
||||
|
|
|
|||
|
|
@ -159,6 +159,9 @@ func (self *MenuViewModel) FilterAsYouType() bool {
|
|||
// Whether the user has started to filter, which is when the filter row appears.
|
||||
func (self *MenuViewModel) SetFilterStarted(value bool) {
|
||||
self.filterStarted = value
|
||||
// As long as there is nothing to type into, printable keys keep driving the
|
||||
// menu, so that the configured navigation keys work like in any other menu.
|
||||
self.c.Views().MenuFilter.KeybindOnEdit = !value
|
||||
}
|
||||
|
||||
func (self *MenuViewModel) FilterStarted() bool {
|
||||
|
|
@ -242,6 +245,16 @@ func (self *MenuViewModel) GetNonModelItems() []*NonModelItem {
|
|||
|
||||
func (self *MenuContext) GetKeybindings(opts types.KeybindingsOpts) []*types.Binding {
|
||||
basicBindings := self.ListContextTrait.GetKeybindings(opts)
|
||||
|
||||
if self.filterAsYouType {
|
||||
// A menu item's keys are shown as a reminder of what they do outside the
|
||||
// menu, but pressing one types it into the filter rather than executing the
|
||||
// item, so we don't bind them at all. That leaves the bindings that drive
|
||||
// the menu itself, and the printable ones among those give way to the filter
|
||||
// as soon as there is something to type into (see View.KeybindOnEdit).
|
||||
return basicBindings
|
||||
}
|
||||
|
||||
menuItemsWithKeys := lo.Filter(self.menuItems, func(item *types.MenuItem, _ int) bool {
|
||||
return len(item.Keys) > 0
|
||||
})
|
||||
|
|
@ -300,6 +313,17 @@ func (self *MenuContext) RangeSelectEnabled() bool {
|
|||
return false
|
||||
}
|
||||
|
||||
// A menu that filters as you type points the keyboard at its filter input, so
|
||||
// that whatever the user types ends up there. Keys that the input doesn't take
|
||||
// still reach the menu, because the input view is embedded in the menu view.
|
||||
func (self *MenuContext) GetInputViewName() string {
|
||||
if self.filterAsYouType {
|
||||
return self.c.Views().MenuFilter.Name()
|
||||
}
|
||||
|
||||
return self.GetViewName()
|
||||
}
|
||||
|
||||
func (self *MenuContext) FilterPrefix(tr *i18n.TranslationSet) string {
|
||||
if self.allowFilteringKeybindings {
|
||||
return tr.FilterPrefixMenu
|
||||
|
|
|
|||
|
|
@ -33,7 +33,17 @@ func (self *FilterController) Context() types.Context {
|
|||
return self.context
|
||||
}
|
||||
|
||||
// A context that filters as the user types has an input field of its own, so it
|
||||
// has no use for the filter prompt.
|
||||
type contextThatFiltersAsYouType interface {
|
||||
FilterAsYouType() bool
|
||||
}
|
||||
|
||||
func (self *FilterController) GetKeybindings(opts types.KeybindingsOpts) []*types.Binding {
|
||||
if context, ok := self.context.(contextThatFiltersAsYouType); ok && context.FilterAsYouType() {
|
||||
return nil
|
||||
}
|
||||
|
||||
return []*types.Binding{
|
||||
{
|
||||
Keys: opts.GetKeys(opts.Config.Universal.StartSearch),
|
||||
|
|
|
|||
|
|
@ -73,6 +73,11 @@ func (self *MenuController) press(selectedItem *types.MenuItem) error {
|
|||
}
|
||||
|
||||
func (self *MenuController) close() error {
|
||||
if self.context().FilterStarted() {
|
||||
self.stopFiltering()
|
||||
return nil
|
||||
}
|
||||
|
||||
if self.context().IsFiltering() {
|
||||
self.c.Helpers().Search.Cancel()
|
||||
return nil
|
||||
|
|
@ -81,6 +86,17 @@ func (self *MenuController) close() error {
|
|||
return self.context().OnMenuPress(nil)
|
||||
}
|
||||
|
||||
// Hides the filter row again and puts the menu back the way it was, keeping the
|
||||
// item that was selected. It takes another escape to close the menu.
|
||||
func (self *MenuController) stopFiltering() {
|
||||
self.c.Views().MenuFilter.ClearTextArea()
|
||||
self.c.Views().MenuFilter.RenderTextArea()
|
||||
|
||||
self.context().SetFilterStarted(false)
|
||||
self.context().ClearFilter()
|
||||
self.c.PostRefreshUpdate(self.context())
|
||||
}
|
||||
|
||||
func (self *MenuController) context() *context.MenuContext {
|
||||
return self.c.Contexts().Menu
|
||||
}
|
||||
|
|
|
|||
|
|
@ -49,6 +49,32 @@ func (gui *Gui) promptEditor(v *gocui.View, key gocui.Key) bool {
|
|||
return matched
|
||||
}
|
||||
|
||||
func (gui *Gui) menuFilterEditor(v *gocui.View, key gocui.Key) bool {
|
||||
contentBefore := v.TextArea.GetContent()
|
||||
|
||||
matched := gui.handleEditorKeypress(v, key, false)
|
||||
if !matched {
|
||||
// Give the global keybindings a chance at the key, e.g. so that ctrl-c
|
||||
// still quits while a menu is open.
|
||||
return false
|
||||
}
|
||||
|
||||
v.RenderTextArea()
|
||||
|
||||
content := v.TextArea.GetContent()
|
||||
if content == contentBefore {
|
||||
// The key just moved the cursor around within the filter; refiltering would
|
||||
// throw away the menu's selection for nothing.
|
||||
return true
|
||||
}
|
||||
|
||||
menuContext := gui.State.Contexts.Menu
|
||||
menuContext.SetFilterStarted(true)
|
||||
gui.helpers.Search.ApplyFilter(menuContext, content)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (gui *Gui) searchEditor(v *gocui.View, key gocui.Key) bool {
|
||||
matched := gui.handleEditorKeypress(v, key, false)
|
||||
v.RenderTextArea()
|
||||
|
|
|
|||
|
|
@ -928,6 +928,7 @@ func (gui *Gui) viewTabMap() map[string][]context.TabView {
|
|||
var popupPanelViewGroups = [][]string{
|
||||
{"commitMessage", "commitDescription"},
|
||||
{"prompt", "suggestions"},
|
||||
{"menu", "menuFilterFrame", "menuFilter"},
|
||||
}
|
||||
|
||||
func viewsBelongToSamePopupPanel(viewName string, otherViewName string) bool {
|
||||
|
|
|
|||
|
|
@ -152,6 +152,17 @@ func (gui *Gui) layout(g *gocui.Gui) error {
|
|||
filterRowVisible := gui.Views.Menu.Visible && gui.State.Contexts.Menu.FilterStarted()
|
||||
gui.Views.MenuFilterFrame.Visible = filterRowVisible
|
||||
gui.Views.MenuFilter.Visible = filterRowVisible
|
||||
if gui.Views.Menu.Visible {
|
||||
// Until the user types something there is no filter row to advertise the
|
||||
// filter, so the menu says that typing is a thing.
|
||||
gui.Views.Menu.Subtitle = lo.Ternary(menuWithFilterRowVisible && !filterRowVisible, gui.c.Tr.MenuFilterHint, "")
|
||||
}
|
||||
if menuWithFilterRowVisible {
|
||||
// The filter input is the current view for as long as such a menu is open,
|
||||
// so without this the cursor would sit on the menu's bottom border, where
|
||||
// the filter row is yet to appear.
|
||||
gui.g.Cursor = filterRowVisible
|
||||
}
|
||||
gui.Views.Tooltip.Visible = gui.Views.Menu.Visible && gui.Views.Tooltip.Buffer() != ""
|
||||
|
||||
for _, context := range gui.transientContexts() {
|
||||
|
|
|
|||
|
|
@ -57,6 +57,10 @@ type IBaseContext interface {
|
|||
|
||||
GetKind() ContextKind
|
||||
GetViewName() string
|
||||
// The view that keyboard input goes to while this context is focused. That is
|
||||
// the context's own view, unless the context has an editable view embedded in
|
||||
// it which takes the keyboard instead, like the menu's filter input.
|
||||
GetInputViewName() string
|
||||
GetView() *gocui.View
|
||||
GetViewTrait() IViewTrait
|
||||
GetWindowName() string
|
||||
|
|
|
|||
|
|
@ -140,7 +140,7 @@ func (gui *Gui) postRefreshUpdate(c types.Context, opts types.OnFocusOpts) {
|
|||
|
||||
c.HandleRender()
|
||||
|
||||
if gui.currentViewName() == c.GetViewName() {
|
||||
if gui.currentViewName() == c.GetInputViewName() {
|
||||
c.HandleFocus(opts)
|
||||
} else {
|
||||
// The FocusLine call is included in the HandleFocus method which we
|
||||
|
|
|
|||
|
|
@ -148,6 +148,8 @@ func (gui *Gui) createAllViews() error {
|
|||
gui.Views.MenuFilterFrame.Visible = false
|
||||
gui.Views.MenuFilter.Visible = false
|
||||
gui.Views.MenuFilter.Frame = false
|
||||
gui.Views.MenuFilter.Editable = true
|
||||
gui.Views.MenuFilter.Editor = gocui.EditorFunc(gui.menuFilterEditor)
|
||||
// The filter row belongs to the menu: it shares the menu's focus, and keys
|
||||
// that the input field doesn't take are the menu's to handle.
|
||||
gui.Views.MenuFilterFrame.ParentView = gui.Views.Menu
|
||||
|
|
|
|||
|
|
@ -879,6 +879,7 @@ type TranslationSet struct {
|
|||
SearchPrefix string
|
||||
FilterPrefix string
|
||||
FilterPrefixMenu string
|
||||
MenuFilterHint string
|
||||
ExitSearchMode string
|
||||
ExitTextFilterMode string
|
||||
Switch string
|
||||
|
|
@ -2040,6 +2041,7 @@ func EnglishTranslationSet() *TranslationSet {
|
|||
SearchPrefix: "Search: ",
|
||||
FilterPrefix: "Filter: ",
|
||||
FilterPrefixMenu: "Filter (prepend '@' to filter keybindings): ",
|
||||
MenuFilterHint: "(Type to filter)",
|
||||
WorktreesTitle: "Worktrees",
|
||||
WorktreeTitle: "Worktree",
|
||||
Switch: "Switch",
|
||||
|
|
|
|||
Loading…
Reference in a new issue