1
0
mirror of https://github.com/jesseduffield/lazygit.git synced 2026-01-26 01:41:35 +03:00

Avoid scrolling the selection into view on refresh (#5134)

It is possible to scroll the selection out of view using the mouse
wheel; after doing this, it would sometimes scroll into view by itself
again, for example when a background fetch occurred. In the files panel
this would even happen every 10s with every regular files refresh.

Fix this by adding a scrollIntoView parameter to HandleFocus, which is
false by default, and is only set to true from controllers that change
the selection.

Fixes #5097.
This commit is contained in:
Stefan Haller
2025-12-23 15:37:18 +01:00
committed by GitHub
17 changed files with 53 additions and 37 deletions

2
go.mod
View File

@@ -18,7 +18,7 @@ require (
github.com/integrii/flaggy v1.4.0
github.com/jesseduffield/generics v0.0.0-20250517122708-b0b4a53a6f5c
github.com/jesseduffield/go-git/v5 v5.14.1-0.20250407170251-e1a013310ccd
github.com/jesseduffield/gocui v0.3.1-0.20251214132308-02ab34c1c624
github.com/jesseduffield/gocui v0.3.1-0.20251223143206-950739ccd44a
github.com/jesseduffield/lazycore v0.0.0-20221012050358-03d2e40243c5
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0
github.com/karimkhaleel/jsonschema v0.0.0-20231001195015-d933f0d94ea3

4
go.sum
View File

@@ -194,8 +194,8 @@ github.com/jesseduffield/generics v0.0.0-20250517122708-b0b4a53a6f5c h1:tC2Paiis
github.com/jesseduffield/generics v0.0.0-20250517122708-b0b4a53a6f5c/go.mod h1:F2fEBk0ddf6ixrBrJjY7phfQ3hL9rXG0uSjvwYe50bE=
github.com/jesseduffield/go-git/v5 v5.14.1-0.20250407170251-e1a013310ccd h1:ViKj6qth8FgcIWizn9KiACWwPemWSymx62OPN0tHT+Q=
github.com/jesseduffield/go-git/v5 v5.14.1-0.20250407170251-e1a013310ccd/go.mod h1:lRhCiBr6XjQrvcQVa+UYsy/99d3wMXn/a0nSQlhnhlA=
github.com/jesseduffield/gocui v0.3.1-0.20251214132308-02ab34c1c624 h1:30mIX4f52zrO4fWfZQKHJG29t2apcSOtR/sbd3BNsVE=
github.com/jesseduffield/gocui v0.3.1-0.20251214132308-02ab34c1c624/go.mod h1:sLIyZ2J42R6idGdtemZzsiR3xY5EF0KsvYEGh3dQv3s=
github.com/jesseduffield/gocui v0.3.1-0.20251223143206-950739ccd44a h1:XRsyqrSljes4TlaPczQViIAA4xqdnB0fKEEpZdqWWTA=
github.com/jesseduffield/gocui v0.3.1-0.20251223143206-950739ccd44a/go.mod h1:sLIyZ2J42R6idGdtemZzsiR3xY5EF0KsvYEGh3dQv3s=
github.com/jesseduffield/lazycore v0.0.0-20221012050358-03d2e40243c5 h1:CDuQmfOjAtb1Gms6a1p5L2P8RhbLUq5t8aL7PiQd2uY=
github.com/jesseduffield/lazycore v0.0.0-20221012050358-03d2e40243c5/go.mod h1:qxN4mHOAyeIDLP7IK7defgPClM/z1Kze8VVQiaEjzsQ=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=

View File

@@ -25,8 +25,8 @@ type ListContextTrait struct {
func (self *ListContextTrait) IsListContext() {}
func (self *ListContextTrait) FocusLine() {
self.Context.FocusLine()
func (self *ListContextTrait) FocusLine(scrollIntoView bool) {
self.Context.FocusLine(scrollIntoView)
// Doing this at the end of the layout function because we need the view to be
// resized before we focus the line, otherwise if we're in accordion mode
@@ -36,7 +36,7 @@ func (self *ListContextTrait) FocusLine() {
oldOrigin, _ := self.GetViewTrait().ViewPortYBounds()
self.GetViewTrait().FocusPoint(
self.ModelIndexToViewIndex(self.list.GetSelectedLineIdx()))
self.ModelIndexToViewIndex(self.list.GetSelectedLineIdx()), scrollIntoView)
selectRangeIndex, isSelectingRange := self.list.GetRangeStartIdx()
if isSelectingRange {
@@ -75,7 +75,7 @@ func formatListFooter(selectedLineIdx int, length int) string {
}
func (self *ListContextTrait) HandleFocus(opts types.OnFocusOpts) {
self.FocusLine()
self.FocusLine(opts.ScrollSelectionIntoView)
self.GetViewTrait().SetHighlight(self.list.Len() > 0)

View File

@@ -54,7 +54,7 @@ func (self *SimpleContext) HandleFocusLost(opts types.OnFocusLostOpts) {
}
}
func (self *SimpleContext) FocusLine() {
func (self *SimpleContext) FocusLine(scrollIntoView bool) {
}
func (self *SimpleContext) HandleRender() {

View File

@@ -17,8 +17,8 @@ func NewViewTrait(view *gocui.View) *ViewTrait {
return &ViewTrait{view: view}
}
func (self *ViewTrait) FocusPoint(yIdx int) {
self.view.FocusPoint(self.view.OriginX(), yIdx)
func (self *ViewTrait) FocusPoint(yIdx int, scrollIntoView bool) {
self.view.FocusPoint(self.view.OriginX(), yIdx, scrollIntoView)
}
func (self *ViewTrait) SetRangeSelectStart(yIdx int) {

View File

@@ -531,7 +531,7 @@ func (self *BranchesController) createNewBranchWithName(newBranchName string) er
return err
}
self.context().SetSelection(0)
self.c.Helpers().Refs.SelectFirstBranchAndFirstCommit()
self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC, KeepBranchSelectionIndex: true})
return nil
}

View File

@@ -101,7 +101,7 @@ func (self *CherryPickHelper) Paste() error {
// below the selection.
if commit := self.c.Contexts().LocalCommits.GetSelected(); commit != nil && !commit.IsTODO() {
self.c.Contexts().LocalCommits.MoveSelection(len(cherryPickedCommits))
self.c.Contexts().LocalCommits.FocusLine()
self.c.Contexts().LocalCommits.FocusLine(true)
}
// If we're in the cherry-picking state at this point, it must

View File

@@ -31,6 +31,15 @@ func NewRefsHelper(
}
}
func (self *RefsHelper) SelectFirstBranchAndFirstCommit() {
self.c.Contexts().Branches.SetSelection(0)
self.c.Contexts().ReflogCommits.SetSelection(0)
self.c.Contexts().LocalCommits.SetSelection(0)
self.c.Contexts().Branches.GetView().SetOriginY(0)
self.c.Contexts().ReflogCommits.GetView().SetOriginY(0)
self.c.Contexts().LocalCommits.GetView().SetOriginY(0)
}
func (self *RefsHelper) CheckoutRef(ref string, options types.CheckoutRefOptions) error {
waitingStatus := options.WaitingStatus
if waitingStatus == "" {
@@ -40,9 +49,8 @@ func (self *RefsHelper) CheckoutRef(ref string, options types.CheckoutRefOptions
cmdOptions := git_commands.CheckoutOptions{Force: false, EnvVars: options.EnvVars}
refresh := func() {
self.c.Contexts().Branches.SetSelection(0)
self.c.Contexts().ReflogCommits.SetSelection(0)
self.c.Contexts().LocalCommits.SetSelection(0)
self.SelectFirstBranchAndFirstCommit()
// loading a heap of commits is slow so we limit them whenever doing a reset
self.c.Contexts().LocalCommits.SetLimitCommits(true)
@@ -348,8 +356,7 @@ func (self *RefsHelper) NewBranch(from string, fromFormattedName string, suggest
self.c.Context().Push(self.c.Contexts().Branches, types.OnFocusOpts{})
}
self.c.Contexts().LocalCommits.SetSelection(0)
self.c.Contexts().Branches.SetSelection(0)
self.SelectFirstBranchAndFirstCommit()
self.c.Refresh(types.RefreshOptions{Mode: types.BLOCK_UI, KeepBranchSelectionIndex: true})
}
@@ -504,8 +511,7 @@ func (self *RefsHelper) moveCommitsToNewBranchStackedOnCurrentBranch(newBranchNa
}
}
self.c.Contexts().LocalCommits.SetSelection(0)
self.c.Contexts().Branches.SetSelection(0)
self.SelectFirstBranchAndFirstCommit()
self.c.Refresh(types.RefreshOptions{Mode: types.BLOCK_UI, KeepBranchSelectionIndex: true})
return nil
@@ -543,8 +549,7 @@ func (self *RefsHelper) moveCommitsToNewBranchOffOfMainBranch(newBranchName stri
}
}
self.c.Contexts().LocalCommits.SetSelection(0)
self.c.Contexts().Branches.SetSelection(0)
self.SelectFirstBranchAndFirstCommit()
self.c.Refresh(types.RefreshOptions{Mode: types.BLOCK_UI, KeepBranchSelectionIndex: true})
return nil

View File

@@ -116,7 +116,12 @@ func (self *ListController) handleLineChangeAux(f func(int), change int) error {
}
if cursorMoved || rangeBefore != rangeAfter {
self.context.HandleFocus(types.OnFocusOpts{})
self.context.HandleFocus(types.OnFocusOpts{ScrollSelectionIntoView: true})
} else {
// If the selection did not change (because, for example, we are at the top of the list and
// press up), we still want to ensure that the selection is visible. This is useful after
// scrolling the selection out of view with the mouse.
self.context.FocusLine(true)
}
return nil
@@ -173,6 +178,8 @@ func (self *ListController) handlePageChange(delta int) error {
}
}
// Since we are maintaining the scroll position ourselves above, there's no point in passing
// ScrollSelectionIntoView=true here.
self.context.HandleFocus(types.OnFocusOpts{})
return nil

View File

@@ -869,7 +869,7 @@ func (self *LocalCommitsController) revert(commits []*models.Commit, start, end
return err
}
self.context().MoveSelection(len(commits))
self.context().HandleFocus(types.OnFocusOpts{})
self.context().HandleFocus(types.OnFocusOpts{ScrollSelectionIntoView: true})
if mustStash {
if err := self.c.Git().Stash.Pop(0); err != nil {

View File

@@ -368,7 +368,7 @@ func (self *RemotesController) fetchAndCheckout(remote *models.Remote, branchNam
err = self.c.Git().Branch.New(branchName, remote.Name+"/"+branchName)
if err == nil {
self.c.Context().Push(self.c.Contexts().Branches, types.OnFocusOpts{})
self.c.Contexts().Branches.SetSelection(0)
self.c.Helpers().Refs.SelectFirstBranchAndFirstCommit()
refreshOptions.KeepBranchSelectionIndex = true
}
}

View File

@@ -205,7 +205,7 @@ func (self *StashController) handleRenameStashEntry(stashEntry *models.StashEntr
return err
}
self.context().SetSelection(0) // Select the renamed stash
self.context().FocusLine()
self.context().FocusLine(true)
self.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.STASH}})
return nil
},

View File

@@ -109,7 +109,7 @@ type Context interface {
HandleFocus(opts OnFocusOpts)
HandleFocusLost(opts OnFocusLostOpts)
FocusLine()
FocusLine(scrollIntoView bool)
HandleRender()
HandleRenderToMain()
}
@@ -201,7 +201,7 @@ type IPatchExplorerContext interface {
}
type IViewTrait interface {
FocusPoint(yIdx int)
FocusPoint(yIdx int, scrollIntoView bool)
SetRangeSelectStart(yIdx int)
CancelRangeSelect()
SetViewPortContent(content string)
@@ -221,8 +221,9 @@ type IViewTrait interface {
}
type OnFocusOpts struct {
ClickedWindowName string
ClickedViewLineIdx int
ClickedWindowName string
ClickedViewLineIdx int
ScrollSelectionIntoView bool
}
type OnFocusLostOpts struct {

View File

@@ -140,7 +140,7 @@ func (gui *Gui) postRefreshUpdate(c types.Context) {
// non-focused views to ensure that an inactive selection is painted
// correctly, and that integration tests see the up to date selection
// state.
c.FocusLine()
c.FocusLine(false)
currentCtx := gui.State.ContextMgr.Current()
if currentCtx.GetKey() == context.NORMAL_MAIN_CONTEXT_KEY || currentCtx.GetKey() == context.NORMAL_SECONDARY_CONTEXT_KEY {

View File

@@ -51,7 +51,7 @@ func RunTUI(raceDetector bool) {
if err != nil {
return err
}
listView.FocusPoint(0, app.itemIdx)
listView.FocusPoint(0, app.itemIdx, true)
return nil
}); err != nil {
log.Panicln(err)
@@ -66,7 +66,7 @@ func RunTUI(raceDetector bool) {
if err != nil {
return err
}
listView.FocusPoint(0, app.itemIdx)
listView.FocusPoint(0, app.itemIdx, true)
return nil
}); err != nil {
log.Panicln(err)

View File

@@ -262,7 +262,7 @@ func (v *View) SelectSearchResult(index int) error {
y := v.searcher.searchPositions[index].Y
v.FocusPoint(v.ox, y)
v.FocusPoint(v.ox, y, true)
if v.searcher.onSelectItem != nil {
return v.searcher.onSelectItem(y, index, itemCount)
}
@@ -325,14 +325,17 @@ func (v *View) IsSearching() bool {
return v.searcher.searchString != ""
}
func (v *View) FocusPoint(cx int, cy int) {
func (v *View) FocusPoint(cx int, cy int, scrollIntoView bool) {
lineCount := len(v.lines)
if cy < 0 || cy > lineCount {
return
}
height := v.InnerHeight()
v.oy = calculateNewOrigin(cy, v.oy, lineCount, height)
if scrollIntoView {
height := v.InnerHeight()
v.oy = calculateNewOrigin(cy, v.oy, lineCount, height)
}
v.cx = cx
v.cy = cy - v.oy
}

2
vendor/modules.txt vendored
View File

@@ -221,7 +221,7 @@ github.com/jesseduffield/go-git/v5/utils/merkletrie/internal/frame
github.com/jesseduffield/go-git/v5/utils/merkletrie/noder
github.com/jesseduffield/go-git/v5/utils/sync
github.com/jesseduffield/go-git/v5/utils/trace
# github.com/jesseduffield/gocui v0.3.1-0.20251214132308-02ab34c1c624
# github.com/jesseduffield/gocui v0.3.1-0.20251223143206-950739ccd44a
## explicit; go 1.12
github.com/jesseduffield/gocui
# github.com/jesseduffield/lazycore v0.0.0-20221012050358-03d2e40243c5