Add proper fuzzy matching for time tags

This commit is contained in:
Martin Pander
2026-02-02 15:54:39 +01:00
parent 938ed177f1
commit f5d297e6ab
2 changed files with 74 additions and 16 deletions

View File

@ -1,18 +1,18 @@
package autocomplete
import (
"strings"
"github.com/charmbracelet/bubbles/key"
"github.com/charmbracelet/bubbles/textinput"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/sahilm/fuzzy"
)
type Autocomplete struct {
input textinput.Model
allSuggestions []string // All available suggestions (newest first)
filteredSuggestions []string // Currently matching suggestions
matchedIndexes [][]int // Matched character positions for each suggestion
selectedIndex int // -1 = input focused, 0+ = suggestion selected
showSuggestions bool // Whether to display suggestion box
maxVisible int // Max suggestions to show
@ -23,7 +23,7 @@ type Autocomplete struct {
}
// New creates a new autocomplete component
func New(suggestions []string, minChars int) *Autocomplete {
func New(suggestions []string, minChars int, maxVisible int) *Autocomplete {
ti := textinput.New()
ti.Width = 50
@ -31,7 +31,7 @@ func New(suggestions []string, minChars int) *Autocomplete {
input: ti,
allSuggestions: suggestions,
selectedIndex: -1,
maxVisible: 5,
maxVisible: maxVisible,
minChars: minChars,
width: 50,
}
@ -73,6 +73,16 @@ func (a *Autocomplete) SetWidth(width int) {
a.input.Width = width
}
// SetMaxVisible sets the maximum number of visible suggestions
func (a *Autocomplete) SetMaxVisible(max int) {
a.maxVisible = max
}
// SetMinChars sets the minimum characters required before showing suggestions
func (a *Autocomplete) SetMinChars(min int) {
a.minChars = min
}
// Init initializes the autocomplete
func (a *Autocomplete) Init() tea.Cmd {
return textinput.Blink
@ -171,15 +181,23 @@ func (a *Autocomplete) View() string {
}
prefix := " "
style := lipgloss.NewStyle().Padding(0, 1)
baseStyle := lipgloss.NewStyle().Padding(0, 1)
if i == a.selectedIndex {
// Highlight selected suggestion
style = style.Bold(true).Foreground(lipgloss.Color("12"))
baseStyle = baseStyle.Bold(true).Foreground(lipgloss.Color("12"))
prefix = "→ "
}
suggestionViews = append(suggestionViews, style.Render(prefix+suggestion))
// Build suggestion with highlighted matched characters
var rendered string
if i < len(a.matchedIndexes) {
rendered = a.renderWithHighlights(suggestion, a.matchedIndexes[i], i == a.selectedIndex)
} else {
rendered = suggestion
}
suggestionViews = append(suggestionViews, baseStyle.Render(prefix+rendered))
}
// Box style
@ -195,30 +213,70 @@ func (a *Autocomplete) View() string {
return lipgloss.JoinVertical(lipgloss.Left, inputView, suggestionsBox)
}
// renderWithHighlights renders a suggestion with matched characters highlighted
func (a *Autocomplete) renderWithHighlights(str string, matchedIndexes []int, isSelected bool) string {
if len(matchedIndexes) == 0 {
return str
}
// Create a map for quick lookup
matchedMap := make(map[int]bool)
for _, idx := range matchedIndexes {
matchedMap[idx] = true
}
// Choose highlight style based on selection state
var highlightStyle lipgloss.Style
if isSelected {
// When selected, use underline to distinguish from selection bold
highlightStyle = lipgloss.NewStyle().Underline(true)
} else {
// When not selected, use bold and accent color
highlightStyle = lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("12"))
}
// Build the string with highlights
var result string
runes := []rune(str)
for i, r := range runes {
if matchedMap[i] {
result += highlightStyle.Render(string(r))
} else {
result += string(r)
}
}
return result
}
// updateFilteredSuggestions filters suggestions based on current input
func (a *Autocomplete) updateFilteredSuggestions() {
value := strings.ToLower(a.input.Value())
value := a.input.Value()
// Only show if >= minChars
if len(value) < a.minChars {
a.showSuggestions = false
a.filteredSuggestions = nil
a.matchedIndexes = nil
a.selectedIndex = -1
return
}
// Substring match (case-insensitive)
// Fuzzy match using sahilm/fuzzy
matches := fuzzy.Find(value, a.allSuggestions)
var filtered []string
for _, suggestion := range a.allSuggestions {
if strings.Contains(strings.ToLower(suggestion), value) {
filtered = append(filtered, suggestion)
if len(filtered) >= a.maxVisible {
break
}
var indexes [][]int
for _, match := range matches {
filtered = append(filtered, match.Str)
indexes = append(indexes, match.MatchedIndexes)
if len(filtered) >= a.maxVisible {
break
}
}
a.filteredSuggestions = filtered
a.matchedIndexes = indexes
a.showSuggestions = len(filtered) > 0 && a.focused
a.selectedIndex = -1 // Reset to input
}