cmd/horsebot: move to here
This commit is contained in:
56
cmd/horsebot/autocomplete/autocomplete.go
Normal file
56
cmd/horsebot/autocomplete/autocomplete.go
Normal file
@@ -0,0 +1,56 @@
|
||||
package autocomplete
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"cmp"
|
||||
"slices"
|
||||
"sync"
|
||||
|
||||
"github.com/junegunn/fzf/src/algo"
|
||||
"github.com/junegunn/fzf/src/util"
|
||||
)
|
||||
|
||||
// Set is an autocomplete set.
|
||||
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[V]) Add(key string, val V) {
|
||||
k := util.ToChars([]byte(key))
|
||||
i, _ := slices.BinarySearchFunc(s.keys, k, func(a, b util.Chars) int {
|
||||
return bytes.Compare(a.Bytes(), b.Bytes())
|
||||
})
|
||||
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[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
|
||||
}
|
||||
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 append(r, got...)
|
||||
}
|
||||
|
||||
var initFzf = sync.OnceFunc(func() { algo.Init("default") })
|
||||
70
cmd/horsebot/autocomplete/autocomplete_test.go
Normal file
70
cmd/horsebot/autocomplete/autocomplete_test.go
Normal file
@@ -0,0 +1,70 @@
|
||||
package autocomplete_test
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"testing"
|
||||
|
||||
"git.sunturtle.xyz/zephyr/horse/cmd/horsebot/autocomplete"
|
||||
)
|
||||
|
||||
func these(s ...string) []string { return s }
|
||||
|
||||
func TestAutocomplete(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
add []string
|
||||
search string
|
||||
want []string
|
||||
}{
|
||||
{
|
||||
name: "empty",
|
||||
add: nil,
|
||||
search: "",
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
name: "exact",
|
||||
add: these("bocchi"),
|
||||
search: "bocchi",
|
||||
want: these("bocchi"),
|
||||
},
|
||||
{
|
||||
name: "extra",
|
||||
add: these("bocchi", "ryo", "nijika", "kita"),
|
||||
search: "bocchi",
|
||||
want: these("bocchi"),
|
||||
},
|
||||
{
|
||||
name: "short",
|
||||
add: these("bocchi", "ryo", "nijika", "kita"),
|
||||
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"),
|
||||
search: "corazo",
|
||||
want: these("Corazón ☆ Ardiente"),
|
||||
},
|
||||
}
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
var set autocomplete.Set[string]
|
||||
for _, s := range c.add {
|
||||
set.Add(s, s)
|
||||
}
|
||||
got := set.Find(nil, c.search)
|
||||
slices.Sort(c.want)
|
||||
slices.Sort(got)
|
||||
if !slices.Equal(c.want, got) {
|
||||
t.Errorf("wrong results: want %q, got %q", c.want, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user