nushell/crates/nu-engine/src/documentation.rs
Yash Thakur 0303d709e6
Spread operator in record literals (#11144)
Goes towards implementing #10598, which asks for a spread operator in
lists, in records, and when calling commands (continuation of #11006,
which only implements it in lists)

# Description
This PR is for adding a spread operator that can be used when building
records. Additional functionality can be added later.

Changes:

- Previously, the `Expr::Record` variant held `(Expression, Expression)`
pairs. It now holds instances of an enum `RecordItem` (the name isn't
amazing) that allows either a key-value mapping or a spread operator.
- `...` will be treated as the spread operator when it appears before
`$`, `{`, or `(` inside records (no whitespace allowed in between) (not
implemented yet)
- The error message for duplicate columns now includes the column name
itself, because if two spread records are involved in such an error, you
can't tell which field was duplicated from the spans alone

`...` will still be treated as a normal string outside records, and even
in records, it is not treated as a spread operator when not followed
immediately by a `$`, `{`, or `(`.

# User-Facing Changes
Users will be able to use `...` when building records.

```
> let rec = { x: 1, ...{ a: 2 } }
> $rec
╭───┬───╮
│ x │ 1 │
│ a │ 2 │
╰───┴───╯
> { foo: bar, ...$rec, baz: blah }
╭─────┬──────╮
│ foo │ bar  │
│ x   │ 1    │
│ a   │ 2    │
│ baz │ blah │
╰─────┴──────╯
```
If you want to update a field of a record, you'll have to use `merge`
instead:
```
> { ...$rec, x: 5 }
Error: nu:🐚:column_defined_twice

  × Record field or table column used twice: x
   ╭─[entry #2:1:1]
 1 │  { ...$rec, x: 5 }
   ·       ──┬─  ┬
   ·         │   ╰── field redefined here
   ·         ╰── field first defined here
   ╰────
> $rec | merge { x: 5 }
╭───┬───╮
│ x │ 5 │
│ a │ 2 │
╰───┴───╯
```

# Tests + Formatting

# After Submitting
2023-11-29 18:31:31 +01:00

557 lines
19 KiB
Rust

use nu_protocol::ast::{Argument, Expr, Expression, RecordItem};
use nu_protocol::{
ast::Call,
engine::{EngineState, Stack},
record, Category, Example, IntoPipelineData, PipelineData, Signature, Span, SyntaxShape, Type,
Value,
};
use std::{collections::HashMap, fmt::Write};
use crate::eval_call;
pub fn get_full_help(
sig: &Signature,
examples: &[Example],
engine_state: &EngineState,
stack: &mut Stack,
is_parser_keyword: bool,
) -> String {
let config = engine_state.get_config();
let doc_config = DocumentationConfig {
no_subcommands: false,
no_color: !config.use_ansi_coloring,
brief: false,
};
get_documentation(
sig,
examples,
engine_state,
stack,
&doc_config,
is_parser_keyword,
)
}
#[derive(Default)]
struct DocumentationConfig {
no_subcommands: bool,
no_color: bool,
brief: bool,
}
// Utility returns nu-highlighted string
fn nu_highlight_string(code_string: &str, engine_state: &EngineState, stack: &mut Stack) -> String {
if let Some(highlighter) = engine_state.find_decl(b"nu-highlight", &[]) {
let decl = engine_state.get_decl(highlighter);
if let Ok(output) = decl.run(
engine_state,
stack,
&Call::new(Span::unknown()),
Value::string(code_string, Span::unknown()).into_pipeline_data(),
) {
let result = output.into_value(Span::unknown());
if let Ok(s) = result.as_string() {
return s; // successfully highlighted string
}
}
}
code_string.to_string()
}
#[allow(clippy::cognitive_complexity)]
fn get_documentation(
sig: &Signature,
examples: &[Example],
engine_state: &EngineState,
stack: &mut Stack,
config: &DocumentationConfig,
is_parser_keyword: bool,
) -> String {
// Create ansi colors
//todo make these configurable -- pull from enginestate.config
let help_section_name: String =
get_ansi_color_for_component_or_default(engine_state, "shape_string", "\x1b[32m"); // default: green
let help_subcolor_one: String =
get_ansi_color_for_component_or_default(engine_state, "shape_external", "\x1b[36m"); // default: cyan
// was const bb: &str = "\x1b[1;34m"; // bold blue
let help_subcolor_two: String =
get_ansi_color_for_component_or_default(engine_state, "shape_block", "\x1b[94m"); // default: light blue (nobold, should be bolding the *names*)
const RESET: &str = "\x1b[0m"; // reset
let cmd_name = &sig.name;
let mut long_desc = String::new();
let usage = &sig.usage;
if !usage.is_empty() {
long_desc.push_str(usage);
long_desc.push_str("\n\n");
}
let extra_usage = if config.brief { "" } else { &sig.extra_usage };
if !extra_usage.is_empty() {
long_desc.push_str(extra_usage);
long_desc.push_str("\n\n");
}
let mut subcommands = vec![];
if !config.no_subcommands {
let signatures = engine_state.get_signatures(true);
for sig in signatures {
if sig.name.starts_with(&format!("{cmd_name} "))
// Don't display removed/deprecated commands in the Subcommands list
&& !matches!(sig.category, Category::Removed)
{
subcommands.push(format!(
" {help_subcolor_one}{}{RESET} - {}",
sig.name, sig.usage
));
}
}
}
if !sig.search_terms.is_empty() {
let text = format!(
"{help_section_name}Search terms{RESET}: {help_subcolor_one}{}{}\n\n",
sig.search_terms.join(", "),
RESET
);
let _ = write!(long_desc, "{text}");
}
let text = format!(
"{}Usage{}:\n > {}\n",
help_section_name,
RESET,
sig.call_signature()
);
let _ = write!(long_desc, "{text}");
if !subcommands.is_empty() {
let _ = write!(long_desc, "\n{help_section_name}Subcommands{RESET}:\n");
subcommands.sort();
long_desc.push_str(&subcommands.join("\n"));
long_desc.push('\n');
}
if !sig.named.is_empty() {
long_desc.push_str(&get_flags_section(Some(engine_state), sig, |v| {
nu_highlight_string(
&v.into_string_parsable(", ", &engine_state.config),
engine_state,
stack,
)
}))
}
if !sig.required_positional.is_empty()
|| !sig.optional_positional.is_empty()
|| sig.rest_positional.is_some()
{
let _ = write!(long_desc, "\n{help_section_name}Parameters{RESET}:\n");
for positional in &sig.required_positional {
let text = match &positional.shape {
SyntaxShape::Keyword(kw, shape) => {
format!(
" {help_subcolor_one}\"{}\" + {RESET}<{help_subcolor_two}{}{RESET}>: {}",
String::from_utf8_lossy(kw),
document_shape(*shape.clone()),
positional.desc
)
}
_ => {
format!(
" {help_subcolor_one}{}{RESET} <{help_subcolor_two}{}{RESET}>: {}",
positional.name,
document_shape(positional.shape.clone()),
positional.desc
)
}
};
let _ = writeln!(long_desc, "{text}");
}
for positional in &sig.optional_positional {
let text = match &positional.shape {
SyntaxShape::Keyword(kw, shape) => {
format!(
" {help_subcolor_one}\"{}\" + {RESET}<{help_subcolor_two}{}{RESET}>: {} (optional)",
String::from_utf8_lossy(kw),
document_shape(*shape.clone()),
positional.desc
)
}
_ => {
let opt_suffix = if let Some(value) = &positional.default_value {
format!(
" (optional, default: {})",
nu_highlight_string(
&value.into_string_parsable(", ", &engine_state.config),
engine_state,
stack
)
)
} else {
(" (optional)").to_string()
};
format!(
" {help_subcolor_one}{}{RESET} <{help_subcolor_two}{}{RESET}>: {}{}",
positional.name,
document_shape(positional.shape.clone()),
positional.desc,
opt_suffix,
)
}
};
let _ = writeln!(long_desc, "{text}");
}
if let Some(rest_positional) = &sig.rest_positional {
let text = format!(
" ...{help_subcolor_one}{}{RESET} <{help_subcolor_two}{}{RESET}>: {}",
rest_positional.name,
document_shape(rest_positional.shape.clone()),
rest_positional.desc
);
let _ = writeln!(long_desc, "{text}");
}
}
if !is_parser_keyword && !sig.input_output_types.is_empty() {
if let Some(decl_id) = engine_state.find_decl(b"table", &[]) {
// FIXME: we may want to make this the span of the help command in the future
let span = Span::unknown();
let mut vals = vec![];
for (input, output) in &sig.input_output_types {
vals.push(Value::record(
record! {
"input" => Value::string(input.to_string(), span),
"output" => Value::string(output.to_string(), span),
},
span,
));
}
let mut caller_stack = Stack::new();
if let Ok(result) = eval_call(
engine_state,
&mut caller_stack,
&Call {
decl_id,
head: span,
arguments: vec![],
redirect_stdout: true,
redirect_stderr: true,
parser_info: HashMap::new(),
},
PipelineData::Value(Value::list(vals, span), None),
) {
if let Ok((str, ..)) = result.collect_string_strict(span) {
let _ = writeln!(long_desc, "\n{help_section_name}Input/output types{RESET}:");
for line in str.lines() {
let _ = writeln!(long_desc, " {line}");
}
}
}
}
}
if !examples.is_empty() {
let _ = write!(long_desc, "\n{help_section_name}Examples{RESET}:");
}
for example in examples {
long_desc.push('\n');
long_desc.push_str(" ");
long_desc.push_str(example.description);
if config.no_color {
let _ = write!(long_desc, "\n > {}\n", example.example);
} else if let Some(highlighter) = engine_state.find_decl(b"nu-highlight", &[]) {
let decl = engine_state.get_decl(highlighter);
match decl.run(
engine_state,
stack,
&Call::new(Span::unknown()),
Value::string(example.example, Span::unknown()).into_pipeline_data(),
) {
Ok(output) => {
let result = output.into_value(Span::unknown());
match result.as_string() {
Ok(s) => {
let _ = write!(long_desc, "\n > {s}\n");
}
_ => {
let _ = write!(long_desc, "\n > {}\n", example.example);
}
}
}
Err(_) => {
let _ = write!(long_desc, "\n > {}\n", example.example);
}
}
} else {
let _ = write!(long_desc, "\n > {}\n", example.example);
}
if let Some(result) = &example.result {
let table = engine_state
.find_decl("table".as_bytes(), &[])
.and_then(|decl_id| {
engine_state
.get_decl(decl_id)
.run(
engine_state,
stack,
&Call::new(Span::new(0, 0)),
PipelineData::Value(result.clone(), None),
)
.ok()
});
for item in table.into_iter().flatten() {
let _ = writeln!(
long_desc,
" {}",
item.into_string("", engine_state.get_config())
.replace('\n', "\n ")
.trim()
);
}
}
}
long_desc.push('\n');
if config.no_color {
nu_utils::strip_ansi_string_likely(long_desc)
} else {
long_desc
}
}
fn get_ansi_color_for_component_or_default(
engine_state: &EngineState,
theme_component: &str,
default: &str,
) -> String {
if let Some(color) = &engine_state.get_config().color_config.get(theme_component) {
let mut caller_stack = Stack::new();
let span = Span::unknown();
let argument_opt = get_argument_for_color_value(engine_state, color, span);
// Call ansi command using argument
if let Some(argument) = argument_opt {
if let Some(decl_id) = engine_state.find_decl(b"ansi", &[]) {
if let Ok(result) = eval_call(
engine_state,
&mut caller_stack,
&Call {
decl_id,
head: span,
arguments: vec![argument],
redirect_stdout: true,
redirect_stderr: true,
parser_info: HashMap::new(),
},
PipelineData::Empty,
) {
if let Ok((str, ..)) = result.collect_string_strict(span) {
return str;
}
}
}
}
}
default.to_string()
}
fn get_argument_for_color_value(
engine_state: &EngineState,
color: &&Value,
span: Span,
) -> Option<Argument> {
match color {
Value::Record { val, .. } => {
let record_exp: Vec<RecordItem> = val
.into_iter()
.map(|(k, v)| {
RecordItem::Pair(
Expression {
expr: Expr::String(k.clone()),
span,
ty: Type::String,
custom_completion: None,
},
Expression {
expr: Expr::String(
v.clone().into_string("", engine_state.get_config()),
),
span,
ty: Type::String,
custom_completion: None,
},
)
})
.collect();
Some(Argument::Positional(Expression {
span: Span::unknown(),
ty: Type::Record(vec![
("fg".to_string(), Type::String),
("attr".to_string(), Type::String),
]),
expr: Expr::Record(record_exp),
custom_completion: None,
}))
}
Value::String { val, .. } => Some(Argument::Positional(Expression {
span: Span::unknown(),
ty: Type::String,
expr: Expr::String(val.clone()),
custom_completion: None,
})),
_ => None,
}
}
// document shape helps showing more useful information
pub fn document_shape(shape: SyntaxShape) -> SyntaxShape {
match shape {
SyntaxShape::CompleterWrapper(inner_shape, _) => *inner_shape,
_ => shape,
}
}
pub fn get_flags_section<F>(
engine_state_opt: Option<&EngineState>,
signature: &Signature,
mut value_formatter: F, // format default Value (because some calls cant access config or nu-highlight)
) -> String
where
F: FnMut(&nu_protocol::Value) -> String,
{
//todo make these configurable -- pull from enginestate.config
let help_section_name: String;
let help_subcolor_one: String;
let help_subcolor_two: String;
// Sometimes we want to get the flags without engine_state
// For example, in nu-plugin. In that case, we fall back on default values
if let Some(engine_state) = engine_state_opt {
help_section_name =
get_ansi_color_for_component_or_default(engine_state, "shape_string", "\x1b[32m"); // default: green
help_subcolor_one =
get_ansi_color_for_component_or_default(engine_state, "shape_external", "\x1b[36m"); // default: cyan
// was const bb: &str = "\x1b[1;34m"; // bold blue
help_subcolor_two =
get_ansi_color_for_component_or_default(engine_state, "shape_block", "\x1b[94m");
// default: light blue (nobold, should be bolding the *names*)
} else {
help_section_name = "\x1b[32m".to_string();
help_subcolor_one = "\x1b[36m".to_string();
help_subcolor_two = "\x1b[94m".to_string();
}
const RESET: &str = "\x1b[0m"; // reset
const D: &str = "\x1b[39m"; // default
let mut long_desc = String::new();
let _ = write!(long_desc, "\n{help_section_name}Flags{RESET}:\n");
for flag in &signature.named {
let default_str = if let Some(value) = &flag.default_value {
format!(
" (default: {help_subcolor_two}{}{RESET})",
&value_formatter(value)
)
} else {
"".to_string()
};
let msg = if let Some(arg) = &flag.arg {
if let Some(short) = flag.short {
if flag.required {
format!(
" {help_subcolor_one}-{}{}{RESET} (required parameter) {:?} - {}{}\n",
short,
if !flag.long.is_empty() {
format!("{D},{RESET} {help_subcolor_one}--{}", flag.long)
} else {
"".into()
},
arg,
flag.desc,
default_str,
)
} else {
format!(
" {help_subcolor_one}-{}{}{RESET} <{help_subcolor_two}{:?}{RESET}> - {}{}\n",
short,
if !flag.long.is_empty() {
format!("{D},{RESET} {help_subcolor_one}--{}", flag.long)
} else {
"".into()
},
arg,
flag.desc,
default_str,
)
}
} else if flag.required {
format!(
" {help_subcolor_one}--{}{RESET} (required parameter) <{help_subcolor_two}{:?}{RESET}> - {}{}\n",
flag.long, arg, flag.desc, default_str,
)
} else {
format!(
" {help_subcolor_one}--{}{RESET} <{help_subcolor_two}{:?}{RESET}> - {}{}\n",
flag.long, arg, flag.desc, default_str,
)
}
} else if let Some(short) = flag.short {
if flag.required {
format!(
" {help_subcolor_one}-{}{}{RESET} (required parameter) - {}{}\n",
short,
if !flag.long.is_empty() {
format!("{D},{RESET} {help_subcolor_one}--{}", flag.long)
} else {
"".into()
},
flag.desc,
default_str,
)
} else {
format!(
" {help_subcolor_one}-{}{}{RESET} - {}{}\n",
short,
if !flag.long.is_empty() {
format!("{D},{RESET} {help_subcolor_one}--{}", flag.long)
} else {
"".into()
},
flag.desc,
default_str
)
}
} else if flag.required {
format!(
" {help_subcolor_one}--{}{RESET} (required parameter) - {}{}\n",
flag.long, flag.desc, default_str,
)
} else {
format!(
" {help_subcolor_one}--{}{RESET} - {}\n",
flag.long, flag.desc
)
};
long_desc.push_str(&msg);
}
long_desc
}