doing 2.0.19 → 2.0.23

Sign up to get free protection for your applications and to get access to all the features.
Files changed (107) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +15 -0
  3. data/Gemfile.lock +15 -5
  4. data/README.md +1 -1
  5. data/bin/doing +2 -18
  6. data/doing.gemspec +5 -4
  7. data/doing.rdoc +2 -2
  8. data/generate_completions.sh +3 -3
  9. data/lib/doing/cli_status.rb +6 -2
  10. data/lib/doing/completion/bash_completion.rb +185 -0
  11. data/lib/doing/completion/fish_completion.rb +175 -0
  12. data/lib/doing/completion/string.rb +17 -0
  13. data/lib/doing/completion/zsh_completion.rb +140 -0
  14. data/lib/doing/completion.rb +39 -0
  15. data/lib/doing/version.rb +1 -1
  16. data/lib/doing/wwid.rb +19 -5
  17. data/lib/doing.rb +1 -1
  18. data/lib/helpers/fzf/.goreleaser.yml +119 -0
  19. data/lib/helpers/fzf/.rubocop.yml +28 -0
  20. data/lib/helpers/fzf/ADVANCED.md +565 -0
  21. data/lib/helpers/fzf/BUILD.md +49 -0
  22. data/lib/helpers/fzf/CHANGELOG.md +1193 -0
  23. data/lib/helpers/fzf/Dockerfile +11 -0
  24. data/lib/helpers/fzf/LICENSE +21 -0
  25. data/lib/helpers/fzf/Makefile +166 -0
  26. data/lib/helpers/fzf/README-VIM.md +486 -0
  27. data/lib/helpers/fzf/README.md +712 -0
  28. data/lib/helpers/fzf/bin/fzf-tmux +233 -0
  29. data/lib/helpers/fzf/doc/fzf.txt +512 -0
  30. data/lib/helpers/fzf/go.mod +17 -0
  31. data/lib/helpers/fzf/go.sum +31 -0
  32. data/lib/helpers/fzf/install +382 -0
  33. data/lib/helpers/fzf/install.ps1 +65 -0
  34. data/lib/helpers/fzf/main.go +14 -0
  35. data/lib/helpers/fzf/man/man1/fzf-tmux.1 +68 -0
  36. data/lib/helpers/fzf/man/man1/fzf.1 +1001 -0
  37. data/lib/helpers/fzf/plugin/fzf.vim +1048 -0
  38. data/lib/helpers/fzf/shell/completion.bash +381 -0
  39. data/lib/helpers/fzf/shell/completion.zsh +329 -0
  40. data/lib/helpers/fzf/shell/key-bindings.bash +96 -0
  41. data/lib/helpers/fzf/shell/key-bindings.fish +172 -0
  42. data/lib/helpers/fzf/shell/key-bindings.zsh +114 -0
  43. data/lib/helpers/fzf/src/LICENSE +21 -0
  44. data/lib/helpers/fzf/src/algo/algo.go +884 -0
  45. data/lib/helpers/fzf/src/algo/algo_test.go +197 -0
  46. data/lib/helpers/fzf/src/algo/normalize.go +492 -0
  47. data/lib/helpers/fzf/src/ansi.go +409 -0
  48. data/lib/helpers/fzf/src/ansi_test.go +427 -0
  49. data/lib/helpers/fzf/src/cache.go +81 -0
  50. data/lib/helpers/fzf/src/cache_test.go +39 -0
  51. data/lib/helpers/fzf/src/chunklist.go +89 -0
  52. data/lib/helpers/fzf/src/chunklist_test.go +80 -0
  53. data/lib/helpers/fzf/src/constants.go +85 -0
  54. data/lib/helpers/fzf/src/core.go +351 -0
  55. data/lib/helpers/fzf/src/history.go +96 -0
  56. data/lib/helpers/fzf/src/history_test.go +68 -0
  57. data/lib/helpers/fzf/src/item.go +44 -0
  58. data/lib/helpers/fzf/src/item_test.go +23 -0
  59. data/lib/helpers/fzf/src/matcher.go +235 -0
  60. data/lib/helpers/fzf/src/merger.go +120 -0
  61. data/lib/helpers/fzf/src/merger_test.go +88 -0
  62. data/lib/helpers/fzf/src/options.go +1691 -0
  63. data/lib/helpers/fzf/src/options_test.go +457 -0
  64. data/lib/helpers/fzf/src/pattern.go +425 -0
  65. data/lib/helpers/fzf/src/pattern_test.go +209 -0
  66. data/lib/helpers/fzf/src/protector/protector.go +8 -0
  67. data/lib/helpers/fzf/src/protector/protector_openbsd.go +10 -0
  68. data/lib/helpers/fzf/src/reader.go +201 -0
  69. data/lib/helpers/fzf/src/reader_test.go +63 -0
  70. data/lib/helpers/fzf/src/result.go +243 -0
  71. data/lib/helpers/fzf/src/result_others.go +16 -0
  72. data/lib/helpers/fzf/src/result_test.go +159 -0
  73. data/lib/helpers/fzf/src/result_x86.go +16 -0
  74. data/lib/helpers/fzf/src/terminal.go +2832 -0
  75. data/lib/helpers/fzf/src/terminal_test.go +638 -0
  76. data/lib/helpers/fzf/src/terminal_unix.go +26 -0
  77. data/lib/helpers/fzf/src/terminal_windows.go +45 -0
  78. data/lib/helpers/fzf/src/tokenizer.go +253 -0
  79. data/lib/helpers/fzf/src/tokenizer_test.go +112 -0
  80. data/lib/helpers/fzf/src/tui/dummy.go +46 -0
  81. data/lib/helpers/fzf/src/tui/light.go +987 -0
  82. data/lib/helpers/fzf/src/tui/light_unix.go +110 -0
  83. data/lib/helpers/fzf/src/tui/light_windows.go +145 -0
  84. data/lib/helpers/fzf/src/tui/tcell.go +721 -0
  85. data/lib/helpers/fzf/src/tui/tcell_test.go +392 -0
  86. data/lib/helpers/fzf/src/tui/ttyname_unix.go +47 -0
  87. data/lib/helpers/fzf/src/tui/ttyname_windows.go +14 -0
  88. data/lib/helpers/fzf/src/tui/tui.go +625 -0
  89. data/lib/helpers/fzf/src/tui/tui_test.go +20 -0
  90. data/lib/helpers/fzf/src/util/atomicbool.go +34 -0
  91. data/lib/helpers/fzf/src/util/atomicbool_test.go +17 -0
  92. data/lib/helpers/fzf/src/util/chars.go +198 -0
  93. data/lib/helpers/fzf/src/util/chars_test.go +46 -0
  94. data/lib/helpers/fzf/src/util/eventbox.go +96 -0
  95. data/lib/helpers/fzf/src/util/eventbox_test.go +61 -0
  96. data/lib/helpers/fzf/src/util/slab.go +12 -0
  97. data/lib/helpers/fzf/src/util/util.go +138 -0
  98. data/lib/helpers/fzf/src/util/util_test.go +40 -0
  99. data/lib/helpers/fzf/src/util/util_unix.go +47 -0
  100. data/lib/helpers/fzf/src/util/util_windows.go +83 -0
  101. data/lib/helpers/fzf/test/fzf.vader +175 -0
  102. data/lib/helpers/fzf/test/test_go.rb +2626 -0
  103. data/lib/helpers/fzf/uninstall +117 -0
  104. data/scripts/generate_bash_completions.rb +6 -12
  105. data/scripts/generate_fish_completions.rb +7 -16
  106. data/scripts/generate_zsh_completions.rb +6 -15
  107. metadata +144 -9
@@ -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
+ }