Derive the selection highlight from the context stack

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>
This commit is contained in:
Stefan Haller 2026-08-21 13:21:35 +02:00
parent d1707d5dd2
commit ec3f681ebf
8 changed files with 33 additions and 23 deletions

View file

@ -3,6 +3,7 @@ package gui
import (
"sync"
"github.com/jesseduffield/generics/set"
"github.com/jesseduffield/lazygit/pkg/gui/context"
"github.com/jesseduffield/lazygit/pkg/gui/types"
"github.com/jesseduffield/lazygit/pkg/utils"
@ -180,10 +181,6 @@ func (self *ContextMgr) Activate(c types.Context, opts types.OnFocusOpts) {
self.gui.helpers.Window.MoveToTopOfWindow(c)
inputViewName := c.GetInputViewName()
oldView := self.gui.c.GocuiGui().CurrentView()
if oldView != nil && oldView.Name() != inputViewName {
oldView.HighlightInactive = true
}
if _, err := self.gui.c.GocuiGui().SetCurrentView(inputViewName); err != nil {
panic(err)
}
@ -199,9 +196,37 @@ func (self *ContextMgr) Activate(c types.Context, opts types.OnFocusOpts) {
self.gui.c.GocuiGui().Cursor = v.Editable && v.Mask == ""
self.updateSelectionHighlights()
c.HandleFocus(opts)
}
// updateSelectionHighlights re-derives which views draw a selection, and which of
// them draw theirs as the active one: 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 the
// active selection while the ones behind it show inactive ones.
//
// Both of those can change, so this is called wherever they do: from Activate, which
// every change to the stack goes through; after a refresh, since that is when the
// contents of a list change; and from whoever tells a context that its content has
// gained or lost something to select.
func (self *ContextMgr) updateSelectionHighlights() {
self.RLock()
defer self.RUnlock()
onStack := set.NewFromSlice(lo.Map(self.ContextStack,
func(c types.Context, _ int) types.ContextKey { return c.GetKey() }))
currentKey := self.currentContextWithoutLock().GetKey()
for _, c := range self.allContexts.Flatten() {
// The global context has no view of its own.
if view := c.GetView(); view != nil {
view.Highlight = onStack.Includes(c.GetKey()) && c.HasSelectableContent()
view.HighlightInactive = c.GetKey() != currentKey
}
}
}
func (self *ContextMgr) Current() types.Context {
self.RLock()
defer self.RUnlock()

View file

@ -106,8 +106,6 @@ func formatListFooter(selectedLineIdx int, length int) string {
func (self *ListContextTrait) HandleFocus(opts types.OnFocusOpts) {
self.FocusLine(!opts.KeepScrollPosition)
self.GetViewTrait().SetHighlight(self.HasSelectableContent())
self.Context.HandleFocus(opts)
}

View file

@ -33,10 +33,6 @@ func NewDisplayContext(key types.ContextKey, view *gocui.View, windowName string
}
func (self *SimpleContext) HandleFocus(opts types.OnFocusOpts) {
if self.hasSelectableContent {
self.GetViewTrait().SetHighlight(true)
}
for _, fn := range self.onFocusFns {
fn(opts)
}
@ -47,7 +43,6 @@ func (self *SimpleContext) HandleFocus(opts types.OnFocusOpts) {
}
func (self *SimpleContext) HandleFocusLost(opts types.OnFocusLostOpts) {
self.GetViewTrait().SetHighlight(false)
self.view.SetOriginX(0)
for _, fn := range self.onFocusLostFns {
fn(opts)

View file

@ -43,11 +43,6 @@ func (self *ViewTrait) SetContent(content string) {
self.view.SetContent(content)
}
func (self *ViewTrait) SetHighlight(highlight bool) {
self.view.Highlight = highlight
self.view.HighlightInactive = false
}
func (self *ViewTrait) SetFooter(value string) {
self.view.Footer = value
}

View file

@ -229,7 +229,6 @@ type IViewTrait interface {
ScrollDown(value int)
PageDelta() int
SelectedLineIdx() int
SetHighlight(bool)
}
type OnFocusOpts struct {

View file

@ -140,6 +140,10 @@ func (gui *Gui) postRefreshUpdate(c types.Context, opts types.OnFocusOpts) {
c.HandleRender()
// The render may have given the context its first item, or taken its last one
// away, which decides whether its view draws a selection at all.
gui.State.ContextMgr.updateSelectionHighlights()
if gui.currentViewName() == c.GetInputViewName() {
c.HandleFocus(opts)
} else {

View file

@ -31,9 +31,6 @@ var UnfocusedListHidesSelectionWhenEmptied = NewIntegrationTest(NewIntegrationTe
t.RefreshInBackground()
}).
IsEmpty().
/* EXPECTED:
SelectionIsHidden()
ACTUAL: */
SelectionIsInactive()
},
})

View file

@ -29,9 +29,6 @@ var UnfocusedListShowsSelectionWhenFilled = NewIntegrationTest(NewIntegrationTes
t.RefreshInBackground()
}).
Lines(Contains("file2")).
/* EXPECTED:
SelectionIsInactive()
ACTUAL: */
SelectionIsHidden()
},
})