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
+61
View File
@@ -158,6 +158,67 @@ func TestParse(t *testing.T) {
}
}
func TestParseQueryComposition(t *testing.T) {
tests := []struct {
name string
input string
wantBody string
wantTags []string
wantDateFrom bool
wantDateTo bool
wantCardType *string
}{
{"today", "?@today", "", nil, true, true, nil},
{"yesterday", "?@yesterday", "", nil, true, true, nil},
{"week", "?@week", "", nil, true, false, nil},
{"month", "?@month", "", nil, true, false, nil},
{"newer than", "?<7d", "", nil, true, false, nil},
{"older than", "?>30d", "", nil, false, true, nil},
{"card type snippet", "?^snippet", "", nil, false, false, sp("snippet")},
{"card type shorthand", "?^c", "", nil, false, false, sp("snippet")},
{"card type checklist", "?^checklist", "", nil, false, false, sp("checklist")},
{"invalid card type stays as body", "?^bogus", "^bogus", nil, false, false, nil},
{"combined text and date", "?deploy @today", "deploy", nil, true, true, nil},
{"combined tags and date", "?#ops @week", "", []string{"ops"}, true, false, nil},
{"combined all", "?deploy #ops @week ^snippet", "deploy", []string{"ops"}, true, false, sp("snippet")},
{"invalid age stays as body", "?>abcd", ">abcd", nil, false, false, nil},
{"zero days stays as body", "?>0d", ">0d", nil, false, false, nil},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := Parse(tt.input)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !got.Query {
t.Fatal("expected Query=true")
}
if got.Body != tt.wantBody {
t.Errorf("body: got %q, want %q", got.Body, tt.wantBody)
}
if !tagsEq(got.FilterTags, tt.wantTags) {
t.Errorf("tags: got %v, want %v", got.FilterTags, tt.wantTags)
}
if tt.wantDateFrom && got.QueryDateFrom == nil {
t.Error("expected QueryDateFrom to be set")
}
if !tt.wantDateFrom && got.QueryDateFrom != nil {
t.Errorf("expected QueryDateFrom nil, got %v", *got.QueryDateFrom)
}
if tt.wantDateTo && got.QueryDateTo == nil {
t.Error("expected QueryDateTo to be set")
}
if !tt.wantDateTo && got.QueryDateTo != nil {
t.Errorf("expected QueryDateTo nil, got %v", *got.QueryDateTo)
}
if !ptrEq(got.QueryCardType, tt.wantCardType) {
t.Errorf("card type: got %v, want %v", strPtr(got.QueryCardType), strPtr(tt.wantCardType))
}
})
}
}
func ptrEq(a, b *string) bool {
if a == nil && b == nil {
return true