Hold a task while a view is read to its end

ReadToEnd reads the rest of a view's content on the render task's own
goroutine, and calls back once it has. Nothing held a task for that, so
lazygit counted as idle from the moment the caller returned until the
callback ran. The search prompt in the focused main view opens from such
a callback, so an integration test takes the idle report as its cue to
carry on, and presses its next key while the prompt is not open yet.

Hold the task in ReadToEnd rather than in the caller, so that every
caller is covered (see docs/dev/Busy.md).

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Stefan Haller 2026-09-05 10:14:45 +02:00
parent dd2a1a634f
commit acb6e48a98
3 changed files with 71 additions and 3 deletions

View file

@ -0,0 +1,57 @@
package filter_and_search
import (
"fmt"
"strings"
"github.com/jesseduffield/lazygit/pkg/config"
. "github.com/jesseduffield/lazygit/pkg/integration/components"
)
// longFileWithThreeMatches is long enough that a render of its diff stops well short
// of the end, with two of the three matches for the search below the point it stops at.
func longFileWithThreeMatches() string {
lines := make([]string, 0, 2000)
for i := range 2000 {
switch i {
case 100:
lines = append(lines, "NEEDLE first")
case 1000:
lines = append(lines, "NEEDLE middle")
case 1900:
lines = append(lines, "NEEDLE last")
default:
lines = append(lines, fmt.Sprintf("line %d", i))
}
}
return strings.Join(lines, "\n") + "\n"
}
var SearchALongDiff = NewIntegrationTest(NewIntegrationTestArgs{
Description: "Search a diff that is longer than a single render of it reads",
ExtraCmdArgs: []string{},
Skip: false,
// A small window, so that a render stops well short of 2000 lines.
Width: 120,
Height: 30,
SetupConfig: func(config *config.AppConfig) {},
SetupRepo: func(shell *Shell) {
shell.CreateFileAndAdd("file1", "")
shell.Commit("one")
shell.UpdateFile("file1", longFileWithThreeMatches())
},
Run: func(t *TestDriver, keys config.KeybindingConfig) {
t.Views().Files().
IsFocused().
Press(keys.Universal.FocusMainView)
// All three matches are counted: opening the prompt reads the whole diff
// first, however much of it the render had got to.
t.Views().Main().
IsFocused().
FilterOrSearch("NEEDLE")
t.Views().Search().Content(Contains("matches for 'NEEDLE' (1 of 3)"))
},
})

View file

@ -277,6 +277,7 @@ var tests = []*components.IntegrationTest{
filter_and_search.NestedFilter,
filter_and_search.NestedFilterTransient,
filter_and_search.NewSearch,
filter_and_search.SearchALongDiff,
filter_and_search.StageAllStagesOnlyTrackedFilesInTrackedOnlyFilter,
filter_and_search.StagingFolderStagesOnlyTrackedFilesInTrackedOnlyFilter,
filter_by_author.SelectAuthor,

View file

@ -212,10 +212,20 @@ func (self *ViewBufferManager) StartLoading() {
}
func (self *ViewBufferManager) ReadToEnd(then func()) {
request := LinesToRead{Total: -1, InitialRefreshAfter: -1, Then: then}
if !self.readRequests.enqueue(request) && then != nil {
// The reading happens on the task's own goroutine, and the caller hears about
// it through then, so lazygit must not count as idle in between.
task := self.newGocuiTask()
answered := func() {
task.Done()
if then != nil {
then()
}
}
request := LinesToRead{Total: -1, InitialRefreshAfter: -1, Then: answered}
if !self.readRequests.enqueue(request) {
// With no task reading, everything there is to read has been read.
then()
answered()
}
}