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 Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· 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 { | |||
| 501 | 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 | 504 | func findMatches(query string, rows textRows) []SearchMatch { | |
| 547 | 505 | needle := lowerRunes(query) | |
| 548 | 506 | 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 | |||
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | 4 | "strconv" | |
| 5 | - | "strings" | |
| 6 | 5 | ||
| 7 | 6 | renderer "github.com/bnema/vev-vt" | |
| 8 | 7 | "github.com/bnema/vev/internal/domain" | |
| @@ -14,10 +13,8 @@ type Model struct { | |||
| 14 | 13 | snapshot scopy.Snapshot | |
| 15 | 14 | input ui.TextInput | |
| 16 | 15 | 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 | |
| 21 | 18 | } | |
| 22 | 19 | ||
| 23 | 20 | func New(snapshot scopy.Snapshot) *Model { | |
| @@ -118,15 +115,7 @@ func (m *Model) Matches() []scopy.SearchMatch { | |||
| 118 | 115 | } | |
| 119 | 116 | ||
| 120 | 117 | 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()) | |
| 130 | 119 | m.clamp() | |
| 131 | 120 | m.ensureVisible(defaultVisibleRows) | |
| 132 | 121 | } | |
| @@ -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.Snapshot | 13 | snapshot scopy.Snapshot | ||
| 15 | input ui.TextInput | 14 | input ui.TextInput | ||
| 16 | matches []scopy.SearchMatch | 15 | matches []scopy.SearchMatch | ||
| 17 | - | // matchedQuery is the trimmed query matches was computed for. | 16 | + | selected int |
| 18 | - | matchedQuery string | 17 | + | 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 | } | ||