Skip to content

Commit 3745655

Browse files
authored
fix(filter): wrong highlight when option has grapheme clusters (#799)
1 parent c11af42 commit 3745655

File tree

4 files changed

+53
-24
lines changed

4 files changed

+53
-24
lines changed

filter/filter.go

Lines changed: 31 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import (
1919
"github.com/charmbracelet/bubbles/viewport"
2020
tea "github.com/charmbracelet/bubbletea"
2121
"github.com/charmbracelet/lipgloss"
22-
"github.com/charmbracelet/x/ansi"
22+
"github.com/rivo/uniseg"
2323
"github.com/sahilm/fuzzy"
2424
)
2525

@@ -211,28 +211,16 @@ func (m model) View() string {
211211
continue
212212
}
213213

214-
var buf strings.Builder
215-
lastIdx := 0
216-
217-
// Use ansi.Truncate and ansi.TruncateLeft and ansi.StringWidth to
218-
// style match.MatchedIndexes without losing the original option style:
214+
var ranges []lipgloss.Range
219215
for _, rng := range matchedRanges(match.MatchedIndexes) {
220-
// fmt.Print("here ", lastIdx, rng, " - ", match.Str[rng[0]:rng[1]+1], "\r\n")
221-
// Add the text before this match
222-
if rng[0] > lastIdx {
223-
buf.WriteString(ansi.Cut(styledOption, lastIdx, rng[0]))
224-
}
225-
226-
// Add the matched character with highlight
227-
buf.WriteString(m.matchStyle.Render(ansi.Cut(match.Str, rng[0], rng[1]+1)))
228-
lastIdx = rng[1] + 1
216+
// ansi.Cut is grapheme and ansi sequence aware, we match against a ansi.Stripped string, but we might still have graphemes.
217+
// all that to say that rng is byte positions, but we need to pass it down to ansi.Cut as char positions.
218+
// so we need to adjust it here:
219+
start, stop := bytePosToVisibleCharPos(match.Str, rng)
220+
ranges = append(ranges, lipgloss.NewRange(start, stop+1, m.matchStyle))
229221
}
230222

231-
// Add any remaining text after the last match
232-
buf.WriteString(ansi.TruncateLeft(styledOption, lastIdx, ""))
233-
234-
// Flush text buffer.
235-
s.WriteString(lineTextStyle.Render(buf.String()))
223+
s.WriteString(lineTextStyle.Render(lipgloss.StyleRanges(styledOption, ranges...)))
236224

237225
// We have finished displaying the match with all of it's matched
238226
// characters highlighted and the rest filled in.
@@ -540,3 +528,26 @@ func matchedRanges(in []int) [][2]int {
540528
out = append(out, current)
541529
return out
542530
}
531+
532+
func bytePosToVisibleCharPos(str string, rng [2]int) (int, int) {
533+
bytePos, byteStart, byteStop := 0, rng[0], rng[1]
534+
pos, start, stop := 0, 0, 0
535+
gr := uniseg.NewGraphemes(str)
536+
for byteStart > bytePos {
537+
if !gr.Next() {
538+
break
539+
}
540+
bytePos += len(gr.Str())
541+
pos += max(1, gr.Width())
542+
}
543+
start = pos
544+
for byteStop > bytePos {
545+
if !gr.Next() {
546+
break
547+
}
548+
bytePos += len(gr.Str())
549+
pos += max(1, gr.Width())
550+
}
551+
stop = pos
552+
return start, stop
553+
}

filter/filter_test.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ package filter
33
import (
44
"reflect"
55
"testing"
6+
7+
"github.com/charmbracelet/x/ansi"
68
)
79

810
func TestMatchedRanges(t *testing.T) {
@@ -39,3 +41,19 @@ func TestMatchedRanges(t *testing.T) {
3941
})
4042
}
4143
}
44+
45+
func TestByteToChar(t *testing.T) {
46+
stStr := "\x1b[90m\ue615\x1b[39m \x1b[3m\x1b[32mDow\x1b[0m\x1b[90m\x1b[39m\x1b[3wnloads"
47+
str := " Downloads"
48+
rng := [2]int{4, 7}
49+
expect := "Dow"
50+
51+
if got := str[rng[0]:rng[1]]; got != expect {
52+
t.Errorf("expected %q, got %q", expect, got)
53+
}
54+
55+
start, stop := bytePosToVisibleCharPos(str, rng)
56+
if got := ansi.Strip(ansi.Cut(stStr, start, stop)); got != expect {
57+
t.Errorf("expected %+q, got %+q", expect, got)
58+
}
59+
}

go.mod

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,15 @@ require (
99
github.com/charmbracelet/bubbles v0.20.0
1010
github.com/charmbracelet/bubbletea v1.2.5-0.20241207142916-e0515bc22ad1
1111
github.com/charmbracelet/glamour v0.8.0
12-
github.com/charmbracelet/lipgloss v1.0.0
12+
github.com/charmbracelet/lipgloss v1.0.1-0.20250110214317-ecc1bd014d51
1313
github.com/charmbracelet/log v0.4.0
1414
github.com/charmbracelet/x/ansi v0.7.0
1515
github.com/charmbracelet/x/editor v0.1.0
1616
github.com/charmbracelet/x/term v0.2.1
1717
github.com/muesli/reflow v0.3.0
1818
github.com/muesli/roff v0.1.0
1919
github.com/muesli/termenv v0.15.3-0.20241211131612-0d230cb6eb15
20+
github.com/rivo/uniseg v0.4.7
2021
github.com/sahilm/fuzzy v0.1.1
2122
golang.org/x/text v0.21.0
2223
)
@@ -39,7 +40,6 @@ require (
3940
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
4041
github.com/muesli/cancelreader v0.2.2 // indirect
4142
github.com/muesli/mango v0.2.0 // indirect
42-
github.com/rivo/uniseg v0.4.7 // indirect
4343
github.com/yuin/goldmark v1.7.4 // indirect
4444
github.com/yuin/goldmark-emoji v1.0.4 // indirect
4545
golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 // indirect

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,8 @@ github.com/charmbracelet/bubbletea v1.2.5-0.20241207142916-e0515bc22ad1 h1:osd3d
2626
github.com/charmbracelet/bubbletea v1.2.5-0.20241207142916-e0515bc22ad1/go.mod h1:Hbk5+oE4a7cDyjfdPi4sHZ42aGTMYcmHnVDhsRswn7A=
2727
github.com/charmbracelet/glamour v0.8.0 h1:tPrjL3aRcQbn++7t18wOpgLyl8wrOHUEDS7IZ68QtZs=
2828
github.com/charmbracelet/glamour v0.8.0/go.mod h1:ViRgmKkf3u5S7uakt2czJ272WSg2ZenlYEZXT2x7Bjw=
29-
github.com/charmbracelet/lipgloss v1.0.0 h1:O7VkGDvqEdGi93X+DeqsQ7PKHDgtQfF8j8/O2qFMQNg=
30-
github.com/charmbracelet/lipgloss v1.0.0/go.mod h1:U5fy9Z+C38obMs+T+tJqst9VGzlOYGj4ri9reL3qUlo=
29+
github.com/charmbracelet/lipgloss v1.0.1-0.20250110214317-ecc1bd014d51 h1:f+0mEkhorXNiBaHb4V9wyd364OH/aF7md7ZngkS+1gU=
30+
github.com/charmbracelet/lipgloss v1.0.1-0.20250110214317-ecc1bd014d51/go.mod h1:QRGthpgH59/perglqXZC8xPHqDGZ9BB45ChJCFEWEMI=
3131
github.com/charmbracelet/log v0.4.0 h1:G9bQAcx8rWA2T3pWvx7YtPTPwgqpk7D68BX21IRW8ZM=
3232
github.com/charmbracelet/log v0.4.0/go.mod h1:63bXt/djrizTec0l11H20t8FDSvA4CRZJ1KH22MdptM=
3333
github.com/charmbracelet/x/ansi v0.7.0 h1:/QfFmiXOGGwN6fRbzvQaYp7fu1pkxpZ3qFBZWBsP404=

0 commit comments

Comments
 (0)