e22e040688
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.
113 lines
2.1 KiB
Go
113 lines
2.1 KiB
Go
package tui
|
||
|
||
import (
|
||
"strings"
|
||
|
||
"github.com/charmbracelet/lipgloss"
|
||
"github.com/lerko/nib/internal/db"
|
||
)
|
||
|
||
const maxSuggestions = 5
|
||
|
||
type autocompleteModel struct {
|
||
suggestions []string
|
||
cursor int
|
||
active bool
|
||
prefix string
|
||
tokenStart int
|
||
tokenEnd int
|
||
}
|
||
|
||
func (a *autocompleteModel) moveUp() {
|
||
if a.cursor > 0 {
|
||
a.cursor--
|
||
}
|
||
}
|
||
|
||
func (a *autocompleteModel) moveDown() {
|
||
if a.cursor < len(a.suggestions)-1 {
|
||
a.cursor++
|
||
}
|
||
}
|
||
|
||
func (a autocompleteModel) selected() string {
|
||
if len(a.suggestions) == 0 || a.cursor >= len(a.suggestions) {
|
||
return ""
|
||
}
|
||
return a.suggestions[a.cursor]
|
||
}
|
||
|
||
func (a autocompleteModel) visibleCount() int {
|
||
if len(a.suggestions) > maxSuggestions {
|
||
return maxSuggestions
|
||
}
|
||
return len(a.suggestions)
|
||
}
|
||
|
||
func (a autocompleteModel) view(width int) string {
|
||
if !a.active || len(a.suggestions) == 0 {
|
||
return ""
|
||
}
|
||
|
||
var b strings.Builder
|
||
n := a.visibleCount()
|
||
for i := 0; i < n; i++ {
|
||
tag := "#" + a.suggestions[i]
|
||
if i == a.cursor {
|
||
b.WriteString(acSelectedStyle.Render(" › " + tag))
|
||
} else {
|
||
b.WriteString(acItemStyle.Render(" " + tag))
|
||
}
|
||
if i < n-1 {
|
||
b.WriteString("\n")
|
||
}
|
||
}
|
||
if len(a.suggestions) > maxSuggestions {
|
||
b.WriteString("\n")
|
||
b.WriteString(acItemStyle.Render(" …"))
|
||
}
|
||
|
||
box := lipgloss.NewStyle().
|
||
Width(min(30, width)).
|
||
Render(b.String())
|
||
return box
|
||
}
|
||
|
||
func tagTokenAtCursor(val string, cursorPos int) (tokenStart, tokenEnd int, prefix string, ok bool) {
|
||
if cursorPos > len(val) {
|
||
cursorPos = len(val)
|
||
}
|
||
|
||
start := cursorPos
|
||
for start > 0 && val[start-1] != ' ' {
|
||
start--
|
||
}
|
||
|
||
if start >= len(val) || val[start] != '#' {
|
||
return 0, 0, "", false
|
||
}
|
||
|
||
end := cursorPos
|
||
for end < len(val) && val[end] != ' ' {
|
||
end++
|
||
}
|
||
|
||
prefix = strings.ToLower(val[start+1 : cursorPos])
|
||
return start, end, prefix, true
|
||
}
|
||
|
||
func filterTagSuggestions(tags []db.TagCount, prefix string) []string {
|
||
if prefix == "" {
|
||
return nil
|
||
}
|
||
prefix = strings.ToLower(prefix)
|
||
var result []string
|
||
for _, tc := range tags {
|
||
lower := strings.ToLower(tc.Tag)
|
||
if strings.HasPrefix(lower, prefix) && lower != prefix {
|
||
result = append(result, tc.Tag)
|
||
}
|
||
}
|
||
return result
|
||
}
|