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:
@@ -0,0 +1,112 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user