From 3b073773d43168c2ef707346aee2941ccf9e193f Mon Sep 17 00:00:00 2001 From: Ayman Bagabas Date: Mon, 1 Apr 2024 18:27:17 -0400 Subject: [PATCH 1/9] fix(term): ansi: account for some wrap edge cases Properly count escape codes, better handling of breakpoints, and only break word/breakpoint when necessary. Fixes: https://github.com/charmbracelet/x/issues/58 --- exp/term/ansi/wrap.go | 69 +++++++++++++++++++------------------- exp/term/ansi/wrap_test.go | 48 +++++++++++++++++++++++--- 2 files changed, 77 insertions(+), 40 deletions(-) diff --git a/exp/term/ansi/wrap.go b/exp/term/ansi/wrap.go index 36dcbe4c..fe2fccfc 100644 --- a/exp/term/ansi/wrap.go +++ b/exp/term/ansi/wrap.go @@ -105,6 +105,8 @@ func Hardwrap(s string, limit int, preserveSpace bool) string { // The breakpoints string is a list of characters that are considered // breakpoints for word wrapping. A hyphen (-) is always considered a // breakpoint. +// +// Note: breakpoints must be a string of 1-cell wide rune character. func Wordwrap(s string, limit int, breakpoints string) string { if limit < 1 { return s @@ -132,10 +134,9 @@ func Wordwrap(s string, limit int, breakpoints string) string { } addWord := func() { - if word.Len() == 0 { - return + if wordLen > 0 { + addSpace() } - addSpace() curWidth += wordLen buf.Write(word.Bytes()) word.Reset() @@ -163,11 +164,6 @@ func Wordwrap(s string, limit int, breakpoints string) string { if r != utf8.RuneError && unicode.IsSpace(r) { addWord() space.WriteRune(r) - } else if bytes.ContainsAny(cluster, breakpoints) { - addSpace() - addWord() - buf.Write(cluster) - curWidth++ } else { word.Write(cluster) wordLen += width @@ -234,6 +230,8 @@ func Wordwrap(s string, limit int, breakpoints string) string { // account for wide-characters in the string. The breakpoints string is a list // of characters that are considered breakpoints for word wrapping. A hyphen // (-) is always considered a breakpoint. +// +// Note: breakpoints must be a string of 1-cell wide rune character. func Wrap(s string, limit int, breakpoints string) string { if limit < 1 { return s @@ -247,6 +245,7 @@ func Wrap(s string, limit int, breakpoints string) string { buf bytes.Buffer word bytes.Buffer space bytes.Buffer + bpoint bytes.Buffer curWidth int wordLen int gstate = -1 @@ -254,6 +253,12 @@ func Wrap(s string, limit int, breakpoints string) string { b = []byte(s) ) + addBpoint := func() { + curWidth += bpoint.Len() + buf.Write(bpoint.Bytes()) + bpoint.Reset() + } + addSpace := func() { curWidth += space.Len() buf.Write(space.Bytes()) @@ -261,10 +266,14 @@ func Wrap(s string, limit int, breakpoints string) string { } addWord := func() { - if word.Len() == 0 { - return + addBpoint() + if wordLen > 0 { + // We use wordLen to determine if we have a word to add + // to the buffer. If wordLen is 0, we don't add spaces at the + // beginning of a line. + addSpace() } - addSpace() + curWidth += wordLen buf.Write(word.Bytes()) word.Reset() @@ -292,26 +301,16 @@ func Wrap(s string, limit int, breakpoints string) string { if r != utf8.RuneError && unicode.IsSpace(r) { addWord() space.WriteRune(r) - } else if bytes.ContainsAny(cluster, breakpoints) { - addSpace() - addWord() - buf.Write(cluster) - curWidth++ } else { if wordLen+width > limit { + // If the word is longer than the limit, we break it addWord() - addNewline() } word.Write(cluster) wordLen += width - if curWidth+space.Len()+wordLen > limit && - wordLen < limit { + if curWidth+space.Len()+wordLen+bpoint.Len() > limit { + addBpoint() addNewline() - } else if curWidth+wordLen >= limit { - addWord() - if i < len(b)-1 { - addNewline() - } } } @@ -340,23 +339,23 @@ func Wrap(s string, limit int, breakpoints string) string { case runeContainsAny(r, breakpoints): addSpace() addWord() - buf.WriteByte(b[i]) - curWidth++ + if curWidth+1 <= limit { + bpoint.WriteByte(b[i]) + break + } + // If we can't fit the breakpoint in the current line, we treat + // it as a word character. + fallthrough default: - if wordLen+1 > limit { + if wordLen >= limit { + // If the word is longer than the limit, we break it addWord() - addNewline() } word.WriteByte(b[i]) wordLen++ - if curWidth+space.Len()+wordLen > limit && - wordLen < limit { + if curWidth+space.Len()+wordLen+bpoint.Len() > limit { + addBpoint() addNewline() - } else if curWidth+wordLen >= limit { - addWord() - if i < len(b)-1 { - addNewline() - } } } diff --git a/exp/term/ansi/wrap_test.go b/exp/term/ansi/wrap_test.go index 21eea25d..109273e7 100644 --- a/exp/term/ansi/wrap_test.go +++ b/exp/term/ansi/wrap_test.go @@ -34,7 +34,7 @@ var cases = []struct { {"osc8_wrap", "สวัสดีสวัสดี\x1b]8;;https://example.com\x1b\\สวัสดีสวัสดี\x1b]8;;\x1b\\", 8, "สวัสดีสวัสดี\x1b]8;;https://example.com\x1b\\\nสวัสดีสวัสดี\x1b]8;;\x1b\\", false}, } -func TestWrap(t *testing.T) { +func TestHardwrap(t *testing.T) { for i, tt := range cases { t.Run(tt.name, func(t *testing.T) { if got := ansi.Hardwrap(tt.input, tt.limit, tt.preserveSpace); got != tt.expected { @@ -92,7 +92,7 @@ func TestWrapWordwrap(t *testing.T) { } } -var smartWrapCases = []struct { +var wrapCases = []struct { name string input string expected string @@ -149,19 +149,57 @@ var smartWrapCases = []struct { { name: "long input2", input: "Rotated keys for a-good-offensive-cheat-code-incorporated/crypto-line-operating-system.", - expected: "Rotated keys for a-good-offensive-cheat-code-incorporated/crypto-line-operat\ning-system.", + expected: "Rotated keys for a-good-offensive-cheat-code-incorporated/crypto-line-\noperating-system.", width: 76, }, + { + name: "hyphen breakpoint", + input: "a-good-offensive-cheat-code", + expected: "a-good-\noffensive-\ncheat-code", + width: 10, + }, + { + name: "exact", + input: "\x1b[91mfoo\x1b[0", + expected: "\x1b[91mfoo\x1b[0", + width: 3, + }, + { + name: "extra space", + input: "\x1b[mfoo \x1b[m", + expected: "\x1b[mfoo\x1b[m", + width: 3, + }, { name: "paragraph with styles", input: "Lorem ipsum dolor \x1b[1msit\x1b[m amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. \x1b[31mUt enim\x1b[m ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea \x1b[38;5;200mcommodo consequat\x1b[m. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. \x1b[1;2;33mExcepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\x1b[m", expected: "Lorem ipsum dolor \x1b[1msit\x1b[m amet,\nconsectetur adipiscing elit,\nsed do eiusmod tempor\nincididunt ut labore et dolore\nmagna aliqua. \x1b[31mUt enim\x1b[m ad minim\nveniam, quis nostrud\nexercitation ullamco laboris\nnisi ut aliquip ex ea \x1b[38;5;200mcommodo\nconsequat\x1b[m. Duis aute irure\ndolor in reprehenderit in\nvoluptate velit esse cillum\ndolore eu fugiat nulla\npariatur. \x1b[1;2;33mExcepteur sint\noccaecat cupidatat non\nproident, sunt in culpa qui\nofficia deserunt mollit anim\nid est laborum.\x1b[m", width: 30, }, + {"hyphen break", "foo-bar", "foo-\nbar", 5}, + {"double space", "f bar foobaz", "f bar\nfoobaz", 6}, + {"passthrough", "foobar\n ", "foobar\n ", 0}, + {"pass", "foo", "foo", 3}, + {"toolong", "foobarfoo", "foob\narfo\no", 4}, + {"white space", "foo bar foo", "foo\nbar\nfoo", 4}, + {"broken_at_spaces", "foo bars foobars", "foo\nbars\nfoob\nars", 4}, + {"hyphen", "foob-foobar", "foob\n-foo\nbar", 4}, + {"wide_emoji_breakpoint", "foo🫧 foobar", "foo\n🫧\nfoob\nar", 4}, + {"space_breakpoint", "foo --bar", "foo --bar", 9}, + {"simple", "foo bars foobars", "foo\nbars\nfoob\nars", 4}, + {"limit", "foo bar", "foo\nbar", 5}, + {"remove white spaces", "foo \nb ar ", "foo\nb\nar", 4}, + {"white space trail width", "foo\nb\t a\n bar", "foo\nb\t a\n bar", 4}, + {"explicit_line_break", "foo bar foo\n", "foo\nbar\nfoo\n", 4}, + {"explicit_breaks", "\nfoo bar\n\n\nfoo\n", "\nfoo\nbar\n\n\nfoo\n", 4}, + {"example", " This is a list: \n\n\t* foo\n\t* bar\n\n\n\t* foo \nbar ", " This\nis a\nlist: \n\n\t* foo\n\t* bar\n\n\n\t* foo\nbar", 6}, + {"style_code_dont_affect_length", "\x1B[38;2;249;38;114mfoo\x1B[0m\x1B[38;2;248;248;242m \x1B[0m\x1B[38;2;230;219;116mbar\x1B[0m", "\x1B[38;2;249;38;114mfoo\x1B[0m\x1B[38;2;248;248;242m \x1B[0m\x1B[38;2;230;219;116mbar\x1B[0m", 7}, + {"style_code_dont_get_wrapped", "\x1B[38;2;249;38;114m(\x1B[0m\x1B[38;2;248;248;242mjust another test\x1B[38;2;249;38;114m)\x1B[0m", "\x1b[38;2;249;38;114m(\x1b[0m\x1b[38;2;248;248;242mjust\nanother\ntest\x1b[38;2;249;38;114m)\x1b[0m", 7}, + {"osc8_wrap", "สวัสดีสวัสดี\x1b]8;;https://example.com\x1b\\ สวัสดีสวัสดี\x1b]8;;\x1b\\", "สวัสดีสวัสดี\x1b]8;;https://example.com\x1b\\\nสวัสดีสวัสดี\x1b]8;;\x1b\\", 8}, } -func TestSmartWrap(t *testing.T) { - for i, tc := range smartWrapCases { +func TestWrap(t *testing.T) { + for i, tc := range wrapCases { t.Run(tc.name, func(t *testing.T) { output := ansi.Wrap(tc.input, tc.width, "") if output != tc.expected { From 1f1d7e4a186a977c64d34fa00b5e5805c8c56f71 Mon Sep 17 00:00:00 2001 From: Ayman Bagabas Date: Mon, 1 Apr 2024 16:11:39 -0700 Subject: [PATCH 2/9] Update wrap.go --- exp/term/ansi/wrap.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/exp/term/ansi/wrap.go b/exp/term/ansi/wrap.go index fe2fccfc..55ef0780 100644 --- a/exp/term/ansi/wrap.go +++ b/exp/term/ansi/wrap.go @@ -134,7 +134,7 @@ func Wordwrap(s string, limit int, breakpoints string) string { } addWord := func() { - if wordLen > 0 { + if curWidth > 0 { addSpace() } curWidth += wordLen From d5b68c691f4bbef29f92528f1ddf262788198bcb Mon Sep 17 00:00:00 2001 From: Ayman Bagabas Date: Mon, 1 Apr 2024 16:11:44 -0700 Subject: [PATCH 3/9] Update wrap.go --- exp/term/ansi/wrap.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/exp/term/ansi/wrap.go b/exp/term/ansi/wrap.go index 55ef0780..4f322727 100644 --- a/exp/term/ansi/wrap.go +++ b/exp/term/ansi/wrap.go @@ -267,7 +267,7 @@ func Wrap(s string, limit int, breakpoints string) string { addWord := func() { addBpoint() - if wordLen > 0 { + if curWidth > 0 { // We use wordLen to determine if we have a word to add // to the buffer. If wordLen is 0, we don't add spaces at the // beginning of a line. From 37ba4e2c7fcc19ec86e61582b2f98c306f3989de Mon Sep 17 00:00:00 2001 From: Ayman Bagabas Date: Mon, 1 Apr 2024 16:21:17 -0700 Subject: [PATCH 4/9] Update wrap.go --- exp/term/ansi/wrap.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/exp/term/ansi/wrap.go b/exp/term/ansi/wrap.go index 4f322727..24784a58 100644 --- a/exp/term/ansi/wrap.go +++ b/exp/term/ansi/wrap.go @@ -134,7 +134,7 @@ func Wordwrap(s string, limit int, breakpoints string) string { } addWord := func() { - if curWidth > 0 { + if curWidth > 0 && word.Len() > 0 { addSpace() } curWidth += wordLen From 3a164899c21b8965acf5f3316fb0153fa86d6c04 Mon Sep 17 00:00:00 2001 From: Ayman Bagabas Date: Mon, 1 Apr 2024 16:21:22 -0700 Subject: [PATCH 5/9] Update wrap.go --- exp/term/ansi/wrap.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/exp/term/ansi/wrap.go b/exp/term/ansi/wrap.go index 24784a58..5ad91dfe 100644 --- a/exp/term/ansi/wrap.go +++ b/exp/term/ansi/wrap.go @@ -267,7 +267,7 @@ func Wrap(s string, limit int, breakpoints string) string { addWord := func() { addBpoint() - if curWidth > 0 { + if curWidth > 0 && word.Len() > 0 { // We use wordLen to determine if we have a word to add // to the buffer. If wordLen is 0, we don't add spaces at the // beginning of a line. From 014cbcce7305f1f8c8a1dddeaff1f8dc45583f4e Mon Sep 17 00:00:00 2001 From: Ayman Bagabas Date: Wed, 3 Apr 2024 07:27:16 +0300 Subject: [PATCH 6/9] wip --- exp/term/ansi/wrap.go | 14 +++++++------- exp/term/ansi/wrap_test.go | 3 ++- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/exp/term/ansi/wrap.go b/exp/term/ansi/wrap.go index 5ad91dfe..9b9c5a18 100644 --- a/exp/term/ansi/wrap.go +++ b/exp/term/ansi/wrap.go @@ -134,9 +134,11 @@ func Wordwrap(s string, limit int, breakpoints string) string { } addWord := func() { - if curWidth > 0 && word.Len() > 0 { - addSpace() + if wordLen == 0 { + return } + + addSpace() curWidth += wordLen buf.Write(word.Bytes()) word.Reset() @@ -267,13 +269,11 @@ func Wrap(s string, limit int, breakpoints string) string { addWord := func() { addBpoint() - if curWidth > 0 && word.Len() > 0 { - // We use wordLen to determine if we have a word to add - // to the buffer. If wordLen is 0, we don't add spaces at the - // beginning of a line. - addSpace() + if word.Len() == 0 { + return } + addSpace() curWidth += wordLen buf.Write(word.Bytes()) word.Reset() diff --git a/exp/term/ansi/wrap_test.go b/exp/term/ansi/wrap_test.go index 109273e7..8aa0ba09 100644 --- a/exp/term/ansi/wrap_test.go +++ b/exp/term/ansi/wrap_test.go @@ -165,9 +165,10 @@ var wrapCases = []struct { width: 3, }, { + // TODO: fixme: this is a bug name: "extra space", input: "\x1b[mfoo \x1b[m", - expected: "\x1b[mfoo\x1b[m", + expected: "\x1b[mfoo \x1b[m", width: 3, }, { From fb0cfc3de7408d9f1f879d8bac8d8ee7ae3168ac Mon Sep 17 00:00:00 2001 From: Ayman Bagabas Date: Thu, 4 Apr 2024 04:42:08 +0300 Subject: [PATCH 7/9] fix --- exp/term/ansi/wrap.go | 7 ++++++- exp/term/ansi/wrap_test.go | 8 +++++++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/exp/term/ansi/wrap.go b/exp/term/ansi/wrap.go index 9b9c5a18..c3c39132 100644 --- a/exp/term/ansi/wrap.go +++ b/exp/term/ansi/wrap.go @@ -134,7 +134,7 @@ func Wordwrap(s string, limit int, breakpoints string) string { } addWord := func() { - if wordLen == 0 { + if word.Len() == 0 { return } @@ -301,6 +301,11 @@ func Wrap(s string, limit int, breakpoints string) string { if r != utf8.RuneError && unicode.IsSpace(r) { addWord() space.WriteRune(r) + } else if bytes.ContainsAny(cluster, breakpoints) { + addSpace() + addWord() + buf.Write(cluster) + curWidth++ } else { if wordLen+width > limit { // If the word is longer than the limit, we break it diff --git a/exp/term/ansi/wrap_test.go b/exp/term/ansi/wrap_test.go index 8aa0ba09..9a9a361c 100644 --- a/exp/term/ansi/wrap_test.go +++ b/exp/term/ansi/wrap_test.go @@ -165,8 +165,14 @@ var wrapCases = []struct { width: 3, }, { - // TODO: fixme: this is a bug name: "extra space", + input: "foo ", + expected: "foo", + width: 3, + }, + { + // FIXME: invalid expected + name: "extra space style", input: "\x1b[mfoo \x1b[m", expected: "\x1b[mfoo \x1b[m", width: 3, From 5b3147b8bb415192a53a05cb2312b840b79abf67 Mon Sep 17 00:00:00 2001 From: Ayman Bagabas Date: Fri, 5 Apr 2024 14:46:18 +0300 Subject: [PATCH 8/9] fix: preserve spaces in ansi strings and account for breakpoints Breakpoints are now respected and wrapped properly. Support non-breaking spaces --- exp/term/ansi/wrap.go | 106 +++++++++++++++++++++---------------- exp/term/ansi/wrap_test.go | 14 +++-- 2 files changed, 71 insertions(+), 49 deletions(-) diff --git a/exp/term/ansi/wrap.go b/exp/term/ansi/wrap.go index c3c39132..c83e3483 100644 --- a/exp/term/ansi/wrap.go +++ b/exp/term/ansi/wrap.go @@ -9,6 +9,9 @@ import ( "github.com/rivo/uniseg" ) +// nbsp is a non-breaking space +const nbsp = 0xA0 + // Hardwrap wraps a string or a block of text to a given line length, breaking // word boundaries. This will preserve ANSI escape codes and will account for // wide-characters in the string. @@ -106,15 +109,12 @@ func Hardwrap(s string, limit int, preserveSpace bool) string { // breakpoints for word wrapping. A hyphen (-) is always considered a // breakpoint. // -// Note: breakpoints must be a string of 1-cell wide rune character. +// Note: breakpoints must be a string of 1-cell wide rune characters. func Wordwrap(s string, limit int, breakpoints string) string { if limit < 1 { return s } - // Add a hyphen to the breakpoints - breakpoints += "-" - var ( cluster []byte buf bytes.Buffer @@ -163,9 +163,14 @@ func Wordwrap(s string, limit int, breakpoints string) string { i += len(cluster) r, _ := utf8.DecodeRune(cluster) - if r != utf8.RuneError && unicode.IsSpace(r) { + if r != utf8.RuneError && unicode.IsSpace(r) && r != nbsp { addWord() space.WriteRune(r) + } else if bytes.ContainsAny(cluster, breakpoints) { + addSpace() + addWord() + buf.Write(cluster) + curWidth++ } else { word.Write(cluster) wordLen += width @@ -197,6 +202,8 @@ func Wordwrap(s string, limit int, breakpoints string) string { case unicode.IsSpace(r): addWord() space.WriteByte(b[i]) + case r == '-': + fallthrough case runeContainsAny(r, breakpoints): addSpace() addWord() @@ -233,34 +240,24 @@ func Wordwrap(s string, limit int, breakpoints string) string { // of characters that are considered breakpoints for word wrapping. A hyphen // (-) is always considered a breakpoint. // -// Note: breakpoints must be a string of 1-cell wide rune character. +// Note: breakpoints must be a string of 1-cell wide rune characters. func Wrap(s string, limit int, breakpoints string) string { if limit < 1 { return s } - // Add a hyphen to the breakpoints - breakpoints += "-" - var ( cluster []byte buf bytes.Buffer word bytes.Buffer space bytes.Buffer - bpoint bytes.Buffer - curWidth int - wordLen int + curWidth int // written width of the line + wordLen int // word buffer len without ANSI escape codes gstate = -1 pstate = parser.GroundState // initial state b = []byte(s) ) - addBpoint := func() { - curWidth += bpoint.Len() - buf.Write(bpoint.Bytes()) - bpoint.Reset() - } - addSpace := func() { curWidth += space.Len() buf.Write(space.Bytes()) @@ -268,7 +265,6 @@ func Wrap(s string, limit int, breakpoints string) string { } addWord := func() { - addBpoint() if word.Len() == 0 { return } @@ -298,23 +294,30 @@ func Wrap(s string, limit int, breakpoints string) string { i += len(cluster) r, _ := utf8.DecodeRune(cluster) - if r != utf8.RuneError && unicode.IsSpace(r) { + switch { + case r != utf8.RuneError && unicode.IsSpace(r) && r != nbsp: // nbsp is a non-breaking space addWord() space.WriteRune(r) - } else if bytes.ContainsAny(cluster, breakpoints) { + case bytes.ContainsAny(cluster, breakpoints): addSpace() - addWord() - buf.Write(cluster) - curWidth++ - } else { + if curWidth+wordLen+width > limit { + word.Write(cluster) + wordLen += width + } else { + addWord() + buf.Write(cluster) + curWidth += width + } + default: if wordLen+width > limit { - // If the word is longer than the limit, we break it + // Hardwrap the word if it's too long addWord() } + word.Write(cluster) wordLen += width - if curWidth+space.Len()+wordLen+bpoint.Len() > limit { - addBpoint() + + if curWidth+wordLen+space.Len() > limit { addNewline() } } @@ -322,15 +325,16 @@ func Wrap(s string, limit int, breakpoints string) string { pstate = parser.GroundState continue } + fallthrough case parser.ExecuteAction: - r := rune(b[i]) - switch { + switch r := rune(b[i]); { case r == '\n': if wordLen == 0 { if curWidth+space.Len() > limit { curWidth = 0 } else { + // preserve whitespaces buf.Write(space.Bytes()) } space.Reset() @@ -340,26 +344,31 @@ func Wrap(s string, limit int, breakpoints string) string { addNewline() case unicode.IsSpace(r): addWord() - space.WriteByte(b[i]) + space.WriteRune(r) + case r == '-': + fallthrough case runeContainsAny(r, breakpoints): addSpace() - addWord() - if curWidth+1 <= limit { - bpoint.WriteByte(b[i]) - break + if curWidth+wordLen+1 > limit { + // We can't fit the breakpoint in the current line, treat + // it as part of the word. + word.WriteRune(r) + wordLen++ + } else { + addWord() + buf.WriteRune(r) + curWidth++ } - // If we can't fit the breakpoint in the current line, we treat - // it as a word character. - fallthrough default: - if wordLen >= limit { - // If the word is longer than the limit, we break it + word.WriteRune(r) + wordLen++ + + if wordLen == limit { + // Hardwrap the word if it's too long addWord() } - word.WriteByte(b[i]) - wordLen++ - if curWidth+space.Len()+wordLen+bpoint.Len() > limit { - addBpoint() + + if curWidth+wordLen+space.Len() > limit { addNewline() } } @@ -375,7 +384,14 @@ func Wrap(s string, limit int, breakpoints string) string { i++ } - addWord() + if word.Len() != 0 { + // Preserve ANSI wrapped spaces at the end of string + if curWidth+space.Len() > limit { + buf.WriteByte('\n') + } + addSpace() + } + buf.Write(word.Bytes()) return buf.String() } diff --git a/exp/term/ansi/wrap_test.go b/exp/term/ansi/wrap_test.go index 9a9a361c..026c3b5f 100644 --- a/exp/term/ansi/wrap_test.go +++ b/exp/term/ansi/wrap_test.go @@ -128,6 +128,12 @@ var wrapCases = []struct { expected: "\x1B[38;2;249;38;114ma really\nlong\nstring\x1B[0m", width: 10, }, + { + name: "long style nbsp", + input: "\x1B[38;2;249;38;114ma really\u00a0long string\x1B[0m", + expected: "\x1b[38;2;249;38;114ma\nreally\u00a0lon\ng string\x1b[0m", + width: 10, + }, { name: "longer", input: "the quick brown foxxxxxxxxxxxxxxxx jumped over the lazy dog.", @@ -143,7 +149,7 @@ var wrapCases = []struct { { name: "long input", input: "Rotated keys for a-good-offensive-cheat-code-incorporated/animal-like-law-on-the-rocks.", - expected: "Rotated keys for a-good-offensive-cheat-code-incorporated/animal-like-law-on\n-the-rocks.", + expected: "Rotated keys for a-good-offensive-cheat-code-incorporated/animal-like-law-\non-the-rocks.", width: 76, }, { @@ -165,16 +171,16 @@ var wrapCases = []struct { width: 3, }, { + // XXX: Should we preserve spaces on text wrapping? name: "extra space", input: "foo ", expected: "foo", width: 3, }, { - // FIXME: invalid expected name: "extra space style", input: "\x1b[mfoo \x1b[m", - expected: "\x1b[mfoo \x1b[m", + expected: "\x1b[mfoo\n \x1b[m", width: 3, }, { @@ -210,7 +216,7 @@ func TestWrap(t *testing.T) { t.Run(tc.name, func(t *testing.T) { output := ansi.Wrap(tc.input, tc.width, "") if output != tc.expected { - t.Errorf("case %d, expected %q, got %q", i+1, tc.expected, output) + t.Errorf("case %d, input %q, expected %q, got %q", i+1, tc.input, tc.expected, output) } }) } From 0de7fd253ac4836949645ed882f1492682b8967f Mon Sep 17 00:00:00 2001 From: Ayman Bagabas Date: Sun, 7 Apr 2024 17:44:23 -0700 Subject: [PATCH 9/9] Update wrap.go --- exp/term/ansi/wrap.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/exp/term/ansi/wrap.go b/exp/term/ansi/wrap.go index c83e3483..5413d0c6 100644 --- a/exp/term/ansi/wrap.go +++ b/exp/term/ansi/wrap.go @@ -349,7 +349,7 @@ func Wrap(s string, limit int, breakpoints string) string { fallthrough case runeContainsAny(r, breakpoints): addSpace() - if curWidth+wordLen+1 > limit { + if curWidth+wordLen >= limit { // We can't fit the breakpoint in the current line, treat // it as part of the word. word.WriteRune(r)