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..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,6 +251,67 @@ func (v *View) gotoPreviousMatch() error { return v.SelectSearchResult(v.searcher.currentSearchIndex) } +func (v *View) gotoNextMatchFromCursor() error { + if len(v.searcher.searchPositions) == 0 { + return nil + } + + positions := v.searcher.searchPositions + currentIdx := v.searcher.currentSearchIndex + currentLine := v.SelectedLineIdx() + + // 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) + } + } + + // 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(nextIndex) +} + +func (v *View) gotoPreviousMatchFromCursor() error { + if len(v.searcher.searchPositions) == 0 { + return nil + } + + positions := v.searcher.searchPositions + currentIdx := v.searcher.currentSearchIndex + currentLine := v.SelectedLineIdx() + + // 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) + } + } + + // 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(prevIndex) +} + func (v *View) SelectSearchResult(index int) error { itemCount := len(v.searcher.searchPositions) if itemCount == 0 {