From 811b3fbdd16faf0598705c39d27177c09b18d6df Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Sat, 5 Sep 2026 10:20:24 +0200 Subject: [PATCH] Show the search status of what a re-rendered view now holds Rendering a view's content again while a search is on leaves the "x of y" describing the content that has just been replaced. The status is worked out when the search is typed and again when a key steps through the matches, and a render is neither. Change the diff context size while searching the focused main view, and the count stays as it was, however many matches the wider context brought in or took away. Run the search again over the new content once the render has finished putting it there. Co-authored-by: Claude Opus 5 (1M context) --- pkg/gocui/view.go | 13 ++++++ pkg/gui/main_panels.go | 15 +++++++ pkg/gui/tasks_adapter.go | 5 +++ .../search_status_after_a_rerender.go | 45 +++++++++++++++++++ pkg/integration/tests/test_list.go | 1 + 5 files changed, 79 insertions(+) create mode 100644 pkg/integration/tests/filter_and_search/search_status_after_a_rerender.go diff --git a/pkg/gocui/view.go b/pkg/gocui/view.go index cc5e6c0e4..0a06d1981 100644 --- a/pkg/gocui/view.go +++ b/pkg/gocui/view.go @@ -311,6 +311,19 @@ func (v *View) refreshSearchPositionsIfNeeded() { } } +// RefreshSearch runs the search again over content the view has just been re-rendered +// with, and shows the "x of y" status of what it finds. The view stays where it is: the +// position in the content is the user's, and the search follows it rather than moving +// it. +func (v *View) RefreshSearch() { + if !v.IsSearching() { + return + } + + v.UpdateSearchResults(v.searcher.searchString, v.searcher.modelSearchResults) + v.renderSearchStatus(v.searcher.currentSearchIndex, len(v.searcher.searchPositions)) +} + func (v *View) gotoNextMatch() error { v.refreshSearchPositions() diff --git a/pkg/gui/main_panels.go b/pkg/gui/main_panels.go index a0efcb14c..bc4a4219e 100644 --- a/pkg/gui/main_panels.go +++ b/pkg/gui/main_panels.go @@ -2,6 +2,7 @@ package gui import ( "github.com/jesseduffield/lazygit/pkg/gocui" + "github.com/jesseduffield/lazygit/pkg/gui/context" "github.com/jesseduffield/lazygit/pkg/gui/types" ) @@ -139,3 +140,17 @@ func (gui *Gui) refreshMainViews(opts types.RefreshMainOpts) { func (gui *Gui) splitMainPanel(splitMainPanel bool) { gui.State.SplitMainPanel = splitMainPanel } + +// reApplySearch runs a search the view holds again over the content a render has just +// finished putting there, so that the matches highlighted and the "x of y" status +// describe what the view shows now rather than what it showed when the search was +// typed. Call it once the content is final. +func (gui *Gui) reApplySearch(view *gocui.View) { + // While the prompt is open, the search view holds what the user is typing, and the + // status would be written over it. + if gui.State.ContextMgr.Current().GetKey() == context.SEARCH_CONTEXT_KEY { + return + } + + view.RefreshSearch() +} diff --git a/pkg/gui/tasks_adapter.go b/pkg/gui/tasks_adapter.go index 5e5295639..3ed141d68 100644 --- a/pkg/gui/tasks_adapter.go +++ b/pkg/gui/tasks_adapter.go @@ -91,6 +91,7 @@ func (gui *Gui) newStringTaskWithoutScroll(view *gocui.View, str string) error { f := func(tasks.TaskOpts) error { return gui.g.OnUIThreadAndWaitBackground(func() { gui.c.SetViewContent(view, str) + gui.reApplySearch(view) }) } @@ -108,6 +109,7 @@ func (gui *Gui) newStringTaskWithScroll(view *gocui.View, str string, originX in return gui.g.OnUIThreadAndWaitBackground(func() { gui.c.SetViewContent(view, str) view.SetOrigin(originX, originY) + gui.reApplySearch(view) }) } @@ -125,6 +127,7 @@ func (gui *Gui) newStringTaskWithKey(view *gocui.View, str string, key string) e return gui.g.OnUIThreadAndWaitBackground(func() { gui.c.ResetViewOrigin(view) gui.c.SetViewContent(view, str) + gui.reApplySearch(view) }) } @@ -170,6 +173,8 @@ func (gui *Gui) getManager(view *gocui.View) *tasks.ViewBufferManager { view.SetOrigin(0, newOriginY) } + + gui.reApplySearch(view) }, func() { view.SetOrigin(0, 0) diff --git a/pkg/integration/tests/filter_and_search/search_status_after_a_rerender.go b/pkg/integration/tests/filter_and_search/search_status_after_a_rerender.go new file mode 100644 index 000000000..8116049b9 --- /dev/null +++ b/pkg/integration/tests/filter_and_search/search_status_after_a_rerender.go @@ -0,0 +1,45 @@ +package filter_and_search + +import ( + "github.com/jesseduffield/lazygit/pkg/config" + . "github.com/jesseduffield/lazygit/pkg/integration/components" +) + +var SearchStatusAfterARerender = NewIntegrationTest(NewIntegrationTestArgs{ + Description: "The search status counts the matches in a diff that has been rendered again", + ExtraCmdArgs: []string{}, + Skip: false, + SetupConfig: func(config *config.AppConfig) {}, + SetupRepo: func(shell *Shell) { + shell.CreateFileAndAdd("file1", + "line 1\nline 2\nline 3\nline 4\nline 5\nline 6\nline 7\nline 8\nline 9\nNEEDLE\nline 11\nline 12\nline 13\nline 14\n") + shell.Commit("one") + + // Four lines above NEEDLE, so that it is context at a context size of 4 but + // not at 3. + shell.UpdateFile("file1", + "line 1\nline 2\nline 3\nline 4\nline 5\nchanged\nline 7\nline 8\nline 9\nNEEDLE\nline 11\nline 12\nline 13\nline 14\n") + }, + Run: func(t *TestDriver, keys config.KeybindingConfig) { + t.Views().Files(). + IsFocused(). + Press(keys.Universal.FocusMainView) + + t.Views().Main(). + IsFocused(). + Content(DoesNotContain("NEEDLE")). + FilterOrSearch("NEEDLE") + + t.Views().Search().Content(Contains("No matches for 'NEEDLE'")) + + // A wider context brings NEEDLE into the diff, and the search counts it. + t.Views().Main(). + Press(keys.Universal.IncreaseContextInDiffView). + Tap(func() { + t.ExpectToast(Equals("Changed diff context size to 4")) + }). + Content(Contains("NEEDLE")) + + t.Views().Search().Content(Contains("matches for 'NEEDLE' (1 of 1)")) + }, +}) diff --git a/pkg/integration/tests/test_list.go b/pkg/integration/tests/test_list.go index 1324a4160..3d9edadfe 100644 --- a/pkg/integration/tests/test_list.go +++ b/pkg/integration/tests/test_list.go @@ -278,6 +278,7 @@ var tests = []*components.IntegrationTest{ filter_and_search.NestedFilterTransient, filter_and_search.NewSearch, filter_and_search.SearchALongDiff, + filter_and_search.SearchStatusAfterARerender, filter_and_search.StageAllStagesOnlyTrackedFilesInTrackedOnlyFilter, filter_and_search.StagingFolderStagesOnlyTrackedFilesInTrackedOnlyFilter, filter_by_author.SelectAuthor,