detached f0478a2

fix(copy): drop refined search that was slower and panicked on corrupt history

RefineMatches re-copied cells for every prior match, which made short-prefix refinements up to 7x slower than a full RangeText scan, and read rows through a path that panics on corrupt cold history. Visual search now always runs FindMatches.
commit
f0478a2c83c0dd50ca80df9da4db416d1c822f9b
author
brice · · 2026-10-08 05:37 UTC
committer
brice · · 2026-10-08 05:46 UTC
parent
65bd500
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
2 files · +3 -56
M internal/usecase/copy/mode.go +0 -42
@@ -501,48 +501,6 @@ func FindMatches(doc *Document, query string) []SearchMatch {
501501 return findMatches(query, doc.Snapshot().rangeText)
502502 }
503503
504-// RefineMatches returns FindMatches(doc, query) given previous, the complete
505-// result for a query that query extends. Any row matching the longer query also
506-// matched the shorter one, so only rows listed in previous are scanned again.
507-func RefineMatches(doc *Document, previous []SearchMatch, query string) []SearchMatch {
508- query = strings.TrimSpace(query)
509- if doc == nil || query == "" {
510- return nil
511- }
512- snapshot := doc.Snapshot()
513- return findMatches(query, func(yield func(int, []rune, []int, int) bool) error {
514- var cells []renderer.Cell
515- var runes []rune
516- var columns []int
517- last := -1
518- for _, match := range previous {
519- if match.Row == last {
520- continue
521- }
522- last = match.Row
523- width := snapshot.RowWidth(match.Row)
524- cells = slices.Grow(cells[:0], width)[:width]
525- cells = cells[:snapshot.CopyRow(match.Row, cells)]
526- runes, columns = runes[:0], columns[:0]
527- for x, cell := range cells {
528- if cell.Continuation {
529- continue
530- }
531- r := cell.Rune
532- if r == 0 {
533- r = ' '
534- }
535- runes = append(runes, r)
536- columns = append(columns, x)
537- }
538- if !yield(match.Row, runes, columns, len(cells)) {
539- return nil
540- }
541- }
542- return nil
543- })
544-}
545-
546504 func findMatches(query string, rows textRows) []SearchMatch {
547505 needle := lowerRunes(query)
548506 matches := []SearchMatch{}
@@ -501,48 +501,6 @@ func FindMatches(doc *Document, query string) []SearchMatch {
501 return findMatches(query, doc.Snapshot().rangeText)501 return findMatches(query, doc.Snapshot().rangeText)
502 }502 }
503 503
504-// RefineMatches returns FindMatches(doc, query) given previous, the complete
505-// result for a query that query extends. Any row matching the longer query also
506-// matched the shorter one, so only rows listed in previous are scanned again.
507-func RefineMatches(doc *Document, previous []SearchMatch, query string) []SearchMatch {
508- query = strings.TrimSpace(query)
509- if doc == nil || query == "" {
510- return nil
511- }
512- snapshot := doc.Snapshot()
513- return findMatches(query, func(yield func(int, []rune, []int, int) bool) error {
514- var cells []renderer.Cell
515- var runes []rune
516- var columns []int
517- last := -1
518- for _, match := range previous {
519- if match.Row == last {
520- continue
521- }
522- last = match.Row
523- width := snapshot.RowWidth(match.Row)
524- cells = slices.Grow(cells[:0], width)[:width]
525- cells = cells[:snapshot.CopyRow(match.Row, cells)]
526- runes, columns = runes[:0], columns[:0]
527- for x, cell := range cells {
528- if cell.Continuation {
529- continue
530- }
531- r := cell.Rune
532- if r == 0 {
533- r = ' '
534- }
535- runes = append(runes, r)
536- columns = append(columns, x)
537- }
538- if !yield(match.Row, runes, columns, len(cells)) {
539- return nil
540- }
541- }
542- return nil
543- })
544-}
545-
546 func findMatches(query string, rows textRows) []SearchMatch {504 func findMatches(query string, rows textRows) []SearchMatch {
547 needle := lowerRunes(query)505 needle := lowerRunes(query)
548 matches := []SearchMatch{}506 matches := []SearchMatch{}
M internal/usecase/visualsearch/model.go +3 -14
@@ -2,7 +2,6 @@ package visualsearch
22
33 import (
44 "strconv"
5- "strings"
65
76 renderer "github.com/bnema/vev-vt"
87 "github.com/bnema/vev/internal/domain"
@@ -14,10 +13,8 @@ type Model struct {
1413 snapshot scopy.Snapshot
1514 input ui.TextInput
1615 matches []scopy.SearchMatch
17- // matchedQuery is the trimmed query matches was computed for.
18- matchedQuery string
19- selected int
20- scroll int
16+ selected int
17+ scroll int
2118 }
2219
2320 func New(snapshot scopy.Snapshot) *Model {
@@ -118,15 +115,7 @@ func (m *Model) Matches() []scopy.SearchMatch {
118115 }
119116
120117 func (m *Model) refresh() {
121- doc := scopy.NewDocument(m.snapshot, "")
122- query := strings.TrimSpace(m.input.Value())
123- // Typing extends the query, so only rows that already matched can match.
124- if m.matchedQuery != "" && strings.HasPrefix(query, m.matchedQuery) {
125- m.matches = scopy.RefineMatches(doc, m.matches, query)
126- } else {
127- m.matches = scopy.FindMatches(doc, query)
128- }
129- m.matchedQuery = query
118+ m.matches = scopy.FindMatches(scopy.NewDocument(m.snapshot, ""), m.input.Value())
130119 m.clamp()
131120 m.ensureVisible(defaultVisibleRows)
132121 }
@@ -2,7 +2,6 @@ package visualsearch
2 2
3 import (3 import (
4 "strconv"4 "strconv"
5- "strings"
6 5
7 renderer "github.com/bnema/vev-vt"6 renderer "github.com/bnema/vev-vt"
8 "github.com/bnema/vev/internal/domain"7 "github.com/bnema/vev/internal/domain"
@@ -14,10 +13,8 @@ type Model struct {
14 snapshot scopy.Snapshot13 snapshot scopy.Snapshot
15 input ui.TextInput14 input ui.TextInput
16 matches []scopy.SearchMatch15 matches []scopy.SearchMatch
17- // matchedQuery is the trimmed query matches was computed for.16+ selected int
18- matchedQuery string17+ scroll int
19- selected int
20- scroll int
21 }18 }
22 19
23 func New(snapshot scopy.Snapshot) *Model {20 func New(snapshot scopy.Snapshot) *Model {
@@ -118,15 +115,7 @@ func (m *Model) Matches() []scopy.SearchMatch {
118 }115 }
119 116
120 func (m *Model) refresh() {117 func (m *Model) refresh() {
121- doc := scopy.NewDocument(m.snapshot, "")118+ m.matches = scopy.FindMatches(scopy.NewDocument(m.snapshot, ""), m.input.Value())
122- query := strings.TrimSpace(m.input.Value())
123- // Typing extends the query, so only rows that already matched can match.
124- if m.matchedQuery != "" && strings.HasPrefix(query, m.matchedQuery) {
125- m.matches = scopy.RefineMatches(doc, m.matches, query)
126- } else {
127- m.matches = scopy.FindMatches(doc, query)
128- }
129- m.matchedQuery = query
130 m.clamp()119 m.clamp()
131 m.ensureVisible(defaultVisibleRows)120 m.ensureVisible(defaultVisibleRows)
132 }121 }

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 12

Tags 11

↑↓ moveenter openesc close