main ece7682

fuzzy.go RawHistoryFolder

1package palette
2
3import (
4	"sort"
5	"strings"
6
7	"github.com/bnema/vev/internal/usecase/fuzzy"
8)
9
10// Match is a fuzzy-matched immutable palette result.
11type Match struct {
12	Result    Result
13	Positions []int
14	rank      int
15	span      int
16	first     int
17	order     int
18	search    string
19}
20
21// Fuzzy searches immutable palette results.
22func Fuzzy(results []Result, query string) []Match {
23	needle := strings.ToLower(query)
24	if query == "" {
25		out := make([]Match, 0, len(results))
26		for i, result := range results {
27			out = append(out, newMatch(result, i))
28		}
29		return out
30	}
31
32	needleRunes := []rune(needle)
33	var out []Match
34	for i, result := range results {
35		if match, ok := score(result, needle, needleRunes, i); ok {
36			out = append(out, match)
37		}
38	}
39	sort.SliceStable(out, func(i, j int) bool {
40		a, b := out[i], out[j]
41		aScore, bScore := fuzzy.Score{Span: a.span, First: a.first}, fuzzy.Score{Span: b.span, First: b.first}
42		if fuzzy.Less(a.rank, aScore, b.rank, bScore) {
43			return true
44		}
45		if fuzzy.Less(b.rank, bScore, a.rank, aScore) {
46			return false
47		}
48		if a.Result.Kind() != b.Result.Kind() {
49			return a.Result.Kind() < b.Result.Kind()
50		}
51		if a.search != b.search {
52			return a.search < b.search
53		}
54		return a.order < b.order
55	})
56	return out
57}
58
59func score(result Result, needle string, needleRunes []rune, order int) (Match, bool) {
60	match := newMatch(result, order)
61
62	identity, label, offset, hasTerms := result.searchTerms()
63	if hasTerms {
64		exactRank := 1
65		if result.Kind() == ResultKindCommand {
66			exactRank = 0
67		}
68		best, matched := scoreField(identity, needle, needleRunes, offset, exactRank, 2, 3)
69		if label != identity {
70			if candidate, ok := scoreField(label, needle, needleRunes, offset, exactRank, 2, 3); ok && (!matched || candidate.betterThan(best)) {
71				best, matched = candidate, true
72			}
73		}
74		if matched {
75			best.apply(&match)
76			return match, true
77		}
78	}
79
80	// Action text remains searchable, but semantic codes and session labels
81	// outrank matches caused only by a rendered action prefix.
82	if fallback, ok := scoreField(match.search, needle, needleRunes, 0, 4, 4, 4); ok {
83		fallback.apply(&match)
84		return match, true
85	}
86	if cmd, ok := result.Command(); ok {
87		// Description matches only rank and never highlight, so Positions
88		// stays empty.
89		if matched, ok := fuzzy.Match(strings.ToLower(cmd.Desc), needle, needleRunes); ok {
90			match.rank, match.span, match.first = 5, matched.Span, matched.First
91			return match, true
92		}
93	}
94	return Match{}, false
95}
96
97type fieldScore struct {
98	positions []int
99	rank      int
100	span      int
101	first     int
102}
103
104func scoreField(text, needle string, needleRunes []rune, displayOffset, exactRank, prefixRank, subsequenceRank int) (fieldScore, bool) {
105	matched, ok := fuzzy.Match(strings.ToLower(text), needle, needleRunes)
106	if !ok {
107		return fieldScore{}, false
108	}
109	scored := fieldScore{positions: matched.Positions, span: matched.Span, first: matched.First}
110	switch matched.Kind {
111	case fuzzy.Exact:
112		scored.rank = exactRank
113	case fuzzy.Prefix:
114		scored.rank = prefixRank
115	default:
116		scored.rank = subsequenceRank
117	}
118	for i := range scored.positions {
119		scored.positions[i] += displayOffset
120	}
121	return scored, true
122}
123
124func (s fieldScore) betterThan(other fieldScore) bool {
125	return fuzzy.Less(s.rank, fuzzy.Score{Span: s.span, First: s.first}, other.rank, fuzzy.Score{Span: other.span, First: other.first})
126}
127
128func (s fieldScore) apply(match *Match) {
129	match.Positions = s.positions
130	match.rank = s.rank
131	match.span = s.span
132	match.first = s.first
133}
134
135func newMatch(result Result, order int) Match {
136	return Match{Result: result, order: order, search: strings.ToLower(result.SearchText())}
137}

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