mirror of
https://github.com/jesseduffield/lazygit.git
synced 2026-09-10 15:46:26 -04:00
A refresh from the UI thread returns immediately and applies its model and view updates as queued UI-thread callbacks. A key pressed before those have run is handled against the stale, pre-refresh state. For most keys that's harmless, but some handlers turn that state into git commands: pressing space twice in quick succession in the staging panel builds the second patch from the already-applied diff and fails with 'patch does not apply', because the refresh after the first press is what moves the selection to the next stageable hunk. Notably, this is not just a regression of the recent change that made UI-thread refreshes non-blocking; the window was merely much narrower before. A blocking refresh parked the UI thread while the scopes' bounces were queued, and the event loop drains pending keyboard input with priority over queued user events, so a key pressed during the blocked window still beat the queued state updates. The guarantee that the next keypress sees post-refresh state had already ended when the scopes' state updates moved from worker-side mutex-guarded writes to UI-thread bounces. Fix it with the input-blocking mechanism we already use for commit surgery, exposed as a new RefreshBlockingInput entry point: it begins blocking events synchronously in the calling handler, and ends the block from a callback that the finishing step queues behind the refresh's own updates. Keys pressed while the refresh is in flight are buffered and replayed, in order, against the fully refreshed state; since a replayed key's handler re-enters this same path, a burst of keypresses applies sequentially, each one seeing the previous one's refresh. Unlike the old blocking refreshes, this doesn't freeze the UI thread: rendering, spinners, resizing, and mouse scrolling keep working while input is withheld. Blocking input is opt-in per call site rather than the default for all UI-thread refreshes, because most refreshes (the focus-in and startup refreshes, say) don't produce state that the next keypress depends on, and blocking on them would delay typing for no reason. It should also be limited to quick, narrow-scoped refreshes: a full refresh, or any scope that pulls in COMMITS, can take very long in large repos and should usually not hold up input. The staging panel's stage/discard/edit-hunk refreshes use it now.
228 lines
6.2 KiB
Go
228 lines
6.2 KiB
Go
package gui
|
|
|
|
import (
|
|
"github.com/jesseduffield/lazygit/pkg/commands"
|
|
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
|
"github.com/jesseduffield/lazygit/pkg/config"
|
|
"github.com/jesseduffield/lazygit/pkg/gocui"
|
|
"github.com/jesseduffield/lazygit/pkg/gui/controllers/helpers"
|
|
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
|
"github.com/jesseduffield/lazygit/pkg/tasks"
|
|
)
|
|
|
|
// hacking this by including the gui struct for now until we split more things out
|
|
type guiCommon struct {
|
|
gui *Gui
|
|
types.IPopupHandler
|
|
}
|
|
|
|
var _ types.IGuiCommon = &guiCommon{}
|
|
|
|
func (self *guiCommon) LogAction(msg string) {
|
|
self.gui.LogAction(msg)
|
|
}
|
|
|
|
func (self *guiCommon) LogCommand(cmdStr string, isCommandLine bool) {
|
|
self.gui.LogCommand(cmdStr, isCommandLine)
|
|
}
|
|
|
|
func (self *guiCommon) Refresh(opts types.RefreshOptions) {
|
|
self.gui.helpers.Refresh.Refresh(opts)
|
|
}
|
|
|
|
func (self *guiCommon) RefreshBlockingInput(opts types.RefreshOptions) {
|
|
self.gui.helpers.Refresh.RefreshBlockingInput(opts)
|
|
}
|
|
|
|
func (self *guiCommon) RefreshFromWorker(opts types.RefreshOptions) {
|
|
self.gui.helpers.Refresh.RefreshFromWorker(opts)
|
|
}
|
|
|
|
func (self *guiCommon) PostRefreshUpdate(context types.Context) {
|
|
self.gui.postRefreshUpdate(context)
|
|
}
|
|
|
|
func (self *guiCommon) RunSubprocessAndRefresh(cmdObj *oscommands.CmdObj) error {
|
|
return self.gui.runSubprocessWithSuspenseAndRefresh(cmdObj)
|
|
}
|
|
|
|
func (self *guiCommon) RunSubprocess(cmdObj *oscommands.CmdObj) (bool, error) {
|
|
return self.gui.runSubprocessWithSuspense(cmdObj)
|
|
}
|
|
|
|
func (self *guiCommon) Suspend() error {
|
|
return self.gui.suspend()
|
|
}
|
|
|
|
func (self *guiCommon) Resume() error {
|
|
return self.gui.resume()
|
|
}
|
|
|
|
func (self *guiCommon) PauseBackgroundRefreshes(pause bool) {
|
|
self.gui.BackgroundRoutineMgr.PauseBackgroundRefreshes(pause)
|
|
}
|
|
|
|
// assertOnUIThread panics (in debug builds) if called from a worker goroutine.
|
|
// Use it to guard accessors for state that only the UI thread may touch, so
|
|
// that a stray worker access fails deterministically -- and points at itself --
|
|
// rather than surfacing later as a probabilistic data race.
|
|
func (self *guiCommon) assertOnUIThread(accessor string) {
|
|
if self.GetConfig().GetDebug() && !self.GocuiGui().IsUIThread() {
|
|
panic(accessor + " accessed from a worker")
|
|
}
|
|
}
|
|
|
|
func (self *guiCommon) Context() types.IContextMgr {
|
|
self.assertOnUIThread("Context()")
|
|
return self.gui.State.ContextMgr
|
|
}
|
|
|
|
func (self *guiCommon) ContextForKey(key types.ContextKey) types.Context {
|
|
return self.gui.State.ContextMgr.ContextForKey(key)
|
|
}
|
|
|
|
func (self *guiCommon) GetAppState() *config.AppState {
|
|
return self.gui.Config.GetAppState()
|
|
}
|
|
|
|
func (self *guiCommon) SaveAppState() error {
|
|
return self.gui.Config.SaveAppState()
|
|
}
|
|
|
|
func (self *guiCommon) SaveAppStateAndLogError() {
|
|
if err := self.gui.Config.SaveAppState(); err != nil {
|
|
self.gui.Log.Errorf("error when saving app state: %v", err)
|
|
}
|
|
}
|
|
|
|
func (self *guiCommon) GetConfig() config.AppConfigurer {
|
|
return self.gui.Config
|
|
}
|
|
|
|
func (self *guiCommon) ResetViewOrigin(view *gocui.View) {
|
|
self.gui.resetViewOrigin(view)
|
|
}
|
|
|
|
func (self *guiCommon) SetViewContent(view *gocui.View, content string) {
|
|
self.gui.setViewContent(view, content)
|
|
}
|
|
|
|
func (self *guiCommon) Render() {
|
|
self.gui.render()
|
|
}
|
|
|
|
func (self *guiCommon) Views() types.Views {
|
|
return self.gui.Views
|
|
}
|
|
|
|
func (self *guiCommon) Git() *commands.GitCommand {
|
|
return self.gui.git
|
|
}
|
|
|
|
func (self *guiCommon) OS() *oscommands.OSCommand {
|
|
return self.gui.os
|
|
}
|
|
|
|
func (self *guiCommon) Modes() *types.Modes {
|
|
return self.gui.State.Modes
|
|
}
|
|
|
|
func (self *guiCommon) Model() *types.Model {
|
|
self.assertOnUIThread("Model()")
|
|
return self.gui.State.Model
|
|
}
|
|
|
|
func (self *guiCommon) Mutexes() *types.Mutexes {
|
|
return &self.gui.Mutexes
|
|
}
|
|
|
|
func (self *guiCommon) GocuiGui() *gocui.Gui {
|
|
return self.gui.g
|
|
}
|
|
|
|
func (self *guiCommon) OnUIThread(f func() error) {
|
|
self.gui.onUIThread(f)
|
|
}
|
|
|
|
func (self *guiCommon) OnUIThreadBackground(f func() error) {
|
|
self.gui.onUIThreadBackground(f)
|
|
}
|
|
|
|
func (self *guiCommon) OnUIThreadContentOnly(f func() error) {
|
|
self.gui.onUIThreadContentOnly(f)
|
|
}
|
|
|
|
func (self *guiCommon) OnUIThreadContentOnlyBackground(f func() error) {
|
|
self.gui.onUIThreadContentOnlyBackground(f)
|
|
}
|
|
|
|
func (self *guiCommon) OnWorker(f func(gocui.Task) error) {
|
|
self.gui.onWorker(f)
|
|
}
|
|
|
|
func (self *guiCommon) OnWorkerBackground(f func(gocui.Task) error) {
|
|
self.gui.onWorkerBackground(f)
|
|
}
|
|
|
|
func (self *guiCommon) RenderToMainViews(opts types.RefreshMainOpts) {
|
|
self.gui.refreshMainViews(opts)
|
|
}
|
|
|
|
func (self *guiCommon) MainViewPairs() types.MainViewPairs {
|
|
return types.MainViewPairs{
|
|
Normal: self.gui.normalMainContextPair(),
|
|
Staging: self.gui.stagingMainContextPair(),
|
|
PatchBuilding: self.gui.patchBuildingMainContextPair(),
|
|
MergeConflicts: self.gui.mergingMainContextPair(),
|
|
}
|
|
}
|
|
|
|
func (self *guiCommon) GetViewBufferManagerForView(view *gocui.View) *tasks.ViewBufferManager {
|
|
return self.gui.getViewBufferManagerForView(view)
|
|
}
|
|
|
|
func (self *guiCommon) ReadLinesToFillView(view *gocui.View) {
|
|
self.gui.readLinesToFillView(view)
|
|
}
|
|
|
|
func (self *guiCommon) State() types.IStateAccessor {
|
|
return self.gui.stateAccessor
|
|
}
|
|
|
|
func (self *guiCommon) KeybindingsOpts() types.KeybindingsOpts {
|
|
return self.gui.keybindingOpts()
|
|
}
|
|
|
|
func (self *guiCommon) CallKeybindingHandler(binding *types.Binding) error {
|
|
return self.gui.callKeybindingHandler(binding)
|
|
}
|
|
|
|
func (self *guiCommon) ResetKeybindings() error {
|
|
return self.gui.resetKeybindings()
|
|
}
|
|
|
|
func (self *guiCommon) IsAnyModeActive() bool {
|
|
return self.gui.helpers.Mode.IsAnyModeActive()
|
|
}
|
|
|
|
func (self *guiCommon) GetInitialKeybindingsWithCustomCommands() ([]*types.Binding, []*gocui.ViewMouseBinding) {
|
|
return self.gui.GetInitialKeybindingsWithCustomCommands()
|
|
}
|
|
|
|
func (self *guiCommon) AfterLayout(f func() error) {
|
|
self.gui.afterLayout(f)
|
|
}
|
|
|
|
func (self *guiCommon) RunningIntegrationTest() bool {
|
|
return self.gui.integrationTest != nil
|
|
}
|
|
|
|
func (self *guiCommon) InDemo() bool {
|
|
return self.gui.integrationTest != nil && self.gui.integrationTest.IsDemo()
|
|
}
|
|
|
|
func (self *guiCommon) WithInlineStatus(item types.HasUrn, operation types.ItemOperation, contextKey types.ContextKey, f func(gocui.Task) error) error {
|
|
self.gui.helpers.InlineStatus.WithInlineStatus(helpers.InlineStatusOpts{Item: item, Operation: operation, ContextKey: contextKey}, f)
|
|
return nil
|
|
}
|