From ec3f681ebf7409f9b64f113c4c757dc642147720 Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Fri, 21 Aug 2026 13:21:35 +0200 Subject: [PATCH] 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) --- pkg/gui/context.go | 33 ++++++++++++++++--- pkg/gui/context/list_context_trait.go | 2 -- pkg/gui/context/simple_context.go | 5 --- pkg/gui/context/view_trait.go | 5 --- pkg/gui/types/context.go | 1 - pkg/gui/view_helpers.go | 4 +++ ...cused_list_hides_selection_when_emptied.go | 3 -- ...ocused_list_shows_selection_when_filled.go | 3 -- 8 files changed, 33 insertions(+), 23 deletions(-) diff --git a/pkg/gui/context.go b/pkg/gui/context.go index cd959274c..d83b144f2 100644 --- a/pkg/gui/context.go +++ b/pkg/gui/context.go @@ -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() diff --git a/pkg/gui/context/list_context_trait.go b/pkg/gui/context/list_context_trait.go index 102684e58..2e4ae7267 100644 --- a/pkg/gui/context/list_context_trait.go +++ b/pkg/gui/context/list_context_trait.go @@ -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) } diff --git a/pkg/gui/context/simple_context.go b/pkg/gui/context/simple_context.go index 36cd90d96..2de4199e2 100644 --- a/pkg/gui/context/simple_context.go +++ b/pkg/gui/context/simple_context.go @@ -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) diff --git a/pkg/gui/context/view_trait.go b/pkg/gui/context/view_trait.go index 8e12e083f..9fc078e61 100644 --- a/pkg/gui/context/view_trait.go +++ b/pkg/gui/context/view_trait.go @@ -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 } diff --git a/pkg/gui/types/context.go b/pkg/gui/types/context.go index ddf7b345c..93b92c70e 100644 --- a/pkg/gui/types/context.go +++ b/pkg/gui/types/context.go @@ -229,7 +229,6 @@ type IViewTrait interface { ScrollDown(value int) PageDelta() int SelectedLineIdx() int - SetHighlight(bool) } type OnFocusOpts struct { diff --git a/pkg/gui/view_helpers.go b/pkg/gui/view_helpers.go index 2151a5692..5f3e4c2ab 100644 --- a/pkg/gui/view_helpers.go +++ b/pkg/gui/view_helpers.go @@ -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 { diff --git a/pkg/integration/tests/ui/unfocused_list_hides_selection_when_emptied.go b/pkg/integration/tests/ui/unfocused_list_hides_selection_when_emptied.go index f6522770d..eef409cb2 100644 --- a/pkg/integration/tests/ui/unfocused_list_hides_selection_when_emptied.go +++ b/pkg/integration/tests/ui/unfocused_list_hides_selection_when_emptied.go @@ -31,9 +31,6 @@ var UnfocusedListHidesSelectionWhenEmptied = NewIntegrationTest(NewIntegrationTe t.RefreshInBackground() }). IsEmpty(). - /* EXPECTED: SelectionIsHidden() - ACTUAL: */ - SelectionIsInactive() }, }) diff --git a/pkg/integration/tests/ui/unfocused_list_shows_selection_when_filled.go b/pkg/integration/tests/ui/unfocused_list_shows_selection_when_filled.go index 9ff5388a5..78a8f210d 100644 --- a/pkg/integration/tests/ui/unfocused_list_shows_selection_when_filled.go +++ b/pkg/integration/tests/ui/unfocused_list_shows_selection_when_filled.go @@ -29,9 +29,6 @@ var UnfocusedListShowsSelectionWhenFilled = NewIntegrationTest(NewIntegrationTes t.RefreshInBackground() }). Lines(Contains("file2")). - /* EXPECTED: SelectionIsInactive() - ACTUAL: */ - SelectionIsHidden() }, })