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) <noreply@anthropic.com>
This commit is contained in:
Stefan Haller 2026-09-05 10:20:24 +02:00
parent 04a7ae3ec8
commit 811b3fbdd1
5 changed files with 79 additions and 0 deletions

View file

@ -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()

View file

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

View file

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

View file

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

View file

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