mirror of
https://github.com/jesseduffield/lazygit.git
synced 2026-08-24 10:13:41 -05:00
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>
198 lines
6.1 KiB
Go
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
|
|
}
|