Files
lazygit/pkg/gui/gui_common.go
Stefan HallerandClaude Opus 5 aebf495dce Scroll the selection into view by default
Ever since scrolling the selection into view became opt-in, we have been
fixing the same class of regression by hand, five times so far: a
controller moves the selection somewhere new, doesn't say that it wants
the view to follow, and the selection ends up off screen. The decision
needs facts from two places — whether the selection went somewhere new is
known to the list, whether the scroll position is the caller's to manage
is known to the caller — and asking every caller for both is what keeps
going wrong. The callers that get it wrong are usually not even the ones
that moved the selection: they are pass-throughs like postRefreshUpdate,
which can't know what a refresh did to the selection.

So default to scrolling, and let the two callers that maintain the scroll
position themselves say so.

The one case where scrolling is always wrong is a refresh that no user
action is behind: a background poll, or a reload of state on window
focus, after a subprocess, or after a repo switch. Those must leave the
viewport wherever the user last scrolled it to — that is what made the
scrolling opt-in in the first place. Both are already marked in
RefreshOptions, so the refresh can decide it once, centrally, instead of
each caller judging it.

A user action that ends in a foreground refresh does now yank the view
back to the selection if the user had scrolled away from it. That's a
behaviour change, and there may be actions where it turns out to be
unwelcome; those we can fix individually, and it beats the ones that
don't scroll today.

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

232 lines
6.3 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, false)
}
func (self *guiCommon) PostRefreshUpdateKeepingScrollPosition(context types.Context) {
self.gui.postRefreshUpdate(context, true)
}
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
}