mirror of
https://github.com/charmbracelet/gum
synced 2026-03-14 21:55:45 +01:00
fix(filter): properly handle options with ansi styles (#789)
* fix(filter): handle styles option matches * perf: use ranges * fix: cut * fix: ansi update
This commit is contained in:
parent
cd151b51bf
commit
d3d20efc70
5 changed files with 112 additions and 29 deletions
|
|
@ -14,6 +14,7 @@ import (
|
|||
"github.com/charmbracelet/gum/internal/stdin"
|
||||
"github.com/charmbracelet/gum/internal/timeout"
|
||||
"github.com/charmbracelet/gum/internal/tty"
|
||||
"github.com/charmbracelet/x/ansi"
|
||||
"github.com/sahilm/fuzzy"
|
||||
)
|
||||
|
||||
|
|
@ -59,13 +60,21 @@ func (o Options) Run() error {
|
|||
if o.Value != "" {
|
||||
i.SetValue(o.Value)
|
||||
}
|
||||
|
||||
choices := map[string]string{}
|
||||
filteringChoices := []string{}
|
||||
for _, opt := range o.Options {
|
||||
s := ansi.Strip(opt)
|
||||
choices[s] = opt
|
||||
filteringChoices = append(filteringChoices, s)
|
||||
}
|
||||
switch {
|
||||
case o.Value != "" && o.Fuzzy:
|
||||
matches = fuzzy.Find(o.Value, o.Options)
|
||||
matches = fuzzy.Find(o.Value, filteringChoices)
|
||||
case o.Value != "" && !o.Fuzzy:
|
||||
matches = exactMatches(o.Value, o.Options)
|
||||
matches = exactMatches(o.Value, filteringChoices)
|
||||
default:
|
||||
matches = matchAll(o.Options)
|
||||
matches = matchAll(filteringChoices)
|
||||
}
|
||||
|
||||
if o.NoLimit {
|
||||
|
|
@ -86,7 +95,8 @@ func (o Options) Run() error {
|
|||
}
|
||||
|
||||
m := model{
|
||||
choices: o.Options,
|
||||
choices: choices,
|
||||
filteringChoices: filteringChoices,
|
||||
indicator: o.Indicator,
|
||||
matches: matches,
|
||||
header: o.Header,
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import (
|
|||
"github.com/charmbracelet/bubbles/viewport"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
"github.com/charmbracelet/x/ansi"
|
||||
"github.com/sahilm/fuzzy"
|
||||
)
|
||||
|
||||
|
|
@ -124,7 +125,8 @@ func (k keymap) ShortHelp() []key.Binding {
|
|||
type model struct {
|
||||
textinput textinput.Model
|
||||
viewport *viewport.Model
|
||||
choices []string
|
||||
choices map[string]string
|
||||
filteringChoices []string
|
||||
matches []fuzzy.Match
|
||||
cursor int
|
||||
header string
|
||||
|
|
@ -201,28 +203,37 @@ func (m model) View() string {
|
|||
s.WriteString(" ")
|
||||
}
|
||||
|
||||
// For this match, there are a certain number of characters that have
|
||||
// caused the match. i.e. fuzzy matching.
|
||||
// We should indicate to the users which characters are being matched.
|
||||
mi := 0
|
||||
var buf strings.Builder
|
||||
for ci, c := range match.Str {
|
||||
// Check if the current character index matches the current matched
|
||||
// index. If so, color the character to indicate a match.
|
||||
if mi < len(match.MatchedIndexes) && ci == match.MatchedIndexes[mi] {
|
||||
// Flush text buffer.
|
||||
s.WriteString(lineTextStyle.Render(buf.String()))
|
||||
buf.Reset()
|
||||
|
||||
s.WriteString(m.matchStyle.Render(string(c)))
|
||||
// We have matched this character, so we never have to check it
|
||||
// again. Move on to the next match.
|
||||
mi++
|
||||
} else {
|
||||
// Not a match, buffer a regular character.
|
||||
buf.WriteRune(c)
|
||||
}
|
||||
styledOption := m.choices[match.Str]
|
||||
if len(match.MatchedIndexes) == 0 {
|
||||
// No matches, just render the text.
|
||||
s.WriteString(lineTextStyle.Render(styledOption))
|
||||
s.WriteRune('\n')
|
||||
continue
|
||||
}
|
||||
|
||||
// Use ansi.Truncate and ansi.TruncateLeft and ansi.StringWidth to
|
||||
// style match.MatchedIndexes without losing the original option style:
|
||||
var buf strings.Builder
|
||||
lastIdx := 0
|
||||
for _, rng := range matchedRanges(match.MatchedIndexes) {
|
||||
// fmt.Print("here ", lastIdx, rng, " - ", match.Str[rng[0]:rng[1]+1], "\r\n")
|
||||
// Add the text before this match
|
||||
if rng[0] > lastIdx {
|
||||
buf.WriteString(ansi.Cut(styledOption, lastIdx, rng[0]))
|
||||
}
|
||||
|
||||
// Add the matched character with highlight
|
||||
buf.WriteString(m.matchStyle.Render(match.Str[rng[0] : rng[1]+1]))
|
||||
lastIdx = rng[1] + 1
|
||||
}
|
||||
|
||||
// Add any remaining text after the last match
|
||||
// fmt.Print("here ", lastIdx, ansi.StringWidth(styledOption), len(match.Str), "\r\n")
|
||||
if lastIdx < ansi.StringWidth(styledOption) {
|
||||
remaining := ansi.TruncateLeft(styledOption, lastIdx, "")
|
||||
buf.WriteString(remaining)
|
||||
}
|
||||
|
||||
// Flush text buffer.
|
||||
s.WriteString(lineTextStyle.Render(buf.String()))
|
||||
|
||||
|
|
@ -356,7 +367,7 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||
if !m.strict {
|
||||
choices = append(choices, m.textinput.Value())
|
||||
}
|
||||
choices = append(choices, m.choices...)
|
||||
choices = append(choices, m.filteringChoices...)
|
||||
if m.fuzzy {
|
||||
if m.sort {
|
||||
m.matches = fuzzy.Find(m.textinput.Value(), choices)
|
||||
|
|
@ -370,7 +381,7 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||
// If the search field is empty, let's not display the matches
|
||||
// (none), but rather display all possible choices.
|
||||
if m.textinput.Value() == "" {
|
||||
m.matches = matchAll(m.choices)
|
||||
m.matches = matchAll(m.filteringChoices)
|
||||
}
|
||||
|
||||
// For reverse layout, we need to offset the viewport so that the
|
||||
|
|
@ -511,3 +522,24 @@ func clamp(low, high, val int) int {
|
|||
}
|
||||
return val
|
||||
}
|
||||
|
||||
func matchedRanges(in []int) [][2]int {
|
||||
if len(in) == 0 {
|
||||
return [][2]int{}
|
||||
}
|
||||
current := [2]int{in[0], in[0]}
|
||||
if len(in) == 1 {
|
||||
return [][2]int{current}
|
||||
}
|
||||
var out [][2]int
|
||||
for i := 1; i < len(in); i++ {
|
||||
if in[i] == current[1]+1 {
|
||||
current[1] = in[i]
|
||||
} else {
|
||||
out = append(out, current)
|
||||
current = [2]int{in[i], in[i]}
|
||||
}
|
||||
}
|
||||
out = append(out, current)
|
||||
return out
|
||||
}
|
||||
|
|
|
|||
41
filter/filter_test.go
Normal file
41
filter/filter_test.go
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
package filter
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestMatchedRanges(t *testing.T) {
|
||||
for name, tt := range map[string]struct {
|
||||
in []int
|
||||
out [][2]int
|
||||
}{
|
||||
"empty": {
|
||||
in: []int{},
|
||||
out: [][2]int{},
|
||||
},
|
||||
"one char": {
|
||||
in: []int{1},
|
||||
out: [][2]int{{1, 1}},
|
||||
},
|
||||
"2 char range": {
|
||||
in: []int{1, 2},
|
||||
out: [][2]int{{1, 2}},
|
||||
},
|
||||
"multiple char range": {
|
||||
in: []int{1, 2, 3, 4, 5, 6},
|
||||
out: [][2]int{{1, 6}},
|
||||
},
|
||||
"multiple char ranges": {
|
||||
in: []int{1, 2, 3, 5, 6, 10, 11, 12, 13, 23, 24, 40, 42, 43, 45, 52},
|
||||
out: [][2]int{{1, 3}, {5, 6}, {10, 13}, {23, 24}, {40, 40}, {42, 43}, {45, 45}, {52, 52}},
|
||||
},
|
||||
} {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
match := matchedRanges(tt.in)
|
||||
if !reflect.DeepEqual(match, tt.out) {
|
||||
t.Errorf("expected %v, got %v", tt.out, match)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
2
go.mod
2
go.mod
|
|
@ -11,7 +11,7 @@ require (
|
|||
github.com/charmbracelet/glamour v0.8.0
|
||||
github.com/charmbracelet/lipgloss v1.0.0
|
||||
github.com/charmbracelet/log v0.4.0
|
||||
github.com/charmbracelet/x/ansi v0.6.0
|
||||
github.com/charmbracelet/x/ansi v0.6.1-0.20250107110353-48b574af22a5
|
||||
github.com/charmbracelet/x/editor v0.1.0
|
||||
github.com/charmbracelet/x/term v0.2.1
|
||||
github.com/muesli/reflow v0.3.0
|
||||
|
|
|
|||
4
go.sum
4
go.sum
|
|
@ -30,8 +30,8 @@ github.com/charmbracelet/lipgloss v1.0.0 h1:O7VkGDvqEdGi93X+DeqsQ7PKHDgtQfF8j8/O
|
|||
github.com/charmbracelet/lipgloss v1.0.0/go.mod h1:U5fy9Z+C38obMs+T+tJqst9VGzlOYGj4ri9reL3qUlo=
|
||||
github.com/charmbracelet/log v0.4.0 h1:G9bQAcx8rWA2T3pWvx7YtPTPwgqpk7D68BX21IRW8ZM=
|
||||
github.com/charmbracelet/log v0.4.0/go.mod h1:63bXt/djrizTec0l11H20t8FDSvA4CRZJ1KH22MdptM=
|
||||
github.com/charmbracelet/x/ansi v0.6.0 h1:qOznutrb93gx9oMiGf7caF7bqqubh6YIM0SWKyA08pA=
|
||||
github.com/charmbracelet/x/ansi v0.6.0/go.mod h1:KBUFw1la39nl0dLl10l5ORDAqGXaeurTQmwyyVKse/Q=
|
||||
github.com/charmbracelet/x/ansi v0.6.1-0.20250107110353-48b574af22a5 h1:TSjbA80sXnABV/Vxhnb67Ho7p8bEYqz6NIdhLAx+1yg=
|
||||
github.com/charmbracelet/x/ansi v0.6.1-0.20250107110353-48b574af22a5/go.mod h1:KBUFw1la39nl0dLl10l5ORDAqGXaeurTQmwyyVKse/Q=
|
||||
github.com/charmbracelet/x/editor v0.1.0 h1:p69/dpvlwRTs9uYiPeAWruwsHqTFzHhTvQOd/WVSX98=
|
||||
github.com/charmbracelet/x/editor v0.1.0/go.mod h1:oivrEbcP/AYt/Hpvk5pwDXXrQ933gQS6UzL6fxqAGSA=
|
||||
github.com/charmbracelet/x/exp/golden v0.0.0-20240815200342-61de596daa2b h1:MnAMdlwSltxJyULnrYbkZpp4k58Co7Tah3ciKhSNo0Q=
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue