mirror of
https://github.com/jesseduffield/lazygit.git
synced 2026-08-28 02:24:49 -05:00
891 lines
32 KiB
Go
891 lines
32 KiB
Go
package tasks
|
|
|
|
import (
|
|
"bufio"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"os/exec"
|
|
"strconv"
|
|
"sync"
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
|
"github.com/jesseduffield/lazygit/pkg/gocui"
|
|
"github.com/jesseduffield/lazygit/pkg/utils"
|
|
"github.com/sasha-s/go-deadlock"
|
|
"github.com/sirupsen/logrus"
|
|
)
|
|
|
|
// Cmd abstracts over a started external process. *exec.Cmd satisfies the bulk
|
|
// of it via ExecCmd, but pty implementations can supply their own types — on
|
|
// Windows, ConPTY has to spawn via CreateProcess directly and can't use
|
|
// *exec.Cmd (see golang/go#62708).
|
|
type Cmd interface {
|
|
Wait() error
|
|
String() string
|
|
// Terminate makes the process stop early, as gracefully as the platform
|
|
// allows. It doesn't wait for the process to exit.
|
|
Terminate() error
|
|
}
|
|
|
|
// ExecCmd adapts *exec.Cmd to Cmd.
|
|
type ExecCmd struct {
|
|
*exec.Cmd
|
|
}
|
|
|
|
// Terminate sends SIGTERM on Unix. On Windows it does nothing, so a stopped
|
|
// command keeps running until it next writes to its (by then closed) output
|
|
// pipe.
|
|
func (c ExecCmd) Terminate() error {
|
|
return oscommands.TerminateProcessGracefully(c.Process)
|
|
}
|
|
|
|
// This file revolves around running commands that will be output to the main panel
|
|
// in the gui. If we're flicking through the commits panel, we want to invoke a
|
|
// `git show` command for each commit, but we don't want to read the entire output
|
|
// at once (because that would slow things down); we just want to fill the panel
|
|
// and then read more as the user scrolls down. We also want to ensure that we're only
|
|
// ever running one `git show` command at time, and that we only have one command
|
|
// writing its output to the main panel at a time.
|
|
|
|
const THROTTLE_TIME = time.Millisecond * 30
|
|
|
|
// we use this to check if the system is under stress right now. Hopefully this makes sense on other machines
|
|
const COMMAND_START_THRESHOLD = time.Millisecond * 10
|
|
|
|
type ViewBufferManager struct {
|
|
// this blocks until the task has been properly stopped
|
|
stopCurrentTask func()
|
|
|
|
// this is what we write the output of the task to. It's typically a view
|
|
writer io.Writer
|
|
|
|
waitingMutex deadlock.Mutex
|
|
// Guards newTaskID and taskKey, which identify the most recently requested
|
|
// task. Both are written on the goroutine NewTask spawns, and taskKey is
|
|
// read from the UI thread (GetTaskKey), so neither may be touched without
|
|
// holding this.
|
|
taskIDMutex deadlock.Mutex
|
|
Log *logrus.Entry
|
|
newTaskID int
|
|
// The channel by which the currently-running task is told to read more
|
|
// lines (e.g. as the user scrolls). Held in an atomic because it's swapped
|
|
// out as tasks come and go while ReadLines/ReadToEnd read it from the UI
|
|
// thread; nil when no task is running.
|
|
readLines atomic.Pointer[chan LinesToRead]
|
|
taskKey string
|
|
|
|
// Resets the view's scroll position to the top. A render whose content is
|
|
// different from what the view last showed (a different command key) calls
|
|
// this — but at its *first paint*, not when the task starts: the off-screen
|
|
// render leaves the previous content displayed until the swap, so resetting
|
|
// the origin up front would scroll that still-displayed content to the top
|
|
// before the new content replaces it. See newContentPending.
|
|
resetOrigin func()
|
|
|
|
// Whether the content the running task is rendering differs from what the
|
|
// view is currently showing (i.e. the command key changed). Two things key
|
|
// off it: the loading indicator only takes the view over when it is set,
|
|
// since there is no point clearing content we are about to render
|
|
// identically; and the first paint that reveals the content resets the
|
|
// scroll to the top and clears it.
|
|
//
|
|
// It deliberately outlives the task that set it: a task can be stopped and
|
|
// replaced before it ever paints — a background refresh landing just after
|
|
// the user clicked a different item, say — and the replacement, which
|
|
// renders the same content and so sets nothing of its own, still has to do
|
|
// what that task was owed.
|
|
newContentPending atomic.Bool
|
|
|
|
// When set, the next command task puts the view back where it was once it has
|
|
// re-rendered the content, instead of showing the new render from the top (see
|
|
// RenderRestore). It is installed just before the re-render is triggered.
|
|
//
|
|
// Like newContentPending it outlives the task it was installed for, and for the
|
|
// same reason: that task can be stopped and replaced before it ever paints, and
|
|
// the replacement, rendering the same content, is then the one that owes the
|
|
// user their position. It is cleared by whichever task applies it. Guarded by
|
|
// taskIDMutex, like the task key.
|
|
restoreForNextTask *RenderRestore
|
|
|
|
// When set, the next command task leaves the view's scroll position alone even
|
|
// though it renders a different command's output, that output being the same
|
|
// content laid out differently (see SetKeepScrollPositionForNextTask). The task
|
|
// that starts consumes it, in place of noting that new content is on its way.
|
|
// Guarded by taskIDMutex, like the task key.
|
|
keepScrollForNextTask bool
|
|
|
|
// Whether a command task is currently reading content into the view. While
|
|
// this is true the content is still growing, so callers (e.g. the layout)
|
|
// must not clamp the view's scroll position to the amount loaded so far.
|
|
loading atomic.Bool
|
|
|
|
// beforeStart is the function that is called before starting a new task
|
|
beforeStart func()
|
|
refreshView func()
|
|
onEndOfInput func()
|
|
|
|
// beginRender starts an off-screen render: the new content is built without
|
|
// disturbing what's displayed. swapInRender then promotes it to the display
|
|
// in one step. Together they keep the view showing the previous render until
|
|
// the new one has read enough to paint, instead of revealing it line by line.
|
|
beginRender func()
|
|
swapInRender func()
|
|
|
|
// see docs/dev/Busy.md
|
|
// A gocui task is not the same thing as the tasks defined in this file.
|
|
// A gocui task simply represents the fact that lazygit is busy doing something,
|
|
// whereas the tasks in this file are about rendering content to a view.
|
|
newGocuiTask func() gocui.Task
|
|
|
|
// Runs f on the UI thread and blocks until it has completed. All mutations
|
|
// of the view happen through this, so that the view is only ever touched on
|
|
// the UI thread (where it is also laid out and drawn), never on the task's
|
|
// own goroutine.
|
|
onUIThread func(f func()) error
|
|
|
|
// if the user flicks through a heap of items, with each one
|
|
// spawning a process to render something to the main view,
|
|
// it can slow things down quite a bit. In these situations we
|
|
// want to throttle the spawning of processes. Atomic because it's set
|
|
// from one task's stop goroutine and read when the next task starts.
|
|
throttle atomic.Bool
|
|
}
|
|
|
|
type LinesToRead struct {
|
|
// The total number of lines the task should have read once this request is
|
|
// satisfied. This is an absolute count from the start of the task, not a
|
|
// delta: the task keeps track of how many lines it has already read and only
|
|
// reads the shortfall, so a request for a total at or below what has already
|
|
// been read reads nothing. -1 means read all the way to the end.
|
|
Total int
|
|
|
|
// Number of lines after which we have read enough to fill the view, and can
|
|
// do an initial refresh. Only set for the initial read request; -1 for
|
|
// subsequent requests.
|
|
InitialRefreshAfter int
|
|
|
|
// Function to call after reading the lines is done
|
|
Then func()
|
|
}
|
|
|
|
// RenderRestore puts a view back where it was when it re-renders content the user
|
|
// is already looking at, laid out differently — a different context size, whitespace
|
|
// ignored, another diff renderer — instead of showing the new render from the top.
|
|
//
|
|
// The task reads the new content into an off-screen buffer; the restore says when
|
|
// enough of it has arrived to show the remembered position (FirstPaintReady), and
|
|
// then finds that position and reveals it (Apply). It is a pair of callbacks rather
|
|
// than a scroll position because a different layout of the same content puts the
|
|
// remembered line somewhere else, which only looking at the new content can answer.
|
|
type RenderRestore struct {
|
|
// FirstPaintReady reports whether enough of the new content has been read for
|
|
// the restore to show what it is looking for. It is consulted after each line
|
|
// is read, on the task's own goroutine.
|
|
FirstPaintReady func() bool
|
|
|
|
// Apply runs once, on the UI thread, at the first paint. It finds its target in
|
|
// the off-screen content, calls swapIn to promote that content to the display,
|
|
// and places the view on the target — in that order, so that the search runs
|
|
// while the previous content is still displayed, and the new content is never
|
|
// drawn at the previous render's scroll position.
|
|
//
|
|
// It must call swapIn even when it finds nothing to place the view on, in which
|
|
// case the view keeps the position the paint gave it: the offset it had, or the
|
|
// top for content the view hasn't seen.
|
|
Apply func(swapIn func())
|
|
|
|
// Done is called once the restore has had its render — after Apply, or when it
|
|
// is given up because the view is being shown something other than a re-render
|
|
// of what it was remembered from. It is how a caller that has to wait for the
|
|
// view to be back where it belongs knows that it either is, or never will be.
|
|
// Optional, and called on the UI thread, as Apply is.
|
|
Done func()
|
|
}
|
|
|
|
// resolved reports that this restore's render has happened, or that there will not be
|
|
// one. Called on the UI thread, from wherever the restore ends: once, whichever way it
|
|
// ended.
|
|
func (self *RenderRestore) resolved() {
|
|
if self.Done != nil {
|
|
done := self.Done
|
|
self.Done = nil
|
|
done()
|
|
}
|
|
}
|
|
|
|
// SetRestoreForNextTask arranges for the next command task to put the view back
|
|
// where it is now once it has re-rendered. Call it right before triggering a
|
|
// re-render of the content the view is showing; see RenderRestore.
|
|
func (self *ViewBufferManager) SetRestoreForNextTask(restore *RenderRestore) {
|
|
self.taskIDMutex.Lock()
|
|
defer self.taskIDMutex.Unlock()
|
|
|
|
self.restoreForNextTask = restore
|
|
}
|
|
|
|
// HasRestoreForNextTask reports whether the next command task already has a position
|
|
// waiting to be put back, for a caller that would otherwise install one of its own
|
|
// over it.
|
|
func (self *ViewBufferManager) HasRestoreForNextTask() bool {
|
|
self.taskIDMutex.Lock()
|
|
defer self.taskIDMutex.Unlock()
|
|
|
|
return self.restoreForNextTask != nil
|
|
}
|
|
|
|
func (self *ViewBufferManager) getRestoreForNextTask() *RenderRestore {
|
|
self.taskIDMutex.Lock()
|
|
defer self.taskIDMutex.Unlock()
|
|
|
|
return self.restoreForNextTask
|
|
}
|
|
|
|
// SetKeepScrollPositionForNextTask arranges for the next command task to leave the
|
|
// view's scroll position alone, rather than showing its content from the top the way a
|
|
// render of different content does. Call it right before triggering a re-render of the
|
|
// content the view is showing, when the command producing it is not the one that
|
|
// produced what is on screen — a different context size, another diff renderer.
|
|
//
|
|
// It is the coarser sibling of SetRestoreForNextTask, for the same moment: the restore
|
|
// puts the view back on the line it remembers, which is only possible where the lines
|
|
// of the new rendering can be told apart; this says merely "the content is a
|
|
// rearrangement of what is there, so the offset into it is nearer to where the user was
|
|
// than the top is". Both can be set at once, and then the restore has the first say.
|
|
func (self *ViewBufferManager) SetKeepScrollPositionForNextTask() {
|
|
self.taskIDMutex.Lock()
|
|
defer self.taskIDMutex.Unlock()
|
|
|
|
self.keepScrollForNextTask = true
|
|
}
|
|
|
|
// clearRestore drops a restore once a task has applied it, so that it rides exactly
|
|
// one re-render. One installed since — the user pressing the key again while this
|
|
// task was still reading — is left alone: it belongs to the render on its way.
|
|
func (self *ViewBufferManager) clearRestore(restore *RenderRestore) {
|
|
self.taskIDMutex.Lock()
|
|
defer self.taskIDMutex.Unlock()
|
|
|
|
if self.restoreForNextTask == restore {
|
|
self.restoreForNextTask = nil
|
|
}
|
|
}
|
|
|
|
// DropRestoreForNextTask gives up a restore that has no render to ride, because the
|
|
// view is being given something other than a re-render of the content it was
|
|
// remembered from — a message where a diff was. Without this the restore would sit
|
|
// there and claim some later render of that view, putting the user somewhere they
|
|
// haven't been for a while.
|
|
func (self *ViewBufferManager) DropRestoreForNextTask() {
|
|
self.taskIDMutex.Lock()
|
|
restore := self.restoreForNextTask
|
|
self.restoreForNextTask = nil
|
|
self.taskIDMutex.Unlock()
|
|
|
|
if restore != nil {
|
|
restore.resolved()
|
|
}
|
|
}
|
|
|
|
func (self *ViewBufferManager) GetTaskKey() string {
|
|
self.taskIDMutex.Lock()
|
|
defer self.taskIDMutex.Unlock()
|
|
|
|
return self.taskKey
|
|
}
|
|
|
|
// ForgetRenderedContent records that the view no longer shows the render whose key it
|
|
// is holding, because it has been emptied. The key says what the view is showing, and
|
|
// is what the next task is compared against to know whether it is rendering something
|
|
// new; a view with nothing in it is showing nothing, so whatever comes next is.
|
|
func (self *ViewBufferManager) ForgetRenderedContent() {
|
|
self.taskIDMutex.Lock()
|
|
defer self.taskIDMutex.Unlock()
|
|
|
|
self.taskKey = ""
|
|
}
|
|
|
|
func NewViewBufferManager(
|
|
log *logrus.Entry,
|
|
writer io.Writer,
|
|
beforeStart func(),
|
|
refreshView func(),
|
|
onEndOfInput func(),
|
|
resetOrigin func(),
|
|
beginRender func(),
|
|
swapInRender func(),
|
|
newGocuiTask func() gocui.Task,
|
|
onUIThread func(f func()) error,
|
|
) *ViewBufferManager {
|
|
return &ViewBufferManager{
|
|
Log: log,
|
|
writer: writer,
|
|
beforeStart: beforeStart,
|
|
refreshView: refreshView,
|
|
onEndOfInput: onEndOfInput,
|
|
resetOrigin: resetOrigin,
|
|
beginRender: beginRender,
|
|
swapInRender: swapInRender,
|
|
newGocuiTask: newGocuiTask,
|
|
onUIThread: onUIThread,
|
|
}
|
|
}
|
|
|
|
// ReadLines asks the task to ensure it has read at least totalLines lines in
|
|
// total. Because the count is absolute rather than a delta, repeated requests
|
|
// (e.g. as the user scrolls down, back up, and down again) don't re-read lines
|
|
// that have already been read: the task only ever reads the shortfall.
|
|
func (self *ViewBufferManager) ReadLines(totalLines int) {
|
|
if ch := self.readLines.Load(); ch != nil {
|
|
readLines := *ch
|
|
go utils.Safe(func() {
|
|
readLines <- LinesToRead{Total: totalLines, InitialRefreshAfter: -1}
|
|
})
|
|
}
|
|
}
|
|
|
|
// IsLoading reports whether a command task is currently reading content into the
|
|
// view, meaning the content is still growing.
|
|
func (self *ViewBufferManager) IsLoading() bool {
|
|
return self.loading.Load()
|
|
}
|
|
|
|
// StartLoading marks the view as loading content. It must be called
|
|
// synchronously when a command/pty task is started, before the task's goroutine
|
|
// runs, so that a layout pass happening in between doesn't clamp the scroll
|
|
// position to the not-yet-loaded content. It is cleared when the task reaches
|
|
// the end of its input.
|
|
func (self *ViewBufferManager) StartLoading() {
|
|
self.loading.Store(true)
|
|
}
|
|
|
|
func (self *ViewBufferManager) ReadToEnd(then func()) {
|
|
if ch := self.readLines.Load(); ch != nil {
|
|
readLines := *ch
|
|
go utils.Safe(func() {
|
|
readLines <- LinesToRead{Total: -1, InitialRefreshAfter: -1, Then: then}
|
|
})
|
|
} else if then != nil {
|
|
then()
|
|
}
|
|
}
|
|
|
|
func (self *ViewBufferManager) NewCmdTask(start func() (Cmd, io.Reader), prefix string, linesToRead LinesToRead, onDoneFn func()) func(TaskOpts) error {
|
|
return func(opts TaskOpts) error {
|
|
var onDoneOnce sync.Once
|
|
var onFirstPageShownOnce sync.Once
|
|
|
|
onFirstPageShown := func() {
|
|
onFirstPageShownOnce.Do(func() {
|
|
opts.InitialContentLoaded()
|
|
})
|
|
}
|
|
|
|
onDone := func() {
|
|
if onDoneFn != nil {
|
|
onDoneOnce.Do(onDoneFn)
|
|
}
|
|
onFirstPageShown()
|
|
}
|
|
|
|
// Whatever position is owed to the user belongs to this render: it was
|
|
// remembered just before the re-render that led here was triggered.
|
|
restore := self.getRestoreForNextTask()
|
|
|
|
if self.throttle.Load() {
|
|
self.Log.Info("throttling task")
|
|
time.Sleep(THROTTLE_TIME)
|
|
}
|
|
|
|
select {
|
|
case <-opts.Stop:
|
|
onDone()
|
|
return nil
|
|
default:
|
|
}
|
|
|
|
startTime := time.Now()
|
|
cmd, r := start()
|
|
timeToStart := time.Since(startTime)
|
|
|
|
done := make(chan struct{})
|
|
|
|
go utils.Safe(func() {
|
|
select {
|
|
case <-done:
|
|
// The command finished and did not have to be preemptively stopped before the next command.
|
|
// No need to throttle.
|
|
self.throttle.Store(false)
|
|
case <-opts.Stop:
|
|
// we use the time it took to start the program as a way of checking if things
|
|
// are running slow at the moment. This is admittedly a crude estimate, but
|
|
// the point is that we only want to throttle when things are running slow
|
|
// and the user is flicking through a bunch of items.
|
|
self.throttle.Store(time.Since(startTime) < THROTTLE_TIME && timeToStart > COMMAND_START_THRESHOLD)
|
|
|
|
// Kill the still-running command. The only reason to do this is to save CPU usage
|
|
// when flicking through several very long diffs when diff.algorithm = histogram is
|
|
// being used, in which case multiple git processes continue to calculate expensive
|
|
// diffs in the background even though they have been stopped already.
|
|
if err := cmd.Terminate(); err != nil {
|
|
self.Log.Errorf("error when trying to terminate cmd task: %v; Command: %v", err, cmd.String())
|
|
}
|
|
|
|
// close the task's stdout pipe (or the pty if we're using one) to make the command terminate
|
|
onDone()
|
|
}
|
|
})
|
|
|
|
loadingMutex := deadlock.Mutex{}
|
|
|
|
readLines := make(chan LinesToRead, 1024)
|
|
self.readLines.Store(&readLines)
|
|
|
|
scanner := bufio.NewScanner(r)
|
|
scanner.Split(utils.ScanLinesAndTruncateWhenLongerThanBuffer(bufio.MaxScanTokenSize))
|
|
|
|
lineChan := make(chan []byte)
|
|
lineWrittenChan := make(chan struct{})
|
|
|
|
// We're reading from the scanner in a separate goroutine because on windows
|
|
// if running git through a shim, we sometimes kill the parent process without
|
|
// killing its children, meaning the scanner blocks forever. This solution
|
|
// leaves us with a dead goroutine, but it's better than blocking all
|
|
// rendering to main views.
|
|
go utils.Safe(func() {
|
|
defer close(lineChan)
|
|
for scanner.Scan() {
|
|
select {
|
|
case <-opts.Stop:
|
|
return
|
|
case lineChan <- scanner.Bytes():
|
|
// We need to confirm the data has been fed into the view before we
|
|
// pull more from the scanner because the scanner uses the same backing
|
|
// array and we don't want to be mutating that while it's being written
|
|
<-lineWrittenChan
|
|
}
|
|
}
|
|
|
|
if err := scanner.Err(); err != nil {
|
|
self.Log.Error(err)
|
|
}
|
|
})
|
|
|
|
loaded := false
|
|
|
|
go utils.Safe(func() {
|
|
ticker := time.NewTicker(time.Millisecond * 200)
|
|
defer ticker.Stop()
|
|
select {
|
|
case <-opts.Stop:
|
|
return
|
|
case <-ticker.C:
|
|
loadingMutex.Lock()
|
|
// Only take the view over to say "loading..." when the content coming
|
|
// is different from what's on screen. A re-render of the same content
|
|
// leaves the view showing exactly what it should already, so clearing
|
|
// it for the message and then rendering the same thing back is a
|
|
// visible flicker for nothing — and a slow re-render of unchanged
|
|
// content is common (a background refresh over a repo with submodules
|
|
// that have uncommitted changes, say). The pending flag isn't consumed
|
|
// here; the first paint still owes the scroll reset.
|
|
//
|
|
// A restore keeps the view too: it is there to make a re-render of what
|
|
// the user is looking at seamless, and blanking the view for a message
|
|
// before putting them back where they were is the flicker it exists to
|
|
// avoid.
|
|
if !loaded && restore == nil && self.newContentPending.Load() {
|
|
self.beforeStart()
|
|
// beforeStart cleared the previous content to show "loading...", so
|
|
// put the view back at the top for it (beforeStart doesn't touch the
|
|
// origin). The origin is view state the UI thread reads while laying
|
|
// out, so write it there.
|
|
_ = self.onUIThread(self.resetOrigin)
|
|
_, _ = self.writer.Write([]byte("loading..."))
|
|
self.refreshView()
|
|
}
|
|
loadingMutex.Unlock()
|
|
}
|
|
})
|
|
|
|
go utils.Safe(func() {
|
|
isViewStale := true
|
|
writeToView := func(content []byte) {
|
|
isViewStale = true
|
|
_, _ = self.writer.Write(content)
|
|
}
|
|
refreshViewIfStale := func() {
|
|
if isViewStale {
|
|
self.refreshView()
|
|
isViewStale = false
|
|
}
|
|
}
|
|
|
|
// Go's select picks randomly among ready cases, so once opts.Stop is
|
|
// closed the selects below could still service a ready data channel
|
|
// instead of bailing. Check stop explicitly first to give it priority:
|
|
// a task that's been stopped (it's being replaced by a newer one) must
|
|
// not touch the view here — it would start an off-screen render and
|
|
// write the prefix into it, clobbering what the incoming task is about
|
|
// to render.
|
|
stopped := func() bool {
|
|
select {
|
|
case <-opts.Stop:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
// The total number of lines we have read so far. Requests specify an
|
|
// absolute target total (see LinesToRead.Total), so we compare against
|
|
// this to work out how many more lines, if any, we still need to read.
|
|
linesRead := 0
|
|
|
|
// The first paint swaps the off-screen render in to reveal the new
|
|
// content, and settles the scroll position in the same step — so the new
|
|
// content first appears already where it belongs, and no draw can land
|
|
// between the two and show it at the previous render's scroll. It happens
|
|
// once, either when we've read far enough (below) or at end of input for
|
|
// content shorter than that. Callers run it on the UI thread: it writes
|
|
// the view's origin.
|
|
painted := false
|
|
firstPaint := func() {
|
|
if painted {
|
|
return
|
|
}
|
|
painted = true
|
|
// Content the view hasn't seen is shown from the top, and this is where
|
|
// the view goes there — before the restore below, which decides where to
|
|
// put the view from where it is. The position the paint settles on is the
|
|
// restore's to move from, so it has to be the one the new content is
|
|
// about to be revealed at.
|
|
if self.newContentPending.Swap(false) {
|
|
self.resetOrigin()
|
|
}
|
|
if restore != nil {
|
|
// The restore does the swap itself, so that it can find where the
|
|
// user was in the new content before it is revealed.
|
|
restore.Apply(self.swapInRender)
|
|
self.clearRestore(restore)
|
|
restore.resolved()
|
|
return
|
|
}
|
|
self.swapInRender()
|
|
}
|
|
|
|
// Set LAZYGIT_SLOW_RENDER=<milliseconds> to sleep that long after each
|
|
// line is written to the view, stretching async loads out so the frames
|
|
// of a re-render become visible. Useful for debugging scroll/flicker
|
|
// behaviour; has no effect when the variable is unset.
|
|
var slowRenderPerLine time.Duration
|
|
if v := os.Getenv("LAZYGIT_SLOW_RENDER"); v != "" {
|
|
if ms, err := strconv.Atoi(v); err == nil {
|
|
slowRenderPerLine = time.Duration(ms) * time.Millisecond
|
|
}
|
|
}
|
|
|
|
outer:
|
|
for {
|
|
if stopped() {
|
|
break outer
|
|
}
|
|
select {
|
|
case <-opts.Stop:
|
|
break outer
|
|
case linesToRead := <-readLines:
|
|
callThen := func() {
|
|
if linesToRead.Then != nil {
|
|
linesToRead.Then()
|
|
}
|
|
}
|
|
// A restore that hasn't painted yet keeps us reading past the lines
|
|
// asked for, all the way to the end of the input if need be. What it
|
|
// is looking for may be anywhere in the new content, and a rendering
|
|
// that has to be parsed as a diff to be searched at all can only be
|
|
// parsed whole — so stopping early would leave it nothing to find,
|
|
// and the view somewhere the user didn't put it.
|
|
for linesToRead.Total == -1 || linesRead < linesToRead.Total || (restore != nil && !painted) {
|
|
if stopped() {
|
|
callThen()
|
|
break outer
|
|
}
|
|
var ok bool
|
|
var line []byte
|
|
select {
|
|
case <-opts.Stop:
|
|
callThen()
|
|
break outer
|
|
case line, ok = <-lineChan:
|
|
// process line below
|
|
}
|
|
|
|
loadingMutex.Lock()
|
|
if !loaded {
|
|
// Build the new content off-screen, leaving the previous render
|
|
// displayed until we swap in below; this is what keeps an async
|
|
// re-render from showing a half-loaded buffer.
|
|
self.beginRender()
|
|
if prefix != "" {
|
|
writeToView([]byte(prefix))
|
|
}
|
|
loaded = true
|
|
}
|
|
loadingMutex.Unlock()
|
|
|
|
if !ok {
|
|
// 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: do the first paint now if it hasn't happened
|
|
// yet (the content was shorter than a screenful, so we never reached
|
|
// the point below), 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 — as is
|
|
// firstPaint, which also writes the origin.
|
|
_ = self.onUIThread(func() {
|
|
firstPaint()
|
|
self.onEndOfInput()
|
|
})
|
|
// The content is fully loaded now, so it's safe again for the
|
|
// layout to clamp the scroll position to it. We deliberately
|
|
// don't clear this when stopped (rather than EOF'd), because that
|
|
// means a newer task is taking over and is still loading.
|
|
self.loading.Store(false)
|
|
callThen()
|
|
// Any read requests that were queued while we were reading are
|
|
// now trivially satisfied, since we've read everything. Fire
|
|
// their callbacks instead of dropping them when we break out of
|
|
// the loop below (and nil out readLines).
|
|
drain:
|
|
for {
|
|
select {
|
|
case queued := <-readLines:
|
|
if queued.Then != nil {
|
|
queued.Then()
|
|
}
|
|
default:
|
|
break drain
|
|
}
|
|
}
|
|
break outer
|
|
}
|
|
writeToView(append(line, '\n'))
|
|
lineWrittenChan <- struct{}{}
|
|
linesRead++
|
|
|
|
if slowRenderPerLine > 0 {
|
|
time.Sleep(slowRenderPerLine)
|
|
}
|
|
|
|
if !painted {
|
|
// Do the first paint once we have read enough lines to fill the
|
|
// view — or, when a position is waiting to be restored, once the
|
|
// restore says it can show it, since where the view should be is
|
|
// its call. Continue reading afterwards and refresh again at the
|
|
// end to make sure the scrollbar has the right size.
|
|
var ready bool
|
|
if restore != nil {
|
|
ready = restore.FirstPaintReady()
|
|
} else {
|
|
ready = linesRead == linesToRead.InitialRefreshAfter
|
|
}
|
|
if ready {
|
|
_ = self.onUIThread(firstPaint)
|
|
refreshViewIfStale()
|
|
}
|
|
}
|
|
}
|
|
refreshViewIfStale()
|
|
onFirstPageShown()
|
|
callThen()
|
|
}
|
|
}
|
|
|
|
self.readLines.Store(nil)
|
|
|
|
refreshViewIfStale()
|
|
|
|
select {
|
|
case <-opts.Stop:
|
|
// If we stopped the task, don't block waiting for it; this could cause a delay if
|
|
// the process takes a while until it actually terminates. We still want to call
|
|
// Wait to reclaim any resources, but do it on a background goroutine, and ignore
|
|
// any errors.
|
|
go func() { _ = cmd.Wait() }()
|
|
default:
|
|
if err := cmd.Wait(); err != nil {
|
|
self.Log.Errorf("Unexpected error when running cmd task: %v; Failed command: %v", err, cmd.String())
|
|
}
|
|
}
|
|
|
|
// calling this here again in case the program ended on its own accord
|
|
onDone()
|
|
|
|
close(done)
|
|
close(lineWrittenChan)
|
|
})
|
|
|
|
readLines <- linesToRead
|
|
|
|
<-done
|
|
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// Close closes the task manager, killing whatever task may currently be running
|
|
func (self *ViewBufferManager) Close() {
|
|
// stopCurrentTask is written by NewTask's goroutine under waitingMutex (and
|
|
// so is the sync.Once it closes over), so read it under the lock and call
|
|
// the captured value; a task starting on shutdown must not race us here.
|
|
self.waitingMutex.Lock()
|
|
stopCurrentTask := self.stopCurrentTask
|
|
self.waitingMutex.Unlock()
|
|
|
|
if stopCurrentTask == nil {
|
|
return
|
|
}
|
|
|
|
c := make(chan struct{})
|
|
|
|
go utils.Safe(func() {
|
|
stopCurrentTask()
|
|
c <- struct{}{}
|
|
})
|
|
|
|
select {
|
|
case <-c:
|
|
return
|
|
case <-time.After(3 * time.Second):
|
|
fmt.Println("cannot kill child process")
|
|
}
|
|
}
|
|
|
|
// different kinds of tasks:
|
|
// 1) command based, where the manager can be asked to read more lines, but the command can be killed
|
|
// 2) string based, where the manager can also be asked to read more lines
|
|
|
|
type TaskOpts struct {
|
|
// Channel that tells the task to stop, because another task wants to run.
|
|
Stop chan struct{}
|
|
|
|
// Only for tasks which are long-running, where we read more lines sporadically.
|
|
// We use this to keep track of when a user's action is complete (i.e. all views
|
|
// have been refreshed to display the results of their action)
|
|
InitialContentLoaded func()
|
|
}
|
|
|
|
func (self *ViewBufferManager) NewTask(f func(TaskOpts) error, key string) error {
|
|
gocuiTask := self.newGocuiTask()
|
|
|
|
var completeTaskOnce sync.Once
|
|
|
|
completeGocuiTask := func() {
|
|
completeTaskOnce.Do(func() {
|
|
gocuiTask.Done()
|
|
})
|
|
}
|
|
|
|
// Assign the taskID synchronously so it reflects NewTask call order
|
|
// rather than the order in which the spawned goroutines happen to be
|
|
// scheduled. Otherwise two NewTask calls in quick succession can have
|
|
// their goroutines race, with the later-called task ending up with the
|
|
// lower taskID and losing the staleness check below.
|
|
self.taskIDMutex.Lock()
|
|
self.newTaskID++
|
|
taskID := self.newTaskID
|
|
self.taskIDMutex.Unlock()
|
|
|
|
go utils.Safe(func() {
|
|
defer completeGocuiTask()
|
|
|
|
self.taskIDMutex.Lock()
|
|
|
|
// Bail out before touching shared view state if a newer task has
|
|
// already been queued: if we reset the view here we'd do it for a task
|
|
// that's about to exit, potentially wiping output the winning task has
|
|
// already written.
|
|
if taskID < self.newTaskID {
|
|
self.taskIDMutex.Unlock()
|
|
return
|
|
}
|
|
|
|
// Note we don't reset the origin here even when the command key changed:
|
|
// that's deferred to the first paint that reveals the new content (see
|
|
// newContentPending), so the previous content — left displayed until the
|
|
// swap — doesn't visibly jump to the top before the new content appears.
|
|
// Read taskKey directly: we already hold the mutex that guards it, and
|
|
// GetTaskKey would take it again. A pending restore isn't dropped here
|
|
// either, even for a different command: the re-renders it rides are all
|
|
// different commands (a different context size, another diff renderer), and
|
|
// it validates itself against the content it lands in anyway.
|
|
// A task told to keep the scroll position renders the content the view is
|
|
// already showing, laid out differently, so the reset it would otherwise owe
|
|
// would take the user away from what they are reading — and the loading
|
|
// message, which the same flag governs, would blank content that is about to
|
|
// come back looking much the same.
|
|
if self.taskKey != key && self.resetOrigin != nil && !self.keepScrollForNextTask {
|
|
self.newContentPending.Store(true)
|
|
}
|
|
self.keepScrollForNextTask = false
|
|
self.taskKey = key
|
|
|
|
self.taskIDMutex.Unlock()
|
|
|
|
self.waitingMutex.Lock()
|
|
|
|
// Re-check staleness after acquiring waitingMutex: a newer task
|
|
// may have arrived while we were blocked here.
|
|
self.taskIDMutex.Lock()
|
|
if taskID < self.newTaskID {
|
|
self.waitingMutex.Unlock()
|
|
self.taskIDMutex.Unlock()
|
|
return
|
|
}
|
|
self.taskIDMutex.Unlock()
|
|
|
|
if self.stopCurrentTask != nil {
|
|
self.stopCurrentTask()
|
|
}
|
|
|
|
self.readLines.Store(nil)
|
|
|
|
stop := make(chan struct{})
|
|
notifyStopped := make(chan struct{})
|
|
|
|
var once sync.Once
|
|
onStop := func() {
|
|
close(stop)
|
|
<-notifyStopped
|
|
}
|
|
|
|
self.stopCurrentTask = func() { once.Do(onStop) }
|
|
|
|
self.waitingMutex.Unlock()
|
|
|
|
if err := f(TaskOpts{Stop: stop, InitialContentLoaded: completeGocuiTask}); err != nil {
|
|
self.Log.Error(err) // might need an onError callback
|
|
}
|
|
|
|
close(notifyStopped)
|
|
})
|
|
|
|
return nil
|
|
}
|