Speed up drawing a view with many search matches

If a search has many matches, scrolling the view becomes laggy. For
example, a search for "e" in a long branch log can find 44000 matches.
With that many, drawing a 100x40 view takes 40ms, where it takes 0.14ms
without a search.

For every cell it draws, draw looks through all the matches to see if
the cell is part of one.

The matches are sorted in the same order as draw visits the cells, so
let draw step through them along with the cells. It finds the first
match on screen with a binary search. Drawing that view now takes
0.14ms with or without a search.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Stefan Haller
2026-11-12 11:28:00 +01:00
co-authored by Claude Opus 5.5
parent 0bb4566554
commit e64b209aed
+56 -14
View File
@@ -9,6 +9,7 @@ import (
"fmt"
"io"
"slices"
"sort"
"strings"
"sync"
"unicode"
@@ -429,8 +430,13 @@ func (v *View) viewLineSpanOfBufferLine(bufferLine int) (int, int, bool) {
}
type searcher struct {
searchString string
searchPositions []SearchPosition
searchString string
// The matches, sorted by line, and within a line by the column they start in.
// Since all matches in a line have the same width, they are in the order of the
// column they end in, too.
searchPositions []SearchPosition
modelSearchResults []SearchPosition
currentSearchIndex int
onSelectItem func(*View, int)
@@ -780,6 +786,11 @@ type SearchPosition struct {
Y int
}
// contains returns whether the position covers content column x of view line y.
func (p SearchPosition) contains(x, y int) bool {
return p.Y == y && p.XStart <= x && x < p.XEnd
}
type viewLine struct {
linesX, linesY int // coordinates relative to v.buf.lines
line []cell
@@ -1955,6 +1966,7 @@ func (v *View) draw(isWindowFocused bool) {
emptyCell := cell{chr: " ", width: 1, fgColor: ColorDefault, bgColor: ColorDefault}
gutterWidth := v.inclusionGutterWidth()
searchMatches := v.searcher.matchWalker(v.oy)
for y, vline := range v.viewLines[start:] {
if y >= maxY {
@@ -2035,7 +2047,7 @@ func (v *View) draw(isWindowFocused bool) {
contentX, contentY := x-gutterWidth+v.ox, v.oy+y
v.setCharacter(x, y, c.chr, c.width, fgColor, bgColor, isWindowFocused,
v.searchMatchAt(contentX, contentY), v.isHoveredHyperlink(contentX, contentY))
searchMatches.matchAt(contentX, contentY), v.isHoveredHyperlink(contentX, contentY))
x += c.width
cellIdx++
@@ -2141,18 +2153,48 @@ const (
currentSearchMatch
)
// searchMatchAt returns whether the cell at content column x of view line y is part of
// a search match.
func (v *View) searchMatchAt(x, y int) searchMatch {
for i, pos := range v.searcher.searchPositions {
if y == pos.Y && x >= pos.XStart && x < pos.XEnd {
if i == v.searcher.currentSearchIndex {
return currentSearchMatch
}
return otherSearchMatch
}
// searchMatchWalker tells which cells of the view are part of a search match, for drawing
// them. It has to be asked about the cells in the order they are drawn: line by line, and
// from left to right within a line. The matches are in the same order, so the walker steps
// through them along with the cells.
type searchMatchWalker struct {
positions []SearchPosition
currentIndex int
// The first match that doesn't end before the cell asked about last.
index int
}
// matchWalker returns a searchMatchWalker for drawing the view from view line firstY
// on.
func (s *searcher) matchWalker(firstY int) searchMatchWalker {
return searchMatchWalker{
positions: s.searchPositions,
currentIndex: s.currentSearchIndex,
index: sort.Search(len(s.searchPositions), func(i int) bool {
return s.searchPositions[i].Y >= firstY
}),
}
return noSearchMatch
}
// matchAt returns whether the cell at content column x of view line y is part of a
// search match.
func (w *searchMatchWalker) matchAt(x, y int) searchMatch {
for w.index < len(w.positions) {
pos := w.positions[w.index]
if pos.Y > y || (pos.Y == y && pos.XEnd > x) {
break
}
w.index++
}
if w.index == len(w.positions) || !w.positions[w.index].contains(x, y) {
return noSearchMatch
}
if w.index == w.currentIndex {
return currentSearchMatch
}
return otherSearchMatch
}
// isHoveredHyperlink returns whether the cell at content column x of view line y is