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		if a.rank != b.rank {
42			return a.rank < b.rank
43		}
44		if a.span != b.span {
45			return a.span < b.span
46		}
47		if a.first != b.first {
48			return a.first < b.first
49		}
50		if a.Result.Kind() != b.Result.Kind() {
51			return a.Result.Kind() < b.Result.Kind()
52		}
53		if a.search != b.search {
54			return a.search < b.search
55		}
56		return a.order < b.order
57	})
58	return out
59}
60
61func score(result Result, needle string, needleRunes []rune, order int) (Match, bool) {
62	match := newMatch(result, order)
63
64	identity, label, offset, hasTerms := result.searchTerms()
65	if hasTerms {
66		exactRank := 1
67		if result.Kind() == ResultKindCommand {
68			exactRank = 0
69		}
70		best, matched := scoreField(identity, needle, needleRunes, offset, exactRank, 2, 3)
71		if label != identity {
72			if candidate, ok := scoreField(label, needle, needleRunes, offset, exactRank, 2, 3); ok && (!matched || candidate.betterThan(best)) {
73				best, matched = candidate, true
74			}
75		}
76		if matched {
77			best.apply(&match)
78			return match, true
79		}
80	}
81
82	// Action text remains searchable, but semantic codes and session labels
83	// outrank matches caused only by a rendered action prefix.
84	if fallback, ok := scoreField(match.search, needle, needleRunes, 0, 4, 4, 4); ok {
85		fallback.apply(&match)
86		return match, true
87	}
88	if cmd, ok := result.Command(); ok {
89		if positions, ok := fuzzy.SubsequencePositions([]rune(strings.ToLower(cmd.Desc)), needleRunes); ok {
90			match.rank, match.span, match.first = 5, positions[len(positions)-1]-positions[0]+1, positions[0]
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