diff --git a/gui.go b/gui.go index 39ba4743..1d4f8559 100644 --- a/gui.go +++ b/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/view.go b/view.go index e5b5c046..33a52afc 100644 --- a/view.go +++ b/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 {