Files
lazygit/pkg/gui/tasks_adapter.go
Stefan HallerandClaude Opus 5 ebfa8c71b2 Drop FlushStaleCells, which no longer has anything to flush
It existed for the incremental re-render: a shorter render left the previous
one's view lines in the tail (deliberately, to avoid a blank frame), and this
cleared them once the new content was fully read. Async renders now build
off-screen and swap in whole, so refreshViewLinesIfNeeded truncates the view
lines to the buffer and no tail can form. All the call at end-of-input still
did was discard every wrapped line and force the whole buffer to be re-wrapped
on the next draw, which is pure work on a large diff.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-15 15:30:27 +02:00

198 lines
6.1 KiB
Go

package gui
import (
"io"
"os/exec"
"strings"
"github.com/jesseduffield/lazygit/pkg/gocui"
"github.com/jesseduffield/lazygit/pkg/tasks"
"github.com/sirupsen/logrus"
)
func (gui *Gui) newCmdTask(view *gocui.View, cmd *exec.Cmd, prefix string) error {
cmdStr := strings.Join(cmd.Args, " ")
gui.c.Log.WithField(
"command",
cmdStr,
).Debug("RunCommand")
manager := gui.getManager(view)
// Mark the view as loading synchronously (before the task's goroutine runs
// and before the next layout pass) so the layout doesn't clamp the scroll
// position to the not-yet-loaded content.
manager.StartLoading()
// Hold the scrollbar at the height the view has now (the previous render),
// while it still shows that render: once the re-render swaps in its first
// partial paint the displayed buffer is briefly short, and we don't want the
// thumb to shrink and snap back as the rest loads.
view.FreezeScrollbarHeight()
// Snapshot the view width here, on the UI thread, so the task goroutine
// doesn't read the view's live dimensions while it streams output. It's
// applied inside start() below rather than now, because start() runs once
// the previous task has stopped -- applying it here would race that task's
// still-running writes (see View.SetContentWidth).
contentWidth := view.InnerWidth()
var r io.ReadCloser
start := func() (tasks.Cmd, io.Reader) {
view.SetContentWidth(contentWidth)
execCmd, pipe := startCmdWithPipe(cmd, gui.c.Log)
r = pipe
return execCmd, pipe
}
onClose := func() {
if r != nil {
r.Close()
r = nil
}
}
linesToRead := gui.linesToReadFromCmdTask(view)
if err := manager.NewTask(manager.NewCmdTask(start, prefix, linesToRead, onClose), cmdStr); err != nil {
gui.c.Log.Error(err)
}
return nil
}
// startCmdWithPipe starts cmd with its stdout and stderr going to a single
// pipe, and returns the command along with the pipe's read end, in the shape
// that NewCmdTask expects from its start func. It never returns a nil reader,
// because NewCmdTask's scanner panics on one: when the pipe can't be created
// the command isn't started at all, and an empty reader is returned so that
// the task shuts down cleanly with the error in the log.
func startCmdWithPipe(cmd *exec.Cmd, log *logrus.Entry) (tasks.Cmd, io.ReadCloser) {
r, err := cmd.StdoutPipe()
if err != nil {
log.Error(err)
return tasks.ExecCmd{Cmd: cmd}, io.NopCloser(strings.NewReader(""))
}
cmd.Stderr = cmd.Stdout
if err := cmd.Start(); err != nil {
log.Error(err)
}
return tasks.ExecCmd{Cmd: cmd}, r
}
func (gui *Gui) newStringTask(view *gocui.View, str string) error {
// using str so that if rendering the exact same thing we don't reset the origin
return gui.newStringTaskWithKey(view, str, str)
}
func (gui *Gui) newStringTaskWithoutScroll(view *gocui.View, str string) error {
manager := gui.getManager(view)
f := func(tasks.TaskOpts) error {
return gui.g.OnUIThreadAndWaitBackground(func() {
gui.c.SetViewContent(view, str)
})
}
if err := manager.NewTask(f, manager.GetTaskKey()); err != nil {
return err
}
return nil
}
func (gui *Gui) newStringTaskWithScroll(view *gocui.View, str string, originX int, originY int) error {
manager := gui.getManager(view)
f := func(tasks.TaskOpts) error {
return gui.g.OnUIThreadAndWaitBackground(func() {
gui.c.SetViewContent(view, str)
view.SetOrigin(originX, originY)
})
}
if err := manager.NewTask(f, manager.GetTaskKey()); err != nil {
return err
}
return nil
}
func (gui *Gui) newStringTaskWithKey(view *gocui.View, str string, key string) error {
manager := gui.getManager(view)
f := func(tasks.TaskOpts) error {
return gui.g.OnUIThreadAndWaitBackground(func() {
gui.c.ResetViewOrigin(view)
gui.c.SetViewContent(view, str)
})
}
if err := manager.NewTask(f, key); err != nil {
return err
}
return nil
}
func (gui *Gui) getManager(view *gocui.View) *tasks.ViewBufferManager {
manager, ok := gui.viewBufferManagerMap[view.Name()]
if !ok {
manager = tasks.NewViewBufferManager(
gui.Log,
view,
func() {
// Called before showing the "loading..." indicator: clear the
// displayed buffer so only "loading..." is shown. The actual content
// is rendered off-screen (beginRender below) and swapped in, so it
// never overwrites the displayed buffer incrementally.
view.Reset()
},
func() {
// As the task reads more lines, the only thing that changes is the
// view's content (and its scrollbar); the window layout doesn't. So a
// content-only render is enough, and it's much cheaper than a full
// layout-and-redraw on every read - which matters a lot when reading
// a long diff, where reads happen repeatedly as the user scrolls.
gui.renderContentOnly()
},
func() {
// The content is fully loaded now, so let the scrollbar track it
// directly again (it was held at the previous render's height while
// loading, see FreezeScrollbarHeight).
view.UnfreezeScrollbarHeight()
// Need to check if the content of the view is well past the origin.
linesHeight := view.ViewLinesHeight()
_, originY := view.Origin()
if linesHeight < originY {
newOriginY := linesHeight
view.SetOrigin(0, newOriginY)
}
},
func() {
view.SetOrigin(0, 0)
},
view.BeginOffscreenRender,
view.SwapInOffscreenRender,
func() gocui.Task {
// A background task: rendering content into a view is display
// work, not lazygit driving a git operation, so it must not
// count towards being busy and block a repo switch. These
// renders fire on nearly every focus/selection change, including
// the context activation that happens right before a menu/prompt
// handler runs (e.g. confirming worktree creation), which would
// otherwise make the switch that handler triggers refuse itself.
return gui.c.GocuiGui().NewBackgroundTask()
},
// Rendering is background work too (see above), so the view mutations
// it bounces onto the UI thread mustn't count towards being busy.
gui.g.OnUIThreadAndWaitBackground,
)
gui.viewBufferManagerMap[view.Name()] = manager
}
return manager
}