From b7bcdace902df9838e3905183c79219966a4193b Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Sun, 8 Dec 2024 13:10:25 +0100 Subject: [PATCH 1/6] Fix FocusPoint when Wrap is true --- view.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/view.go b/view.go index e5b5c046..1e78f44a 100644 --- a/view.go +++ b/view.go @@ -325,7 +325,11 @@ func (v *View) IsSearching() 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 { return } From b0f54f1e56b613f8379360cfac6aa586f84b2397 Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Sun, 4 Jan 2026 11:15:35 +0100 Subject: [PATCH 2/6] Fix highlighting the first search result for views that don't have a selection For views that don't show the highlighted line it doesn't make sense to start searching from the cursor position, so select the first match in that case. --- view.go | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/view.go b/view.go index 1e78f44a..88b24532 100644 --- a/view.go +++ b/view.go @@ -294,12 +294,15 @@ func (v *View) UpdateSearchResults(str string, modelSearchResults []SearchPositi if len(v.searcher.searchPositions) > 0 { // get the first result past the current cursor currentIndex := 0 - adjustedY := v.oy + v.cy - adjustedX := v.ox + v.cx - for i, pos := range v.searcher.searchPositions { - if pos.Y > adjustedY || (pos.Y == adjustedY && pos.XStart > adjustedX) { - currentIndex = i - break + if v.Highlight { + // ...but only if we're showing the highlighted line + adjustedY := v.oy + v.cy + adjustedX := v.ox + v.cx + for i, pos := range v.searcher.searchPositions { + if pos.Y > adjustedY || (pos.Y == adjustedY && pos.XStart > adjustedX) { + currentIndex = i + break + } } } v.searcher.currentSearchIndex = currentIndex From 06933e46fd9bd76d3730d14b51b20b729618252c Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Sat, 3 Jan 2026 20:44:32 +0100 Subject: [PATCH 3/6] Split searcher.onSelectItem in two functions One for setting the selection, and one for rendering the "match x of y" string; we want to use them separately in the next commit. --- view.go | 37 +++++++++++++++++++++++++------------ 1 file changed, 25 insertions(+), 12 deletions(-) diff --git a/view.go b/view.go index 88b24532..97fead38 100644 --- a/view.go +++ b/view.go @@ -217,13 +217,24 @@ type searcher struct { searchPositions []SearchPosition modelSearchResults []SearchPosition 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 } +func (v *View) renderSearchStatus(index int, itemCount int) { + if v.searcher.renderSearchStatus != nil { + v.searcher.renderSearchStatus(index, itemCount) + } +} + func (v *View) gotoNextMatch() error { if len(v.searcher.searchPositions) == 0 { return nil @@ -233,7 +244,8 @@ func (v *View) gotoNextMatch() error { } else { v.searcher.currentSearchIndex++ } - return v.SelectSearchResult(v.searcher.currentSearchIndex) + v.SelectSearchResult(v.searcher.currentSearchIndex) + return nil } func (v *View) gotoPreviousMatch() error { @@ -247,13 +259,14 @@ func (v *View) gotoPreviousMatch() error { } else { 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) if itemCount == 0 { - return nil + return } if index > itemCount-1 { index = itemCount - 1 @@ -262,10 +275,10 @@ func (v *View) SelectSearchResult(index int) error { y := v.searcher.searchPositions[index].Y v.FocusPoint(v.ox, y, true) + v.renderSearchStatus(index, itemCount) if v.searcher.onSelectItem != nil { - return v.searcher.onSelectItem(y, index, itemCount) + v.searcher.onSelectItem(y) } - return nil } // Returns , @@ -309,14 +322,14 @@ func (v *View) UpdateSearchResults(str string, modelSearchResults []SearchPositi } } -func (v *View) Search(str string, modelSearchResults []SearchPosition) error { +func (v *View) Search(str string, modelSearchResults []SearchPosition) { v.UpdateSearchResults(str, modelSearchResults) 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() { From ab6cc87fc09318a6daf102bd5833f7791ca1f95e Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Sat, 3 Jan 2026 20:44:06 +0100 Subject: [PATCH 4/6] Add method SetNearestSearchPosition Can be used after changing the selection in a view, to make the search result follow. --- view.go | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/view.go b/view.go index 97fead38..8437e47e 100644 --- a/view.go +++ b/view.go @@ -340,6 +340,31 @@ func (v *View) IsSearching() bool { 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) { v.writeMutex.Lock() defer v.writeMutex.Unlock() From df333b24564df56fa2f2432c92714c7d36adf2c4 Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Sun, 4 Jan 2026 11:25:49 +0100 Subject: [PATCH 5/6] Improve jumping to prev/next match if selection has moved This improves the case where the user has moved the selection to before the first match and presses 'n', or to after the current match and presses 'N'. In both cases we now simply jump to the current match again. --- view.go | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/view.go b/view.go index 8437e47e..8782b19e 100644 --- a/view.go +++ b/view.go @@ -239,6 +239,12 @@ func (v *View) gotoNextMatch() error { if len(v.searcher.searchPositions) == 0 { 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 { v.searcher.currentSearchIndex = 0 } else { @@ -252,6 +258,12 @@ func (v *View) gotoPreviousMatch() error { if len(v.searcher.searchPositions) == 0 { 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 len(v.searcher.searchPositions) > 0 { v.searcher.currentSearchIndex = len(v.searcher.searchPositions) - 1 From e006983969b0cc8616aac0257eda255864bf5a22 Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Sun, 4 Jan 2026 12:11:31 +0100 Subject: [PATCH 6/6] Update current search result when scrolling a view that doesn't have a selection --- view.go | 48 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/view.go b/view.go index 8782b19e..ead9aa98 100644 --- a/view.go +++ b/view.go @@ -1814,6 +1814,52 @@ func (v *View) setContentLineCount(lineCount int) { 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) { if amount > v.oy { amount = v.oy @@ -1824,6 +1870,7 @@ func (v *View) ScrollUp(amount int) { v.cy += amount v.clearHover() + v.selectVisibleSearchResultAfterScrollUp() } } @@ -1835,6 +1882,7 @@ func (v *View) ScrollDown(amount int) { v.cy -= adjustedAmount v.clearHover() + v.selectVisibleSearchResultAfterScrollDown() } }