mirror of
https://github.com/jesseduffield/lazygit.git
synced 2026-09-10 07:36:27 -04:00
A view drew a selection because something told it to, from four places on three different schedules: a context being focused, a context losing focus, a context being activated over another one, and a list being re-rendered. Whether the flags ended up describing the state of the app depended on which of those had run last, and the last one to run was often none of them: a refresh only re-focuses the view that has the focus, so a list whose contents changed underneath an unfocused panel kept whichever highlight it happened to have. Derive both flags instead, in one place, from the two things they mean: a view shows a selection while its context is on the stack and has something to select, and the context the user is in shows an active one where the ones behind it show inactive ones. Nothing else needs to say anything about highlighting, so nothing else can leave a view saying something untrue about where the focus is. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
187 lines
6 KiB
Go
187 lines
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
|
|
|
|
// If set, this renders the "x of y" footer instead of the default, which puts
|
|
// it on the bottom border of the list's own view. A list that is part of a
|
|
// composite panel can use this to put it somewhere else; see MenuContext.
|
|
renderFooter func(footer string)
|
|
}
|
|
|
|
func (self *ListContextTrait) IsListContext() {}
|
|
|
|
func (self *ListContextTrait) HasSelectableContent() bool {
|
|
return self.list.Len() > 0
|
|
}
|
|
|
|
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() {
|
|
footer := formatListFooter(self.list.GetSelectedLineIdx(), self.list.Len())
|
|
if self.renderFooter != nil {
|
|
self.renderFooter(footer)
|
|
return
|
|
}
|
|
|
|
self.GetViewTrait().SetFooter(footer)
|
|
}
|
|
|
|
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.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
|
|
}
|