mirror of
https://github.com/jesseduffield/lazygit.git
synced 2026-06-20 01:19:23 +02:00
Update search position (match x of y) when changing the selection in a list view (#5169)
When searching (not filtering) in a list view using `/`, moving the selection now updates the "x of y" indicator of the search results to the last one before the selection. Pressing `n` again searches from there rather than from the previously shown result. Also see corresponding gocui PR at https://github.com/jesseduffield/gocui/pull/95.
This commit is contained in:
@@ -18,7 +18,7 @@ require (
|
|||||||
github.com/integrii/flaggy v1.4.0
|
github.com/integrii/flaggy v1.4.0
|
||||||
github.com/jesseduffield/generics v0.0.0-20250517122708-b0b4a53a6f5c
|
github.com/jesseduffield/generics v0.0.0-20250517122708-b0b4a53a6f5c
|
||||||
github.com/jesseduffield/go-git/v5 v5.14.1-0.20250407170251-e1a013310ccd
|
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/jesseduffield/lazycore v0.0.0-20221012050358-03d2e40243c5
|
||||||
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0
|
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0
|
||||||
github.com/karimkhaleel/jsonschema v0.0.0-20231001195015-d933f0d94ea3
|
github.com/karimkhaleel/jsonschema v0.0.0-20231001195015-d933f0d94ea3
|
||||||
|
|||||||
@@ -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/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 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/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.20260104174656-7b510338b235 h1:1MjdFm1rUneE1eMYeRkAA3kXswY+h5eLhgJFaZQs9j0=
|
||||||
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/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 h1:CDuQmfOjAtb1Gms6a1p5L2P8RhbLUq5t8aL7PiQd2uY=
|
||||||
github.com/jesseduffield/lazycore v0.0.0-20221012050358-03d2e40243c5/go.mod h1:qxN4mHOAyeIDLP7IK7defgPClM/z1Kze8VVQiaEjzsQ=
|
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=
|
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||||
|
|||||||
@@ -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
|
return ctx
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,10 @@ type ListContextTrait struct {
|
|||||||
// If renderOnlyVisibleLines is true, needRerenderVisibleLines indicates whether we need to
|
// If renderOnlyVisibleLines is true, needRerenderVisibleLines indicates whether we need to
|
||||||
// rerender the visible lines e.g. because the scroll position changed
|
// rerender the visible lines e.g. because the scroll position changed
|
||||||
needRerenderVisibleLines bool
|
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) IsListContext() {}
|
||||||
@@ -31,6 +35,10 @@ func (self *ListContextTrait) IsListContext() {}
|
|||||||
func (self *ListContextTrait) FocusLine(scrollIntoView bool) {
|
func (self *ListContextTrait) FocusLine(scrollIntoView bool) {
|
||||||
self.Context.FocusLine(scrollIntoView)
|
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
|
// 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
|
// 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.
|
// the view could be squashed and won't know how to adjust the cursor/origin.
|
||||||
@@ -40,6 +48,9 @@ func (self *ListContextTrait) FocusLine(scrollIntoView bool) {
|
|||||||
|
|
||||||
self.GetViewTrait().FocusPoint(
|
self.GetViewTrait().FocusPoint(
|
||||||
self.ModelIndexToViewIndex(self.list.GetSelectedLineIdx()), scrollIntoView)
|
self.ModelIndexToViewIndex(self.list.GetSelectedLineIdx()), scrollIntoView)
|
||||||
|
if !inOnSearchSelect {
|
||||||
|
self.GetView().SetNearestSearchPosition()
|
||||||
|
}
|
||||||
|
|
||||||
selectRangeIndex, isSelectingRange := self.list.GetRangeStartIdx()
|
selectRangeIndex, isSelectingRange := self.list.GetRangeStartIdx()
|
||||||
if isSelectingRange {
|
if isSelectingRange {
|
||||||
@@ -117,10 +128,11 @@ func (self *ListContextTrait) HandleRender() {
|
|||||||
self.setFooter()
|
self.setFooter()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (self *ListContextTrait) OnSearchSelect(selectedLineIdx int) error {
|
func (self *ListContextTrait) OnSearchSelect(selectedLineIdx int) {
|
||||||
self.GetList().SetSelection(self.ViewIndexToModelIndex(selectedLineIdx))
|
self.GetList().SetSelection(self.ViewIndexToModelIndex(selectedLineIdx))
|
||||||
|
self.inOnSearchSelect = true
|
||||||
self.HandleFocus(types.OnFocusOpts{})
|
self.HandleFocus(types.OnFocusOpts{})
|
||||||
return nil
|
self.inOnSearchSelect = false
|
||||||
}
|
}
|
||||||
|
|
||||||
func (self *ListContextTrait) IsItemVisible(item types.HasUrn) bool {
|
func (self *ListContextTrait) IsItemVisible(item types.HasUrn) bool {
|
||||||
|
|||||||
@@ -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
|
return ctx
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,7 +31,8 @@ func NewMainContext(
|
|||||||
SearchTrait: NewSearchTrait(c),
|
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
|
return ctx
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,10 @@ type PatchExplorerContext struct {
|
|||||||
getIncludedLineIndices func() []int
|
getIncludedLineIndices func() []int
|
||||||
c *ContextCommon
|
c *ContextCommon
|
||||||
mutex deadlock.Mutex
|
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 (
|
var (
|
||||||
@@ -49,14 +53,14 @@ func NewPatchExplorerContext(
|
|||||||
SearchTrait: NewSearchTrait(c),
|
SearchTrait: NewSearchTrait(c),
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.GetView().SetOnSelectItem(ctx.SearchTrait.onSelectItemWrapper(
|
ctx.GetView().SetRenderSearchStatus(ctx.SearchTrait.RenderSearchStatus)
|
||||||
func(selectedLineIdx int) error {
|
ctx.GetView().SetOnSelectItem(func(selectedLineIdx int) {
|
||||||
ctx.GetMutex().Lock()
|
ctx.GetMutex().Lock()
|
||||||
defer ctx.GetMutex().Unlock()
|
defer ctx.GetMutex().Unlock()
|
||||||
ctx.NavigateTo(selectedLineIdx)
|
ctx.inOnSelectItemCallback = true
|
||||||
return nil
|
ctx.NavigateTo(selectedLineIdx)
|
||||||
}),
|
ctx.inOnSelectItemCallback = false
|
||||||
)
|
})
|
||||||
|
|
||||||
ctx.SetHandleRenderFunc(ctx.OnViewWidthChanged)
|
ctx.SetHandleRenderFunc(ctx.OnViewWidthChanged)
|
||||||
|
|
||||||
@@ -113,6 +117,10 @@ func (self *PatchExplorerContext) FocusSelection() {
|
|||||||
// As far as the view is concerned, we are always selecting a range
|
// As far as the view is concerned, we are always selecting a range
|
||||||
view.SetRangeSelectStart(startIdx)
|
view.SetRangeSelectStart(startIdx)
|
||||||
view.SetCursorY(endIdx - newOriginY)
|
view.SetCursorY(endIdx - newOriginY)
|
||||||
|
|
||||||
|
if !self.inOnSelectItemCallback {
|
||||||
|
view.SetNearestSearchPosition()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (self *PatchExplorerContext) GetContentToRender() string {
|
func (self *PatchExplorerContext) GetContentToRender() string {
|
||||||
|
|||||||
@@ -36,20 +36,6 @@ func (self *SearchTrait) ClearSearchString() {
|
|||||||
// used for type switch
|
// used for type switch
|
||||||
func (self *SearchTrait) IsSearchableContext() {}
|
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) {
|
func (self *SearchTrait) RenderSearchStatus(index int, total int) {
|
||||||
keybindingConfig := self.c.UserConfig().Keybinding
|
keybindingConfig := self.c.UserConfig().Keybinding
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
return ctx
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
return ctx
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -108,20 +108,15 @@ func (self *SearchHelper) Confirm() error {
|
|||||||
return self.CancelPrompt()
|
return self.CancelPrompt()
|
||||||
}
|
}
|
||||||
|
|
||||||
var err error
|
|
||||||
switch state.SearchType() {
|
switch state.SearchType() {
|
||||||
case types.SearchTypeFilter:
|
case types.SearchTypeFilter:
|
||||||
self.ConfirmFilter()
|
self.ConfirmFilter()
|
||||||
case types.SearchTypeSearch:
|
case types.SearchTypeSearch:
|
||||||
err = self.ConfirmSearch()
|
self.ConfirmSearch()
|
||||||
case types.SearchTypeNone:
|
case types.SearchTypeNone:
|
||||||
self.c.Context().Pop()
|
self.c.Context().Pop()
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return self.c.ResetKeybindings()
|
return self.c.ResetKeybindings()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -144,13 +139,13 @@ func (self *SearchHelper) ConfirmFilter() {
|
|||||||
self.c.Context().Pop()
|
self.c.Context().Pop()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (self *SearchHelper) ConfirmSearch() error {
|
func (self *SearchHelper) ConfirmSearch() {
|
||||||
state := self.searchState()
|
state := self.searchState()
|
||||||
|
|
||||||
context, ok := state.Context.(types.ISearchableContext)
|
context, ok := state.Context.(types.ISearchableContext)
|
||||||
if !ok {
|
if !ok {
|
||||||
self.c.Log.Warnf("Context %s is searchable", state.Context.GetKey())
|
self.c.Log.Warnf("Context %s is searchable", state.Context.GetKey())
|
||||||
return nil
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
searchString := self.promptContent()
|
searchString := self.promptContent()
|
||||||
@@ -161,7 +156,7 @@ func (self *SearchHelper) ConfirmSearch() error {
|
|||||||
|
|
||||||
self.c.Context().Pop()
|
self.c.Context().Pop()
|
||||||
|
|
||||||
return context.GetView().Search(searchString, modelSearchResults(context))
|
context.GetView().Search(searchString, modelSearchResults(context))
|
||||||
}
|
}
|
||||||
|
|
||||||
func modelSearchResults(context types.ISearchableContext) []gocui.SearchPosition {
|
func modelSearchResults(context types.ISearchableContext) []gocui.SearchPosition {
|
||||||
|
|||||||
@@ -11,6 +11,10 @@ var Search = NewIntegrationTest(NewIntegrationTestArgs{
|
|||||||
Skip: false,
|
Skip: false,
|
||||||
SetupConfig: func(config *config.AppConfig) {},
|
SetupConfig: func(config *config.AppConfig) {},
|
||||||
SetupRepo: func(shell *Shell) {
|
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("one")
|
||||||
shell.EmptyCommit("two")
|
shell.EmptyCommit("two")
|
||||||
shell.EmptyCommit("three")
|
shell.EmptyCommit("three")
|
||||||
@@ -103,6 +107,54 @@ var Search = NewIntegrationTest(NewIntegrationTestArgs{
|
|||||||
Contains("three"),
|
Contains("three"),
|
||||||
Contains("two"),
|
Contains("two"),
|
||||||
Contains("one").IsSelected(),
|
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"),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
+124
-19
@@ -217,29 +217,53 @@ type searcher struct {
|
|||||||
searchPositions []SearchPosition
|
searchPositions []SearchPosition
|
||||||
modelSearchResults []SearchPosition
|
modelSearchResults []SearchPosition
|
||||||
currentSearchIndex int
|
currentSearchIndex int
|
||||||
onSelectItem func(int, int, int) error
|
onSelectItem func(int)
|
||||||
|
renderSearchStatus func(int, int)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *View) SetOnSelectItem(onSelectItem func(int, int, int) error) {
|
func (v *View) SetRenderSearchStatus(renderSearchStatus func(int, int)) {
|
||||||
|
v.searcher.renderSearchStatus = renderSearchStatus
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *View) SetOnSelectItem(onSelectItem func(int)) {
|
||||||
v.searcher.onSelectItem = onSelectItem
|
v.searcher.onSelectItem = onSelectItem
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (v *View) renderSearchStatus(index int, itemCount int) {
|
||||||
|
if v.searcher.renderSearchStatus != nil {
|
||||||
|
v.searcher.renderSearchStatus(index, itemCount)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (v *View) gotoNextMatch() error {
|
func (v *View) gotoNextMatch() error {
|
||||||
if len(v.searcher.searchPositions) == 0 {
|
if len(v.searcher.searchPositions) == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
if v.Highlight && v.oy+v.cy < v.searcher.searchPositions[v.searcher.currentSearchIndex].Y {
|
||||||
|
// If the selection is before the current match, just jump to the current match and return.
|
||||||
|
// This can only happen if the user has moved the cursor to before the first match.
|
||||||
|
v.SelectSearchResult(v.searcher.currentSearchIndex)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
if v.searcher.currentSearchIndex >= len(v.searcher.searchPositions)-1 {
|
if v.searcher.currentSearchIndex >= len(v.searcher.searchPositions)-1 {
|
||||||
v.searcher.currentSearchIndex = 0
|
v.searcher.currentSearchIndex = 0
|
||||||
} else {
|
} else {
|
||||||
v.searcher.currentSearchIndex++
|
v.searcher.currentSearchIndex++
|
||||||
}
|
}
|
||||||
return v.SelectSearchResult(v.searcher.currentSearchIndex)
|
v.SelectSearchResult(v.searcher.currentSearchIndex)
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *View) gotoPreviousMatch() error {
|
func (v *View) gotoPreviousMatch() error {
|
||||||
if len(v.searcher.searchPositions) == 0 {
|
if len(v.searcher.searchPositions) == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
if v.Highlight && v.oy+v.cy > v.searcher.searchPositions[v.searcher.currentSearchIndex].Y {
|
||||||
|
// If the selection is after the current match, just jump to the current match and return.
|
||||||
|
// This happens if the user has moved the cursor down from the current match.
|
||||||
|
v.SelectSearchResult(v.searcher.currentSearchIndex)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
if v.searcher.currentSearchIndex == 0 {
|
if v.searcher.currentSearchIndex == 0 {
|
||||||
if len(v.searcher.searchPositions) > 0 {
|
if len(v.searcher.searchPositions) > 0 {
|
||||||
v.searcher.currentSearchIndex = len(v.searcher.searchPositions) - 1
|
v.searcher.currentSearchIndex = len(v.searcher.searchPositions) - 1
|
||||||
@@ -247,13 +271,14 @@ func (v *View) gotoPreviousMatch() error {
|
|||||||
} else {
|
} else {
|
||||||
v.searcher.currentSearchIndex--
|
v.searcher.currentSearchIndex--
|
||||||
}
|
}
|
||||||
return v.SelectSearchResult(v.searcher.currentSearchIndex)
|
v.SelectSearchResult(v.searcher.currentSearchIndex)
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *View) SelectSearchResult(index int) error {
|
func (v *View) SelectSearchResult(index int) {
|
||||||
itemCount := len(v.searcher.searchPositions)
|
itemCount := len(v.searcher.searchPositions)
|
||||||
if itemCount == 0 {
|
if itemCount == 0 {
|
||||||
return nil
|
return
|
||||||
}
|
}
|
||||||
if index > itemCount-1 {
|
if index > itemCount-1 {
|
||||||
index = itemCount - 1
|
index = itemCount - 1
|
||||||
@@ -262,10 +287,10 @@ func (v *View) SelectSearchResult(index int) error {
|
|||||||
y := v.searcher.searchPositions[index].Y
|
y := v.searcher.searchPositions[index].Y
|
||||||
|
|
||||||
v.FocusPoint(v.ox, y, true)
|
v.FocusPoint(v.ox, y, true)
|
||||||
|
v.renderSearchStatus(index, itemCount)
|
||||||
if v.searcher.onSelectItem != nil {
|
if v.searcher.onSelectItem != nil {
|
||||||
return v.searcher.onSelectItem(y, index, itemCount)
|
v.searcher.onSelectItem(y)
|
||||||
}
|
}
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns <current match index>, <total matches>
|
// Returns <current match index>, <total matches>
|
||||||
@@ -294,26 +319,29 @@ func (v *View) UpdateSearchResults(str string, modelSearchResults []SearchPositi
|
|||||||
if len(v.searcher.searchPositions) > 0 {
|
if len(v.searcher.searchPositions) > 0 {
|
||||||
// get the first result past the current cursor
|
// get the first result past the current cursor
|
||||||
currentIndex := 0
|
currentIndex := 0
|
||||||
adjustedY := v.oy + v.cy
|
if v.Highlight {
|
||||||
adjustedX := v.ox + v.cx
|
// ...but only if we're showing the highlighted line
|
||||||
for i, pos := range v.searcher.searchPositions {
|
adjustedY := v.oy + v.cy
|
||||||
if pos.Y > adjustedY || (pos.Y == adjustedY && pos.XStart > adjustedX) {
|
adjustedX := v.ox + v.cx
|
||||||
currentIndex = i
|
for i, pos := range v.searcher.searchPositions {
|
||||||
break
|
if pos.Y > adjustedY || (pos.Y == adjustedY && pos.XStart > adjustedX) {
|
||||||
|
currentIndex = i
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
v.searcher.currentSearchIndex = currentIndex
|
v.searcher.currentSearchIndex = currentIndex
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *View) Search(str string, modelSearchResults []SearchPosition) error {
|
func (v *View) Search(str string, modelSearchResults []SearchPosition) {
|
||||||
v.UpdateSearchResults(str, modelSearchResults)
|
v.UpdateSearchResults(str, modelSearchResults)
|
||||||
|
|
||||||
if len(v.searcher.searchPositions) > 0 {
|
if len(v.searcher.searchPositions) > 0 {
|
||||||
return v.SelectSearchResult(v.searcher.currentSearchIndex)
|
v.SelectSearchResult(v.searcher.currentSearchIndex)
|
||||||
|
} else {
|
||||||
|
v.renderSearchStatus(0, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
return v.searcher.onSelectItem(-1, -1, 0)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *View) ClearSearch() {
|
func (v *View) ClearSearch() {
|
||||||
@@ -324,8 +352,37 @@ func (v *View) IsSearching() bool {
|
|||||||
return v.searcher.searchString != ""
|
return v.searcher.searchString != ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (v *View) nearestSearchPosition() int {
|
||||||
|
currentLineIndex := v.cy + v.oy
|
||||||
|
lastSearchPos := 0
|
||||||
|
for i, pos := range v.searcher.searchPositions {
|
||||||
|
if pos.Y == currentLineIndex {
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
if pos.Y > currentLineIndex {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
lastSearchPos = i
|
||||||
|
}
|
||||||
|
return lastSearchPos
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *View) SetNearestSearchPosition() {
|
||||||
|
if len(v.searcher.searchPositions) > 0 {
|
||||||
|
newPos := v.nearestSearchPosition()
|
||||||
|
if newPos != v.searcher.currentSearchIndex {
|
||||||
|
v.searcher.currentSearchIndex = newPos
|
||||||
|
v.renderSearchStatus(newPos, len(v.searcher.searchPositions))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (v *View) FocusPoint(cx int, cy int, scrollIntoView bool) {
|
func (v *View) FocusPoint(cx int, cy int, scrollIntoView bool) {
|
||||||
lineCount := len(v.lines)
|
v.writeMutex.Lock()
|
||||||
|
defer v.writeMutex.Unlock()
|
||||||
|
|
||||||
|
v.refreshViewLinesIfNeeded()
|
||||||
|
lineCount := len(v.viewLines)
|
||||||
if cy < 0 || cy > lineCount {
|
if cy < 0 || cy > lineCount {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -1757,6 +1814,52 @@ func (v *View) setContentLineCount(lineCount int) {
|
|||||||
v.lines = v.lines[:lineCount]
|
v.lines = v.lines[:lineCount]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If the current search result is no longer visible after a scroll up, select the last search
|
||||||
|
// result that is visible in the view, if any, or the first one that is below the view if none is
|
||||||
|
// visible.
|
||||||
|
func (v *View) selectVisibleSearchResultAfterScrollUp() {
|
||||||
|
if !v.Highlight && len(v.searcher.searchPositions) != 0 {
|
||||||
|
windowBottom := v.oy + v.InnerHeight()
|
||||||
|
if v.searcher.searchPositions[v.searcher.currentSearchIndex].Y >= windowBottom {
|
||||||
|
newSearchIndex := v.searcher.currentSearchIndex
|
||||||
|
for newSearchIndex > 0 &&
|
||||||
|
v.searcher.searchPositions[newSearchIndex-1].Y >= v.oy {
|
||||||
|
newSearchIndex--
|
||||||
|
if v.searcher.searchPositions[newSearchIndex].Y < windowBottom {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if v.searcher.currentSearchIndex != newSearchIndex {
|
||||||
|
v.searcher.currentSearchIndex = newSearchIndex
|
||||||
|
v.renderSearchStatus(newSearchIndex, len(v.searcher.searchPositions))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the current search result is no longer visible after a scroll down, select the first search
|
||||||
|
// result that is visible in the view, if any, or the last one that is above the view if none is
|
||||||
|
// visible.
|
||||||
|
func (v *View) selectVisibleSearchResultAfterScrollDown() {
|
||||||
|
if !v.Highlight && len(v.searcher.searchPositions) != 0 {
|
||||||
|
if v.searcher.searchPositions[v.searcher.currentSearchIndex].Y < v.oy {
|
||||||
|
newSearchIndex := v.searcher.currentSearchIndex
|
||||||
|
windowBottom := v.oy + v.InnerHeight()
|
||||||
|
for newSearchIndex+1 < len(v.searcher.searchPositions) &&
|
||||||
|
v.searcher.searchPositions[newSearchIndex+1].Y < windowBottom {
|
||||||
|
newSearchIndex++
|
||||||
|
if v.searcher.searchPositions[newSearchIndex].Y >= v.oy {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if v.searcher.currentSearchIndex != newSearchIndex {
|
||||||
|
v.searcher.currentSearchIndex = newSearchIndex
|
||||||
|
v.renderSearchStatus(newSearchIndex, len(v.searcher.searchPositions))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (v *View) ScrollUp(amount int) {
|
func (v *View) ScrollUp(amount int) {
|
||||||
if amount > v.oy {
|
if amount > v.oy {
|
||||||
amount = v.oy
|
amount = v.oy
|
||||||
@@ -1767,6 +1870,7 @@ func (v *View) ScrollUp(amount int) {
|
|||||||
v.cy += amount
|
v.cy += amount
|
||||||
|
|
||||||
v.clearHover()
|
v.clearHover()
|
||||||
|
v.selectVisibleSearchResultAfterScrollUp()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1778,6 +1882,7 @@ func (v *View) ScrollDown(amount int) {
|
|||||||
v.cy -= adjustedAmount
|
v.cy -= adjustedAmount
|
||||||
|
|
||||||
v.clearHover()
|
v.clearHover()
|
||||||
|
v.selectVisibleSearchResultAfterScrollDown()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Vendored
+1
-1
@@ -211,7 +211,7 @@ github.com/jesseduffield/go-git/v5/utils/merkletrie/internal/frame
|
|||||||
github.com/jesseduffield/go-git/v5/utils/merkletrie/noder
|
github.com/jesseduffield/go-git/v5/utils/merkletrie/noder
|
||||||
github.com/jesseduffield/go-git/v5/utils/sync
|
github.com/jesseduffield/go-git/v5/utils/sync
|
||||||
github.com/jesseduffield/go-git/v5/utils/trace
|
github.com/jesseduffield/go-git/v5/utils/trace
|
||||||
# github.com/jesseduffield/gocui v0.3.1-0.20260103133810-b7e030324985
|
# github.com/jesseduffield/gocui v0.3.1-0.20260104174656-7b510338b235
|
||||||
## explicit; go 1.25
|
## explicit; go 1.25
|
||||||
github.com/jesseduffield/gocui
|
github.com/jesseduffield/gocui
|
||||||
# github.com/jesseduffield/lazycore v0.0.0-20221012050358-03d2e40243c5
|
# github.com/jesseduffield/lazycore v0.0.0-20221012050358-03d2e40243c5
|
||||||
|
|||||||
Reference in New Issue
Block a user