Add proper fuzzy matching for time tags
This commit is contained in:
@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user