mirror of
https://github.com/jesseduffield/lazygit.git
synced 2026-08-24 10:13:41 -05:00
Don't run end-of-input handling for a render that was stopped
When a task is stopped to make way for a newer one, stopping closes opts.Stop, and the scanner goroutine then closes lineChan. The read loop's select between those two channels is therefore non-deterministic: it can land on the closed lineChan (ok == false) instead of the opts.Stop case, sending a stopped task into the end-of-input branch. There it runs the full finalize — swapping its half-read off-screen buffer in, clamping the origin to the truncated content, and clearing the loading flag — all of which corrupt what the incoming task is about to render. The most visible symptom is a brief frame of truncated content with the scroll yanked to the top, seen when re-renders overlap rapidly (e.g. the periodic background refresh re-rendering a main view faster than it can load, very easy to hit under LAZYGIT_SLOW_RENDER). The underlying bug predates the off-screen render (the EOF branch always clamped the origin via onEndOfInput), but that change made it far worse by also swapping a truncated buffer into the display. Fix it at the source: in the EOF branch, check whether we were stopped and, if so, bail out like the explicit stop case, leaving the view entirely to the task that replaces us. There's no test because the bug is the non-deterministic select itself: any test would have to win a coin flip to observe it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
9e23111172
commit
bf6c34798c
+19
-3
@@ -422,9 +422,25 @@ func (self *ViewBufferManager) NewCmdTask(start func() (Cmd, io.Reader), prefix
|
||||
loadingMutex.Unlock()
|
||||
|
||||
if !ok {
|
||||
// We're at EOF before reaching InitialRefreshAfter (the content was
|
||||
// shorter than a screenful), so swap in whatever we read now, and
|
||||
// flush the stale content.
|
||||
// lineChan is closed. At a genuine end of input we swap in what we
|
||||
// read and finalize. But lineChan is also closed when this task has
|
||||
// been stopped to make way for a newer one: stopping closes
|
||||
// opts.Stop, and the scanner goroutine then closes lineChan, so the
|
||||
// select above can land here instead of on the opts.Stop case. A
|
||||
// stopped task is being replaced and must leave the view to the
|
||||
// incoming task — swapping in its half-read buffer, clamping the
|
||||
// origin, or clearing `loading` would all corrupt what that task is
|
||||
// about to render. So bail out here, the same as the explicit stop
|
||||
// case above.
|
||||
select {
|
||||
case <-opts.Stop:
|
||||
callThen()
|
||||
break outer
|
||||
default:
|
||||
}
|
||||
// Genuine end of input: swap in whatever we read (the content was
|
||||
// shorter than a screenful, so we never hit the InitialRefreshAfter
|
||||
// swap), and flush the stale content.
|
||||
// onEndOfInput reads the view's dimensions (to decide
|
||||
// whether to scroll) and sets the origin, both of which
|
||||
// are UI-thread-only, so run it there.
|
||||
|
||||
Reference in New Issue
Block a user