fix https://github.com/nushell/nushell/issues/7380
This commit is contained in:
Maxim Zhiburt
2022-12-15 17:47:04 +03:00
committed by GitHub
parent b6683a3010
commit 33aea56ccd
16 changed files with 1006 additions and 609 deletions

View File

@ -2,54 +2,10 @@ mod nu_protocol_table;
mod table;
mod table_theme;
mod textstyle;
mod util;
pub use nu_protocol_table::NuTable;
pub use table::{Alignments, Table};
pub use table::{Alignments, Table, TableConfig};
pub use table_theme::TableTheme;
pub use textstyle::{Alignment, TextStyle};
use tabled::{Padding, Style, Width};
pub fn string_width(text: &str) -> usize {
tabled::papergrid::util::string_width_multiline_tab(text, 4)
}
pub fn wrap_string(text: &str, width: usize) -> String {
// well... it's not effitient to build a table to wrap a string,
// but ... it's better than a copy paste
tabled::builder::Builder::from_iter([[text]])
.build()
.with(Padding::zero())
.with(Style::empty())
.with(Width::wrap(width))
.to_string()
}
pub fn string_truncate(text: &str, width: usize) -> String {
// todo: change me...
match text.lines().next() {
Some(first_line) => tabled::builder::Builder::from_iter([[first_line]])
.build()
.with(tabled::Style::empty())
.with(tabled::Padding::zero())
.with(tabled::Width::truncate(width))
.to_string(),
None => String::new(),
}
}
pub fn string_wrap(text: &str, width: usize) -> String {
// todo: change me...
if text.is_empty() {
return String::new();
}
tabled::builder::Builder::from_iter([[text]])
.build()
.with(tabled::Style::empty())
.with(tabled::Padding::zero())
.with(tabled::Width::wrap(width))
.to_string()
}
pub use util::*;

View File

@ -1,9 +1,12 @@
use std::collections::HashMap;
use nu_protocol::{Config, Span, Value};
use tabled::{color::Color, papergrid::records::Records, Table};
use tabled::{
color::Color, formatting::AlignmentStrategy, object::Segment, papergrid::records::Records,
Alignment, Modify, Table,
};
use crate::{table::TrimStrategyModifier, TableTheme};
use crate::{table::TrimStrategyModifier, Alignments, TableTheme};
/// NuTable has a recursive table representation of nu_prorocol::Value.
///
@ -215,4 +218,10 @@ fn load_theme<R>(
table.with(color);
}
}
table.with(
Modify::new(Segment::all())
.with(Alignment::Horizontal(Alignments::default().data))
.with(AlignmentStrategy::PerLine),
);
}

View File

@ -1,6 +1,7 @@
use std::{collections::HashMap, fmt::Display};
use std::{cmp::min, collections::HashMap, fmt::Display};
use nu_protocol::{Config, FooterMode, TrimStrategy};
use nu_ansi_term::Style;
use nu_protocol::TrimStrategy;
use tabled::{
alignment::AlignmentHorizontal,
builder::Builder,
@ -12,8 +13,11 @@ use tabled::{
records::{
cell_info::CellInfo, tcell::TCell, vec_records::VecRecords, Records, RecordsMut,
},
util::string_width_multiline,
width::CfgWidthFunction,
Estimate,
},
peaker::Peaker,
Alignment, Modify, ModifyObject, TableOption, Width,
};
@ -23,9 +27,6 @@ use crate::{table_theme::TableTheme, TextStyle};
#[derive(Debug, Clone)]
pub struct Table {
data: Data,
is_empty: bool,
with_header: bool,
with_index: bool,
}
type Data = VecRecords<TCell<CellInfo<'static>, TextStyle>>;
@ -34,33 +35,22 @@ impl Table {
/// Creates a [Table] instance.
///
/// If `headers.is_empty` then no headers will be rendered.
pub fn new(
mut data: Vec<Vec<TCell<CellInfo<'static>, TextStyle>>>,
size: (usize, usize),
termwidth: usize,
with_header: bool,
with_index: bool,
) -> Table {
pub fn new(data: Vec<Vec<TCell<CellInfo<'static>, TextStyle>>>, size: (usize, usize)) -> Table {
// it's not guaranted that data will have all rows with the same number of columns.
// but VecRecords::with_hint require this constrain.
for row in &mut data {
if row.len() < size.1 {
row.extend(
std::iter::repeat(Self::create_cell(String::default(), TextStyle::default()))
.take(size.1 - row.len()),
);
}
}
//
// so we do a check to make it certainly true
let mut data = VecRecords::with_hint(data, size.1);
let is_empty = maybe_truncate_columns(&mut data, size.1, termwidth);
let mut data = data;
make_data_consistent(&mut data, size);
Table {
data,
is_empty,
with_header,
with_index,
}
let data = VecRecords::with_hint(data, size.1);
Table { data }
}
pub fn count_rows(&self) -> usize {
self.data.count_rows()
}
pub fn create_cell(
@ -70,29 +60,15 @@ impl Table {
TCell::new(CellInfo::new(text.into(), CfgWidthFunction::new(4)), style)
}
pub fn is_empty(&self) -> bool {
self.is_empty
}
pub fn size(&self) -> (usize, usize) {
(self.data.count_rows(), self.data.count_columns())
}
pub fn is_with_index(&self) -> bool {
self.with_index
}
pub fn truncate(&mut self, width: usize, theme: &TableTheme) -> bool {
let mut truncated = false;
while self.data.count_rows() > 0 && self.data.count_columns() > 0 {
let mut table = Builder::custom(self.data.clone()).build();
load_theme(&mut table, &HashMap::new(), theme, false, false);
let total = table.total_width();
// println!("{}", table);
// println!("width={:?} total={:?}", width, total);
drop(table);
let total;
{
let mut table = Builder::custom(self.data.clone()).build();
load_theme(&mut table, theme, false, false, None);
total = table.total_width();
}
if total > width {
truncated = true;
@ -117,19 +93,69 @@ impl Table {
false
}
/// Draws a trable on a String.
/// Converts a table to a String.
///
/// It returns None in case where table cannot be fit to a terminal width.
pub fn draw_table(
self,
config: &Config,
color_hm: &HashMap<String, nu_ansi_term::Style>,
alignments: Alignments,
theme: &TableTheme,
termwidth: usize,
expand: bool,
) -> Option<String> {
draw_table(self, config, color_hm, alignments, theme, termwidth, expand)
pub fn draw(self, config: TableConfig, termwidth: usize) -> Option<String> {
build_table(self.data, config, termwidth)
}
}
fn make_data_consistent(data: &mut Vec<Vec<TCell<CellInfo, TextStyle>>>, size: (usize, usize)) {
for row in data {
if row.len() < size.1 {
row.extend(
std::iter::repeat(Table::create_cell(String::default(), TextStyle::default()))
.take(size.1 - row.len()),
);
}
}
}
#[derive(Debug, Clone)]
pub struct TableConfig {
theme: TableTheme,
alignments: Alignments,
trim: TrimStrategy,
split_color: Option<Style>,
expand: bool,
with_index: bool,
with_header: bool,
with_footer: bool,
}
impl TableConfig {
pub fn new(
theme: TableTheme,
with_header: bool,
with_index: bool,
append_footer: bool,
) -> Self {
Self {
theme,
with_header,
with_index,
with_footer: append_footer,
expand: false,
alignments: Alignments::default(),
trim: TrimStrategy::truncate(None),
split_color: None,
}
}
pub fn expand(mut self) -> Self {
self.expand = true;
self
}
pub fn trim(mut self, strategy: TrimStrategy) -> Self {
self.trim = strategy;
self
}
pub fn splitline_style(mut self, color: Style) -> Self {
self.split_color = Some(color);
self
}
}
@ -150,65 +176,63 @@ impl Default for Alignments {
}
}
fn draw_table(
mut table: Table,
config: &Config,
color_hm: &HashMap<String, nu_ansi_term::Style>,
alignments: Alignments,
theme: &TableTheme,
termwidth: usize,
expand: bool,
) -> Option<String> {
if table.is_empty {
fn build_table(mut data: Data, cfg: TableConfig, termwidth: usize) -> Option<String> {
let priority = TruncationPriority::Content;
let is_empty = maybe_truncate_columns(&mut data, &cfg.theme, termwidth, priority);
if is_empty {
return None;
}
let with_header = table.with_header;
let with_footer = with_header && need_footer(config, (table.data).size().0 as u64);
let with_index = table.with_index;
if with_footer {
table.data.duplicate_row(0);
if cfg.with_footer {
data.duplicate_row(0);
}
let mut table = Builder::custom(table.data).build();
load_theme(&mut table, color_hm, theme, with_footer, with_header);
draw_table(
data,
&cfg.theme,
cfg.alignments,
cfg.with_index,
cfg.with_header,
cfg.with_footer,
cfg.expand,
cfg.split_color,
&cfg.trim,
termwidth,
)
}
#[allow(clippy::too_many_arguments)]
fn draw_table(
data: Data,
theme: &TableTheme,
alignments: Alignments,
with_index: bool,
with_header: bool,
with_footer: bool,
expand: bool,
split_color: Option<Style>,
trim_strategy: &TrimStrategy,
termwidth: usize,
) -> Option<String> {
let mut table = Builder::custom(data).build();
load_theme(&mut table, theme, with_footer, with_header, split_color);
align_table(&mut table, alignments, with_index, with_header, with_footer);
if expand {
table.with(Width::increase(termwidth));
}
table_trim_columns(&mut table, termwidth, &config.trim_strategy);
table_trim_columns(&mut table, termwidth, trim_strategy);
let table = print_table(table, config);
if table_width(&table) > termwidth {
let text = table.to_string();
if string_width_multiline(&text) > termwidth {
None
} else {
Some(table)
Some(text)
}
}
fn print_table(table: tabled::Table<Data>, config: &Config) -> String {
let output = table.to_string();
// the atty is for when people do ls from vim, there should be no coloring there
if !config.use_ansi_coloring || !atty::is(atty::Stream::Stdout) {
// Draw the table without ansi colors
nu_utils::strip_ansi_string_likely(output)
} else {
// Draw the table with ansi colors
output
}
}
fn table_width(table: &str) -> usize {
table
.lines()
.next()
.map_or(0, papergrid::util::string_width)
}
fn align_table(
table: &mut tabled::Table<Data>,
alignments: Alignments,
@ -268,10 +292,10 @@ fn override_alignments(
fn load_theme<R>(
table: &mut tabled::Table<R>,
color_hm: &HashMap<String, nu_ansi_term::Style>,
theme: &TableTheme,
with_footer: bool,
with_header: bool,
separator_color: Option<Style>,
) where
R: Records,
{
@ -282,7 +306,7 @@ fn load_theme<R>(
table.with(theme);
if let Some(color) = color_hm.get("separator") {
if let Some(color) = separator_color {
let color = color.paint(" ").to_string();
if let Ok(color) = Color::try_from(color) {
table.with(color);
@ -298,11 +322,6 @@ fn load_theme<R>(
}
}
fn need_footer(config: &Config, count_records: u64) -> bool {
matches!(config.footer_mode, FooterMode::RowCount(limit) if count_records > limit)
|| matches!(config.footer_mode, FooterMode::Always)
}
struct FooterStyle;
impl<R> TableOption<R> for FooterStyle
@ -352,7 +371,7 @@ where
fn change(&mut self, table: &mut tabled::Table<R>) {
match self.trim_strategy {
TrimStrategy::Wrap { try_to_keep_words } => {
let mut w = Width::wrap(self.termwidth).priority::<tabled::peaker::PriorityMax>();
let mut w = Width::wrap(self.termwidth).priority::<PriorityMax>();
if *try_to_keep_words {
w = w.keep_words();
}
@ -360,8 +379,7 @@ where
w.change(table)
}
TrimStrategy::Truncate { suffix } => {
let mut w =
Width::truncate(self.termwidth).priority::<tabled::peaker::PriorityMax>();
let mut w = Width::truncate(self.termwidth).priority::<PriorityMax>();
if let Some(suffix) = suffix {
w = w.suffix(suffix).suffix_try_color(true);
}
@ -372,20 +390,188 @@ where
}
}
fn maybe_truncate_columns(data: &mut Data, length: usize, termwidth: usize) -> bool {
// Make sure we have enough space for the columns we have
let max_num_of_columns = termwidth / 10;
if max_num_of_columns == 0 {
enum TruncationPriority {
// VERSION where we are showing AS LITTLE COLUMNS AS POSSIBLE but WITH AS MUCH CONTENT AS POSSIBLE.
Content,
// VERSION where we are showing AS MANY COLUMNS AS POSSIBLE but as a side affect they MIGHT CONTAIN AS LITTLE CONTENT AS POSSIBLE
//
// not used so far.
#[allow(dead_code)]
Columns,
}
fn maybe_truncate_columns(
data: &mut Data,
theme: &TableTheme,
termwidth: usize,
priority: TruncationPriority,
) -> bool {
if data.count_columns() == 0 {
return true;
}
// If we have too many columns, truncate the table
if max_num_of_columns < length {
data.truncate(max_num_of_columns);
data.push(Table::create_cell(
String::from("..."),
TextStyle::default(),
));
match priority {
TruncationPriority::Content => truncate_columns_by_content(data, theme, termwidth),
TruncationPriority::Columns => truncate_columns_by_columns(data, theme, termwidth),
}
}
// VERSION where we are showing AS LITTLE COLUMNS AS POSSIBLE but WITH AS MUCH CONTENT AS POSSIBLE.
fn truncate_columns_by_content(data: &mut Data, theme: &TableTheme, termwidth: usize) -> bool {
const MIN_ACCEPTABLE_WIDTH: usize = 3;
const TRAILING_COLUMN_WIDTH: usize = 5;
const TRAILING_COLUMN_STR: &str = "...";
let config;
let total;
{
let mut table = Builder::custom(&*data).build();
load_theme(&mut table, theme, false, false, None);
total = table.total_width();
config = table.get_config().clone();
}
if total <= termwidth {
return false;
}
let mut width_ctrl = tabled::papergrid::width::WidthEstimator::default();
width_ctrl.estimate(&*data, &config);
let widths = Vec::from(width_ctrl);
let borders = config.get_borders();
let vertical_border_i = borders.has_vertical() as usize;
let mut width = borders.has_left() as usize + borders.has_right() as usize;
let mut truncate_pos = 0;
for column_width in widths {
width += column_width;
width += vertical_border_i;
if width >= termwidth {
// check whether we CAN limit the column width
width -= column_width;
width += MIN_ACCEPTABLE_WIDTH;
if width <= termwidth {
truncate_pos += 1;
}
break;
}
truncate_pos += 1;
}
// we don't need any truncation then (is it possible?)
if truncate_pos + 1 == data.count_columns() {
return false;
}
if truncate_pos == 0 {
return true;
}
data.truncate(truncate_pos);
// Append columns with a trailing column
let min_width = borders.has_left() as usize
+ borders.has_right() as usize
+ data.count_columns() * MIN_ACCEPTABLE_WIDTH
+ (data.count_columns() - 1) * vertical_border_i;
let diff = termwidth - min_width;
let can_be_squeezed = diff > TRAILING_COLUMN_WIDTH + vertical_border_i;
if can_be_squeezed {
let cell = Table::create_cell(String::from(TRAILING_COLUMN_STR), TextStyle::default());
data.push(cell);
} else {
if data.count_columns() == 1 {
return true;
}
data.truncate(data.count_columns() - 1);
let cell = Table::create_cell(String::from(TRAILING_COLUMN_STR), TextStyle::default());
data.push(cell);
}
false
}
fn truncate_columns_by_columns(data: &mut Data, theme: &TableTheme, termwidth: usize) -> bool {
const MIN_ACCEPTABLE_WIDTH: usize = 3;
const TRAILING_COLUMN_WIDTH: usize = 3;
const TRAILING_COLUMN_PADDING: usize = 2;
const TRAILING_COLUMN_STR: &str = "...";
let config;
let total;
{
let mut table = Builder::custom(&*data).build();
load_theme(&mut table, theme, false, false, None);
total = table.total_width();
config = table.get_config().clone();
}
if total <= termwidth {
return false;
}
let mut width_ctrl = tabled::papergrid::width::WidthEstimator::default();
width_ctrl.estimate(&*data, &config);
let widths = Vec::from(width_ctrl);
let widths_total = widths.iter().sum::<usize>();
let min_widths = widths
.iter()
.map(|w| min(*w, MIN_ACCEPTABLE_WIDTH))
.sum::<usize>();
let mut min_total = total - widths_total + min_widths;
if min_total <= termwidth {
return false;
}
while data.count_columns() > 0 {
let column = data.count_columns() - 1;
data.truncate(column);
let width = widths[column];
let min_width = min(width, MIN_ACCEPTABLE_WIDTH);
min_total -= min_width;
if config.get_borders().has_vertical() {
min_total -= 1;
}
if min_total <= termwidth {
break;
}
}
if data.count_columns() == 0 {
return true;
}
// Append columns with a trailing column
let diff = termwidth - min_total;
if diff > TRAILING_COLUMN_WIDTH + TRAILING_COLUMN_PADDING {
let cell = Table::create_cell(String::from(TRAILING_COLUMN_STR), TextStyle::default());
data.push(cell);
} else {
if data.count_columns() == 1 {
return true;
}
data.truncate(data.count_columns() - 1);
let cell = Table::create_cell(String::from(TRAILING_COLUMN_STR), TextStyle::default());
data.push(cell);
}
false
@ -410,3 +596,27 @@ impl papergrid::Color for TextStyle {
Ok(())
}
}
/// The same as [`tabled::peaker::PriorityMax`] but prioritizes left columns first in case of equal width.
#[derive(Debug, Default, Clone)]
pub struct PriorityMax;
impl Peaker for PriorityMax {
fn create() -> Self {
Self
}
fn peak(&mut self, _: &[usize], widths: &[usize]) -> Option<usize> {
let col = (0..widths.len()).rev().max_by_key(|&i| widths[i]);
match col {
Some(col) => {
if widths[col] == 0 {
None
} else {
Some(col)
}
}
None => None,
}
}
}

View File

@ -0,0 +1,54 @@
use tabled::{builder::Builder, Padding, Style, Width};
pub fn string_width(text: &str) -> usize {
tabled::papergrid::util::string_width_multiline_tab(text, 4)
}
pub fn wrap_string(text: &str, width: usize) -> String {
// todo: change me...
//
// well... it's not effitient to build a table to wrap a string,
// but ... it's better than a copy paste (is it?)
if text.is_empty() {
return String::new();
}
Builder::from_iter([[text]])
.build()
.with(Padding::zero())
.with(Style::empty())
.with(Width::wrap(width))
.to_string()
}
pub fn string_truncate(text: &str, width: usize) -> String {
// todo: change me...
let line = match text.lines().next() {
Some(first_line) => first_line,
None => return String::new(),
};
Builder::from_iter([[line]])
.build()
.with(Style::empty())
.with(Padding::zero())
.with(Width::truncate(width))
.to_string()
}
pub fn string_wrap(text: &str, width: usize) -> String {
// todo: change me...
if text.is_empty() {
return String::new();
}
Builder::from_iter([[text]])
.build()
.with(Style::empty())
.with(Padding::zero())
.with(Width::wrap(width))
.to_string()
}