fix(filter): wrong highlight when option has grapheme clusters (#799)

This commit is contained in:
Carlos Alexandro Becker 2025-01-22 12:51:18 -03:00 committed by GitHub
commit 37456557c4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 53 additions and 24 deletions

View file

@ -19,7 +19,7 @@ import (
"github.com/charmbracelet/bubbles/viewport"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/charmbracelet/x/ansi"
"github.com/rivo/uniseg"
"github.com/sahilm/fuzzy"
)
@ -211,28 +211,16 @@ func (m model) View() string {
continue
}
var buf strings.Builder
lastIdx := 0
// Use ansi.Truncate and ansi.TruncateLeft and ansi.StringWidth to
// style match.MatchedIndexes without losing the original option style:
var ranges []lipgloss.Range
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(ansi.Cut(match.Str, rng[0], rng[1]+1)))
lastIdx = rng[1] + 1
// ansi.Cut is grapheme and ansi sequence aware, we match against a ansi.Stripped string, but we might still have graphemes.
// all that to say that rng is byte positions, but we need to pass it down to ansi.Cut as char positions.
// so we need to adjust it here:
start, stop := bytePosToVisibleCharPos(match.Str, rng)
ranges = append(ranges, lipgloss.NewRange(start, stop+1, m.matchStyle))
}
// Add any remaining text after the last match
buf.WriteString(ansi.TruncateLeft(styledOption, lastIdx, ""))
// Flush text buffer.
s.WriteString(lineTextStyle.Render(buf.String()))
s.WriteString(lineTextStyle.Render(lipgloss.StyleRanges(styledOption, ranges...)))
// We have finished displaying the match with all of it's matched
// characters highlighted and the rest filled in.
@ -540,3 +528,26 @@ func matchedRanges(in []int) [][2]int {
out = append(out, current)
return out
}
func bytePosToVisibleCharPos(str string, rng [2]int) (int, int) {
bytePos, byteStart, byteStop := 0, rng[0], rng[1]
pos, start, stop := 0, 0, 0
gr := uniseg.NewGraphemes(str)
for byteStart > bytePos {
if !gr.Next() {
break
}
bytePos += len(gr.Str())
pos += max(1, gr.Width())
}
start = pos
for byteStop > bytePos {
if !gr.Next() {
break
}
bytePos += len(gr.Str())
pos += max(1, gr.Width())
}
stop = pos
return start, stop
}

View file

@ -3,6 +3,8 @@ package filter
import (
"reflect"
"testing"
"github.com/charmbracelet/x/ansi"
)
func TestMatchedRanges(t *testing.T) {
@ -39,3 +41,19 @@ func TestMatchedRanges(t *testing.T) {
})
}
}
func TestByteToChar(t *testing.T) {
stStr := "\x1b[90m\ue615\x1b[39m \x1b[3m\x1b[32mDow\x1b[0m\x1b[90m\x1b[39m\x1b[3wnloads"
str := " Downloads"
rng := [2]int{4, 7}
expect := "Dow"
if got := str[rng[0]:rng[1]]; got != expect {
t.Errorf("expected %q, got %q", expect, got)
}
start, stop := bytePosToVisibleCharPos(str, rng)
if got := ansi.Strip(ansi.Cut(stStr, start, stop)); got != expect {
t.Errorf("expected %+q, got %+q", expect, got)
}
}

4
go.mod
View file

@ -9,7 +9,7 @@ require (
github.com/charmbracelet/bubbles v0.20.0
github.com/charmbracelet/bubbletea v1.2.5-0.20241207142916-e0515bc22ad1
github.com/charmbracelet/glamour v0.8.0
github.com/charmbracelet/lipgloss v1.0.0
github.com/charmbracelet/lipgloss v1.0.1-0.20250110214317-ecc1bd014d51
github.com/charmbracelet/log v0.4.0
github.com/charmbracelet/x/ansi v0.7.0
github.com/charmbracelet/x/editor v0.1.0
@ -17,6 +17,7 @@ require (
github.com/muesli/reflow v0.3.0
github.com/muesli/roff v0.1.0
github.com/muesli/termenv v0.15.3-0.20241211131612-0d230cb6eb15
github.com/rivo/uniseg v0.4.7
github.com/sahilm/fuzzy v0.1.1
golang.org/x/text v0.21.0
)
@ -39,7 +40,6 @@ require (
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
github.com/muesli/cancelreader v0.2.2 // indirect
github.com/muesli/mango v0.2.0 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/yuin/goldmark v1.7.4 // indirect
github.com/yuin/goldmark-emoji v1.0.4 // indirect
golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 // indirect

4
go.sum
View file

@ -26,8 +26,8 @@ github.com/charmbracelet/bubbletea v1.2.5-0.20241207142916-e0515bc22ad1 h1:osd3d
github.com/charmbracelet/bubbletea v1.2.5-0.20241207142916-e0515bc22ad1/go.mod h1:Hbk5+oE4a7cDyjfdPi4sHZ42aGTMYcmHnVDhsRswn7A=
github.com/charmbracelet/glamour v0.8.0 h1:tPrjL3aRcQbn++7t18wOpgLyl8wrOHUEDS7IZ68QtZs=
github.com/charmbracelet/glamour v0.8.0/go.mod h1:ViRgmKkf3u5S7uakt2czJ272WSg2ZenlYEZXT2x7Bjw=
github.com/charmbracelet/lipgloss v1.0.0 h1:O7VkGDvqEdGi93X+DeqsQ7PKHDgtQfF8j8/O2qFMQNg=
github.com/charmbracelet/lipgloss v1.0.0/go.mod h1:U5fy9Z+C38obMs+T+tJqst9VGzlOYGj4ri9reL3qUlo=
github.com/charmbracelet/lipgloss v1.0.1-0.20250110214317-ecc1bd014d51 h1:f+0mEkhorXNiBaHb4V9wyd364OH/aF7md7ZngkS+1gU=
github.com/charmbracelet/lipgloss v1.0.1-0.20250110214317-ecc1bd014d51/go.mod h1:QRGthpgH59/perglqXZC8xPHqDGZ9BB45ChJCFEWEMI=
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.7.0 h1:/QfFmiXOGGwN6fRbzvQaYp7fu1pkxpZ3qFBZWBsP404=