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}