package fzf import ( "bytes" "fmt" "slices" "strings" "unicode" "unicode/utf8" "github.com/kovidgoyal/go-parallel" "golang.org/x/text/unicode/norm" ) var _ = fmt.Print /* Algorithm --------- Based on code from fzf (MIT licensed): https://github.com/junegunn/fzf FuzzyMatch implements a modified version of Smith-Waterman algorithm to find the optimal solution (highest score) according to the scoring criteria. Unlike the original algorithm, omission or mismatch of a character in the pattern is not allowed. Scoring criteria ---------------- - We prefer matches at special positions, such as the start of a word, or uppercase character in camelCase words. - That is, we prefer an occurrence of the pattern with more characters matching at special positions, even if the total match length is longer. e.g. "fuzzyfinder" vs. "fuzzy-finder" on "ff" ```````````` - Also, if the first character in the pattern appears at one of the special positions, the bonus point for the position is multiplied by a constant as it is extremely likely that the first character in the typed pattern has more significance than the rest. e.g. "fo-bar" vs. "foob-r" on "br" `````` - But since fzf is still a fuzzy finder, not an acronym finder, we should also consider the total length of the matched substring. This is why we have the gap penalty. The gap penalty increases as the length of the gap (distance between the matching characters) increases, so the effect of the bonus is eventually cancelled at some point. e.g. "fuzzyfinder" vs. "fuzzy-blurry-finder" on "ff" ``````````` - Consequently, it is crucial to find the right balance between the bonus and the gap penalty. The parameters were chosen that the bonus is cancelled when the gap size increases beyond 8 characters. - The bonus mechanism can have the undesirable side effect where consecutive matches are ranked lower than the ones with gaps. e.g. "foobar" vs. "foo-bar" on "foob" ``````` - To correct this anomaly, we also give extra bonus point to each character in a consecutive matching chunk. e.g. "foobar" vs. "foo-bar" on "foob" `````` - The amount of consecutive bonus is primarily determined by the bonus of the first character in the chunk. e.g. "foobar" vs. "out-of-bound" on "oob" ```````````` */ func try_skip(input *Chars, case_sensitive bool, b byte, from int) int { byteArray := input.Bytes()[from:] idx := bytes.IndexByte(byteArray, b) if idx == 0 { // Can't skip any further return from } // We may need to search for the uppercase letter again. We don't have to // consider normalization as we can be sure that this is an ASCII string. if !case_sensitive && b >= 'a' && b <= 'z' { if idx > 0 { byteArray = byteArray[:idx] } uidx := bytes.IndexByte(byteArray, b-32) if uidx >= 0 { idx = uidx } } if idx < 0 { return -1 } return from + idx } func ascii_fuzzy_index(input *Chars, pattern []rune, pattern_is_ascii bool, case_sensitive bool) (int, int) { // Can't determine if !input.Is_ASCII() { return 0, input.Length() } // Can't match if !pattern_is_ascii { return -1, -1 } firstIdx, idx, lastIdx := 0, 0, 0 var b byte for pidx := range len(pattern) { b = byte(pattern[pidx]) idx = try_skip(input, case_sensitive, b, idx) if idx < 0 { return -1, -1 } if pidx == 0 && idx > 0 { // Step back to find the right bonus point firstIdx = idx - 1 } lastIdx = idx idx++ } // Find the last appearance of the last character of the pattern to limit the search scope bu := b if !case_sensitive && b >= 'a' && b <= 'z' { bu = b - 32 } scope := input.Bytes()[lastIdx:] for offset := len(scope) - 1; offset > 0; offset-- { if scope[offset] == b || scope[offset] == bu { return firstIdx, lastIdx + offset + 1 } } return firstIdx, lastIdx + 1 } func (m *FuzzyMatcher) charClassOfNonAscii(char rune) charClass { if unicode.IsLower(char) { return charLower } else if unicode.IsUpper(char) { return charUpper } else if unicode.IsNumber(char) { return charNumber } else if unicode.IsLetter(char) { return charLetter } else if unicode.IsSpace(char) { return charWhite } else if strings.ContainsRune(m.delimiterChars, char) { return charDelimiter } return charNonWord } // Score the input against pattern. If !m.Case_sensitive pattern must be // lowercased already. pattern must be non-empty. When m.Ignore_accents // accents must already be removed from both pattern and input. func (m *FuzzyMatcher) score_one(input *Chars, pattern []rune, pattern_is_ascii bool, slab *slab) (ans Result) { M := len(pattern) N := input.Length() if M > N { return } // Phase 1. Optimized search for ASCII string minIdx, maxIdx := ascii_fuzzy_index(input, pattern, pattern_is_ascii, m.Case_sensitive) if minIdx < 0 { return } // fmt.Println(N, maxIdx, idx, maxIdx-idx, input.ToString()) N = maxIdx - minIdx slab.reset() H0 := slab.alloc16(N) C0 := slab.alloc16(N) // Bonus point for each position B := slab.alloc16(N) // The first occurrence of each character in the pattern F := slab.alloc32(M) // Rune array T := slab.alloc32(N) input.CopyRunes(T, minIdx) // Phase 2. Calculate bonus for each point maxScore, maxScorePos := int16(0), 0 pidx, lastIdx := 0, 0 pchar0, pchar, prevH0, prevClass, inGap := pattern[0], pattern[0], int16(0), m.initialCharClass, false for off, char := range T { var class charClass if char <= unicode.MaxASCII { class = m.asciiCharClasses[char] if !m.Case_sensitive && class == charUpper { char += 32 T[off] = char } } else { class = m.charClassOfNonAscii(char) if !m.Case_sensitive && class == charUpper { char = unicode.To(unicode.LowerCase, char) } T[off] = char } bonus := m.bonusMatrix[prevClass][class] B[off] = bonus prevClass = class if char == pchar { if pidx < M { F[pidx] = int32(off) pidx++ pchar = pattern[min(pidx, M-1)] } lastIdx = off } if char == pchar0 { score := scoreMatch + bonus*bonusFirstCharMultiplier H0[off] = score C0[off] = 1 if M == 1 && (!m.Backwards && score > maxScore || m.Backwards && score >= maxScore) { maxScore, maxScorePos = score, off if !m.Backwards && bonus >= bonusBoundary { break } } inGap = false } else { if inGap { H0[off] = max(prevH0+scoreGapExtension, 0) } else { H0[off] = max(prevH0+scoreGapStart, 0) } C0[off] = 0 inGap = true } prevH0 = H0[off] } if pidx != M { return } if M == 1 { if m.Without_positions { return Result{Score: uint(maxScore)} } return Result{Score: uint(maxScore), Positions: []int{minIdx + maxScorePos}} } // Phase 3. Fill in score matrix (H) // Unlike the original algorithm, we do not allow omission. f0 := int(F[0]) width := lastIdx - f0 + 1 H := slab.alloc16(width * M) copy(H, H0[f0:lastIdx+1]) // Possible length of consecutive chunk at each position. C := slab.alloc16(width * M) copy(C, C0[f0:lastIdx+1]) Fsub := F[1:] Psub := pattern[1:][:len(Fsub)] for off, f := range Fsub { f := int(f) pchar := Psub[off] pidx := off + 1 row := pidx * width inGap := false Tsub := T[f : lastIdx+1] Bsub := B[f:][:len(Tsub)] Csub := C[row+f-f0:][:len(Tsub)] Cdiag := C[row+f-f0-1-width:][:len(Tsub)] Hsub := H[row+f-f0:][:len(Tsub)] Hdiag := H[row+f-f0-1-width:][:len(Tsub)] Hleft := H[row+f-f0-1:][:len(Tsub)] Hleft[0] = 0 for off, char := range Tsub { col := off + f var s1, s2, consecutive int16 if inGap { s2 = Hleft[off] + scoreGapExtension } else { s2 = Hleft[off] + scoreGapStart } if pchar == char { s1 = Hdiag[off] + scoreMatch b := Bsub[off] consecutive = Cdiag[off] + 1 if consecutive > 1 { fb := B[col-int(consecutive)+1] // Break consecutive chunk if b >= bonusBoundary && b > fb { consecutive = 1 } else { b = max(b, max(bonusConsecutive, fb)) } } if s1+b < s2 { s1 += Bsub[off] consecutive = 0 } else { s1 += b } } Csub[off] = consecutive inGap = s1 < s2 score := max(max(s1, s2), 0) if pidx == M-1 && (!m.Backwards && score > maxScore || m.Backwards && score >= maxScore) { maxScore, maxScorePos = score, col } Hsub[off] = score } } // Phase 4. (Optional) Backtrace to find character positions var pos []int j := f0 if !m.Without_positions { pos = make([]int, 0, M) i := M - 1 j = maxScorePos preferMatch := true for { I := i * width j0 := j - f0 s := H[I+j0] var s1, s2 int16 if i > 0 && j >= int(F[i]) { s1 = H[I-width+j0-1] } if j > int(F[i]) { s2 = H[I+j0-1] } if s > s1 && (s > s2 || s == s2 && preferMatch) { pos = append(pos, j+minIdx) if i == 0 { break } i-- } preferMatch = C[I+j0] > 1 || I+width+j0+1 < len(C) && C[I+width+j0+1] > 0 j-- } } return Result{Score: uint(maxScore), Positions: pos} } func (m *FuzzyMatcher) score(items []string, pattern string, scoring_func func(string, []rune, bool, *slab, func(string) Chars) Result) (ans []Result, err error) { if pattern == "" || len(items) < 1 { return make([]Result, len(items)), nil } as_chars := CharsFromString if m.Ignore_accents { pattern = string(CharsFromStringWithoutAccents(pattern).runes) as_chars = CharsFromStringWithoutAccents } pattern = norm.NFC.String(pattern) if !m.Case_sensitive { pattern = strings.ToLower(pattern) } pat := []rune(pattern) pattern_is_ascii := !slices.ContainsFunc(pat, func(r rune) bool { return r >= utf8.RuneSelf }) ans = make([]Result, len(items)) err = parallel.Run_in_parallel_over_range(0, func(start, end int) { s := slab{} for i := start; i < end; i++ { ans[i] = scoring_func(items[i], pat, pattern_is_ascii, &s, as_chars) } }, 0, len(items)) return }