Expand Nushell's help system (#7611)

This commit is contained in:
Jakub Žádník 2022-12-30 17:44:37 +02:00 committed by GitHub
parent f3d2be7a56
commit 8bfcea8054
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 1509 additions and 446 deletions

View File

@ -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.

View 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 {})
}
}

View 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 {})
}
}

View 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 {})
}
}

View File

@ -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;

View File

@ -50,6 +50,9 @@ pub fn create_default_context() -> EngineState {
Extern,
For,
Help,
HelpAliases,
HelpCommands,
HelpModules,
HelpOperators,
Hide,
HideEnv,

View File

@ -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 {

View File

@ -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 => {

View File

@ -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"));
}

View File

@ -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"));
})
}

View File

@ -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`"
));
}

View File

@ -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;

View File

@ -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;

View File

@ -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(),

View File

@ -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),

View File

@ -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);

View File

@ -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::*;

View File

@ -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()
}
}

View File

@ -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

View File

@ -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,

View File

@ -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",
)
}

View File

@ -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",

View File

@ -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]