nu-table: optimize table creation and width functions (#15900)

> Further tests are welcomed.

It was already implemented that we precalculate widths, but nothing
stops to do heights as well.
Because before all the calculus were wasted (literally).

It affects `table` and `table --expand`.
The only case when it does not work (even makes things slightly less
optimal in case of `table` when `truncation` is used)

Sadly my tests are not showing the clear benefit.
I have no idea why I was expecting something 😞 
But it must be there :)

Running `scope commands` + `$env.CMD_DURATION_MS`:

```log
# patch (release)
2355 2462 2210 2356 2303

# main (release)
2375 2240 2202 2297 2385
```

PS: as once mentioned all this stuff ought to be moved out `nu-table`

---------

Signed-off-by: Maxim Zhiburt <zhiburt@gmail.com>
This commit is contained in:
Maxim Zhiburt
2025-07-06 20:56:42 +03:00
committed by GitHub
parent 647a740c11
commit 71d78b41c4

View File

@ -21,7 +21,7 @@ use tabled::{
},
dimension::{CompleteDimension, PeekableGridDimension},
records::{
IterRecords,
IterRecords, PeekableRecords,
vec_records::{Cell, Text, VecRecords},
},
},
@ -47,6 +47,7 @@ pub type NuRecordsValue = Text<String>;
pub struct NuTable {
data: Vec<Vec<NuRecordsValue>>,
widths: Vec<usize>,
heights: Vec<usize>,
count_rows: usize,
count_cols: usize,
styles: Styles,
@ -59,6 +60,7 @@ impl NuTable {
Self {
data: vec![vec![Text::default(); count_cols]; count_rows],
widths: vec![2; count_cols],
heights: vec![0; count_rows],
count_rows,
count_cols,
styles: Styles {
@ -98,14 +100,19 @@ impl NuTable {
pub fn insert_value(&mut self, pos: (usize, usize), value: NuRecordsValue) {
let width = value.width() + indent_sum(self.config.indent);
let height = value.count_lines();
self.widths[pos.1] = max(self.widths[pos.1], width);
self.heights[pos.0] = max(self.heights[pos.0], height);
self.data[pos.0][pos.1] = value;
}
pub fn insert(&mut self, pos: (usize, usize), text: String) {
let text = Text::new(text);
let width = text.width() + indent_sum(self.config.indent);
let pad = indent_sum(self.config.indent);
let width = text.width() + pad;
let height = text.count_lines();
self.widths[pos.1] = max(self.widths[pos.1], width);
self.heights[pos.0] = max(self.heights[pos.0], height);
self.data[pos.0][pos.1] = text;
}
@ -113,10 +120,12 @@ impl NuTable {
assert_eq!(self.data[index].len(), row.len());
for (i, text) in row.iter().enumerate() {
self.widths[i] = max(
self.widths[i],
text.width() + indent_sum(self.config.indent),
);
let pad = indent_sum(self.config.indent);
let width = text.width() + pad;
let height = text.count_lines();
self.widths[i] = max(self.widths[i], width);
self.heights[index] = max(self.heights[index], height);
}
self.data[index] = row;
@ -125,8 +134,23 @@ impl NuTable {
pub fn pop_column(&mut self, count: usize) {
self.count_cols -= count;
self.widths.truncate(self.count_cols);
for row in &mut self.data[..] {
for (row, height) in self.data.iter_mut().zip(self.heights.iter_mut()) {
row.truncate(self.count_cols);
let row_height = *height;
let mut new_height = 0;
for cell in row.iter() {
let height = cell.count_lines();
if height == row_height {
new_height = height;
break;
}
new_height = max(new_height, height);
}
*height = new_height;
}
// set to default styles of the popped columns
@ -146,8 +170,14 @@ impl NuTable {
pub fn push_column(&mut self, text: String) {
let value = Text::new(text);
self.widths
.push(value.width() + indent_sum(self.config.indent));
let pad = indent_sum(self.config.indent);
let width = value.width() + pad;
let height = value.count_lines();
self.widths.push(width);
for row in 0..self.count_rows {
self.heights[row] = max(self.heights[row], height);
}
for row in &mut self.data[..] {
row.push(value.clone());
@ -274,13 +304,19 @@ impl NuTable {
}
}
// NOTE: Must never be called from nu-table - made only for tests
// FIXME: remove it?
// #[cfg(test)]
impl From<Vec<Vec<Text<String>>>> for NuTable {
fn from(value: Vec<Vec<Text<String>>>) -> Self {
let count_rows = value.len();
let count_cols = if value.is_empty() { 0 } else { value[0].len() };
let mut t = Self::new(count_rows, count_cols);
t.data = value;
for (i, row) in value.into_iter().enumerate() {
t.set_row(i, row);
}
table_recalculate_widths(&mut t);
t
@ -398,8 +434,15 @@ fn is_header_on_border(t: &NuTable) -> bool {
}
fn table_insert_footer_if(t: &mut NuTable) {
if t.config.structure.with_header && t.config.structure.with_footer {
duplicate_row(&mut t.data, 0);
let with_footer = t.config.structure.with_header && t.config.structure.with_footer;
if !with_footer {
return;
}
duplicate_row(&mut t.data, 0);
if !t.heights.is_empty() {
t.heights.push(t.heights[0]);
}
}
@ -453,8 +496,12 @@ fn remove_header(t: &mut NuTable) -> HeadInfo {
.map(|s| s.to_string())
.collect();
// drop height row
t.heights.remove(0);
// WE NEED TO RELCULATE WIDTH.
// TODO: cause we have configuration beforehand we can just not calculate it in?
// Why we do it exactly??
table_recalculate_widths(t);
let alignment = t.styles.alignments.header;
@ -482,7 +529,7 @@ fn draw_table(
set_styles(&mut table, t.styles, &structure);
set_indent(&mut table, t.config.indent);
load_theme(&mut table, &t.config.theme, &structure, sep_color);
truncate_table(&mut table, &t.config, width, termwidth);
truncate_table(&mut table, &t.config, width, termwidth, t.heights);
table_set_border_header(&mut table, head, &t.config);
let string = table.to_string();
@ -527,10 +574,16 @@ fn table_set_border_header(table: &mut Table, head: Option<HeadInfo>, cfg: &Tabl
table.with(SetLineHeaders::new(head, 0, pad));
}
fn truncate_table(table: &mut Table, cfg: &TableConfig, width: WidthEstimation, termwidth: usize) {
fn truncate_table(
table: &mut Table,
cfg: &TableConfig,
width: WidthEstimation,
termwidth: usize,
heights: Vec<usize>,
) {
let trim = cfg.trim.clone();
let pad = cfg.indent.left + cfg.indent.right;
let ctrl = WidthCtrl::new(termwidth, width, trim, cfg.expand, pad);
let pad = indent_sum(cfg.indent);
let ctrl = DimensionCtrl::new(termwidth, width, trim, cfg.expand, pad, heights);
table.with(ctrl);
}
@ -542,21 +595,23 @@ fn set_indent(table: &mut Table, indent: TableIndent) {
table.with(Padding::new(indent.left, indent.right, 0, 0));
}
struct WidthCtrl {
struct DimensionCtrl {
width: WidthEstimation,
trim_strategy: TrimStrategy,
max_width: usize,
expand: bool,
pad: usize,
heights: Vec<usize>,
}
impl WidthCtrl {
impl DimensionCtrl {
fn new(
max_width: usize,
width: WidthEstimation,
trim_strategy: TrimStrategy,
expand: bool,
pad: usize,
heights: Vec<usize>,
) -> Self {
Self {
width,
@ -564,6 +619,7 @@ impl WidthCtrl {
max_width,
expand,
pad,
heights,
}
}
}
@ -596,7 +652,7 @@ impl WidthEstimation {
}
}
impl TableOption<NuRecords, ColoredConfig, CompleteDimension> for WidthCtrl {
impl TableOption<NuRecords, ColoredConfig, CompleteDimension> for DimensionCtrl {
fn change(self, recs: &mut NuRecords, cfg: &mut ColoredConfig, dims: &mut CompleteDimension) {
if self.width.truncate {
width_ctrl_truncate(self, recs, cfg, dims);
@ -609,30 +665,43 @@ impl TableOption<NuRecords, ColoredConfig, CompleteDimension> for WidthCtrl {
}
// NOTE: just an optimization; to not recalculate it internally
dims.set_heights(self.heights);
dims.set_widths(self.width.needed);
}
fn hint_change(&self) -> Option<Entity> {
None
// NOTE:
// Because we are assuming that:
// len(lines(wrapped(string))) >= len(lines(string))
//
// Only truncation case must be relaclucated in term of height.
if self.width.truncate && matches!(self.trim_strategy, TrimStrategy::Truncate { .. }) {
Some(Entity::Row(0))
} else {
None
}
}
}
fn width_ctrl_expand(
ctrl: WidthCtrl,
ctrl: DimensionCtrl,
recs: &mut NuRecords,
cfg: &mut ColoredConfig,
dims: &mut CompleteDimension,
) {
dims.set_heights(ctrl.heights);
let opt = Width::increase(ctrl.max_width);
TableOption::<NuRecords, _, _>::change(opt, recs, cfg, dims);
}
fn width_ctrl_truncate(
ctrl: WidthCtrl,
ctrl: DimensionCtrl,
recs: &mut NuRecords,
cfg: &mut ColoredConfig,
dims: &mut CompleteDimension,
) {
let mut heights = ctrl.heights;
// todo: maybe general for loop better
for (col, (&width, width_original)) in ctrl
.width
@ -652,6 +721,13 @@ fn width_ctrl_truncate(
let wrap = Width::wrap(width).keep_words(*try_to_keep_words);
CellOption::<NuRecords, _>::change(wrap, recs, cfg, Entity::Column(col));
// NOTE: An optimization to have proper heights without going over all the data again.
// We are going only for all rows in changed columns
for (row, row_height) in heights.iter_mut().enumerate() {
let height = recs.count_lines(Position::new(row, col));
*row_height = max(*row_height, height);
}
}
TrimStrategy::Truncate { suffix } => {
let mut truncate = Width::truncate(width);
@ -664,6 +740,7 @@ fn width_ctrl_truncate(
}
}
dims.set_heights(heights);
dims.set_widths(ctrl.width.needed);
}
@ -675,22 +752,12 @@ fn align_table(
table.with(AlignmentStrategy::PerLine);
if structure.with_header {
table.modify(
Rows::first(),
(
AlignmentStrategy::PerCell,
Alignment::from(alignments.header),
),
);
table.modify(Rows::first(), AlignmentStrategy::PerCell);
table.modify(Rows::first(), Alignment::from(alignments.header));
if structure.with_footer {
table.modify(
Rows::last(),
(
AlignmentStrategy::PerCell,
Alignment::from(alignments.header),
),
);
table.modify(Rows::last(), AlignmentStrategy::PerCell);
table.modify(Rows::last(), Alignment::from(alignments.header));
}
}