diff --git a/crates/nu-command/src/core_commands/help.rs b/crates/nu-command/src/core_commands/help.rs index 9833742545..74d8de92d4 100644 --- a/crates/nu-command/src/core_commands/help.rs +++ b/crates/nu-command/src/core_commands/help.rs @@ -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 { - help(engine_state, stack, call) - } + let head = call.head; + let find: Option> = call.get_flag(engine_state, stack, "find")?; + let rest: Vec> = call.rest(engine_state, stack, 0)?; - fn examples(&self) -> Vec { - 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 { - let head = call.head; - let find: Option> = call.get_flag(engine_state, stack, "find")?; - let rest: Vec> = 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::>() - .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::>(); - - 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 - display help about a particular command - * help --find - search through all of help + * help - display help about a particular command, alias, or module + * help --find - 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 = rest.iter().map(|arg| arg.span).collect(); + Err(ShellError::NotFound(span(&rest_spans))) + } else { + result + } + } } + + fn examples(&self) -> Vec { + 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, // list of records + search_string: &str, + searched_cols: &[&str], + string_style: &Style, +) -> Result, 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, (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. diff --git a/crates/nu-command/src/core_commands/help_aliases.rs b/crates/nu-command/src/core_commands/help_aliases.rs new file mode 100644 index 0000000000..565813dbfe --- /dev/null +++ b/crates/nu-command/src/core_commands/help_aliases.rs @@ -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 { + 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 { + help_aliases(engine_state, stack, call) + } +} + +pub fn help_aliases( + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, +) -> Result { + let head = call.head; + let find: Option> = call.get_flag(engine_state, stack, "find")?; + let rest: Vec> = 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::>(), + ))); + }; + + let alias_expansion = engine_state + .get_alias(alias_id) + .iter() + .map(|span| String::from_utf8_lossy(engine_state.get_span_contents(span))) + .collect::>>() + .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 { + 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 {}) + } +} diff --git a/crates/nu-command/src/core_commands/help_commands.rs b/crates/nu-command/src/core_commands/help_commands.rs new file mode 100644 index 0000000000..dc60709d4a --- /dev/null +++ b/crates/nu-command/src/core_commands/help_commands.rs @@ -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 { + help_commands(engine_state, stack, call) + } +} + +pub fn help_commands( + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, +) -> Result { + let head = call.head; + let find: Option> = call.get_flag(engine_state, stack, "find")?; + let rest: Vec> = 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::>(); + + 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 { + 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 {}) + } +} diff --git a/crates/nu-command/src/core_commands/help_modules.rs b/crates/nu-command/src/core_commands/help_modules.rs new file mode 100644 index 0000000000..ad98a855b0 --- /dev/null +++ b/crates/nu-command/src/core_commands/help_modules.rs @@ -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 { + 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 { + help_modules(engine_state, stack, call) + } +} + +pub fn help_modules( + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, +) -> Result { + let head = call.head; + let find: Option> = call.get_flag(engine_state, stack, "find")?; + let rest: Vec> = 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::>()), + )); + }; + + 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, 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::>() + .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, 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::>() + .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 { + 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 {}) + } +} diff --git a/crates/nu-command/src/core_commands/mod.rs b/crates/nu-command/src/core_commands/mod.rs index e8ce32bde2..6ed0d3bdb0 100644 --- a/crates/nu-command/src/core_commands/mod.rs +++ b/crates/nu-command/src/core_commands/mod.rs @@ -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; diff --git a/crates/nu-command/src/default_context.rs b/crates/nu-command/src/default_context.rs index 39f5c79ca9..f722e20f9c 100644 --- a/crates/nu-command/src/default_context.rs +++ b/crates/nu-command/src/default_context.rs @@ -50,6 +50,9 @@ pub fn create_default_context() -> EngineState { Extern, For, Help, + HelpAliases, + HelpCommands, + HelpModules, HelpOperators, Hide, HideEnv, diff --git a/crates/nu-command/src/system/nu_check.rs b/crates/nu-command/src/system/nu_check.rs index 7e285f0c0e..e8a9145e4b 100644 --- a/crates/nu-command/src/system/nu_check.rs +++ b/crates/nu-command/src/system/nu_check.rs @@ -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 { diff --git a/crates/nu-command/src/system/run_external.rs b/crates/nu-command/src/system/run_external.rs index 9b29365882..6ecbcf28cb 100644 --- a/crates/nu-command/src/system/run_external.rs +++ b/crates/nu-command/src/system/run_external.rs @@ -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 => { diff --git a/crates/nu-command/tests/commands/alias.rs b/crates/nu-command/tests/commands/alias.rs index 8978d883a2..2bf0c97300 100644 --- a/crates/nu-command/tests/commands/alias.rs +++ b/crates/nu-command/tests/commands/alias.rs @@ -47,7 +47,7 @@ fn alias_fails_with_invalid_name() { let actual = nu!( cwd: ".", pipeline( r#" - alias 1234 = echo "test" + alias 1234 = echo "test" "# )); assert!(actual.err.contains(err_msg)); @@ -55,7 +55,7 @@ fn alias_fails_with_invalid_name() { let actual = nu!( cwd: ".", pipeline( r#" - alias 5gib = echo "test" + alias 5gib = echo "test" "# )); assert!(actual.err.contains(err_msg)); @@ -63,7 +63,7 @@ fn alias_fails_with_invalid_name() { let actual = nu!( cwd: ".", pipeline( r#" - alias "te#t" = echo "test" + alias "te#t" = echo "test" "# )); assert!(actual.err.contains(err_msg)); @@ -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")); } diff --git a/crates/nu-command/tests/commands/help.rs b/crates/nu-command/tests/commands/help.rs index 679c124d9a..98515628af 100644 --- a/crates/nu-command/tests/commands/help.rs +++ b/crates/nu-command/tests/commands/help.rs @@ -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")); + }) +} diff --git a/crates/nu-command/tests/commands/use_.rs b/crates/nu-command/tests/commands/use_.rs index 9e2273b4c9..ab1a84a6f6 100644 --- a/crates/nu-command/tests/commands/use_.rs +++ b/crates/nu-command/tests/commands/use_.rs @@ -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`" )); } diff --git a/crates/nu-command/tests/main.rs b/crates/nu-command/tests/main.rs index d81eb00e6b..9619114094 100644 --- a/crates/nu-command/tests/main.rs +++ b/crates/nu-command/tests/main.rs @@ -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; diff --git a/crates/nu-engine/src/lib.rs b/crates/nu-engine/src/lib.rs index f12a204d38..2853f9b891 100644 --- a/crates/nu-engine/src/lib.rs +++ b/crates/nu-engine/src/lib.rs @@ -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; diff --git a/crates/nu-engine/src/scope.rs b/crates/nu-engine/src/scope.rs index bc0316ea9c..4486529055 100644 --- a/crates/nu-engine/src/scope.rs +++ b/crates/nu-engine/src/scope.rs @@ -12,7 +12,7 @@ pub fn create_scope( ) -> Result { 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, &'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 { + 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 { 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 { + pub fn collect_commands(&self, span: Span) -> Vec { 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 { 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 { + pub fn collect_modules(&self, span: Span) -> Vec { 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 = module + .decls + .keys() + .map(|bytes| Value::string(String::from_utf8_lossy(bytes), span)) + .collect(); + + let export_aliases: Vec = 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(), diff --git a/crates/nu-parser/src/parse_keywords.rs b/crates/nu-parser/src/parse_keywords.rs index 2ef8db284e..e2f03cc8d5 100644 --- a/crates/nu-parser/src/parse_keywords.rs +++ b/crates/nu-parser/src/parse_keywords.rs @@ -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) { 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) { + 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 { + 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) { +) -> (Block, Module, Vec, Option) { 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) { // 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), diff --git a/crates/nu-parser/src/parser.rs b/crates/nu-parser/src/parser.rs index 67578c46f2..f143e8a798 100644 --- a/crates/nu-parser/src/parser.rs +++ b/crates/nu-parser/src/parser.rs @@ -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); diff --git a/crates/nu-protocol/src/engine/engine_state.rs b/crates/nu-protocol/src/engine/engine_state.rs index 0ced8ec66f..106e8d1bd9 100644 --- a/crates/nu-protocol/src/engine/engine_state.rs +++ b/crates/nu-protocol/src/engine/engine_state.rs @@ -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>, + module_comments: HashMap>, +} + +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) { + self.alias_comments.insert(alias_id, comments); + } + + pub fn add_module_comments(&mut self, module_id: ModuleId, comments: Vec) { + 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>, blocks: Vec, modules: Vec, + usage: Usage, pub scope: ScopeFrame, pub ctrlc: Option>, 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> { 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], + ) -> 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 { @@ -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 { + /// Get all aliases within scope, sorted by the alias names + pub fn get_aliases_sorted( + &self, + include_hidden: bool, + ) -> impl Iterator, 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, 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, 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 { - 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, 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>, // indexed by AliasId pub blocks: Vec, // indexed by BlockId modules: Vec, // indexed by ModuleId + usage: Usage, pub scope: Vec, #[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) -> 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, replacement: Vec) { + pub fn add_alias(&mut self, name: Vec, replacement: Vec, comments: Vec) { 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::*; diff --git a/crates/nu-protocol/src/engine/overlay.rs b/crates/nu-protocol/src/engine/overlay.rs index c7a673630c..f926851fab 100644 --- a/crates/nu-protocol/src/engine/overlay.rs +++ b/crates/nu-protocol/src/engine/overlay.rs @@ -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, - /// Deactivated overlays from permanent state. - /// ! Stores OverlayIds from the permanent state, not from this frame. ! - // removed_overlays: Vec, - /// Removed overlays from previous scope frames / permanent state pub removed_overlays: Vec>, @@ -281,3 +277,15 @@ impl<'a> Borrow for (Vec, Type) { self } } + +impl Default for Visibility { + fn default() -> Self { + Self::new() + } +} + +impl Default for ScopeFrame { + fn default() -> Self { + Self::new() + } +} diff --git a/crates/nu-protocol/src/shell_error.rs b/crates/nu-protocol/src/shell_error.rs index a0ca4ef07c..ee212ae02d 100644 --- a/crates/nu-protocol/src/shell_error.rs +++ b/crates/nu-protocol/src/shell_error.rs @@ -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 diff --git a/crates/nu-protocol/src/signature.rs b/crates/nu-protocol/src/signature.rs index fd44604ba6..1ac3c3d32e 100644 --- a/crates/nu-protocol/src/signature.rs +++ b/crates/nu-protocol/src/signature.rs @@ -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, diff --git a/src/tests/test_custom_commands.rs b/src/tests/test_custom_commands.rs index 3c651f884b..6a1e142f0a 100644 --- a/src/tests/test_custom_commands.rs +++ b/src/tests/test_custom_commands.rs @@ -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", ) } diff --git a/tests/modules/mod.rs b/tests/modules/mod.rs index 25a13525ca..d01f0970be 100644 --- a/tests/modules/mod.rs +++ b/tests/modules/mod.rs @@ -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", diff --git a/tests/overlays/mod.rs b/tests/overlays/mod.rs index bc3a57a6ed..caf140e437 100644 --- a/tests/overlays/mod.rs +++ b/tests/overlays/mod.rs @@ -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]