mirror of
https://github.com/jesseduffield/lazygit.git
synced 2026-09-10 07:36:27 -04:00
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>
174 lines
5.6 KiB
Go
174 lines
5.6 KiB
Go
package context
|
|
|
|
import (
|
|
"fmt"
|
|
|
|
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
|
)
|
|
|
|
type ListContextTrait struct {
|
|
types.Context
|
|
ListRenderer
|
|
|
|
c *ContextCommon
|
|
// Some contexts, like the commit context, will highlight the path from the selected commit
|
|
// to its parents, because it's ambiguous otherwise. For these, we need to refresh the viewport
|
|
// so that we show the highlighted path.
|
|
// TODO: now that we allow scrolling, we should be smarter about what gets refreshed:
|
|
// we should find out exactly which lines are now part of the path and refresh those.
|
|
// We should also keep track of the previous path and refresh those lines too.
|
|
refreshViewportOnChange bool
|
|
// If this is true, we only render the visible lines of the list. Useful for lists that can
|
|
// get very long, because it can save a lot of memory
|
|
renderOnlyVisibleLines bool
|
|
// If renderOnlyVisibleLines is true, needRerenderVisibleLines indicates whether we need to
|
|
// rerender the visible lines e.g. because the scroll position changed
|
|
needRerenderVisibleLines bool
|
|
|
|
// true if we're inside the OnSearchSelect call; in that case we don't want to update the search
|
|
// result index.
|
|
inOnSearchSelect bool
|
|
}
|
|
|
|
func (self *ListContextTrait) IsListContext() {}
|
|
|
|
func (self *ListContextTrait) FocusLine(scrollIntoView bool) {
|
|
self.Context.FocusLine(scrollIntoView)
|
|
|
|
// Need to capture this in a local variable because by the time the AfterLayout function runs,
|
|
// the field will have been reset to false already
|
|
inOnSearchSelect := self.inOnSearchSelect
|
|
|
|
// 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
|
|
// the view could be squashed and won't know how to adjust the cursor/origin.
|
|
// Also, refreshing the viewport needs to happen after the view has been resized.
|
|
self.c.AfterLayout(func() error {
|
|
oldOrigin, _ := self.GetViewTrait().ViewPortYBounds()
|
|
|
|
self.GetViewTrait().FocusPoint(
|
|
self.ModelIndexToViewIndex(self.list.GetSelectedLineIdx()), scrollIntoView)
|
|
if !inOnSearchSelect {
|
|
self.GetView().SetNearestSearchPosition()
|
|
}
|
|
|
|
selectRangeIndex, isSelectingRange := self.list.GetRangeStartIdx()
|
|
if isSelectingRange {
|
|
selectRangeIndex = self.ModelIndexToViewIndex(selectRangeIndex)
|
|
self.GetViewTrait().SetRangeSelectStart(selectRangeIndex)
|
|
} else {
|
|
self.GetViewTrait().CancelRangeSelect()
|
|
}
|
|
|
|
if self.refreshViewportOnChange {
|
|
self.refreshViewport()
|
|
} else if self.renderOnlyVisibleLines {
|
|
newOrigin, _ := self.GetViewTrait().ViewPortYBounds()
|
|
if oldOrigin != newOrigin || self.needRerenderVisibleLines {
|
|
self.refreshViewport()
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
|
|
self.setFooter()
|
|
}
|
|
|
|
func (self *ListContextTrait) refreshViewport() {
|
|
startIdx, length := self.GetViewTrait().ViewPortYBounds()
|
|
content := self.renderLines(startIdx, startIdx+length)
|
|
self.GetViewTrait().SetViewPortContent(content)
|
|
}
|
|
|
|
func (self *ListContextTrait) setFooter() {
|
|
self.GetViewTrait().SetFooter(formatListFooter(self.list.GetSelectedLineIdx(), self.list.Len()))
|
|
}
|
|
|
|
func formatListFooter(selectedLineIdx int, length int) string {
|
|
return fmt.Sprintf("%d of %d", selectedLineIdx+1, length)
|
|
}
|
|
|
|
func (self *ListContextTrait) HandleFocus(opts types.OnFocusOpts) {
|
|
self.FocusLine(!opts.KeepScrollPosition)
|
|
|
|
self.GetViewTrait().SetHighlight(self.list.Len() > 0)
|
|
|
|
self.Context.HandleFocus(opts)
|
|
}
|
|
|
|
func (self *ListContextTrait) HandleFocusLost(opts types.OnFocusLostOpts) {
|
|
self.GetViewTrait().SetOriginX(0)
|
|
|
|
if self.refreshViewportOnChange {
|
|
self.refreshViewport()
|
|
}
|
|
|
|
self.Context.HandleFocusLost(opts)
|
|
}
|
|
|
|
// OnFocus assumes that the content of the context has already been rendered to the view. OnRender is the function which actually renders the content to the view
|
|
func (self *ListContextTrait) HandleRender() {
|
|
self.list.ClampSelection()
|
|
if self.renderOnlyVisibleLines {
|
|
// Rendering only the visible area can save a lot of cell memory for
|
|
// those views that support it.
|
|
totalLength := self.list.Len()
|
|
if self.getNonModelItems != nil {
|
|
totalLength += len(self.getNonModelItems())
|
|
}
|
|
startIdx, length := self.GetViewTrait().ViewPortYBounds()
|
|
content := self.renderLines(startIdx, startIdx+length)
|
|
self.GetViewTrait().SetViewPortContentAndClearEverythingElse(totalLength, content)
|
|
self.needRerenderVisibleLines = false
|
|
} else {
|
|
content := self.renderLines(-1, -1)
|
|
self.GetViewTrait().SetContent(content)
|
|
}
|
|
self.setFooter()
|
|
}
|
|
|
|
func (self *ListContextTrait) OnSearchSelect(selectedLineIdx int) {
|
|
self.GetList().SetSelection(self.ViewIndexToModelIndex(selectedLineIdx))
|
|
self.inOnSearchSelect = true
|
|
self.HandleFocus(types.OnFocusOpts{})
|
|
self.inOnSearchSelect = false
|
|
}
|
|
|
|
func (self *ListContextTrait) IsItemVisible(item types.HasUrn) bool {
|
|
startIdx, length := self.GetViewTrait().ViewPortYBounds()
|
|
selectionStart := self.ViewIndexToModelIndex(startIdx)
|
|
selectionEnd := self.ViewIndexToModelIndex(startIdx + length)
|
|
for i := selectionStart; i < selectionEnd; i++ {
|
|
iterItem := self.GetList().GetItem(i)
|
|
if iterItem != nil && iterItem.URN() == item.URN() {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// By default, list contexts supports range select
|
|
func (self *ListContextTrait) RangeSelectEnabled() bool {
|
|
return true
|
|
}
|
|
|
|
func (self *ListContextTrait) RenderOnlyVisibleLines() bool {
|
|
return self.renderOnlyVisibleLines
|
|
}
|
|
|
|
func (self *ListContextTrait) SetNeedRerenderVisibleLines() {
|
|
self.needRerenderVisibleLines = true
|
|
}
|
|
|
|
func (self *ListContextTrait) TotalContentHeight() int {
|
|
result := self.list.Len()
|
|
if self.getNonModelItems != nil {
|
|
result += len(self.getNonModelItems())
|
|
}
|
|
return result
|
|
}
|
|
|
|
func (self *ListContextTrait) IndexForGotoBottom() int {
|
|
return self.list.Len() - 1
|
|
}
|