Add fuzzy matching for time tags
This commit is contained in:
224
components/autocomplete/autocomplete.go
Normal file
224
components/autocomplete/autocomplete.go
Normal file
@ -0,0 +1,224 @@
|
||||
package autocomplete
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/charmbracelet/bubbles/key"
|
||||
"github.com/charmbracelet/bubbles/textinput"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
)
|
||||
|
||||
type Autocomplete struct {
|
||||
input textinput.Model
|
||||
allSuggestions []string // All available suggestions (newest first)
|
||||
filteredSuggestions []string // Currently matching suggestions
|
||||
selectedIndex int // -1 = input focused, 0+ = suggestion selected
|
||||
showSuggestions bool // Whether to display suggestion box
|
||||
maxVisible int // Max suggestions to show
|
||||
minChars int // Min chars before showing suggestions
|
||||
focused bool
|
||||
width int
|
||||
placeholder string
|
||||
}
|
||||
|
||||
// New creates a new autocomplete component
|
||||
func New(suggestions []string, minChars int) *Autocomplete {
|
||||
ti := textinput.New()
|
||||
ti.Width = 50
|
||||
|
||||
return &Autocomplete{
|
||||
input: ti,
|
||||
allSuggestions: suggestions,
|
||||
selectedIndex: -1,
|
||||
maxVisible: 5,
|
||||
minChars: minChars,
|
||||
width: 50,
|
||||
}
|
||||
}
|
||||
|
||||
// SetValue sets the input value
|
||||
func (a *Autocomplete) SetValue(value string) {
|
||||
a.input.SetValue(value)
|
||||
a.updateFilteredSuggestions()
|
||||
}
|
||||
|
||||
// GetValue returns the current input value
|
||||
func (a *Autocomplete) GetValue() string {
|
||||
return a.input.Value()
|
||||
}
|
||||
|
||||
// Focus focuses the autocomplete input
|
||||
func (a *Autocomplete) Focus() {
|
||||
a.focused = true
|
||||
a.input.Focus()
|
||||
}
|
||||
|
||||
// Blur blurs the autocomplete input
|
||||
func (a *Autocomplete) Blur() {
|
||||
a.focused = false
|
||||
a.input.Blur()
|
||||
a.showSuggestions = false
|
||||
}
|
||||
|
||||
// SetPlaceholder sets the placeholder text
|
||||
func (a *Autocomplete) SetPlaceholder(placeholder string) {
|
||||
a.placeholder = placeholder
|
||||
a.input.Placeholder = placeholder
|
||||
}
|
||||
|
||||
// SetWidth sets the width of the autocomplete
|
||||
func (a *Autocomplete) SetWidth(width int) {
|
||||
a.width = width
|
||||
a.input.Width = width
|
||||
}
|
||||
|
||||
// Init initializes the autocomplete
|
||||
func (a *Autocomplete) Init() tea.Cmd {
|
||||
return textinput.Blink
|
||||
}
|
||||
|
||||
// Update handles messages for the autocomplete
|
||||
func (a *Autocomplete) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
if !a.focused {
|
||||
return a, nil
|
||||
}
|
||||
|
||||
var cmd tea.Cmd
|
||||
|
||||
switch msg := msg.(type) {
|
||||
case tea.KeyMsg:
|
||||
switch {
|
||||
case key.Matches(msg, key.NewBinding(key.WithKeys("down"))):
|
||||
if a.showSuggestions && len(a.filteredSuggestions) > 0 {
|
||||
a.selectedIndex++
|
||||
if a.selectedIndex >= len(a.filteredSuggestions) {
|
||||
a.selectedIndex = 0
|
||||
}
|
||||
return a, nil
|
||||
}
|
||||
|
||||
case key.Matches(msg, key.NewBinding(key.WithKeys("up"))):
|
||||
if a.showSuggestions && len(a.filteredSuggestions) > 0 {
|
||||
a.selectedIndex--
|
||||
if a.selectedIndex < 0 {
|
||||
a.selectedIndex = len(a.filteredSuggestions) - 1
|
||||
}
|
||||
return a, nil
|
||||
}
|
||||
|
||||
case key.Matches(msg, key.NewBinding(key.WithKeys("enter"))):
|
||||
if a.showSuggestions && a.selectedIndex >= 0 && a.selectedIndex < len(a.filteredSuggestions) {
|
||||
// Accept selected suggestion
|
||||
a.input.SetValue(a.filteredSuggestions[a.selectedIndex])
|
||||
a.showSuggestions = false
|
||||
a.selectedIndex = -1
|
||||
return a, nil
|
||||
}
|
||||
|
||||
case key.Matches(msg, key.NewBinding(key.WithKeys("tab"))):
|
||||
if a.showSuggestions && len(a.filteredSuggestions) > 0 {
|
||||
// Accept first or selected suggestion
|
||||
if a.selectedIndex >= 0 && a.selectedIndex < len(a.filteredSuggestions) {
|
||||
a.input.SetValue(a.filteredSuggestions[a.selectedIndex])
|
||||
} else {
|
||||
a.input.SetValue(a.filteredSuggestions[0])
|
||||
}
|
||||
a.showSuggestions = false
|
||||
a.selectedIndex = -1
|
||||
return a, nil
|
||||
}
|
||||
|
||||
case key.Matches(msg, key.NewBinding(key.WithKeys("esc"))):
|
||||
if a.showSuggestions {
|
||||
a.showSuggestions = false
|
||||
a.selectedIndex = -1
|
||||
return a, nil
|
||||
}
|
||||
|
||||
default:
|
||||
// Handle regular text input
|
||||
prevValue := a.input.Value()
|
||||
a.input, cmd = a.input.Update(msg)
|
||||
|
||||
// Update suggestions if value changed
|
||||
if a.input.Value() != prevValue {
|
||||
a.updateFilteredSuggestions()
|
||||
}
|
||||
|
||||
return a, cmd
|
||||
}
|
||||
}
|
||||
|
||||
a.input, cmd = a.input.Update(msg)
|
||||
return a, cmd
|
||||
}
|
||||
|
||||
// View renders the autocomplete
|
||||
func (a *Autocomplete) View() string {
|
||||
// Input field
|
||||
inputView := a.input.View()
|
||||
|
||||
if !a.showSuggestions || len(a.filteredSuggestions) == 0 {
|
||||
return inputView
|
||||
}
|
||||
|
||||
// Suggestion box
|
||||
var suggestionViews []string
|
||||
for i, suggestion := range a.filteredSuggestions {
|
||||
if i >= a.maxVisible {
|
||||
break
|
||||
}
|
||||
|
||||
prefix := " "
|
||||
style := lipgloss.NewStyle().Padding(0, 1)
|
||||
|
||||
if i == a.selectedIndex {
|
||||
// Highlight selected suggestion
|
||||
style = style.Bold(true).Foreground(lipgloss.Color("12"))
|
||||
prefix = "→ "
|
||||
}
|
||||
|
||||
suggestionViews = append(suggestionViews, style.Render(prefix+suggestion))
|
||||
}
|
||||
|
||||
// Box style
|
||||
boxStyle := lipgloss.NewStyle().
|
||||
Border(lipgloss.RoundedBorder()).
|
||||
BorderForeground(lipgloss.Color("8")).
|
||||
Width(a.width)
|
||||
|
||||
suggestionsBox := boxStyle.Render(
|
||||
lipgloss.JoinVertical(lipgloss.Left, suggestionViews...),
|
||||
)
|
||||
|
||||
return lipgloss.JoinVertical(lipgloss.Left, inputView, suggestionsBox)
|
||||
}
|
||||
|
||||
// updateFilteredSuggestions filters suggestions based on current input
|
||||
func (a *Autocomplete) updateFilteredSuggestions() {
|
||||
value := strings.ToLower(a.input.Value())
|
||||
|
||||
// Only show if >= minChars
|
||||
if len(value) < a.minChars {
|
||||
a.showSuggestions = false
|
||||
a.filteredSuggestions = nil
|
||||
a.selectedIndex = -1
|
||||
return
|
||||
}
|
||||
|
||||
// Substring match (case-insensitive)
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
a.filteredSuggestions = filtered
|
||||
a.showSuggestions = len(filtered) > 0 && a.focused
|
||||
a.selectedIndex = -1 // Reset to input
|
||||
}
|
||||
Reference in New Issue
Block a user