From d41c26755e5412fb9778ede50b55b80002443023 Mon Sep 17 00:00:00 2001 From: Ammar Bandukwala Date: Tue, 5 Sep 2023 19:33:18 -0400 Subject: [PATCH] trying to wrap --- text.go | 58 +++++++++++++++++++++++++++++++++++++++++++++++----- text_test.go | 41 +++++++++++++++++++++++++++++++++++++ 2 files changed, 94 insertions(+), 5 deletions(-) diff --git a/text.go b/text.go index f0600da..eed8138 100644 --- a/text.go +++ b/text.go @@ -4,6 +4,7 @@ import ( "fmt" "io" "strings" + "unicode" "github.com/muesli/termenv" ) @@ -32,9 +33,14 @@ func (t *Text) Tail() *Text { } // Split splits the current text into two parts at the given index. +// It returns the new node. func (t *Text) Split(n int) *Text { if n > len(t.S) { - panic("split index out of bounds") + panic(fmt.Sprintf("split index %d > len(t.S) (%v) ", n, len(t.S))) + } + + if n <= 0 { + panic("split index must be > 0") } if len(t.S) == n || len(t.S) == 0 { @@ -42,25 +48,27 @@ func (t *Text) Split(n int) *Text { } // Split the string. - right := t.S[n:] + nextStr := t.S[n:] t.S = t.S[:n] if t.Next == nil { - t.Next = &Text{S: right, Prev: t} + t.Next = &Text{S: nextStr, Prev: t} return t.Next } - t.Next.Insert(right) + t.Next.Insert(nextStr) return t.Next } // Insert inserts the given text before the current text. -func (t *Text) Insert(s string) { +// It returns the new node. +func (t *Text) Insert(s string) *Text { tt := &Text{S: s} oldPrev := t.Prev oldPrev.Next = tt tt.Prev = oldPrev tt.Next = t t.Prev = tt + return tt } func (t *Text) debugString() string { @@ -215,3 +223,43 @@ func Wrap(prefix, suffix string) Formatter { t.Append(suffix) }) } + +// LineWrap wraps the text at the given width. +// It breaks lines at word boundaries, trailing whitespace is ignored. +func LineWrap(width int) Formatter { + return formatterFunc(func(t *Text) { + var col int + + for at := t.Head(); at != nil; at = at.Next { + nlAt := strings.IndexByte(at.S, '\n') + if nlAt < 0 { + nlAt = len(at.S) + } + col += nlAt + + overflow := (width - col) * -1 + if overflow <= 0 { + continue + } + + // Find the best place to break the line. + var spaceAt int + for { + s := strings.IndexFunc(at.S[spaceAt:nlAt], unicode.IsSpace) + println("s", s, "spaceAt", spaceAt, "nlAt", nlAt, "overflow", overflow) + spaceAt += s + 1 + if s < 0 || spaceAt > overflow || spaceAt > width { + break + } + } + + if spaceAt > 0 { + next := at.Split(spaceAt) + at.S = strings.TrimRight(at.S, " \t") + next.S = strings.TrimLeft(next.S, " \t") + next.Insert("\n") + col = 0 + } + } + }) +} diff --git a/text_test.go b/text_test.go index bb1f939..c43c20a 100644 --- a/text_test.go +++ b/text_test.go @@ -68,6 +68,47 @@ func TestFgColor(t *testing.T) { t.Logf("txt: %s", txt) } +func TestLineWrap(t *testing.T) { + t.Parallel() + + t.Run("None", func(t *testing.T) { + txt := String( + "The crazy fox jumped", + ) + + LineWrap(100).Format(txt) + + requireText(t, txt, "The crazy fox jumped") + }) + t.Run("Basic", func(t *testing.T) { + txt := String( + "The crazy fox jumped", + ) + + LineWrap(10).Format(txt) + + requireText(t, txt, "The crazy\nfox jumped") + }) + t.Run("WordBoundary", func(t *testing.T) { + txt := String( + "The crazy_fox_jumped", + ) + + LineWrap(10).Format(txt) + + requireText(t, txt, "The\ncrazy_fox_jumped") + }) + t.Run("MultiLine", func(t *testing.T) { + txt := String( + "aabb cc dd ee ff", + ) + + LineWrap(4).Format(txt) + + requireText(t, txt, "aabb\ncc\ndd\nee\nff") + }) +} + func TestStyle(t *testing.T) { errorStyle := Style{ FgColor(termenv.RGBColor("#ff0000")),