Fix searching commits or main view after switching repos

When switching to a different repo, and then back to the original one, searching
would no longer work. The reason is that our contexts set callbacks on their
views; when switching to a different repo we instantiate a new set of contexts,
so they will overwrite the views' callbacks with their new ones, but when
switching back to the original repo, we reuse the old contexts because they are
still in memory, but they won't set their callbacks again since they only do
this on construction.

To fix this, replace the view-local callbacks with a global one on the gui that
takes the view as an argument, so that the callback can look up the associated
context dynamically.
This commit is contained in:
Stefan Haller 2026-03-25 18:55:28 +01:00
parent 587a8bbf4f
commit 3eb5841b83
11 changed files with 95 additions and 58 deletions

8
go.mod
View file

@ -17,7 +17,7 @@ require (
github.com/gookit/color v1.4.2
github.com/integrii/flaggy v1.4.0
github.com/jesseduffield/generics v0.0.0-20250517122708-b0b4a53a6f5c
github.com/jesseduffield/gocui v0.3.1-0.20260308162933-5e45e57b5564
github.com/jesseduffield/gocui v0.3.1-0.20260327132312-944dab3bc980
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
@ -37,7 +37,7 @@ require (
github.com/stretchr/testify v1.10.0
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56
golang.org/x/sync v0.19.0
golang.org/x/sync v0.20.0
golang.org/x/sys v0.42.0
gopkg.in/ozeidan/fuzzy-patricia.v3 v3.0.0
gopkg.in/yaml.v3 v3.0.1
@ -66,8 +66,8 @@ require (
github.com/rogpeppe/go-internal v1.14.1 // indirect
github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect
golang.org/x/net v0.47.0 // indirect
golang.org/x/term v0.40.0 // indirect
golang.org/x/text v0.34.0 // indirect
golang.org/x/term v0.41.0 // indirect
golang.org/x/text v0.35.0 // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
gopkg.in/fsnotify.v1 v1.4.7 // indirect
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect

16
go.sum
View file

@ -159,8 +159,8 @@ github.com/invopop/jsonschema v0.10.0 h1:c1ktzNLBun3LyQQhyty5WE3lulbOdIIyOVlkmDL
github.com/invopop/jsonschema v0.10.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0=
github.com/jesseduffield/generics v0.0.0-20250517122708-b0b4a53a6f5c h1:tC2PaiisXAC5sOjDPfMArSnbswDObtCssx+xn28edX4=
github.com/jesseduffield/generics v0.0.0-20250517122708-b0b4a53a6f5c/go.mod h1:F2fEBk0ddf6ixrBrJjY7phfQ3hL9rXG0uSjvwYe50bE=
github.com/jesseduffield/gocui v0.3.1-0.20260308162933-5e45e57b5564 h1:aB/Ytu+OCEpjft/BehqbH8/PTdgLREqbCvjK1JXctoo=
github.com/jesseduffield/gocui v0.3.1-0.20260308162933-5e45e57b5564/go.mod h1:lQCd2TvvNXVKFBowy4A7xxZbUp+1KEiGs4j0Q5Zt9gQ=
github.com/jesseduffield/gocui v0.3.1-0.20260327132312-944dab3bc980 h1:LEZwOrBm9S+4lRlXpoz+RSzSvhOVE+6v/Rk+A7Kg00Q=
github.com/jesseduffield/gocui v0.3.1-0.20260327132312-944dab3bc980/go.mod h1:lQCd2TvvNXVKFBowy4A7xxZbUp+1KEiGs4j0Q5Zt9gQ=
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=
@ -366,8 +366,8 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.0.0-20170407050850-f3918c30c5c2/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@ -419,8 +419,8 @@ golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg=
golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM=
golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU=
golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@ -431,8 +431,8 @@ golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=

View file

@ -134,9 +134,6 @@ func NewLocalCommitsContext(c *ContextCommon) *LocalCommitsContext {
},
}
ctx.GetView().SetRenderSearchStatus(ctx.SearchTrait.RenderSearchStatus)
ctx.GetView().SetOnSelectItem(ctx.OnSearchSelect)
return ctx
}

View file

@ -31,12 +31,12 @@ func NewMainContext(
SearchTrait: NewSearchTrait(c),
}
ctx.GetView().SetRenderSearchStatus(ctx.SearchTrait.RenderSearchStatus)
ctx.GetView().SetOnSelectItem(func(int) {})
return ctx
}
func (self *MainContext) ModelSearchResults(searchStr string, caseSensitive bool) []gocui.SearchPosition {
return nil
}
func (self *MainContext) OnSearchSelect(int) {
}

View file

@ -53,15 +53,6 @@ func NewPatchExplorerContext(
SearchTrait: NewSearchTrait(c),
}
ctx.GetView().SetRenderSearchStatus(ctx.SearchTrait.RenderSearchStatus)
ctx.GetView().SetOnSelectItem(func(selectedLineIdx int) {
ctx.GetMutex().Lock()
defer ctx.GetMutex().Unlock()
ctx.inOnSelectItemCallback = true
ctx.NavigateTo(selectedLineIdx)
ctx.inOnSelectItemCallback = false
})
ctx.SetHandleRenderFunc(ctx.OnViewWidthChanged)
return ctx
@ -146,6 +137,14 @@ func (self *PatchExplorerContext) ModelSearchResults(searchStr string, caseSensi
return nil
}
func (self *PatchExplorerContext) OnSearchSelect(selectedLineIdx int) {
self.GetMutex().Lock()
defer self.GetMutex().Unlock()
self.inOnSelectItemCallback = true
self.NavigateTo(selectedLineIdx)
self.inOnSelectItemCallback = false
}
func (self *PatchExplorerContext) OnViewWidthChanged() {
if state := self.GetState(); state != nil {
state.OnViewWidthChanged(self.GetView())

View file

@ -134,9 +134,6 @@ func NewSubCommitsContext(
},
}
ctx.GetView().SetRenderSearchStatus(ctx.SearchTrait.RenderSearchStatus)
ctx.GetView().SetOnSelectItem(ctx.OnSearchSelect)
return ctx
}

View file

@ -398,6 +398,24 @@ func (gui *Gui) onNewRepo(startArgs appTypes.StartArgs, contextKey types.Context
return nil
})
gui.g.SetOnSelectSearchResultFunc(func(v *gocui.View, selectedLineIdx int) {
ctx, ok := gui.helpers.View.ContextForView(v.Name())
if ok {
if searchableContext, ok := ctx.(types.ISearchableContext); ok {
searchableContext.OnSearchSelect(selectedLineIdx)
}
}
})
gui.g.SetRenderSearchStatusFunc(func(v *gocui.View, index int, total int) {
ctx, ok := gui.helpers.View.ContextForView(v.Name())
if ok {
if searchableContext, ok := ctx.(types.ISearchableContext); ok {
searchableContext.RenderSearchStatus(index, total)
}
}
})
// if a context key has been given, push that instead, and set its index to 0
if contextKey != context.NO_CONTEXT {
contextToPush = gui.c.ContextForKey(contextKey)

View file

@ -151,6 +151,7 @@ type ISearchableContext interface {
// This must be implemented by each concrete context. Return nil if not searching the model.
ModelSearchResults(searchStr string, caseSensitive bool) []gocui.SearchPosition
OnSearchSelect(selectedLineIdx int)
}
type DiffableContext interface {

View file

@ -139,21 +139,23 @@ type Gui struct {
ReplayedEvents replayedEvents
playRecording bool
tabClickBindings []*tabClickBinding
viewMouseBindings []*ViewMouseBinding
lastClick *clickInfo
gEvents chan GocuiEvent
userEvents chan userEvent
views []*View
currentView *View
managers []Manager
keybindings []*keybinding
focusHandler func(bool) error
openHyperlink func(string, string) error
maxX, maxY int
outputMode OutputMode
stop chan struct{}
blacklist []Key
tabClickBindings []*tabClickBinding
viewMouseBindings []*ViewMouseBinding
lastClick *clickInfo
gEvents chan GocuiEvent
userEvents chan userEvent
views []*View
currentView *View
managers []Manager
keybindings []*keybinding
focusHandler func(bool) error
openHyperlink func(string, string) error
onSelectSearchResultFunc func(*View, int)
renderSearchStatusFunc func(*View, int, int)
maxX, maxY int
outputMode OutputMode
stop chan struct{}
blacklist []Key
// BgColor and FgColor allow to configure the background and foreground
// colors of the GUI.
@ -355,11 +357,26 @@ func (g *Gui) SetView(name string, x0, y0, x1, y1 int, overlaps byte) (*View, er
v.Overlaps = overlaps
g.views = append(g.views, v)
v.setOnSelectResult(g.onSelectSearchItem)
v.setRenderSearchStatus(g.renderSearchStatus)
g.Mutexes.ViewsMutex.Unlock()
return v, errors.Wrap(ErrUnknownView, 0)
}
func (g *Gui) onSelectSearchItem(v *View, selectedLineIdx int) {
if g.onSelectSearchResultFunc != nil {
g.onSelectSearchResultFunc(v, selectedLineIdx)
}
}
func (g *Gui) renderSearchStatus(v *View, selected int, total int) {
if g.renderSearchStatusFunc != nil {
g.renderSearchStatusFunc(v, selected, total)
}
}
// SetViewBeneath sets a view stacked beneath another view
func (g *Gui) SetViewBeneath(name string, aboveViewName string, height int) (*View, error) {
aboveView, err := g.View(aboveViewName)
@ -643,6 +660,14 @@ func (g *Gui) SetOpenHyperlinkFunc(openHyperlinkFunc func(string, string) error)
g.openHyperlink = openHyperlinkFunc
}
func (g *Gui) SetOnSelectSearchResultFunc(onSelectSearchResultFunc func(*View, int)) {
g.onSelectSearchResultFunc = onSelectSearchResultFunc
}
func (g *Gui) SetRenderSearchStatusFunc(renderSearchStatusFunc func(*View, int, int)) {
g.renderSearchStatusFunc = renderSearchStatusFunc
}
// getKey takes an empty interface with a key and returns the corresponding
// typed Key or rune.
func getKey(key any) (Key, rune, error) {

View file

@ -214,21 +214,21 @@ type searcher struct {
searchPositions []SearchPosition
modelSearchResults []SearchPosition
currentSearchIndex int
onSelectItem func(int)
renderSearchStatus func(int, int)
onSelectItem func(*View, int)
renderSearchStatus func(*View, int, int)
}
func (v *View) SetRenderSearchStatus(renderSearchStatus func(int, int)) {
func (v *View) setRenderSearchStatus(renderSearchStatus func(*View, int, int)) {
v.searcher.renderSearchStatus = renderSearchStatus
}
func (v *View) SetOnSelectItem(onSelectItem func(int)) {
func (v *View) setOnSelectResult(onSelectItem func(*View, int)) {
v.searcher.onSelectItem = onSelectItem
}
func (v *View) renderSearchStatus(index int, itemCount int) {
if v.searcher.renderSearchStatus != nil {
v.searcher.renderSearchStatus(index, itemCount)
v.searcher.renderSearchStatus(v, index, itemCount)
}
}
@ -286,7 +286,7 @@ func (v *View) SelectSearchResult(index int) {
v.FocusPoint(v.ox, y, true)
v.renderSearchStatus(index, itemCount)
if v.searcher.onSelectItem != nil {
v.searcher.onSelectItem(y)
v.searcher.onSelectItem(v, y)
}
}

14
vendor/modules.txt vendored
View file

@ -89,7 +89,7 @@ github.com/integrii/flaggy
github.com/jesseduffield/generics/maps
github.com/jesseduffield/generics/orderedset
github.com/jesseduffield/generics/set
# github.com/jesseduffield/gocui v0.3.1-0.20260308162933-5e45e57b5564
# github.com/jesseduffield/gocui v0.3.1-0.20260327132312-944dab3bc980
## explicit; go 1.25
github.com/jesseduffield/gocui
# github.com/jesseduffield/lazycore v0.0.0-20221012050358-03d2e40243c5
@ -188,19 +188,19 @@ golang.org/x/exp/constraints
golang.org/x/exp/slices
# golang.org/x/net v0.47.0
## explicit; go 1.24.0
# golang.org/x/sync v0.19.0
## explicit; go 1.24.0
# golang.org/x/sync v0.20.0
## explicit; go 1.25.0
golang.org/x/sync/errgroup
# golang.org/x/sys v0.42.0
## explicit; go 1.25.0
golang.org/x/sys/plan9
golang.org/x/sys/unix
golang.org/x/sys/windows
# golang.org/x/term v0.40.0
## explicit; go 1.24.0
# golang.org/x/term v0.41.0
## explicit; go 1.25.0
golang.org/x/term
# golang.org/x/text v0.34.0
## explicit; go 1.24.0
# golang.org/x/text v0.35.0
## explicit; go 1.25.0
golang.org/x/text/encoding
golang.org/x/text/encoding/internal/identifier
golang.org/x/text/runes