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}, dimension::{CompleteDimension, PeekableGridDimension},
records::{ records::{
IterRecords, IterRecords, PeekableRecords,
vec_records::{Cell, Text, VecRecords}, vec_records::{Cell, Text, VecRecords},
}, },
}, },
@ -47,6 +47,7 @@ pub type NuRecordsValue = Text<String>;
pub struct NuTable { pub struct NuTable {
data: Vec<Vec<NuRecordsValue>>, data: Vec<Vec<NuRecordsValue>>,
widths: Vec<usize>, widths: Vec<usize>,
heights: Vec<usize>,
count_rows: usize, count_rows: usize,
count_cols: usize, count_cols: usize,
styles: Styles, styles: Styles,
@ -59,6 +60,7 @@ impl NuTable {
Self { Self {
data: vec![vec![Text::default(); count_cols]; count_rows], data: vec![vec![Text::default(); count_cols]; count_rows],
widths: vec![2; count_cols], widths: vec![2; count_cols],
heights: vec![0; count_rows],
count_rows, count_rows,
count_cols, count_cols,
styles: Styles { styles: Styles {
@ -98,14 +100,19 @@ impl NuTable {
pub fn insert_value(&mut self, pos: (usize, usize), value: NuRecordsValue) { pub fn insert_value(&mut self, pos: (usize, usize), value: NuRecordsValue) {
let width = value.width() + indent_sum(self.config.indent); 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.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; self.data[pos.0][pos.1] = value;
} }
pub fn insert(&mut self, pos: (usize, usize), text: String) { pub fn insert(&mut self, pos: (usize, usize), text: String) {
let text = Text::new(text); 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.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; self.data[pos.0][pos.1] = text;
} }
@ -113,10 +120,12 @@ impl NuTable {
assert_eq!(self.data[index].len(), row.len()); assert_eq!(self.data[index].len(), row.len());
for (i, text) in row.iter().enumerate() { for (i, text) in row.iter().enumerate() {
self.widths[i] = max( let pad = indent_sum(self.config.indent);
self.widths[i], let width = text.width() + pad;
text.width() + indent_sum(self.config.indent), 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; self.data[index] = row;
@ -125,8 +134,23 @@ impl NuTable {
pub fn pop_column(&mut self, count: usize) { pub fn pop_column(&mut self, count: usize) {
self.count_cols -= count; self.count_cols -= count;
self.widths.truncate(self.count_cols); 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); 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 // set to default styles of the popped columns
@ -146,8 +170,14 @@ impl NuTable {
pub fn push_column(&mut self, text: String) { pub fn push_column(&mut self, text: String) {
let value = Text::new(text); let value = Text::new(text);
self.widths let pad = indent_sum(self.config.indent);
.push(value.width() + 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[..] { for row in &mut self.data[..] {
row.push(value.clone()); 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 { impl From<Vec<Vec<Text<String>>>> for NuTable {
fn from(value: Vec<Vec<Text<String>>>) -> Self { fn from(value: Vec<Vec<Text<String>>>) -> Self {
let count_rows = value.len(); let count_rows = value.len();
let count_cols = if value.is_empty() { 0 } else { value[0].len() }; let count_cols = if value.is_empty() { 0 } else { value[0].len() };
let mut t = Self::new(count_rows, count_cols); 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); table_recalculate_widths(&mut t);
t t
@ -398,8 +434,15 @@ fn is_header_on_border(t: &NuTable) -> bool {
} }
fn table_insert_footer_if(t: &mut NuTable) { fn table_insert_footer_if(t: &mut NuTable) {
if t.config.structure.with_header && t.config.structure.with_footer { let with_footer = t.config.structure.with_header && t.config.structure.with_footer;
duplicate_row(&mut t.data, 0); 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()) .map(|s| s.to_string())
.collect(); .collect();
// drop height row
t.heights.remove(0);
// WE NEED TO RELCULATE WIDTH. // WE NEED TO RELCULATE WIDTH.
// TODO: cause we have configuration beforehand we can just not calculate it in? // TODO: cause we have configuration beforehand we can just not calculate it in?
// Why we do it exactly??
table_recalculate_widths(t); table_recalculate_widths(t);
let alignment = t.styles.alignments.header; let alignment = t.styles.alignments.header;
@ -482,7 +529,7 @@ fn draw_table(
set_styles(&mut table, t.styles, &structure); set_styles(&mut table, t.styles, &structure);
set_indent(&mut table, t.config.indent); set_indent(&mut table, t.config.indent);
load_theme(&mut table, &t.config.theme, &structure, sep_color); 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); table_set_border_header(&mut table, head, &t.config);
let string = table.to_string(); 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)); 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 trim = cfg.trim.clone();
let pad = cfg.indent.left + cfg.indent.right; let pad = indent_sum(cfg.indent);
let ctrl = WidthCtrl::new(termwidth, width, trim, cfg.expand, pad); let ctrl = DimensionCtrl::new(termwidth, width, trim, cfg.expand, pad, heights);
table.with(ctrl); 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)); table.with(Padding::new(indent.left, indent.right, 0, 0));
} }
struct WidthCtrl { struct DimensionCtrl {
width: WidthEstimation, width: WidthEstimation,
trim_strategy: TrimStrategy, trim_strategy: TrimStrategy,
max_width: usize, max_width: usize,
expand: bool, expand: bool,
pad: usize, pad: usize,
heights: Vec<usize>,
} }
impl WidthCtrl { impl DimensionCtrl {
fn new( fn new(
max_width: usize, max_width: usize,
width: WidthEstimation, width: WidthEstimation,
trim_strategy: TrimStrategy, trim_strategy: TrimStrategy,
expand: bool, expand: bool,
pad: usize, pad: usize,
heights: Vec<usize>,
) -> Self { ) -> Self {
Self { Self {
width, width,
@ -564,6 +619,7 @@ impl WidthCtrl {
max_width, max_width,
expand, expand,
pad, 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) { fn change(self, recs: &mut NuRecords, cfg: &mut ColoredConfig, dims: &mut CompleteDimension) {
if self.width.truncate { if self.width.truncate {
width_ctrl_truncate(self, recs, cfg, dims); 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 // NOTE: just an optimization; to not recalculate it internally
dims.set_heights(self.heights);
dims.set_widths(self.width.needed); dims.set_widths(self.width.needed);
} }
fn hint_change(&self) -> Option<Entity> { 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( fn width_ctrl_expand(
ctrl: WidthCtrl, ctrl: DimensionCtrl,
recs: &mut NuRecords, recs: &mut NuRecords,
cfg: &mut ColoredConfig, cfg: &mut ColoredConfig,
dims: &mut CompleteDimension, dims: &mut CompleteDimension,
) { ) {
dims.set_heights(ctrl.heights);
let opt = Width::increase(ctrl.max_width); let opt = Width::increase(ctrl.max_width);
TableOption::<NuRecords, _, _>::change(opt, recs, cfg, dims); TableOption::<NuRecords, _, _>::change(opt, recs, cfg, dims);
} }
fn width_ctrl_truncate( fn width_ctrl_truncate(
ctrl: WidthCtrl, ctrl: DimensionCtrl,
recs: &mut NuRecords, recs: &mut NuRecords,
cfg: &mut ColoredConfig, cfg: &mut ColoredConfig,
dims: &mut CompleteDimension, dims: &mut CompleteDimension,
) { ) {
let mut heights = ctrl.heights;
// todo: maybe general for loop better // todo: maybe general for loop better
for (col, (&width, width_original)) in ctrl for (col, (&width, width_original)) in ctrl
.width .width
@ -652,6 +721,13 @@ fn width_ctrl_truncate(
let wrap = Width::wrap(width).keep_words(*try_to_keep_words); let wrap = Width::wrap(width).keep_words(*try_to_keep_words);
CellOption::<NuRecords, _>::change(wrap, recs, cfg, Entity::Column(col)); 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 } => { TrimStrategy::Truncate { suffix } => {
let mut truncate = Width::truncate(width); let mut truncate = Width::truncate(width);
@ -664,6 +740,7 @@ fn width_ctrl_truncate(
} }
} }
dims.set_heights(heights);
dims.set_widths(ctrl.width.needed); dims.set_widths(ctrl.width.needed);
} }
@ -675,22 +752,12 @@ fn align_table(
table.with(AlignmentStrategy::PerLine); table.with(AlignmentStrategy::PerLine);
if structure.with_header { if structure.with_header {
table.modify( table.modify(Rows::first(), AlignmentStrategy::PerCell);
Rows::first(), table.modify(Rows::first(), Alignment::from(alignments.header));
(
AlignmentStrategy::PerCell,
Alignment::from(alignments.header),
),
);
if structure.with_footer { if structure.with_footer {
table.modify( table.modify(Rows::last(), AlignmentStrategy::PerCell);
Rows::last(), table.modify(Rows::last(), Alignment::from(alignments.header));
(
AlignmentStrategy::PerCell,
Alignment::from(alignments.header),
),
);
} }
} }