nu_command/debug/
inspect_table.rs

1// note: Seems like could be simplified
2//       IMHO: it shall not take 300+ lines :)
3// TODO: Simplify
4// NOTE: Pool table could be used?
5// FIXME: `inspect` wrapping produces too much new lines with small terminal
6
7use self::global_horizontal_char::SetHorizontalChar;
8use nu_protocol::Value;
9use nu_protocol::engine::EngineState;
10use nu_table::{string_width, string_wrap};
11use tabled::{
12    Table,
13    grid::config::ColoredConfig,
14    settings::{Style, peaker::Priority, width::Wrap},
15};
16
17pub fn build_table(
18    engine_state: &EngineState,
19    value: Value,
20    description: String,
21    termsize: usize,
22) -> String {
23    let (head, mut data) = util::collect_input(engine_state, value);
24    let count_columns = head.len();
25    data.insert(0, head);
26
27    let mut desc = description;
28    let mut desc_width = string_width(&desc);
29    let mut desc_table_width = get_total_width_2_column_table(11, desc_width);
30
31    let cfg = Table::default().with(Style::modern()).get_config().clone();
32    let mut widths = get_data_widths(&data, count_columns);
33    truncate_data(&mut data, &mut widths, &cfg, termsize);
34
35    let val_table_width = get_total_width2(&widths, &cfg);
36    if val_table_width < desc_table_width {
37        increase_widths(&mut widths, desc_table_width - val_table_width);
38        increase_data_width(&mut data, &widths);
39    }
40
41    if val_table_width > desc_table_width {
42        desc_width += val_table_width - desc_table_width;
43        increase_string_width(&mut desc, desc_width);
44    }
45
46    if desc_table_width > termsize {
47        let delete_width = desc_table_width - termsize;
48        if delete_width >= desc_width {
49            // we can't fit in a description; we consider it's no point in showing then?
50            return String::new();
51        }
52
53        desc_width -= delete_width;
54        desc = string_wrap(&desc, desc_width, false);
55        desc_table_width = termsize;
56    }
57
58    add_padding_to_widths(&mut widths);
59
60    let width = val_table_width.max(desc_table_width).min(termsize);
61
62    let mut desc_table = Table::from_iter([[String::from("description"), desc]]);
63    desc_table.with(Style::rounded().remove_bottom().remove_horizontals());
64
65    let mut val_table = Table::from_iter(data);
66    val_table.get_dimension_mut().set_widths(widths);
67    val_table.with(Style::rounded().corner_top_left('├').corner_top_right('┤'));
68    val_table.with((
69        Wrap::new(width).priority(Priority::max(true)),
70        SetHorizontalChar::new('┼', '┴', 11 + 2 + 1),
71    ));
72
73    format!("{desc_table}\n{val_table}")
74}
75
76fn get_data_widths(data: &[Vec<String>], count_columns: usize) -> Vec<usize> {
77    let mut widths = vec![0; count_columns];
78    for row in data {
79        for col in 0..count_columns {
80            let text = &row[col];
81            let width = string_width(text);
82            widths[col] = std::cmp::max(widths[col], width);
83        }
84    }
85
86    widths
87}
88
89fn add_padding_to_widths(widths: &mut [usize]) {
90    for width in widths {
91        *width += 2;
92    }
93}
94
95fn increase_widths(widths: &mut [usize], need: usize) {
96    let all = need / widths.len();
97    let mut rest = need - all * widths.len();
98
99    for width in widths {
100        *width += all;
101
102        if rest > 0 {
103            *width += 1;
104            rest -= 1;
105        }
106    }
107}
108
109fn increase_data_width(data: &mut Vec<Vec<String>>, widths: &[usize]) {
110    for row in data {
111        for (col, max_width) in widths.iter().enumerate() {
112            let text = &mut row[col];
113            increase_string_width(text, *max_width);
114        }
115    }
116}
117
118fn increase_string_width(text: &mut String, total: usize) {
119    let width = string_width(text);
120    let rest = total - width;
121
122    if rest > 0 {
123        text.extend(std::iter::repeat_n(' ', rest));
124    }
125}
126
127fn get_total_width_2_column_table(col1: usize, col2: usize) -> usize {
128    const PAD: usize = 1;
129    const SPLIT_LINE: usize = 1;
130    SPLIT_LINE + PAD + col1 + PAD + SPLIT_LINE + PAD + col2 + PAD + SPLIT_LINE
131}
132
133fn truncate_data(
134    data: &mut Vec<Vec<String>>,
135    widths: &mut Vec<usize>,
136    cfg: &ColoredConfig,
137    expected_width: usize,
138) {
139    const SPLIT_LINE_WIDTH: usize = 1;
140    const PAD: usize = 2;
141
142    let total_width = get_total_width2(widths, cfg);
143    if total_width <= expected_width {
144        return;
145    }
146
147    let mut width = 0;
148    let mut peak_count = 0;
149    for column_width in widths.iter() {
150        let next_width = width + *column_width + SPLIT_LINE_WIDTH + PAD;
151        if next_width >= expected_width {
152            break;
153        }
154
155        width = next_width;
156        peak_count += 1;
157    }
158
159    debug_assert!(peak_count < widths.len());
160
161    let left_space = expected_width - width;
162    let has_space_for_truncation_column = left_space > PAD;
163    if !has_space_for_truncation_column {
164        peak_count = peak_count.saturating_sub(1);
165    }
166
167    remove_columns(data, peak_count);
168    widths.drain(peak_count..);
169    push_empty_column(data);
170    widths.push(1);
171}
172
173fn remove_columns(data: &mut Vec<Vec<String>>, peak_count: usize) {
174    if peak_count == 0 {
175        for row in data {
176            row.clear();
177        }
178    } else {
179        for row in data {
180            row.drain(peak_count..);
181        }
182    }
183}
184
185fn get_total_width2(widths: &[usize], cfg: &ColoredConfig) -> usize {
186    let pad = 2;
187    let total = widths.iter().sum::<usize>() + pad * widths.len();
188    let countv = cfg.count_vertical(widths.len());
189    let margin = cfg.get_margin();
190
191    total + countv + margin.left.size + margin.right.size
192}
193
194fn push_empty_column(data: &mut Vec<Vec<String>>) {
195    let empty_cell = String::from("‥");
196    for row in data {
197        row.push(empty_cell.clone());
198    }
199}
200
201mod util {
202    use crate::debug::explain::debug_string_without_formatting;
203    use nu_engine::get_columns;
204    use nu_protocol::Value;
205    use nu_protocol::engine::EngineState;
206
207    /// Try to build column names and a table grid.
208    pub fn collect_input(
209        engine_state: &EngineState,
210        value: Value,
211    ) -> (Vec<String>, Vec<Vec<String>>) {
212        let span = value.span();
213        match value {
214            Value::Record { val: record, .. } => {
215                let (cols, vals): (Vec<_>, Vec<_>) = record.into_owned().into_iter().unzip();
216                (
217                    match cols.is_empty() {
218                        true => vec![String::from("")],
219                        false => cols,
220                    },
221                    match vals
222                        .into_iter()
223                        .map(|s| debug_string_without_formatting(engine_state, &s))
224                        .collect::<Vec<String>>()
225                    {
226                        vals if vals.is_empty() => vec![],
227                        vals => vec![vals],
228                    },
229                )
230            }
231            Value::List { vals, .. } => {
232                let mut columns = get_columns(&vals);
233                let data = convert_records_to_dataset(engine_state, &columns, vals);
234
235                if columns.is_empty() {
236                    columns = vec![String::from("")];
237                }
238
239                (columns, data)
240            }
241            Value::String { val, .. } => {
242                let lines = val
243                    .lines()
244                    .map(|line| Value::string(line.to_string(), span))
245                    .map(|val| vec![debug_string_without_formatting(engine_state, &val)])
246                    .collect();
247
248                (vec![String::from("")], lines)
249            }
250            Value::Nothing { .. } => (vec![], vec![]),
251            value => (
252                vec![String::from("")],
253                vec![vec![debug_string_without_formatting(engine_state, &value)]],
254            ),
255        }
256    }
257
258    fn convert_records_to_dataset(
259        engine_state: &EngineState,
260        cols: &[String],
261        records: Vec<Value>,
262    ) -> Vec<Vec<String>> {
263        if !cols.is_empty() {
264            create_table_for_record(engine_state, cols, &records)
265        } else if cols.is_empty() && records.is_empty() {
266            vec![]
267        } else if cols.len() == records.len() {
268            vec![
269                records
270                    .into_iter()
271                    .map(|s| debug_string_without_formatting(engine_state, &s))
272                    .collect(),
273            ]
274        } else {
275            records
276                .into_iter()
277                .map(|record| vec![debug_string_without_formatting(engine_state, &record)])
278                .collect()
279        }
280    }
281
282    fn create_table_for_record(
283        engine_state: &EngineState,
284        headers: &[String],
285        items: &[Value],
286    ) -> Vec<Vec<String>> {
287        let mut data = vec![Vec::new(); items.len()];
288
289        for (i, item) in items.iter().enumerate() {
290            let row = record_create_row(engine_state, headers, item);
291            data[i] = row;
292        }
293
294        data
295    }
296
297    fn record_create_row(
298        engine_state: &EngineState,
299        headers: &[String],
300        item: &Value,
301    ) -> Vec<String> {
302        if let Value::Record { val, .. } = item {
303            headers
304                .iter()
305                .map(|col| {
306                    val.get(col)
307                        .map(|v| debug_string_without_formatting(engine_state, v))
308                        .unwrap_or_else(String::new)
309                })
310                .collect()
311        } else {
312            // should never reach here due to `get_columns` above which will return
313            // empty columns if any value in the list is not a record
314            vec![String::new(); headers.len()]
315        }
316    }
317}
318
319mod global_horizontal_char {
320    use nu_table::NuRecords;
321    use tabled::{
322        grid::{
323            config::{ColoredConfig, Offset, Position},
324            dimension::{CompleteDimension, Dimension},
325            records::{ExactRecords, Records},
326        },
327        settings::TableOption,
328    };
329
330    pub struct SetHorizontalChar {
331        intersection: char,
332        split: char,
333        index: usize,
334    }
335
336    impl SetHorizontalChar {
337        pub fn new(intersection: char, split: char, index: usize) -> Self {
338            Self {
339                intersection,
340                split,
341                index,
342            }
343        }
344    }
345
346    impl TableOption<NuRecords, ColoredConfig, CompleteDimension> for SetHorizontalChar {
347        fn change(
348            self,
349            records: &mut NuRecords,
350            cfg: &mut ColoredConfig,
351            dimension: &mut CompleteDimension,
352        ) {
353            let count_columns = records.count_columns();
354            let count_rows = records.count_rows();
355
356            if count_columns == 0 || count_rows == 0 {
357                return;
358            }
359
360            let widths = get_widths(dimension, records.count_columns());
361
362            let has_vertical = cfg.has_vertical(0, count_columns);
363            if has_vertical && self.index == 0 {
364                let mut border = cfg.get_border(Position::new(0, 0), (count_rows, count_columns));
365                border.left_top_corner = Some(self.intersection);
366                cfg.set_border(Position::new(0, 0), border);
367                return;
368            }
369
370            let mut i = 1;
371            for (col, width) in widths.into_iter().enumerate() {
372                if self.index < i + width {
373                    let o = self.index - i;
374                    cfg.set_horizontal_char(Position::new(0, col), Offset::Start(o), self.split);
375                    return;
376                }
377
378                i += width;
379
380                let has_vertical = cfg.has_vertical(col, count_columns);
381                if has_vertical {
382                    if self.index == i {
383                        let mut border =
384                            cfg.get_border(Position::new(0, col), (count_rows, count_columns));
385                        border.right_top_corner = Some(self.intersection);
386                        cfg.set_border(Position::new(0, col), border);
387                        return;
388                    }
389
390                    i += 1;
391                }
392            }
393        }
394    }
395
396    fn get_widths(dims: &CompleteDimension, count_columns: usize) -> Vec<usize> {
397        let mut widths = vec![0; count_columns];
398        for (col, width) in widths.iter_mut().enumerate() {
399            *width = dims.get_width(col);
400        }
401
402        widths
403    }
404}