diff --git a/tools/utils/style/indent-and-wrap.go b/tools/utils/style/indent-and-wrap.go index ce7bb0fbc..f69163ccc 100644 --- a/tools/utils/style/indent-and-wrap.go +++ b/tools/utils/style/indent-and-wrap.go @@ -4,7 +4,6 @@ package style import ( "fmt" - "io" "strconv" "strings" "unicode" @@ -64,40 +63,67 @@ func (self *sgr_state) reset() { *self = sgr_state{} } -func (self sgr_state) as_sgr() string { +func (self sgr_state) as_sgr(for_close bool) string { ans := make([]string, 0, 4) - if self.bold { - ans = append(ans, "1") - } - if self.dim { - ans = append(ans, "2") - } - if self.italic { - ans = append(ans, "3") - } - if self.reverse { - ans = append(ans, "7") - } - if self.strikethrough { - ans = append(ans, "9") - } - if self.underline_style != no_underline && self.underline_style != nil_underline { - ans = append(ans, fmt.Sprintf("4:%d", self.underline_style)) - } - if q := self.fg.as_sgr(30); q != "" { - ans = append(ans, q) - } - if q := self.bg.as_sgr(40); q != "" { - ans = append(ans, q) - } - if q := self.uc.as_sgr(50); q != "" { - ans = append(ans, q) + if for_close { + if self.bold || self.dim { + ans = append(ans, "22") + } + if self.italic { + ans = append(ans, "23") + } + if self.reverse { + ans = append(ans, "27") + } + if self.strikethrough { + ans = append(ans, "29") + } + if self.underline_style != no_underline && self.underline_style != nil_underline { + ans = append(ans, "4:0") + } + if self.fg.number != 0 { + ans = append(ans, "39") + } + if self.bg.number != 0 { + ans = append(ans, "49") + } + if self.uc.number != 0 { + ans = append(ans, "59") + } + } else { + if self.bold { + ans = append(ans, "1") + } + if self.dim { + ans = append(ans, "2") + } + if self.italic { + ans = append(ans, "3") + } + if self.reverse { + ans = append(ans, "7") + } + if self.strikethrough { + ans = append(ans, "9") + } + if self.underline_style != no_underline && self.underline_style != nil_underline { + ans = append(ans, fmt.Sprintf("4:%d", self.underline_style)) + } + if q := self.fg.as_sgr(30); q != "" { + ans = append(ans, q) + } + if q := self.bg.as_sgr(40); q != "" { + ans = append(ans, q) + } + if q := self.uc.as_sgr(50); q != "" { + ans = append(ans, q) + } } return strings.Join(ans, ";") } -func (self sgr_state) as_escape_codes() string { - q := self.as_sgr() +func (self sgr_state) as_escape_codes(for_close bool) string { + q := self.as_sgr(for_close) if q == "" { return q } @@ -116,11 +142,11 @@ func (self *sgr_state) apply_csi(raw string) { nums := make([]int, 0, 8) for _, part := range parts { subparts := strings.Split(part, ":") - nums = nums[:len(subparts)] - for i, b := range subparts { + nums = nums[:0] + for _, b := range subparts { q, err := strconv.Atoi(b) - if err != nil { - nums[i] = q + if err == nil { + nums = append(nums, q) } } if len(nums) == 0 { @@ -210,93 +236,164 @@ func (self hyperlink_state) reset() { self.url = "" } -func (self hyperlink_state) as_escape_codes() string { +func (self hyperlink_state) as_escape_codes(for_close bool) string { if self.id == "" && self.url == "" { return "" } + if for_close { + return "\x1b]8;;\x1b\\" + } return fmt.Sprintf("\x1b]8;%s;%s\x1b\\", self.id, self.url) } +type line_builder struct { + buf strings.Builder + last_text_pos, cursor_pos int +} + +func (self *line_builder) reset() string { + ans := self.buf.String() + if len(ans) > self.last_text_pos { + ans = ans[:self.last_text_pos] + } + sz := self.buf.Len() + self.buf.Reset() + self.last_text_pos = 0 + self.cursor_pos = 0 + if sz > 1024 { + self.buf.Grow(sz) + } else { + self.buf.Grow(1024) + } + return ans +} + +func (self *line_builder) has_space_for_width(w, max_width int) bool { + return w+self.cursor_pos <= max_width +} + +func (self *line_builder) add_char(ch rune) { + self.buf.WriteRune(ch) + self.last_text_pos = self.buf.Len() + self.cursor_pos += wcswidth.Runewidth(ch) +} + +func (self *line_builder) add_word(word string, width int) { + self.buf.WriteString(word) + self.last_text_pos = self.buf.Len() + self.cursor_pos += width +} + +func (self *line_builder) add_escape_code(code string) { + self.buf.WriteString(code) +} + +func (self *line_builder) add_escape_code2(prefix string, body []byte, suffix string) { + self.buf.WriteString(prefix) + self.buf.Write(body) + self.buf.WriteString(suffix) +} + type wrapper struct { ep wcswidth.EscapeCodeParser - output io.Writer indent string width, indent_width int sgr sgr_state hyperlink hyperlink_state current_word strings.Builder - x int + current_line line_builder + lines []string } -func (self *wrapper) print_newline() { - fmt.Fprint(self.output, "\x1b[m\x1b]8;;\x1b\\\n", self.indent, self.sgr.as_escape_codes(), self.hyperlink.as_escape_codes()) - self.x = self.indent_width +func (self *wrapper) newline_prefix() { + self.current_line.add_escape_code(self.sgr.as_escape_codes(true)) + self.current_line.add_escape_code(self.hyperlink.as_escape_codes(true)) + self.current_line.add_word(self.indent, self.indent_width) + self.current_line.add_escape_code(self.sgr.as_escape_codes(false)) + self.current_line.add_escape_code(self.hyperlink.as_escape_codes(false)) } -func (self *wrapper) print_word(ch rune) { +func (self *wrapper) end_current_line() { + line := self.current_line.reset() + line = strings.TrimRightFunc(line, unicode.IsSpace) + if strings.HasSuffix(line, self.indent) && wcswidth.Stringwidth(line) == self.indent_width { + line = line[:len(line)-len(self.indent)] + } + self.lines = append(self.lines, line) + self.newline_prefix() +} + +func (self *wrapper) print_word() { w := wcswidth.Stringwidth(self.current_word.String()) - if self.x+w > self.width { - self.print_newline() + if !self.current_line.has_space_for_width(w, self.width) { + self.end_current_line() s := strings.TrimSpace(self.current_word.String()) self.current_word.Reset() self.current_word.WriteString(s) + w = wcswidth.Stringwidth(s) } - fmt.Fprint(self.output, self.current_word.String()) + self.current_line.add_word(self.current_word.String(), w) self.current_word.Reset() - if ch > 0 { - self.current_word.WriteRune(ch) - } - self.x += w } func (self *wrapper) handle_rune(ch rune) error { if ch == '\n' { - self.print_newline() + self.print_word() + self.end_current_line() } else if self.current_word.Len() != 0 && ch != 0xa0 && unicode.IsSpace(ch) { - self.print_word(ch) + self.print_word() + self.current_line.add_char(ch) } else { self.current_word.WriteRune(ch) } - - io.WriteString(self.output, string(ch)) return nil } func (self *wrapper) handle_csi(raw []byte) error { self.sgr.apply_csi(utils.UnsafeBytesToString(raw)) - self.output.Write(raw) + self.current_line.add_escape_code2("\x1b[", raw, "") return nil } func (self *wrapper) handle_osc(raw []byte) error { self.hyperlink.apply_osc(utils.UnsafeBytesToString(raw)) - self.output.Write(raw) + self.current_line.add_escape_code2("\x1b]", raw, "\x1b\\") return nil } -func (self *wrapper) wrap_text(text string) { - self.x = self.indent_width - fmt.Fprint(self.output, self.indent) +func (self *wrapper) wrap_text(text string) string { + if text == "" { + return text + } + self.current_line.reset() + self.current_word.Reset() + self.lines = self.lines[:0] + self.current_line.add_word(self.indent, self.indent_width) self.ep.ParseString(text) if self.current_word.Len() > 0 { - self.print_word(0) + self.print_word() } - if len(text) > 0 { - self.print_newline() + self.end_current_line() + last_line := self.current_line.reset() + self.newline_prefix() + if last_line == self.current_line.reset() { + last_line = "" } + self.lines = append(self.lines, last_line) + return strings.Join(self.lines, "\n") } -func new_wrapper(output io.Writer, indent string, width int) *wrapper { - ans := wrapper{output: output, indent: indent, width: width, indent_width: wcswidth.Stringwidth(indent)} +func new_wrapper(indent string, width int) *wrapper { + ans := wrapper{indent: indent, width: width, indent_width: wcswidth.Stringwidth(indent)} ans.ep.HandleRune = ans.handle_rune ans.ep.HandleCSI = ans.handle_csi ans.ep.HandleOSC = ans.handle_osc + ans.lines = make([]string, 0, 32) return &ans } -func WrapText(text string, output io.Writer, indent string, width int) { - w := new_wrapper(output, indent, width) - w.wrap_text(text) - return +func WrapText(text string, indent string, width int) string { + w := new_wrapper(indent, width) + return w.wrap_text(text) } diff --git a/tools/utils/style/indent-and-wrap_test.go b/tools/utils/style/indent-and-wrap_test.go index b825bcb35..620db74a7 100644 --- a/tools/utils/style/indent-and-wrap_test.go +++ b/tools/utils/style/indent-and-wrap_test.go @@ -8,19 +8,19 @@ import ( ) func TestFormatWithIndent(t *testing.T) { - var output strings.Builder indent := "__" screen_width := 11 - run := func(text string, expected ...string) { - output.Reset() - q := indent + strings.Join(expected, "\n"+indent) + "\n" - WrapText(text, &output, indent, screen_width) - if output.String() != q { - t.Fatalf("expected != actual: %#v != %#v", q, output.String()) + tx := func(text string, expected ...string) { + q := indent + strings.Join(expected, "") + "\n" + actual := WrapText(text, indent, screen_width) + if actual != q { + t.Fatalf("%#v\nexpected: %#v\nactual: %#v", text, q, actual) } } - run("testing \x1b[31mstyled\x1b[m", "testing ", "\x1b[31mstyled\x1b[m") - run("testing\n\ntwo", "testing", "", "two") - run("testing\n \ntwo", "testing", "", "two") + tx("testing\n\ntwo", "testing\n\n__two") + tx("testing\n \ntwo", "testing\n\n__two") + + tx("123456 \x1b[31m789a", "123456\n\x1b[39m__\x1b[31m789a") + tx("12 \x1b[31m789 abcd", "12 \x1b[31m789\n\x1b[39m__\x1b[31mabcd") }