From e68f195e333086deb9f5407dd79864314ae34598 Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Thu, 12 Nov 2026 09:12:31 +0100 Subject: [PATCH] Let a menu's filter receive pasted text in one piece The filter takes a single line of text. Drop the newlines at the start and end of the pasted text, and replace the remaining newlines and tabs with spaces. A pasted newline therefore no longer runs the selected menu item. Co-Authored-By: Claude Opus 5.5 --- pkg/gui/editors.go | 30 +++++++++++++++++ pkg/gui/editors_test.go | 27 ++++++++++++++++ pkg/gui/views.go | 4 +++ .../paste_into_keybindings_menu.go | 3 -- .../paste_into_menu_filter.go | 32 +++++++++++++++++++ pkg/integration/tests/test_list.go | 1 + 6 files changed, 94 insertions(+), 3 deletions(-) create mode 100644 pkg/gui/editors_test.go create mode 100644 pkg/integration/tests/filter_and_search/paste_into_menu_filter.go diff --git a/pkg/gui/editors.go b/pkg/gui/editors.go index 35d7adaac..6e4f3591b 100644 --- a/pkg/gui/editors.go +++ b/pkg/gui/editors.go @@ -1,6 +1,8 @@ package gui import ( + "strings" + "github.com/jesseduffield/lazygit/pkg/gocui" ) @@ -76,6 +78,34 @@ func (gui *Gui) menuFilterEditor(v *gocui.View, key gocui.Key) bool { return true } +func (gui *Gui) pasteIntoMenuFilter(text string) { + v := gui.Views.MenuFilter + if typePastedTextIntoSingleLineView(v, text) { + gui.onMenuFilterContentChanged(v.TextArea.GetContent()) + } +} + +// typePastedTextIntoSingleLineView returns false if nothing is left of the text +// to type. +func typePastedTextIntoSingleLineView(v *gocui.View, text string) bool { + text = pastedTextAsSingleLine(text) + if text == "" { + return false + } + + v.TextArea.TypeString(text) + v.RenderTextArea() + return true +} + +// Views that take a single line of text get pasted text without the newlines at +// its start and end, which are often part of a copied line, and with spaces in +// place of the newlines and tabs inside it. +func pastedTextAsSingleLine(text string) string { + text = strings.Trim(text, "\n") + return strings.NewReplacer("\n", " ", "\t", " ").Replace(text) +} + func (gui *Gui) onMenuFilterContentChanged(content string) { menuContext := gui.State.Contexts.Menu menuContext.SetFilterStarted(true) diff --git a/pkg/gui/editors_test.go b/pkg/gui/editors_test.go new file mode 100644 index 000000000..ea26afb81 --- /dev/null +++ b/pkg/gui/editors_test.go @@ -0,0 +1,27 @@ +package gui + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestPastedTextAsSingleLine(t *testing.T) { + scenarios := []struct { + name string + text string + expected string + }{ + {"single line", "foo bar", "foo bar"}, + {"newlines at start and end", "\n\nfoo\n", "foo"}, + {"newlines inside", "foo\nbar\n\nbaz", "foo bar baz"}, + {"tabs", "\tfoo\tbar", " foo bar"}, + {"only newlines", "\n\n", ""}, + } + + for _, s := range scenarios { + t.Run(s.name, func(t *testing.T) { + assert.Equal(t, s.expected, pastedTextAsSingleLine(s.text)) + }) + } +} diff --git a/pkg/gui/views.go b/pkg/gui/views.go index 6bc414da6..eb93d6db7 100644 --- a/pkg/gui/views.go +++ b/pkg/gui/views.go @@ -150,6 +150,10 @@ func (gui *Gui) createAllViews() error { gui.Views.MenuFilter.Frame = false gui.Views.MenuFilter.Editable = true gui.Views.MenuFilter.Editor = gocui.EditorFunc(gui.menuFilterEditor) + gui.Views.MenuFilter.OnPaste = func(text string) error { + gui.pasteIntoMenuFilter(text) + return nil + } // 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 diff --git a/pkg/integration/tests/filter_and_search/paste_into_keybindings_menu.go b/pkg/integration/tests/filter_and_search/paste_into_keybindings_menu.go index d87f1cb54..92413c644 100644 --- a/pkg/integration/tests/filter_and_search/paste_into_keybindings_menu.go +++ b/pkg/integration/tests/filter_and_search/paste_into_keybindings_menu.go @@ -27,10 +27,7 @@ var PasteIntoKeybindingsMenu = NewIntegrationTest(NewIntegrationTestArgs{ t.Views().Files(). Lines( - /* EXPECTED: Contains(`??`).Contains(`myfile`), - ACTUAL: */ - Contains(`A `).Contains(`myfile`), ) }, }) diff --git a/pkg/integration/tests/filter_and_search/paste_into_menu_filter.go b/pkg/integration/tests/filter_and_search/paste_into_menu_filter.go new file mode 100644 index 000000000..e04c9f36f --- /dev/null +++ b/pkg/integration/tests/filter_and_search/paste_into_menu_filter.go @@ -0,0 +1,32 @@ +package filter_and_search + +import ( + "github.com/jesseduffield/lazygit/pkg/config" + . "github.com/jesseduffield/lazygit/pkg/integration/components" +) + +var PasteIntoMenuFilter = NewIntegrationTest(NewIntegrationTestArgs{ + Description: "Paste text into the filter of a menu that filters as you type", + ExtraCmdArgs: []string{}, + Skip: false, + SetupConfig: func(config *config.AppConfig) {}, + SetupRepo: func(shell *Shell) { + shell.CreateFile("myfile", "myfile") + }, + Run: func(t *TestDriver, keys config.KeybindingConfig) { + t.Views().Files(). + IsFocused(). + Press(keys.Universal.OptionMenu) + + t.ExpectPopup().Menu(). + Title(Equals("Keybindings")). + Paste("\nIgnore\n"). + Lines( + Contains(`─── Local`), + Contains(`Ignore`).IsSelected(), + ) + + t.Views().MenuFilter(). + Content(Equals("Ignore")) + }, +}) diff --git a/pkg/integration/tests/test_list.go b/pkg/integration/tests/test_list.go index aeded3da4..cb53307b2 100644 --- a/pkg/integration/tests/test_list.go +++ b/pkg/integration/tests/test_list.go @@ -338,6 +338,7 @@ var tests = []*components.IntegrationTest{ filter_and_search.NestedFilterTransient, filter_and_search.NewSearch, filter_and_search.PasteIntoKeybindingsMenu, + filter_and_search.PasteIntoMenuFilter, filter_and_search.RerenderTheSearchedMainView, filter_and_search.SearchALongDiff, filter_and_search.SearchStatusAfterARerender,