Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 19 additions & 5 deletions whitespace.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,20 @@ import (

// whitespace is a whitespace renderer.
type whitespace struct {
re *Renderer
style termenv.Style
chars string
re *Renderer
style termenv.Style
chars string
tabWidth int
}

// newWhitespace creates a new whitespace renderer. The order of the options
// matters, if you're using WithWhitespaceRenderer, make sure it comes first as
// other options might depend on it.
func newWhitespace(r *Renderer, opts ...WhitespaceOption) *whitespace {
w := &whitespace{
re: r,
style: r.ColorProfile().String(),
re: r,
style: r.ColorProfile().String(),
tabWidth: 4, // default tab width
}
for _, opt := range opts {
opt(w)
Expand All @@ -34,6 +36,11 @@ func (w whitespace) render(width int) string {
w.chars = " "
}

// replaces tabs with spaces matching tabWidth
if strings.Contains(w.chars, "\t") {
w.chars = strings.ReplaceAll(w.chars, "\t", strings.Repeat(" ", w.tabWidth))
}

r := []rune(w.chars)
j := 0
b := strings.Builder{}
Expand Down Expand Up @@ -81,3 +88,10 @@ func WithWhitespaceChars(s string) WhitespaceOption {
w.chars = s
}
}

// WithWhitespaceTabWidth sets the tab width, which has a default of 4
func WithWhitespaceTabWidth(width int) WhitespaceOption {
return func(w *whitespace) {
w.tabWidth = max(-1, width)
}
}
95 changes: 95 additions & 0 deletions whitespace_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package lipgloss

import (
"io"
"testing"
"time"
)

// TestPlaceWithTabCharacterHang tests that Place doesn't hang when using
// tab characters with WithWhitespaceChars option.
func TestPlaceWithTabCharacterHang(t *testing.T) {
// Set up a timeout to prevent test from hanging indefinitely
done := make(chan bool)

go func() {
// This should not hang
_ = Place(10, 3, Center, Center, "hello",
WithWhitespaceChars("\t"),
)
done <- true
}()

select {
case <-done:
// Test passed - function completed
case <-time.After(2 * time.Second):
t.Fatal("Place() hung when using tab character in WithWhitespaceChars - issue #108")
}
}

// TestPlaceWithTabVariations tests various tab character combinations
func TestPlaceWithTabVariations(t *testing.T) {
testCases := []struct {
name string
chars string
}{
{"tab character", "\t"},
{"multiple tabs", "\t\t"},
{"tab and space", "\t "},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
done := make(chan bool)

go func() {
_ = Place(10, 3, Center, Center, "hello",
WithWhitespaceChars(tc.chars),
)
done <- true
}()

select {
case <-done:
// Test passed
case <-time.After(2 * time.Second):
t.Fatalf("Place() hung with whitespace chars %q", tc.chars)
}
})
}
}

// TestWhitespaceRenderWithTabChar tests the whitespace.render method directly with tabs
func TestWhitespaceRenderWithTabChar(t *testing.T) {
r := NewRenderer(io.Discard)

testCases := []struct {
name string
chars string
width int
}{
{"tab character small width", "\t", 5},
{"tab character large width", "\t", 20},
{"multiple tabs", "\t\t", 10},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
done := make(chan bool)

go func() {
ws := newWhitespace(r, WithWhitespaceChars(tc.chars))
_ = ws.render(tc.width)
done <- true
}()

select {
case <-done:
// Test passed
case <-time.After(2 * time.Second):
t.Fatalf("whitespace.render() hung with chars %q and width %d", tc.chars, tc.width)
}
})
}
}