diff --git a/whitespace.go b/whitespace.go index e353a076..7f927276 100644 --- a/whitespace.go +++ b/whitespace.go @@ -33,11 +33,15 @@ func (w whitespace) render(width int) string { // Cycle through runes and print them into the whitespace. for i := 0; i < width; { - b.WriteRune(r[j]) - // Measure the width of the rune we just wrote, ensuring we always - // make progress to avoid infinite loops with zero-width characters - // like tabs. runeWidth := ansi.StringWidth(string(r[j])) + // Don't overshoot the target width with a wide (multi-cell) rune. + // Any remaining gap is padded with spaces below instead. + if i+runeWidth > width { + break + } + b.WriteRune(r[j]) + // Ensure we always make progress to avoid infinite loops with + // zero-width characters like tabs. if runeWidth < 1 { runeWidth = 1 } diff --git a/whitespace_test.go b/whitespace_test.go index fdc1d09a..085c41c5 100644 --- a/whitespace_test.go +++ b/whitespace_test.go @@ -3,6 +3,8 @@ package lipgloss import ( "testing" "time" + + "github.com/charmbracelet/x/ansi" ) func TestWhitespaceRenderWithTab(t *testing.T) { @@ -50,3 +52,25 @@ func TestWhitespaceRenderNormal(t *testing.T) { t.Errorf("expected 5 characters, got %d", len(result)) } } + +func TestWhitespaceRenderWideChars(t *testing.T) { + // Rendering with wide (multi-cell) characters must never exceed the + // requested width. Any leftover cells are padded with spaces instead. + for _, tc := range []struct { + chars string + width int + }{ + {"橋", 1}, + {"橋", 3}, + {"橋", 5}, + {"a橋", 4}, + {"橋", 0}, + } { + ws := newWhitespace(WithWhitespaceChars(tc.chars)) + got := ansi.StringWidth(ws.render(tc.width)) + if got != tc.width { + t.Errorf("render(%d) with chars %q produced width %d, want %d", + tc.width, tc.chars, got, tc.width) + } + } +}