doing 2.0.20 → 2.0.24
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +5 -0
- data/Gemfile.lock +1 -1
- data/README.md +1 -1
- data/doing.rdoc +1 -1
- data/lib/doing/log_adapter.rb +3 -1
- data/lib/doing/version.rb +1 -1
- data/lib/doing/wwid.rb +19 -8
- data/lib/helpers/fzf/.goreleaser.yml +119 -0
- data/lib/helpers/fzf/.rubocop.yml +28 -0
- data/lib/helpers/fzf/ADVANCED.md +565 -0
- data/lib/helpers/fzf/BUILD.md +49 -0
- data/lib/helpers/fzf/CHANGELOG.md +1193 -0
- data/lib/helpers/fzf/Dockerfile +11 -0
- data/lib/helpers/fzf/LICENSE +21 -0
- data/lib/helpers/fzf/Makefile +166 -0
- data/lib/helpers/fzf/README-VIM.md +486 -0
- data/lib/helpers/fzf/README.md +712 -0
- data/lib/helpers/fzf/bin/fzf-tmux +233 -0
- data/lib/helpers/fzf/doc/fzf.txt +512 -0
- data/lib/helpers/fzf/go.mod +17 -0
- data/lib/helpers/fzf/go.sum +31 -0
- data/lib/helpers/fzf/install +382 -0
- data/lib/helpers/fzf/install.ps1 +65 -0
- data/lib/helpers/fzf/main.go +14 -0
- data/lib/helpers/fzf/man/man1/fzf-tmux.1 +68 -0
- data/lib/helpers/fzf/man/man1/fzf.1 +1001 -0
- data/lib/helpers/fzf/plugin/fzf.vim +1048 -0
- data/lib/helpers/fzf/shell/completion.bash +381 -0
- data/lib/helpers/fzf/shell/completion.zsh +329 -0
- data/lib/helpers/fzf/shell/key-bindings.bash +96 -0
- data/lib/helpers/fzf/shell/key-bindings.fish +172 -0
- data/lib/helpers/fzf/shell/key-bindings.zsh +114 -0
- data/lib/helpers/fzf/src/LICENSE +21 -0
- data/lib/helpers/fzf/src/algo/algo.go +884 -0
- data/lib/helpers/fzf/src/algo/algo_test.go +197 -0
- data/lib/helpers/fzf/src/algo/normalize.go +492 -0
- data/lib/helpers/fzf/src/ansi.go +409 -0
- data/lib/helpers/fzf/src/ansi_test.go +427 -0
- data/lib/helpers/fzf/src/cache.go +81 -0
- data/lib/helpers/fzf/src/cache_test.go +39 -0
- data/lib/helpers/fzf/src/chunklist.go +89 -0
- data/lib/helpers/fzf/src/chunklist_test.go +80 -0
- data/lib/helpers/fzf/src/constants.go +85 -0
- data/lib/helpers/fzf/src/core.go +351 -0
- data/lib/helpers/fzf/src/history.go +96 -0
- data/lib/helpers/fzf/src/history_test.go +68 -0
- data/lib/helpers/fzf/src/item.go +44 -0
- data/lib/helpers/fzf/src/item_test.go +23 -0
- data/lib/helpers/fzf/src/matcher.go +235 -0
- data/lib/helpers/fzf/src/merger.go +120 -0
- data/lib/helpers/fzf/src/merger_test.go +88 -0
- data/lib/helpers/fzf/src/options.go +1691 -0
- data/lib/helpers/fzf/src/options_test.go +457 -0
- data/lib/helpers/fzf/src/pattern.go +425 -0
- data/lib/helpers/fzf/src/pattern_test.go +209 -0
- data/lib/helpers/fzf/src/protector/protector.go +8 -0
- data/lib/helpers/fzf/src/protector/protector_openbsd.go +10 -0
- data/lib/helpers/fzf/src/reader.go +201 -0
- data/lib/helpers/fzf/src/reader_test.go +63 -0
- data/lib/helpers/fzf/src/result.go +243 -0
- data/lib/helpers/fzf/src/result_others.go +16 -0
- data/lib/helpers/fzf/src/result_test.go +159 -0
- data/lib/helpers/fzf/src/result_x86.go +16 -0
- data/lib/helpers/fzf/src/terminal.go +2832 -0
- data/lib/helpers/fzf/src/terminal_test.go +638 -0
- data/lib/helpers/fzf/src/terminal_unix.go +26 -0
- data/lib/helpers/fzf/src/terminal_windows.go +45 -0
- data/lib/helpers/fzf/src/tokenizer.go +253 -0
- data/lib/helpers/fzf/src/tokenizer_test.go +112 -0
- data/lib/helpers/fzf/src/tui/dummy.go +46 -0
- data/lib/helpers/fzf/src/tui/light.go +987 -0
- data/lib/helpers/fzf/src/tui/light_unix.go +110 -0
- data/lib/helpers/fzf/src/tui/light_windows.go +145 -0
- data/lib/helpers/fzf/src/tui/tcell.go +721 -0
- data/lib/helpers/fzf/src/tui/tcell_test.go +392 -0
- data/lib/helpers/fzf/src/tui/ttyname_unix.go +47 -0
- data/lib/helpers/fzf/src/tui/ttyname_windows.go +14 -0
- data/lib/helpers/fzf/src/tui/tui.go +625 -0
- data/lib/helpers/fzf/src/tui/tui_test.go +20 -0
- data/lib/helpers/fzf/src/util/atomicbool.go +34 -0
- data/lib/helpers/fzf/src/util/atomicbool_test.go +17 -0
- data/lib/helpers/fzf/src/util/chars.go +198 -0
- data/lib/helpers/fzf/src/util/chars_test.go +46 -0
- data/lib/helpers/fzf/src/util/eventbox.go +96 -0
- data/lib/helpers/fzf/src/util/eventbox_test.go +61 -0
- data/lib/helpers/fzf/src/util/slab.go +12 -0
- data/lib/helpers/fzf/src/util/util.go +138 -0
- data/lib/helpers/fzf/src/util/util_test.go +40 -0
- data/lib/helpers/fzf/src/util/util_unix.go +47 -0
- data/lib/helpers/fzf/src/util/util_windows.go +83 -0
- data/lib/helpers/fzf/test/fzf.vader +175 -0
- data/lib/helpers/fzf/test/test_go.rb +2626 -0
- data/lib/helpers/fzf/uninstall +117 -0
- metadata +88 -2
@@ -0,0 +1,884 @@
|
|
1
|
+
package algo
|
2
|
+
|
3
|
+
/*
|
4
|
+
|
5
|
+
Algorithm
|
6
|
+
---------
|
7
|
+
|
8
|
+
FuzzyMatchV1 finds the first "fuzzy" occurrence of the pattern within the given
|
9
|
+
text in O(n) time where n is the length of the text. Once the position of the
|
10
|
+
last character is located, it traverses backwards to see if there's a shorter
|
11
|
+
substring that matches the pattern.
|
12
|
+
|
13
|
+
a_____b___abc__ To find "abc"
|
14
|
+
*-----*-----*> 1. Forward scan
|
15
|
+
<*** 2. Backward scan
|
16
|
+
|
17
|
+
The algorithm is simple and fast, but as it only sees the first occurrence,
|
18
|
+
it is not guaranteed to find the occurrence with the highest score.
|
19
|
+
|
20
|
+
a_____b__c__abc
|
21
|
+
*-----*--* ***
|
22
|
+
|
23
|
+
FuzzyMatchV2 implements a modified version of Smith-Waterman algorithm to find
|
24
|
+
the optimal solution (highest score) according to the scoring criteria. Unlike
|
25
|
+
the original algorithm, omission or mismatch of a character in the pattern is
|
26
|
+
not allowed.
|
27
|
+
|
28
|
+
Performance
|
29
|
+
-----------
|
30
|
+
|
31
|
+
The new V2 algorithm is slower than V1 as it examines all occurrences of the
|
32
|
+
pattern instead of stopping immediately after finding the first one. The time
|
33
|
+
complexity of the algorithm is O(nm) if a match is found and O(n) otherwise
|
34
|
+
where n is the length of the item and m is the length of the pattern. Thus, the
|
35
|
+
performance overhead may not be noticeable for a query with high selectivity.
|
36
|
+
However, if the performance is more important than the quality of the result,
|
37
|
+
you can still choose v1 algorithm with --algo=v1.
|
38
|
+
|
39
|
+
Scoring criteria
|
40
|
+
----------------
|
41
|
+
|
42
|
+
- We prefer matches at special positions, such as the start of a word, or
|
43
|
+
uppercase character in camelCase words.
|
44
|
+
|
45
|
+
- That is, we prefer an occurrence of the pattern with more characters
|
46
|
+
matching at special positions, even if the total match length is longer.
|
47
|
+
e.g. "fuzzyfinder" vs. "fuzzy-finder" on "ff"
|
48
|
+
````````````
|
49
|
+
- Also, if the first character in the pattern appears at one of the special
|
50
|
+
positions, the bonus point for the position is multiplied by a constant
|
51
|
+
as it is extremely likely that the first character in the typed pattern
|
52
|
+
has more significance than the rest.
|
53
|
+
e.g. "fo-bar" vs. "foob-r" on "br"
|
54
|
+
``````
|
55
|
+
- But since fzf is still a fuzzy finder, not an acronym finder, we should also
|
56
|
+
consider the total length of the matched substring. This is why we have the
|
57
|
+
gap penalty. The gap penalty increases as the length of the gap (distance
|
58
|
+
between the matching characters) increases, so the effect of the bonus is
|
59
|
+
eventually cancelled at some point.
|
60
|
+
e.g. "fuzzyfinder" vs. "fuzzy-blurry-finder" on "ff"
|
61
|
+
```````````
|
62
|
+
- Consequently, it is crucial to find the right balance between the bonus
|
63
|
+
and the gap penalty. The parameters were chosen that the bonus is cancelled
|
64
|
+
when the gap size increases beyond 8 characters.
|
65
|
+
|
66
|
+
- The bonus mechanism can have the undesirable side effect where consecutive
|
67
|
+
matches are ranked lower than the ones with gaps.
|
68
|
+
e.g. "foobar" vs. "foo-bar" on "foob"
|
69
|
+
```````
|
70
|
+
- To correct this anomaly, we also give extra bonus point to each character
|
71
|
+
in a consecutive matching chunk.
|
72
|
+
e.g. "foobar" vs. "foo-bar" on "foob"
|
73
|
+
``````
|
74
|
+
- The amount of consecutive bonus is primarily determined by the bonus of the
|
75
|
+
first character in the chunk.
|
76
|
+
e.g. "foobar" vs. "out-of-bound" on "oob"
|
77
|
+
````````````
|
78
|
+
*/
|
79
|
+
|
80
|
+
import (
|
81
|
+
"bytes"
|
82
|
+
"fmt"
|
83
|
+
"strings"
|
84
|
+
"unicode"
|
85
|
+
"unicode/utf8"
|
86
|
+
|
87
|
+
"github.com/junegunn/fzf/src/util"
|
88
|
+
)
|
89
|
+
|
90
|
+
var DEBUG bool
|
91
|
+
|
92
|
+
func indexAt(index int, max int, forward bool) int {
|
93
|
+
if forward {
|
94
|
+
return index
|
95
|
+
}
|
96
|
+
return max - index - 1
|
97
|
+
}
|
98
|
+
|
99
|
+
// Result contains the results of running a match function.
|
100
|
+
type Result struct {
|
101
|
+
// TODO int32 should suffice
|
102
|
+
Start int
|
103
|
+
End int
|
104
|
+
Score int
|
105
|
+
}
|
106
|
+
|
107
|
+
const (
|
108
|
+
scoreMatch = 16
|
109
|
+
scoreGapStart = -3
|
110
|
+
scoreGapExtension = -1
|
111
|
+
|
112
|
+
// We prefer matches at the beginning of a word, but the bonus should not be
|
113
|
+
// too great to prevent the longer acronym matches from always winning over
|
114
|
+
// shorter fuzzy matches. The bonus point here was specifically chosen that
|
115
|
+
// the bonus is cancelled when the gap between the acronyms grows over
|
116
|
+
// 8 characters, which is approximately the average length of the words found
|
117
|
+
// in web2 dictionary and my file system.
|
118
|
+
bonusBoundary = scoreMatch / 2
|
119
|
+
|
120
|
+
// Although bonus point for non-word characters is non-contextual, we need it
|
121
|
+
// for computing bonus points for consecutive chunks starting with a non-word
|
122
|
+
// character.
|
123
|
+
bonusNonWord = scoreMatch / 2
|
124
|
+
|
125
|
+
// Edge-triggered bonus for matches in camelCase words.
|
126
|
+
// Compared to word-boundary case, they don't accompany single-character gaps
|
127
|
+
// (e.g. FooBar vs. foo-bar), so we deduct bonus point accordingly.
|
128
|
+
bonusCamel123 = bonusBoundary + scoreGapExtension
|
129
|
+
|
130
|
+
// Minimum bonus point given to characters in consecutive chunks.
|
131
|
+
// Note that bonus points for consecutive matches shouldn't have needed if we
|
132
|
+
// used fixed match score as in the original algorithm.
|
133
|
+
bonusConsecutive = -(scoreGapStart + scoreGapExtension)
|
134
|
+
|
135
|
+
// The first character in the typed pattern usually has more significance
|
136
|
+
// than the rest so it's important that it appears at special positions where
|
137
|
+
// bonus points are given, e.g. "to-go" vs. "ongoing" on "og" or on "ogo".
|
138
|
+
// The amount of the extra bonus should be limited so that the gap penalty is
|
139
|
+
// still respected.
|
140
|
+
bonusFirstCharMultiplier = 2
|
141
|
+
)
|
142
|
+
|
143
|
+
type charClass int
|
144
|
+
|
145
|
+
const (
|
146
|
+
charNonWord charClass = iota
|
147
|
+
charLower
|
148
|
+
charUpper
|
149
|
+
charLetter
|
150
|
+
charNumber
|
151
|
+
)
|
152
|
+
|
153
|
+
func posArray(withPos bool, len int) *[]int {
|
154
|
+
if withPos {
|
155
|
+
pos := make([]int, 0, len)
|
156
|
+
return &pos
|
157
|
+
}
|
158
|
+
return nil
|
159
|
+
}
|
160
|
+
|
161
|
+
func alloc16(offset int, slab *util.Slab, size int) (int, []int16) {
|
162
|
+
if slab != nil && cap(slab.I16) > offset+size {
|
163
|
+
slice := slab.I16[offset : offset+size]
|
164
|
+
return offset + size, slice
|
165
|
+
}
|
166
|
+
return offset, make([]int16, size)
|
167
|
+
}
|
168
|
+
|
169
|
+
func alloc32(offset int, slab *util.Slab, size int) (int, []int32) {
|
170
|
+
if slab != nil && cap(slab.I32) > offset+size {
|
171
|
+
slice := slab.I32[offset : offset+size]
|
172
|
+
return offset + size, slice
|
173
|
+
}
|
174
|
+
return offset, make([]int32, size)
|
175
|
+
}
|
176
|
+
|
177
|
+
func charClassOfAscii(char rune) charClass {
|
178
|
+
if char >= 'a' && char <= 'z' {
|
179
|
+
return charLower
|
180
|
+
} else if char >= 'A' && char <= 'Z' {
|
181
|
+
return charUpper
|
182
|
+
} else if char >= '0' && char <= '9' {
|
183
|
+
return charNumber
|
184
|
+
}
|
185
|
+
return charNonWord
|
186
|
+
}
|
187
|
+
|
188
|
+
func charClassOfNonAscii(char rune) charClass {
|
189
|
+
if unicode.IsLower(char) {
|
190
|
+
return charLower
|
191
|
+
} else if unicode.IsUpper(char) {
|
192
|
+
return charUpper
|
193
|
+
} else if unicode.IsNumber(char) {
|
194
|
+
return charNumber
|
195
|
+
} else if unicode.IsLetter(char) {
|
196
|
+
return charLetter
|
197
|
+
}
|
198
|
+
return charNonWord
|
199
|
+
}
|
200
|
+
|
201
|
+
func charClassOf(char rune) charClass {
|
202
|
+
if char <= unicode.MaxASCII {
|
203
|
+
return charClassOfAscii(char)
|
204
|
+
}
|
205
|
+
return charClassOfNonAscii(char)
|
206
|
+
}
|
207
|
+
|
208
|
+
func bonusFor(prevClass charClass, class charClass) int16 {
|
209
|
+
if prevClass == charNonWord && class != charNonWord {
|
210
|
+
// Word boundary
|
211
|
+
return bonusBoundary
|
212
|
+
} else if prevClass == charLower && class == charUpper ||
|
213
|
+
prevClass != charNumber && class == charNumber {
|
214
|
+
// camelCase letter123
|
215
|
+
return bonusCamel123
|
216
|
+
} else if class == charNonWord {
|
217
|
+
return bonusNonWord
|
218
|
+
}
|
219
|
+
return 0
|
220
|
+
}
|
221
|
+
|
222
|
+
func bonusAt(input *util.Chars, idx int) int16 {
|
223
|
+
if idx == 0 {
|
224
|
+
return bonusBoundary
|
225
|
+
}
|
226
|
+
return bonusFor(charClassOf(input.Get(idx-1)), charClassOf(input.Get(idx)))
|
227
|
+
}
|
228
|
+
|
229
|
+
func normalizeRune(r rune) rune {
|
230
|
+
if r < 0x00C0 || r > 0x2184 {
|
231
|
+
return r
|
232
|
+
}
|
233
|
+
|
234
|
+
n := normalized[r]
|
235
|
+
if n > 0 {
|
236
|
+
return n
|
237
|
+
}
|
238
|
+
return r
|
239
|
+
}
|
240
|
+
|
241
|
+
// Algo functions make two assumptions
|
242
|
+
// 1. "pattern" is given in lowercase if "caseSensitive" is false
|
243
|
+
// 2. "pattern" is already normalized if "normalize" is true
|
244
|
+
type Algo func(caseSensitive bool, normalize bool, forward bool, input *util.Chars, pattern []rune, withPos bool, slab *util.Slab) (Result, *[]int)
|
245
|
+
|
246
|
+
func trySkip(input *util.Chars, caseSensitive bool, b byte, from int) int {
|
247
|
+
byteArray := input.Bytes()[from:]
|
248
|
+
idx := bytes.IndexByte(byteArray, b)
|
249
|
+
if idx == 0 {
|
250
|
+
// Can't skip any further
|
251
|
+
return from
|
252
|
+
}
|
253
|
+
// We may need to search for the uppercase letter again. We don't have to
|
254
|
+
// consider normalization as we can be sure that this is an ASCII string.
|
255
|
+
if !caseSensitive && b >= 'a' && b <= 'z' {
|
256
|
+
if idx > 0 {
|
257
|
+
byteArray = byteArray[:idx]
|
258
|
+
}
|
259
|
+
uidx := bytes.IndexByte(byteArray, b-32)
|
260
|
+
if uidx >= 0 {
|
261
|
+
idx = uidx
|
262
|
+
}
|
263
|
+
}
|
264
|
+
if idx < 0 {
|
265
|
+
return -1
|
266
|
+
}
|
267
|
+
return from + idx
|
268
|
+
}
|
269
|
+
|
270
|
+
func isAscii(runes []rune) bool {
|
271
|
+
for _, r := range runes {
|
272
|
+
if r >= utf8.RuneSelf {
|
273
|
+
return false
|
274
|
+
}
|
275
|
+
}
|
276
|
+
return true
|
277
|
+
}
|
278
|
+
|
279
|
+
func asciiFuzzyIndex(input *util.Chars, pattern []rune, caseSensitive bool) int {
|
280
|
+
// Can't determine
|
281
|
+
if !input.IsBytes() {
|
282
|
+
return 0
|
283
|
+
}
|
284
|
+
|
285
|
+
// Not possible
|
286
|
+
if !isAscii(pattern) {
|
287
|
+
return -1
|
288
|
+
}
|
289
|
+
|
290
|
+
firstIdx, idx := 0, 0
|
291
|
+
for pidx := 0; pidx < len(pattern); pidx++ {
|
292
|
+
idx = trySkip(input, caseSensitive, byte(pattern[pidx]), idx)
|
293
|
+
if idx < 0 {
|
294
|
+
return -1
|
295
|
+
}
|
296
|
+
if pidx == 0 && idx > 0 {
|
297
|
+
// Step back to find the right bonus point
|
298
|
+
firstIdx = idx - 1
|
299
|
+
}
|
300
|
+
idx++
|
301
|
+
}
|
302
|
+
return firstIdx
|
303
|
+
}
|
304
|
+
|
305
|
+
func debugV2(T []rune, pattern []rune, F []int32, lastIdx int, H []int16, C []int16) {
|
306
|
+
width := lastIdx - int(F[0]) + 1
|
307
|
+
|
308
|
+
for i, f := range F {
|
309
|
+
I := i * width
|
310
|
+
if i == 0 {
|
311
|
+
fmt.Print(" ")
|
312
|
+
for j := int(f); j <= lastIdx; j++ {
|
313
|
+
fmt.Printf(" " + string(T[j]) + " ")
|
314
|
+
}
|
315
|
+
fmt.Println()
|
316
|
+
}
|
317
|
+
fmt.Print(string(pattern[i]) + " ")
|
318
|
+
for idx := int(F[0]); idx < int(f); idx++ {
|
319
|
+
fmt.Print(" 0 ")
|
320
|
+
}
|
321
|
+
for idx := int(f); idx <= lastIdx; idx++ {
|
322
|
+
fmt.Printf("%2d ", H[i*width+idx-int(F[0])])
|
323
|
+
}
|
324
|
+
fmt.Println()
|
325
|
+
|
326
|
+
fmt.Print(" ")
|
327
|
+
for idx, p := range C[I : I+width] {
|
328
|
+
if idx+int(F[0]) < int(F[i]) {
|
329
|
+
p = 0
|
330
|
+
}
|
331
|
+
if p > 0 {
|
332
|
+
fmt.Printf("%2d ", p)
|
333
|
+
} else {
|
334
|
+
fmt.Print(" ")
|
335
|
+
}
|
336
|
+
}
|
337
|
+
fmt.Println()
|
338
|
+
}
|
339
|
+
}
|
340
|
+
|
341
|
+
func FuzzyMatchV2(caseSensitive bool, normalize bool, forward bool, input *util.Chars, pattern []rune, withPos bool, slab *util.Slab) (Result, *[]int) {
|
342
|
+
// Assume that pattern is given in lowercase if case-insensitive.
|
343
|
+
// First check if there's a match and calculate bonus for each position.
|
344
|
+
// If the input string is too long, consider finding the matching chars in
|
345
|
+
// this phase as well (non-optimal alignment).
|
346
|
+
M := len(pattern)
|
347
|
+
if M == 0 {
|
348
|
+
return Result{0, 0, 0}, posArray(withPos, M)
|
349
|
+
}
|
350
|
+
N := input.Length()
|
351
|
+
|
352
|
+
// Since O(nm) algorithm can be prohibitively expensive for large input,
|
353
|
+
// we fall back to the greedy algorithm.
|
354
|
+
if slab != nil && N*M > cap(slab.I16) {
|
355
|
+
return FuzzyMatchV1(caseSensitive, normalize, forward, input, pattern, withPos, slab)
|
356
|
+
}
|
357
|
+
|
358
|
+
// Phase 1. Optimized search for ASCII string
|
359
|
+
idx := asciiFuzzyIndex(input, pattern, caseSensitive)
|
360
|
+
if idx < 0 {
|
361
|
+
return Result{-1, -1, 0}, nil
|
362
|
+
}
|
363
|
+
|
364
|
+
// Reuse pre-allocated integer slice to avoid unnecessary sweeping of garbages
|
365
|
+
offset16 := 0
|
366
|
+
offset32 := 0
|
367
|
+
offset16, H0 := alloc16(offset16, slab, N)
|
368
|
+
offset16, C0 := alloc16(offset16, slab, N)
|
369
|
+
// Bonus point for each position
|
370
|
+
offset16, B := alloc16(offset16, slab, N)
|
371
|
+
// The first occurrence of each character in the pattern
|
372
|
+
offset32, F := alloc32(offset32, slab, M)
|
373
|
+
// Rune array
|
374
|
+
_, T := alloc32(offset32, slab, N)
|
375
|
+
input.CopyRunes(T)
|
376
|
+
|
377
|
+
// Phase 2. Calculate bonus for each point
|
378
|
+
maxScore, maxScorePos := int16(0), 0
|
379
|
+
pidx, lastIdx := 0, 0
|
380
|
+
pchar0, pchar, prevH0, prevClass, inGap := pattern[0], pattern[0], int16(0), charNonWord, false
|
381
|
+
Tsub := T[idx:]
|
382
|
+
H0sub, C0sub, Bsub := H0[idx:][:len(Tsub)], C0[idx:][:len(Tsub)], B[idx:][:len(Tsub)]
|
383
|
+
for off, char := range Tsub {
|
384
|
+
var class charClass
|
385
|
+
if char <= unicode.MaxASCII {
|
386
|
+
class = charClassOfAscii(char)
|
387
|
+
if !caseSensitive && class == charUpper {
|
388
|
+
char += 32
|
389
|
+
}
|
390
|
+
} else {
|
391
|
+
class = charClassOfNonAscii(char)
|
392
|
+
if !caseSensitive && class == charUpper {
|
393
|
+
char = unicode.To(unicode.LowerCase, char)
|
394
|
+
}
|
395
|
+
if normalize {
|
396
|
+
char = normalizeRune(char)
|
397
|
+
}
|
398
|
+
}
|
399
|
+
|
400
|
+
Tsub[off] = char
|
401
|
+
bonus := bonusFor(prevClass, class)
|
402
|
+
Bsub[off] = bonus
|
403
|
+
prevClass = class
|
404
|
+
|
405
|
+
if char == pchar {
|
406
|
+
if pidx < M {
|
407
|
+
F[pidx] = int32(idx + off)
|
408
|
+
pidx++
|
409
|
+
pchar = pattern[util.Min(pidx, M-1)]
|
410
|
+
}
|
411
|
+
lastIdx = idx + off
|
412
|
+
}
|
413
|
+
|
414
|
+
if char == pchar0 {
|
415
|
+
score := scoreMatch + bonus*bonusFirstCharMultiplier
|
416
|
+
H0sub[off] = score
|
417
|
+
C0sub[off] = 1
|
418
|
+
if M == 1 && (forward && score > maxScore || !forward && score >= maxScore) {
|
419
|
+
maxScore, maxScorePos = score, idx+off
|
420
|
+
if forward && bonus == bonusBoundary {
|
421
|
+
break
|
422
|
+
}
|
423
|
+
}
|
424
|
+
inGap = false
|
425
|
+
} else {
|
426
|
+
if inGap {
|
427
|
+
H0sub[off] = util.Max16(prevH0+scoreGapExtension, 0)
|
428
|
+
} else {
|
429
|
+
H0sub[off] = util.Max16(prevH0+scoreGapStart, 0)
|
430
|
+
}
|
431
|
+
C0sub[off] = 0
|
432
|
+
inGap = true
|
433
|
+
}
|
434
|
+
prevH0 = H0sub[off]
|
435
|
+
}
|
436
|
+
if pidx != M {
|
437
|
+
return Result{-1, -1, 0}, nil
|
438
|
+
}
|
439
|
+
if M == 1 {
|
440
|
+
result := Result{maxScorePos, maxScorePos + 1, int(maxScore)}
|
441
|
+
if !withPos {
|
442
|
+
return result, nil
|
443
|
+
}
|
444
|
+
pos := []int{maxScorePos}
|
445
|
+
return result, &pos
|
446
|
+
}
|
447
|
+
|
448
|
+
// Phase 3. Fill in score matrix (H)
|
449
|
+
// Unlike the original algorithm, we do not allow omission.
|
450
|
+
f0 := int(F[0])
|
451
|
+
width := lastIdx - f0 + 1
|
452
|
+
offset16, H := alloc16(offset16, slab, width*M)
|
453
|
+
copy(H, H0[f0:lastIdx+1])
|
454
|
+
|
455
|
+
// Possible length of consecutive chunk at each position.
|
456
|
+
_, C := alloc16(offset16, slab, width*M)
|
457
|
+
copy(C, C0[f0:lastIdx+1])
|
458
|
+
|
459
|
+
Fsub := F[1:]
|
460
|
+
Psub := pattern[1:][:len(Fsub)]
|
461
|
+
for off, f := range Fsub {
|
462
|
+
f := int(f)
|
463
|
+
pchar := Psub[off]
|
464
|
+
pidx := off + 1
|
465
|
+
row := pidx * width
|
466
|
+
inGap := false
|
467
|
+
Tsub := T[f : lastIdx+1]
|
468
|
+
Bsub := B[f:][:len(Tsub)]
|
469
|
+
Csub := C[row+f-f0:][:len(Tsub)]
|
470
|
+
Cdiag := C[row+f-f0-1-width:][:len(Tsub)]
|
471
|
+
Hsub := H[row+f-f0:][:len(Tsub)]
|
472
|
+
Hdiag := H[row+f-f0-1-width:][:len(Tsub)]
|
473
|
+
Hleft := H[row+f-f0-1:][:len(Tsub)]
|
474
|
+
Hleft[0] = 0
|
475
|
+
for off, char := range Tsub {
|
476
|
+
col := off + f
|
477
|
+
var s1, s2, consecutive int16
|
478
|
+
|
479
|
+
if inGap {
|
480
|
+
s2 = Hleft[off] + scoreGapExtension
|
481
|
+
} else {
|
482
|
+
s2 = Hleft[off] + scoreGapStart
|
483
|
+
}
|
484
|
+
|
485
|
+
if pchar == char {
|
486
|
+
s1 = Hdiag[off] + scoreMatch
|
487
|
+
b := Bsub[off]
|
488
|
+
consecutive = Cdiag[off] + 1
|
489
|
+
// Break consecutive chunk
|
490
|
+
if b == bonusBoundary {
|
491
|
+
consecutive = 1
|
492
|
+
} else if consecutive > 1 {
|
493
|
+
b = util.Max16(b, util.Max16(bonusConsecutive, B[col-int(consecutive)+1]))
|
494
|
+
}
|
495
|
+
if s1+b < s2 {
|
496
|
+
s1 += Bsub[off]
|
497
|
+
consecutive = 0
|
498
|
+
} else {
|
499
|
+
s1 += b
|
500
|
+
}
|
501
|
+
}
|
502
|
+
Csub[off] = consecutive
|
503
|
+
|
504
|
+
inGap = s1 < s2
|
505
|
+
score := util.Max16(util.Max16(s1, s2), 0)
|
506
|
+
if pidx == M-1 && (forward && score > maxScore || !forward && score >= maxScore) {
|
507
|
+
maxScore, maxScorePos = score, col
|
508
|
+
}
|
509
|
+
Hsub[off] = score
|
510
|
+
}
|
511
|
+
}
|
512
|
+
|
513
|
+
if DEBUG {
|
514
|
+
debugV2(T, pattern, F, lastIdx, H, C)
|
515
|
+
}
|
516
|
+
|
517
|
+
// Phase 4. (Optional) Backtrace to find character positions
|
518
|
+
pos := posArray(withPos, M)
|
519
|
+
j := f0
|
520
|
+
if withPos {
|
521
|
+
i := M - 1
|
522
|
+
j = maxScorePos
|
523
|
+
preferMatch := true
|
524
|
+
for {
|
525
|
+
I := i * width
|
526
|
+
j0 := j - f0
|
527
|
+
s := H[I+j0]
|
528
|
+
|
529
|
+
var s1, s2 int16
|
530
|
+
if i > 0 && j >= int(F[i]) {
|
531
|
+
s1 = H[I-width+j0-1]
|
532
|
+
}
|
533
|
+
if j > int(F[i]) {
|
534
|
+
s2 = H[I+j0-1]
|
535
|
+
}
|
536
|
+
|
537
|
+
if s > s1 && (s > s2 || s == s2 && preferMatch) {
|
538
|
+
*pos = append(*pos, j)
|
539
|
+
if i == 0 {
|
540
|
+
break
|
541
|
+
}
|
542
|
+
i--
|
543
|
+
}
|
544
|
+
preferMatch = C[I+j0] > 1 || I+width+j0+1 < len(C) && C[I+width+j0+1] > 0
|
545
|
+
j--
|
546
|
+
}
|
547
|
+
}
|
548
|
+
// Start offset we return here is only relevant when begin tiebreak is used.
|
549
|
+
// However finding the accurate offset requires backtracking, and we don't
|
550
|
+
// want to pay extra cost for the option that has lost its importance.
|
551
|
+
return Result{j, maxScorePos + 1, int(maxScore)}, pos
|
552
|
+
}
|
553
|
+
|
554
|
+
// Implement the same sorting criteria as V2
|
555
|
+
func calculateScore(caseSensitive bool, normalize bool, text *util.Chars, pattern []rune, sidx int, eidx int, withPos bool) (int, *[]int) {
|
556
|
+
pidx, score, inGap, consecutive, firstBonus := 0, 0, false, 0, int16(0)
|
557
|
+
pos := posArray(withPos, len(pattern))
|
558
|
+
prevClass := charNonWord
|
559
|
+
if sidx > 0 {
|
560
|
+
prevClass = charClassOf(text.Get(sidx - 1))
|
561
|
+
}
|
562
|
+
for idx := sidx; idx < eidx; idx++ {
|
563
|
+
char := text.Get(idx)
|
564
|
+
class := charClassOf(char)
|
565
|
+
if !caseSensitive {
|
566
|
+
if char >= 'A' && char <= 'Z' {
|
567
|
+
char += 32
|
568
|
+
} else if char > unicode.MaxASCII {
|
569
|
+
char = unicode.To(unicode.LowerCase, char)
|
570
|
+
}
|
571
|
+
}
|
572
|
+
// pattern is already normalized
|
573
|
+
if normalize {
|
574
|
+
char = normalizeRune(char)
|
575
|
+
}
|
576
|
+
if char == pattern[pidx] {
|
577
|
+
if withPos {
|
578
|
+
*pos = append(*pos, idx)
|
579
|
+
}
|
580
|
+
score += scoreMatch
|
581
|
+
bonus := bonusFor(prevClass, class)
|
582
|
+
if consecutive == 0 {
|
583
|
+
firstBonus = bonus
|
584
|
+
} else {
|
585
|
+
// Break consecutive chunk
|
586
|
+
if bonus == bonusBoundary {
|
587
|
+
firstBonus = bonus
|
588
|
+
}
|
589
|
+
bonus = util.Max16(util.Max16(bonus, firstBonus), bonusConsecutive)
|
590
|
+
}
|
591
|
+
if pidx == 0 {
|
592
|
+
score += int(bonus * bonusFirstCharMultiplier)
|
593
|
+
} else {
|
594
|
+
score += int(bonus)
|
595
|
+
}
|
596
|
+
inGap = false
|
597
|
+
consecutive++
|
598
|
+
pidx++
|
599
|
+
} else {
|
600
|
+
if inGap {
|
601
|
+
score += scoreGapExtension
|
602
|
+
} else {
|
603
|
+
score += scoreGapStart
|
604
|
+
}
|
605
|
+
inGap = true
|
606
|
+
consecutive = 0
|
607
|
+
firstBonus = 0
|
608
|
+
}
|
609
|
+
prevClass = class
|
610
|
+
}
|
611
|
+
return score, pos
|
612
|
+
}
|
613
|
+
|
614
|
+
// FuzzyMatchV1 performs fuzzy-match
|
615
|
+
func FuzzyMatchV1(caseSensitive bool, normalize bool, forward bool, text *util.Chars, pattern []rune, withPos bool, slab *util.Slab) (Result, *[]int) {
|
616
|
+
if len(pattern) == 0 {
|
617
|
+
return Result{0, 0, 0}, nil
|
618
|
+
}
|
619
|
+
if asciiFuzzyIndex(text, pattern, caseSensitive) < 0 {
|
620
|
+
return Result{-1, -1, 0}, nil
|
621
|
+
}
|
622
|
+
|
623
|
+
pidx := 0
|
624
|
+
sidx := -1
|
625
|
+
eidx := -1
|
626
|
+
|
627
|
+
lenRunes := text.Length()
|
628
|
+
lenPattern := len(pattern)
|
629
|
+
|
630
|
+
for index := 0; index < lenRunes; index++ {
|
631
|
+
char := text.Get(indexAt(index, lenRunes, forward))
|
632
|
+
// This is considerably faster than blindly applying strings.ToLower to the
|
633
|
+
// whole string
|
634
|
+
if !caseSensitive {
|
635
|
+
// Partially inlining `unicode.ToLower`. Ugly, but makes a noticeable
|
636
|
+
// difference in CPU cost. (Measured on Go 1.4.1. Also note that the Go
|
637
|
+
// compiler as of now does not inline non-leaf functions.)
|
638
|
+
if char >= 'A' && char <= 'Z' {
|
639
|
+
char += 32
|
640
|
+
} else if char > unicode.MaxASCII {
|
641
|
+
char = unicode.To(unicode.LowerCase, char)
|
642
|
+
}
|
643
|
+
}
|
644
|
+
if normalize {
|
645
|
+
char = normalizeRune(char)
|
646
|
+
}
|
647
|
+
pchar := pattern[indexAt(pidx, lenPattern, forward)]
|
648
|
+
if char == pchar {
|
649
|
+
if sidx < 0 {
|
650
|
+
sidx = index
|
651
|
+
}
|
652
|
+
if pidx++; pidx == lenPattern {
|
653
|
+
eidx = index + 1
|
654
|
+
break
|
655
|
+
}
|
656
|
+
}
|
657
|
+
}
|
658
|
+
|
659
|
+
if sidx >= 0 && eidx >= 0 {
|
660
|
+
pidx--
|
661
|
+
for index := eidx - 1; index >= sidx; index-- {
|
662
|
+
tidx := indexAt(index, lenRunes, forward)
|
663
|
+
char := text.Get(tidx)
|
664
|
+
if !caseSensitive {
|
665
|
+
if char >= 'A' && char <= 'Z' {
|
666
|
+
char += 32
|
667
|
+
} else if char > unicode.MaxASCII {
|
668
|
+
char = unicode.To(unicode.LowerCase, char)
|
669
|
+
}
|
670
|
+
}
|
671
|
+
|
672
|
+
pidx_ := indexAt(pidx, lenPattern, forward)
|
673
|
+
pchar := pattern[pidx_]
|
674
|
+
if char == pchar {
|
675
|
+
if pidx--; pidx < 0 {
|
676
|
+
sidx = index
|
677
|
+
break
|
678
|
+
}
|
679
|
+
}
|
680
|
+
}
|
681
|
+
|
682
|
+
if !forward {
|
683
|
+
sidx, eidx = lenRunes-eidx, lenRunes-sidx
|
684
|
+
}
|
685
|
+
|
686
|
+
score, pos := calculateScore(caseSensitive, normalize, text, pattern, sidx, eidx, withPos)
|
687
|
+
return Result{sidx, eidx, score}, pos
|
688
|
+
}
|
689
|
+
return Result{-1, -1, 0}, nil
|
690
|
+
}
|
691
|
+
|
692
|
+
// ExactMatchNaive is a basic string searching algorithm that handles case
|
693
|
+
// sensitivity. Although naive, it still performs better than the combination
|
694
|
+
// of strings.ToLower + strings.Index for typical fzf use cases where input
|
695
|
+
// strings and patterns are not very long.
|
696
|
+
//
|
697
|
+
// Since 0.15.0, this function searches for the match with the highest
|
698
|
+
// bonus point, instead of stopping immediately after finding the first match.
|
699
|
+
// The solution is much cheaper since there is only one possible alignment of
|
700
|
+
// the pattern.
|
701
|
+
func ExactMatchNaive(caseSensitive bool, normalize bool, forward bool, text *util.Chars, pattern []rune, withPos bool, slab *util.Slab) (Result, *[]int) {
|
702
|
+
if len(pattern) == 0 {
|
703
|
+
return Result{0, 0, 0}, nil
|
704
|
+
}
|
705
|
+
|
706
|
+
lenRunes := text.Length()
|
707
|
+
lenPattern := len(pattern)
|
708
|
+
|
709
|
+
if lenRunes < lenPattern {
|
710
|
+
return Result{-1, -1, 0}, nil
|
711
|
+
}
|
712
|
+
|
713
|
+
if asciiFuzzyIndex(text, pattern, caseSensitive) < 0 {
|
714
|
+
return Result{-1, -1, 0}, nil
|
715
|
+
}
|
716
|
+
|
717
|
+
// For simplicity, only look at the bonus at the first character position
|
718
|
+
pidx := 0
|
719
|
+
bestPos, bonus, bestBonus := -1, int16(0), int16(-1)
|
720
|
+
for index := 0; index < lenRunes; index++ {
|
721
|
+
index_ := indexAt(index, lenRunes, forward)
|
722
|
+
char := text.Get(index_)
|
723
|
+
if !caseSensitive {
|
724
|
+
if char >= 'A' && char <= 'Z' {
|
725
|
+
char += 32
|
726
|
+
} else if char > unicode.MaxASCII {
|
727
|
+
char = unicode.To(unicode.LowerCase, char)
|
728
|
+
}
|
729
|
+
}
|
730
|
+
if normalize {
|
731
|
+
char = normalizeRune(char)
|
732
|
+
}
|
733
|
+
pidx_ := indexAt(pidx, lenPattern, forward)
|
734
|
+
pchar := pattern[pidx_]
|
735
|
+
if pchar == char {
|
736
|
+
if pidx_ == 0 {
|
737
|
+
bonus = bonusAt(text, index_)
|
738
|
+
}
|
739
|
+
pidx++
|
740
|
+
if pidx == lenPattern {
|
741
|
+
if bonus > bestBonus {
|
742
|
+
bestPos, bestBonus = index, bonus
|
743
|
+
}
|
744
|
+
if bonus == bonusBoundary {
|
745
|
+
break
|
746
|
+
}
|
747
|
+
index -= pidx - 1
|
748
|
+
pidx, bonus = 0, 0
|
749
|
+
}
|
750
|
+
} else {
|
751
|
+
index -= pidx
|
752
|
+
pidx, bonus = 0, 0
|
753
|
+
}
|
754
|
+
}
|
755
|
+
if bestPos >= 0 {
|
756
|
+
var sidx, eidx int
|
757
|
+
if forward {
|
758
|
+
sidx = bestPos - lenPattern + 1
|
759
|
+
eidx = bestPos + 1
|
760
|
+
} else {
|
761
|
+
sidx = lenRunes - (bestPos + 1)
|
762
|
+
eidx = lenRunes - (bestPos - lenPattern + 1)
|
763
|
+
}
|
764
|
+
score, _ := calculateScore(caseSensitive, normalize, text, pattern, sidx, eidx, false)
|
765
|
+
return Result{sidx, eidx, score}, nil
|
766
|
+
}
|
767
|
+
return Result{-1, -1, 0}, nil
|
768
|
+
}
|
769
|
+
|
770
|
+
// PrefixMatch performs prefix-match
|
771
|
+
func PrefixMatch(caseSensitive bool, normalize bool, forward bool, text *util.Chars, pattern []rune, withPos bool, slab *util.Slab) (Result, *[]int) {
|
772
|
+
if len(pattern) == 0 {
|
773
|
+
return Result{0, 0, 0}, nil
|
774
|
+
}
|
775
|
+
|
776
|
+
trimmedLen := 0
|
777
|
+
if !unicode.IsSpace(pattern[0]) {
|
778
|
+
trimmedLen = text.LeadingWhitespaces()
|
779
|
+
}
|
780
|
+
|
781
|
+
if text.Length()-trimmedLen < len(pattern) {
|
782
|
+
return Result{-1, -1, 0}, nil
|
783
|
+
}
|
784
|
+
|
785
|
+
for index, r := range pattern {
|
786
|
+
char := text.Get(trimmedLen + index)
|
787
|
+
if !caseSensitive {
|
788
|
+
char = unicode.ToLower(char)
|
789
|
+
}
|
790
|
+
if normalize {
|
791
|
+
char = normalizeRune(char)
|
792
|
+
}
|
793
|
+
if char != r {
|
794
|
+
return Result{-1, -1, 0}, nil
|
795
|
+
}
|
796
|
+
}
|
797
|
+
lenPattern := len(pattern)
|
798
|
+
score, _ := calculateScore(caseSensitive, normalize, text, pattern, trimmedLen, trimmedLen+lenPattern, false)
|
799
|
+
return Result{trimmedLen, trimmedLen + lenPattern, score}, nil
|
800
|
+
}
|
801
|
+
|
802
|
+
// SuffixMatch performs suffix-match
|
803
|
+
func SuffixMatch(caseSensitive bool, normalize bool, forward bool, text *util.Chars, pattern []rune, withPos bool, slab *util.Slab) (Result, *[]int) {
|
804
|
+
lenRunes := text.Length()
|
805
|
+
trimmedLen := lenRunes
|
806
|
+
if len(pattern) == 0 || !unicode.IsSpace(pattern[len(pattern)-1]) {
|
807
|
+
trimmedLen -= text.TrailingWhitespaces()
|
808
|
+
}
|
809
|
+
if len(pattern) == 0 {
|
810
|
+
return Result{trimmedLen, trimmedLen, 0}, nil
|
811
|
+
}
|
812
|
+
diff := trimmedLen - len(pattern)
|
813
|
+
if diff < 0 {
|
814
|
+
return Result{-1, -1, 0}, nil
|
815
|
+
}
|
816
|
+
|
817
|
+
for index, r := range pattern {
|
818
|
+
char := text.Get(index + diff)
|
819
|
+
if !caseSensitive {
|
820
|
+
char = unicode.ToLower(char)
|
821
|
+
}
|
822
|
+
if normalize {
|
823
|
+
char = normalizeRune(char)
|
824
|
+
}
|
825
|
+
if char != r {
|
826
|
+
return Result{-1, -1, 0}, nil
|
827
|
+
}
|
828
|
+
}
|
829
|
+
lenPattern := len(pattern)
|
830
|
+
sidx := trimmedLen - lenPattern
|
831
|
+
eidx := trimmedLen
|
832
|
+
score, _ := calculateScore(caseSensitive, normalize, text, pattern, sidx, eidx, false)
|
833
|
+
return Result{sidx, eidx, score}, nil
|
834
|
+
}
|
835
|
+
|
836
|
+
// EqualMatch performs equal-match
|
837
|
+
func EqualMatch(caseSensitive bool, normalize bool, forward bool, text *util.Chars, pattern []rune, withPos bool, slab *util.Slab) (Result, *[]int) {
|
838
|
+
lenPattern := len(pattern)
|
839
|
+
if lenPattern == 0 {
|
840
|
+
return Result{-1, -1, 0}, nil
|
841
|
+
}
|
842
|
+
|
843
|
+
// Strip leading whitespaces
|
844
|
+
trimmedLen := 0
|
845
|
+
if !unicode.IsSpace(pattern[0]) {
|
846
|
+
trimmedLen = text.LeadingWhitespaces()
|
847
|
+
}
|
848
|
+
|
849
|
+
// Strip trailing whitespaces
|
850
|
+
trimmedEndLen := 0
|
851
|
+
if !unicode.IsSpace(pattern[lenPattern-1]) {
|
852
|
+
trimmedEndLen = text.TrailingWhitespaces()
|
853
|
+
}
|
854
|
+
|
855
|
+
if text.Length()-trimmedLen-trimmedEndLen != lenPattern {
|
856
|
+
return Result{-1, -1, 0}, nil
|
857
|
+
}
|
858
|
+
match := true
|
859
|
+
if normalize {
|
860
|
+
runes := text.ToRunes()
|
861
|
+
for idx, pchar := range pattern {
|
862
|
+
char := runes[trimmedLen+idx]
|
863
|
+
if !caseSensitive {
|
864
|
+
char = unicode.To(unicode.LowerCase, char)
|
865
|
+
}
|
866
|
+
if normalizeRune(pchar) != normalizeRune(char) {
|
867
|
+
match = false
|
868
|
+
break
|
869
|
+
}
|
870
|
+
}
|
871
|
+
} else {
|
872
|
+
runes := text.ToRunes()
|
873
|
+
runesStr := string(runes[trimmedLen : len(runes)-trimmedEndLen])
|
874
|
+
if !caseSensitive {
|
875
|
+
runesStr = strings.ToLower(runesStr)
|
876
|
+
}
|
877
|
+
match = runesStr == string(pattern)
|
878
|
+
}
|
879
|
+
if match {
|
880
|
+
return Result{trimmedLen, trimmedLen + lenPattern, (scoreMatch+bonusBoundary)*lenPattern +
|
881
|
+
(bonusFirstCharMultiplier-1)*bonusBoundary}, nil
|
882
|
+
}
|
883
|
+
return Result{-1, -1, 0}, nil
|
884
|
+
}
|