From 8018ba45f424b892fa6ea091ffe8d1046091c0ea Mon Sep 17 00:00:00 2001 From: phanium <91544758+phanen@users.noreply.github.com> Date: Tue, 30 Dec 2025 20:43:11 +0800 Subject: [PATCH 1/2] feat: nextMatchFromCursor, prevMatchFromCursor --- docs-master/Config.md | 2 + pkg/config/user_config.go | 2 + pkg/gui/gui.go | 2 + schema-master/config.json | 8 +++ vendor/github.com/jesseduffield/gocui/gui.go | 14 +++-- vendor/github.com/jesseduffield/gocui/view.go | 54 +++++++++++++++++++ 6 files changed, 79 insertions(+), 3 deletions(-) diff --git a/docs-master/Config.md b/docs-master/Config.md index 21709da755a..314514f08fe 100644 --- a/docs-master/Config.md +++ b/docs-master/Config.md @@ -612,6 +612,8 @@ keybinding: focusMainView: "0" nextMatch: "n" prevMatch: "N" + nextMatchFromCursor: "" + prevMatchFromCursor: "" startSearch: / optionMenu: optionMenu-alt1: '?' diff --git a/pkg/config/user_config.go b/pkg/config/user_config.go index bcb5c2f6f85..75a6edfe20f 100644 --- a/pkg/config/user_config.go +++ b/pkg/config/user_config.go @@ -444,6 +444,8 @@ type KeybindingUniversalConfig struct { FocusMainView string `yaml:"focusMainView"` NextMatch string `yaml:"nextMatch"` PrevMatch string `yaml:"prevMatch"` + NextSearchMatchFromCursor string `yaml:"nextMatchFromCursor"` + PrevSearchMatchFromCursor string `yaml:"prevMatchFromCursor"` StartSearch string `yaml:"startSearch"` OptionMenu string `yaml:"optionMenu"` OptionMenuAlt1 string `yaml:"optionMenu-alt1"` diff --git a/pkg/gui/gui.go b/pkg/gui/gui.go index a7e17ef9f47..68634d3f1ae 100644 --- a/pkg/gui/gui.go +++ b/pkg/gui/gui.go @@ -456,6 +456,8 @@ func (gui *Gui) onUserConfigLoaded() error { gui.g.SearchEscapeKey = keybindings.GetKey(userConfig.Keybinding.Universal.Return) gui.g.NextSearchMatchKey = keybindings.GetKey(userConfig.Keybinding.Universal.NextMatch) gui.g.PrevSearchMatchKey = keybindings.GetKey(userConfig.Keybinding.Universal.PrevMatch) + gui.g.NextSearchMatchFromCursorKey = keybindings.GetKey(userConfig.Keybinding.Universal.NextSearchMatchFromCursor) + gui.g.PrevSearchMatchFromCursorKey = keybindings.GetKey(userConfig.Keybinding.Universal.PrevSearchMatchFromCursor) gui.g.ShowListFooter = userConfig.Gui.ShowListFooter diff --git a/schema-master/config.json b/schema-master/config.json index c371f14e9f7..cbee5fd586c 100644 --- a/schema-master/config.json +++ b/schema-master/config.json @@ -1358,6 +1358,14 @@ "type": "string", "default": "N" }, + "nextMatchFromCursor": { + "type": "string", + "default": "n" + }, + "prevMatchFromCursor": { + "type": "string", + "default": "p" + }, "startSearch": { "type": "string", "default": "/" diff --git a/vendor/github.com/jesseduffield/gocui/gui.go b/vendor/github.com/jesseduffield/gocui/gui.go index 39ba4743947..1d4f855947b 100644 --- a/vendor/github.com/jesseduffield/gocui/gui.go +++ b/vendor/github.com/jesseduffield/gocui/gui.go @@ -190,9 +190,11 @@ type Gui struct { OnSearchEscape func() error // these keys must either be of type Key of rune - SearchEscapeKey any - NextSearchMatchKey any - PrevSearchMatchKey any + SearchEscapeKey any + NextSearchMatchKey any + PrevSearchMatchKey any + NextSearchMatchFromCursorKey any + PrevSearchMatchFromCursorKey any ErrorHandler func(error) error @@ -269,6 +271,8 @@ func NewGui(opts NewGuiOpts) (*Gui, error) { g.SearchEscapeKey = KeyEsc g.NextSearchMatchKey = 'n' g.PrevSearchMatchKey = 'N' + g.NextSearchMatchFromCursorKey = "" + g.PrevSearchMatchFromCursorKey = "" g.playRecording = opts.PlayRecording @@ -1521,6 +1525,10 @@ func (g *Gui) execKeybindings(v *View, ev *GocuiEvent) error { return v.gotoNextMatch() } else if eventMatchesKey(ev, g.PrevSearchMatchKey) { return v.gotoPreviousMatch() + } else if eventMatchesKey(ev, g.NextSearchMatchFromCursorKey) { + return v.gotoNextMatchFromCursor() + } else if eventMatchesKey(ev, g.PrevSearchMatchFromCursorKey) { + return v.gotoPreviousMatchFromCursor() } else if eventMatchesKey(ev, g.SearchEscapeKey) { v.searcher.clearSearch() if g.OnSearchEscape != nil { diff --git a/vendor/github.com/jesseduffield/gocui/view.go b/vendor/github.com/jesseduffield/gocui/view.go index e5b5c046646..a4ab787c42a 100644 --- a/vendor/github.com/jesseduffield/gocui/view.go +++ b/vendor/github.com/jesseduffield/gocui/view.go @@ -250,6 +250,60 @@ func (v *View) gotoPreviousMatch() error { return v.SelectSearchResult(v.searcher.currentSearchIndex) } +// gotoNextMatchFromCursor finds the next match after the current cursor position +func (v *View) gotoNextMatchFromCursor() error { + if len(v.searcher.searchPositions) == 0 { + return nil + } + + // Get current line position + currentLine := v.SelectedLineIdx() + + // Find the next match after the current line + nextIndex := -1 + for i, pos := range v.searcher.searchPositions { + if pos.Y > currentLine { + nextIndex = i + break + } + } + + // If no match found after current line, wrap around to first match + if nextIndex == -1 { + nextIndex = 0 + } + + v.searcher.currentSearchIndex = nextIndex + return v.SelectSearchResult(v.searcher.currentSearchIndex) +} + +// gotoPreviousMatchFromCursor finds the previous match before the current cursor position +func (v *View) gotoPreviousMatchFromCursor() error { + if len(v.searcher.searchPositions) == 0 { + return nil + } + + // Get current line position + currentLine := v.SelectedLineIdx() + + // Find the previous match before the current line (search backwards) + prevIndex := -1 + for i := len(v.searcher.searchPositions) - 1; i >= 0; i-- { + if v.searcher.searchPositions[i].Y < currentLine { + prevIndex = i + break + } + } + + // If no match found before current line, wrap around to last match + if prevIndex == -1 { + prevIndex = len(v.searcher.searchPositions) - 1 + } + + v.searcher.currentSearchIndex = prevIndex + return v.SelectSearchResult(v.searcher.currentSearchIndex) +} + func (v *View) SelectSearchResult(index int) error { itemCount := len(v.searcher.searchPositions) if itemCount == 0 { From 8c528b9eeb38e9ce30555e693c277098fbd8fccb Mon Sep 17 00:00:00 2001 From: phanium <91544758+phanen@users.noreply.github.com> Date: Fri, 2 Jan 2026 10:37:40 +0800 Subject: [PATCH 2/2] fixup! feat: nextMatchFromCursor, prevSearchMatchFromCursor --- vendor/github.com/jesseduffield/gocui/view.go | 54 +++++++++++-------- 1 file changed, 31 insertions(+), 23 deletions(-) diff --git a/vendor/github.com/jesseduffield/gocui/view.go b/vendor/github.com/jesseduffield/gocui/view.go index a4ab787c42a..33a52afc67f 100644 --- a/vendor/github.com/jesseduffield/gocui/view.go +++ b/vendor/github.com/jesseduffield/gocui/view.go @@ -7,6 +7,7 @@ package gocui import ( "fmt" "io" + "sort" "strings" "sync" "unicode" @@ -250,58 +251,65 @@ func (v *View) gotoPreviousMatch() error { return v.SelectSearchResult(v.searcher.currentSearchIndex) } -// gotoNextMatchFromCursor finds the next match after the current cursor position func (v *View) gotoNextMatchFromCursor() error { if len(v.searcher.searchPositions) == 0 { return nil } - // Get current line position + positions := v.searcher.searchPositions + currentIdx := v.searcher.currentSearchIndex currentLine := v.SelectedLineIdx() - // Find the next match after the current line - nextIndex := -1 - for i, pos := range v.searcher.searchPositions { - if pos.Y > currentLine { - nextIndex = i - break + // If current match is on same line, check next position + if currentIdx >= 0 && currentIdx < len(positions) && positions[currentIdx].Y == currentLine { + if currentIdx+1 < len(positions) && positions[currentIdx+1].Y == currentLine { + v.searcher.currentSearchIndex = currentIdx + 1 + return v.SelectSearchResult(currentIdx + 1) } } - // If no match found after current line, wrap around to first match - if nextIndex == -1 { + // Find first match after current line + nextIndex := sort.Search(len(positions), func(i int) bool { + return positions[i].Y > currentLine + }) + + if nextIndex >= len(positions) { nextIndex = 0 } v.searcher.currentSearchIndex = nextIndex - return v.SelectSearchResult(v.searcher.currentSearchIndex) + return v.SelectSearchResult(nextIndex) } -// gotoPreviousMatchFromCursor finds the previous match before the current cursor position func (v *View) gotoPreviousMatchFromCursor() error { if len(v.searcher.searchPositions) == 0 { return nil } - // Get current line position + positions := v.searcher.searchPositions + currentIdx := v.searcher.currentSearchIndex currentLine := v.SelectedLineIdx() - // Find the previous match before the current line (search backwards) - prevIndex := -1 - for i := len(v.searcher.searchPositions) - 1; i >= 0; i-- { - if v.searcher.searchPositions[i].Y < currentLine { - prevIndex = i - break + // If current match is on same line, check previous position + if currentIdx >= 0 && currentIdx < len(positions) && positions[currentIdx].Y == currentLine { + if currentIdx-1 >= 0 && positions[currentIdx-1].Y == currentLine { + v.searcher.currentSearchIndex = currentIdx - 1 + return v.SelectSearchResult(currentIdx - 1) } } - // If no match found before current line, wrap around to last match - if prevIndex == -1 { - prevIndex = len(v.searcher.searchPositions) - 1 + // Find first match on or after current line, then go back one + idx := sort.Search(len(positions), func(i int) bool { + return positions[i].Y >= currentLine + }) + + prevIndex := idx - 1 + if prevIndex < 0 { + prevIndex = len(positions) - 1 } v.searcher.currentSearchIndex = prevIndex - return v.SelectSearchResult(v.searcher.currentSearchIndex) + return v.SelectSearchResult(prevIndex) } func (v *View) SelectSearchResult(index int) error {