fix(whitespace): prevent wide runes from overflowing render width

whitespace.render checked the loop bound only before writing a rune,
then advanced by the rune width. A rune wider than the remaining space
was still written, overshooting the requested width by up to
runeWidth-1 cells, and this made the trailing space-fill unreachable.

Stop before a wide rune would overflow and let the existing space-fill
pad the remaining cells, so the result is always exactly the requested
width. This also fixes overflow surfaced through Place/PlaceHorizontal
with WithWhitespaceChars for multi-cell whitespace characters.
This commit is contained in:
dualfroz committed 2026-09-02 09:44:11 +02:00
1 parent 080e6dc737
commit 4e196d82d8
2 files changed
+34 -5

No files matched your search

+10 -5
View File
@@ -33,14 +33,19 @@ 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.
// Measure the width of the current rune, ensuring we always make
// progress to avoid infinite loops with zero-width characters like
// tabs.
runeWidth := ansi.StringWidth(string(r[j]))
if runeWidth < 1 {
runeWidth = 1
}
// Stop before a wide rune would overflow the requested width; the
// remaining cells are filled with spaces below.
if i+runeWidth > width {
break
}
b.WriteRune(r[j])
i += runeWidth
j++
if j >= len(r) {
@@ -48,7 +53,7 @@ func (w whitespace) render(width int) string {
}
}
// Fill any extra gaps white spaces. This might be necessary if any runes
// Fill any extra gaps with spaces. This might be necessary if any runes
// are more than one cell wide, which could leave a one-rune gap.
short := width - ansi.StringWidth(b.String())
if short > 0 {
+24
View File
@@ -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 TestWhitespaceRenderWideRune(t *testing.T) {
// A whitespace char that is more than one cell wide must never render
// wider than the requested width; the trailing cells are padded with
// spaces instead of overflowing.
ws := newWhitespace(WithWhitespaceChars("東")) // width 2
for _, width := range []int{0, 1, 2, 3, 4, 5} {
got := ansi.StringWidth(ws.render(width))
if got != width {
t.Errorf("render(%d): expected rendered width %d, got %d", width, width, got)
}
}
}
func TestPlaceHorizontalWideWhitespace(t *testing.T) {
// Placing content in a box with a wide whitespace char must keep the
// box exactly the requested width.
got := PlaceHorizontal(5, Left, "ab", WithWhitespaceChars("東"))
if w := ansi.StringWidth(got); w != 5 {
t.Errorf("PlaceHorizontal width: expected 5, got %d (%q)", w, got)
}
}