Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ require (
github.com/integrii/flaggy v1.4.0
github.com/jesseduffield/generics v0.0.0-20250517122708-b0b4a53a6f5c
github.com/jesseduffield/go-git/v5 v5.14.1-0.20250407170251-e1a013310ccd
github.com/jesseduffield/gocui v0.3.1-0.20260103133810-b7e030324985
github.com/jesseduffield/gocui v0.3.1-0.20260104174656-7b510338b235
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
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -189,8 +189,8 @@ github.com/jesseduffield/generics v0.0.0-20250517122708-b0b4a53a6f5c h1:tC2Paiis
github.com/jesseduffield/generics v0.0.0-20250517122708-b0b4a53a6f5c/go.mod h1:F2fEBk0ddf6ixrBrJjY7phfQ3hL9rXG0uSjvwYe50bE=
github.com/jesseduffield/go-git/v5 v5.14.1-0.20250407170251-e1a013310ccd h1:ViKj6qth8FgcIWizn9KiACWwPemWSymx62OPN0tHT+Q=
github.com/jesseduffield/go-git/v5 v5.14.1-0.20250407170251-e1a013310ccd/go.mod h1:lRhCiBr6XjQrvcQVa+UYsy/99d3wMXn/a0nSQlhnhlA=
github.com/jesseduffield/gocui v0.3.1-0.20260103133810-b7e030324985 h1:qdjGSiNnlGtoi+nzyERQJvee50JpJjeQ6sEhP7jCfMo=
github.com/jesseduffield/gocui v0.3.1-0.20260103133810-b7e030324985/go.mod h1:lQCd2TvvNXVKFBowy4A7xxZbUp+1KEiGs4j0Q5Zt9gQ=
github.com/jesseduffield/gocui v0.3.1-0.20260104174656-7b510338b235 h1:1MjdFm1rUneE1eMYeRkAA3kXswY+h5eLhgJFaZQs9j0=
github.com/jesseduffield/gocui v0.3.1-0.20260104174656-7b510338b235/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=
Expand Down
3 changes: 2 additions & 1 deletion pkg/gui/context/commit_files_context.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,8 @@ func NewCommitFilesContext(c *ContextCommon) *CommitFilesContext {
},
}

ctx.GetView().SetOnSelectItem(ctx.SearchTrait.onSelectItemWrapper(ctx.OnSearchSelect))
ctx.GetView().SetRenderSearchStatus(ctx.SearchTrait.RenderSearchStatus)
ctx.GetView().SetOnSelectItem(ctx.OnSearchSelect)

return ctx
}
Expand Down
16 changes: 14 additions & 2 deletions pkg/gui/context/list_context_trait.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,21 @@ type ListContextTrait struct {
// 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.
Expand All @@ -40,6 +48,9 @@ func (self *ListContextTrait) FocusLine(scrollIntoView bool) {

self.GetViewTrait().FocusPoint(
self.ModelIndexToViewIndex(self.list.GetSelectedLineIdx()), scrollIntoView)
if !inOnSearchSelect {
self.GetView().SetNearestSearchPosition()
}

selectRangeIndex, isSelectingRange := self.list.GetRangeStartIdx()
if isSelectingRange {
Expand Down Expand Up @@ -117,10 +128,11 @@ func (self *ListContextTrait) HandleRender() {
self.setFooter()
}

func (self *ListContextTrait) OnSearchSelect(selectedLineIdx int) error {
func (self *ListContextTrait) OnSearchSelect(selectedLineIdx int) {
self.GetList().SetSelection(self.ViewIndexToModelIndex(selectedLineIdx))
self.inOnSearchSelect = true
self.HandleFocus(types.OnFocusOpts{})
return nil
self.inOnSearchSelect = false
}

func (self *ListContextTrait) IsItemVisible(item types.HasUrn) bool {
Expand Down
3 changes: 2 additions & 1 deletion pkg/gui/context/local_commits_context.go
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,8 @@ func NewLocalCommitsContext(c *ContextCommon) *LocalCommitsContext {
},
}

ctx.GetView().SetOnSelectItem(ctx.SearchTrait.onSelectItemWrapper(ctx.OnSearchSelect))
ctx.GetView().SetRenderSearchStatus(ctx.SearchTrait.RenderSearchStatus)
ctx.GetView().SetOnSelectItem(ctx.OnSearchSelect)

return ctx
}
Expand Down
3 changes: 2 additions & 1 deletion pkg/gui/context/main_context.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ func NewMainContext(
SearchTrait: NewSearchTrait(c),
}

ctx.GetView().SetOnSelectItem(ctx.SearchTrait.onSelectItemWrapper(func(int) error { return nil }))
ctx.GetView().SetRenderSearchStatus(ctx.SearchTrait.RenderSearchStatus)
ctx.GetView().SetOnSelectItem(func(int) {})

return ctx
}
Expand Down
24 changes: 16 additions & 8 deletions pkg/gui/context/patch_explorer_context.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ type PatchExplorerContext struct {
getIncludedLineIndices func() []int
c *ContextCommon
mutex deadlock.Mutex

// true if we're inside the OnSelectItem callback; in that case we don't want to update the
// search result index.
inOnSelectItemCallback bool
}

var (
Expand Down Expand Up @@ -49,14 +53,14 @@ func NewPatchExplorerContext(
SearchTrait: NewSearchTrait(c),
}

ctx.GetView().SetOnSelectItem(ctx.SearchTrait.onSelectItemWrapper(
func(selectedLineIdx int) error {
ctx.GetMutex().Lock()
defer ctx.GetMutex().Unlock()
ctx.NavigateTo(selectedLineIdx)
return nil
}),
)
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)

Expand Down Expand Up @@ -113,6 +117,10 @@ func (self *PatchExplorerContext) FocusSelection() {
// As far as the view is concerned, we are always selecting a range
view.SetRangeSelectStart(startIdx)
view.SetCursorY(endIdx - newOriginY)

if !self.inOnSelectItemCallback {
view.SetNearestSearchPosition()
}
}

func (self *PatchExplorerContext) GetContentToRender() string {
Expand Down
14 changes: 0 additions & 14 deletions pkg/gui/context/search_trait.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,20 +36,6 @@ func (self *SearchTrait) ClearSearchString() {
// used for type switch
func (self *SearchTrait) IsSearchableContext() {}

func (self *SearchTrait) onSelectItemWrapper(innerFunc func(int) error) func(int, int, int) error {
return func(selectedLineIdx int, index int, total int) error {
self.RenderSearchStatus(index, total)

if total != 0 {
if err := innerFunc(selectedLineIdx); err != nil {
return err
}
}

return nil
}
}

func (self *SearchTrait) RenderSearchStatus(index int, total int) {
keybindingConfig := self.c.UserConfig().Keybinding

Expand Down
3 changes: 2 additions & 1 deletion pkg/gui/context/sub_commits_context.go
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,8 @@ func NewSubCommitsContext(
},
}

ctx.GetView().SetOnSelectItem(ctx.SearchTrait.onSelectItemWrapper(ctx.OnSearchSelect))
ctx.GetView().SetRenderSearchStatus(ctx.SearchTrait.RenderSearchStatus)
ctx.GetView().SetOnSelectItem(ctx.OnSearchSelect)

return ctx
}
Expand Down
3 changes: 2 additions & 1 deletion pkg/gui/context/working_tree_context.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,8 @@ func NewWorkingTreeContext(c *ContextCommon) *WorkingTreeContext {
},
}

ctx.GetView().SetOnSelectItem(ctx.SearchTrait.onSelectItemWrapper(ctx.OnSearchSelect))
ctx.GetView().SetRenderSearchStatus(ctx.SearchTrait.RenderSearchStatus)
ctx.GetView().SetOnSelectItem(ctx.OnSearchSelect)

return ctx
}
Expand Down
13 changes: 4 additions & 9 deletions pkg/gui/controllers/helpers/search_helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,20 +108,15 @@ func (self *SearchHelper) Confirm() error {
return self.CancelPrompt()
}

var err error
switch state.SearchType() {
case types.SearchTypeFilter:
self.ConfirmFilter()
case types.SearchTypeSearch:
err = self.ConfirmSearch()
self.ConfirmSearch()
case types.SearchTypeNone:
self.c.Context().Pop()
}

if err != nil {
return err
}

return self.c.ResetKeybindings()
}

Expand All @@ -144,13 +139,13 @@ func (self *SearchHelper) ConfirmFilter() {
self.c.Context().Pop()
}

func (self *SearchHelper) ConfirmSearch() error {
func (self *SearchHelper) ConfirmSearch() {
state := self.searchState()

context, ok := state.Context.(types.ISearchableContext)
if !ok {
self.c.Log.Warnf("Context %s is searchable", state.Context.GetKey())
return nil
return
}

searchString := self.promptContent()
Expand All @@ -161,7 +156,7 @@ func (self *SearchHelper) ConfirmSearch() error {

self.c.Context().Pop()

return context.GetView().Search(searchString, modelSearchResults(context))
context.GetView().Search(searchString, modelSearchResults(context))
}

func modelSearchResults(context types.ISearchableContext) []gocui.SearchPosition {
Expand Down
52 changes: 52 additions & 0 deletions pkg/integration/tests/commit/search.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ var Search = NewIntegrationTest(NewIntegrationTestArgs{
Skip: false,
SetupConfig: func(config *config.AppConfig) {},
SetupRepo: func(shell *Shell) {
// Creating a branch avoids that searching for 't' will unexpectedly match the first commit
// (since it finds it in the extra info line, which is "HEAD -> master")
shell.NewBranch("branch")

shell.EmptyCommit("one")
shell.EmptyCommit("two")
shell.EmptyCommit("three")
Expand Down Expand Up @@ -103,6 +107,54 @@ var Search = NewIntegrationTest(NewIntegrationTestArgs{
Contains("three"),
Contains("two"),
Contains("one").IsSelected(),
).
NavigateToLine(Contains("three")).
Tap(func() {
t.Views().Search().IsVisible().Content(Contains("matches for 'o' (1 of 3)"))
}).
Press("N").
Tap(func() {
t.Views().Search().IsVisible().Content(Contains("matches for 'o' (1 of 3)"))
}).
Lines(
Contains("four").IsSelected(),
Contains("three"),
Contains("two"),
Contains("one"),
).
Press(keys.Universal.StartSearch).
Tap(func() {
t.ExpectSearch().
Type("t").
Confirm()

t.Views().Search().IsVisible().Content(Contains("matches for 't' (1 of 2)"))
}).
Lines(
Contains("four"),
Contains("three").IsSelected(),
Contains("two"),
Contains("one"),
).
SelectPreviousItem().
Tap(func() {
t.Views().Search().IsVisible().Content(Contains("matches for 't' (1 of 2)"))
}).
Lines(
Contains("four").IsSelected(),
Contains("three"),
Contains("two"),
Contains("one"),
).
Press("n").
Tap(func() {
t.Views().Search().IsVisible().Content(Contains("matches for 't' (1 of 2)"))
}).
Lines(
Contains("four"),
Contains("three").IsSelected(),
Contains("two"),
Contains("one"),
)
},
})
Loading