From acb6e48a9890f307e5dde46fe9b05b607828a0f7 Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Sat, 5 Sep 2026 10:14:45 +0200 Subject: [PATCH] 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) --- .../filter_and_search/search_a_long_diff.go | 57 +++++++++++++++++++ pkg/integration/tests/test_list.go | 1 + pkg/tasks/tasks.go | 16 +++++- 3 files changed, 71 insertions(+), 3 deletions(-) create mode 100644 pkg/integration/tests/filter_and_search/search_a_long_diff.go diff --git a/pkg/integration/tests/filter_and_search/search_a_long_diff.go b/pkg/integration/tests/filter_and_search/search_a_long_diff.go new file mode 100644 index 000000000..6da59ce10 --- /dev/null +++ b/pkg/integration/tests/filter_and_search/search_a_long_diff.go @@ -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)")) + }, +}) diff --git a/pkg/integration/tests/test_list.go b/pkg/integration/tests/test_list.go index 0d25ddba6..1324a4160 100644 --- a/pkg/integration/tests/test_list.go +++ b/pkg/integration/tests/test_list.go @@ -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, diff --git a/pkg/tasks/tasks.go b/pkg/tasks/tasks.go index 377e74c0a..deaeee042 100644 --- a/pkg/tasks/tasks.go +++ b/pkg/tasks/tasks.go @@ -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() } }