diff --git a/filter/filter.go b/filter/filter.go index 0c7202352..4ea2cbbad 100644 --- a/filter/filter.go +++ b/filter/filter.go @@ -20,6 +20,7 @@ import ( tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" "github.com/charmbracelet/x/ansi" + "github.com/rivo/uniseg" "github.com/sahilm/fuzzy" ) @@ -217,15 +218,20 @@ func (m model) View() string { // Use ansi.Truncate and ansi.TruncateLeft and ansi.StringWidth to // style match.MatchedIndexes without losing the original option style: for _, rng := range matchedRanges(match.MatchedIndexes) { - // fmt.Print("here ", lastIdx, rng, " - ", match.Str[rng[0]:rng[1]+1], "\r\n") + + // ansi.Cut is grapheme and ansi sequence aware, we match against a ansi.Stripped string, but we might still have grahemes. + // all that to say that rng is byte positions, but we pass it down to ansi.Cut as char positions. + // so we need to adjust it here. + start, stop := byteToChar(match.Str, rng) + // Add the text before this match - if rng[0] > lastIdx { - buf.WriteString(ansi.Cut(styledOption, lastIdx, rng[0])) + if start > lastIdx { + buf.WriteString(ansi.Cut(styledOption, lastIdx, start)) } // Add the matched character with highlight - buf.WriteString(m.matchStyle.Render(ansi.Cut(match.Str, rng[0], rng[1]+1))) - lastIdx = rng[1] + 1 + buf.WriteString(m.matchStyle.Render(ansi.Cut(match.Str, start, stop+1))) + lastIdx = stop + 1 } // Add any remaining text after the last match @@ -540,3 +546,26 @@ func matchedRanges(in []int) [][2]int { out = append(out, current) return out } + +func byteToChar(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 +} diff --git a/filter/filter_test.go b/filter/filter_test.go index 56e759044..3705ac76b 100644 --- a/filter/filter_test.go +++ b/filter/filter_test.go @@ -3,6 +3,8 @@ package filter import ( "reflect" "testing" + + "github.com/charmbracelet/x/ansi" ) func TestMatchedRanges(t *testing.T) { @@ -39,3 +41,18 @@ func TestMatchedRanges(t *testing.T) { }) } } + +func TestByteToChar(t *testing.T) { + 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 := byteToChar(str, rng) + if got := ansi.Cut(str, start, stop); got != expect { + t.Errorf("expected %+q, got %+q", expect, got) + } +}