From 10e84038afe55ba63c9b3187e6d3a1749fa2cc65 Mon Sep 17 00:00:00 2001 From: Maxim Zhiburt Date: Fri, 21 Jun 2024 22:07:57 +0300 Subject: [PATCH] nu-explore: Add vertical lines && fix index/transpose issue (#13147) Somehow I believe that split lines were implemented originally; (I haven't got to find it though; from a quick look) I mean a long time ago before a lot a changes were made. Probably adding horizontal lines would make also some sense. ref #13116 close #13140 Take care ________________ If `explore` is used, frequently, or planned to be so. I guess it would be a good one to create a test suite for it; to not break things occasionally :sweat_smile: I did approached it one time back then using `expectrl` (literally `expect`), but there was some issues. Maybe smth. did change. Or some `clean` mode could be introduced for it, to being able to be used by outer programs; to control `nu`. Just thoughts here. --- crates/nu-explore/src/views/record/mod.rs | 76 +++++++++------ .../src/views/record/table_widget.rs | 96 ++++++++++++++----- 2 files changed, 123 insertions(+), 49 deletions(-) diff --git a/crates/nu-explore/src/views/record/mod.rs b/crates/nu-explore/src/views/record/mod.rs index 9e67e08120..87b0952a68 100644 --- a/crates/nu-explore/src/views/record/mod.rs +++ b/crates/nu-explore/src/views/record/mod.rs @@ -131,7 +131,7 @@ impl RecordView { Orientation::Left => (column, row), }; - if row >= layer.count_rows() || column >= layer.count_columns() { + if row >= layer.record_values.len() || column >= layer.column_names.len() { // actually must never happen; unless cursor works incorrectly // if being sure about cursor it can be deleted; return Value::nothing(Span::unknown()); @@ -610,7 +610,7 @@ fn estimate_page_size(area: Rect, show_head: bool) -> u16 { /// scroll to the end of the data fn tail_data(state: &mut RecordView, page_size: usize) { let layer = state.get_layer_last_mut(); - let count_rows = layer.count_rows(); + let count_rows = layer.record_values.len(); if count_rows > page_size { layer .cursor @@ -722,43 +722,66 @@ fn get_percentage(value: usize, max: usize) -> usize { } fn transpose_table(layer: &mut RecordLayer) { + if layer.was_transposed { + transpose_from(layer); + } else { + transpose_to(layer); + } + + layer.was_transposed = !layer.was_transposed; +} + +fn transpose_from(layer: &mut RecordLayer) { let count_rows = layer.record_values.len(); let count_columns = layer.column_names.len(); - if layer.was_transposed { - let headers = pop_first_column(&mut layer.record_values); - let headers = headers - .into_iter() - .map(|value| match value { - Value::String { val, .. } => val, - _ => unreachable!("must never happen"), - }) - .collect(); + if let Some(data) = &mut layer.record_text { + pop_first_column(data); + *data = _transpose_table(data, count_rows, count_columns - 1); + } - let data = _transpose_table(&layer.record_values, count_rows, count_columns - 1); + let headers = pop_first_column(&mut layer.record_values); + let headers = headers + .into_iter() + .map(|value| match value { + Value::String { val, .. } => val, + _ => unreachable!("must never happen"), + }) + .collect(); - layer.record_values = data; - layer.column_names = headers; + let data = _transpose_table(&layer.record_values, count_rows, count_columns - 1); - return; + layer.record_values = data; + layer.column_names = headers; +} + +fn transpose_to(layer: &mut RecordLayer) { + let count_rows = layer.record_values.len(); + let count_columns = layer.column_names.len(); + + if let Some(data) = &mut layer.record_text { + *data = _transpose_table(data, count_rows, count_columns); + for (column, column_name) in layer.column_names.iter().enumerate() { + let value = (column_name.to_owned(), Default::default()); + data[column].insert(0, value); + } } let mut data = _transpose_table(&layer.record_values, count_rows, count_columns); - for (column, column_name) in layer.column_names.iter().enumerate() { let value = Value::string(column_name, NuSpan::unknown()); - data[column].insert(0, value); } layer.record_values = data; layer.column_names = (1..count_rows + 1 + 1).map(|i| i.to_string()).collect(); - - layer.was_transposed = !layer.was_transposed; } -fn pop_first_column(values: &mut [Vec]) -> Vec { - let mut data = vec![Value::default(); values.len()]; +fn pop_first_column(values: &mut [Vec]) -> Vec +where + T: Default + Clone, +{ + let mut data = vec![T::default(); values.len()]; for (row, values) in values.iter_mut().enumerate() { data[row] = values.remove(0); } @@ -766,12 +789,11 @@ fn pop_first_column(values: &mut [Vec]) -> Vec { data } -fn _transpose_table( - values: &[Vec], - count_rows: usize, - count_columns: usize, -) -> Vec> { - let mut data = vec![vec![Value::default(); count_rows]; count_columns]; +fn _transpose_table(values: &[Vec], count_rows: usize, count_columns: usize) -> Vec> +where + T: Clone + Default, +{ + let mut data = vec![vec![T::default(); count_rows]; count_columns]; for (row, values) in values.iter().enumerate() { for (column, value) in values.iter().enumerate() { data[column][row].clone_from(value); diff --git a/crates/nu-explore/src/views/record/table_widget.rs b/crates/nu-explore/src/views/record/table_widget.rs index 7149a7ce60..ae39d46858 100644 --- a/crates/nu-explore/src/views/record/table_widget.rs +++ b/crates/nu-explore/src/views/record/table_widget.rs @@ -88,6 +88,7 @@ impl StatefulWidget for TableWidget<'_> { // todo: refactoring these to methods as they have quite a bit in common. impl<'a> TableWidget<'a> { + // header at the top; header is always 1 line fn render_table_horizontal(self, area: Rect, buf: &mut Buffer, state: &mut TableWidgetState) { let padding_l = self.config.column_padding_left as u16; let padding_r = self.config.column_padding_right as u16; @@ -130,25 +131,16 @@ impl<'a> TableWidget<'a> { } if show_index { - let area = Rect::new(width, data_y, area.width, data_height); width += render_index( buf, - area, + Rect::new(width, data_y, area.width, data_height), self.style_computer, self.index_row, padding_l, padding_r, ); - width += render_vertical_line_with_split( - buf, - width, - data_y, - data_height, - show_head, - false, - separator_s, - ); + width += render_split_line(buf, width, area.y, area.height, show_head, separator_s); } // if there is more data than we can show, add an ellipsis to the column headers to hint at that @@ -162,6 +154,11 @@ impl<'a> TableWidget<'a> { } for col in self.index_column..self.columns.len() { + let need_split_line = state.count_columns > 0 && width < area.width; + if need_split_line { + width += render_split_line(buf, width, area.y, area.height, show_head, separator_s); + } + let mut column = create_column(data, col); let column_width = calculate_column_width(&column); @@ -200,6 +197,7 @@ impl<'a> TableWidget<'a> { } let head_iter = [(&head, head_style)].into_iter(); + // we don't change width here cause the whole column have the same width; so we add it when we print data let mut w = width; w += render_space(buf, w, head_y, 1, padding_l); w += render_column(buf, w, head_y, use_space, head_iter); @@ -209,10 +207,10 @@ impl<'a> TableWidget<'a> { state.layout.push(&head, x, head_y, use_space, 1); } - let head_rows = column.iter().map(|(t, s)| (t, *s)); + let column_rows = column.iter().map(|(t, s)| (t, *s)); width += render_space(buf, width, data_y, data_height, padding_l); - width += render_column(buf, width, data_y, use_space, head_rows); + width += render_column(buf, width, data_y, use_space, column_rows); width += render_space(buf, width, data_y, data_height, padding_r); for (row, (text, _)) in column.iter().enumerate() { @@ -235,15 +233,7 @@ impl<'a> TableWidget<'a> { } if width < area.width { - width += render_vertical_line_with_split( - buf, - width, - data_y, - data_height, - show_head, - false, - separator_s, - ); + width += render_split_line(buf, width, area.y, area.height, show_head, separator_s); } let rest = area.width.saturating_sub(width); @@ -255,6 +245,7 @@ impl<'a> TableWidget<'a> { } } + // header at the left; header is always 1 line fn render_table_vertical(self, area: Rect, buf: &mut Buffer, state: &mut TableWidgetState) { if area.width == 0 || area.height == 0 { return; @@ -353,6 +344,9 @@ impl<'a> TableWidget<'a> { state.count_rows = columns.len(); state.count_columns = 0; + // note: is there a time where we would have more then 1 column? + // seems like not really; cause it's literally KV table, or am I wrong? + for col in self.index_column..self.data.len() { let mut column = self.data[col][self.index_row..self.index_row + columns.len()].to_vec(); @@ -361,6 +355,13 @@ impl<'a> TableWidget<'a> { break; } + // see KV comment; this block might never got used + let need_split_line = state.count_columns > 0 && left_w < area.width; + if need_split_line { + render_vertical_line(buf, area.x + left_w, area.y, area.height, separator_s); + left_w += 1; + } + let column_width = column_width as u16; let available = area.width - left_w; let is_last = col + 1 == self.data.len(); @@ -555,6 +556,51 @@ fn render_index( width } +fn render_split_line( + buf: &mut Buffer, + x: u16, + y: u16, + height: u16, + has_head: bool, + style: NuStyle, +) -> u16 { + if has_head { + render_vertical_split_line(buf, x, y, height, &[0], &[2], &[], style); + } else { + render_vertical_split_line(buf, x, y, height, &[], &[], &[], style); + } + + 1 +} + +#[allow(clippy::too_many_arguments)] +fn render_vertical_split_line( + buf: &mut Buffer, + x: u16, + y: u16, + height: u16, + top_slit: &[u16], + inner_slit: &[u16], + bottom_slit: &[u16], + style: NuStyle, +) -> u16 { + render_vertical_line(buf, x, y, height, style); + + for &y in top_slit { + render_top_connector(buf, x, y, style); + } + + for &y in inner_slit { + render_inner_connector(buf, x, y, style); + } + + for &y in bottom_slit { + render_bottom_connector(buf, x, y, style); + } + + 1 +} + fn render_vertical_line_with_split( buf: &mut Buffer, x: u16, @@ -668,6 +714,12 @@ fn render_bottom_connector(buf: &mut Buffer, x: u16, y: u16, style: NuStyle) { buf.set_span(x, y, &span, 1); } +fn render_inner_connector(buf: &mut Buffer, x: u16, y: u16, style: NuStyle) { + let style = nu_style_to_tui(style); + let span = Span::styled("┼", style); + buf.set_span(x, y, &span, 1); +} + fn calculate_column_width(column: &[NuText]) -> usize { column .iter()