diff --git a/go.mod b/go.mod index 400c98ec4..b2ac4b960 100644 --- a/go.mod +++ b/go.mod @@ -22,7 +22,7 @@ require ( github.com/gorilla/websocket v1.5.4-0.20240702125206-a62d9d2a8413 github.com/h2non/filetype v1.1.3 github.com/jackc/pgx/v5 v5.10.0 - github.com/jedib0t/go-pretty/v6 v6.8.2 + github.com/jedib0t/go-pretty/v6 v6.8.3 github.com/manifoldco/promptui v0.9.0 github.com/mattn/go-sqlite3 v1.14.47 github.com/nbutton23/zxcvbn-go v0.0.0-20210217022336-fa2cb2858354 diff --git a/go.sum b/go.sum index c0237c8f2..dc82e3b6a 100644 --- a/go.sum +++ b/go.sum @@ -141,8 +141,8 @@ github.com/jackc/pgx/v5 v5.10.0 h1:VhSvgU2jSli8o3AqIEOTJr7rZwAEUVo4E4XhR94Zfr0= github.com/jackc/pgx/v5 v5.10.0/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4= github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= -github.com/jedib0t/go-pretty/v6 v6.8.2 h1:FmKNr1GOyot/zqNQplE8HLhFguJaeHJTCArntnI4uxE= -github.com/jedib0t/go-pretty/v6 v6.8.2/go.mod h1:YwC5CE4fJ1HFUDeivSV1r//AmANFHyqczZk+U6BDALU= +github.com/jedib0t/go-pretty/v6 v6.8.3 h1:yVSk5aemoYHCvcrtqyXklwqcgHQIQzmy/oUzFlmffSQ= +github.com/jedib0t/go-pretty/v6 v6.8.3/go.mod h1:YwC5CE4fJ1HFUDeivSV1r//AmANFHyqczZk+U6BDALU= github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= diff --git a/vendor/github.com/jedib0t/go-pretty/v6/table/render.go b/vendor/github.com/jedib0t/go-pretty/v6/table/render.go index 37c4b6bd5..0e59bbff6 100644 --- a/vendor/github.com/jedib0t/go-pretty/v6/table/render.go +++ b/vendor/github.com/jedib0t/go-pretty/v6/table/render.go @@ -270,7 +270,7 @@ func (t *Table) renderRow(out *strings.Builder, row rowStr, hint renderHint) { // fit every column into the allowedColumnLength/maxColumnLength limit // and in the process find the max. number of lines in any column in // this row - colMaxLines, rowWrapped := t.wrapRow(row) + colMaxLines, rowWrapped := t.wrapRow(row, hint) // if there is just 1 line in all columns, add the row as such; else // split each column into individual lines and render them one-by-one @@ -313,9 +313,22 @@ func (t *Table) renderRowSeparator(out *strings.Builder, hint renderHint) { } func (t *Table) renderRows(out *strings.Builder, rows []rowStr, hint renderHint) { - for rowIdx, row := range rows { + interleaveVerticalMerge := t.shouldInterleaveVerticalMerge(hint) + for rowIdx := 0; rowIdx < len(rows); rowIdx++ { + row := rows[rowIdx] + + // stack rows that are being vertically merged into one shared block so + // the differing columns fill the merged cell's wrapped height instead + // of leaving blank lines below it (issue #261) + groupEnd := rowIdx + 1 + if interleaveVerticalMerge { + if groupEnd = t.verticalMergeGroupEnd(rows, rowIdx); groupEnd > rowIdx+1 { + row = t.combineVerticalMergeGroup(rows, rowIdx, groupEnd) + } + } + hint.isFirstRow = rowIdx == 0 - hint.isLastRow = rowIdx == len(rows)-1 + hint.isLastRow = groupEnd == len(rows) hint.rowNumber = rowIdx + 1 t.renderRow(out, row, hint) @@ -332,6 +345,9 @@ func (t *Table) renderRows(out *strings.Builder, rows []rowStr, hint renderHint) } t.renderRowSeparator(out, hintSep) } + + // skip the rows that were stacked into the block rendered above + rowIdx = groupEnd - 1 } } diff --git a/vendor/github.com/jedib0t/go-pretty/v6/table/table.go b/vendor/github.com/jedib0t/go-pretty/v6/table/table.go index f654f3680..bf1af4d9a 100644 --- a/vendor/github.com/jedib0t/go-pretty/v6/table/table.go +++ b/vendor/github.com/jedib0t/go-pretty/v6/table/table.go @@ -947,20 +947,187 @@ func (t *Table) shouldSeparateRows(rowIdx int, numRows int) bool { return true } -func (t *Table) wrapRow(row rowStr) (int, rowStr) { +// wrapCell fits a single column's value into its width limit (WidthMax, or the +// column's longest line when no limit is set) using the column's enforcer. +func (t *Table) wrapCell(colIdx int, colStr string) string { + widthEnforcer := t.columnConfigMap[colIdx].getWidthMaxEnforcer() + maxWidth := t.getColumnWidthMax(colIdx) + if maxWidth == 0 { + maxWidth = t.maxColumnLengths[colIdx] + } + return widthEnforcer(colStr, maxWidth) +} + +func (t *Table) wrapRow(row rowStr, hint renderHint) (int, rowStr) { colMaxLines := 0 rowWrapped := make(rowStr, len(row)) for colIdx, colStr := range row { - widthEnforcer := t.columnConfigMap[colIdx].getWidthMaxEnforcer() - maxWidth := t.getColumnWidthMax(colIdx) - if maxWidth == 0 { - maxWidth = t.maxColumnLengths[colIdx] + rowWrapped[colIdx] = t.wrapCell(colIdx, colStr) + // a cell that is being merged into the one above renders empty in this + // row, so its (possibly wrapped) height must not stretch the row and + // leave blank lines trailing the merged content; see issue #261 + if t.shouldMergeCellsVerticallyAbove(colIdx, hint) { + continue } - rowWrapped[colIdx] = widthEnforcer(colStr, maxWidth) colNumLines := strings.Count(rowWrapped[colIdx], "\n") + 1 if colNumLines > colMaxLines { colMaxLines = colNumLines } } + if colMaxLines == 0 { + // every column merged into the row above; keep one (blank) line so the + // row still occupies its place + colMaxLines = 1 + } return colMaxLines, rowWrapped } + +// shouldInterleaveVerticalMerge reports whether consecutive body rows that are +// being vertically merged (ColumnConfig.AutoMerge) may be rendered as a single +// shared block. Doing so lets the differing columns stack into the merged +// cell's wrapped height instead of leaving blank lines below it (issue #261). +// +// It is intentionally restricted to the plain, unambiguous case - no row +// separators, auto-index, or per-row coloring - so it never alters any other +// layout and falls back to the regular row-by-row rendering everywhere else. +func (t *Table) shouldInterleaveVerticalMerge(hint renderHint) bool { + if hint.isHeaderRow || hint.isFooterRow { + return false + } + if t.style.Options.SeparateRows || len(t.separators) > 0 { + return false + } + if t.autoIndex || t.pager.size > 0 { + return false + } + if t.rowPainter != nil || t.rowPainterWithAttributes != nil || t.style.Color.RowAlternate != nil { + return false + } + for colIdx := 0; colIdx < t.numColumns; colIdx++ { + if t.columnConfigMap[colIdx].AutoMerge { + return true + } + } + return false +} + +// verticalMergeGroupEnd returns the exclusive end index of the maximal run of +// rows starting at "start" that can be stacked into a single block: rows that +// carry the same config and each add just one line, so they slot into the +// wrapped height of the merged cell(s) spanning them. +func (t *Table) verticalMergeGroupEnd(rows []rowStr, start int) int { + if !t.rowVerticallyStackable(rows[start]) { + return start + 1 + } + end := start + 1 + for end < len(rows) { + if t.rowsConfigMap[start] != t.rowsConfigMap[end] { + break + } + if !t.rowStacksBelow(rows[end-1], rows[end]) { + break + } + end++ + } + return end +} + +// rowVerticallyStackable reports whether a row's non-merged cells each render on +// a single line, so it occupies a single line when stacked (a merged cell may +// still wrap - it provides the vertical space the stacked rows slot into). +func (t *Table) rowVerticallyStackable(row rowStr) bool { + for colIdx := 0; colIdx < t.numColumns && colIdx < len(row); colIdx++ { + if t.columnConfigMap[colIdx].AutoMerge { + continue + } + if t.cellWraps(colIdx, row[colIdx]) { + return false + } + } + return true +} + +// rowStacksBelow reports whether "cur" can be stacked right below "prev" within +// a shared merge block, i.e. it adds exactly one line. A merged column that +// keeps its value simply continues the cell above; any column that changes must +// fit on a single line (and a merged column that changes must also have left +// its previous value on a single line, else its wrapped height would not line +// up with the stacked rows). +func (t *Table) rowStacksBelow(prev, cur rowStr) bool { + for colIdx := 0; colIdx < t.numColumns; colIdx++ { + prevVal, curVal := "", "" + if colIdx < len(prev) { + prevVal = prev[colIdx] + } + if colIdx < len(cur) { + curVal = cur[colIdx] + } + merged := t.columnConfigMap[colIdx].AutoMerge + if merged && curVal == prevVal { + continue + } + if t.cellWraps(colIdx, curVal) { + return false + } + if merged && t.cellWraps(colIdx, prevVal) { + return false + } + } + return true +} + +// cellWraps reports whether a column's value renders on more than one line. +func (t *Table) cellWraps(colIdx int, colStr string) bool { + return strings.Contains(t.wrapCell(colIdx, colStr), "\n") +} + +// combineVerticalMergeGroup collapses rows[start:end] into a single row. A +// merged column that never changes keeps its single (spanning) value; every +// other column stacks the rows' values one below the other, blanking a merged +// value that repeats so it still reads as one vertically merged cell. +func (t *Table) combineVerticalMergeGroup(rows []rowStr, start, end int) rowStr { + combined := make(rowStr, t.numColumns) + for colIdx := 0; colIdx < t.numColumns; colIdx++ { + if t.columnConfigMap[colIdx].AutoMerge && t.mergedColumnIsConstant(rows, start, end, colIdx) { + if colIdx < len(rows[start]) { + combined[colIdx] = rows[start][colIdx] + } + continue + } + lines := make([]string, 0, end-start) + prevCell := "" + for rowIdx := start; rowIdx < end; rowIdx++ { + cell := "" + if colIdx < len(rows[rowIdx]) { + cell = rows[rowIdx][colIdx] + } + line := cell + if t.columnConfigMap[colIdx].AutoMerge && rowIdx > start && cell == prevCell { + line = "" + } + prevCell = cell + lines = append(lines, line) + } + combined[colIdx] = strings.Join(lines, "\n") + } + return combined +} + +// mergedColumnIsConstant reports whether a merged column holds the same value +// across every row in rows[start:end]. +func (t *Table) mergedColumnIsConstant(rows []rowStr, start, end, colIdx int) bool { + first := "" + if colIdx < len(rows[start]) { + first = rows[start][colIdx] + } + for rowIdx := start + 1; rowIdx < end; rowIdx++ { + val := "" + if colIdx < len(rows[rowIdx]) { + val = rows[rowIdx][colIdx] + } + if val != first { + return false + } + } + return true +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 96d0dd081..1362abe14 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -249,7 +249,7 @@ github.com/jackc/pgx/v5/stdlib ## explicit; go 1.19 github.com/jackc/puddle/v2 github.com/jackc/puddle/v2/internal/genstack -# github.com/jedib0t/go-pretty/v6 v6.8.2 +# github.com/jedib0t/go-pretty/v6 v6.8.3 ## explicit; go 1.18 github.com/jedib0t/go-pretty/v6/table github.com/jedib0t/go-pretty/v6/text