feat(tui): add tag autocomplete and query composition
CI / test (pull_request) Successful in 2m31s

Tag autocomplete shows suggestions when typing #partial in capture bar.
Tab/enter accepts, up/down navigates, esc dismisses.

Query composition extends ? search with date filters (@today, @week,
@month, <7d, >30d), card type filters (^snippet), all composable
with existing text and tag filters.
This commit is contained in:
2026-05-21 12:12:07 -04:00
parent 29bd7d3dc6
commit e22e040688
8 changed files with 485 additions and 38 deletions
+85
View File
@@ -0,0 +1,85 @@
package tui
import (
"testing"
"github.com/lerko/nib/internal/db"
)
func TestTagTokenAtCursor(t *testing.T) {
tests := []struct {
name string
val string
cursor int
wantStart int
wantEnd int
wantPfx string
wantOk bool
}{
{"mid tag cursor after a", "hello #par world", 9, 6, 10, "pa", true},
{"end of tag", "hello #par world", 10, 6, 10, "par", true},
{"end of input", "hello #parenting", 16, 6, 16, "parenting", true},
{"start of tag just hash", "hello # world", 7, 6, 7, "", true},
{"not in tag", "hello world", 5, 0, 0, "", false},
{"tag at start", "#ops stuff", 4, 0, 4, "ops", true},
{"cursor at hash", "#ops", 1, 0, 4, "", true},
{"multiple tags second", "hello #ops #inf", 15, 11, 15, "inf", true},
{"empty string", "", 0, 0, 0, "", false},
{"cursor past end", "#ops", 10, 0, 4, "ops", true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
start, end, pfx, ok := tagTokenAtCursor(tt.val, tt.cursor)
if ok != tt.wantOk {
t.Fatalf("ok = %v, want %v", ok, tt.wantOk)
}
if !ok {
return
}
if start != tt.wantStart || end != tt.wantEnd {
t.Fatalf("range = [%d,%d), want [%d,%d)", start, end, tt.wantStart, tt.wantEnd)
}
if pfx != tt.wantPfx {
t.Fatalf("prefix = %q, want %q", pfx, tt.wantPfx)
}
})
}
}
func TestFilterTagSuggestions(t *testing.T) {
tags := []db.TagCount{
{Tag: "ops", Count: 5},
{Tag: "ops-deploy", Count: 3},
{Tag: "infra", Count: 2},
{Tag: "ops-team", Count: 1},
}
tests := []struct {
name string
prefix string
want []string
}{
{"empty prefix", "", nil},
{"exact match excluded", "ops", []string{"ops-deploy", "ops-team"}},
{"partial match", "op", []string{"ops", "ops-deploy", "ops-team"}},
{"no match", "zzz", nil},
{"case insensitive", "OP", []string{"ops", "ops-deploy", "ops-team"}},
{"single match", "inf", []string{"infra"}},
{"full match excluded", "infra", nil},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := filterTagSuggestions(tags, tt.prefix)
if len(got) != len(tt.want) {
t.Fatalf("got %v, want %v", got, tt.want)
}
for i := range got {
if got[i] != tt.want[i] {
t.Fatalf("got %v, want %v", got, tt.want)
}
}
})
}
}