mirror of
https://github.com/nushell/nushell.git
synced 2024-11-22 00:13:21 +01:00
Expand Nushell's help system (#7611)
This commit is contained in:
parent
f3d2be7a56
commit
8bfcea8054
@ -1,17 +1,18 @@
|
||||
use crate::help_aliases::help_aliases;
|
||||
use crate::help_commands::help_commands;
|
||||
use crate::help_modules::help_modules;
|
||||
use fancy_regex::Regex;
|
||||
use nu_ansi_term::{
|
||||
Color::{Red, White},
|
||||
Style,
|
||||
};
|
||||
use nu_color_config::StyleComputer;
|
||||
use nu_engine::{get_full_help, CallExt};
|
||||
use nu_engine::CallExt;
|
||||
use nu_protocol::{
|
||||
ast::Call,
|
||||
engine::{Command, EngineState, Stack},
|
||||
span, Category, Example, IntoInterruptiblePipelineData, IntoPipelineData, PipelineData,
|
||||
ShellError, Signature, Span, Spanned, SyntaxShape, Type, Value,
|
||||
span, Category, Example, IntoPipelineData, PipelineData, ShellError, Signature, Span, Spanned,
|
||||
SyntaxShape, Type, Value,
|
||||
};
|
||||
use std::borrow::Borrow;
|
||||
#[derive(Clone)]
|
||||
pub struct Help;
|
||||
|
||||
@ -26,7 +27,7 @@ impl Command for Help {
|
||||
.rest(
|
||||
"rest",
|
||||
SyntaxShape::String,
|
||||
"the name of command to get help on",
|
||||
"the name of command, alias or module to get help on",
|
||||
)
|
||||
.named(
|
||||
"find",
|
||||
@ -38,7 +39,11 @@ impl Command for Help {
|
||||
}
|
||||
|
||||
fn usage(&self) -> &str {
|
||||
"Display help information about commands."
|
||||
"Display help information about different parts of Nushell."
|
||||
}
|
||||
|
||||
fn extra_usage(&self) -> &str {
|
||||
r#"`help word` searches for "word" in commands, aliases and modules, in that order."#
|
||||
}
|
||||
|
||||
fn run(
|
||||
@ -48,269 +53,18 @@ impl Command for Help {
|
||||
call: &Call,
|
||||
_input: PipelineData,
|
||||
) -> Result<PipelineData, ShellError> {
|
||||
help(engine_state, stack, call)
|
||||
}
|
||||
let head = call.head;
|
||||
let find: Option<Spanned<String>> = call.get_flag(engine_state, stack, "find")?;
|
||||
let rest: Vec<Spanned<String>> = call.rest(engine_state, stack, 0)?;
|
||||
|
||||
fn examples(&self) -> Vec<Example> {
|
||||
vec![
|
||||
Example {
|
||||
description: "show all commands and sub-commands",
|
||||
example: "help commands",
|
||||
result: None,
|
||||
},
|
||||
Example {
|
||||
description: "show help for single command",
|
||||
example: "help match",
|
||||
result: None,
|
||||
},
|
||||
Example {
|
||||
description: "show help for single sub-command",
|
||||
example: "help str lpad",
|
||||
result: None,
|
||||
},
|
||||
Example {
|
||||
description: "search for string in command names, usage and search terms",
|
||||
example: "help --find char",
|
||||
result: None,
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
fn help(
|
||||
engine_state: &EngineState,
|
||||
stack: &mut Stack,
|
||||
call: &Call,
|
||||
) -> Result<PipelineData, ShellError> {
|
||||
let head = call.head;
|
||||
let find: Option<Spanned<String>> = call.get_flag(engine_state, stack, "find")?;
|
||||
let rest: Vec<Spanned<String>> = call.rest(engine_state, stack, 0)?;
|
||||
let commands = engine_state.get_decl_ids_sorted(false);
|
||||
|
||||
// 🚩The following two-lines are copied from filters/find.rs:
|
||||
let style_computer = StyleComputer::from_config(engine_state, stack);
|
||||
// Currently, search results all use the same style.
|
||||
// Also note that this sample string is passed into user-written code (the closure that may or may not be
|
||||
// defined for "string").
|
||||
let string_style = style_computer.compute("string", &Value::string("search result", head));
|
||||
|
||||
if let Some(f) = find {
|
||||
let org_search_string = f.item.clone();
|
||||
let search_string = f.item.to_lowercase();
|
||||
let mut found_cmds_vec = Vec::new();
|
||||
|
||||
for decl_id in commands {
|
||||
let mut cols = vec![];
|
||||
let mut vals = vec![];
|
||||
let decl = engine_state.get_decl(decl_id);
|
||||
let sig = decl.signature().update_from_command(decl.borrow());
|
||||
let signatures = sig.to_string();
|
||||
let key = sig.name;
|
||||
let usage = sig.usage;
|
||||
let search_terms = sig.search_terms;
|
||||
|
||||
let matches_term = if !search_terms.is_empty() {
|
||||
search_terms
|
||||
.iter()
|
||||
.any(|term| term.to_lowercase().contains(&search_string))
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
let key_match = key.to_lowercase().contains(&search_string);
|
||||
let usage_match = usage.to_lowercase().contains(&search_string);
|
||||
if key_match || usage_match || matches_term {
|
||||
cols.push("name".into());
|
||||
vals.push(Value::String {
|
||||
val: if key_match {
|
||||
highlight_search_string(&key, &org_search_string, &string_style)?
|
||||
} else {
|
||||
key
|
||||
},
|
||||
span: head,
|
||||
});
|
||||
|
||||
cols.push("category".into());
|
||||
vals.push(Value::string(sig.category.to_string(), head));
|
||||
|
||||
cols.push("command_type".into());
|
||||
vals.push(Value::String {
|
||||
val: format!("{:?}", decl.command_type()).to_lowercase(),
|
||||
span: head,
|
||||
});
|
||||
|
||||
cols.push("usage".into());
|
||||
vals.push(Value::String {
|
||||
val: if usage_match {
|
||||
highlight_search_string(&usage, &org_search_string, &string_style)?
|
||||
} else {
|
||||
usage
|
||||
},
|
||||
span: head,
|
||||
});
|
||||
|
||||
cols.push("signatures".into());
|
||||
vals.push(Value::String {
|
||||
val: if decl.is_parser_keyword() {
|
||||
"".to_string()
|
||||
} else {
|
||||
signatures
|
||||
},
|
||||
span: head,
|
||||
});
|
||||
|
||||
cols.push("search_terms".into());
|
||||
vals.push(if search_terms.is_empty() {
|
||||
Value::nothing(head)
|
||||
} else {
|
||||
Value::String {
|
||||
val: if matches_term {
|
||||
search_terms
|
||||
.iter()
|
||||
.map(|term| {
|
||||
if term.to_lowercase().contains(&search_string) {
|
||||
match highlight_search_string(
|
||||
term,
|
||||
&org_search_string,
|
||||
&string_style,
|
||||
) {
|
||||
Ok(s) => s,
|
||||
Err(_) => {
|
||||
string_style.paint(term.to_string()).to_string()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
string_style.paint(term.to_string()).to_string()
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
} else {
|
||||
search_terms.join(", ")
|
||||
},
|
||||
span: head,
|
||||
}
|
||||
});
|
||||
|
||||
found_cmds_vec.push(Value::Record {
|
||||
cols,
|
||||
vals,
|
||||
span: head,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return Ok(found_cmds_vec
|
||||
.into_iter()
|
||||
.into_pipeline_data(engine_state.ctrlc.clone()));
|
||||
}
|
||||
|
||||
if !rest.is_empty() {
|
||||
let mut found_cmds_vec = Vec::new();
|
||||
|
||||
if rest[0].item == "commands" {
|
||||
for decl_id in commands {
|
||||
let mut cols = vec![];
|
||||
let mut vals = vec![];
|
||||
|
||||
let decl = engine_state.get_decl(decl_id);
|
||||
let sig = decl.signature().update_from_command(decl.borrow());
|
||||
|
||||
let signatures = sig.to_string();
|
||||
let key = sig.name;
|
||||
let usage = sig.usage;
|
||||
let search_terms = sig.search_terms;
|
||||
|
||||
cols.push("name".into());
|
||||
vals.push(Value::String {
|
||||
val: key,
|
||||
span: head,
|
||||
});
|
||||
|
||||
cols.push("category".into());
|
||||
vals.push(Value::string(sig.category.to_string(), head));
|
||||
|
||||
cols.push("command_type".into());
|
||||
vals.push(Value::String {
|
||||
val: format!("{:?}", decl.command_type()).to_lowercase(),
|
||||
span: head,
|
||||
});
|
||||
|
||||
cols.push("usage".into());
|
||||
vals.push(Value::String {
|
||||
val: usage,
|
||||
span: head,
|
||||
});
|
||||
|
||||
cols.push("signatures".into());
|
||||
vals.push(Value::String {
|
||||
val: if decl.is_parser_keyword() {
|
||||
"".to_string()
|
||||
} else {
|
||||
signatures
|
||||
},
|
||||
span: head,
|
||||
});
|
||||
|
||||
cols.push("search_terms".into());
|
||||
vals.push(if search_terms.is_empty() {
|
||||
Value::nothing(head)
|
||||
} else {
|
||||
Value::String {
|
||||
val: search_terms.join(", "),
|
||||
span: head,
|
||||
}
|
||||
});
|
||||
|
||||
found_cmds_vec.push(Value::Record {
|
||||
cols,
|
||||
vals,
|
||||
span: head,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(found_cmds_vec
|
||||
.into_iter()
|
||||
.into_pipeline_data(engine_state.ctrlc.clone()))
|
||||
} else {
|
||||
let mut name = String::new();
|
||||
|
||||
for r in &rest {
|
||||
if !name.is_empty() {
|
||||
name.push(' ');
|
||||
}
|
||||
name.push_str(&r.item);
|
||||
}
|
||||
|
||||
let output = engine_state
|
||||
.get_signatures_with_examples(false)
|
||||
.iter()
|
||||
.filter(|(signature, _, _, _, _)| signature.name == name)
|
||||
.map(|(signature, examples, _, _, is_parser_keyword)| {
|
||||
get_full_help(signature, examples, engine_state, stack, *is_parser_keyword)
|
||||
})
|
||||
.collect::<Vec<String>>();
|
||||
|
||||
if !output.is_empty() {
|
||||
Ok(Value::String {
|
||||
val: output.join("======================\n\n"),
|
||||
span: call.head,
|
||||
}
|
||||
.into_pipeline_data())
|
||||
} else {
|
||||
Err(ShellError::CommandNotFound(span(&[
|
||||
rest[0].span,
|
||||
rest[rest.len() - 1].span,
|
||||
])))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let msg = r#"Welcome to Nushell.
|
||||
if rest.is_empty() && find.is_none() {
|
||||
let msg = r#"Welcome to Nushell.
|
||||
|
||||
Here are some tips to help you get started.
|
||||
* help -h or help help - show available `help` subcommands and examples
|
||||
* help commands - list all available commands
|
||||
* help <command name> - display help about a particular command
|
||||
* help --find <text to search> - search through all of help
|
||||
* help <name> - display help about a particular command, alias, or module
|
||||
* help --find <text to search> - search through all help commands table
|
||||
|
||||
Nushell works on the idea of a "pipeline". Pipelines are commands connected with the '|' character.
|
||||
Each stage in the pipeline works together to load, parse, and display information to you.
|
||||
@ -328,8 +82,111 @@ Get the processes on your system actively using CPU:
|
||||
|
||||
You can also learn more at https://www.nushell.sh/book/"#;
|
||||
|
||||
Ok(Value::string(msg, head).into_pipeline_data())
|
||||
Ok(Value::string(msg, head).into_pipeline_data())
|
||||
} else if find.is_some() {
|
||||
help_commands(engine_state, stack, call)
|
||||
} else {
|
||||
let result = help_commands(engine_state, stack, call);
|
||||
|
||||
let result = if let Err(ShellError::CommandNotFound(_)) = result {
|
||||
help_aliases(engine_state, stack, call)
|
||||
} else {
|
||||
result
|
||||
};
|
||||
|
||||
let result = if let Err(ShellError::AliasNotFound(_)) = result {
|
||||
help_modules(engine_state, stack, call)
|
||||
} else {
|
||||
result
|
||||
};
|
||||
|
||||
if let Err(ShellError::ModuleNotFoundAtRuntime(_, _)) = result {
|
||||
let rest_spans: Vec<Span> = rest.iter().map(|arg| arg.span).collect();
|
||||
Err(ShellError::NotFound(span(&rest_spans)))
|
||||
} else {
|
||||
result
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn examples(&self) -> Vec<Example> {
|
||||
vec![
|
||||
Example {
|
||||
description: "show help for single command, alias, or module",
|
||||
example: "help match",
|
||||
result: None,
|
||||
},
|
||||
Example {
|
||||
description: "show help for single sub-command, alias, or module",
|
||||
example: "help str lpad",
|
||||
result: None,
|
||||
},
|
||||
Example {
|
||||
description: "search for string in command names, usage and search terms",
|
||||
example: "help --find char",
|
||||
result: None,
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
pub fn highlight_search_in_table(
|
||||
table: Vec<Value>, // list of records
|
||||
search_string: &str,
|
||||
searched_cols: &[&str],
|
||||
string_style: &Style,
|
||||
) -> Result<Vec<Value>, ShellError> {
|
||||
let orig_search_string = search_string;
|
||||
let search_string = search_string.to_lowercase();
|
||||
let mut matches = vec![];
|
||||
|
||||
for record in table {
|
||||
let (cols, mut vals, record_span) = if let Value::Record { cols, vals, span } = record {
|
||||
(cols, vals, span)
|
||||
} else {
|
||||
return Err(ShellError::NushellFailedSpanned(
|
||||
"Expected record".to_string(),
|
||||
format!("got {}", record.get_type()),
|
||||
record.span()?,
|
||||
));
|
||||
};
|
||||
|
||||
let has_match = cols.iter().zip(vals.iter_mut()).fold(
|
||||
Ok(false),
|
||||
|acc: Result<bool, ShellError>, (col, val)| {
|
||||
if searched_cols.contains(&col.as_str()) {
|
||||
if let Value::String { val: s, span } = val {
|
||||
if s.to_lowercase().contains(&search_string) {
|
||||
*val = Value::String {
|
||||
val: highlight_search_string(s, orig_search_string, string_style)?,
|
||||
span: *span,
|
||||
};
|
||||
Ok(true)
|
||||
} else {
|
||||
// column does not contain the searched string
|
||||
acc
|
||||
}
|
||||
} else {
|
||||
// ignore non-string values
|
||||
acc
|
||||
}
|
||||
} else {
|
||||
// don't search this column
|
||||
acc
|
||||
}
|
||||
},
|
||||
)?;
|
||||
|
||||
if has_match {
|
||||
matches.push(Value::Record {
|
||||
cols,
|
||||
vals,
|
||||
span: record_span,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Ok(matches)
|
||||
}
|
||||
|
||||
// Highlight the search string using ANSI escape sequences and regular expressions.
|
||||
|
181
crates/nu-command/src/core_commands/help_aliases.rs
Normal file
181
crates/nu-command/src/core_commands/help_aliases.rs
Normal file
@ -0,0 +1,181 @@
|
||||
use crate::help::highlight_search_in_table;
|
||||
use nu_color_config::StyleComputer;
|
||||
use nu_engine::{scope::ScopeData, CallExt};
|
||||
use nu_protocol::{
|
||||
ast::Call,
|
||||
engine::{Command, EngineState, Stack},
|
||||
span, Category, Example, IntoInterruptiblePipelineData, IntoPipelineData, PipelineData,
|
||||
ShellError, Signature, Span, Spanned, SyntaxShape, Type, Value,
|
||||
};
|
||||
use std::borrow::Cow;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct HelpAliases;
|
||||
|
||||
impl Command for HelpAliases {
|
||||
fn name(&self) -> &str {
|
||||
"help aliases"
|
||||
}
|
||||
|
||||
fn usage(&self) -> &str {
|
||||
"Show help on nushell aliases."
|
||||
}
|
||||
|
||||
fn signature(&self) -> Signature {
|
||||
Signature::build("help aliases")
|
||||
.category(Category::Core)
|
||||
.rest(
|
||||
"rest",
|
||||
SyntaxShape::String,
|
||||
"the name of alias to get help on",
|
||||
)
|
||||
.named(
|
||||
"find",
|
||||
SyntaxShape::String,
|
||||
"string to find in alias names and usage",
|
||||
Some('f'),
|
||||
)
|
||||
.input_output_types(vec![(Type::Nothing, Type::Table(vec![]))])
|
||||
.allow_variants_without_examples(true)
|
||||
}
|
||||
|
||||
fn examples(&self) -> Vec<Example> {
|
||||
vec![
|
||||
Example {
|
||||
description: "show all aliases",
|
||||
example: "help aliases",
|
||||
result: None,
|
||||
},
|
||||
Example {
|
||||
description: "show help for single alias",
|
||||
example: "help aliases my-alias",
|
||||
result: None,
|
||||
},
|
||||
Example {
|
||||
description: "search for string in alias names and usages",
|
||||
example: "help aliases --find my-alias",
|
||||
result: None,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
fn run(
|
||||
&self,
|
||||
engine_state: &EngineState,
|
||||
stack: &mut Stack,
|
||||
call: &Call,
|
||||
_input: PipelineData,
|
||||
) -> Result<PipelineData, ShellError> {
|
||||
help_aliases(engine_state, stack, call)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn help_aliases(
|
||||
engine_state: &EngineState,
|
||||
stack: &mut Stack,
|
||||
call: &Call,
|
||||
) -> Result<PipelineData, ShellError> {
|
||||
let head = call.head;
|
||||
let find: Option<Spanned<String>> = call.get_flag(engine_state, stack, "find")?;
|
||||
let rest: Vec<Spanned<String>> = call.rest(engine_state, stack, 0)?;
|
||||
|
||||
// 🚩The following two-lines are copied from filters/find.rs:
|
||||
let style_computer = StyleComputer::from_config(engine_state, stack);
|
||||
// Currently, search results all use the same style.
|
||||
// Also note that this sample string is passed into user-written code (the closure that may or may not be
|
||||
// defined for "string").
|
||||
let string_style = style_computer.compute("string", &Value::string("search result", head));
|
||||
|
||||
if let Some(f) = find {
|
||||
let all_cmds_vec = build_help_aliases(engine_state, stack, head);
|
||||
let found_cmds_vec =
|
||||
highlight_search_in_table(all_cmds_vec, &f.item, &["name", "usage"], &string_style)?;
|
||||
|
||||
return Ok(found_cmds_vec
|
||||
.into_iter()
|
||||
.into_pipeline_data(engine_state.ctrlc.clone()));
|
||||
}
|
||||
|
||||
if rest.is_empty() {
|
||||
let found_cmds_vec = build_help_aliases(engine_state, stack, head);
|
||||
|
||||
Ok(found_cmds_vec
|
||||
.into_iter()
|
||||
.into_pipeline_data(engine_state.ctrlc.clone()))
|
||||
} else {
|
||||
let mut name = String::new();
|
||||
|
||||
for r in &rest {
|
||||
if !name.is_empty() {
|
||||
name.push(' ');
|
||||
}
|
||||
name.push_str(&r.item);
|
||||
}
|
||||
|
||||
let alias_id = if let Some(id) = engine_state.find_alias(name.as_bytes(), &[]) {
|
||||
id
|
||||
} else {
|
||||
return Err(ShellError::AliasNotFound(span(
|
||||
&rest.iter().map(|r| r.span).collect::<Vec<Span>>(),
|
||||
)));
|
||||
};
|
||||
|
||||
let alias_expansion = engine_state
|
||||
.get_alias(alias_id)
|
||||
.iter()
|
||||
.map(|span| String::from_utf8_lossy(engine_state.get_span_contents(span)))
|
||||
.collect::<Vec<Cow<str>>>()
|
||||
.join(" ");
|
||||
|
||||
let alias_usage = engine_state.build_alias_usage(alias_id);
|
||||
|
||||
// TODO: merge this into documentation.rs at some point
|
||||
const G: &str = "\x1b[32m"; // green
|
||||
const C: &str = "\x1b[36m"; // cyan
|
||||
const RESET: &str = "\x1b[0m"; // reset
|
||||
|
||||
let mut long_desc = String::new();
|
||||
|
||||
if let Some((usage, extra_usage)) = alias_usage {
|
||||
long_desc.push_str(&usage);
|
||||
long_desc.push_str("\n\n");
|
||||
|
||||
if !extra_usage.is_empty() {
|
||||
long_desc.push_str(&extra_usage);
|
||||
long_desc.push_str("\n\n");
|
||||
}
|
||||
}
|
||||
|
||||
long_desc.push_str(&format!("{G}Alias{RESET}: {C}{name}{RESET}"));
|
||||
long_desc.push_str("\n\n");
|
||||
long_desc.push_str(&format!("{G}Expansion{RESET}:\n {alias_expansion}"));
|
||||
|
||||
let config = engine_state.get_config();
|
||||
if !config.use_ansi_coloring {
|
||||
long_desc = nu_utils::strip_ansi_string_likely(long_desc);
|
||||
}
|
||||
|
||||
Ok(Value::String {
|
||||
val: long_desc,
|
||||
span: call.head,
|
||||
}
|
||||
.into_pipeline_data())
|
||||
}
|
||||
}
|
||||
|
||||
fn build_help_aliases(engine_state: &EngineState, stack: &Stack, span: Span) -> Vec<Value> {
|
||||
let mut scope_data = ScopeData::new(engine_state, stack);
|
||||
scope_data.populate_aliases();
|
||||
|
||||
scope_data.collect_aliases(span)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
#[test]
|
||||
fn test_examples() {
|
||||
use super::HelpAliases;
|
||||
use crate::test_examples;
|
||||
test_examples(HelpAliases {})
|
||||
}
|
||||
}
|
189
crates/nu-command/src/core_commands/help_commands.rs
Normal file
189
crates/nu-command/src/core_commands/help_commands.rs
Normal file
@ -0,0 +1,189 @@
|
||||
use crate::help::highlight_search_in_table;
|
||||
use nu_color_config::StyleComputer;
|
||||
use nu_engine::{get_full_help, CallExt};
|
||||
use nu_protocol::{
|
||||
ast::Call,
|
||||
engine::{Command, EngineState, Stack},
|
||||
span, Category, IntoInterruptiblePipelineData, IntoPipelineData, PipelineData, ShellError,
|
||||
Signature, Span, Spanned, SyntaxShape, Type, Value,
|
||||
};
|
||||
use std::borrow::Borrow;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct HelpCommands;
|
||||
|
||||
impl Command for HelpCommands {
|
||||
fn name(&self) -> &str {
|
||||
"help commands"
|
||||
}
|
||||
|
||||
fn usage(&self) -> &str {
|
||||
"Show help on nushell commands."
|
||||
}
|
||||
|
||||
fn signature(&self) -> Signature {
|
||||
Signature::build("help commands")
|
||||
.category(Category::Core)
|
||||
.rest(
|
||||
"rest",
|
||||
SyntaxShape::String,
|
||||
"the name of command to get help on",
|
||||
)
|
||||
.named(
|
||||
"find",
|
||||
SyntaxShape::String,
|
||||
"string to find in command names, usage, and search terms",
|
||||
Some('f'),
|
||||
)
|
||||
.input_output_types(vec![(Type::Nothing, Type::Table(vec![]))])
|
||||
.allow_variants_without_examples(true)
|
||||
}
|
||||
|
||||
fn run(
|
||||
&self,
|
||||
engine_state: &EngineState,
|
||||
stack: &mut Stack,
|
||||
call: &Call,
|
||||
_input: PipelineData,
|
||||
) -> Result<PipelineData, ShellError> {
|
||||
help_commands(engine_state, stack, call)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn help_commands(
|
||||
engine_state: &EngineState,
|
||||
stack: &mut Stack,
|
||||
call: &Call,
|
||||
) -> Result<PipelineData, ShellError> {
|
||||
let head = call.head;
|
||||
let find: Option<Spanned<String>> = call.get_flag(engine_state, stack, "find")?;
|
||||
let rest: Vec<Spanned<String>> = call.rest(engine_state, stack, 0)?;
|
||||
|
||||
// 🚩The following two-lines are copied from filters/find.rs:
|
||||
let style_computer = StyleComputer::from_config(engine_state, stack);
|
||||
// Currently, search results all use the same style.
|
||||
// Also note that this sample string is passed into user-written code (the closure that may or may not be
|
||||
// defined for "string").
|
||||
let string_style = style_computer.compute("string", &Value::string("search result", head));
|
||||
|
||||
if let Some(f) = find {
|
||||
let all_cmds_vec = build_help_commands(engine_state, head);
|
||||
let found_cmds_vec = highlight_search_in_table(
|
||||
all_cmds_vec,
|
||||
&f.item,
|
||||
&["name", "usage", "search_terms"],
|
||||
&string_style,
|
||||
)?;
|
||||
|
||||
return Ok(found_cmds_vec
|
||||
.into_iter()
|
||||
.into_pipeline_data(engine_state.ctrlc.clone()));
|
||||
}
|
||||
|
||||
if rest.is_empty() {
|
||||
let found_cmds_vec = build_help_commands(engine_state, head);
|
||||
|
||||
Ok(found_cmds_vec
|
||||
.into_iter()
|
||||
.into_pipeline_data(engine_state.ctrlc.clone()))
|
||||
} else {
|
||||
let mut name = String::new();
|
||||
|
||||
for r in &rest {
|
||||
if !name.is_empty() {
|
||||
name.push(' ');
|
||||
}
|
||||
name.push_str(&r.item);
|
||||
}
|
||||
|
||||
let output = engine_state
|
||||
.get_signatures_with_examples(false)
|
||||
.iter()
|
||||
.filter(|(signature, _, _, _, _)| signature.name == name)
|
||||
.map(|(signature, examples, _, _, is_parser_keyword)| {
|
||||
get_full_help(signature, examples, engine_state, stack, *is_parser_keyword)
|
||||
})
|
||||
.collect::<Vec<String>>();
|
||||
|
||||
if !output.is_empty() {
|
||||
Ok(Value::String {
|
||||
val: output.join("======================\n\n"),
|
||||
span: call.head,
|
||||
}
|
||||
.into_pipeline_data())
|
||||
} else {
|
||||
Err(ShellError::CommandNotFound(span(&[
|
||||
rest[0].span,
|
||||
rest[rest.len() - 1].span,
|
||||
])))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn build_help_commands(engine_state: &EngineState, span: Span) -> Vec<Value> {
|
||||
let commands = engine_state.get_decls_sorted(false);
|
||||
let mut found_cmds_vec = Vec::new();
|
||||
|
||||
for (name_bytes, decl_id) in commands {
|
||||
let mut cols = vec![];
|
||||
let mut vals = vec![];
|
||||
|
||||
let name = String::from_utf8_lossy(&name_bytes).to_string();
|
||||
let decl = engine_state.get_decl(decl_id);
|
||||
let sig = decl.signature().update_from_command(name, decl.borrow());
|
||||
|
||||
let signatures = sig.to_string();
|
||||
let key = sig.name;
|
||||
let usage = sig.usage;
|
||||
let search_terms = sig.search_terms;
|
||||
|
||||
cols.push("name".into());
|
||||
vals.push(Value::String { val: key, span });
|
||||
|
||||
cols.push("category".into());
|
||||
vals.push(Value::string(sig.category.to_string(), span));
|
||||
|
||||
cols.push("command_type".into());
|
||||
vals.push(Value::String {
|
||||
val: format!("{:?}", decl.command_type()).to_lowercase(),
|
||||
span,
|
||||
});
|
||||
|
||||
cols.push("usage".into());
|
||||
vals.push(Value::String { val: usage, span });
|
||||
|
||||
cols.push("signatures".into());
|
||||
vals.push(Value::String {
|
||||
val: if decl.is_parser_keyword() {
|
||||
"".to_string()
|
||||
} else {
|
||||
signatures
|
||||
},
|
||||
span,
|
||||
});
|
||||
|
||||
cols.push("search_terms".into());
|
||||
vals.push(if search_terms.is_empty() {
|
||||
Value::nothing(span)
|
||||
} else {
|
||||
Value::String {
|
||||
val: search_terms.join(", "),
|
||||
span,
|
||||
}
|
||||
});
|
||||
|
||||
found_cmds_vec.push(Value::Record { cols, vals, span });
|
||||
}
|
||||
|
||||
found_cmds_vec
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
#[test]
|
||||
fn test_examples() {
|
||||
use super::HelpCommands;
|
||||
use crate::test_examples;
|
||||
test_examples(HelpCommands {})
|
||||
}
|
||||
}
|
258
crates/nu-command/src/core_commands/help_modules.rs
Normal file
258
crates/nu-command/src/core_commands/help_modules.rs
Normal file
@ -0,0 +1,258 @@
|
||||
use crate::help::highlight_search_in_table;
|
||||
use nu_color_config::StyleComputer;
|
||||
use nu_engine::{scope::ScopeData, CallExt};
|
||||
use nu_protocol::{
|
||||
ast::Call,
|
||||
engine::{Command, EngineState, Stack},
|
||||
span, AliasId, Category, DeclId, Example, IntoInterruptiblePipelineData, IntoPipelineData,
|
||||
PipelineData, ShellError, Signature, Span, Spanned, SyntaxShape, Type, Value,
|
||||
};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct HelpModules;
|
||||
|
||||
impl Command for HelpModules {
|
||||
fn name(&self) -> &str {
|
||||
"help modules"
|
||||
}
|
||||
|
||||
fn usage(&self) -> &str {
|
||||
"Show help on nushell modules."
|
||||
}
|
||||
|
||||
fn extra_usage(&self) -> &str {
|
||||
r#"When requesting help for a single module, its commands and aliases will be highlighted if they
|
||||
are also available in the current scope. Commands/aliases that were imported under a different name
|
||||
(such as with a prefix after `use some-module`) will be highlighted in parentheses."#
|
||||
}
|
||||
|
||||
fn signature(&self) -> Signature {
|
||||
Signature::build("help modules")
|
||||
.category(Category::Core)
|
||||
.rest(
|
||||
"rest",
|
||||
SyntaxShape::String,
|
||||
"the name of module to get help on",
|
||||
)
|
||||
.named(
|
||||
"find",
|
||||
SyntaxShape::String,
|
||||
"string to find in module names and usage",
|
||||
Some('f'),
|
||||
)
|
||||
.input_output_types(vec![(Type::Nothing, Type::Table(vec![]))])
|
||||
.allow_variants_without_examples(true)
|
||||
}
|
||||
|
||||
fn examples(&self) -> Vec<Example> {
|
||||
vec![
|
||||
Example {
|
||||
description: "show all modules",
|
||||
example: "help modules",
|
||||
result: None,
|
||||
},
|
||||
Example {
|
||||
description: "show help for single module",
|
||||
example: "help modules my-module",
|
||||
result: None,
|
||||
},
|
||||
Example {
|
||||
description: "search for string in module names and usages",
|
||||
example: "help modules --find my-module",
|
||||
result: None,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
fn run(
|
||||
&self,
|
||||
engine_state: &EngineState,
|
||||
stack: &mut Stack,
|
||||
call: &Call,
|
||||
_input: PipelineData,
|
||||
) -> Result<PipelineData, ShellError> {
|
||||
help_modules(engine_state, stack, call)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn help_modules(
|
||||
engine_state: &EngineState,
|
||||
stack: &mut Stack,
|
||||
call: &Call,
|
||||
) -> Result<PipelineData, ShellError> {
|
||||
let head = call.head;
|
||||
let find: Option<Spanned<String>> = call.get_flag(engine_state, stack, "find")?;
|
||||
let rest: Vec<Spanned<String>> = call.rest(engine_state, stack, 0)?;
|
||||
|
||||
// 🚩The following two-lines are copied from filters/find.rs:
|
||||
let style_computer = StyleComputer::from_config(engine_state, stack);
|
||||
// Currently, search results all use the same style.
|
||||
// Also note that this sample string is passed into user-written code (the closure that may or may not be
|
||||
// defined for "string").
|
||||
let string_style = style_computer.compute("string", &Value::string("search result", head));
|
||||
|
||||
if let Some(f) = find {
|
||||
let all_cmds_vec = build_help_modules(engine_state, stack, head);
|
||||
let found_cmds_vec =
|
||||
highlight_search_in_table(all_cmds_vec, &f.item, &["name", "usage"], &string_style)?;
|
||||
|
||||
return Ok(found_cmds_vec
|
||||
.into_iter()
|
||||
.into_pipeline_data(engine_state.ctrlc.clone()));
|
||||
}
|
||||
|
||||
if rest.is_empty() {
|
||||
let found_cmds_vec = build_help_modules(engine_state, stack, head);
|
||||
|
||||
Ok(found_cmds_vec
|
||||
.into_iter()
|
||||
.into_pipeline_data(engine_state.ctrlc.clone()))
|
||||
} else {
|
||||
let mut name = String::new();
|
||||
|
||||
for r in &rest {
|
||||
if !name.is_empty() {
|
||||
name.push(' ');
|
||||
}
|
||||
name.push_str(&r.item);
|
||||
}
|
||||
|
||||
let module_id = if let Some(id) = engine_state.find_module(name.as_bytes(), &[]) {
|
||||
id
|
||||
} else {
|
||||
return Err(ShellError::ModuleNotFoundAtRuntime(
|
||||
name,
|
||||
span(&rest.iter().map(|r| r.span).collect::<Vec<Span>>()),
|
||||
));
|
||||
};
|
||||
|
||||
let module = engine_state.get_module(module_id);
|
||||
|
||||
let module_usage = engine_state.build_module_usage(module_id);
|
||||
|
||||
// TODO: merge this into documentation.rs at some point
|
||||
const G: &str = "\x1b[32m"; // green
|
||||
const C: &str = "\x1b[36m"; // cyan
|
||||
const CB: &str = "\x1b[1;36m"; // cyan bold
|
||||
const RESET: &str = "\x1b[0m"; // reset
|
||||
|
||||
let mut long_desc = String::new();
|
||||
|
||||
if let Some((usage, extra_usage)) = module_usage {
|
||||
long_desc.push_str(&usage);
|
||||
long_desc.push_str("\n\n");
|
||||
|
||||
if !extra_usage.is_empty() {
|
||||
long_desc.push_str(&extra_usage);
|
||||
long_desc.push_str("\n\n");
|
||||
}
|
||||
}
|
||||
|
||||
long_desc.push_str(&format!("{G}Module{RESET}: {C}{name}{RESET}"));
|
||||
long_desc.push_str("\n\n");
|
||||
|
||||
if !module.decls.is_empty() {
|
||||
let commands: Vec<(Vec<u8>, DeclId)> = engine_state.get_decls_sorted(false).collect();
|
||||
|
||||
let mut module_commands: Vec<(&[u8], DeclId)> = module
|
||||
.decls
|
||||
.iter()
|
||||
.map(|(name, id)| (name.as_ref(), *id))
|
||||
.collect();
|
||||
module_commands.sort_by(|a, b| a.0.cmp(b.0));
|
||||
|
||||
let commands_str = module_commands
|
||||
.iter()
|
||||
.map(|(name_bytes, id)| {
|
||||
let name = String::from_utf8_lossy(name_bytes);
|
||||
if let Some((used_name_bytes, _)) =
|
||||
commands.iter().find(|(_, decl_id)| id == decl_id)
|
||||
{
|
||||
if engine_state.find_decl(name.as_bytes(), &[]).is_some() {
|
||||
format!("{CB}{name}{RESET}")
|
||||
} else {
|
||||
let command_name = String::from_utf8_lossy(used_name_bytes);
|
||||
format!("{name} ({CB}{command_name}{RESET})")
|
||||
}
|
||||
} else {
|
||||
format!("{name}")
|
||||
}
|
||||
})
|
||||
.collect::<Vec<String>>()
|
||||
.join(", ");
|
||||
|
||||
long_desc.push_str(&format!("{G}Exported commands{RESET}:\n {commands_str}"));
|
||||
long_desc.push_str("\n\n");
|
||||
}
|
||||
|
||||
if !module.aliases.is_empty() {
|
||||
let aliases: Vec<(Vec<u8>, AliasId)> = engine_state.get_aliases_sorted(false).collect();
|
||||
|
||||
let mut module_aliases: Vec<(&[u8], AliasId)> = module
|
||||
.aliases
|
||||
.iter()
|
||||
.map(|(name, id)| (name.as_ref(), *id))
|
||||
.collect();
|
||||
module_aliases.sort_by(|a, b| a.0.cmp(b.0));
|
||||
|
||||
let aliases_str = module_aliases
|
||||
.iter()
|
||||
.map(|(name_bytes, id)| {
|
||||
let name = String::from_utf8_lossy(name_bytes);
|
||||
if let Some((used_name_bytes, _)) =
|
||||
aliases.iter().find(|(_, alias_id)| id == alias_id)
|
||||
{
|
||||
if engine_state.find_alias(name.as_bytes(), &[]).is_some() {
|
||||
format!("{CB}{name}{RESET}")
|
||||
} else {
|
||||
let alias_name = String::from_utf8_lossy(used_name_bytes);
|
||||
format!("{name} ({CB}{alias_name}{RESET})")
|
||||
}
|
||||
} else {
|
||||
format!("{name}")
|
||||
}
|
||||
})
|
||||
.collect::<Vec<String>>()
|
||||
.join(", ");
|
||||
|
||||
long_desc.push_str(&format!("{G}Exported aliases{RESET}:\n {aliases_str}"));
|
||||
long_desc.push_str("\n\n");
|
||||
}
|
||||
|
||||
if module.env_block.is_some() {
|
||||
long_desc.push_str(&format!("This module {C}exports{RESET} environment."));
|
||||
} else {
|
||||
long_desc.push_str(&format!(
|
||||
"This module {C}does not export{RESET} environment."
|
||||
));
|
||||
}
|
||||
|
||||
let config = engine_state.get_config();
|
||||
if !config.use_ansi_coloring {
|
||||
long_desc = nu_utils::strip_ansi_string_likely(long_desc);
|
||||
}
|
||||
|
||||
Ok(Value::String {
|
||||
val: long_desc,
|
||||
span: call.head,
|
||||
}
|
||||
.into_pipeline_data())
|
||||
}
|
||||
}
|
||||
|
||||
fn build_help_modules(engine_state: &EngineState, stack: &Stack, span: Span) -> Vec<Value> {
|
||||
let mut scope_data = ScopeData::new(engine_state, stack);
|
||||
scope_data.populate_modules();
|
||||
|
||||
scope_data.collect_modules(span)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
#[test]
|
||||
fn test_examples() {
|
||||
use super::HelpModules;
|
||||
use crate::test_examples;
|
||||
test_examples(HelpModules {})
|
||||
}
|
||||
}
|
@ -20,6 +20,9 @@ mod export_use;
|
||||
mod extern_;
|
||||
mod for_;
|
||||
pub mod help;
|
||||
pub mod help_aliases;
|
||||
pub mod help_commands;
|
||||
pub mod help_modules;
|
||||
mod help_operators;
|
||||
mod hide;
|
||||
mod hide_env;
|
||||
@ -59,6 +62,9 @@ pub use export_use::ExportUse;
|
||||
pub use extern_::Extern;
|
||||
pub use for_::For;
|
||||
pub use help::Help;
|
||||
pub use help_aliases::HelpAliases;
|
||||
pub use help_commands::HelpCommands;
|
||||
pub use help_modules::HelpModules;
|
||||
pub use help_operators::HelpOperators;
|
||||
pub use hide::Hide;
|
||||
pub use hide_env::HideEnv;
|
||||
|
@ -50,6 +50,9 @@ pub fn create_default_context() -> EngineState {
|
||||
Extern,
|
||||
For,
|
||||
Help,
|
||||
HelpAliases,
|
||||
HelpCommands,
|
||||
HelpModules,
|
||||
HelpOperators,
|
||||
Hide,
|
||||
HideEnv,
|
||||
|
@ -307,7 +307,7 @@ fn parse_module(
|
||||
let end = working_set.next_span_start();
|
||||
|
||||
let new_span = Span::new(start, end);
|
||||
let (_, _, err) = parse_module_block(working_set, new_span, &[]);
|
||||
let (_, _, _, err) = parse_module_block(working_set, new_span, &[]);
|
||||
|
||||
if err.is_some() {
|
||||
if is_debug {
|
||||
|
@ -296,16 +296,25 @@ impl ExternalCommand {
|
||||
"'{}' was not found; did you mean '{s}'?",
|
||||
self.name.item
|
||||
)
|
||||
} else if self.name.item == s {
|
||||
let sugg = engine_state.which_module_has_decl(s.as_bytes());
|
||||
if let Some(sugg) = sugg {
|
||||
let sugg = String::from_utf8_lossy(sugg);
|
||||
format!("command '{s}' was not found but it exists in module '{sugg}'; try using `{sugg} {s}`")
|
||||
} else {
|
||||
let cmd_name = &self.name.item;
|
||||
let maybe_module = engine_state
|
||||
.which_module_has_decl(cmd_name.as_bytes(), &[]);
|
||||
if let Some(module_name) = maybe_module {
|
||||
let module_name = String::from_utf8_lossy(module_name);
|
||||
let new_name = &[module_name.as_ref(), cmd_name].join(" ");
|
||||
|
||||
if engine_state
|
||||
.find_decl(new_name.as_bytes(), &[])
|
||||
.is_some()
|
||||
{
|
||||
format!("command '{cmd_name}' was not found but it was imported from module '{module_name}'; try using `{new_name}`")
|
||||
} else {
|
||||
format!("command '{cmd_name}' was not found but it exists in module '{module_name}'; try importing it with `use`")
|
||||
}
|
||||
} else {
|
||||
format!("did you mean '{s}'?")
|
||||
}
|
||||
} else {
|
||||
format!("did you mean '{s}'?")
|
||||
}
|
||||
}
|
||||
None => {
|
||||
|
@ -85,5 +85,5 @@ fn alias_alone_lists_aliases() {
|
||||
alias a = 3; alias
|
||||
"#
|
||||
));
|
||||
assert!(actual.out.contains("alias") && actual.out.contains("expansion"));
|
||||
assert!(actual.out.contains("name") && actual.out.contains("expansion"));
|
||||
}
|
||||
|
@ -1,9 +1,11 @@
|
||||
use nu_test_support::{nu, pipeline};
|
||||
use nu_test_support::fs::Stub::FileWithContent;
|
||||
use nu_test_support::playground::Playground;
|
||||
use nu_test_support::{nu, nu_repl_code, pipeline};
|
||||
|
||||
#[test]
|
||||
fn help_commands_length() {
|
||||
let actual = nu!(
|
||||
cwd: ".", pipeline(
|
||||
cwd: ".", pipeline(
|
||||
r#"
|
||||
help commands | length
|
||||
"#
|
||||
@ -26,3 +28,289 @@ fn help_shows_signature() {
|
||||
let actual = nu!(cwd: ".", pipeline("help alias"));
|
||||
assert!(!actual.out.contains("Signatures"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn help_aliases() {
|
||||
let code = &[
|
||||
"alias SPAM = print 'spam'",
|
||||
"help aliases | where name == SPAM | length",
|
||||
];
|
||||
let actual = nu!(cwd: ".", nu_repl_code(code));
|
||||
|
||||
assert_eq!(actual.out, "1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn help_alias_usage_1() {
|
||||
Playground::setup("help_alias_usage_1", |dirs, sandbox| {
|
||||
sandbox.with_files(vec![FileWithContent(
|
||||
"spam.nu",
|
||||
r#"
|
||||
# line1
|
||||
alias SPAM = print 'spam'
|
||||
"#,
|
||||
)]);
|
||||
|
||||
let code = &[
|
||||
"source spam.nu",
|
||||
"help aliases | where name == SPAM | get 0.usage",
|
||||
];
|
||||
let actual = nu!(cwd: dirs.test(), nu_repl_code(code));
|
||||
|
||||
assert_eq!(actual.out, "line1");
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn help_alias_usage_2() {
|
||||
let code = &[
|
||||
"alias SPAM = print 'spam' # line2",
|
||||
"help aliases | where name == SPAM | get 0.usage",
|
||||
];
|
||||
let actual = nu!(cwd: ".", nu_repl_code(code));
|
||||
|
||||
assert_eq!(actual.out, "line2");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn help_alias_usage_3() {
|
||||
Playground::setup("help_alias_usage_3", |dirs, sandbox| {
|
||||
sandbox.with_files(vec![FileWithContent(
|
||||
"spam.nu",
|
||||
r#"
|
||||
# line1
|
||||
alias SPAM = print 'spam' # line2
|
||||
"#,
|
||||
)]);
|
||||
|
||||
let code = &[
|
||||
"source spam.nu",
|
||||
"help aliases | where name == SPAM | get 0.usage",
|
||||
];
|
||||
let actual = nu!(cwd: dirs.test(), nu_repl_code(code));
|
||||
|
||||
assert!(actual.out.contains("line1"));
|
||||
assert!(actual.out.contains("line2"));
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn help_alias_name() {
|
||||
Playground::setup("help_alias_name", |dirs, sandbox| {
|
||||
sandbox.with_files(vec![FileWithContent(
|
||||
"spam.nu",
|
||||
r#"
|
||||
# line1
|
||||
alias SPAM = print 'spam' # line2
|
||||
"#,
|
||||
)]);
|
||||
|
||||
let code = &["source spam.nu", "help aliases SPAM"];
|
||||
let actual = nu!(cwd: dirs.test(), nu_repl_code(code));
|
||||
|
||||
assert!(actual.out.contains("line1"));
|
||||
assert!(actual.out.contains("line2"));
|
||||
assert!(actual.out.contains("SPAM"));
|
||||
assert!(actual.out.contains("print 'spam'"));
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn help_alias_name_f() {
|
||||
Playground::setup("help_alias_name_f", |dirs, sandbox| {
|
||||
sandbox.with_files(vec![FileWithContent(
|
||||
"spam.nu",
|
||||
r#"
|
||||
# line1
|
||||
alias SPAM = print 'spam' # line2
|
||||
"#,
|
||||
)]);
|
||||
|
||||
let code = &["source spam.nu", "help aliases -f SPAM | get 0.usage"];
|
||||
let actual = nu!(cwd: dirs.test(), nu_repl_code(code));
|
||||
|
||||
assert!(actual.out.contains("line1"));
|
||||
assert!(actual.out.contains("line2"));
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn help_export_alias_name_single_word() {
|
||||
Playground::setup("help_export_alias_name_single_word", |dirs, sandbox| {
|
||||
sandbox.with_files(vec![FileWithContent(
|
||||
"spam.nu",
|
||||
r#"
|
||||
# line1
|
||||
export alias SPAM = print 'spam' # line2
|
||||
"#,
|
||||
)]);
|
||||
|
||||
let code = &["use spam.nu SPAM", "help aliases SPAM"];
|
||||
let actual = nu!(cwd: dirs.test(), nu_repl_code(code));
|
||||
|
||||
assert!(actual.out.contains("line1"));
|
||||
assert!(actual.out.contains("line2"));
|
||||
assert!(actual.out.contains("SPAM"));
|
||||
assert!(actual.out.contains("print 'spam'"));
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn help_export_alias_name_multi_word() {
|
||||
Playground::setup("help_export_alias_name_multi_word", |dirs, sandbox| {
|
||||
sandbox.with_files(vec![FileWithContent(
|
||||
"spam.nu",
|
||||
r#"
|
||||
# line1
|
||||
export alias SPAM = print 'spam' # line2
|
||||
"#,
|
||||
)]);
|
||||
|
||||
let code = &["use spam.nu", "help aliases spam SPAM"];
|
||||
let actual = nu!(cwd: dirs.test(), nu_repl_code(code));
|
||||
|
||||
assert!(actual.out.contains("line1"));
|
||||
assert!(actual.out.contains("line2"));
|
||||
assert!(actual.out.contains("SPAM"));
|
||||
assert!(actual.out.contains("print 'spam'"));
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn help_module_usage_1() {
|
||||
Playground::setup("help_module_usage", |dirs, sandbox| {
|
||||
sandbox.with_files(vec![FileWithContent(
|
||||
"spam.nu",
|
||||
r#"
|
||||
# line1
|
||||
module SPAM {
|
||||
# line2
|
||||
} #line3
|
||||
"#,
|
||||
)]);
|
||||
|
||||
let code = &[
|
||||
"source spam.nu",
|
||||
"help modules | where name == SPAM | get 0.usage",
|
||||
];
|
||||
let actual = nu!(cwd: dirs.test(), nu_repl_code(code));
|
||||
|
||||
assert!(actual.out.contains("line1"));
|
||||
assert!(actual.out.contains("line2"));
|
||||
assert!(actual.out.contains("line3"));
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn help_module_name() {
|
||||
Playground::setup("help_module_name", |dirs, sandbox| {
|
||||
sandbox.with_files(vec![FileWithContent(
|
||||
"spam.nu",
|
||||
r#"
|
||||
# line1
|
||||
module SPAM {
|
||||
# line2
|
||||
} #line3
|
||||
"#,
|
||||
)]);
|
||||
|
||||
let code = &["source spam.nu", "help modules SPAM"];
|
||||
let actual = nu!(cwd: dirs.test(), nu_repl_code(code));
|
||||
|
||||
assert!(actual.out.contains("line1"));
|
||||
assert!(actual.out.contains("line2"));
|
||||
assert!(actual.out.contains("line3"));
|
||||
assert!(actual.out.contains("SPAM"));
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn help_module_sorted_decls() {
|
||||
Playground::setup("help_module_sorted_decls", |dirs, sandbox| {
|
||||
sandbox.with_files(vec![FileWithContent(
|
||||
"spam.nu",
|
||||
r#"
|
||||
module SPAM {
|
||||
export def z [] {}
|
||||
export def a [] {}
|
||||
}
|
||||
"#,
|
||||
)]);
|
||||
|
||||
let code = &["source spam.nu", "help modules SPAM"];
|
||||
let actual = nu!(cwd: dirs.test(), nu_repl_code(code));
|
||||
|
||||
assert!(actual.out.contains("a, z"));
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn help_module_sorted_aliases() {
|
||||
Playground::setup("help_module_sorted_aliases", |dirs, sandbox| {
|
||||
sandbox.with_files(vec![FileWithContent(
|
||||
"spam.nu",
|
||||
r#"
|
||||
module SPAM {
|
||||
export alias z = 'z'
|
||||
export alias a = 'a'
|
||||
}
|
||||
"#,
|
||||
)]);
|
||||
|
||||
let code = &["source spam.nu", "help modules SPAM"];
|
||||
let actual = nu!(cwd: dirs.test(), nu_repl_code(code));
|
||||
|
||||
assert!(actual.out.contains("a, z"));
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn help_usage_extra_usage() {
|
||||
Playground::setup("help_usage_extra_usage", |dirs, sandbox| {
|
||||
sandbox.with_files(vec![FileWithContent(
|
||||
"spam.nu",
|
||||
r#"
|
||||
# module_line1
|
||||
#
|
||||
# module_line2
|
||||
|
||||
# def_line1
|
||||
#
|
||||
# def_line2
|
||||
export def foo [] {}
|
||||
|
||||
# alias_line1
|
||||
#
|
||||
# alias_line2
|
||||
export alias bar = 'bar'
|
||||
"#,
|
||||
)]);
|
||||
|
||||
let actual = nu!(cwd: dirs.test(), pipeline("use spam.nu *; help modules spam"));
|
||||
assert!(actual.out.contains("module_line1"));
|
||||
assert!(actual.out.contains("module_line2"));
|
||||
|
||||
let actual = nu!(cwd: dirs.test(),
|
||||
pipeline("use spam.nu *; help modules | where name == spam | get 0.usage"));
|
||||
assert!(actual.out.contains("module_line1"));
|
||||
assert!(!actual.out.contains("module_line2"));
|
||||
|
||||
let actual = nu!(cwd: dirs.test(), pipeline("use spam.nu *; help commands foo"));
|
||||
assert!(actual.out.contains("def_line1"));
|
||||
assert!(actual.out.contains("def_line2"));
|
||||
|
||||
let actual = nu!(cwd: dirs.test(),
|
||||
pipeline("use spam.nu *; help commands | where name == foo | get 0.usage"));
|
||||
assert!(actual.out.contains("def_line1"));
|
||||
assert!(!actual.out.contains("def_line2"));
|
||||
|
||||
let actual = nu!(cwd: dirs.test(), pipeline("use spam.nu *; help aliases bar"));
|
||||
assert!(actual.out.contains("alias_line1"));
|
||||
assert!(actual.out.contains("alias_line2"));
|
||||
|
||||
let actual = nu!(cwd: dirs.test(),
|
||||
pipeline("use spam.nu *; help aliases | where name == bar | get 0.usage"));
|
||||
assert!(actual.out.contains("alias_line1"));
|
||||
assert!(!actual.out.contains("alias_line2"));
|
||||
})
|
||||
}
|
||||
|
@ -185,15 +185,29 @@ fn use_export_env_combined() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn use_module_creates_accurate_did_you_mean() {
|
||||
fn use_module_creates_accurate_did_you_mean_1() {
|
||||
let actual = nu!(
|
||||
cwd: ".", pipeline(
|
||||
r#"
|
||||
module spam { export def foo [] { "foo" } }; use spam; foo
|
||||
"#
|
||||
module spam { export def foo [] { "foo" } }; use spam; foo
|
||||
"#
|
||||
)
|
||||
);
|
||||
assert!(actual.err.contains(
|
||||
"command 'foo' was not found but it exists in module 'spam'; try using `spam foo`"
|
||||
"command 'foo' was not found but it was imported from module 'spam'; try using `spam foo`"
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn use_module_creates_accurate_did_you_mean_2() {
|
||||
let actual = nu!(
|
||||
cwd: ".", pipeline(
|
||||
r#"
|
||||
module spam { export def foo [] { "foo" } }; foo
|
||||
"#
|
||||
)
|
||||
);
|
||||
assert!(actual.err.contains(
|
||||
"command 'foo' was not found but it exists in module 'spam'; try importing it with `use`"
|
||||
));
|
||||
}
|
||||
|
@ -26,12 +26,12 @@ fn quickcheck_parse(data: String) -> bool {
|
||||
#[test]
|
||||
fn signature_name_matches_command_name() {
|
||||
let ctx = crate::create_default_context();
|
||||
let decls = ctx.get_decl_ids_sorted(true);
|
||||
let decls = ctx.get_decls_sorted(true);
|
||||
let mut failures = Vec::new();
|
||||
|
||||
for decl_id in decls {
|
||||
for (name_bytes, decl_id) in decls {
|
||||
let cmd = ctx.get_decl(decl_id);
|
||||
let cmd_name = cmd.name();
|
||||
let cmd_name = String::from_utf8_lossy(&name_bytes);
|
||||
let sig_name = cmd.signature().name;
|
||||
let category = cmd.signature().category;
|
||||
|
||||
@ -52,10 +52,10 @@ fn signature_name_matches_command_name() {
|
||||
#[test]
|
||||
fn commands_declare_input_output_types() {
|
||||
let ctx = crate::create_default_context();
|
||||
let decls = ctx.get_decl_ids_sorted(true);
|
||||
let decls = ctx.get_decls_sorted(true);
|
||||
let mut failures = Vec::new();
|
||||
|
||||
for decl_id in decls {
|
||||
for (_, decl_id) in decls {
|
||||
let cmd = ctx.get_decl(decl_id);
|
||||
let sig_name = cmd.signature().name;
|
||||
let category = cmd.signature().category;
|
||||
@ -83,12 +83,12 @@ fn commands_declare_input_output_types() {
|
||||
#[test]
|
||||
fn no_search_term_duplicates() {
|
||||
let ctx = crate::create_default_context();
|
||||
let decls = ctx.get_decl_ids_sorted(true);
|
||||
let decls = ctx.get_decls_sorted(true);
|
||||
let mut failures = Vec::new();
|
||||
|
||||
for decl_id in decls {
|
||||
for (name_bytes, decl_id) in decls {
|
||||
let cmd = ctx.get_decl(decl_id);
|
||||
let cmd_name = cmd.name();
|
||||
let cmd_name = String::from_utf8_lossy(&name_bytes);
|
||||
let search_terms = cmd.search_terms();
|
||||
let category = cmd.signature().category;
|
||||
|
||||
|
@ -4,7 +4,7 @@ pub mod documentation;
|
||||
pub mod env;
|
||||
mod eval;
|
||||
mod glob_from;
|
||||
mod scope;
|
||||
pub mod scope;
|
||||
|
||||
pub use call_ext::CallExt;
|
||||
pub use column::get_columns;
|
||||
|
@ -12,7 +12,7 @@ pub fn create_scope(
|
||||
) -> Result<Value, ShellError> {
|
||||
let mut scope_data = ScopeData::new(engine_state, stack);
|
||||
|
||||
scope_data.populate_from_overlays();
|
||||
scope_data.populate_all();
|
||||
|
||||
let mut cols = vec![];
|
||||
let mut vals = vec![];
|
||||
@ -31,15 +31,7 @@ pub fn create_scope(
|
||||
|
||||
cols.push("aliases".to_string());
|
||||
vals.push(Value::List {
|
||||
vals: scope_data
|
||||
.collect_aliases(span)
|
||||
.into_iter()
|
||||
.map(|(alias, value)| Value::Record {
|
||||
cols: vec!["alias".into(), "expansion".into()],
|
||||
vals: vec![alias, value],
|
||||
span,
|
||||
})
|
||||
.collect(),
|
||||
vals: scope_data.collect_aliases(span),
|
||||
span,
|
||||
});
|
||||
|
||||
@ -55,7 +47,7 @@ pub fn create_scope(
|
||||
Ok(Value::Record { cols, vals, span })
|
||||
}
|
||||
|
||||
struct ScopeData<'e, 's> {
|
||||
pub struct ScopeData<'e, 's> {
|
||||
engine_state: &'e EngineState,
|
||||
stack: &'s Stack,
|
||||
vars_map: HashMap<&'e Vec<u8>, &'e usize>,
|
||||
@ -78,7 +70,7 @@ impl<'e, 's> ScopeData<'e, 's> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn populate_from_overlays(&mut self) {
|
||||
pub fn populate_all(&mut self) {
|
||||
for overlay_frame in self.engine_state.active_overlays(&[]) {
|
||||
self.vars_map.extend(&overlay_frame.vars);
|
||||
self.commands_map.extend(&overlay_frame.decls);
|
||||
@ -88,7 +80,19 @@ impl<'e, 's> ScopeData<'e, 's> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn collect_vars(&mut self, span: Span) -> Vec<Value> {
|
||||
pub fn populate_aliases(&mut self) {
|
||||
for overlay_frame in self.engine_state.active_overlays(&[]) {
|
||||
self.aliases_map.extend(&overlay_frame.aliases);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn populate_modules(&mut self) {
|
||||
for overlay_frame in self.engine_state.active_overlays(&[]) {
|
||||
self.modules_map.extend(&overlay_frame.modules);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn collect_vars(&self, span: Span) -> Vec<Value> {
|
||||
let mut vars = vec![];
|
||||
for var in &self.vars_map {
|
||||
let var_name = Value::string(String::from_utf8_lossy(var.0).to_string(), span);
|
||||
@ -110,7 +114,7 @@ impl<'e, 's> ScopeData<'e, 's> {
|
||||
vars
|
||||
}
|
||||
|
||||
pub fn collect_commands(&mut self, span: Span) -> Vec<Value> {
|
||||
pub fn collect_commands(&self, span: Span) -> Vec<Value> {
|
||||
let mut commands = vec![];
|
||||
for ((command_name, _), decl_id) in &self.commands_map {
|
||||
if self.visibility.is_decl_id_visible(decl_id) {
|
||||
@ -457,12 +461,13 @@ impl<'e, 's> ScopeData<'e, 's> {
|
||||
sig_records
|
||||
}
|
||||
|
||||
pub fn collect_aliases(&mut self, span: Span) -> Vec<(Value, Value)> {
|
||||
pub fn collect_aliases(&self, span: Span) -> Vec<Value> {
|
||||
let mut aliases = vec![];
|
||||
for (alias_name, alias_id) in &self.aliases_map {
|
||||
if self.visibility.is_alias_id_visible(alias_id) {
|
||||
let alias = self.engine_state.get_alias(**alias_id);
|
||||
let mut alias_text = String::new();
|
||||
|
||||
for span in alias {
|
||||
let contents = self.engine_state.get_span_contents(span);
|
||||
if !alias_text.is_empty() {
|
||||
@ -470,13 +475,22 @@ impl<'e, 's> ScopeData<'e, 's> {
|
||||
}
|
||||
alias_text.push_str(&String::from_utf8_lossy(contents));
|
||||
}
|
||||
aliases.push((
|
||||
Value::String {
|
||||
val: String::from_utf8_lossy(alias_name).to_string(),
|
||||
span,
|
||||
},
|
||||
Value::string(alias_text, span),
|
||||
));
|
||||
|
||||
let alias_usage = self
|
||||
.engine_state
|
||||
.build_alias_usage(**alias_id)
|
||||
.map(|(usage, _)| usage)
|
||||
.unwrap_or_default();
|
||||
|
||||
aliases.push(Value::Record {
|
||||
cols: vec!["name".into(), "expansion".into(), "usage".into()],
|
||||
vals: vec![
|
||||
Value::string(String::from_utf8_lossy(alias_name), span),
|
||||
Value::string(alias_text, span),
|
||||
Value::string(alias_usage, span),
|
||||
],
|
||||
span,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -484,12 +498,59 @@ impl<'e, 's> ScopeData<'e, 's> {
|
||||
aliases
|
||||
}
|
||||
|
||||
pub fn collect_modules(&mut self, span: Span) -> Vec<Value> {
|
||||
pub fn collect_modules(&self, span: Span) -> Vec<Value> {
|
||||
let mut modules = vec![];
|
||||
|
||||
for module in &self.modules_map {
|
||||
modules.push(Value::String {
|
||||
val: String::from_utf8_lossy(module.0).to_string(),
|
||||
for (module_name, module_id) in &self.modules_map {
|
||||
let module = self.engine_state.get_module(**module_id);
|
||||
|
||||
let export_commands: Vec<Value> = module
|
||||
.decls
|
||||
.keys()
|
||||
.map(|bytes| Value::string(String::from_utf8_lossy(bytes), span))
|
||||
.collect();
|
||||
|
||||
let export_aliases: Vec<Value> = module
|
||||
.aliases
|
||||
.keys()
|
||||
.map(|bytes| Value::string(String::from_utf8_lossy(bytes), span))
|
||||
.collect();
|
||||
|
||||
let export_env_block = module.env_block.map_or_else(
|
||||
|| Value::nothing(span),
|
||||
|block_id| Value::Block {
|
||||
val: block_id,
|
||||
span,
|
||||
},
|
||||
);
|
||||
|
||||
let module_usage = self
|
||||
.engine_state
|
||||
.build_module_usage(**module_id)
|
||||
.map(|(usage, _)| usage)
|
||||
.unwrap_or_default();
|
||||
|
||||
modules.push(Value::Record {
|
||||
cols: vec![
|
||||
"name".into(),
|
||||
"commands".into(),
|
||||
"aliases".into(),
|
||||
"env_block".into(),
|
||||
"usage".into(),
|
||||
],
|
||||
vals: vec![
|
||||
Value::string(String::from_utf8_lossy(module_name), span),
|
||||
Value::List {
|
||||
vals: export_commands,
|
||||
span,
|
||||
},
|
||||
Value::List {
|
||||
vals: export_aliases,
|
||||
span,
|
||||
},
|
||||
export_env_block,
|
||||
Value::string(module_usage, span),
|
||||
],
|
||||
span,
|
||||
});
|
||||
}
|
||||
@ -497,7 +558,7 @@ impl<'e, 's> ScopeData<'e, 's> {
|
||||
modules
|
||||
}
|
||||
|
||||
pub fn collect_engine_state(&mut self, span: Span) -> Value {
|
||||
pub fn collect_engine_state(&self, span: Span) -> Value {
|
||||
let engine_state_cols = vec![
|
||||
"source_bytes".to_string(),
|
||||
"num_vars".to_string(),
|
||||
|
@ -25,7 +25,7 @@ use crate::{
|
||||
parse_internal_call, parse_multispan_value, parse_signature, parse_string, parse_value,
|
||||
parse_var_with_opt_type, trim_quotes, ParsedInternalCall,
|
||||
},
|
||||
unescape_unquote_string, ParseError,
|
||||
unescape_unquote_string, ParseError, Token, TokenContents,
|
||||
};
|
||||
|
||||
pub fn parse_def_predecl(
|
||||
@ -229,57 +229,6 @@ pub fn parse_for(
|
||||
)
|
||||
}
|
||||
|
||||
fn build_usage(working_set: &StateWorkingSet, spans: &[Span]) -> String {
|
||||
let mut usage = String::new();
|
||||
|
||||
let mut num_spaces = 0;
|
||||
let mut first = true;
|
||||
|
||||
// Use the comments to build the usage
|
||||
for comment_part in spans {
|
||||
let contents = working_set.get_span_contents(*comment_part);
|
||||
|
||||
let comment_line = if first {
|
||||
// Count the number of spaces still at the front, skipping the '#'
|
||||
let mut pos = 1;
|
||||
while pos < contents.len() {
|
||||
if let Some(b' ') = contents.get(pos) {
|
||||
// continue
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
pos += 1;
|
||||
}
|
||||
|
||||
num_spaces = pos;
|
||||
|
||||
first = false;
|
||||
|
||||
String::from_utf8_lossy(&contents[pos..]).to_string()
|
||||
} else {
|
||||
let mut pos = 1;
|
||||
|
||||
while pos < contents.len() && pos < num_spaces {
|
||||
if let Some(b' ') = contents.get(pos) {
|
||||
// continue
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
pos += 1;
|
||||
}
|
||||
|
||||
String::from_utf8_lossy(&contents[pos..]).to_string()
|
||||
};
|
||||
|
||||
if !usage.is_empty() {
|
||||
usage.push('\n');
|
||||
}
|
||||
usage.push_str(&comment_line);
|
||||
}
|
||||
|
||||
usage
|
||||
}
|
||||
|
||||
pub fn parse_def(
|
||||
working_set: &mut StateWorkingSet,
|
||||
lite_command: &LiteCommand,
|
||||
@ -287,7 +236,7 @@ pub fn parse_def(
|
||||
) -> (Pipeline, Option<ParseError>) {
|
||||
let spans = &lite_command.parts[..];
|
||||
|
||||
let usage = build_usage(working_set, &lite_command.comments);
|
||||
let (usage, extra_usage) = working_set.build_usage(&lite_command.comments);
|
||||
|
||||
// Checking that the function is used with the correct name
|
||||
// Maybe this is not necessary but it is a sanity check
|
||||
@ -397,6 +346,7 @@ pub fn parse_def(
|
||||
signature.name = name.clone();
|
||||
*signature = signature.add_help();
|
||||
signature.usage = usage;
|
||||
signature.extra_usage = extra_usage;
|
||||
|
||||
*declaration = signature.clone().into_block_command(block_id);
|
||||
|
||||
@ -444,7 +394,7 @@ pub fn parse_extern(
|
||||
let spans = &lite_command.parts;
|
||||
let mut error = None;
|
||||
|
||||
let usage = build_usage(working_set, &lite_command.comments);
|
||||
let (usage, extra_usage) = working_set.build_usage(&lite_command.comments);
|
||||
|
||||
// Checking that the function is used with the correct name
|
||||
// Maybe this is not necessary but it is a sanity check
|
||||
@ -515,11 +465,12 @@ pub fn parse_extern(
|
||||
|
||||
signature.name = name.clone();
|
||||
signature.usage = usage.clone();
|
||||
signature.extra_usage = extra_usage.clone();
|
||||
signature.allows_unknown_args = true;
|
||||
|
||||
let decl = KnownExternal {
|
||||
name: name.to_string(),
|
||||
usage,
|
||||
usage: [usage, extra_usage].join("\n"),
|
||||
signature,
|
||||
};
|
||||
|
||||
@ -559,9 +510,11 @@ pub fn parse_extern(
|
||||
|
||||
pub fn parse_alias(
|
||||
working_set: &mut StateWorkingSet,
|
||||
spans: &[Span],
|
||||
lite_command: &LiteCommand,
|
||||
expand_aliases_denylist: &[usize],
|
||||
) -> (Pipeline, Option<ParseError>) {
|
||||
let spans = &lite_command.parts;
|
||||
|
||||
// if the call is "alias", turn it into "print $nu.scope.aliases"
|
||||
if spans.len() == 1 {
|
||||
let head = Expression {
|
||||
@ -672,7 +625,7 @@ pub fn parse_alias(
|
||||
);
|
||||
}
|
||||
|
||||
working_set.add_alias(alias_name, replacement);
|
||||
working_set.add_alias(alias_name, replacement, lite_command.comments.clone());
|
||||
}
|
||||
|
||||
let err = if spans.len() < 4 {
|
||||
@ -785,7 +738,7 @@ pub fn parse_export_in_block(
|
||||
}
|
||||
|
||||
match full_name.as_slice() {
|
||||
b"export alias" => parse_alias(working_set, &lite_command.parts, expand_aliases_denylist),
|
||||
b"export alias" => parse_alias(working_set, lite_command, expand_aliases_denylist),
|
||||
b"export def" | b"export def-env" => {
|
||||
parse_def(working_set, lite_command, expand_aliases_denylist)
|
||||
}
|
||||
@ -1075,7 +1028,7 @@ pub fn parse_export_in_module(
|
||||
parts: spans[1..].to_vec(),
|
||||
};
|
||||
let (pipeline, err) =
|
||||
parse_alias(working_set, &lite_command.parts, expand_aliases_denylist);
|
||||
parse_alias(working_set, &lite_command, expand_aliases_denylist);
|
||||
error = error.or(err);
|
||||
|
||||
let export_alias_decl_id =
|
||||
@ -1328,11 +1281,41 @@ pub fn parse_export_env(
|
||||
(pipeline, Some(block_id), None)
|
||||
}
|
||||
|
||||
fn collect_first_comments(tokens: &[Token]) -> Vec<Span> {
|
||||
let mut comments = vec![];
|
||||
|
||||
let mut tokens_iter = tokens.iter().peekable();
|
||||
while let Some(token) = tokens_iter.next() {
|
||||
match token.contents {
|
||||
TokenContents::Comment => {
|
||||
comments.push(token.span);
|
||||
}
|
||||
TokenContents::Eol => {
|
||||
if let Some(Token {
|
||||
contents: TokenContents::Eol,
|
||||
..
|
||||
}) = tokens_iter.peek()
|
||||
{
|
||||
if !comments.is_empty() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
comments.clear();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
comments
|
||||
}
|
||||
|
||||
pub fn parse_module_block(
|
||||
working_set: &mut StateWorkingSet,
|
||||
span: Span,
|
||||
expand_aliases_denylist: &[usize],
|
||||
) -> (Block, Module, Option<ParseError>) {
|
||||
) -> (Block, Module, Vec<Span>, Option<ParseError>) {
|
||||
let mut error = None;
|
||||
|
||||
working_set.enter_scope();
|
||||
@ -1342,6 +1325,8 @@ pub fn parse_module_block(
|
||||
let (output, err) = lex(source, span.start, &[], &[], false);
|
||||
error = error.or(err);
|
||||
|
||||
let module_comments = collect_first_comments(&output);
|
||||
|
||||
let (output, err) = lite_parse(&output);
|
||||
error = error.or(err);
|
||||
|
||||
@ -1378,11 +1363,8 @@ pub fn parse_module_block(
|
||||
(pipeline, err)
|
||||
}
|
||||
b"alias" => {
|
||||
let (pipeline, err) = parse_alias(
|
||||
working_set,
|
||||
&command.parts,
|
||||
expand_aliases_denylist,
|
||||
);
|
||||
let (pipeline, err) =
|
||||
parse_alias(working_set, command, expand_aliases_denylist);
|
||||
|
||||
(pipeline, err)
|
||||
}
|
||||
@ -1452,17 +1434,20 @@ pub fn parse_module_block(
|
||||
|
||||
working_set.exit_scope();
|
||||
|
||||
(block, module, error)
|
||||
(block, module, module_comments, error)
|
||||
}
|
||||
|
||||
pub fn parse_module(
|
||||
working_set: &mut StateWorkingSet,
|
||||
spans: &[Span],
|
||||
lite_command: &LiteCommand,
|
||||
expand_aliases_denylist: &[usize],
|
||||
) -> (Pipeline, Option<ParseError>) {
|
||||
// TODO: Currently, module is closing over its parent scope (i.e., defs in the parent scope are
|
||||
// visible and usable in this module's scope). We want to disable that for files.
|
||||
|
||||
let spans = &lite_command.parts;
|
||||
let mut module_comments = lite_command.comments.clone();
|
||||
|
||||
let mut error = None;
|
||||
let bytes = working_set.get_span_contents(spans[0]);
|
||||
|
||||
@ -1496,12 +1481,14 @@ pub fn parse_module(
|
||||
|
||||
let block_span = Span::new(start, end);
|
||||
|
||||
let (block, module, err) =
|
||||
let (block, module, inner_comments, err) =
|
||||
parse_module_block(working_set, block_span, expand_aliases_denylist);
|
||||
error = error.or(err);
|
||||
|
||||
let block_id = working_set.add_block(block);
|
||||
let _ = working_set.add_module(&module_name, module);
|
||||
|
||||
module_comments.extend(inner_comments);
|
||||
let _ = working_set.add_module(&module_name, module, module_comments);
|
||||
|
||||
let block_expr = Expression {
|
||||
expr: Expr::Block(block_id),
|
||||
@ -1734,7 +1721,7 @@ pub fn parse_use(
|
||||
working_set.parsed_module_files.push(module_path);
|
||||
|
||||
// Parse the module
|
||||
let (block, module, err) = parse_module_block(
|
||||
let (block, module, module_comments, err) = parse_module_block(
|
||||
working_set,
|
||||
Span::new(span_start, span_end),
|
||||
expand_aliases_denylist,
|
||||
@ -1748,7 +1735,8 @@ pub fn parse_use(
|
||||
working_set.currently_parsed_cwd = prev_currently_parsed_cwd;
|
||||
|
||||
let _ = working_set.add_block(block);
|
||||
let module_id = working_set.add_module(&module_name, module.clone());
|
||||
let module_id =
|
||||
working_set.add_module(&module_name, module.clone(), module_comments);
|
||||
|
||||
(
|
||||
ImportPattern {
|
||||
@ -2320,7 +2308,7 @@ pub fn parse_overlay_new(
|
||||
custom_completion: None,
|
||||
}]);
|
||||
|
||||
let module_id = working_set.add_module(&overlay_name, Module::new());
|
||||
let module_id = working_set.add_module(&overlay_name, Module::new(), vec![]);
|
||||
|
||||
working_set.add_overlay(
|
||||
overlay_name.as_bytes().to_vec(),
|
||||
@ -2557,7 +2545,7 @@ pub fn parse_overlay_use(
|
||||
working_set.currently_parsed_cwd.clone()
|
||||
};
|
||||
|
||||
let (block, module, err) = parse_module_block(
|
||||
let (block, module, module_comments, err) = parse_module_block(
|
||||
working_set,
|
||||
Span::new(span_start, span_end),
|
||||
expand_aliases_denylist,
|
||||
@ -2568,7 +2556,8 @@ pub fn parse_overlay_use(
|
||||
working_set.currently_parsed_cwd = prev_currently_parsed_cwd;
|
||||
|
||||
let _ = working_set.add_block(block);
|
||||
let module_id = working_set.add_module(&overlay_name, module.clone());
|
||||
let module_id =
|
||||
working_set.add_module(&overlay_name, module.clone(), module_comments);
|
||||
|
||||
(
|
||||
new_name.map(|spanned| spanned.item).unwrap_or(overlay_name),
|
||||
|
@ -5248,8 +5248,8 @@ pub fn parse_builtin_commands(
|
||||
let (expr, err) = parse_for(working_set, &lite_command.parts, expand_aliases_denylist);
|
||||
(Pipeline::from_vec(vec![expr]), err)
|
||||
}
|
||||
b"alias" => parse_alias(working_set, &lite_command.parts, expand_aliases_denylist),
|
||||
b"module" => parse_module(working_set, &lite_command.parts, expand_aliases_denylist),
|
||||
b"alias" => parse_alias(working_set, lite_command, expand_aliases_denylist),
|
||||
b"module" => parse_module(working_set, lite_command, expand_aliases_denylist),
|
||||
b"use" => {
|
||||
let (pipeline, _, err) =
|
||||
parse_use(working_set, &lite_command.parts, expand_aliases_denylist);
|
||||
|
@ -31,6 +31,51 @@ pub enum ReplOperation {
|
||||
Replace(String),
|
||||
}
|
||||
|
||||
/// Organizes usage messages for various primitives
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Usage {
|
||||
// TODO: Move decl usages here
|
||||
alias_comments: HashMap<AliasId, Vec<Span>>,
|
||||
module_comments: HashMap<ModuleId, Vec<Span>>,
|
||||
}
|
||||
|
||||
impl Usage {
|
||||
pub fn new() -> Self {
|
||||
Usage {
|
||||
alias_comments: HashMap::new(),
|
||||
module_comments: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_alias_comments(&mut self, alias_id: AliasId, comments: Vec<Span>) {
|
||||
self.alias_comments.insert(alias_id, comments);
|
||||
}
|
||||
|
||||
pub fn add_module_comments(&mut self, module_id: ModuleId, comments: Vec<Span>) {
|
||||
self.module_comments.insert(module_id, comments);
|
||||
}
|
||||
|
||||
pub fn get_alias_comments(&self, alias_id: AliasId) -> Option<&[Span]> {
|
||||
self.alias_comments.get(&alias_id).map(|v| v.as_ref())
|
||||
}
|
||||
|
||||
pub fn get_module_comments(&self, module_id: ModuleId) -> Option<&[Span]> {
|
||||
self.module_comments.get(&module_id).map(|v| v.as_ref())
|
||||
}
|
||||
|
||||
/// Overwrite own values with the other
|
||||
pub fn merge_with(&mut self, other: Usage) {
|
||||
self.alias_comments.extend(other.alias_comments);
|
||||
self.module_comments.extend(other.module_comments);
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Usage {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
/// The core global engine state. This includes all global definitions as well as any global state that
|
||||
/// will persist for the whole session.
|
||||
///
|
||||
@ -82,6 +127,7 @@ pub struct EngineState {
|
||||
aliases: Vec<Vec<Span>>,
|
||||
blocks: Vec<Block>,
|
||||
modules: Vec<Module>,
|
||||
usage: Usage,
|
||||
pub scope: ScopeFrame,
|
||||
pub ctrlc: Option<Arc<AtomicBool>>,
|
||||
pub env_vars: EnvVars,
|
||||
@ -125,6 +171,7 @@ impl EngineState {
|
||||
aliases: vec![],
|
||||
blocks: vec![],
|
||||
modules: vec![Module::new()],
|
||||
usage: Usage::new(),
|
||||
// make sure we have some default overlay:
|
||||
scope: ScopeFrame::with_empty_overlay(
|
||||
DEFAULT_OVERLAY_NAME.as_bytes().to_vec(),
|
||||
@ -167,6 +214,7 @@ impl EngineState {
|
||||
self.vars.extend(delta.vars);
|
||||
self.blocks.extend(delta.blocks);
|
||||
self.modules.extend(delta.modules);
|
||||
self.usage.merge_with(delta.usage);
|
||||
|
||||
let first = delta.scope.remove(0);
|
||||
|
||||
@ -553,6 +601,14 @@ impl EngineState {
|
||||
None
|
||||
}
|
||||
|
||||
pub fn get_alias_comments(&self, alias_id: AliasId) -> Option<&[Span]> {
|
||||
self.usage.get_alias_comments(alias_id)
|
||||
}
|
||||
|
||||
pub fn get_module_comments(&self, module_id: ModuleId) -> Option<&[Span]> {
|
||||
self.usage.get_module_comments(module_id)
|
||||
}
|
||||
|
||||
#[cfg(feature = "plugin")]
|
||||
pub fn plugin_decls(&self) -> impl Iterator<Item = &Box<dyn Command + 'static>> {
|
||||
let mut unique_plugin_decls = HashMap::new();
|
||||
@ -580,24 +636,39 @@ impl EngineState {
|
||||
None
|
||||
}
|
||||
|
||||
pub fn which_module_has_decl(&self, name: &[u8]) -> Option<&[u8]> {
|
||||
for (module_id, m) in self.modules.iter().enumerate() {
|
||||
if m.has_decl(name) {
|
||||
for overlay_frame in self.active_overlays(&[]).iter() {
|
||||
let module_name = overlay_frame.modules.iter().find_map(|(key, &val)| {
|
||||
if val == module_id {
|
||||
Some(key)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
if let Some(final_name) = module_name {
|
||||
return Some(&final_name[..]);
|
||||
}
|
||||
pub fn which_module_has_decl(
|
||||
&self,
|
||||
decl_name: &[u8],
|
||||
removed_overlays: &[Vec<u8>],
|
||||
) -> Option<&[u8]> {
|
||||
for overlay_frame in self.active_overlays(removed_overlays).iter().rev() {
|
||||
for (module_name, module_id) in overlay_frame.modules.iter() {
|
||||
let module = self.get_module(*module_id);
|
||||
if module.has_decl(decl_name) {
|
||||
return Some(module_name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
|
||||
// for (module_id, m) in self.modules.iter().enumerate() {
|
||||
// if m.has_decl(name) {
|
||||
// for overlay_frame in self.active_overlays(&[]).iter() {
|
||||
// let module_name = overlay_frame.modules.iter().find_map(|(key, &val)| {
|
||||
// if val == module_id {
|
||||
// Some(key)
|
||||
// } else {
|
||||
// None
|
||||
// }
|
||||
// });
|
||||
// if let Some(final_name) = module_name {
|
||||
// return Some(&final_name[..]);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// None
|
||||
}
|
||||
|
||||
pub fn find_overlay(&self, name: &[u8]) -> Option<OverlayId> {
|
||||
@ -687,8 +758,39 @@ impl EngineState {
|
||||
.as_ref()
|
||||
}
|
||||
|
||||
/// Get all IDs of all commands within scope, sorted by the commads' names
|
||||
pub fn get_decl_ids_sorted(&self, include_hidden: bool) -> impl Iterator<Item = DeclId> {
|
||||
/// Get all aliases within scope, sorted by the alias names
|
||||
pub fn get_aliases_sorted(
|
||||
&self,
|
||||
include_hidden: bool,
|
||||
) -> impl Iterator<Item = (Vec<u8>, DeclId)> {
|
||||
let mut aliases_map = HashMap::new();
|
||||
|
||||
for overlay_frame in self.active_overlays(&[]) {
|
||||
let new_aliases = if include_hidden {
|
||||
overlay_frame.aliases.clone()
|
||||
} else {
|
||||
overlay_frame
|
||||
.aliases
|
||||
.clone()
|
||||
.into_iter()
|
||||
.filter(|(_, id)| overlay_frame.visibility.is_alias_id_visible(id))
|
||||
.collect()
|
||||
};
|
||||
|
||||
aliases_map.extend(new_aliases);
|
||||
}
|
||||
|
||||
let mut aliases: Vec<(Vec<u8>, DeclId)> = aliases_map.into_iter().collect();
|
||||
|
||||
aliases.sort_by(|a, b| a.0.cmp(&b.0));
|
||||
aliases.into_iter()
|
||||
}
|
||||
|
||||
/// Get all commands within scope, sorted by the commads' names
|
||||
pub fn get_decls_sorted(
|
||||
&self,
|
||||
include_hidden: bool,
|
||||
) -> impl Iterator<Item = (Vec<u8>, DeclId)> {
|
||||
let mut decls_map = HashMap::new();
|
||||
|
||||
for overlay_frame in self.active_overlays(&[]) {
|
||||
@ -710,16 +812,19 @@ impl EngineState {
|
||||
decls_map.into_iter().map(|(v, k)| (v.0, k)).collect();
|
||||
|
||||
decls.sort_by(|a, b| a.0.cmp(&b.0));
|
||||
decls.into_iter().map(|(_, id)| id)
|
||||
decls.into_iter()
|
||||
}
|
||||
|
||||
/// Get signatures of all commands within scope.
|
||||
pub fn get_signatures(&self, include_hidden: bool) -> Vec<Signature> {
|
||||
self.get_decl_ids_sorted(include_hidden)
|
||||
.map(|id| {
|
||||
self.get_decls_sorted(include_hidden)
|
||||
.map(|(name_bytes, id)| {
|
||||
let decl = self.get_decl(id);
|
||||
// the reason to create the name this way is because the command could be renamed
|
||||
// during module imports but the signature still contains the old name
|
||||
let name = String::from_utf8_lossy(&name_bytes).to_string();
|
||||
|
||||
(*decl).signature().update_from_command(decl.borrow())
|
||||
(*decl).signature().update_from_command(name, decl.borrow())
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
@ -733,11 +838,14 @@ impl EngineState {
|
||||
&self,
|
||||
include_hidden: bool,
|
||||
) -> Vec<(Signature, Vec<Example>, bool, bool, bool)> {
|
||||
self.get_decl_ids_sorted(include_hidden)
|
||||
.map(|id| {
|
||||
self.get_decls_sorted(include_hidden)
|
||||
.map(|(name_bytes, id)| {
|
||||
let decl = self.get_decl(id);
|
||||
// the reason to create the name this way is because the command could be renamed
|
||||
// during module imports but the signature still contains the old name
|
||||
let name = String::from_utf8_lossy(&name_bytes).to_string();
|
||||
|
||||
let signature = (*decl).signature().update_from_command(decl.borrow());
|
||||
let signature = (*decl).signature().update_from_command(name, decl.borrow());
|
||||
|
||||
(
|
||||
signature,
|
||||
@ -839,6 +947,24 @@ impl EngineState {
|
||||
pub fn get_config_path(&self, key: &str) -> Option<&PathBuf> {
|
||||
self.config_path.get(key)
|
||||
}
|
||||
|
||||
pub fn build_usage(&self, spans: &[Span]) -> (String, String) {
|
||||
let comment_lines: Vec<&[u8]> = spans
|
||||
.iter()
|
||||
.map(|span| self.get_span_contents(span))
|
||||
.collect();
|
||||
build_usage(&comment_lines)
|
||||
}
|
||||
|
||||
pub fn build_alias_usage(&self, alias_id: AliasId) -> Option<(String, String)> {
|
||||
self.get_alias_comments(alias_id)
|
||||
.map(|comment_spans| self.build_usage(comment_spans))
|
||||
}
|
||||
|
||||
pub fn build_module_usage(&self, module_id: ModuleId) -> Option<(String, String)> {
|
||||
self.get_module_comments(module_id)
|
||||
.map(|comment_spans| self.build_usage(comment_spans))
|
||||
}
|
||||
}
|
||||
|
||||
/// A temporary extension to the global state. This handles bridging between the global state and the
|
||||
@ -915,6 +1041,7 @@ pub struct StateDelta {
|
||||
aliases: Vec<Vec<Span>>, // indexed by AliasId
|
||||
pub blocks: Vec<Block>, // indexed by BlockId
|
||||
modules: Vec<Module>, // indexed by ModuleId
|
||||
usage: Usage,
|
||||
pub scope: Vec<ScopeFrame>,
|
||||
#[cfg(feature = "plugin")]
|
||||
plugins_changed: bool, // marks whether plugin file should be updated
|
||||
@ -938,6 +1065,7 @@ impl StateDelta {
|
||||
blocks: vec![],
|
||||
modules: vec![],
|
||||
scope: vec![scope_frame],
|
||||
usage: Usage::new(),
|
||||
#[cfg(feature = "plugin")]
|
||||
plugins_changed: false,
|
||||
}
|
||||
@ -1305,12 +1433,16 @@ impl<'a> StateWorkingSet<'a> {
|
||||
self.num_blocks() - 1
|
||||
}
|
||||
|
||||
pub fn add_module(&mut self, name: &str, module: Module) -> ModuleId {
|
||||
pub fn add_module(&mut self, name: &str, module: Module, comments: Vec<Span>) -> ModuleId {
|
||||
let name = name.as_bytes().to_vec();
|
||||
|
||||
self.delta.modules.push(module);
|
||||
let module_id = self.num_modules() - 1;
|
||||
|
||||
if !comments.is_empty() {
|
||||
self.delta.usage.add_module_comments(module_id, comments);
|
||||
}
|
||||
|
||||
self.last_overlay_mut().modules.insert(name, module_id);
|
||||
|
||||
module_id
|
||||
@ -1633,10 +1765,14 @@ impl<'a> StateWorkingSet<'a> {
|
||||
next_id
|
||||
}
|
||||
|
||||
pub fn add_alias(&mut self, name: Vec<u8>, replacement: Vec<Span>) {
|
||||
pub fn add_alias(&mut self, name: Vec<u8>, replacement: Vec<Span>, comments: Vec<Span>) {
|
||||
self.delta.aliases.push(replacement);
|
||||
let alias_id = self.num_aliases() - 1;
|
||||
|
||||
if !comments.is_empty() {
|
||||
self.delta.usage.add_alias_comments(alias_id, comments);
|
||||
}
|
||||
|
||||
let last = self.last_overlay_mut();
|
||||
|
||||
last.aliases.insert(name, alias_id);
|
||||
@ -2051,17 +2187,13 @@ impl<'a> StateWorkingSet<'a> {
|
||||
pub fn render(self) -> StateDelta {
|
||||
self.delta
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Visibility {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ScopeFrame {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
pub fn build_usage(&self, spans: &[Span]) -> (String, String) {
|
||||
let comment_lines: Vec<&[u8]> = spans
|
||||
.iter()
|
||||
.map(|span| self.get_span_contents(*span))
|
||||
.collect();
|
||||
build_usage(&comment_lines)
|
||||
}
|
||||
}
|
||||
|
||||
@ -2148,6 +2280,59 @@ impl<'a> miette::SourceCode for &StateWorkingSet<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
fn build_usage(comment_lines: &[&[u8]]) -> (String, String) {
|
||||
let mut usage = String::new();
|
||||
|
||||
let mut num_spaces = 0;
|
||||
let mut first = true;
|
||||
|
||||
// Use the comments to build the usage
|
||||
for contents in comment_lines {
|
||||
let comment_line = if first {
|
||||
// Count the number of spaces still at the front, skipping the '#'
|
||||
let mut pos = 1;
|
||||
while pos < contents.len() {
|
||||
if let Some(b' ') = contents.get(pos) {
|
||||
// continue
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
pos += 1;
|
||||
}
|
||||
|
||||
num_spaces = pos;
|
||||
|
||||
first = false;
|
||||
|
||||
String::from_utf8_lossy(&contents[pos..]).to_string()
|
||||
} else {
|
||||
let mut pos = 1;
|
||||
|
||||
while pos < contents.len() && pos < num_spaces {
|
||||
if let Some(b' ') = contents.get(pos) {
|
||||
// continue
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
pos += 1;
|
||||
}
|
||||
|
||||
String::from_utf8_lossy(&contents[pos..]).to_string()
|
||||
};
|
||||
|
||||
if !usage.is_empty() {
|
||||
usage.push('\n');
|
||||
}
|
||||
usage.push_str(&comment_line);
|
||||
}
|
||||
|
||||
if let Some((brief_usage, extra_usage)) = usage.split_once("\n\n") {
|
||||
(brief_usage.to_string(), extra_usage.to_string())
|
||||
} else {
|
||||
(usage, String::default())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod engine_state_tests {
|
||||
use super::*;
|
||||
|
@ -44,14 +44,14 @@ impl Visibility {
|
||||
self.alias_ids.insert(*alias_id, true);
|
||||
}
|
||||
|
||||
/// Overwrite own values with the other
|
||||
pub fn merge_with(&mut self, other: Visibility) {
|
||||
// overwrite own values with the other
|
||||
self.decl_ids.extend(other.decl_ids);
|
||||
self.alias_ids.extend(other.alias_ids);
|
||||
}
|
||||
|
||||
/// Take new values from the other but keep own values
|
||||
pub fn append(&mut self, other: &Visibility) {
|
||||
// take new values from the other but keep own values
|
||||
for (decl_id, visible) in other.decl_ids.iter() {
|
||||
if !self.decl_ids.contains_key(decl_id) {
|
||||
self.decl_ids.insert(*decl_id, *visible);
|
||||
@ -79,10 +79,6 @@ pub struct ScopeFrame {
|
||||
/// Order is significant: The last item points at the last activated overlay.
|
||||
pub active_overlays: Vec<OverlayId>,
|
||||
|
||||
/// Deactivated overlays from permanent state.
|
||||
/// ! Stores OverlayIds from the permanent state, not from this frame. !
|
||||
// removed_overlays: Vec<OverlayId>,
|
||||
|
||||
/// Removed overlays from previous scope frames / permanent state
|
||||
pub removed_overlays: Vec<Vec<u8>>,
|
||||
|
||||
@ -281,3 +277,15 @@ impl<'a> Borrow<dyn DeclKey + 'a> for (Vec<u8>, Type) {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Visibility {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ScopeFrame {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
@ -542,6 +542,15 @@ Either make sure {0} is a string, or add a 'to_string' entry for it in ENV_CONVE
|
||||
#[diagnostic(code(nu::shell::command_not_found), url(docsrs))]
|
||||
CommandNotFound(#[label("command not found")] Span),
|
||||
|
||||
/// This alias could not be found
|
||||
///
|
||||
/// ## Resolution
|
||||
///
|
||||
/// The alias does not exist in the current scope. It might exist in another scope or overlay or be hidden.
|
||||
#[error("Alias not found")]
|
||||
#[diagnostic(code(nu::shell::alias_not_found), url(docsrs))]
|
||||
AliasNotFound(#[label("alias not found")] Span),
|
||||
|
||||
/// A flag was not found.
|
||||
#[error("Flag not found")]
|
||||
#[diagnostic(code(nu::shell::flag_not_found), url(docsrs))]
|
||||
@ -868,9 +877,6 @@ Either make sure {0} is a string, or add a 'to_string' entry for it in ENV_CONVE
|
||||
#[diagnostic(code(nu::shell::non_unicode_input), url(docsrs))]
|
||||
NonUnicodeInput,
|
||||
|
||||
// /// Path not found.
|
||||
// #[error("Path not found.")]
|
||||
// PathNotFound,
|
||||
/// Unexpected abbr component.
|
||||
///
|
||||
/// ## Resolution
|
||||
|
@ -265,7 +265,8 @@ impl Signature {
|
||||
}
|
||||
|
||||
/// Update signature's fields from a Command trait implementation
|
||||
pub fn update_from_command(mut self, command: &dyn Command) -> Signature {
|
||||
pub fn update_from_command(mut self, name: String, command: &dyn Command) -> Signature {
|
||||
self.name = name;
|
||||
self.search_terms = command
|
||||
.search_terms()
|
||||
.into_iter()
|
||||
@ -669,6 +670,10 @@ impl Command for Predeclaration {
|
||||
&self.signature.usage
|
||||
}
|
||||
|
||||
fn extra_usage(&self) -> &str {
|
||||
&self.signature.extra_usage
|
||||
}
|
||||
|
||||
fn run(
|
||||
&self,
|
||||
_engine_state: &EngineState,
|
||||
@ -718,6 +723,10 @@ impl Command for BlockCommand {
|
||||
&self.signature.usage
|
||||
}
|
||||
|
||||
fn extra_usage(&self) -> &str {
|
||||
&self.signature.extra_usage
|
||||
}
|
||||
|
||||
fn run(
|
||||
&self,
|
||||
_engine_state: &EngineState,
|
||||
|
@ -130,7 +130,7 @@ fn help_present_in_def() -> TestResult {
|
||||
#[test]
|
||||
fn help_not_present_in_extern() -> TestResult {
|
||||
run_test(
|
||||
"module test {export extern \"git fetch\" []}; use test; help git fetch | ansi strip",
|
||||
"module test {export extern \"git fetch\" []}; use test `git fetch`; help git fetch | ansi strip",
|
||||
"Usage:\n > git fetch",
|
||||
)
|
||||
}
|
||||
|
@ -292,7 +292,7 @@ fn module_nested_imports_in_dirs_prefixed() {
|
||||
|
||||
#[test]
|
||||
fn module_import_env_1() {
|
||||
Playground::setup("module_imprt_env_1", |dirs, sandbox| {
|
||||
Playground::setup("module_import_env_1", |dirs, sandbox| {
|
||||
sandbox
|
||||
.with_files(vec![FileWithContentToBeTrimmed(
|
||||
"main.nu",
|
||||
|
@ -795,8 +795,8 @@ fn overlay_remove_renamed_overlay() {
|
||||
let actual = nu!(cwd: "tests/overlays", pipeline(&inp.join("; ")));
|
||||
let actual_repl = nu!(cwd: "tests/overlays", nu_repl_code(inp));
|
||||
|
||||
assert!(actual.err.contains("did you mean 'for'?"));
|
||||
assert!(actual_repl.err.contains("did you mean 'for'?"));
|
||||
assert!(actual.err.contains("external_command"));
|
||||
assert!(actual_repl.err.contains("external_command"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
Loading…
Reference in New Issue
Block a user