From 70bf8bc35dfb31eb1963c92fa72e38261fa0056a Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Tue, 25 Jun 2024 17:08:47 +0900 Subject: Add --wrap option and 'toggle-wrap' action (#3887) * `--wrap` * `--wrap-sign` * `toggle-wrap` Close #3619 Close #2236 Close #577 Close #461 --- src/util/chars.go | 82 ++++++++++++++++++++++++++++++++++++++++++++++++++ src/util/chars_test.go | 39 +++++++++++++++++++++++- 2 files changed, 120 insertions(+), 1 deletion(-) (limited to 'src/util') diff --git a/src/util/chars.go b/src/util/chars.go index 82773f40..a0ea9436 100644 --- a/src/util/chars.go +++ b/src/util/chars.go @@ -226,3 +226,85 @@ func (chars *Chars) Prepend(prefix string) { chars.slice = append([]byte(prefix), chars.slice...) } } + +func (chars *Chars) Lines(multiLine bool, maxLines int, wrapCols int, wrapSignWidth int, tabstop int) ([][]rune, bool) { + text := make([]rune, chars.Length()) + copy(text, chars.ToRunes()) + + lines := [][]rune{} + overflow := false + if !multiLine { + lines = append(lines, text) + } else { + from := 0 + for off := 0; off < len(text); off++ { + if text[off] == '\n' { + lines = append(lines, text[from:off+1]) // Include '\n' + from = off + 1 + if len(lines) >= maxLines { + break + } + } + } + + var lastLine []rune + if from < len(text) { + lastLine = text[from:] + } + + overflow = false + if len(lines) >= maxLines { + overflow = true + } else { + lines = append(lines, lastLine) + } + } + + // If wrapping is disabled, we're done + if wrapCols == 0 { + return lines, overflow + } + + wrapped := [][]rune{} + for _, line := range lines { + // Remove trailing '\n' and remember if it was there + newline := len(line) > 0 && line[len(line)-1] == '\n' + if newline { + line = line[:len(line)-1] + } + + for { + cols := wrapCols + if len(wrapped) > 0 { + cols -= wrapSignWidth + } + _, overflowIdx := RunesWidth(line, 0, tabstop, cols) + if overflowIdx >= 0 { + // Might be a wide character + if overflowIdx == 0 { + overflowIdx = 1 + } + if len(wrapped) >= maxLines { + return wrapped, true + } + wrapped = append(wrapped, line[:overflowIdx]) + line = line[overflowIdx:] + continue + } + + // Restore trailing '\n' + if newline { + line = append(line, '\n') + } + + if len(wrapped) >= maxLines { + return wrapped, true + } + + wrapped = append(wrapped, line) + break + } + } + + return wrapped, false +} diff --git a/src/util/chars_test.go b/src/util/chars_test.go index b7983f30..0d3e4f37 100644 --- a/src/util/chars_test.go +++ b/src/util/chars_test.go @@ -1,6 +1,9 @@ package util -import "testing" +import ( + "fmt" + "testing" +) func TestToCharsAscii(t *testing.T) { chars := ToChars([]byte("foobar")) @@ -44,3 +47,37 @@ func TestTrimLength(t *testing.T) { check(" h o ", 5) check(" ", 0) } + +func TestCharsLines(t *testing.T) { + chars := ToChars([]byte("abcdef\n가나다\n\tdef")) + check := func(multiLine bool, maxLines int, wrapCols int, wrapSignWidth int, tabstop int, expectedNumLines int, expectedOverflow bool) { + lines, overflow := chars.Lines(multiLine, maxLines, wrapCols, wrapSignWidth, tabstop) + fmt.Println(lines, overflow) + if len(lines) != expectedNumLines || overflow != expectedOverflow { + t.Errorf("Invalid result: %d %v (expected %d %v)", len(lines), overflow, expectedNumLines, expectedOverflow) + } + } + + // No wrap + check(true, 1, 0, 0, 8, 1, true) + check(true, 2, 0, 0, 8, 2, true) + check(true, 3, 0, 0, 8, 3, false) + + // Wrap (2) + check(true, 4, 2, 0, 8, 4, true) + check(true, 5, 2, 0, 8, 5, true) + check(true, 6, 2, 0, 8, 6, true) + check(true, 7, 2, 0, 8, 7, true) + check(true, 8, 2, 0, 8, 8, true) + check(true, 9, 2, 0, 8, 9, false) + check(true, 9, 2, 0, 1, 8, false) // Smaller tab size + + // With wrap sign (3 + 1) + check(true, 100, 3, 1, 1, 8, false) + + // With wrap sign (3 + 2) + check(true, 100, 3, 2, 1, 12, false) + + // With wrap sign (3 + 2) and no multi-line + check(false, 100, 3, 2, 1, 13, false) +} -- cgit v1.2.3