diff --git a/go.mod b/go.mod index ff8f7f37..60d49091 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.19 require ( code.rocketnine.space/tslocum/cbind v0.1.5 github.com/atotto/clipboard v0.1.4 - github.com/creack/pty v1.1.21 + github.com/creack/pty v1.1.23 github.com/fsnotify/fsnotify v1.7.0 github.com/gdamore/tcell/v2 v2.7.4 github.com/hashicorp/golang-lru/v2 v2.0.7 diff --git a/go.sum b/go.sum index 212e9835..5c1a605b 100644 --- a/go.sum +++ b/go.sum @@ -3,8 +3,8 @@ code.rocketnine.space/tslocum/cbind v0.1.5/go.mod h1:LtfqJTzM7qhg88nAvNhx+VnTjZ0 github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= -github.com/creack/pty v1.1.21 h1:1/QdRyBaHHJP61QkWMXlOIBfsgdDeeKfK8SYVUWJKf0= -github.com/creack/pty v1.1.21/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= +github.com/creack/pty v1.1.23 h1:4M6+isWdcStXEf15G/RbrMPOQj1dZ7HPZCGwE4kOeP0= +github.com/creack/pty v1.1.23/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= diff --git a/oviewer/content.go b/oviewer/content.go index fde486dc..0120ea48 100644 --- a/oviewer/content.go +++ b/oviewer/content.go @@ -28,6 +28,14 @@ var DefaultContent = content{ style: tcell.StyleDefault, } +// SpaceContent is a space character. +var SpaceContent = content{ + mainc: ' ', + combc: nil, + width: 1, + style: tcell.StyleDefault, +} + // EOFC is the EOF character. const EOFC rune = '~' @@ -96,6 +104,9 @@ func parseString(conv Converter, str string, tabWidth int) contents { } st.parseChar(st.mainc, st.combc) } + st.mainc = '\n' + st.combc = nil + conv.convert(st) return st.lc } diff --git a/oviewer/convert_align.go b/oviewer/convert_align.go new file mode 100644 index 00000000..305cd4a1 --- /dev/null +++ b/oviewer/convert_align.go @@ -0,0 +1,108 @@ +package oviewer + +import ( + "regexp" + + "github.com/mattn/go-runewidth" +) + +// align is a converter that aligns columns. +// It is used to align columns when the delimiter is reached or to align columns by adding spaces to the end of the line. +type align struct { + es *escapeSequence + maxWidths []int // column max width + orgWidths []int + WidthF bool + delimiter string + delimiterReg *regexp.Regexp + count int +} + +func newAlignConverter(widthF bool) *align { + return &align{ + es: newESConverter(), + count: 0, + WidthF: widthF, + } +} + +// convert converts only escape sequence codes to display characters and returns them as is. +// Returns true if it is an escape sequence and a non-printing character. +func (a *align) convert(st *parseState) bool { + if len(st.lc) == 0 { + a.reset() + } + if a.es.convert(st) { + return true + } + + if len(a.maxWidths) == 0 { + return false + } + a.count += 1 + if runewidth.RuneWidth(st.mainc) > 1 { + a.count += 1 + } + + if st.mainc != '\n' { + return false + } + + if a.WidthF { + return a.convertWidth(st) + } + return a.convertDelm(st) +} + +func (a *align) reset() { + a.count = 0 +} + +// convertDelm aligns the column widths by adding spaces when it reaches a delimiter. +// convertDelm works line by line. +func (a *align) convertDelm(st *parseState) bool { + str, pos := ContentsToStr(st.lc) + indexes := allIndex(str, a.delimiter, a.delimiterReg) + if len(indexes) == 0 { + return false + } + s := 0 + lc := make(contents, 0, len(st.lc)) + for c := 0; c < len(indexes); c++ { + e := pos.x(indexes[c][0]) + lc = append(lc, st.lc[s:e]...) + width := e - s + // Add space to align columns. + for ; width < a.maxWidths[c]; width++ { + lc = append(lc, SpaceContent) + } + s = e + } + lc = append(lc, st.lc[s:]...) + st.lc = lc + return false +} + +// convertWidth accumulates one line and then adds spaces to align the column widths. +// convertWidth works line by line. +func (a *align) convertWidth(st *parseState) bool { + s := 0 + lc := make(contents, 0, len(st.lc)) + for i := 0; i < len(a.orgWidths); i++ { + e := findColumnEnd(st.lc, a.orgWidths, i) + 1 + e = min(e, len(st.lc)) + width := e - s + if s >= e { + break + } + lc = append(lc, st.lc[s:e]...) + // Add space to align columns. + for ; width <= a.maxWidths[i]; width++ { + lc = append(lc, SpaceContent) + } + s = e + } + lc = append(lc, st.lc[s:]...) + st.lc = lc + return false +} diff --git a/oviewer/convert_es.go b/oviewer/convert_es.go index a01e7db5..d0481eab 100644 --- a/oviewer/convert_es.go +++ b/oviewer/convert_es.go @@ -149,7 +149,7 @@ func (es *escapeSequence) convert(st *parseState) bool { es.state = ansiEscape return true case '\n': - return true + return false } return false } diff --git a/oviewer/document.go b/oviewer/document.go index 4dfa8474..aac224fb 100644 --- a/oviewer/document.go +++ b/oviewer/document.go @@ -55,6 +55,8 @@ type Document struct { // conv is an interface that converts escape sequences, etc. conv Converter + // alignConv is an interface that converts alignment. + alignConv *align // fileName is the file name to display. FileName string @@ -222,6 +224,7 @@ func NewDocument() (*Document, error) { if err := m.NewCache(); err != nil { return nil, err } + m.alignConv = newAlignConverter(m.ColumnWidth) m.conv = m.converterType(m.general.Converter) return m, nil } @@ -244,6 +247,8 @@ func (m *Document) converterType(name string) Converter { return newRawConverter() case "es": return newESConverter() + case "align": + return m.alignConv } return defaultConverter } diff --git a/oviewer/move_vertical.go b/oviewer/move_vertical.go index 6afee098..f871bc06 100644 --- a/oviewer/move_vertical.go +++ b/oviewer/move_vertical.go @@ -236,9 +236,16 @@ func leftX(width int, lc contents) []int { if width <= 0 { return []int{0} } - listX := make([]int, 0, (len(lc)/width)+1) + if len(lc) == 0 { + return []int{0} + } + end := len(lc) + if lc[len(lc)-1].mainc == '\n' { + end-- + } + listX := make([]int, 0, (end/width)+1) listX = append(listX, 0) - for n := width; n < len(lc); n += width { + for n := width; n < end; n += width { if lc[n-1].width == 2 { n-- } diff --git a/oviewer/prepare_draw.go b/oviewer/prepare_draw.go index 4ab6de21..752ba401 100644 --- a/oviewer/prepare_draw.go +++ b/oviewer/prepare_draw.go @@ -5,6 +5,7 @@ import ( "errors" "log" "math" + "reflect" "sort" "strconv" "time" @@ -81,14 +82,103 @@ func (root *Root) prepareDraw(ctx context.Context) { root.Doc.topLX, root.Doc.topLN = tX, tN-root.scr.headerEnd root.Doc.showGotoF = false } + if root.Doc.ColumnWidth && len(root.Doc.columnWidths) == 0 { root.Doc.setColumnWidths() } + // Sets the maximum width of a column. + if root.Doc.Converter == "align" { + root.setAlignColumnWidths() + } + // Prepare the lines. root.scr.lines = root.prepareLines(root.scr.lines) } +// setAlignColumnWidths sets the maximum width of the column. +func (root *Root) setAlignColumnWidths() { + m := root.Doc + m.alignConv.WidthF = m.ColumnWidth + if !m.alignConv.WidthF { + root.Doc.alignConv.delimiter = m.ColumnDelimiter + root.Doc.alignConv.delimiterReg = m.ColumnDelimiterReg + } + + maxWidths := make([]int, 0, len(m.alignConv.maxWidths)) + for ln := root.scr.headerLN; ln < root.scr.headerEnd; ln++ { + maxWidths = m.maxWidths(maxWidths, ln) + } + for ln := root.scr.sectionHeaderLN; ln < root.scr.sectionHeaderEnd; ln++ { + maxWidths = m.maxWidths(maxWidths, ln) + } + startLN := m.topLN + m.firstLine() + endLN := startLN + root.scr.vHeight + for ln := startLN; ln < endLN; ln++ { + maxWidths = m.maxWidths(maxWidths, ln) + } + + if !reflect.DeepEqual(m.alignConv.maxWidths, maxWidths) { + m.alignConv.orgWidths = m.columnWidths + m.alignConv.maxWidths = maxWidths + m.ClearCache() + } +} + +// maxWidths returns the maximum width of the column. +func (m *Document) maxWidths(columnWidth []int, lN int) []int { + str, err := m.LineStr(lN) + if err != nil { + return columnWidth + } + lc := StrToContents(str, m.TabWidth) + if m.ColumnWidth { + return m.maxWidthsWidth(lc, columnWidth) + } + return m.maxWidthsDelm(lc, columnWidth) +} + +// maxWidthsDelm returns the maximum width of the column. +func (m *Document) maxWidthsDelm(lc contents, columnWidth []int) []int { + str, pos := ContentsToStr(lc) + indexes := allIndex(str, m.ColumnDelimiter, m.ColumnDelimiterReg) + if len(indexes) == 0 { + return columnWidth + } + s := 0 + for i := 0; i < len(indexes); i++ { + e := pos.x(indexes[i][1]) + width := e - s + if len(columnWidth) <= i { + columnWidth = append(columnWidth, width) + } else { + columnWidth[i] = max(width, columnWidth[i]) + } + s = e + } + return columnWidth +} + +// maxWidthsWidth returns the maximum width of the column. +func (m *Document) maxWidthsWidth(lc contents, columnWidth []int) []int { + indexes := m.columnWidths + if len(indexes) == 0 { + return columnWidth + } + s := 0 + for i := 0; i < len(indexes); i++ { + e := findColumnEnd(lc, indexes, i) + 1 + width := e - s + if len(columnWidth) <= i { + columnWidth = append(columnWidth, width) + } else { + columnWidth[i] = max(width, columnWidth[i]) + } + s = e + } + return columnWidth +} + // shiftBody shifts the section header so that it is not hidden by it. func (m *Document) shiftBody(lX int, lN int, shStart int, shEnd int) (int, int) { if m.jumpTargetHeight != 0 { @@ -367,7 +457,16 @@ func (root *Root) columnWidthHighlight(line LineC) { start, end := -1, -1 for c := 0; c < len(indexes)+1; c++ { start = end + 1 - end = findColumnEnd(line.lc, indexes, c) + if m.Converter == "align" { + l := len(line.lc) + if c < len(m.alignConv.maxWidths) { + l = m.alignConv.maxWidths[c] + } + end = start + l + end = min(end, len(line.lc)) + } else { + end = findColumnEnd(line.lc, indexes, c) + } if m.ColumnRainbow { RangeStyle(line.lc, start, end, root.StyleColumnRainbow[c%numC]) diff --git a/oviewer/utils.go b/oviewer/utils.go index 0577161b..8eb3abb1 100644 --- a/oviewer/utils.go +++ b/oviewer/utils.go @@ -95,6 +95,13 @@ func allStringIndex(s string, substr string) [][]int { s = s[pos+width:] result = append(result, []int{pos + offSet, pos + offSet + width}) offSet += pos + width + + if len(s) > 0 && s[0] == '"' { + qpos := strings.Index(s[1:], `"`) + s = s[qpos+2:] + offSet += qpos + 2 + } + pos = strings.Index(s, substr) } return result diff --git a/oviewer/utils_test.go b/oviewer/utils_test.go index 51aa15bd..b9533d1a 100644 --- a/oviewer/utils_test.go +++ b/oviewer/utils_test.go @@ -520,6 +520,28 @@ func Test_allStringIndex(t *testing.T) { }, want: nil, }, + { + name: "testDoubleQuote", + args: args{ + s: `a,"b,c",d`, + substr: ",", + }, + want: [][]int{ + {1, 2}, + {7, 8}, + }, + }, + { + name: "testDoubleQuote2", + args: args{ + s: `a,"060 ",d`, + substr: ",", + }, + want: [][]int{ + {1, 2}, + {9, 10}, + }, + }, } for _, tt := range tests { tt := tt