Hello world
Release / Build and publish (push) Successful in 38s

This commit is contained in:
keb committed 2026-10-12 15:40:46 +02:00
commit edf6e79327
134 files changed
+27239

No files matched your search

+218
View File
@@ -0,0 +1,218 @@
// Package match filters and ranks lists the way fzf (and snacks.nvim's
// picker, a port of it) does: fzf's own scoring, so a match at the start
// of a word or in one run of characters beats letters scattered through a
// name, and fzf's extended search syntax:
//
// prod db both terms, in any order
// db | pw either
// 'prod exactly "prod", not fuzzy
// ^kv- -dev$ starts with / ends with
// !test without "test"
//
// ^ and $ anchor to an item's name, the part of it matches count most
// for (a secret's name rather than the path to it). Lowercase ignores case; an uppercase letter makes that term exact
// about case (smartcase). Results sort by score, then the shorter text,
// then their order in the list.
package match
import (
"slices"
"strings"
"github.com/junegunn/fzf/src/algo"
"github.com/junegunn/fzf/src/util"
)
func init() { algo.Init("default") }
// nameBonus is added to a term's score when all its letters are in the
// item's name rather than in its context (a path, a note), like snacks'
// filename_bonus: "pass" should find db-password before a secret that
// only lives in a vault called "passwords".
const nameBonus = 24
type kind int
const (
fuzzy kind = iota
exact
prefix
suffix
equal
)
type term struct {
kind kind
inverse bool
caseSensitive bool
text []rune
}
// Query is a parsed search: every group must match (AND), a group
// matches when any of its terms does (OR).
type Query struct {
groups [][]term
}
// Parse reads a search in fzf's extended syntax.
func Parse(q string) Query {
var out Query
or := false
for _, tok := range strings.Fields(q) {
if tok == "|" {
or = true
continue
}
t, ok := parseTerm(tok)
if !ok {
continue
}
if n := len(out.groups); or && n > 0 {
out.groups[n-1] = append(out.groups[n-1], t)
} else {
out.groups = append(out.groups, []term{t})
}
or = false
}
return out
}
func parseTerm(tok string) (term, bool) {
t := term{kind: fuzzy}
if s, ok := strings.CutPrefix(tok, "!"); ok {
t.inverse, t.kind, tok = true, exact, s
}
switch {
case strings.HasPrefix(tok, "'"):
t.kind, tok = exact, tok[1:]
case strings.HasPrefix(tok, "^"):
t.kind, tok = prefix, tok[1:]
}
if s, ok := strings.CutSuffix(tok, "$"); ok && s != "" {
if t.kind == prefix {
t.kind = equal
} else {
t.kind = suffix
}
tok = s
}
if tok == "" {
return term{}, false
}
t.caseSensitive = strings.ToLower(tok) != tok
if !t.caseSensitive {
tok = strings.ToLower(tok)
}
t.text = []rune(tok)
return t, true
}
// Empty reports whether the query has no terms: everything matches.
func (q Query) Empty() bool { return len(q.groups) == 0 }
// Item is a row to match: Text is everything searched, and runes
// NameStart up to NameEnd of it are the row's name (the rest is context,
// like a path or a note). Without a name, nothing gets the name bonus.
type Item struct {
Text string
NameStart, NameEnd int
}
// Hit is a matching item: its index in the list, score, and the rune
// positions of the matched letters in its Text.
type Hit struct {
Index int
Score int
Pos []int
}
// Rank returns the items matching q, best first.
func Rank(q Query, items []Item) []Hit {
var hits []Hit
for i, it := range items {
if h, ok := q.match(it); ok {
h.Index = i
hits = append(hits, h)
}
}
slices.SortStableFunc(hits, func(a, b Hit) int {
if a.Score != b.Score {
return b.Score - a.Score
}
return len(items[a.Index].Text) - len(items[b.Index].Text)
})
return hits
}
func (q Query) match(it Item) (Hit, bool) {
chars := util.ToChars([]byte(it.Text))
// ^ and $ anchor to the name: "vaults starting with kv-", not to the
// path before it.
name := chars
if it.NameEnd > 0 {
runes := []rune(it.Text)
name = util.RunesToChars(runes[it.NameStart:min(it.NameEnd, len(runes))])
}
var h Hit
for _, group := range q.groups {
best, matched := 0, false
var bestPos []int
for _, t := range group {
in, offset := &chars, 0
if t.kind == prefix || t.kind == suffix || t.kind == equal {
in, offset = &name, it.NameStart
}
score, pos, ok := t.match(in, offset, it.NameStart, it.NameEnd)
if ok && (!matched || score > best) {
best, bestPos, matched = score, pos, true
}
}
if !matched {
return Hit{}, false
}
h.Score += best
h.Pos = append(h.Pos, bestPos...)
}
slices.Sort(h.Pos)
h.Pos = slices.Compact(h.Pos)
return h, true
}
// match matches t in chars, whose first rune is at offset in the item's
// text: positions are in the text.
func (t term) match(chars *util.Chars, offset, nameStart, nameEnd int) (int, []int, bool) {
fn := algo.FuzzyMatchV2
switch t.kind {
case exact:
fn = algo.ExactMatchNaive
case prefix:
fn = algo.PrefixMatch
case suffix:
fn = algo.SuffixMatch
case equal:
fn = algo.EqualMatch
}
res, pos := fn(t.caseSensitive, false, true, chars, t.text, !t.inverse, nil)
found := res.Start >= 0
if t.inverse {
return 0, nil, !found
}
if !found {
return 0, nil, false
}
var p []int
if pos != nil {
for _, i := range *pos {
p = append(p, i+offset)
}
} else { // prefix, suffix and equal give a range
for i := res.Start; i < res.End; i++ {
p = append(p, i+offset)
}
}
score := res.Score
if len(p) > 0 && slices.Min(p) >= nameStart && slices.Max(p) < nameEnd {
score += nameBonus
}
return score, p, true
}
+107
View File
@@ -0,0 +1,107 @@
package match
import (
"slices"
"testing"
)
func names(items []Item, hits []Hit) []string {
var out []string
for _, h := range hits {
out = append(out, items[h.Index].Text)
}
return out
}
func plain(texts ...string) []Item {
var out []Item
for _, t := range texts {
out = append(out, Item{Text: t, NameEnd: len([]rune(t))})
}
return out
}
func TestRanking(t *testing.T) {
for _, c := range []struct {
query string
items []string
want []string // the order of the hits
}{
// A word that's there beats letters scattered through a name.
{"prod", []string{"p-r-o-d-ucer", "kv-app-prod"}, []string{"kv-app-prod", "p-r-o-d-ucer"}},
// At the start of a word beats inside one.
{"pass", []string{"bypass-token", "db-password"}, []string{"db-password", "bypass-token"}},
// Equal scores: the shorter first.
{"db", []string{"db-password-old-backup", "db-password"}, []string{"db-password", "db-password-old-backup"}},
// Terms in any order, all of them.
{"pass prod", []string{"kv-app-prod/db-password", "kv-app-dev/db-password", "kv-app-prod/api-key"}, []string{"kv-app-prod/db-password"}},
// Either.
{"pass | key", []string{"db-password", "api-key", "storage-account"}, []string{"db-password", "api-key"}},
// Exact, prefix, suffix, without.
{"'prod", []string{"p-r-o-d", "kv-prod"}, []string{"kv-prod"}},
{"^kv-", []string{"old-kv-app", "kv-app"}, []string{"kv-app"}},
{"-dev$", []string{"kv-dev-old", "kv-dev"}, []string{"kv-dev"}},
{"kv !dev", []string{"kv-dev", "kv-prod"}, []string{"kv-prod"}},
// Smartcase.
{"API", []string{"api-key", "API-KEY"}, []string{"API-KEY"}},
{"api", []string{"api-key", "API-KEY"}, []string{"api-key", "API-KEY"}},
} {
items := plain(c.items...)
if got := names(items, Rank(Parse(c.query), items)); !slices.Equal(got, c.want) {
t.Errorf("%q: %v, want %v", c.query, got, c.want)
}
}
}
func TestNameBeatsContext(t *testing.T) {
// "secret · note": a hit in the name beats the same hit in the note.
items := []Item{
{Text: "api-key the password for the api", NameEnd: len("api-key")},
{Text: "sub / vault / password", NameStart: len("sub / vault / "), NameEnd: len("sub / vault / password")},
}
got := names(items, Rank(Parse("password"), items))
if got[0] != "sub / vault / password" {
t.Errorf("%v: the name should win over a note", got)
}
}
func TestPositions(t *testing.T) {
items := plain("kv-app-prød")
hits := Rank(Parse("prød"), items)
if len(hits) != 1 || !slices.Equal(hits[0].Pos, []int{7, 8, 9, 10}) {
t.Errorf("%+v: want rune positions 7-10", hits)
}
hits = Rank(Parse("^kv"), items)
if len(hits) != 1 || !slices.Equal(hits[0].Pos, []int{0, 1}) {
t.Errorf("prefix: %+v", hits)
}
}
func TestAnchorsOnTheName(t *testing.T) {
path := func(p, name string) Item {
return Item{Text: p + name + " · ops", NameStart: len([]rune(p)), NameEnd: len([]rune(p + name))}
}
items := []Item{
path("󰠅 platform-prod / ", "kv-app-prod"),
path("󰠅 platform-prod / kv-app-prod / ", "db-password"),
path("󰠅 platform-dev / ", "kv-app-dev"),
}
got := names(items, Rank(Parse("^kv- !dev"), items))
if len(got) != 1 || got[0] != items[0].Text {
t.Errorf("^kv- !dev: %v, want just the kv-app-prod vault", got)
}
hits := Rank(Parse("^kv"), items[:1])
start := len([]rune("󰠅 platform-prod / "))
if len(hits) != 1 || !slices.Equal(hits[0].Pos, []int{start, start + 1}) {
t.Errorf("^kv positions %v, want %d-%d", hits, start, start+1)
}
if got := names(items, Rank(Parse("password$"), items)); len(got) != 1 || got[0] != items[1].Text {
t.Errorf("password$: %v", got)
}
}
func TestEmpty(t *testing.T) {
if !Parse(" | ").Empty() || Parse("x").Empty() {
t.Error("Empty")
}
}