Work on implementing the fzf algorithm for the choose files kitten

People are more used to that, and it is more optimized for use case of
finding files.
This commit is contained in:
Kovid Goyal
2025-06-06 12:29:35 +05:30
parent 3059c87bd0
commit ed45e1354b
6 changed files with 816 additions and 4 deletions

374
tools/fzf/algo.go Normal file
View File

@@ -0,0 +1,374 @@
package fzf
import (
"bytes"
"fmt"
"slices"
"strings"
"unicode"
"unicode/utf8"
"github.com/kovidgoyal/kitty/tools/utils"
"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 = utils.Run_in_parallel_over_range(0, func(start, end int) error {
s := slab{}
for i := start; i < end; i++ {
ans[i] = scoring_func(items[i], pat, pattern_is_ascii, &s, as_chars)
}
return nil
}, 0, len(items))
return
}

90
tools/fzf/algo_test.go Normal file
View File

@@ -0,0 +1,90 @@
package fzf
import (
"fmt"
"sort"
"testing"
)
var _ = fmt.Print
func assertMatch(t *testing.T, m *FuzzyMatcher, item string, query string, start, end, score int) {
r, err := m.Score([]string{item}, query)
if err != nil {
t.Fatal(err)
}
if r[0].Score != uint(score) {
t.Fatalf("Score of %#v in %#v is %d instead of %d", query, item, r[0].Score, score)
}
if start > -1 && end > -1 {
p := r[0].Positions
sort.Ints(p)
if len(p) < 1 {
t.Fatalf("Got no positions for %#v in %#v", query, item)
}
if p[0] != start {
t.Fatalf("First char of %#v in %#v at %d instead of %d", query, item, p[0], start)
}
if p[len(p)-1]+1 != end {
t.Fatalf("Last char of %#v in %#v at %d instead of %d", query, item, p[len(p)-1], end-1)
}
}
}
func TestFZFAlgo(t *testing.T) {
fn := NewFuzzyMatcher(DEFAULT_SCHEME)
for _, forward := range []bool{true, false} {
fn.Backwards = !forward
fn.Case_sensitive = false
assertMatch(t, fn, "fooBarbaz1", "oBZ", 2, 9,
scoreMatch*3+bonusCamel123+scoreGapStart+scoreGapExtension*3)
assertMatch(t, fn, "foo bar baz", "fbb", 0, 9,
scoreMatch*3+int(fn.bonusBoundaryWhite)*bonusFirstCharMultiplier+
int(fn.bonusBoundaryWhite)*2+2*scoreGapStart+4*scoreGapExtension)
assertMatch(t, fn, "/AutomatorDocument.icns", "rdoc", 9, 13,
scoreMatch*4+bonusCamel123+bonusConsecutive*2)
assertMatch(t, fn, "/man1/zshcompctl.1", "zshc", 6, 10,
scoreMatch*4+int(fn.bonusBoundaryDelimiter)*bonusFirstCharMultiplier+int(fn.bonusBoundaryDelimiter)*3)
assertMatch(t, fn, "/.oh-my-zsh/cache", "zshc", 8, 13,
scoreMatch*4+bonusBoundary*bonusFirstCharMultiplier+bonusBoundary*2+scoreGapStart+int(fn.bonusBoundaryDelimiter))
assertMatch(t, fn, "ab0123 456", "12356", 3, 10,
scoreMatch*5+bonusConsecutive*3+scoreGapStart+scoreGapExtension)
assertMatch(t, fn, "abc123 456", "12356", 3, 10,
scoreMatch*5+bonusCamel123*bonusFirstCharMultiplier+bonusCamel123*2+bonusConsecutive+scoreGapStart+scoreGapExtension)
assertMatch(t, fn, "foo/bar/baz", "fbb", 0, 9,
scoreMatch*3+int(fn.bonusBoundaryWhite)*bonusFirstCharMultiplier+
int(fn.bonusBoundaryDelimiter)*2+2*scoreGapStart+4*scoreGapExtension)
assertMatch(t, fn, "fooBarBaz", "fbb", 0, 7,
scoreMatch*3+int(fn.bonusBoundaryWhite)*bonusFirstCharMultiplier+
bonusCamel123*2+2*scoreGapStart+2*scoreGapExtension)
assertMatch(t, fn, "foo barbaz", "fbb", 0, 8,
scoreMatch*3+int(fn.bonusBoundaryWhite)*bonusFirstCharMultiplier+int(fn.bonusBoundaryWhite)+
scoreGapStart*2+scoreGapExtension*3)
assertMatch(t, fn, "fooBar Baz", "foob", 0, 4,
scoreMatch*4+int(fn.bonusBoundaryWhite)*bonusFirstCharMultiplier+int(fn.bonusBoundaryWhite)*3)
assertMatch(t, fn, "xFoo-Bar Baz", "foo-b", 1, 6,
scoreMatch*5+bonusCamel123*bonusFirstCharMultiplier+bonusCamel123*2+
bonusNonWord+bonusBoundary)
fn.Case_sensitive = true
assertMatch(t, fn, "fooBarbaz", "oBz", 2, 9,
scoreMatch*3+bonusCamel123+scoreGapStart+scoreGapExtension*3)
assertMatch(t, fn, "Foo/Bar/Baz", "FBB", 0, 9,
scoreMatch*3+int(fn.bonusBoundaryWhite)*bonusFirstCharMultiplier+int(fn.bonusBoundaryDelimiter)*2+
scoreGapStart*2+scoreGapExtension*4)
assertMatch(t, fn, "FooBarBaz", "FBB", 0, 7,
scoreMatch*3+int(fn.bonusBoundaryWhite)*bonusFirstCharMultiplier+bonusCamel123*2+
scoreGapStart*2+scoreGapExtension*2)
assertMatch(t, fn, "FooBar Baz", "FooB", 0, 4,
scoreMatch*4+int(fn.bonusBoundaryWhite)*bonusFirstCharMultiplier+int(fn.bonusBoundaryWhite)*2+
max(bonusCamel123, int(fn.bonusBoundaryWhite)))
// Consecutive bonus updated
assertMatch(t, fn, "foo-bar", "o-ba", 2, 6, scoreMatch*4+bonusBoundary*3)
// Non-match
assertMatch(t, fn, "fooBarbaz", "oBZ", -1, -1, 0)
assertMatch(t, fn, "Foo Bar Baz", "fbb", -1, -1, 0)
assertMatch(t, fn, "fooBarbaz", "fooBarbazz", -1, -1, 0)
}
}

48
tools/fzf/api.go Normal file
View File

@@ -0,0 +1,48 @@
package fzf
import (
"fmt"
)
var _ = fmt.Print
func NewFuzzyMatcher(scheme Scheme) (ans *FuzzyMatcher) {
return new_fuzzy_matcher(scheme)
}
// Score the specified items using runtime.NumCPU() go routines. This function
// reports a panic in any worker go routine as a regular error.
func (m *FuzzyMatcher) Score(items []string, pattern string) (ans []Result, err error) {
return m.score(items, pattern, func(item string, pat []rune, pattern_is_ascii bool, slab *slab, as_chars func(string) Chars) Result {
c := as_chars(item)
return m.score_one(&c, pat, pattern_is_ascii, slab)
})
}
// Clear the cache used ScoreWithCache(). Useful if you change some of the
// settings used for scoring.
func (m *FuzzyMatcher) ClearScoreCache() {
m.cache_mutex.Lock()
m.cache = make(map[string]Result)
m.cache_mutex.Unlock()
}
// Same as Score, except that it uses a cache. Remember to call
// ClearScoreCache() if you change any scoring settings on this FuzzyMatcher.
func (m *FuzzyMatcher) ScoreWithCache(items []string, pattern string) (ans []Result, err error) {
key_prefix := pattern + "\x00"
return m.score(items, pattern, func(item string, pat []rune, pattern_is_ascii bool, slab *slab, as_chars func(string) Chars) Result {
key := key_prefix + item
m.cache_mutex.Lock()
res, found := m.cache[key]
m.cache_mutex.Unlock()
if !found {
c := as_chars(item)
res = m.score_one(&c, pat, pattern_is_ascii, slab)
m.cache_mutex.Lock()
m.cache[key] = res
m.cache_mutex.Unlock()
}
return res
})
}

301
tools/fzf/types.go Normal file
View File

@@ -0,0 +1,301 @@
package fzf
import (
"fmt"
"os"
"strings"
"sync"
"unicode"
"unicode/utf8"
"unsafe"
"github.com/kovidgoyal/kitty/tools/utils"
"golang.org/x/text/unicode/norm"
)
var _ = fmt.Print
type Chars struct {
bytes []byte
runes []rune
}
const (
overflow64 uint64 = 0x8080808080808080
overflow32 uint32 = 0x80808080
)
func check_ascii(bytes []byte) (ascii_until int) {
i := 0
for ; i <= len(bytes)-8; i += 8 {
if (overflow64 & *(*uint64)(unsafe.Pointer(&bytes[i]))) > 0 {
return i
}
}
for ; i <= len(bytes)-4; i += 4 {
if (overflow32 & *(*uint32)(unsafe.Pointer(&bytes[i]))) > 0 {
return i
}
}
for ; i < len(bytes); i++ {
if bytes[i] >= utf8.RuneSelf {
return i
}
}
return -1
}
func CharsFromString(text string) (ans Chars) {
ans.bytes = utils.UnsafeStringToBytes(text)
ascii_until := check_ascii(ans.bytes)
if ascii_until > -1 {
runes := []rune(norm.NFC.String(text[ascii_until:]))
ans.runes = make([]rune, ascii_until+len(runes))
for i := range ascii_until {
ans.runes[i] = rune(ans.bytes[i])
}
copy(ans.runes[ascii_until:], runes)
}
return
}
func CharsFromStringWithoutAccents(text string) (ans Chars) {
ans.bytes = utils.UnsafeStringToBytes(text)
ascii_until := check_ascii(ans.bytes)
if ascii_until > -1 {
runes := []rune(norm.NFD.String(text[ascii_until:]))
ans.runes = make([]rune, ascii_until, ascii_until+len(runes))
for i := range ascii_until {
ans.runes[i] = rune(ans.bytes[i])
}
for _, r := range runes {
if !unicode.Is(unicode.Mn, r) {
ans.runes = append(ans.runes, r)
}
}
}
return
}
func (c *Chars) Bytes() []byte { return c.bytes }
func (c *Chars) Is_ASCII() bool { return c.runes == nil }
func (c *Chars) Get(i int) rune {
if c.runes != nil {
return c.runes[i]
}
return rune(c.bytes[i])
}
func (c *Chars) Length() int {
if c.runes != nil {
return len(c.runes)
}
return len(c.bytes)
}
func (c *Chars) CopyRunes(dest []rune, from int) {
if c.runes != nil {
copy(dest, c.runes[from:])
return
}
for idx, b := range c.bytes[from:][:len(dest)] {
dest[idx] = rune(b)
}
}
type charClass int
const (
charWhite charClass = iota
charNonWord
charDelimiter
charLower
charUpper
charLetter
charNumber
)
const (
scoreMatch = 16
scoreGapStart = -3
scoreGapExtension = -1
// We prefer matches at the beginning of a word, but the bonus should not be
// too great to prevent the longer acronym matches from always winning over
// shorter fuzzy matches. The bonus point here was specifically chosen that
// the bonus is cancelled when the gap between the acronyms grows over
// 8 characters, which is approximately the average length of the words found
// in web2 dictionary and my file system.
bonusBoundary = scoreMatch / 2
// Although bonus point for non-word characters is non-contextual, we need it
// for computing bonus points for consecutive chunks starting with a non-word
// character.
bonusNonWord = scoreMatch / 2
// Edge-triggered bonus for matches in camelCase words.
// Compared to word-boundary case, they don't accompany single-character gaps
// (e.g. FooBar vs. foo-bar), so we deduct bonus point accordingly.
bonusCamel123 = bonusBoundary + scoreGapExtension
// Minimum bonus point given to characters in consecutive chunks.
// Note that bonus points for consecutive matches shouldn't have needed if we
// used fixed match score as in the original algorithm.
bonusConsecutive = -(scoreGapStart + scoreGapExtension)
// The first character in the typed pattern usually has more significance
// than the rest so it's important that it appears at special positions where
// bonus points are given, e.g. "to-go" vs. "ongoing" on "og" or on "ogo".
// The amount of the extra bonus should be limited so that the gap penalty is
// still respected.
bonusFirstCharMultiplier = 2
)
const whiteChars = " \t\n\v\f\r\x85\xA0"
type Result struct {
Score uint // A value of zero means did not match
Positions []int
}
type FuzzyMatcher struct {
Case_sensitive, Ignore_accents, Backwards, Without_positions bool
// Extra bonus for word boundary after whitespace character or beginning of the string
bonusBoundaryWhite int16
// Extra bonus for word boundary after slash, colon, semi-colon, and comma
bonusBoundaryDelimiter int16
initialCharClass charClass
// A minor optimization that can give 15%+ performance boost
asciiCharClasses [unicode.MaxASCII + 1]charClass
// A minor optimization that can give yet another 5% performance boost
bonusMatrix [charNumber + 1][charNumber + 1]int16
delimiterChars string
cache map[string]Result
cache_mutex sync.Mutex
}
func (m *FuzzyMatcher) bonusFor(prevClass charClass, class charClass) int16 {
if class > charNonWord {
switch prevClass {
case charWhite:
// Word boundary after whitespace
return m.bonusBoundaryWhite
case charDelimiter:
// Word boundary after a delimiter character
return m.bonusBoundaryDelimiter
case charNonWord:
// Word boundary
return bonusBoundary
}
}
if prevClass == charLower && class == charUpper ||
prevClass != charNumber && class == charNumber {
// camelCase letter123
return bonusCamel123
}
switch class {
case charNonWord, charDelimiter:
return bonusNonWord
case charWhite:
return m.bonusBoundaryWhite
}
return 0
}
type Scheme string
const (
DEFAULT_SCHEME Scheme = "default"
PATH_SCHEME Scheme = "path"
HISTORY_SCHEME Scheme = "history"
)
func new_fuzzy_matcher(scheme Scheme) (ans *FuzzyMatcher) {
ans = &FuzzyMatcher{
bonusBoundaryWhite: bonusBoundary + 2,
bonusBoundaryDelimiter: bonusBoundary + 1,
delimiterChars: "/,:;|",
cache: make(map[string]Result),
}
switch scheme {
case PATH_SCHEME:
ans.bonusBoundaryWhite = bonusBoundary
ans.initialCharClass = charDelimiter
if os.PathSeparator == '/' {
ans.delimiterChars = "/"
} else {
ans.delimiterChars = "/" + string(os.PathSeparator)
}
case HISTORY_SCHEME:
ans.bonusBoundaryWhite = bonusBoundary
ans.bonusBoundaryDelimiter = bonusBoundary
}
for i := 0; i <= unicode.MaxASCII; i++ {
char := rune(i)
c := charNonWord
if char >= 'a' && char <= 'z' {
c = charLower
} else if char >= 'A' && char <= 'Z' {
c = charUpper
} else if char >= '0' && char <= '9' {
c = charNumber
} else if strings.ContainsRune(whiteChars, char) {
c = charWhite
} else if strings.ContainsRune(ans.delimiterChars, char) {
c = charDelimiter
}
ans.asciiCharClasses[i] = c
}
for i := 0; i <= int(charNumber); i++ {
for j := 0; j <= int(charNumber); j++ {
ans.bonusMatrix[i][j] = ans.bonusFor(charClass(i), charClass(j))
}
}
return
}
type slab struct {
i16 []int16
i32 []int32
i16_used, i32_used int
}
const slab_initial_size = 8192
func (s *slab) reset() {
if s.i16 == nil {
s.i16 = make([]int16, slab_initial_size)
}
if s.i32 == nil {
s.i32 = make([]int32, slab_initial_size)
}
s.i16_used, s.i32_used = 0, 0
}
func (s *slab) alloc16(sz int) []int16 {
if sz+s.i16_used < len(s.i16) {
s.i16 = make([]int16, max(slab_initial_size, 2*(s.i16_used+sz)))
s.i16_used = 0
}
pos := s.i16_used
s.i16_used += sz
return s.i16[pos:s.i16_used]
}
func (s *slab) alloc32(sz int) []int32 {
if sz+s.i32_used < len(s.i32) {
s.i32 = make([]int32, max(slab_initial_size, 2*(s.i32_used+sz)))
s.i32_used = 0
}
pos := s.i32_used
s.i32_used += sz
return s.i32[pos:s.i32_used]
}