use fzf for autocomplete
This commit is contained in:
@@ -1,122 +1,60 @@
|
||||
package autocomplete
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"cmp"
|
||||
"slices"
|
||||
"strings"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
"sync"
|
||||
|
||||
"github.com/junegunn/fzf/src/algo"
|
||||
"github.com/junegunn/fzf/src/util"
|
||||
)
|
||||
|
||||
// Set is an autocomplete set.
|
||||
type Set struct {
|
||||
mask []uint64
|
||||
keys [][]string
|
||||
vals [][]string
|
||||
}
|
||||
|
||||
var (
|
||||
test [2]uint64
|
||||
idxs [256]byte
|
||||
)
|
||||
|
||||
const careset = " 0123456789abcdefghijklmnopqrstuvwxyz.!'"
|
||||
|
||||
// Certify the size of careset at compile time.
|
||||
// It must be no larger than 64.
|
||||
var _ [0]struct{} = [len(careset) - 40]struct{}{}
|
||||
|
||||
func init() {
|
||||
// Construct the test and idxs maps.
|
||||
for i, c := range []byte(careset) {
|
||||
if c < 64 {
|
||||
test[0] |= 1 << c
|
||||
} else {
|
||||
test[1] |= 1 << (c - 64)
|
||||
}
|
||||
idxs[c] = byte(i)
|
||||
}
|
||||
}
|
||||
|
||||
func normalize(s string) string {
|
||||
s = strings.Map(func(c rune) rune {
|
||||
c = unicode.ToLower(c)
|
||||
// TODO(zeph): map latin letters with diacritics to their not-diacritic
|
||||
// characters; e.g. 2* pasta's unique Corazón ☆ Ardiente should have
|
||||
// ó mapped to o. for now, we just special-case it.
|
||||
if c == 'ó' {
|
||||
c = 'o'
|
||||
}
|
||||
return c
|
||||
}, s)
|
||||
return s
|
||||
}
|
||||
|
||||
func filter(s string) (r uint64) {
|
||||
for _, c := range s {
|
||||
if c > 0x7f {
|
||||
// Skip non-ASCII characters.
|
||||
continue
|
||||
}
|
||||
if test[byte(c)/64]>>(byte(c)%64)&1 == 0 {
|
||||
// Not in the care set.
|
||||
continue
|
||||
}
|
||||
r |= 1 << idxs[byte(c)]
|
||||
}
|
||||
return r
|
||||
type Set[V any] struct {
|
||||
keys []util.Chars
|
||||
vals []V
|
||||
}
|
||||
|
||||
// Add associates a value with a key in the autocomplete set.
|
||||
// The behavior is undefined if the key already has a value.
|
||||
func (s *Set) Add(key, val string) {
|
||||
key = normalize(key)
|
||||
m := filter(key)
|
||||
i, ok := slices.BinarySearch(s.mask, m)
|
||||
if !ok {
|
||||
s.mask = slices.Insert(s.mask, i, m)
|
||||
s.keys = slices.Insert(s.keys, i, nil)
|
||||
s.vals = slices.Insert(s.vals, i, nil)
|
||||
func (s *Set[V]) Add(key string, val V) {
|
||||
k := util.ToChars([]byte(key))
|
||||
i, ok := slices.BinarySearchFunc(s.keys, k, func(a, b util.Chars) int {
|
||||
return bytes.Compare(a.Bytes(), b.Bytes())
|
||||
})
|
||||
if ok {
|
||||
s.vals[i] = val
|
||||
return
|
||||
}
|
||||
j, _ := slices.BinarySearch(s.keys[i], key)
|
||||
s.keys[i] = slices.Insert(s.keys[i], j, key)
|
||||
s.vals[i] = slices.Insert(s.vals[i], j, val)
|
||||
s.keys = slices.Insert(s.keys, i, k)
|
||||
s.vals = slices.Insert(s.vals, i, val)
|
||||
}
|
||||
|
||||
// Find appends to r all values in the set with keys that key matches.
|
||||
func (s *Set) Find(r []string, key string) []string {
|
||||
key = normalize(key)
|
||||
m := filter(key)
|
||||
for i, v := range s.mask {
|
||||
if m&v != m {
|
||||
func (s *Set[V]) Find(r []V, key string) []V {
|
||||
initFzf()
|
||||
var (
|
||||
p = []rune(key)
|
||||
|
||||
got []V
|
||||
t []algo.Result
|
||||
slab util.Slab
|
||||
)
|
||||
for i := range s.keys {
|
||||
res, _ := algo.FuzzyMatchV2(false, true, true, &s.keys[i], p, false, &slab)
|
||||
if res.Score <= 0 {
|
||||
continue
|
||||
}
|
||||
for j, k := range s.keys[i] {
|
||||
if inorder(key, k) {
|
||||
r = append(r, s.vals[i][j])
|
||||
}
|
||||
j, _ := slices.BinarySearchFunc(t, res, func(a, b algo.Result) int { return -cmp.Compare(a.Score, b.Score) })
|
||||
// Insert after all other matches with the same score for stability.
|
||||
for j < len(t) && t[j].Score == res.Score {
|
||||
j++
|
||||
}
|
||||
t = slices.Insert(t, j, res)
|
||||
got = slices.Insert(got, j, s.vals[i])
|
||||
}
|
||||
return r
|
||||
return append(r, got...)
|
||||
}
|
||||
|
||||
// inorder checks whether each character in a appears in the same relative order in b.
|
||||
func inorder(a, b string) bool {
|
||||
for _, c := range a {
|
||||
k := strings.IndexRune(b, c)
|
||||
if k < 0 {
|
||||
return false
|
||||
}
|
||||
_, l := utf8.DecodeRuneInString(b[k:])
|
||||
b = b[k+l:]
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// Metrics gets the number of buckets in the autocomplete set and the length
|
||||
// of the longest bucket.
|
||||
func (s *Set) Metrics() (buckets, longest int) {
|
||||
for _, b := range s.keys {
|
||||
longest = max(longest, len(b))
|
||||
}
|
||||
return len(s.keys), longest
|
||||
}
|
||||
var initFzf = sync.OnceFunc(func() { algo.Init("default") })
|
||||
|
||||
@@ -40,6 +40,12 @@ func TestAutocomplete(t *testing.T) {
|
||||
search: "o",
|
||||
want: these("bocchi", "ryo"),
|
||||
},
|
||||
{
|
||||
name: "unrelated",
|
||||
add: these("bocchi", "ryo", "nijika", "kita"),
|
||||
search: "x",
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
name: "map",
|
||||
add: these("Corazón ☆ Ardiente"),
|
||||
@@ -49,7 +55,7 @@ func TestAutocomplete(t *testing.T) {
|
||||
}
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
var set autocomplete.Set
|
||||
var set autocomplete.Set[string]
|
||||
for _, s := range c.add {
|
||||
set.Add(s, s)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user