Compare commits

...

7 Commits

Author SHA1 Message Date
German David
cb133ed387
feat(table): Add new 'single' table mode (#15672)
<!--
if this PR closes one or more issues, you can automatically link the PR
with
them by using one of the [*linking
keywords*](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword),
e.g.
- this PR should close #xxxx
- fixes #xxxx

you can also mention related issues, PRs or discussions!
-->
closes #15381

# Description
<!--
Thank you for improving Nushell. Please, check our [contributing
guide](../CONTRIBUTING.md) and talk to the core team before making major
changes.

Description of your pull request goes here. **Provide examples and/or
screenshots** if your changes affect the user experience.
-->
Adds a new table mode called `single`, it looks like the `heavy` mode,
but the key difference is that it uses thinner lines. I decided on the
name `single` because it's one of the border styles Neovim uses, and
they look practically the same.

# User-Facing Changes
<!-- List of all changes that impact the user experience here. This
helps us keep track of breaking changes. -->

New config option:

```nushell
$env.config.table.mode = 'single'
```

# Tests + Formatting
<!--
Don't forget to add tests that cover your changes.

Make sure you've run and fixed any issues with these commands:

- `cargo fmt --all -- --check` to check standard code formatting (`cargo
fmt --all` applies these changes)
- `cargo clippy --workspace -- -D warnings -D clippy::unwrap_used` to
check that you're using the standard code style
- `cargo test --workspace` to check that all tests pass (on Windows make
sure to [enable developer
mode](https://learn.microsoft.com/en-us/windows/apps/get-started/developer-mode-features-and-debugging))
- `cargo run -- -c "use toolkit.nu; toolkit test stdlib"` to run the
tests for the standard library

> **Note**
> from `nushell` you can also use the `toolkit` as follows
> ```bash
> use toolkit.nu # or use an `env_change` hook to activate it
automatically
> toolkit check pr
> ```
-->
Added new tests in `crates/nu-table/tests/style.rs` to cover the single
table mode.

# After Submitting
<!-- If your PR had any user-facing changes, update [the
documentation](https://github.com/nushell/nushell.github.io) after the
PR is merged, if necessary. This will help us keep the docs up to date.
-->
2025-05-01 15:30:57 -05:00
zc he
a7547a54bc
fix(parser): namespace pollution of constants by use module.nu (#15518)
A bug introduced by #14920 

When `use module.nu` is called, all exported constants defined in it are
added to the scope.

# Description

On the branch of empty arguments, the constant var_id vector should be
empty, only constant_values (for `$module.foo` access) are injected.

# User-Facing Changes

# Tests + Formatting

~todo!~

adjusted

# After Submitting
2025-05-01 09:47:16 -05:00
Luong Vo
d1969a3c9a
docs: update ubuntu version in PLATFORM_SUPPORT.md (#15662)
<!--
if this PR closes one or more issues, you can automatically link the PR
with
them by using one of the [*linking
keywords*](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword),
e.g.
- this PR should close #xxxx
- fixes #xxxx

you can also mention related issues, PRs or discussions!
-->

# Description
<!--
Thank you for improving Nushell. Please, check our [contributing
guide](../CONTRIBUTING.md) and talk to the core team before making major
changes.

Description of your pull request goes here. **Provide examples and/or
screenshots** if your changes affect the user experience.
-->

I was interested in how nu-shell handles glibc, especially older
versions of it. I figured out from the docs that ubuntu 20.04 is
utilized. However, in reality, github has deprecated ubuntu 20.04, and
the code for ci.yaml in github workflow clearly states that it is 22.04.

This is just a minor doc update to clarify forgotten information
2025-05-01 09:44:49 -05:00
pyz4
ce582cdafb
feat(polars): add polars horizontal aggregation command (#15656)
<!--
if this PR closes one or more issues, you can automatically link the PR
with
them by using one of the [*linking
keywords*](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword),
e.g.
- this PR should close #xxxx
- fixes #xxxx

you can also mention related issues, PRs or discussions!
-->

# Description
<!--
Thank you for improving Nushell. Please, check our [contributing
guide](../CONTRIBUTING.md) and talk to the core team before making major
changes.

Description of your pull request goes here. **Provide examples and/or
screenshots** if your changes affect the user experience.
-->
This PR seeks to port over the `*_horizontal` commands in polars
rust/python (e.g.,
https://docs.pola.rs/api/python/stable/reference/expressions/api/polars.sum_horizontal.html),
which aggregate across multiple columns (as opposed to rows). See below
for several examples.

```nushell
#  Horizontal sum across two columns (ignore nulls by default)
  > [[a b]; [1 2] [2 3] [3 4] [4 5] [5 null]]
                    | polars into-df
                    | polars select (polars horizontal sum a b)
                    | polars collect
  ╭───┬─────╮
  │ # │ sum │
  ├───┼─────┤
  │ 0 │   3 │
  │ 1 │   5 │
  │ 2 │   7 │
  │ 3 │   9 │
  │ 4 │   5 │
  ╰───┴─────╯

#  Horizontal sum across two columns while accounting for nulls
  > [[a b]; [1 2] [2 3] [3 4] [4 5] [5 null]]
                    | polars into-df
                    | polars select (polars horizontal sum a b --nulls)
                    | polars collect
  ╭───┬─────╮
  │ # │ sum │
  ├───┼─────┤
  │ 0 │   3 │
  │ 1 │   5 │
  │ 2 │   7 │
  │ 3 │   9 │
  │ 4 │     │
  ╰───┴─────╯
```

# User-Facing Changes
<!-- List of all changes that impact the user experience here. This
helps us keep track of breaking changes. -->
No breaking changes. Users have access to a new command, `polars
horizontal`.

# Tests + Formatting
<!--
Don't forget to add tests that cover your changes.

Make sure you've run and fixed any issues with these commands:

- `cargo fmt --all -- --check` to check standard code formatting (`cargo
fmt --all` applies these changes)
- `cargo clippy --workspace -- -D warnings -D clippy::unwrap_used` to
check that you're using the standard code style
- `cargo test --workspace` to check that all tests pass (on Windows make
sure to [enable developer
mode](https://learn.microsoft.com/en-us/windows/apps/get-started/developer-mode-features-and-debugging))
- `cargo run -- -c "use toolkit.nu; toolkit test stdlib"` to run the
tests for the standard library

> **Note**
> from `nushell` you can also use the `toolkit` as follows
> ```bash
> use toolkit.nu # or use an `env_change` hook to activate it
automatically
> toolkit check pr
> ```
-->
Example tests were added to `polars horizontal`.

# After Submitting
<!-- If your PR had any user-facing changes, update [the
documentation](https://github.com/nushell/nushell.github.io) after the
PR is merged, if necessary. This will help us keep the docs up to date.
-->
2025-05-01 09:44:15 -05:00
Bahex
55de232a1c
refactor Value::follow_cell_path to reduce clones and return Cow (#15640)
# Description
While working on something else, I noticed that
`Value::follow_cell_path` receives `self`.

While it would be ideal for the signature to be `(&'a self, cell_path)
-> &'a Value`, that's not possible because:
1. Selecting a row from a list and field from a record can be done with
a reference but selecting a column from a table requires creating a new
list.
2. `Value::Custom` returns new `Value`s when indexed.

So the signature becomes `(&'a self, cell_path) -> Cow<'a, Value>`.

Another complication that arises is, once a new `Value` is created, and
it is further indexed, the `current` variable
1. can't be `&'a Value`, as the lifetime requirement means it can't
refer to local variables
2. _shouldn't_ be `Cow<'a, Value>`, as once it becomes an owned value,
it can't be borrowed ever again, as `current` is derived from its
previous value in further iterations. So once it's owned, it can't be
indexed by reference, leading to more clones

We need `current` to have _two_ possible lifetimes
1. `'out`: references derived from `&self`
2. `'local`: references derived from an owned value stored in a local
variable

```rust
enum MultiLife<'out, 'local, T>
where
    'out: 'local,
    T: ?Sized,
{
    Out(&'out T),
    Local(&'local T),
}
```
With `current: MultiLife<'out, '_, Value>`, we can traverse values with
minimal clones, and we can transform it to `Cow<'out, Value>` easily
(`MultiLife::Out -> Cow::Borrowed, MultiLife::Local -> Cow::Owned`) to
return it

# User-Facing Changes

# Tests + Formatting

# After Submitting

---------

Co-authored-by: Bahex <17417311+Bahex@users.noreply.github.com>
2025-05-01 09:43:57 -05:00
Maxim Zhiburt
deca337a56
nu-table/ 1 refactoring + a few optimizations + small fix (#15653)
- A few days back I've got this idea regarding recalculus of width.
Now it calculates step by step.
So 1 loop over all data was removed.
All though there's full recalculation in case of `header_on_border`
😞 (can be fixed..... but I decided to be short)

In perfect world it also shall be refactored ......

- Also have done small refactoring to switch build table from
`Vec<Vec<_>>>` to table itself. To hide internals (kind of still there's
things which I don't like).
It touched the `--expand` algorithm lightly you can see the tests
changes.

- And when doing that noticed one more opportunity, to remove HashMap
usage and directly use `tabled::ColoredConfig`. Which reduces copy
operations and allocations.

- And fixed a small issue where trailing column being using deleted
column styles.


![image](https://github.com/user-attachments/assets/19b09dba-c688-4e91-960a-e11ed11fd275)

To conclude optimizations;
I did small testing and it's not slower.
But I didn't get the faster results either.
But I believe it must be faster well in all cases, I think maybe bigger
tables must be tested.
Maybe someone could have a few runs to compare performance.

cc: @fdncred
2025-05-01 09:43:30 -05:00
Tyarel
60e9f469af
change http get header example to use a record (#15674)
# Description

When first using `http get`, I was confused that all the examples used a
list for headers, leading me to believe this was the only way, and it
seemed a little weird having records in the language. Then, I found out
that you can indeed use record, so I changed the example to show this
behavior in a way users can find. There still is another examples that
uses a list so there should be no problem there.

# User-Facing Changes
<!-- List of all changes that impact the user experience here. This
helps us keep track of breaking changes. -->

# Tests + Formatting
<!--
Don't forget to add tests that cover your changes.

Make sure you've run and fixed any issues with these commands:

- `cargo fmt --all -- --check` to check standard code formatting (`cargo
fmt --all` applies these changes)
- `cargo clippy --workspace -- -D warnings -D clippy::unwrap_used` to
check that you're using the standard code style
- `cargo test --workspace` to check that all tests pass (on Windows make
sure to [enable developer
mode](https://learn.microsoft.com/en-us/windows/apps/get-started/developer-mode-features-and-debugging))
- `cargo run -- -c "use toolkit.nu; toolkit test stdlib"` to run the
tests for the standard library

> **Note**
> from `nushell` you can also use the `toolkit` as follows
> ```bash
> use toolkit.nu # or use an `env_change` hook to activate it
automatically
> toolkit check pr
> ```
-->

# After Submitting
<!-- If your PR had any user-facing changes, update [the
documentation](https://github.com/nushell/nushell.github.io) after the
PR is merged, if necessary. This will help us keep the docs up to date.
-->
2025-05-01 09:42:53 -05:00
43 changed files with 1506 additions and 1061 deletions

View File

@ -1,3 +1,5 @@
use std::borrow::Cow;
use crate::completions::{Completer, CompletionOptions, SemanticSuggestion, SuggestionKind};
use nu_engine::{column::get_columns, eval_variable};
use nu_protocol::{
@ -101,7 +103,9 @@ pub(crate) fn eval_cell_path(
} else {
eval_constant(working_set, head)
}?;
head_value.follow_cell_path(path_members, false)
head_value
.follow_cell_path(path_members, false)
.map(Cow::into_owned)
}
fn get_suggestions_by_value(

View File

@ -253,12 +253,11 @@ fn format_record(
optional: false,
})
.collect();
match data_as_value.clone().follow_cell_path(&path_members, false) {
Ok(value_at_column) => {
output.push_str(value_at_column.to_expanded_string(", ", config).as_str())
}
Err(se) => return Err(se),
}
let expanded_string = data_as_value
.follow_cell_path(&path_members, false)?
.to_expanded_string(", ", config);
output.push_str(expanded_string.as_str())
}
}
}

View File

@ -15,17 +15,8 @@ pub fn empty(
if !columns.is_empty() {
for val in input {
for column in &columns {
let val = val.clone();
match val.follow_cell_path(&column.members, false) {
Ok(Value::Nothing { .. }) => {}
Ok(_) => {
if negate {
return Ok(Value::bool(true, head).into_pipeline_data());
} else {
return Ok(Value::bool(false, head).into_pipeline_data());
}
}
Err(err) => return Err(err),
if !val.follow_cell_path(&column.members, false)?.is_nothing() {
return Ok(Value::bool(negate, head).into_pipeline_data());
}
}
}

View File

@ -1,3 +1,5 @@
use std::borrow::Cow;
use nu_engine::command_prelude::*;
use nu_protocol::{ast::PathMember, Signals};
@ -188,9 +190,11 @@ fn action(
let input = input.into_value(span)?;
for path in paths {
let val = input.clone().follow_cell_path(&path.members, !sensitive);
output.push(val?);
output.push(
input
.follow_cell_path(&path.members, !sensitive)?
.into_owned(),
);
}
Ok(output.into_iter().into_pipeline_data(span, signals))
@ -223,10 +227,10 @@ pub fn follow_cell_path_into_stream(
.map(move |value| {
let span = value.span();
match value.follow_cell_path(&cell_path, insensitive) {
Ok(v) => v,
Err(error) => Value::error(error, span),
}
value
.follow_cell_path(&cell_path, insensitive)
.map(Cow::into_owned)
.unwrap_or_else(|error| Value::error(error, span))
})
.into_pipeline_data(head, signals);

View File

@ -322,11 +322,9 @@ fn group_cell_path(
let mut groups = IndexMap::<_, Vec<_>>::new();
for value in values.into_iter() {
let key = value
.clone()
.follow_cell_path(&column_name.members, false)?;
let key = value.follow_cell_path(&column_name.members, false)?;
if matches!(key, Value::Nothing { .. }) {
if key.is_nothing() {
continue; // likely the result of a failed optional access, ignore this value
}

View File

@ -1,3 +1,5 @@
use std::borrow::Cow;
use nu_engine::{command_prelude::*, ClosureEval, ClosureEvalOnce};
use nu_protocol::ast::PathMember;
@ -299,8 +301,8 @@ fn insert_value_by_closure(
) -> Result<(), ShellError> {
let value_at_path = if first_path_member_int {
value
.clone()
.follow_cell_path(cell_path, false)
.map(Cow::into_owned)
.unwrap_or(Value::nothing(span))
} else {
value.clone()
@ -319,8 +321,8 @@ fn insert_single_value_by_closure(
) -> Result<(), ShellError> {
let value_at_path = if first_path_member_int {
value
.clone()
.follow_cell_path(cell_path, false)
.map(Cow::into_owned)
.unwrap_or(Value::nothing(span))
} else {
value.clone()

View File

@ -229,45 +229,37 @@ fn select(
match v {
Value::List {
vals: input_vals, ..
} => {
Ok(input_vals
.into_iter()
.map(move |input_val| {
if !columns.is_empty() {
let mut record = Record::new();
for path in &columns {
//FIXME: improve implementation to not clone
match input_val.clone().follow_cell_path(&path.members, false) {
Ok(fetcher) => {
record.push(path.to_column_name(), fetcher);
}
Err(e) => return Value::error(e, call_span),
} => Ok(input_vals
.into_iter()
.map(move |input_val| {
if !columns.is_empty() {
let mut record = Record::new();
for path in &columns {
match input_val.follow_cell_path(&path.members, false) {
Ok(fetcher) => {
record.push(path.to_column_name(), fetcher.into_owned());
}
Err(e) => return Value::error(e, call_span),
}
Value::record(record, span)
} else {
input_val.clone()
}
})
.into_pipeline_data_with_metadata(
call_span,
engine_state.signals().clone(),
metadata,
))
}
Value::record(record, span)
} else {
input_val.clone()
}
})
.into_pipeline_data_with_metadata(
call_span,
engine_state.signals().clone(),
metadata,
)),
_ => {
if !columns.is_empty() {
let mut record = Record::new();
for cell_path in columns {
// FIXME: remove clone
match v.clone().follow_cell_path(&cell_path.members, false) {
Ok(result) => {
record.push(cell_path.to_column_name(), result);
}
Err(e) => return Err(e),
}
let result = v.follow_cell_path(&cell_path.members, false)?;
record.push(cell_path.to_column_name(), result.into_owned());
}
Ok(Value::record(record, call_span)
@ -278,31 +270,24 @@ fn select(
}
}
}
PipelineData::ListStream(stream, metadata, ..) => {
Ok(stream
.map(move |x| {
if !columns.is_empty() {
let mut record = Record::new();
for path in &columns {
//FIXME: improve implementation to not clone
match x.clone().follow_cell_path(&path.members, false) {
Ok(value) => {
record.push(path.to_column_name(), value);
}
Err(e) => return Value::error(e, call_span),
PipelineData::ListStream(stream, metadata, ..) => Ok(stream
.map(move |x| {
if !columns.is_empty() {
let mut record = Record::new();
for path in &columns {
match x.follow_cell_path(&path.members, false) {
Ok(value) => {
record.push(path.to_column_name(), value.into_owned());
}
Err(e) => return Value::error(e, call_span),
}
Value::record(record, call_span)
} else {
x
}
})
.into_pipeline_data_with_metadata(
call_span,
engine_state.signals().clone(),
metadata,
))
}
Value::record(record, call_span)
} else {
x
}
})
.into_pipeline_data_with_metadata(call_span, engine_state.signals().clone(), metadata)),
_ => Ok(PipelineData::empty()),
}
}

View File

@ -243,17 +243,17 @@ fn update_value_by_closure(
cell_path: &[PathMember],
first_path_member_int: bool,
) -> Result<(), ShellError> {
let value_at_path = value.clone().follow_cell_path(cell_path, false)?;
let value_at_path = value.follow_cell_path(cell_path, false)?;
let arg = if first_path_member_int {
&value_at_path
value_at_path.as_ref()
} else {
&*value
};
let new_value = closure
.add_arg(arg.clone())
.run_with_input(value_at_path.into_pipeline_data())?
.run_with_input(value_at_path.into_owned().into_pipeline_data())?
.into_value(span)?;
value.update_data_at_cell_path(cell_path, new_value)
@ -266,17 +266,17 @@ fn update_single_value_by_closure(
cell_path: &[PathMember],
first_path_member_int: bool,
) -> Result<(), ShellError> {
let value_at_path = value.clone().follow_cell_path(cell_path, false)?;
let value_at_path = value.follow_cell_path(cell_path, false)?;
let arg = if first_path_member_int {
&value_at_path
value_at_path.as_ref()
} else {
&*value
};
let new_value = closure
.add_arg(arg.clone())
.run_with_input(value_at_path.into_pipeline_data())?
.run_with_input(value_at_path.into_owned().into_pipeline_data())?
.into_value(span)?;
value.update_data_at_cell_path(cell_path, new_value)

View File

@ -1,3 +1,5 @@
use std::borrow::Cow;
use nu_engine::{command_prelude::*, ClosureEval, ClosureEvalOnce};
use nu_protocol::ast::PathMember;
@ -319,15 +321,19 @@ fn upsert_value_by_closure(
cell_path: &[PathMember],
first_path_member_int: bool,
) -> Result<(), ShellError> {
let value_at_path = value.clone().follow_cell_path(cell_path, false);
let value_at_path = value.follow_cell_path(cell_path, false);
let arg = if first_path_member_int {
value_at_path.clone().unwrap_or(Value::nothing(span))
value_at_path
.as_deref()
.cloned()
.unwrap_or(Value::nothing(span))
} else {
value.clone()
};
let input = value_at_path
.map(Cow::into_owned)
.map(IntoPipelineData::into_pipeline_data)
.unwrap_or(PipelineData::Empty);
@ -346,15 +352,19 @@ fn upsert_single_value_by_closure(
cell_path: &[PathMember],
first_path_member_int: bool,
) -> Result<(), ShellError> {
let value_at_path = value.clone().follow_cell_path(cell_path, false);
let value_at_path = value.follow_cell_path(cell_path, false);
let arg = if first_path_member_int {
value_at_path.clone().unwrap_or(Value::nothing(span))
value_at_path
.as_deref()
.cloned()
.unwrap_or(Value::nothing(span))
} else {
value.clone()
};
let input = value_at_path
.map(Cow::into_owned)
.map(IntoPipelineData::into_pipeline_data)
.unwrap_or(PipelineData::Empty);

View File

@ -117,8 +117,13 @@ impl Command for HttpDelete {
result: None,
},
Example {
description: "http delete from example.com, with custom header",
example: "http delete --headers [my-header-key my-header-value] https://www.example.com",
description: "http delete from example.com, with custom header using a record",
example: "http delete --headers {my-header-key: my-header-value} https://www.example.com",
result: None,
},
Example {
description: "http delete from example.com, with custom header using a list",
example: "http delete --headers [my-header-key-A my-header-value-A my-header-key-B my-header-value-B] https://www.example.com",
result: None,
},
Example {

View File

@ -115,12 +115,12 @@ impl Command for HttpGet {
result: None,
},
Example {
description: "Get content from example.com, with custom header",
example: "http get --headers [my-header-key my-header-value] https://www.example.com",
description: "Get content from example.com, with custom header using a record",
example: "http get --headers {my-header-key: my-header-value} https://www.example.com",
result: None,
},
Example {
description: "Get content from example.com, with custom headers",
description: "Get content from example.com, with custom headers using a list",
example: "http get --headers [my-header-key-A my-header-value-A my-header-key-B my-header-value-B] https://www.example.com",
result: None,
},

View File

@ -97,9 +97,15 @@ impl Command for HttpHead {
result: None,
},
Example {
description: "Get headers from example.com, with custom header",
description: "Get headers from example.com, with custom header using a record",
example:
"http head --headers [my-header-key my-header-value] https://www.example.com",
"http head --headers {my-header-key: my-header-value} https://www.example.com",
result: None,
},
Example {
description: "Get headers from example.com, with custom header using a list",
example:
"http head --headers [my-header-key-A my-header-value-A my-header-key-B my-header-value-B] https://www.example.com",
result: None,
},
]

View File

@ -96,12 +96,12 @@ impl Command for HttpOptions {
result: None,
},
Example {
description: "Get options from example.com, with custom header",
example: "http options --headers [my-header-key my-header-value] https://www.example.com",
description: "Get options from example.com, with custom header using a record",
example: "http options --headers {my-header-key: my-header-value} https://www.example.com",
result: None,
},
Example {
description: "Get options from example.com, with custom headers",
description: "Get options from example.com, with custom headers using a list",
example: "http options --headers [my-header-key-A my-header-value-A my-header-key-B my-header-value-B] https://www.example.com",
result: None,
},

View File

@ -114,9 +114,15 @@ impl Command for HttpPatch {
result: None,
},
Example {
description: "Patch content to example.com, with custom header",
description: "Patch content to example.com, with custom header using a record",
example:
"http patch --headers [my-header-key my-header-value] https://www.example.com",
"http patch --headers {my-header-key: my-header-value} https://www.example.com",
result: None,
},
Example {
description: "Patch content to example.com, with custom header using a list",
example:
"http patch --headers [my-header-key-A my-header-value-A my-header-key-B my-header-value-B] https://www.example.com",
result: None,
},
Example {

View File

@ -113,8 +113,13 @@ impl Command for HttpPost {
result: None,
},
Example {
description: "Post content to example.com, with custom header",
example: "http post --headers [my-header-key my-header-value] https://www.example.com",
description: "Post content to example.com, with custom header using a record",
example: "http post --headers {my-header-key: my-header-value} https://www.example.com",
result: None,
},
Example {
description: "Post content to example.com, with custom header using a list",
example: "http post --headers [my-header-key-A my-header-value-A my-header-key-B my-header-value-B] https://www.example.com",
result: None,
},
Example {

View File

@ -113,8 +113,13 @@ impl Command for HttpPut {
result: None,
},
Example {
description: "Put content to example.com, with custom header",
example: "http put --headers [my-header-key my-header-value] https://www.example.com",
description: "Put content to example.com, with custom header using a record",
example: "http put --headers {my-header-key: my-header-value} https://www.example.com",
result: None,
},
Example {
description: "Put content to example.com, with custom header using a list",
example: "http put --headers [my-header-key-A my-header-value-A my-header-key-B my-header-value-B] https://www.example.com",
result: None,
},
Example {

View File

@ -89,8 +89,7 @@ impl Command for InputList {
.into_iter()
.map(move |val| {
let display_value = if let Some(ref cellpath) = display_path {
val.clone()
.follow_cell_path(&cellpath.members, false)?
val.follow_cell_path(&cellpath.members, false)?
.to_expanded_string(", ", &config)
} else {
val.to_expanded_string(", ", &config)

View File

@ -239,8 +239,8 @@ pub fn compare_cell_path(
insensitive: bool,
natural: bool,
) -> Result<Ordering, ShellError> {
let left = left.clone().follow_cell_path(&cell_path.members, false)?;
let right = right.clone().follow_cell_path(&cell_path.members, false)?;
let left = left.follow_cell_path(&cell_path.members, false)?;
let right = right.follow_cell_path(&cell_path.members, false)?;
compare_values(&left, &right, insensitive, natural)
}

View File

@ -17,8 +17,8 @@ use nu_protocol::{
Signals, TableMode, ValueIterator,
};
use nu_table::{
common::configure_table, CollapsedTable, ExpandedTable, JustTable, NuRecordsValue, NuTable,
StringResult, TableOpts, TableOutput,
common::configure_table, CollapsedTable, ExpandedTable, JustTable, NuTable, StringResult,
TableOpts, TableOutput,
};
use nu_utils::{get_ls_colors, terminal_size};
@ -609,7 +609,7 @@ fn build_table_kv(
span: Span,
) -> StringResult {
match table_view {
TableView::General => JustTable::kv_table(&record, opts),
TableView::General => JustTable::kv_table(record, opts),
TableView::Expanded {
limit,
flatten,
@ -645,7 +645,7 @@ fn build_table_batch(
}
match view {
TableView::General => JustTable::table(&vals, opts),
TableView::General => JustTable::table(vals, opts),
TableView::Expanded {
limit,
flatten,
@ -1090,9 +1090,9 @@ fn create_empty_placeholder(
return String::new();
}
let cell = NuRecordsValue::new(format!("empty {}", value_type_name));
let data = vec![vec![cell]];
let mut table = NuTable::from(data);
let cell = format!("empty {}", value_type_name);
let mut table = NuTable::new(1, 1);
table.insert((0, 0), cell);
table.set_data_style(TextStyle::default().dimmed());
let mut out = TableOutput::from_table(table, false, false);

View File

@ -1333,15 +1333,17 @@ fn test_expand_big_0() {
"│ target │ {record 3 fields} │",
"│ dev-dependencies │ {record 9 fields} │",
"│ features │ {record 8 fields} │",
"│ │ ╭───┬─────┬─────╮ │",
"│ bin │ │ # │ nam │ pat │ │",
"│ │ │ │ e │ h │ │",
"│ │ ├───┼─────┼─────┤ │",
"│ │ │ 0 │ nu │ src │ │",
"│ │ │ │ │ /ma │ │",
"│ │ │ │ │ in. │ │",
"│ │ │ │ │ rs │ │",
"│ │ ╰───┴─────┴─────╯ │",
"│ │ ╭───┬──────┬────╮ │",
"│ bin │ │ # │ name │ pa │ │",
"│ │ │ │ │ th │ │",
"│ │ ├───┼──────┼────┤ │",
"│ │ │ 0 │ nu │ sr │ │",
"│ │ │ │ │ c/ │ │",
"│ │ │ │ │ ma │ │",
"│ │ │ │ │ in │ │",
"│ │ │ │ │ .r │ │",
"│ │ │ │ │ s │ │",
"│ │ ╰───┴──────┴────╯ │",
"│ │ ╭───────────┬───╮ │",
"│ patch │ │ crates-io │ { │ │",
"│ │ │ │ r │ │",
@ -1360,16 +1362,16 @@ fn test_expand_big_0() {
"│ │ │ │ d │ │",
"│ │ │ │ } │ │",
"│ │ ╰───────────┴───╯ │",
"│ │ ╭───┬──────────╮ │",
"│ bench │ │ # │ nam │ har │ │",
"│ │ │ │ e │ nes │ │",
"│ │ │ │ │ s │ │",
"│ │ ├───┼─────┼─────┤",
"│ │ │ 0 │ ben │ fal │",
"│ │ │ │ chm │ se │ │",
"│ │ │ │ ark │ │ │",
"│ │ │ │ s │ │ │",
"│ │ ╰───┴──────────╯ │",
"│ │ ╭───┬─────────╮ │",
"│ bench │ │ # │ name │ ha │ │",
"│ │ │ │ │ rn │ │",
"│ │ │ │ es │ │",
"│ │ │ │ │ s │",
"│ │ ├───┼──────┼────┤",
"│ │ │ 0 │ benc │ fa │ │",
"│ │ │ │ hmar │ ls │ │",
"│ │ │ │ ks │ e │ │",
"│ │ ╰───┴─────────╯ │",
"╰──────────────────┴───────────────────╯",
]);
@ -1551,193 +1553,114 @@ fn table_expande_with_no_header_internally_0() {
"│ │ │ │ │ │ ╰─────┴──────────╯ │ │ │",
"│ │ │ │ │ display_output │ │ │ │",
"│ │ │ │ ╰────────────────┴────────────────────╯ │ │",
"│ │ │ │ ╭───┬───────────────────────────┬────────────────────────┬────────┬───┬─────╮ │ │",
"│ │ │ menus │ │ # │ name │ only_buffer_difference │ marker │ t │ ... │ │ │",
"│ │ │ │ │ │ │ │ │ y │ │ │ │",
"│ │ │ │ │ │ │ │ │ p │ │ │ │",
"│ │ │ │ │ │ │ │ │ e │ │ │ │",
"│ │ │ │ ├───┼───────────────────────────┼────────────────────────┼────────┼───┼─────┤ │ │",
"│ │ │ │ │ 0 │ completion_menu │ false │ | │ { │ ... │ │ │",
"│ │ │ │ │ │ │ │ │ r │ │ │ │",
"│ │ │ │ │ │ │ │ │ e │ │ │ │",
"│ │ │ │ │ │ │ │ │ c │ │ │ │",
"│ │ │ │ │ │ │ │ │ o │ │ │ │",
"│ │ │ │ │ │ │ │ │ r │ │ │ │",
"│ │ │ │ │ │ │ │ │ d │ │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ │ │",
"│ │ │ │ │ │ │ │ │ 4 │ │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ │ │",
"│ │ │ │ │ │ │ │ │ f │ │ │ │",
"│ │ │ │ │ │ │ │ │ i │ │ │ │",
"│ │ │ │ │ │ │ │ │ e │ │ │ │",
"│ │ │ │ │ │ │ │ │ l │ │ │ │",
"│ │ │ │ │ │ │ │ │ d │ │ │ │",
"│ │ │ │ │ │ │ │ │ s │ │ │ │",
"│ │ │ │ │ │ │ │ │ } │ │ │ │",
"│ │ │ │ │ 1 │ history_menu │ true │ ? │ { │ ... │ │ │",
"│ │ │ │ │ │ │ │ │ r │ │ │ │",
"│ │ │ │ │ │ │ │ │ e │ │ │ │",
"│ │ │ │ │ │ │ │ │ c │ │ │ │",
"│ │ │ │ │ │ │ │ │ o │ │ │ │",
"│ │ │ │ │ │ │ │ │ r │ │ │ │",
"│ │ │ │ │ │ │ │ │ d │ │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ │ │",
"│ │ │ │ │ │ │ │ │ 2 │ │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ │ │",
"│ │ │ │ │ │ │ │ │ f │ │ │ │",
"│ │ │ │ │ │ │ │ │ i │ │ │ │",
"│ │ │ │ │ │ │ │ │ e │ │ │ │",
"│ │ │ │ │ │ │ │ │ l │ │ │ │",
"│ │ │ │ │ │ │ │ │ d │ │ │ │",
"│ │ │ │ │ │ │ │ │ s │ │ │ │",
"│ │ │ │ │ │ │ │ │ } │ │ │ │",
"│ │ │ │ │ 2 │ help_menu │ true │ ? │ { │ ... │ │ │",
"│ │ │ │ │ │ │ │ │ r │ │ │ │",
"│ │ │ │ │ │ │ │ │ e │ │ │ │",
"│ │ │ │ │ │ │ │ │ c │ │ │ │",
"│ │ │ │ │ │ │ │ │ o │ │ │ │",
"│ │ │ │ │ │ │ │ │ r │ │ │ │",
"│ │ │ │ │ │ │ │ │ d │ │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ │ │",
"│ │ │ │ │ │ │ │ │ 6 │ │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ │ │",
"│ │ │ │ │ │ │ │ │ f │ │ │ │",
"│ │ │ │ │ │ │ │ │ i │ │ │ │",
"│ │ │ │ │ │ │ │ │ e │ │ │ │",
"│ │ │ │ │ │ │ │ │ l │ │ │ │",
"│ │ │ │ │ │ │ │ │ d │ │ │ │",
"│ │ │ │ │ │ │ │ │ s │ │ │ │",
"│ │ │ │ │ │ │ │ │ } │ │ │ │",
"│ │ │ │ │ 3 │ commands_menu │ false │ # │ { │ ... │ │ │",
"│ │ │ │ │ │ │ │ │ r │ │ │ │",
"│ │ │ │ │ │ │ │ │ e │ │ │ │",
"│ │ │ │ │ │ │ │ │ c │ │ │ │",
"│ │ │ │ │ │ │ │ │ o │ │ │ │",
"│ │ │ │ │ │ │ │ │ r │ │ │ │",
"│ │ │ │ │ │ │ │ │ d │ │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ │ │",
"│ │ │ │ │ │ │ │ │ 4 │ │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ │ │",
"│ │ │ │ │ │ │ │ │ f │ │ │ │",
"│ │ │ │ │ │ │ │ │ i │ │ │ │",
"│ │ │ │ │ │ │ │ │ e │ │ │ │",
"│ │ │ │ │ │ │ │ │ l │ │ │ │",
"│ │ │ │ │ │ │ │ │ d │ │ │ │",
"│ │ │ │ │ │ │ │ │ s │ │ │ │",
"│ │ │ │ │ │ │ │ │ } │ │ │ │",
"│ │ │ │ │ 4 │ vars_menu │ true │ # │ { │ ... │ │ │",
"│ │ │ │ │ │ │ │ │ r │ │ │ │",
"│ │ │ │ │ │ │ │ │ e │ │ │ │",
"│ │ │ │ │ │ │ │ │ c │ │ │ │",
"│ │ │ │ │ │ │ │ │ o │ │ │ │",
"│ │ │ │ │ │ │ │ │ r │ │ │ │",
"│ │ │ │ │ │ │ │ │ d │ │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ │ │",
"│ │ │ │ │ │ │ │ │ 2 │ │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ │ │",
"│ │ │ │ │ │ │ │ │ f │ │ │ │",
"│ │ │ │ │ │ │ │ │ i │ │ │ │",
"│ │ │ │ │ │ │ │ │ e │ │ │ │",
"│ │ │ │ │ │ │ │ │ l │ │ │ │",
"│ │ │ │ │ │ │ │ │ d │ │ │ │",
"│ │ │ │ │ │ │ │ │ s │ │ │ │",
"│ │ │ │ │ │ │ │ │ } │ │ │ │",
"│ │ │ │ │ 5 │ commands_with_description │ true │ # │ { │ ... │ │ │",
"│ │ │ │ │ │ │ │ │ r │ │ │ │",
"│ │ │ │ │ │ │ │ │ e │ │ │ │",
"│ │ │ │ │ │ │ │ │ c │ │ │ │",
"│ │ │ │ │ │ │ │ │ o │ │ │ │",
"│ │ │ │ │ │ │ │ │ r │ │ │ │",
"│ │ │ │ │ │ │ │ │ d │ │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ │ │",
"│ │ │ │ │ │ │ │ │ 6 │ │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ │ │",
"│ │ │ │ │ │ │ │ │ f │ │ │ │",
"│ │ │ │ │ │ │ │ │ i │ │ │ │",
"│ │ │ │ │ │ │ │ │ e │ │ │ │",
"│ │ │ │ │ │ │ │ │ l │ │ │ │",
"│ │ │ │ │ │ │ │ │ d │ │ │ │",
"│ │ │ │ │ │ │ │ │ s │ │ │ │",
"│ │ │ │ │ │ │ │ │ } │ │ │ │",
"│ │ │ │ ╰───┴───────────────────────────┴────────────────────────┴────────┴───┴─────╯ │ │",
"│ │ │ │ ╭────┬───────────────────────────┬──────────┬─────────┬───────────────┬─────╮ │ │",
"│ │ │ keybindings │ │ # │ name │ modifier │ keycode │ mode │ eve │ │ │",
"│ │ │ │ │ │ │ │ │ │ nt │ │ │",
"│ │ │ │ ├────┼───────────────────────────┼──────────┼─────────┼───────────────┼─────┤ │ │",
"│ │ │ │ │ 0 │ completion_menu │ none │ tab │ ╭───┬───────╮ │ {re │ │ │",
"│ │ │ │ │ │ │ │ │ │ 0 │ emacs │ │ cor │ │ │",
"│ │ │ │ │ │ │ │ │ │ 1 │ vi_no │ │ d 1 │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ rmal │ │ fi │ │ │",
"│ │ │ │ │ │ │ │ │ │ 2 │ vi_in │ │ eld │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ sert │ │ } │ │ │",
"│ │ │ │ │ │ │ │ │ ╰───┴───────╯ │ │ │ │",
"│ │ │ │ │ 1 │ completion_previous │ shift │ backtab │ ╭───┬───────╮ │ {re │ │ │",
"│ │ │ │ │ │ │ │ │ │ 0 │ emacs │ │ cor │ │ │",
"│ │ │ │ │ │ │ │ │ │ 1 │ vi_no │ │ d 1 │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ rmal │ │ fi │ │ │",
"│ │ │ │ │ │ │ │ │ │ 2 │ vi_in │ │ eld │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ sert │ │ } │ │ │",
"│ │ │ │ │ │ │ │ │ ╰───┴───────╯ │ │ │ │",
"│ │ │ │ │ 2 │ history_menu │ control │ char_r │ emacs │ {re │ │ │",
"│ │ │ │ │ │ │ │ │ │ cor │ │ │",
"│ │ │ │ │ │ │ │ │ │ d 2 │ │ │",
"│ │ │ │ │ │ │ │ │ │ fi │ │ │",
"│ │ │ │ │ │ │ │ │ │ eld │ │ │",
"│ │ │ │ │ │ │ │ │ │ s} │ │ │",
"│ │ │ │ │ 3 │ next_page │ control │ char_x │ emacs │ {re │ │ │",
"│ │ │ │ │ │ │ │ │ │ cor │ │ │",
"│ │ │ │ │ │ │ │ │ │ d 1 │ │ │",
"│ │ │ │ │ │ │ │ │ │ fi │ │ │",
"│ │ │ │ │ │ │ │ │ │ eld │ │ │",
"│ │ │ │ │ │ │ │ │ │ } │ │ │",
"│ │ │ │ │ 4 │ undo_or_previous_page │ control │ char_z │ emacs │ {re │ │ │",
"│ │ │ │ │ │ │ │ │ │ cor │ │ │",
"│ │ │ │ │ │ │ │ │ │ d 1 │ │ │",
"│ │ │ │ │ │ │ │ │ │ fi │ │ │",
"│ │ │ │ │ │ │ │ │ │ eld │ │ │",
"│ │ │ │ │ │ │ │ │ │ } │ │ │",
"│ │ │ │ │ 5 │ yank │ control │ char_y │ emacs │ {re │ │ │",
"│ │ │ │ │ │ │ │ │ │ cor │ │ │",
"│ │ │ │ │ │ │ │ │ │ d 1 │ │ │",
"│ │ │ │ │ │ │ │ │ │ fi │ │ │",
"│ │ │ │ │ │ │ │ │ │ eld │ │ │",
"│ │ │ │ │ │ │ │ │ │ } │ │ │",
"│ │ │ │ │ 6 │ unix-line-discard │ control │ char_u │ ╭───┬───────╮ │ {re │ │ │",
"│ │ │ │ │ │ │ │ │ │ 0 │ emacs │ │ cor │ │ │",
"│ │ │ │ │ │ │ │ │ │ 1 │ vi_no │ │ d 1 │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ rmal │ │ fi │ │ │",
"│ │ │ │ │ │ │ │ │ │ 2 │ vi_in │ │ eld │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ sert │ │ } │ │ │",
"│ │ │ │ │ │ │ │ │ ╰───┴───────╯ │ │ │ │",
"│ │ │ │ │ 7 │ kill-line │ control │ char_k │ ╭───┬───────╮ │ {re │ │ │",
"│ │ │ │ │ │ │ │ │ │ 0 │ emacs │ │ cor │ │ │",
"│ │ │ │ │ │ │ │ │ │ 1 │ vi_no │ │ d 1 │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ rmal │ │ fi │ │ │",
"│ │ │ │ │ │ │ │ │ │ 2 │ vi_in │ │ eld │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ sert │ │ } │ │ │",
"│ │ │ │ │ │ │ │ │ ╰───┴───────╯ │ │ │ │",
"│ │ │ │ │ 8 │ commands_menu │ control │ char_t │ ╭───┬───────╮ │ {re │ │ │",
"│ │ │ │ │ │ │ │ │ │ 0 │ emacs │ │ cor │ │ │",
"│ │ │ │ │ │ │ │ │ │ 1 │ vi_no │ │ d 2 │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ rmal │ │ fi │ │ │",
"│ │ │ │ │ │ │ │ │ │ 2 │ vi_in │ │ eld │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ sert │ │ s} │ │ │",
"│ │ │ │ │ │ │ │ │ ╰───┴───────╯ │ │ │ │",
"│ │ │ │ │ 9 │ vars_menu │ alt │ char_o │ ╭───┬───────╮ │ {re │ │ │",
"│ │ │ │ │ │ │ │ │ │ 0 │ emacs │ │ cor │ │ │",
"│ │ │ │ │ │ │ │ │ │ 1 │ vi_no │ │ d 2 │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ rmal │ │ fi │ │ │",
"│ │ │ │ │ │ │ │ │ │ 2 │ vi_in │ │ eld │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ sert │ │ s} │ │ │",
"│ │ │ │ │ │ │ │ │ ╰───┴───────╯ │ │ │ │",
"│ │ │ │ │ 10 │ commands_with_description │ control │ char_s │ ╭───┬───────╮ │ {re │ │ │",
"│ │ │ │ │ │ │ │ │ │ 0 │ emacs │ │ cor │ │ │",
"│ │ │ │ │ │ │ │ │ │ 1 │ vi_no │ │ d 2 │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ rmal │ │ fi │ │ │",
"│ │ │ │ │ │ │ │ │ │ 2 │ vi_in │ │ eld │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ sert │ │ s} │ │ │",
"│ │ │ │ │ │ │ │ │ ╰───┴───────╯ │ │ │ │",
"│ │ │ │ ╰────┴───────────────────────────┴──────────┴─────────┴───────────────┴─────╯ │ │",
"│ │ │ │ ╭───┬───────────────────────────┬────────────────────────┬────────┬─────╮ │ │",
"│ │ │ menus │ │ # │ name │ only_buffer_difference │ marker │ ... │ │ │",
"│ │ │ │ ├───┼───────────────────────────┼────────────────────────┼────────┼─────┤ │ │",
"│ │ │ │ │ 0 │ completion_menu │ false │ | │ ... │ │ │",
"│ │ │ │ │ 1 │ history_menu │ true │ ? │ ... │ │ │",
"│ │ │ │ │ 2 │ help_menu │ true │ ? │ ... │ │ │",
"│ │ │ │ │ 3 │ commands_menu │ false │ # │ ... │ │ │",
"│ │ │ │ │ 4 │ vars_menu │ true │ # │ ... │ │ │",
"│ │ │ │ │ 5 │ commands_with_description │ true │ # │ ... │ │ │",
"│ │ │ │ ╰───┴───────────────────────────┴────────────────────────┴────────┴─────╯ │ │",
"│ │ │ │ ╭────┬───────────────────────────┬──────────┬─────────┬────────────────┬────╮ │ │",
"│ │ │ keybindings │ │ # │ name │ modifier │ keycode │ mode │ ev │ │ │",
"│ │ │ │ │ │ │ │ │ │ en │ │ │",
"│ │ │ │ │ │ │ │ │ │ t │ │ │",
"│ │ │ │ ├────┼───────────────────────────┼──────────┼─────────┼────────────────┼────┤ │ │",
"│ │ │ │ │ 0 │ completion_menu │ none │ tab │ ╭───┬────────╮ │ {r │ │ │",
"│ │ │ │ │ │ │ │ │ │ 0 │ emacs │ │ ec │ │ │",
"│ │ │ │ │ │ │ │ │ │ 1 │ vi_nor │ │ or │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ mal │ │ d │ │ │",
"│ │ │ │ │ │ │ │ │ │ 2 │ vi_ins │ │ 1 │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ ert │ │ fi │ │ │",
"│ │ │ │ │ │ │ │ │ ╰───┴────────╯ │ el │ │ │",
"│ │ │ │ │ │ │ │ │ │ d} │ │ │",
"│ │ │ │ │ 1 │ completion_previous │ shift │ backtab │ ╭───┬────────╮ │ {r │ │ │",
"│ │ │ │ │ │ │ │ │ │ 0 │ emacs │ │ ec │ │ │",
"│ │ │ │ │ │ │ │ │ │ 1 │ vi_nor │ │ or │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ mal │ │ d │ │ │",
"│ │ │ │ │ │ │ │ │ │ 2 │ vi_ins │ │ 1 │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ ert │ │ fi │ │ │",
"│ │ │ │ │ │ │ │ │ ╰───┴────────╯ │ el │ │ │",
"│ │ │ │ │ │ │ │ │ │ d} │ │ │",
"│ │ │ │ │ 2 │ history_menu │ control │ char_r │ emacs │ {r │ │ │",
"│ │ │ │ │ │ │ │ │ │ ec │ │ │",
"│ │ │ │ │ │ │ │ │ │ or │ │ │",
"│ │ │ │ │ │ │ │ │ │ d │ │ │",
"│ │ │ │ │ │ │ │ │ │ 2 │ │ │",
"│ │ │ │ │ │ │ │ │ │ fi │ │ │",
"│ │ │ │ │ │ │ │ │ │ el │ │ │",
"│ │ │ │ │ │ │ │ │ │ ds │ │ │",
"│ │ │ │ │ │ │ │ │ │ } │ │ │",
"│ │ │ │ │ 3 │ next_page │ control │ char_x │ emacs │ {r │ │ │",
"│ │ │ │ │ │ │ │ │ │ ec │ │ │",
"│ │ │ │ │ │ │ │ │ │ or │ │ │",
"│ │ │ │ │ │ │ │ │ │ d │ │ │",
"│ │ │ │ │ │ │ │ │ │ 1 │ │ │",
"│ │ │ │ │ │ │ │ │ │ fi │ │ │",
"│ │ │ │ │ │ │ │ │ │ el │ │ │",
"│ │ │ │ │ │ │ │ │ │ d} │ │ │",
"│ │ │ │ │ 4 │ undo_or_previous_page │ control │ char_z │ emacs │ {r │ │ │",
"│ │ │ │ │ │ │ │ │ │ ec │ │ │",
"│ │ │ │ │ │ │ │ │ │ or │ │ │",
"│ │ │ │ │ │ │ │ │ │ d │ │ │",
"│ │ │ │ │ │ │ │ │ │ 1 │ │ │",
"│ │ │ │ │ │ │ │ │ │ fi │ │ │",
"│ │ │ │ │ │ │ │ │ │ el │ │ │",
"│ │ │ │ │ │ │ │ │ │ d} │ │ │",
"│ │ │ │ │ 5 │ yank │ control │ char_y │ emacs │ {r │ │ │",
"│ │ │ │ │ │ │ │ │ │ ec │ │ │",
"│ │ │ │ │ │ │ │ │ │ or │ │ │",
"│ │ │ │ │ │ │ │ │ │ d │ │ │",
"│ │ │ │ │ │ │ │ │ │ 1 │ │ │",
"│ │ │ │ │ │ │ │ │ │ fi │ │ │",
"│ │ │ │ │ │ │ │ │ │ el │ │ │",
"│ │ │ │ │ │ │ │ │ │ d} │ │ │",
"│ │ │ │ │ 6 │ unix-line-discard │ control │ char_u │ ╭───┬────────╮ │ {r │ │ │",
"│ │ │ │ │ │ │ │ │ │ 0 │ emacs │ │ ec │ │ │",
"│ │ │ │ │ │ │ │ │ │ 1 │ vi_nor │ │ or │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ mal │ │ d │ │ │",
"│ │ │ │ │ │ │ │ │ │ 2 │ vi_ins │ │ 1 │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ ert │ │ fi │ │ │",
"│ │ │ │ │ │ │ │ │ ╰───┴────────╯ │ el │ │ │",
"│ │ │ │ │ │ │ │ │ │ d} │ │ │",
"│ │ │ │ │ 7 │ kill-line │ control │ char_k │ ╭───┬────────╮ │ {r │ │ │",
"│ │ │ │ │ │ │ │ │ │ 0 │ emacs │ │ ec │ │ │",
"│ │ │ │ │ │ │ │ │ │ 1 │ vi_nor │ │ or │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ mal │ │ d │ │ │",
"│ │ │ │ │ │ │ │ │ │ 2 │ vi_ins │ │ 1 │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ ert │ │ fi │ │ │",
"│ │ │ │ │ │ │ │ │ ╰───┴────────╯ │ el │ │ │",
"│ │ │ │ │ │ │ │ │ │ d} │ │ │",
"│ │ │ │ │ 8 │ commands_menu │ control │ char_t │ ╭───┬────────╮ │ {r │ │ │",
"│ │ │ │ │ │ │ │ │ │ 0 │ emacs │ │ ec │ │ │",
"│ │ │ │ │ │ │ │ │ │ 1 │ vi_nor │ │ or │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ mal │ │ d │ │ │",
"│ │ │ │ │ │ │ │ │ │ 2 │ vi_ins │ │ 2 │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ ert │ │ fi │ │ │",
"│ │ │ │ │ │ │ │ │ ╰───┴────────╯ │ el │ │ │",
"│ │ │ │ │ │ │ │ │ │ ds │ │ │",
"│ │ │ │ │ │ │ │ │ │ } │ │ │",
"│ │ │ │ │ 9 │ vars_menu │ alt │ char_o │ ╭───┬────────╮ │ {r │ │ │",
"│ │ │ │ │ │ │ │ │ │ 0 │ emacs │ │ ec │ │ │",
"│ │ │ │ │ │ │ │ │ │ 1 │ vi_nor │ │ or │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ mal │ │ d │ │ │",
"│ │ │ │ │ │ │ │ │ │ 2 │ vi_ins │ │ 2 │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ ert │ │ fi │ │ │",
"│ │ │ │ │ │ │ │ │ ╰───┴────────╯ │ el │ │ │",
"│ │ │ │ │ │ │ │ │ │ ds │ │ │",
"│ │ │ │ │ │ │ │ │ │ } │ │ │",
"│ │ │ │ │ 10 │ commands_with_description │ control │ char_s │ ╭───┬────────╮ │ {r │ │ │",
"│ │ │ │ │ │ │ │ │ │ 0 │ emacs │ │ ec │ │ │",
"│ │ │ │ │ │ │ │ │ │ 1 │ vi_nor │ │ or │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ mal │ │ d │ │ │",
"│ │ │ │ │ │ │ │ │ │ 2 │ vi_ins │ │ 2 │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ ert │ │ fi │ │ │",
"│ │ │ │ │ │ │ │ │ ╰───┴────────╯ │ el │ │ │",
"│ │ │ │ │ │ │ │ │ │ ds │ │ │",
"│ │ │ │ │ │ │ │ │ │ } │ │ │",
"│ │ │ │ ╰────┴───────────────────────────┴──────────┴─────────┴────────────────┴────╯ │ │",
"│ │ ╰──────────────────────────────────┴───────────────────────────────────────────────────────────────────────────────╯ │",
"╰────────────────────┴──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯",
])
@ -1929,77 +1852,220 @@ fn table_expande_with_no_header_internally_1() {
"│ │ │ │ │ 4 │ vars_menu │ true │ # │ ... │ │ │",
"│ │ │ │ │ 5 │ commands_with_description │ true │ # │ ... │ │ │",
"│ │ │ │ ╰───┴───────────────────────────┴────────────────────────┴───────┴─────╯ │ │",
"│ │ │ │ ╭────┬───────────────────────────┬──────────┬─────────┬──────────┬─────╮ │ │",
"│ │ │ keybindings │ │ # │ name │ modifier │ keycode │ mode │ eve │ │ │",
"│ │ │ │ │ │ │ │ │ │ nt │ │ │",
"│ │ │ │ ├────┼───────────────────────────┼──────────┼─────────┼──────────┼─────┤ │ │",
"│ │ │ │ │ 0 │ completion_menu │ none │ tab │ [list 3 │ {re │ │ │",
"│ │ │ │ │ │ │ │ │ items] │ cor │ │ │",
"│ │ │ │ │ │ │ │ │ │ d 1 │ │ │",
"│ │ │ │ │ │ │ │ │ │ fi │ │ │",
"│ │ │ │ │ │ │ │ │ │ eld │ │ │",
"│ │ │ │ │ │ │ │ │ │ } │ │ │",
"│ │ │ │ │ 1 │ completion_previous │ shift │ backtab │ [list 3 │ {re │ │ │",
"│ │ │ │ │ │ │ │ │ items] │ cor │ │ │",
"│ │ │ │ │ │ │ │ │ │ d 1 │ │ │",
"│ │ │ │ │ │ │ │ │ │ fi │ │ │",
"│ │ │ │ │ │ │ │ │ │ eld │ │ │",
"│ │ │ │ │ │ │ │ │ │ } │ │ │",
"│ │ │ │ │ 2 │ history_menu │ control │ char_r │ emacs │ {re │ │ │",
"│ │ │ │ │ │ │ │ │ │ cor │ │ │",
"│ │ │ │ │ │ │ │ │ │ d 2 │ │ │",
"│ │ │ │ │ │ │ │ │ │ fi │ │ │",
"│ │ │ │ │ │ │ │ │ │ eld │ │ │",
"│ │ │ │ │ │ │ │ │ │ s} │ │ │",
"│ │ │ │ │ 3 │ next_page │ control │ char_x │ emacs │ {re │ │ │",
"│ │ │ │ │ │ │ │ │ │ cor │ │ │",
"│ │ │ │ │ │ │ │ │ │ d 1 │ │ │",
"│ │ │ │ │ │ │ │ │ │ fi │ │ │",
"│ │ │ │ │ │ │ │ │ │ eld │ │ │",
"│ │ │ │ │ │ │ │ │ │ } │ │ │",
"│ │ │ │ │ 4 │ undo_or_previous_page │ control │ char_z │ emacs │ {re │ │ │",
"│ │ │ │ │ │ │ │ │ │ cor │ │ │",
"│ │ │ │ │ │ │ │ │ │ d 1 │ │ │",
"│ │ │ │ │ │ │ │ │ │ fi │ │ │",
"│ │ │ │ │ │ │ │ │ │ eld │ │ │",
"│ │ │ │ │ │ │ │ │ │ } │ │ │",
"│ │ │ │ │ 5 │ yank │ control │ char_y │ emacs │ {re │ │ │",
"│ │ │ │ │ │ │ │ │ │ cor │ │ │",
"│ │ │ │ │ │ │ │ │ │ d 1 │ │ │",
"│ │ │ │ │ │ │ │ │ │ fi │ │ │",
"│ │ │ │ │ │ │ │ │ │ eld │ │ │",
"│ │ │ │ │ │ │ │ │ │ } │ │ │",
"│ │ │ │ │ 6 │ unix-line-discard │ control │ char_u │ [list 3 │ {re │ │ │",
"│ │ │ │ │ │ │ │ │ items] │ cor │ │ │",
"│ │ │ │ │ │ │ │ │ │ d 1 │ │ │",
"│ │ │ │ │ │ │ │ │ │ fi │ │ │",
"│ │ │ │ │ │ │ │ │ │ eld │ │ │",
"│ │ │ │ │ │ │ │ │ │ } │ │ │",
"│ │ │ │ │ 7 │ kill-line │ control │ char_k │ [list 3 │ {re │ │ │",
"│ │ │ │ │ │ │ │ │ items] │ cor │ │ │",
"│ │ │ │ │ │ │ │ │ │ d 1 │ │ │",
"│ │ │ │ │ │ │ │ │ │ fi │ │ │",
"│ │ │ │ │ │ │ │ │ │ eld │ │ │",
"│ │ │ │ │ │ │ │ │ │ } │ │ │",
"│ │ │ │ │ 8 │ commands_menu │ control │ char_t │ [list 3 │ {re │ │ │",
"│ │ │ │ │ │ │ │ │ items] │ cor │ │ │",
"│ │ │ │ │ │ │ │ │ │ d 2 │ │ │",
"│ │ │ │ │ │ │ │ │ │ fi │ │ │",
"│ │ │ │ │ │ │ │ │ │ eld │ │ │",
"│ │ │ │ │ │ │ │ │ │ s} │ │ │",
"│ │ │ │ │ 9 │ vars_menu │ alt │ char_o │ [list 3 │ {re │ │ │",
"│ │ │ │ │ │ │ │ │ items] │ cor │ │ │",
"│ │ │ │ │ │ │ │ │ │ d 2 │ │ │",
"│ │ │ │ │ │ │ │ │ │ fi │ │ │",
"│ │ │ │ │ │ │ │ │ │ eld │ │ │",
"│ │ │ │ │ │ │ │ │ │ s} │ │ │",
"│ │ │ │ │ 10 │ commands_with_description │ control │ char_s │ [list 3 │ {re │ │ │",
"│ │ │ │ │ │ │ │ │ items] │ cor │ │ │",
"│ │ │ │ │ │ │ │ │ │ d 2 │ │ │",
"│ │ │ │ │ │ │ │ │ │ fi │ │ │",
"│ │ │ │ │ │ │ │ │ │ eld │ │ │",
"│ │ │ │ │ │ │ │ │ │ s} │ │ │",
"│ │ │ │ ╰────┴───────────────────────────┴──────────┴─────────┴──────────┴─────╯ │ │",
"│ │ │ │ ╭────┬───────────────────────────┬──────────┬─────────┬───────────┬────╮ │ │",
"│ │ │ keybindings │ │ # │ name │ modifier │ keycode │ mode │ ev │ │ │",
"│ │ │ │ │ │ │ │ │ │ en │ │ │",
"│ │ │ │ │ │ │ │ │ │ t │ │ │",
"│ │ │ │ ├────┼───────────────────────────┼──────────┼─────────┼───────────┼────┤ │ │",
"│ │ │ │ │ 0 │ completion_menu │ none │ tab │ ╭───┬───╮ │ {r │ │ │",
"│ │ │ │ │ │ │ │ │ │ 0 │ e │ │ ec │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ m │ │ or │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ a │ │ d │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ c │ │ 1 │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ s │ │ fi │ │ │",
"│ │ │ │ │ │ │ │ │ │ 1 │ v │ │ el │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ i │ │ d} │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ _ │ │ │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ n │ │ │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ o │ │ │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ r │ │ │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ m │ │ │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ a │ │ │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ l │ │ │ │ │",
"│ │ │ │ │ │ │ │ │ │ 2 │ v │ │ │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ i │ │ │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ _ │ │ │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ i │ │ │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ n │ │ │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ s │ │ │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ e │ │ │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ r │ │ │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ t │ │ │ │ │",
"│ │ │ │ │ │ │ │ │ ╰───┴───╯ │ │ │ │",
"│ │ │ │ │ 1 │ completion_previous │ shift │ backtab │ ╭───┬───╮ │ {r │ │ │",
"│ │ │ │ │ │ │ │ │ │ 0 │ e │ │ ec │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ m │ │ or │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ a │ │ d │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ c │ │ 1 │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ s │ │ fi │ │ │",
"│ │ │ │ │ │ │ │ │ │ 1 │ v │ │ el │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ i │ │ d} │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ _ │ │ │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ n │ │ │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ o │ │ │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ r │ │ │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ m │ │ │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ a │ │ │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ l │ │ │ │ │",
"│ │ │ │ │ │ │ │ │ │ 2 │ v │ │ │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ i │ │ │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ _ │ │ │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ i │ │ │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ n │ │ │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ s │ │ │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ e │ │ │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ r │ │ │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ t │ │ │ │ │",
"│ │ │ │ │ │ │ │ │ ╰───┴───╯ │ │ │ │",
"│ │ │ │ │ 2 │ history_menu │ control │ char_r │ emacs │ {r │ │ │",
"│ │ │ │ │ │ │ │ │ │ ec │ │ │",
"│ │ │ │ │ │ │ │ │ │ or │ │ │",
"│ │ │ │ │ │ │ │ │ │ d │ │ │",
"│ │ │ │ │ │ │ │ │ │ 2 │ │ │",
"│ │ │ │ │ │ │ │ │ │ fi │ │ │",
"│ │ │ │ │ │ │ │ │ │ el │ │ │",
"│ │ │ │ │ │ │ │ │ │ ds │ │ │",
"│ │ │ │ │ │ │ │ │ │ } │ │ │",
"│ │ │ │ │ 3 │ next_page │ control │ char_x │ emacs │ {r │ │ │",
"│ │ │ │ │ │ │ │ │ │ ec │ │ │",
"│ │ │ │ │ │ │ │ │ │ or │ │ │",
"│ │ │ │ │ │ │ │ │ │ d │ │ │",
"│ │ │ │ │ │ │ │ │ │ 1 │ │ │",
"│ │ │ │ │ │ │ │ │ │ fi │ │ │",
"│ │ │ │ │ │ │ │ │ │ el │ │ │",
"│ │ │ │ │ │ │ │ │ │ d} │ │ │",
"│ │ │ │ │ 4 │ undo_or_previous_page │ control │ char_z │ emacs │ {r │ │ │",
"│ │ │ │ │ │ │ │ │ │ ec │ │ │",
"│ │ │ │ │ │ │ │ │ │ or │ │ │",
"│ │ │ │ │ │ │ │ │ │ d │ │ │",
"│ │ │ │ │ │ │ │ │ │ 1 │ │ │",
"│ │ │ │ │ │ │ │ │ │ fi │ │ │",
"│ │ │ │ │ │ │ │ │ │ el │ │ │",
"│ │ │ │ │ │ │ │ │ │ d} │ │ │",
"│ │ │ │ │ 5 │ yank │ control │ char_y │ emacs │ {r │ │ │",
"│ │ │ │ │ │ │ │ │ │ ec │ │ │",
"│ │ │ │ │ │ │ │ │ │ or │ │ │",
"│ │ │ │ │ │ │ │ │ │ d │ │ │",
"│ │ │ │ │ │ │ │ │ │ 1 │ │ │",
"│ │ │ │ │ │ │ │ │ │ fi │ │ │",
"│ │ │ │ │ │ │ │ │ │ el │ │ │",
"│ │ │ │ │ │ │ │ │ │ d} │ │ │",
"│ │ │ │ │ 6 │ unix-line-discard │ control │ char_u │ ╭───┬───╮ │ {r │ │ │",
"│ │ │ │ │ │ │ │ │ │ 0 │ e │ │ ec │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ m │ │ or │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ a │ │ d │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ c │ │ 1 │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ s │ │ fi │ │ │",
"│ │ │ │ │ │ │ │ │ │ 1 │ v │ │ el │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ i │ │ d} │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ _ │ │ │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ n │ │ │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ o │ │ │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ r │ │ │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ m │ │ │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ a │ │ │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ l │ │ │ │ │",
"│ │ │ │ │ │ │ │ │ │ 2 │ v │ │ │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ i │ │ │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ _ │ │ │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ i │ │ │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ n │ │ │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ s │ │ │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ e │ │ │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ r │ │ │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ t │ │ │ │ │",
"│ │ │ │ │ │ │ │ │ ╰───┴───╯ │ │ │ │",
"│ │ │ │ │ 7 │ kill-line │ control │ char_k │ ╭───┬───╮ │ {r │ │ │",
"│ │ │ │ │ │ │ │ │ │ 0 │ e │ │ ec │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ m │ │ or │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ a │ │ d │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ c │ │ 1 │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ s │ │ fi │ │ │",
"│ │ │ │ │ │ │ │ │ │ 1 │ v │ │ el │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ i │ │ d} │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ _ │ │ │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ n │ │ │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ o │ │ │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ r │ │ │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ m │ │ │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ a │ │ │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ l │ │ │ │ │",
"│ │ │ │ │ │ │ │ │ │ 2 │ v │ │ │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ i │ │ │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ _ │ │ │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ i │ │ │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ n │ │ │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ s │ │ │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ e │ │ │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ r │ │ │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ t │ │ │ │ │",
"│ │ │ │ │ │ │ │ │ ╰───┴───╯ │ │ │ │",
"│ │ │ │ │ 8 │ commands_menu │ control │ char_t │ ╭───┬───╮ │ {r │ │ │",
"│ │ │ │ │ │ │ │ │ │ 0 │ e │ │ ec │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ m │ │ or │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ a │ │ d │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ c │ │ 2 │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ s │ │ fi │ │ │",
"│ │ │ │ │ │ │ │ │ │ 1 │ v │ │ el │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ i │ │ ds │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ _ │ │ } │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ n │ │ │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ o │ │ │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ r │ │ │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ m │ │ │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ a │ │ │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ l │ │ │ │ │",
"│ │ │ │ │ │ │ │ │ │ 2 │ v │ │ │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ i │ │ │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ _ │ │ │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ i │ │ │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ n │ │ │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ s │ │ │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ e │ │ │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ r │ │ │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ t │ │ │ │ │",
"│ │ │ │ │ │ │ │ │ ╰───┴───╯ │ │ │ │",
"│ │ │ │ │ 9 │ vars_menu │ alt │ char_o │ ╭───┬───╮ │ {r │ │ │",
"│ │ │ │ │ │ │ │ │ │ 0 │ e │ │ ec │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ m │ │ or │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ a │ │ d │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ c │ │ 2 │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ s │ │ fi │ │ │",
"│ │ │ │ │ │ │ │ │ │ 1 │ v │ │ el │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ i │ │ ds │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ _ │ │ } │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ n │ │ │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ o │ │ │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ r │ │ │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ m │ │ │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ a │ │ │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ l │ │ │ │ │",
"│ │ │ │ │ │ │ │ │ │ 2 │ v │ │ │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ i │ │ │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ _ │ │ │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ i │ │ │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ n │ │ │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ s │ │ │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ e │ │ │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ r │ │ │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ t │ │ │ │ │",
"│ │ │ │ │ │ │ │ │ ╰───┴───╯ │ │ │ │",
"│ │ │ │ │ 10 │ commands_with_description │ control │ char_s │ ╭───┬───╮ │ {r │ │ │",
"│ │ │ │ │ │ │ │ │ │ 0 │ e │ │ ec │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ m │ │ or │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ a │ │ d │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ c │ │ 2 │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ s │ │ fi │ │ │",
"│ │ │ │ │ │ │ │ │ │ 1 │ v │ │ el │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ i │ │ ds │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ _ │ │ } │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ n │ │ │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ o │ │ │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ r │ │ │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ m │ │ │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ a │ │ │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ l │ │ │ │ │",
"│ │ │ │ │ │ │ │ │ │ 2 │ v │ │ │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ i │ │ │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ _ │ │ │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ i │ │ │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ n │ │ │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ s │ │ │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ e │ │ │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ r │ │ │ │ │",
"│ │ │ │ │ │ │ │ │ │ │ t │ │ │ │ │",
"│ │ │ │ │ │ │ │ │ ╰───┴───╯ │ │ │ │",
"│ │ │ │ ╰────┴───────────────────────────┴──────────┴─────────┴───────────┴────╯ │ │",
"│ │ ╰──────────────────────────────────┴──────────────────────────────────────────────────────────────────────────╯ │",
"╰────────────────────┴─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯",
])

View File

@ -269,6 +269,7 @@ pub fn eval_expression_with_input<D: DebugContext>(
input = eval_subexpression::<D>(engine_state, stack, block, input)?
.into_value(*span)?
.follow_cell_path(&full_cell_path.tail, false)?
.into_owned()
.into_pipeline_data()
} else {
input = eval_subexpression::<D>(engine_state, stack, block, input)?;
@ -604,7 +605,7 @@ impl Eval for EvalRuntime {
let is_config = original_key == "config";
stack.add_env_var(original_key, value);
stack.add_env_var(original_key, value.into_owned());
// Trigger the update to config, if we modified that.
if is_config {

View File

@ -694,9 +694,8 @@ fn eval_instruction<D: DebugContext>(
let value = ctx.clone_reg_value(*src, *span)?;
let path = ctx.take_reg(*path);
if let PipelineData::Value(Value::CellPath { val: path, .. }, _) = path {
// TODO: make follow_cell_path() not have to take ownership, probably using Cow
let value = value.follow_cell_path(&path.members, true)?;
ctx.put_reg(*dst, value.into_pipeline_data());
ctx.put_reg(*dst, value.into_owned().into_pipeline_data());
Ok(Continue)
} else if let PipelineData::Value(Value::Error { error, .. }, _) = path {
Err(*error)

View File

@ -63,7 +63,7 @@ impl LanguageServer {
let var = working_set.get_variable(*var_id);
Some(
var.const_val
.clone()
.as_ref()
.and_then(|val| val.follow_cell_path(cell_path, false).ok())
.map(|val| val.span())
.unwrap_or(var.declaration_span),

View File

@ -160,16 +160,15 @@ impl LanguageServer {
let var = working_set.get_variable(var_id);
markdown_hover(
var.const_val
.clone()
.as_ref()
.and_then(|val| val.follow_cell_path(&cell_path, false).ok())
.map(|val| {
let ty = val.get_type().clone();
let value_string = val
.coerce_into_string()
.ok()
.map(|s| format!("\n---\n{}", s))
.unwrap_or_default();
format!("```\n{}\n```{}", ty, value_string)
let ty = val.get_type();
if let Ok(s) = val.coerce_str() {
format!("```\n{}\n```\n---\n{}", ty, s)
} else {
format!("```\n{}\n```", ty)
}
})
.unwrap_or("`unknown`".into()),
)

View File

@ -20,6 +20,7 @@ pub enum TableMode {
Restructured,
AsciiRounded,
BasicCompact,
Single,
}
impl FromStr for TableMode {
@ -44,7 +45,8 @@ impl FromStr for TableMode {
"restructured" => Ok(Self::Restructured),
"ascii_rounded" => Ok(Self::AsciiRounded),
"basic_compact" => Ok(Self::BasicCompact),
_ => Err("'basic', 'thin', 'light', 'compact', 'with_love', 'compact_double', 'rounded', 'reinforced', 'heavy', 'none', 'psql', 'markdown', 'dots', 'restructured', 'ascii_rounded', or 'basic_compact'"),
"single" => Ok(Self::Single),
_ => Err("'basic', 'thin', 'light', 'compact', 'with_love', 'compact_double', 'rounded', 'reinforced', 'heavy', 'none', 'psql', 'markdown', 'dots', 'restructured', 'ascii_rounded', 'basic_compact' or 'single'"),
}
}
}

View File

@ -7,7 +7,7 @@ use crate::{
debugger::DebugContext,
BlockId, Config, GetSpan, Range, Record, ShellError, Span, Value, VarId, ENV_VARIABLE_ID,
};
use std::{collections::HashMap, sync::Arc};
use std::{borrow::Cow, collections::HashMap, sync::Arc};
/// To share implementations for regular eval and const eval
pub trait Eval {
@ -43,11 +43,8 @@ pub trait Eval {
// Cell paths are usually case-sensitive, but we give $env
// special treatment.
if cell_path.head.expr == Expr::Var(ENV_VARIABLE_ID) {
value.follow_cell_path(&cell_path.tail, true)
} else {
value.follow_cell_path(&cell_path.tail, false)
}
let insensitive = cell_path.head.expr == Expr::Var(ENV_VARIABLE_ID);
value.follow_cell_path(&cell_path.tail, insensitive).map(Cow::into_owned)
}
Expr::DateTime(dt) => Ok(Value::date(*dt, expr_span)),
Expr::List(list) => {

View File

@ -128,7 +128,6 @@ impl Module {
} else {
// Import pattern was just name without any members
let mut decls = vec![];
let mut const_vids = vec![];
let mut const_rows = vec![];
let mut errors = vec![];
@ -154,7 +153,6 @@ impl Module {
decls.push((new_name, sub_decl_id));
}
const_vids.extend(sub_results.constants);
const_rows.extend(sub_results.constant_values);
}
@ -162,10 +160,7 @@ impl Module {
for (name, var_id) in self.consts() {
match working_set.get_constant(var_id) {
Ok(const_val) => {
const_vids.push((name.clone(), var_id));
const_rows.push((name, const_val.clone()))
}
Ok(const_val) => const_rows.push((name, const_val.clone())),
Err(err) => errors.push(err),
}
}
@ -192,7 +187,7 @@ impl Module {
ResolvedImportPattern::new(
decls,
vec![(final_name.clone(), self_id)],
const_vids,
vec![],
constant_values,
),
errors,

View File

@ -6,7 +6,7 @@ use crate::{
ByteStream, ByteStreamType, Config, ListStream, OutDest, PipelineMetadata, Range, ShellError,
Signals, Span, Type, Value,
};
use std::io::Write;
use std::{borrow::Cow, io::Write};
const LINE_ENDING_PATTERN: &[char] = &['\r', '\n'];
@ -416,8 +416,11 @@ impl PipelineData {
match self {
// FIXME: there are probably better ways of doing this
PipelineData::ListStream(stream, ..) => Value::list(stream.into_iter().collect(), head)
.follow_cell_path(cell_path, insensitive),
PipelineData::Value(v, ..) => v.follow_cell_path(cell_path, insensitive),
.follow_cell_path(cell_path, insensitive)
.map(Cow::into_owned),
PipelineData::Value(v, ..) => v
.follow_cell_path(cell_path, insensitive)
.map(Cow::into_owned),
PipelineData::Empty => Err(ShellError::IncompatiblePathAccess {
type_name: "empty pipeline".to_string(),
span: head,

View File

@ -38,7 +38,7 @@ use std::{
borrow::Cow,
cmp::Ordering,
fmt::{Debug, Display, Write},
ops::Bound,
ops::{Bound, ControlFlow, Deref},
path::PathBuf,
};
@ -1080,224 +1080,83 @@ impl Value {
}
/// Follow a given cell path into the value: for example accessing select elements in a stream or list
pub fn follow_cell_path(
self,
pub fn follow_cell_path<'out>(
&'out self,
cell_path: &[PathMember],
insensitive: bool,
) -> Result<Value, ShellError> {
let mut current = self;
) -> Result<Cow<'out, Value>, ShellError> {
enum MultiLife<'out, 'local, T>
where
'out: 'local,
T: ?Sized,
{
Out(&'out T),
Local(&'local T),
}
for member in cell_path {
match member {
PathMember::Int {
val: count,
span: origin_span,
optional,
} => {
// Treat a numeric path member as `select <val>`
match current {
Value::List { mut vals, .. } => {
if *count < vals.len() {
// `vals` is owned and will be dropped right after this,
// so we can `swap_remove` the value at index `count`
// without worrying about preserving order.
current = vals.swap_remove(*count);
} else if *optional {
return Ok(Value::nothing(*origin_span)); // short-circuit
} else if vals.is_empty() {
return Err(ShellError::AccessEmptyContent { span: *origin_span });
} else {
return Err(ShellError::AccessBeyondEnd {
max_idx: vals.len() - 1,
span: *origin_span,
});
}
}
Value::Binary { val, .. } => {
if let Some(item) = val.get(*count) {
current = Value::int(*item as i64, *origin_span);
} else if *optional {
return Ok(Value::nothing(*origin_span)); // short-circuit
} else if val.is_empty() {
return Err(ShellError::AccessEmptyContent { span: *origin_span });
} else {
return Err(ShellError::AccessBeyondEnd {
max_idx: val.len() - 1,
span: *origin_span,
});
}
}
Value::Range { ref val, .. } => {
if let Some(item) = val
.into_range_iter(current.span(), Signals::empty())
.nth(*count)
{
current = item;
} else if *optional {
return Ok(Value::nothing(*origin_span)); // short-circuit
} else {
return Err(ShellError::AccessBeyondEndOfStream {
span: *origin_span,
});
}
}
Value::Custom { ref val, .. } => {
current =
match val.follow_path_int(current.span(), *count, *origin_span) {
Ok(val) => val,
Err(err) => {
if *optional {
return Ok(Value::nothing(*origin_span));
// short-circuit
} else {
return Err(err);
}
}
};
}
Value::Nothing { .. } if *optional => {
return Ok(Value::nothing(*origin_span)); // short-circuit
}
// Records (and tables) are the only built-in which support column names,
// so only use this message for them.
Value::Record { .. } => {
return Err(ShellError::TypeMismatch {
err_message:"Can't access record values with a row index. Try specifying a column name instead".into(),
span: *origin_span,
});
}
Value::Error { error, .. } => return Err(*error),
x => {
return Err(ShellError::IncompatiblePathAccess {
type_name: format!("{}", x.get_type()),
span: *origin_span,
});
}
}
}
PathMember::String {
val: column_name,
span: origin_span,
optional,
} => {
let span = current.span();
impl<'out, 'local, T> Deref for MultiLife<'out, 'local, T>
where
'out: 'local,
T: ?Sized,
{
type Target = T;
match current {
Value::Record { mut val, .. } => {
// Make reverse iterate to avoid duplicate column leads to first value, actually last value is expected.
if let Some(found) = val.to_mut().iter_mut().rev().find(|x| {
if insensitive {
x.0.eq_ignore_case(column_name)
} else {
x.0 == column_name
}
}) {
current = std::mem::take(found.1);
} else if *optional {
return Ok(Value::nothing(*origin_span)); // short-circuit
} else if let Some(suggestion) =
did_you_mean(val.columns(), column_name)
{
return Err(ShellError::DidYouMean {
suggestion,
span: *origin_span,
});
} else {
return Err(ShellError::CantFindColumn {
col_name: column_name.clone(),
span: Some(*origin_span),
src_span: span,
});
}
}
// String access of Lists always means Table access.
// Create a List which contains each matching value for contained
// records in the source list.
Value::List { vals, .. } => {
let list = vals
.into_iter()
.map(|val| {
let val_span = val.span();
match val {
Value::Record { mut val, .. } => {
if let Some(found) =
val.to_mut().iter_mut().rev().find(|x| {
if insensitive {
x.0.eq_ignore_case(column_name)
} else {
x.0 == column_name
}
})
{
Ok(std::mem::take(found.1))
} else if *optional {
Ok(Value::nothing(*origin_span))
} else if let Some(suggestion) =
did_you_mean(val.columns(), column_name)
{
Err(ShellError::DidYouMean {
suggestion,
span: *origin_span,
})
} else {
Err(ShellError::CantFindColumn {
col_name: column_name.clone(),
span: Some(*origin_span),
src_span: val_span,
})
}
}
Value::Nothing { .. } if *optional => {
Ok(Value::nothing(*origin_span))
}
_ => Err(ShellError::CantFindColumn {
col_name: column_name.clone(),
span: Some(*origin_span),
src_span: val_span,
}),
}
})
.collect::<Result<_, _>>()?;
current = Value::list(list, span);
}
Value::Custom { ref val, .. } => {
current = match val.follow_path_string(
current.span(),
column_name.clone(),
*origin_span,
) {
Ok(val) => val,
Err(err) => {
if *optional {
return Ok(Value::nothing(*origin_span));
// short-circuit
} else {
return Err(err);
}
}
}
}
Value::Nothing { .. } if *optional => {
return Ok(Value::nothing(*origin_span)); // short-circuit
}
Value::Error { error, .. } => return Err(*error),
x => {
return Err(ShellError::IncompatiblePathAccess {
type_name: format!("{}", x.get_type()),
span: *origin_span,
});
}
}
fn deref(&self) -> &Self::Target {
match *self {
MultiLife::Out(x) => x,
MultiLife::Local(x) => x,
}
}
}
// A dummy value is required, otherwise rust doesn't allow references, which we need for
// the `std::ptr::eq` comparison
let mut store: Value = Value::test_nothing();
let mut current: MultiLife<'out, '_, Value> = MultiLife::Out(self);
for member in cell_path {
current = match current {
MultiLife::Out(current) => match get_value_member(current, member, insensitive)? {
ControlFlow::Break(span) => return Ok(Cow::Owned(Value::nothing(span))),
ControlFlow::Continue(x) => match x {
Cow::Borrowed(x) => MultiLife::Out(x),
Cow::Owned(x) => {
store = x;
MultiLife::Local(&store)
}
},
},
MultiLife::Local(current) => {
match get_value_member(current, member, insensitive)? {
ControlFlow::Break(span) => return Ok(Cow::Owned(Value::nothing(span))),
ControlFlow::Continue(x) => match x {
Cow::Borrowed(x) => MultiLife::Local(x),
Cow::Owned(x) => {
store = x;
MultiLife::Local(&store)
}
},
}
}
};
}
// If a single Value::Error was produced by the above (which won't happen if nullify_errors is true), unwrap it now.
// Note that Value::Errors inside Lists remain as they are, so that the rest of the list can still potentially be used.
if let Value::Error { error, .. } = current {
Err(*error)
if let Value::Error { error, .. } = &*current {
Err(error.as_ref().clone())
} else {
Ok(current)
Ok(match current {
MultiLife::Out(x) => Cow::Borrowed(x),
MultiLife::Local(x) => {
let x = if std::ptr::eq(x, &store) {
store
} else {
x.clone()
};
Cow::Owned(x)
}
})
}
}
@ -1307,9 +1166,7 @@ impl Value {
cell_path: &[PathMember],
callback: Box<dyn FnOnce(&Value) -> Value>,
) -> Result<(), ShellError> {
let orig = self.clone();
let new_val = callback(&orig.follow_cell_path(cell_path, false)?);
let new_val = callback(self.follow_cell_path(cell_path, false)?.as_ref());
match new_val {
Value::Error { error, .. } => Err(*error),
@ -1409,9 +1266,7 @@ impl Value {
cell_path: &[PathMember],
callback: Box<dyn FnOnce(&Value) -> Value + 'a>,
) -> Result<(), ShellError> {
let orig = self.clone();
let new_val = callback(&orig.follow_cell_path(cell_path, false)?);
let new_val = callback(self.follow_cell_path(cell_path, false)?.as_ref());
match new_val {
Value::Error { error, .. } => Err(*error),
@ -2147,6 +2002,198 @@ impl Value {
}
}
fn get_value_member<'a>(
current: &'a Value,
member: &PathMember,
insensitive: bool,
) -> Result<ControlFlow<Span, Cow<'a, Value>>, ShellError> {
match member {
PathMember::Int {
val: count,
span: origin_span,
optional,
} => {
// Treat a numeric path member as `select <val>`
match current {
Value::List { vals, .. } => {
if *count < vals.len() {
Ok(ControlFlow::Continue(Cow::Borrowed(&vals[*count])))
} else if *optional {
Ok(ControlFlow::Break(*origin_span))
// short-circuit
} else if vals.is_empty() {
Err(ShellError::AccessEmptyContent { span: *origin_span })
} else {
Err(ShellError::AccessBeyondEnd {
max_idx: vals.len() - 1,
span: *origin_span,
})
}
}
Value::Binary { val, .. } => {
if let Some(item) = val.get(*count) {
Ok(ControlFlow::Continue(Cow::Owned(Value::int(
*item as i64,
*origin_span,
))))
} else if *optional {
Ok(ControlFlow::Break(*origin_span))
// short-circuit
} else if val.is_empty() {
Err(ShellError::AccessEmptyContent { span: *origin_span })
} else {
Err(ShellError::AccessBeyondEnd {
max_idx: val.len() - 1,
span: *origin_span,
})
}
}
Value::Range { ref val, .. } => {
if let Some(item) = val
.into_range_iter(current.span(), Signals::empty())
.nth(*count)
{
Ok(ControlFlow::Continue(Cow::Owned(item)))
} else if *optional {
Ok(ControlFlow::Break(*origin_span))
// short-circuit
} else {
Err(ShellError::AccessBeyondEndOfStream {
span: *origin_span,
})
}
}
Value::Custom { ref val, .. } => {
match val.follow_path_int(current.span(), *count, *origin_span)
{
Ok(val) => Ok(ControlFlow::Continue(Cow::Owned(val))),
Err(err) => {
if *optional {
Ok(ControlFlow::Break(*origin_span))
// short-circuit
} else {
Err(err)
}
}
}
}
Value::Nothing { .. } if *optional => Ok(ControlFlow::Break(*origin_span)),
// Records (and tables) are the only built-in which support column names,
// so only use this message for them.
Value::Record { .. } => Err(ShellError::TypeMismatch {
err_message:"Can't access record values with a row index. Try specifying a column name instead".into(),
span: *origin_span,
}),
Value::Error { error, .. } => Err(*error.clone()),
x => Err(ShellError::IncompatiblePathAccess { type_name: format!("{}", x.get_type()), span: *origin_span }),
}
}
PathMember::String {
val: column_name,
span: origin_span,
optional,
} => {
let span = current.span();
match current {
Value::Record { val, .. } => {
if let Some(found) = val.iter().rev().find(|x| {
if insensitive {
x.0.eq_ignore_case(column_name)
} else {
x.0 == column_name
}
}) {
Ok(ControlFlow::Continue(Cow::Borrowed(found.1)))
} else if *optional {
Ok(ControlFlow::Break(*origin_span))
// short-circuit
} else if let Some(suggestion) = did_you_mean(val.columns(), column_name) {
Err(ShellError::DidYouMean {
suggestion,
span: *origin_span,
})
} else {
Err(ShellError::CantFindColumn {
col_name: column_name.clone(),
span: Some(*origin_span),
src_span: span,
})
}
}
// String access of Lists always means Table access.
// Create a List which contains each matching value for contained
// records in the source list.
Value::List { vals, .. } => {
let list = vals
.iter()
.map(|val| {
let val_span = val.span();
match val {
Value::Record { val, .. } => {
if let Some(found) = val.iter().rev().find(|x| {
if insensitive {
x.0.eq_ignore_case(column_name)
} else {
x.0 == column_name
}
}) {
Ok(found.1.clone())
} else if *optional {
Ok(Value::nothing(*origin_span))
} else if let Some(suggestion) =
did_you_mean(val.columns(), column_name)
{
Err(ShellError::DidYouMean {
suggestion,
span: *origin_span,
})
} else {
Err(ShellError::CantFindColumn {
col_name: column_name.clone(),
span: Some(*origin_span),
src_span: val_span,
})
}
}
Value::Nothing { .. } if *optional => {
Ok(Value::nothing(*origin_span))
}
_ => Err(ShellError::CantFindColumn {
col_name: column_name.clone(),
span: Some(*origin_span),
src_span: val_span,
}),
}
})
.collect::<Result<_, _>>()?;
Ok(ControlFlow::Continue(Cow::Owned(Value::list(list, span))))
}
Value::Custom { ref val, .. } => {
match val.follow_path_string(current.span(), column_name.clone(), *origin_span)
{
Ok(val) => Ok(ControlFlow::Continue(Cow::Owned(val))),
Err(err) => {
if *optional {
Ok(ControlFlow::Break(*origin_span))
// short-circuit
} else {
Err(err)
}
}
}
}
Value::Nothing { .. } if *optional => Ok(ControlFlow::Break(*origin_span)),
Value::Error { error, .. } => Err(error.as_ref().clone()),
x => Err(ShellError::IncompatiblePathAccess {
type_name: format!("{}", x.get_type()),
span: *origin_span,
}),
}
}
}
}
impl Default for Value {
fn default() -> Self {
Value::Nothing {

View File

@ -21,10 +21,12 @@ fn main() {
let headers = to_cell_info_vec(&table_headers);
let rows = to_cell_info_vec(&row_data);
let mut rows = vec![rows; 3];
rows.insert(0, headers);
let mut table = NuTable::new(4, 3);
table.set_row(0, headers);
let mut table = NuTable::from(rows);
for i in 0..3 {
table.set_row(i + 1, rows.clone());
}
table.set_data_style(TextStyle::basic_left());
table.set_header_style(TextStyle::basic_center().style(Style::new().on(Color::Blue)));

View File

@ -186,6 +186,7 @@ pub fn load_theme(mode: TableMode) -> TableTheme {
TableMode::Restructured => TableTheme::restructured(),
TableMode::AsciiRounded => TableTheme::ascii_rounded(),
TableMode::BasicCompact => TableTheme::basic_compact(),
TableMode::Single => TableTheme::single(),
}
}

View File

@ -3,7 +3,7 @@
// NOTE: TODO the above we could expose something like [`WidthCtrl`] in which case we could also laverage the width list build right away.
// currently it seems like we do recacalculate it for `table -e`?
use std::{cmp::min, collections::HashMap};
use std::cmp::{max, min};
use nu_ansi_term::Style;
use nu_color_config::TextStyle;
@ -13,13 +13,14 @@ use tabled::{
builder::Builder,
grid::{
ansi::ANSIBuf,
colors::Colors,
config::{
AlignmentHorizontal, ColoredConfig, Entity, Indent, Position, Sides, SpannedConfig,
},
dimension::{CompleteDimensionVecRecords, SpannedGridDimension},
records::{
vec_records::{Text, VecRecords},
ExactRecords, Records,
vec_records::{Cell, Text, VecRecords},
IntoRecords, IterRecords, Records,
},
},
settings::{
@ -43,24 +44,30 @@ pub type NuRecordsValue = Text<String>;
/// NuTable is a table rendering implementation.
#[derive(Debug, Clone)]
pub struct NuTable {
data: NuRecords,
data: Vec<Vec<NuRecordsValue>>,
widths: Vec<usize>,
count_rows: usize,
count_cols: usize,
styles: Styles,
alignments: Alignments,
config: TableConfig,
}
impl NuTable {
/// Creates an empty [`NuTable`] instance.
pub fn new(count_rows: usize, count_columns: usize) -> Self {
pub fn new(count_rows: usize, count_cols: usize) -> Self {
Self {
data: VecRecords::new(vec![vec![Text::default(); count_columns]; count_rows]),
styles: Styles::default(),
alignments: Alignments {
data: AlignmentHorizontal::Left,
index: AlignmentHorizontal::Right,
header: AlignmentHorizontal::Center,
columns: HashMap::default(),
cells: HashMap::default(),
data: vec![vec![Text::default(); count_cols]; count_rows],
widths: vec![2; count_cols],
count_rows,
count_cols,
styles: Styles {
cfg: ColoredConfig::default(),
alignments: CellConfiguration {
data: AlignmentHorizontal::Left,
index: AlignmentHorizontal::Right,
header: AlignmentHorizontal::Center,
},
colors: CellConfiguration::default(),
},
config: TableConfig {
theme: TableTheme::basic(),
@ -76,84 +83,125 @@ impl NuTable {
/// Return amount of rows.
pub fn count_rows(&self) -> usize {
self.data.count_rows()
self.count_rows
}
/// Return amount of columns.
pub fn count_columns(&self) -> usize {
self.data.count_columns()
self.count_cols
}
pub fn insert(&mut self, pos: Position, text: String) {
self.data[pos.0][pos.1] = Text::new(text);
}
pub fn insert_row(&mut self, index: usize, row: Vec<String>) {
let data = &mut self.data[index];
for (col, text) in row.into_iter().enumerate() {
data[col] = Text::new(text);
}
let text = Text::new(text);
self.widths[pos.1] = max(
self.widths[pos.1],
text.width() + indent_sum(self.config.indent),
);
self.data[pos.0][pos.1] = text;
}
pub fn set_row(&mut self, index: usize, row: Vec<NuRecordsValue>) {
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),
);
}
self.data[index] = row;
}
pub fn set_column_style(&mut self, column: usize, style: TextStyle) {
if let Some(style) = style.color_style {
let style = convert_style(style);
self.styles.columns.insert(column, style);
pub fn pop_column(&mut self, count: usize) {
self.count_cols -= count;
self.widths.truncate(self.count_cols);
for row in &mut self.data[..] {
row.truncate(self.count_cols);
}
let alignment = convert_alignment(style.alignment);
if alignment != self.alignments.data {
self.alignments.columns.insert(column, alignment);
// set to default styles of the popped columns
for i in 0..count {
let col = self.count_cols + i;
for row in 0..self.count_rows {
self.styles
.cfg
.set_alignment_horizontal(Entity::Cell(row, col), self.styles.alignments.data);
self.styles
.cfg
.set_color(Entity::Cell(row, col), ANSIBuf::default());
}
}
}
pub fn push_column(&mut self, text: String) {
let value = Text::new(text);
self.widths
.push(value.width() + indent_sum(self.config.indent));
for row in &mut self.data[..] {
row.push(value.clone());
}
self.count_cols += 1;
}
pub fn insert_style(&mut self, pos: Position, style: TextStyle) {
if let Some(style) = style.color_style {
let style = convert_style(style);
self.styles.cells.insert(pos, style);
self.styles.cfg.set_color(pos.into(), style.into());
}
let alignment = convert_alignment(style.alignment);
if alignment != self.alignments.data {
self.alignments.cells.insert(pos, alignment);
if alignment != self.styles.alignments.data {
self.styles
.cfg
.set_alignment_horizontal(pos.into(), alignment);
}
}
pub fn set_header_style(&mut self, style: TextStyle) {
if let Some(style) = style.color_style {
let style = convert_style(style);
self.styles.header = style;
self.styles.colors.header = style;
}
self.alignments.header = convert_alignment(style.alignment);
self.styles.alignments.header = convert_alignment(style.alignment);
}
pub fn set_index_style(&mut self, style: TextStyle) {
if let Some(style) = style.color_style {
let style = convert_style(style);
self.styles.index = style;
self.styles.colors.index = style;
}
self.alignments.index = convert_alignment(style.alignment);
self.styles.alignments.index = convert_alignment(style.alignment);
}
pub fn set_data_style(&mut self, style: TextStyle) {
if let Some(style) = style.color_style {
let style = convert_style(style);
self.styles.data = style;
if !style.is_plain() {
let style = convert_style(style);
self.styles.cfg.set_color(Entity::Global, style.into());
}
}
self.alignments.data = convert_alignment(style.alignment);
let alignment = convert_alignment(style.alignment);
self.styles
.cfg
.set_alignment_horizontal(Entity::Global, alignment);
self.styles.alignments.data = alignment;
}
// NOTE: Crusial to be called before data changes (todo fix interface)
pub fn set_indent(&mut self, indent: TableIndent) {
self.config.indent = indent;
let pad = indent_sum(indent);
for w in &mut self.widths {
*w = pad;
}
}
pub fn set_theme(&mut self, theme: TableTheme) {
@ -180,7 +228,9 @@ impl NuTable {
self.config.border_color = (!color.is_plain()).then_some(color);
}
pub fn get_records_mut(&mut self) -> &mut NuRecords {
// NOTE: BE CAREFUL TO KEEP WIDTH UNCHANGED
// TODO: fix interface
pub fn get_records_mut(&mut self) -> &mut [Vec<NuRecordsValue>] {
&mut self.data
}
@ -194,32 +244,42 @@ impl NuTable {
/// Return a total table width.
pub fn total_width(&self) -> usize {
let config = create_config(&self.config.theme, false, None);
let pad = indent_sum(self.config.indent);
let widths = build_width(&self.data, pad);
get_total_width2(&widths, &config)
get_total_width2(&self.widths, &config)
}
}
impl From<Vec<Vec<Text<String>>>> for NuTable {
fn from(value: Vec<Vec<Text<String>>>) -> Self {
let mut nutable = Self::new(0, 0);
nutable.data = VecRecords::new(value);
let count_rows = value.len();
let count_cols = if value.is_empty() { 0 } else { value[0].len() };
nutable
let mut t = Self::new(count_rows, count_cols);
t.data = value;
table_recalculate_widths(&mut t);
t
}
}
type Alignments = CellConfiguration<AlignmentHorizontal>;
fn table_recalculate_widths(t: &mut NuTable) {
let pad = indent_sum(t.config.indent);
let records = IterRecords::new(&t.data, t.count_cols, Some(t.count_rows));
let widths = build_width(records, pad);
t.widths = widths;
}
type Styles = CellConfiguration<Color>;
#[derive(Debug, Default, Clone)]
#[derive(Debug, Default, Clone, PartialEq, Eq, PartialOrd, Ord, Copy, Hash)]
struct CellConfiguration<Value> {
data: Value,
index: Value,
header: Value,
columns: HashMap<usize, Value>,
cells: HashMap<Position, Value>,
data: Value,
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct Styles {
cfg: ColoredConfig,
colors: CellConfiguration<Color>,
alignments: CellConfiguration<AlignmentHorizontal>,
}
#[derive(Debug, Clone)]
@ -304,49 +364,67 @@ fn table_insert_footer_if(t: &mut NuTable) {
}
fn table_truncate(t: &mut NuTable, termwidth: usize) -> Option<WidthEstimation> {
let widths = maybe_truncate_columns(&mut t.data, &t.config, termwidth);
let widths = maybe_truncate_columns(&mut t.data, t.widths.clone(), &t.config, termwidth);
if widths.needed.is_empty() {
return None;
}
// reset style for last column which is a trail one
if widths.trail {
let col = widths.needed.len() - 1;
for row in 0..t.count_rows {
t.styles
.cfg
.set_alignment_horizontal(Entity::Cell(row, col), t.styles.alignments.data);
t.styles
.cfg
.set_color(Entity::Cell(row, col), ANSIBuf::default());
}
}
Some(widths)
}
fn remove_header(t: &mut NuTable) -> HeadInfo {
let head: Vec<String> = t
// move settings by one row down
for row in 1..t.data.len() {
for col in 0..t.count_cols {
let alignment = *t
.styles
.cfg
.get_alignment_horizontal(Entity::Cell(row, col));
if alignment != t.styles.alignments.data {
t.styles
.cfg
.set_alignment_horizontal(Entity::Cell(row - 1, col), alignment);
}
// TODO: use get_color from upstream (when released)
let color = t.styles.cfg.get_colors().get_color((row, col)).cloned();
if let Some(color) = color {
t.styles.cfg.set_color(Entity::Cell(row - 1, col), color);
}
}
}
let head = t
.data
.remove(0)
.into_iter()
.map(|s| s.to_string())
.collect();
let align = t.alignments.header;
let color = if is_color_empty(&t.styles.header) {
None
} else {
Some(t.styles.header.clone())
};
// move settings by one row down
t.alignments.cells = t
.alignments
.cells
.drain()
.filter(|(k, _)| k.0 != 0)
.map(|(k, v)| ((k.0 - 1, k.1), v))
.collect();
t.alignments.header = AlignmentHorizontal::Center;
// WE NEED TO RELCULATE WIDTH.
// TODO: cause we have configuration beforehand we can just not calculate it in?
table_recalculate_widths(t);
// move settings by one row down
t.styles.cells = t
.styles
.cells
.drain()
.filter(|(k, _)| k.0 != 0)
.map(|(k, v)| ((k.0 - 1, k.1), v))
.collect();
t.styles.header = Color::empty();
let alignment = t.styles.alignments.header;
let color = get_color_if_exists(&t.styles.colors.header);
HeadInfo::new(head, align, color)
t.styles.alignments.header = AlignmentHorizontal::Center;
t.styles.colors.header = Color::empty();
HeadInfo::new(head, alignment, color)
}
fn draw_table(
@ -359,19 +437,24 @@ fn draw_table(
structure.with_footer = structure.with_footer && head.is_none();
let sep_color = t.config.border_color;
let data: Vec<Vec<_>> = t.data.into();
let data = t.data;
let mut table = Builder::from_vec(data).build();
set_styles(&mut table, t.styles, &structure);
set_indent(&mut table, t.config.indent);
load_theme(&mut table, &t.config.theme, &structure, sep_color);
align_table(&mut table, t.alignments, &structure);
colorize_table(&mut table, t.styles, &structure);
truncate_table(&mut table, &t.config, width, termwidth);
table_set_border_header(&mut table, head, &t.config);
table_to_string(table, termwidth)
}
fn set_styles(table: &mut Table, styles: Styles, structure: &TableStructure) {
table.with(styles.cfg);
align_table(table, styles.alignments, structure);
colorize_table(table, styles.colors, structure);
}
fn table_set_border_header(table: &mut Table, head: Option<HeadInfo>, cfg: &TableConfig) {
let head = match head {
Some(head) => head,
@ -420,7 +503,6 @@ fn set_indent(table: &mut Table, indent: TableIndent) {
fn table_to_string(table: Table, termwidth: usize) -> Option<String> {
let total_width = table.total_width();
if total_width > termwidth {
None
} else {
@ -462,15 +544,23 @@ struct WidthEstimation {
#[allow(dead_code)]
total: usize,
truncate: bool,
trail: bool,
}
impl WidthEstimation {
fn new(original: Vec<usize>, needed: Vec<usize>, total: usize, truncate: bool) -> Self {
fn new(
original: Vec<usize>,
needed: Vec<usize>,
total: usize,
truncate: bool,
trail: bool,
) -> Self {
Self {
original,
needed,
total,
truncate,
trail,
}
}
}
@ -546,17 +636,12 @@ fn width_ctrl_truncate(
dims.set_widths(ctrl.width.needed);
}
fn align_table(table: &mut Table, alignments: Alignments, structure: &TableStructure) {
fn align_table(
table: &mut Table,
alignments: CellConfiguration<AlignmentHorizontal>,
structure: &TableStructure,
) {
table.with(AlignmentStrategy::PerLine);
table.with(Alignment::from(alignments.data));
for (column, alignment) in alignments.columns {
table.modify(Columns::single(column), Alignment::from(alignment));
}
for (pos, alignment) in alignments.cells {
table.modify(pos, Alignment::from(alignment));
}
if structure.with_header {
table.modify(Rows::first(), Alignment::from(alignments.header));
@ -571,23 +656,7 @@ fn align_table(table: &mut Table, alignments: Alignments, structure: &TableStruc
}
}
fn colorize_table(table: &mut Table, styles: Styles, structure: &TableStructure) {
if !is_color_empty(&styles.data) {
table.with(styles.data);
}
for (column, color) in styles.columns {
if !is_color_empty(&color) {
table.modify(Columns::single(column), color);
}
}
for (pos, color) in styles.cells {
if !is_color_empty(&color) {
table.modify(pos, color);
}
}
fn colorize_table(table: &mut Table, styles: CellConfiguration<Color>, structure: &TableStructure) {
if structure.with_index && !is_color_empty(&styles.index) {
table.modify(Columns::first(), styles.index);
}
@ -629,7 +698,8 @@ fn load_theme(
}
fn maybe_truncate_columns(
data: &mut NuRecords,
data: &mut Vec<Vec<NuRecordsValue>>,
widths: Vec<usize>,
cfg: &TableConfig,
termwidth: usize,
) -> WidthEstimation {
@ -639,15 +709,16 @@ fn maybe_truncate_columns(
let preserve_content = termwidth > TERMWIDTH_THRESHOLD;
if preserve_content {
truncate_columns_by_columns(data, &cfg.theme, pad, termwidth)
truncate_columns_by_columns(data, widths, &cfg.theme, pad, termwidth)
} else {
truncate_columns_by_content(data, &cfg.theme, pad, termwidth)
truncate_columns_by_content(data, widths, &cfg.theme, pad, 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 NuRecords,
data: &mut Vec<Vec<NuRecordsValue>>,
widths: Vec<usize>,
theme: &TableTheme,
pad: usize,
termwidth: usize,
@ -658,13 +729,14 @@ fn truncate_columns_by_content(
let trailing_column_width = TRAILING_COLUMN_WIDTH + pad;
let min_column_width = MIN_ACCEPTABLE_WIDTH + pad;
let count_columns = data[0].len();
let config = create_config(theme, false, None);
let widths_original = build_width(data, pad);
let widths_original = widths;
let mut widths = vec![];
let borders = config.get_borders();
let vertical = borders.has_vertical() as usize;
let count_columns = data.count_columns();
let mut width = borders.has_left() as usize + borders.has_right() as usize;
let mut truncate_pos = 0;
@ -685,7 +757,7 @@ fn truncate_columns_by_content(
}
if truncate_pos == count_columns {
return WidthEstimation::new(widths_original, widths, width, false);
return WidthEstimation::new(widths_original, widths, width, false, false);
}
if truncate_pos == 0 {
@ -702,11 +774,11 @@ fn truncate_columns_by_content(
widths.push(trailing_column_width);
width += trailing_column_width + vertical;
return WidthEstimation::new(widths_original, widths, width, true);
return WidthEstimation::new(widths_original, widths, width, true, true);
}
}
return WidthEstimation::new(widths_original, widths, width, false);
return WidthEstimation::new(widths_original, widths, width, false, false);
}
let available = termwidth - width;
@ -718,7 +790,7 @@ fn truncate_columns_by_content(
widths.push(w);
width += w + vertical;
return WidthEstimation::new(widths_original, widths, width, true);
return WidthEstimation::new(widths_original, widths, width, true, false);
}
// special case where the last column is smaller then a trailing column
@ -736,7 +808,7 @@ fn truncate_columns_by_content(
widths.push(next_column_width);
width += next_column_width + vertical;
return WidthEstimation::new(widths_original, widths, width, true);
return WidthEstimation::new(widths_original, widths, width, true, false);
}
}
@ -753,7 +825,7 @@ fn truncate_columns_by_content(
widths.push(trailing_column_width);
width += trailing_column_width + vertical;
return WidthEstimation::new(widths_original, widths, width, true);
return WidthEstimation::new(widths_original, widths, width, true, true);
}
if available >= trailing_column_width + vertical {
@ -763,7 +835,7 @@ fn truncate_columns_by_content(
widths.push(trailing_column_width);
width += trailing_column_width + vertical;
return WidthEstimation::new(widths_original, widths, width, false);
return WidthEstimation::new(widths_original, widths, width, false, true);
}
let last_width = widths.last().cloned().expect("ok");
@ -787,7 +859,7 @@ fn truncate_columns_by_content(
widths.push(trailing_column_width);
width += trailing_column_width + vertical;
return WidthEstimation::new(widths_original, widths, width, true);
return WidthEstimation::new(widths_original, widths, width, true, true);
}
}
@ -801,10 +873,10 @@ fn truncate_columns_by_content(
if widths.len() == 1 {
// nothing to show anyhow
return WidthEstimation::new(widths_original, vec![], width, false);
return WidthEstimation::new(widths_original, vec![], width, false, true);
}
WidthEstimation::new(widths_original, widths, width, false)
WidthEstimation::new(widths_original, widths, width, false, true)
}
// VERSION where we are showing AS MANY COLUMNS AS POSSIBLE but as a side affect they MIGHT CONTAIN AS LITTLE CONTENT AS POSSIBLE
@ -818,7 +890,8 @@ fn truncate_columns_by_content(
// Point being of the column needs more space we do can give it a little more based on it's distance from the start.
// Percentage wise.
fn truncate_columns_by_columns(
data: &mut NuRecords,
data: &mut Vec<Vec<NuRecordsValue>>,
widths: Vec<usize>,
theme: &TableTheme,
pad: usize,
termwidth: usize,
@ -829,13 +902,14 @@ fn truncate_columns_by_columns(
let trailing_column_width = TRAILING_COLUMN_WIDTH + pad;
let min_column_width = MIN_ACCEPTABLE_WIDTH + pad;
let count_columns = data[0].len();
let config = create_config(theme, false, None);
let widths_original = build_width(data, pad);
let widths_original = widths;
let mut widths = vec![];
let borders = config.get_borders();
let vertical = borders.has_vertical() as usize;
let count_columns = data.count_columns();
let mut width = borders.has_left() as usize + borders.has_right() as usize;
let mut truncate_pos = 0;
@ -857,7 +931,7 @@ fn truncate_columns_by_columns(
}
if truncate_pos == 0 {
return WidthEstimation::new(widths_original, widths, width, false);
return WidthEstimation::new(widths_original, widths, width, false, false);
}
let mut available = termwidth - width;
@ -882,7 +956,7 @@ fn truncate_columns_by_columns(
}
if truncate_pos == count_columns {
return WidthEstimation::new(widths_original, widths, width, true);
return WidthEstimation::new(widths_original, widths, width, true, false);
}
if available >= trailing_column_width + vertical {
@ -892,7 +966,7 @@ fn truncate_columns_by_columns(
widths.push(trailing_column_width);
width += trailing_column_width + vertical;
return WidthEstimation::new(widths_original, widths, width, true);
return WidthEstimation::new(widths_original, widths, width, true, true);
}
truncate_rows(data, truncate_pos - 1);
@ -903,7 +977,7 @@ fn truncate_columns_by_columns(
widths.push(trailing_column_width);
width += trailing_column_width;
WidthEstimation::new(widths_original, widths, width, true)
WidthEstimation::new(widths_original, widths, width, true, true)
}
fn get_total_width2(widths: &[usize], cfg: &ColoredConfig) -> usize {
@ -921,37 +995,22 @@ fn create_config(theme: &TableTheme, with_header: bool, color: Option<Style>) ->
table.get_config().clone()
}
fn push_empty_column(data: &mut NuRecords) {
let records = std::mem::take(data);
let mut inner: Vec<Vec<_>> = records.into();
fn push_empty_column(data: &mut Vec<Vec<NuRecordsValue>>) {
let empty_cell = Text::new(String::from(EMPTY_COLUMN_TEXT));
for row in &mut inner {
for row in data {
row.push(empty_cell.clone());
}
*data = VecRecords::new(inner);
}
fn duplicate_row(data: &mut NuRecords, row: usize) {
let records = std::mem::take(data);
let mut inner: Vec<Vec<_>> = records.into();
let duplicate = inner[row].clone();
inner.push(duplicate);
*data = VecRecords::new(inner);
fn duplicate_row(data: &mut Vec<Vec<NuRecordsValue>>, row: usize) {
let duplicate = data[row].clone();
data.push(duplicate);
}
fn truncate_rows(data: &mut NuRecords, count: usize) {
let records = std::mem::take(data);
let mut inner: Vec<Vec<_>> = records.into();
for row in &mut inner {
fn truncate_rows(data: &mut Vec<Vec<NuRecordsValue>>, count: usize) {
for row in data {
row.truncate(count);
}
*data = VecRecords::new(inner);
}
fn convert_alignment(alignment: nu_color_config::Alignment) -> AlignmentHorizontal {
@ -971,7 +1030,11 @@ impl<R> TableOption<R, ColoredConfig, CompleteDimensionVecRecords<'_>> for SetDi
}
}
fn build_width(records: &NuRecords, pad: usize) -> Vec<usize> {
fn build_width<R>(records: R, pad: usize) -> Vec<usize>
where
R: Records,
<R::Iter as IntoRecords>::Cell: AsRef<str>,
{
// TODO: Expose not spaned version (could be optimized).
let mut cfg = SpannedConfig::default();
let padding = Sides {
@ -981,6 +1044,7 @@ fn build_width(records: &NuRecords, pad: usize) -> Vec<usize> {
cfg.set_padding(Entity::Global, padding);
// TODO: Use peekable width
SpannedGridDimension::width(records, &cfg)
}
@ -1059,3 +1123,11 @@ impl<R, C> TableOption<R, C, CompleteDimensionVecRecords<'_>> for &mut GetDims {
None
}
}
pub fn get_color_if_exists(c: &Color) -> Option<Color> {
if !is_color_empty(c) {
Some(c.clone())
} else {
None
}
}

View File

@ -156,6 +156,16 @@ impl TableTheme {
Self::new(theme, full)
}
pub fn single() -> TableTheme {
let full = Style::modern()
.corner_top_left('┌')
.corner_top_right('┐')
.corner_bottom_left('└')
.corner_bottom_right('┘');
Self::new(Style::sharp(), full)
}
pub fn none() -> TableTheme {
Self::new(Style::blank(), Style::blank())
}

View File

@ -1,11 +1,9 @@
use std::{cmp::max, collections::HashMap};
use std::cmp::max;
use nu_color_config::{Alignment, StyleComputer, TextStyle};
use nu_engine::column::get_columns;
use nu_protocol::{Config, Record, ShellError, Span, Value};
use tabled::grid::config::Position;
use crate::{
common::{
check_value, configure_table, error_sign, get_header_style, get_index_style, load_theme,
@ -14,7 +12,7 @@ use crate::{
},
string_width,
types::has_index,
NuRecordsValue, NuTable, TableOpts, TableOutput,
NuTable, TableOpts, TableOutput,
};
#[derive(Debug, Clone)]
@ -106,7 +104,7 @@ fn expand_list(input: &[Value], cfg: Cfg<'_>) -> TableResult {
const PADDING_SPACE: usize = 2;
const SPLIT_LINE_SPACE: usize = 1;
const ADDITIONAL_CELL_SPACE: usize = PADDING_SPACE + SPLIT_LINE_SPACE;
const MIN_CELL_CONTENT_WIDTH: usize = 1;
const MIN_CELL_CONTENT_WIDTH: usize = 3;
const TRUNCATE_CONTENT_WIDTH: usize = 3;
const TRUNCATE_CELL_WIDTH: usize = TRUNCATE_CONTENT_WIDTH + PADDING_SPACE;
@ -124,10 +122,7 @@ fn expand_list(input: &[Value], cfg: Cfg<'_>) -> TableResult {
}
let headers = get_columns(input);
let with_index = has_index(&cfg.opts, &headers);
let row_offset = cfg.opts.index_offset;
let mut rows_count = 0usize;
// The header with the INDEX is removed from the table headers since
// it is added to the natural table index
@ -135,162 +130,171 @@ fn expand_list(input: &[Value], cfg: Cfg<'_>) -> TableResult {
.into_iter()
.filter(|header| header != INDEX_COLUMN_NAME)
.collect();
let with_header = !headers.is_empty();
let row_offset = cfg.opts.index_offset;
let mut data = vec![vec![]; input.len() + with_header as usize];
let mut data_styles = HashMap::new();
let mut total_rows = 0usize;
if with_index {
if with_header {
data[0].push(NuRecordsValue::exact(String::from("#"), 1, vec![]));
}
for (row, item) in input.iter().enumerate() {
cfg.opts.signals.check(cfg.opts.span)?;
check_value(item)?;
let index = row + row_offset;
let text = item
.as_record()
.ok()
.and_then(|val| val.get(INDEX_COLUMN_NAME))
.map(|value| value.to_expanded_string("", cfg.opts.config))
.unwrap_or_else(|| index.to_string());
let row = row + with_header as usize;
let value = NuRecordsValue::new(text);
data[row].push(value);
}
let column_width = string_width(data[data.len() - 1][0].as_ref());
if column_width + ADDITIONAL_CELL_SPACE > available_width {
available_width = 0;
} else {
available_width -= column_width + ADDITIONAL_CELL_SPACE;
}
}
if !with_header {
if available_width > ADDITIONAL_CELL_SPACE {
available_width -= PADDING_SPACE;
} else {
if !with_index && !with_header {
if available_width <= ADDITIONAL_CELL_SPACE {
// it means we have no space left for actual content;
// which means there's no point in index itself if it was even used.
// so we do not print it.
return Ok(None);
}
available_width -= PADDING_SPACE;
let mut table = NuTable::new(input.len(), 1);
table.set_index_style(get_index_style(&cfg.opts.style_computer));
table.set_header_style(get_header_style(&cfg.opts.style_computer));
table.set_indent(cfg.opts.config.table.padding);
for (row, item) in input.iter().enumerate() {
cfg.opts.signals.check(cfg.opts.span)?;
check_value(item)?;
let inner_cfg = cfg_expand_reset_table(cfg.clone(), available_width);
let mut cell = expand_entry(item, inner_cfg);
let cell = expand_entry(item, inner_cfg);
let value_width = string_width(&cell.text);
if value_width > available_width {
// it must only happen when a string is produced, so we can safely wrap it.
// (it might be string table representation as well) (I guess I mean default { table ...} { list ...})
//
// todo: Maybe convert_to_table2_entry could do for strings to not mess caller code?
table.insert((row, 0), cell.text);
table.insert_style((row, 0), cell.style);
cell.text = wrap_text(&cell.text, available_width, cfg.opts.config);
}
let value = NuRecordsValue::new(cell.text);
data[row].push(value);
data_styles.insert((row, with_index as usize), cell.style);
rows_count = rows_count.saturating_add(cell.size);
total_rows = total_rows.saturating_add(cell.size);
}
let mut table = NuTable::from(data);
table.set_indent(cfg.opts.config.table.padding);
table.set_index_style(get_index_style(&cfg.opts.style_computer));
set_data_styles(&mut table, data_styles);
return Ok(Some(TableOutput::new(table, false, with_index, rows_count)));
return Ok(Some(TableOutput::new(table, false, false, total_rows)));
}
if !headers.is_empty() {
let mut pad_space = PADDING_SPACE;
if headers.len() > 1 {
pad_space += SPLIT_LINE_SPACE;
if !with_header && with_index {
let mut table = NuTable::new(input.len(), 2);
table.set_index_style(get_index_style(&cfg.opts.style_computer));
table.set_header_style(get_header_style(&cfg.opts.style_computer));
table.set_indent(cfg.opts.config.table.padding);
let mut index_column_width = 0;
for (row, item) in input.iter().enumerate() {
cfg.opts.signals.check(cfg.opts.span)?;
check_value(item)?;
let index = row + row_offset;
let index_value = item
.as_record()
.ok()
.and_then(|val| val.get(INDEX_COLUMN_NAME))
.map(|value| value.to_expanded_string("", cfg.opts.config))
.unwrap_or_else(|| index.to_string());
let index_width = string_width(&index_value);
if available_width <= index_width + ADDITIONAL_CELL_SPACE + PADDING_SPACE {
// NOTE: we don't wanna wrap index; so we return
return Ok(None);
}
table.insert((row, 0), index_value);
index_column_width = max(index_column_width, index_width);
}
if available_width < pad_space {
// there's no space for actual data so we don't return index if it's present.
// (also see the comment after the loop)
available_width -= index_column_width + ADDITIONAL_CELL_SPACE + PADDING_SPACE;
for (row, item) in input.iter().enumerate() {
cfg.opts.signals.check(cfg.opts.span)?;
check_value(item)?;
let inner_cfg = cfg_expand_reset_table(cfg.clone(), available_width);
let cell = expand_entry(item, inner_cfg);
table.insert((row, 1), cell.text);
table.insert_style((row, 1), cell.style);
total_rows = total_rows.saturating_add(cell.size);
}
return Ok(Some(TableOutput::new(table, false, true, total_rows)));
}
// NOTE: redefine to not break above logic (fixme)
let mut available_width = cfg.opts.width - SPLIT_LINE_SPACE;
let mut table = NuTable::new(input.len() + 1, headers.len() + with_index as usize);
table.set_index_style(get_index_style(&cfg.opts.style_computer));
table.set_header_style(get_header_style(&cfg.opts.style_computer));
table.set_indent(cfg.opts.config.table.padding);
let mut widths = Vec::new();
if with_index {
table.insert((0, 0), String::from("#"));
let mut index_column_width = 1;
for (row, item) in input.iter().enumerate() {
cfg.opts.signals.check(cfg.opts.span)?;
check_value(item)?;
let index = row + row_offset;
let index_value = item
.as_record()
.ok()
.and_then(|val| val.get(INDEX_COLUMN_NAME))
.map(|value| value.to_expanded_string("", cfg.opts.config))
.unwrap_or_else(|| index.to_string());
let index_width = string_width(&index_value);
table.insert((row + 1, 0), index_value);
index_column_width = max(index_column_width, index_width);
}
if available_width <= index_column_width + ADDITIONAL_CELL_SPACE {
// NOTE: we don't wanna wrap index; so we return
return Ok(None);
}
available_width -= index_column_width + ADDITIONAL_CELL_SPACE;
widths.push(index_column_width);
}
let count_columns = headers.len();
let mut widths = Vec::new();
let mut truncate = false;
let mut rendered_column = 0;
for (col, header) in headers.into_iter().enumerate() {
let column = col + with_index as usize;
let extra_space = PADDING_SPACE + SPLIT_LINE_SPACE;
if available_width <= extra_space {
table.pop_column(table.count_columns() - column);
widths.pop();
truncate = true;
break;
}
let mut available = available_width - extra_space;
// We want to reserver some space for next column
// If we can't fit it in it will be popped anyhow.
let is_last_column = col + 1 == count_columns;
let mut pad_space = PADDING_SPACE;
if !is_last_column {
pad_space += SPLIT_LINE_SPACE;
if !is_last_column && available > TRUNCATE_CELL_WIDTH {
available -= TRUNCATE_CELL_WIDTH;
}
let mut available = available_width - pad_space;
let mut total_column_rows = 0usize;
let mut column_width = 0;
if !is_last_column {
// we need to make sure that we have a space for a next column if we use available width
// so we might need to decrease a bit it.
// we consider a header width be a minimum width
let pad_space = PADDING_SPACE + TRUNCATE_CONTENT_WIDTH;
if available > pad_space {
// In we have no space for a next column,
// We consider showing something better then nothing,
// So we try to decrease the width to show at least a truncution column
available -= pad_space;
} else {
truncate = true;
break;
}
if available < column_width {
truncate = true;
break;
}
}
let mut column_rows = 0usize;
for (row, item) in input.iter().enumerate() {
cfg.opts.signals.check(cfg.opts.span)?;
check_value(item)?;
let inner_cfg = cfg_expand_reset_table(cfg.clone(), available);
let mut cell = expand_entry_with_header(item, &header, inner_cfg);
let mut value_width = string_width(&cell.text);
if value_width > available {
// it must only happen when a string is produced, so we can safely wrap it.
// (it might be string table representation as well)
cell.text = wrap_text(&cell.text, available, cfg.opts.config);
value_width = available;
}
let cell = expand_entry_with_header(item, &header, inner_cfg);
let value_width = string_width(&cell.text); // TODO: optimize cause when we expand we alrready know the width (most of the time or all)
column_width = max(column_width, value_width);
let value = NuRecordsValue::new(cell.text);
data[row + 1].push(value);
data_styles.insert((row + 1, col + with_index as usize), cell.style);
table.insert((row + 1, column), cell.text);
table.insert_style((row + 1, column), cell.style);
column_rows = column_rows.saturating_add(cell.size);
total_column_rows = total_column_rows.saturating_add(cell.size);
}
let mut head_width = string_width(&header);
@ -300,50 +304,37 @@ fn expand_list(input: &[Value], cfg: Cfg<'_>) -> TableResult {
head_width = available;
}
let head_cell = NuRecordsValue::new(header);
data[0].push(head_cell);
table.insert((0, column), header);
column_width = max(column_width, head_width);
if column_width > available {
// remove the column we just inserted
for row in &mut data {
row.pop();
}
truncate = true;
break;
}
assert!(column_width <= available);
widths.push(column_width);
available_width -= pad_space + column_width;
available_width -= column_width + extra_space;
rendered_column += 1;
rows_count = std::cmp::max(rows_count, column_rows);
}
if truncate && rendered_column == 0 {
// it means that no actual data was rendered, there might be only index present,
// so there's no point in rendering the table.
//
// It's actually quite important in case it's called recursively,
// cause we will back up to the basic table view as a string e.g. '[table 123 columns]'.
//
// But potentially if its reached as a 1st called function we might would love to see the index.
return Ok(None);
total_rows = std::cmp::max(total_rows, total_column_rows);
}
if truncate {
if rendered_column == 0 {
// it means that no actual data was rendered, there might be only index present,
// so there's no point in rendering the table.
//
// It's actually quite important in case it's called recursively,
// cause we will back up to the basic table view as a string e.g. '[table 123 columns]'.
//
// But potentially if its reached as a 1st called function we might would love to see the index.
return Ok(None);
}
if available_width < TRUNCATE_CELL_WIDTH {
// back up by removing last column.
// it's LIKELY that removing only 1 column will leave us enough space for a shift column.
while let Some(width) = widths.pop() {
for row in &mut data {
row.pop();
}
table.pop_column(1);
available_width += width + PADDING_SPACE;
if !widths.is_empty() {
@ -364,22 +355,12 @@ fn expand_list(input: &[Value], cfg: Cfg<'_>) -> TableResult {
let is_last_column = widths.len() == count_columns;
if !is_last_column {
let shift = NuRecordsValue::exact(String::from("..."), 3, vec![]);
for row in &mut data {
row.push(shift.clone());
}
table.push_column(String::from("..."));
widths.push(3);
}
}
let mut table = NuTable::from(data);
table.set_index_style(get_index_style(&cfg.opts.style_computer));
table.set_header_style(get_header_style(&cfg.opts.style_computer));
table.set_indent(cfg.opts.config.table.padding);
set_data_styles(&mut table, data_styles);
Ok(Some(TableOutput::new(table, true, with_index, rows_count)))
Ok(Some(TableOutput::new(table, true, with_index, total_rows)))
}
fn expanded_table_kv(record: &Record, cfg: Cfg<'_>) -> CellResult {
@ -400,10 +381,13 @@ fn expanded_table_kv(record: &Record, cfg: Cfg<'_>) -> CellResult {
let value_width = cfg.opts.width - key_width - count_borders - padding - padding;
let mut count_rows = 0usize;
let mut total_rows = 0usize;
let mut data = Vec::with_capacity(record.len());
for (key, value) in record {
let mut table = NuTable::new(record.len(), 2);
table.set_index_style(get_key_style(&cfg));
table.set_indent(cfg.opts.config.table.padding);
for (i, (key, value)) in record.iter().enumerate() {
cfg.opts.signals.check(cfg.opts.span)?;
let cell = match expand_value(value, value_width, &cfg)? {
@ -411,29 +395,24 @@ fn expanded_table_kv(record: &Record, cfg: Cfg<'_>) -> CellResult {
None => return Ok(None),
};
let value = cell.text;
let mut key = key.to_owned();
// we want to have a key being aligned to 2nd line,
// we could use Padding for it but,
// the easiest way to do so is just push a new_line char before
let mut key = key.to_owned();
let is_key_on_next_line = !key.is_empty() && cell.is_expanded && theme.borders_has_top();
if is_key_on_next_line {
key.insert(0, '\n');
}
let key = NuRecordsValue::new(key);
let val = NuRecordsValue::new(cell.text);
let row = vec![key, val];
table.insert((i, 0), key);
table.insert((i, 1), value);
data.push(row);
count_rows = count_rows.saturating_add(cell.size);
total_rows = total_rows.saturating_add(cell.size);
}
let mut table = NuTable::from(data);
table.set_index_style(get_key_style(&cfg));
table.set_indent(cfg.opts.config.table.padding);
let mut out = TableOutput::new(table, false, true, count_rows);
let mut out = TableOutput::new(table, false, true, total_rows);
configure_table(
&mut out,
@ -443,7 +422,7 @@ fn expanded_table_kv(record: &Record, cfg: Cfg<'_>) -> CellResult {
);
maybe_expand_table(out, cfg.opts.width)
.map(|value| value.map(|value| CellOutput::clean(value, count_rows, false)))
.map(|value| value.map(|value| CellOutput::clean(value, total_rows, false)))
}
// the flag is used as an optimization to not do `value.lines().count()` search.
@ -519,6 +498,7 @@ fn expand_entry_with_header(item: &Value, header: &str, cfg: Cfg<'_>) -> CellOut
fn expand_entry(item: &Value, cfg: Cfg<'_>) -> CellOutput {
if is_limit_reached(&cfg) {
let value = nu_value_to_string_clean(item, cfg.opts.config, &cfg.opts.style_computer);
let value = nutext_wrap(value, &cfg);
return CellOutput::styled(value);
}
@ -527,6 +507,7 @@ fn expand_entry(item: &Value, cfg: Cfg<'_>) -> CellOutput {
Value::Record { val: record, .. } => {
if record.is_empty() {
let value = nu_value_to_string(item, cfg.opts.config, &cfg.opts.style_computer);
let value = nutext_wrap(value, &cfg);
return CellOutput::styled(value);
}
@ -538,6 +519,7 @@ fn expand_entry(item: &Value, cfg: Cfg<'_>) -> CellOutput {
Ok(Some(table)) => table,
_ => {
let value = nu_value_to_string(item, cfg.opts.config, &cfg.opts.style_computer);
let value = nutext_wrap(value, &cfg);
CellOutput::styled(value)
}
}
@ -560,6 +542,7 @@ fn expand_entry(item: &Value, cfg: Cfg<'_>) -> CellOutput {
Ok(Some(out)) => out,
_ => {
let value = nu_value_to_string(item, cfg.opts.config, &cfg.opts.style_computer);
let value = nutext_wrap(value, &cfg);
return CellOutput::styled(value);
}
};
@ -571,17 +554,28 @@ fn expand_entry(item: &Value, cfg: Cfg<'_>) -> CellOutput {
Some(table) => CellOutput::clean(table, out.count_rows, false),
None => {
let value = nu_value_to_string(item, cfg.opts.config, &cfg.opts.style_computer);
let value = nutext_wrap(value, &cfg);
CellOutput::styled(value)
}
}
}
_ => {
let value = nu_value_to_string_clean(item, cfg.opts.config, &cfg.opts.style_computer);
let value = nutext_wrap(value, &cfg);
CellOutput::styled(value)
}
}
}
fn nutext_wrap(mut text: NuText, cfg: &Cfg<'_>) -> NuText {
let width = string_width(&text.0);
if width > cfg.opts.width {
text.0 = wrap_text(&text.0, cfg.opts.width, cfg.opts.config);
}
text
}
fn is_limit_reached(cfg: &Cfg<'_>) -> bool {
matches!(cfg.format.expand_limit, Some(0))
}
@ -626,12 +620,6 @@ fn maybe_expand_table(mut out: TableOutput, term_width: usize) -> StringResult {
Ok(table)
}
fn set_data_styles(table: &mut NuTable, styles: HashMap<Position, TextStyle>) {
for (pos, style) in styles {
table.insert_style(pos, style);
}
}
fn table_apply_config(out: &mut TableOutput, cfg: &Cfg<'_>) {
configure_table(
out,

View File

@ -15,23 +15,23 @@ use crate::{
pub struct JustTable;
impl JustTable {
pub fn table(input: &[Value], opts: TableOpts<'_>) -> StringResult {
pub fn table(input: Vec<Value>, opts: TableOpts<'_>) -> StringResult {
list_table(input, opts)
}
pub fn kv_table(record: &Record, opts: TableOpts<'_>) -> StringResult {
pub fn kv_table(record: Record, opts: TableOpts<'_>) -> StringResult {
kv_table(record, opts)
}
}
fn list_table(input: &[Value], opts: TableOpts<'_>) -> Result<Option<String>, ShellError> {
let mut out = match create_table(input, &opts)? {
fn list_table(input: Vec<Value>, opts: TableOpts<'_>) -> Result<Option<String>, ShellError> {
let output = create_table(input, &opts)?;
let mut out = match output {
Some(out) => out,
None => return Ok(None),
};
out.table.set_indent(opts.config.table.padding);
// TODO: It would be WAY more effitient to do right away instead of second pass over the data.
colorize_space(out.table.get_records_mut(), &opts.style_computer);
configure_table(&mut out, opts.config, &opts.style_computer, opts.mode);
@ -40,25 +40,20 @@ fn list_table(input: &[Value], opts: TableOpts<'_>) -> Result<Option<String>, Sh
Ok(table)
}
fn kv_table(record: &Record, opts: TableOpts<'_>) -> StringResult {
let mut data = vec![Vec::with_capacity(2); record.len()];
for ((column, value), row) in record.iter().zip(data.iter_mut()) {
opts.signals.check(opts.span)?;
let key = NuRecordsValue::new(column.to_string());
let value = nu_value_to_string_colored(value, opts.config, &opts.style_computer);
let value = NuRecordsValue::new(value);
row.push(key);
row.push(value);
}
let mut table = NuTable::from(data);
fn kv_table(record: Record, opts: TableOpts<'_>) -> StringResult {
let mut table = NuTable::new(record.len(), 2);
table.set_index_style(TextStyle::default_field());
table.set_indent(opts.config.table.padding);
for (i, (key, value)) in record.into_iter().enumerate() {
opts.signals.check(opts.span)?;
let value = nu_value_to_string_colored(&value, opts.config, &opts.style_computer);
table.insert((i, 0), key);
table.insert((i, 1), value);
}
let mut out = TableOutput::from_table(table, false, true);
configure_table(&mut out, opts.config, &opts.style_computer, opts.mode);
let table = out.table.draw(opts.width);
@ -66,12 +61,12 @@ fn kv_table(record: &Record, opts: TableOpts<'_>) -> StringResult {
Ok(table)
}
fn create_table(input: &[Value], opts: &TableOpts<'_>) -> TableResult {
fn create_table(input: Vec<Value>, opts: &TableOpts<'_>) -> TableResult {
if input.is_empty() {
return Ok(None);
}
let headers = get_columns(input);
let headers = get_columns(&input);
let with_index = has_index(opts, &headers);
let with_header = !headers.is_empty();
let row_offset = opts.index_offset;
@ -89,27 +84,23 @@ fn create_table(input: &[Value], opts: &TableOpts<'_>) -> TableResult {
}
fn create_table_with_header(
input: &[Value],
input: Vec<Value>,
headers: Vec<String>,
opts: &TableOpts<'_>,
) -> Result<Option<NuTable>, ShellError> {
let headers = collect_headers(headers, false);
let count_rows = input.len() + 1;
let count_columns = headers.len();
let mut table = NuTable::new(count_rows, count_columns);
table.set_header_style(get_header_style(&opts.style_computer));
table.set_index_style(get_index_style(&opts.style_computer));
table.set_indent(opts.config.table.padding);
table.set_row(0, headers.clone());
for (row, item) in input.iter().enumerate() {
for (row, item) in input.into_iter().enumerate() {
opts.signals.check(opts.span)?;
check_value(item)?;
check_value(&item)?;
for (col, header) in headers.iter().enumerate() {
let (text, style) = get_string_value_with_header(item, header.as_ref(), opts);
let (text, style) = get_string_value_with_header(&item, header, opts);
let pos = (row + 1, col);
table.insert(pos, text);
@ -117,35 +108,39 @@ fn create_table_with_header(
}
}
let headers = collect_headers(headers, false);
table.set_row(0, headers);
Ok(Some(table))
}
fn create_table_with_header_and_index(
input: &[Value],
input: Vec<Value>,
headers: Vec<String>,
row_offset: usize,
opts: &TableOpts<'_>,
) -> Result<Option<NuTable>, ShellError> {
let headers = collect_headers(headers, true);
let head = collect_headers(headers, true);
let count_rows = input.len() + 1;
let count_columns = headers.len();
let mut table = NuTable::new(count_rows, count_columns);
let count_columns = head.len();
let mut table = NuTable::new(count_rows, count_columns);
table.set_header_style(get_header_style(&opts.style_computer));
table.set_index_style(get_index_style(&opts.style_computer));
table.set_indent(opts.config.table.padding);
table.set_row(0, headers.clone());
table.set_row(0, head.clone());
for (row, item) in input.iter().enumerate() {
for (row, item) in input.into_iter().enumerate() {
opts.signals.check(opts.span)?;
check_value(item)?;
check_value(&item)?;
let text = get_table_row_index(item, opts.config, row, row_offset);
let text = get_table_row_index(&item, opts.config, row, row_offset);
table.insert((row + 1, 0), text);
for (col, header) in headers.iter().enumerate().skip(1) {
let (text, style) = get_string_value_with_header(item, header.as_ref(), opts);
for (col, head) in head.iter().enumerate().skip(1) {
let (text, style) = get_string_value_with_header(&item, head.as_ref(), opts);
let pos = (row + 1, col);
table.insert(pos, text);
@ -157,46 +152,45 @@ fn create_table_with_header_and_index(
}
fn create_table_with_no_header(
input: &[Value],
input: Vec<Value>,
opts: &TableOpts<'_>,
) -> Result<Option<NuTable>, ShellError> {
let mut table = NuTable::new(input.len(), 1);
table.set_index_style(get_index_style(&opts.style_computer));
table.set_indent(opts.config.table.padding);
for (row, item) in input.iter().enumerate() {
for (row, item) in input.into_iter().enumerate() {
opts.signals.check(opts.span)?;
check_value(item)?;
check_value(&item)?;
let (text, style) = get_string_value(item, opts);
let (text, style) = get_string_value(&item, opts);
let pos = (row, 0);
table.insert(pos, text);
table.insert_style(pos, style);
table.insert((row, 0), text);
table.insert_style((row, 0), style);
}
Ok(Some(table))
}
fn create_table_with_no_header_and_index(
input: &[Value],
input: Vec<Value>,
row_offset: usize,
opts: &TableOpts<'_>,
) -> Result<Option<NuTable>, ShellError> {
let mut table = NuTable::new(input.len(), 1 + 1);
table.set_index_style(get_index_style(&opts.style_computer));
table.set_indent(opts.config.table.padding);
for (row, item) in input.iter().enumerate() {
for (row, item) in input.into_iter().enumerate() {
opts.signals.check(opts.span)?;
check_value(item)?;
check_value(&item)?;
let text = get_table_row_index(item, opts.config, row, row_offset);
table.insert((row, 0), text);
let index = get_table_row_index(&item, opts.config, row, row_offset);
let (value, style) = get_string_value(&item, opts);
let (text, style) = get_string_value(item, opts);
let pos = (row, 1);
table.insert(pos, text);
table.insert_style(pos, style);
table.insert((row, 0), index);
table.insert((row, 1), value);
table.insert_style((row, 1), style);
}
Ok(Some(table))

View File

@ -88,7 +88,14 @@ where
}
pub fn create_table(data: Data, case: TestCase) -> Option<String> {
let mut table = NuTable::from(data);
let count_rows = data.len();
let count_cols = data[0].len();
let mut table = NuTable::new(count_rows, count_cols);
for (i, row) in data.into_iter().enumerate() {
table.set_row(i, row);
}
table.set_theme(case.theme);
table.set_structure(case.with_index, case.with_header, case.with_footer);
table.set_trim(case.strategy);

View File

@ -451,6 +451,52 @@ fn test_with_love() {
assert_eq!(create_table_with_size(vec![], true, theme::with_love()), "");
}
#[test]
fn test_single() {
assert_eq!(
create_table(vec![row(4); 3], true, theme::single()),
"┌───┬───┬───┬───┐\n\
0 1 2 3 \n\
\n\
0 1 2 3 \n\
0 1 2 3 \n\
"
);
assert_eq!(
create_table(vec![row(4); 2], true, theme::single()),
"┌───┬───┬───┬───┐\n\
0 1 2 3 \n\
\n\
0 1 2 3 \n\
"
);
assert_eq!(
create_table(vec![row(4); 1], true, theme::single()),
"┌───┬───┬───┬───┐\n\
0 1 2 3 \n\
"
);
assert_eq!(
create_table(vec![row(4); 1], false, theme::single()),
"┌───┬───┬───┬───┐\n\
0 1 2 3 \n\
"
);
assert_eq!(
create_table(vec![row(4); 2], false, theme::single()),
"┌───┬───┬───┬───┐\n\
0 1 2 3 \n\
0 1 2 3 \n\
"
);
assert_eq!(create_table_with_size(vec![], true, theme::single()), "");
}
fn create_table(data: Vec<Vec<Text<String>>>, with_header: bool, theme: theme) -> String {
let mut case = TestCase::new(usize::MAX).theme(theme);
if with_header {

View File

@ -300,7 +300,7 @@ $env.config.footer_mode = 25
# Specifies the visual display style of a table
# One of: "default", "basic", "compact", "compact_double", "heavy", "light", "none", "reinforced",
# "rounded", "thin", "with_love", "psql", "markdown", "dots", "restructured", "ascii_rounded",
# or "basic_compact"
# "basic_compact" or "single"
# Can be overridden by passing a table to `| table --theme/-t`
$env.config.table.mode = "default"

View File

@ -91,8 +91,7 @@ impl Inc {
pub fn inc(&self, head: Span, value: &Value) -> Result<Value, LabeledError> {
if let Some(cell_path) = &self.cell_path {
let working_value = value.clone();
let cell_value = working_value.follow_cell_path(&cell_path.members, false)?;
let cell_value = value.follow_cell_path(&cell_path.members, false)?;
let cell_value = self.inc_value(head, &cell_value)?;

View File

@ -0,0 +1,191 @@
use crate::{
values::{Column, CustomValueSupport, NuDataFrame, NuExpression},
PolarsPlugin,
};
use nu_plugin::{EngineInterface, EvaluatedCall, PluginCommand};
use nu_protocol::{
Category, Example, LabeledError, PipelineData, ShellError, Signature, Span, Spanned,
SyntaxShape, Type, Value,
};
use polars::lazy::dsl::{
all_horizontal, any_horizontal, max_horizontal, mean_horizontal, min_horizontal, sum_horizontal,
};
use polars::prelude::Expr;
enum HorizontalType {
All,
Any,
Min,
Max,
Sum,
Mean,
}
impl HorizontalType {
fn from_str(roll_type: &str, span: Span) -> Result<Self, ShellError> {
match roll_type {
"all" => Ok(Self::All),
"any" => Ok(Self::Any),
"min" => Ok(Self::Min),
"max" => Ok(Self::Max),
"sum" => Ok(Self::Sum),
"mean" => Ok(Self::Mean),
_ => Err(ShellError::GenericError {
error: "Wrong operation".into(),
msg: "Operation not valid for cumulative".into(),
span: Some(span),
help: Some("Allowed values: all, any, max, min, sum, mean".into()),
inner: vec![],
}),
}
}
}
#[derive(Clone)]
pub struct Horizontal;
impl PluginCommand for Horizontal {
type Plugin = PolarsPlugin;
fn name(&self) -> &str {
"polars horizontal"
}
fn description(&self) -> &str {
"Horizontal calculation across multiple columns."
}
fn signature(&self) -> Signature {
Signature::build(self.name())
.input_output_type(Type::Any, Type::Custom("expression".into()))
.required(
"type",
SyntaxShape::String,
"horizontal operation. Values of all, any, min, max, sum, and mean are accepted.",
)
.rest(
"Group-by expressions",
SyntaxShape::Any,
"Expression(s) that define the lazy group-by",
)
.switch(
"nulls",
"If set, null value in the input will lead to null output",
Some('n'),
)
.category(Category::Custom("expression".into()))
}
fn examples(&self) -> Vec<Example> {
vec![
Example {
description: "Horizontal sum across two columns (ignore nulls by default)",
example: "[[a b]; [1 2] [2 3] [3 4] [4 5] [5 null]]
| polars into-df
| polars select (polars horizontal sum a b)
| polars collect",
result: Some(
NuDataFrame::try_from_columns(
vec![Column::new(
"sum".to_string(),
vec![
Value::test_int(3),
Value::test_int(5),
Value::test_int(7),
Value::test_int(9),
Value::test_int(5),
],
)],
None,
)
.expect("simple df for test should not fail")
.into_value(Span::test_data()),
),
},
Example {
description: "Horizontal sum across two columns while accounting for nulls",
example: "[[a b]; [1 2] [2 3] [3 4] [4 5] [5 null]]
| polars into-df
| polars select (polars horizontal sum a b --nulls)
| polars collect",
result: Some(
NuDataFrame::try_from_columns(
vec![Column::new(
"sum".to_string(),
vec![
Value::test_int(3),
Value::test_int(5),
Value::test_int(7),
Value::test_int(9),
Value::test_nothing(),
],
)],
None,
)
.expect("simple df for test should not fail")
.into_value(Span::test_data()),
),
},
]
}
fn run(
&self,
plugin: &Self::Plugin,
engine: &EngineInterface,
call: &EvaluatedCall,
_input: PipelineData,
) -> Result<PipelineData, LabeledError> {
let func_type: Spanned<String> = call.req(0)?;
let func_type = HorizontalType::from_str(&func_type.item, func_type.span)?;
let vals: Vec<Value> = call.rest(1)?;
let expr_value = Value::list(vals, call.head);
let exprs = NuExpression::extract_exprs(plugin, expr_value)?;
let ignore_nulls = !call.has_flag("nulls")?;
command(plugin, engine, call, func_type, exprs, ignore_nulls).map_err(LabeledError::from)
}
}
fn command(
plugin: &PolarsPlugin,
engine: &EngineInterface,
call: &EvaluatedCall,
func_type: HorizontalType,
exprs: Vec<Expr>,
ignore_nulls: bool,
) -> Result<PipelineData, ShellError> {
let res: NuExpression = match func_type {
HorizontalType::All => all_horizontal(exprs),
HorizontalType::Any => any_horizontal(exprs),
HorizontalType::Max => max_horizontal(exprs),
HorizontalType::Min => min_horizontal(exprs),
HorizontalType::Sum => sum_horizontal(exprs, ignore_nulls),
HorizontalType::Mean => mean_horizontal(exprs, ignore_nulls),
}
.map_err(|e| ShellError::GenericError {
error: "Cannot apply horizontal aggregation".to_string(),
msg: "".into(),
span: Some(call.head),
help: Some(e.to_string()),
inner: vec![],
})?
.into();
res.to_pipeline_data(plugin, engine, call.head)
}
#[cfg(test)]
mod test {
use super::*;
use crate::test::test_polars_plugin_command;
#[test]
fn test_examples() -> Result<(), ShellError> {
test_polars_plugin_command(&Horizontal)
}
}

View File

@ -3,6 +3,7 @@ mod aggregate;
mod count;
mod cumulative;
pub mod groupby;
mod horizontal;
mod implode;
mod max;
mod mean;
@ -25,6 +26,7 @@ use nu_plugin::PluginCommand;
pub use aggregate::LazyAggregate;
use count::ExprCount;
pub use cumulative::Cumulative;
pub use horizontal::Horizontal;
use implode::ExprImplode;
use max::ExprMax;
use mean::ExprMean;
@ -45,19 +47,20 @@ pub(crate) fn aggregation_commands() -> Vec<Box<dyn PluginCommand<Plugin = Polar
Box::new(ExprCount),
Box::new(ExprImplode),
Box::new(ExprMax),
Box::new(ExprMin),
Box::new(ExprSum),
Box::new(ExprMean),
Box::new(ExprMin),
Box::new(ExprStd),
Box::new(ExprSum),
Box::new(ExprVar),
Box::new(Horizontal),
Box::new(LazyAggregate),
Box::new(median::LazyMedian),
Box::new(quantile::LazyQuantile),
Box::new(groupby::ToLazyGroupBy),
Box::new(NNull),
Box::new(NUnique),
Box::new(Over),
Box::new(Rolling),
Box::new(ValueCount),
Box::new(NNull),
Box::new(NUnique),
Box::new(groupby::ToLazyGroupBy),
Box::new(median::LazyMedian),
Box::new(quantile::LazyQuantile),
]
}

View File

@ -15,7 +15,7 @@ The Nushell team runs **testing of Nushell for the following platforms** through
- macOS (latest version available through GitHub CI)
- Windows (10 and 11)
- Linux (our test runners use `ubuntu-20.04` to represent distributions with not the latest glibc versions.)
- Linux (our test runners use `ubuntu-22.04` to represent distributions with not the latest glibc versions.)
All PR level tests are performed on x86/AMD64 (at least at the time of writing the default macOS runner was not yet using arm64).

View File

@ -143,6 +143,10 @@ fn export_module_which_defined_const() -> TestResult {
run_test(
r#"module spam { export const b = 3; export const c = 4 }; use spam; $spam.b + $spam.c"#,
"7",
)?;
fail_test(
r#"module spam { export const b = 3; export const c = 4 }; use spam; $b"#,
"variable not found",
)
}