From 7636963732a29f7682461e4c6edda14989770f60 Mon Sep 17 00:00:00 2001 From: Darren Schroeder <343840+fdncred@users.noreply.github.com> Date: Tue, 18 Feb 2025 15:35:52 -0600 Subject: [PATCH] add `attr category` `@category` to custom command attributes (#15137) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Description This PR adds the `@category` attribute to nushell for use with custom commands. ### Example Code ```nushell # Some example with category @category "math" @search-terms "addition" @example "add two numbers together" { blah 5 6 } --result 11 def blah [ a: int # First number to add b: int # Second number to add ] { $a + $b } ``` #### Source & Help ```nushell ❯ source blah.nu ❯ help blah Some example with category Search terms: addition Usage: > blah Flags: -h, --help: Display the help message for this command Parameters: a : First number to add b : Second number to add Input/output types: ╭─#─┬─input─┬─output─╮ │ 0 │ any │ any │ ╰───┴───────┴────────╯ Examples: add two numbers together > blah 5 6 11 ``` #### Show the category ```nushell ❯ help commands | where name == blah ╭─#─┬─name─┬─category─┬─command_type─┬────────description─────────┬─────params─────┬──input_output──┬─search_terms─┬─is_const─╮ │ 0 │ blah │ math │ custom │ Some example with category │ [table 3 rows] │ [list 0 items] │ addition │ false │ ╰───┴──────┴──────────┴──────────────┴────────────────────────────┴────────────────┴────────────────┴──────────────┴──────────╯ ``` # User-Facing Changes # Tests + Formatting # After Submitting /cc @Bahex --- crates/nu-cli/tests/completions/mod.rs | 2 +- .../src/core_commands/attr/category.rs | 61 +++++++++++++++++++ .../nu-cmd-lang/src/core_commands/attr/mod.rs | 2 + crates/nu-cmd-lang/src/default_context.rs | 1 + crates/nu-lsp/src/lib.rs | 2 +- crates/nu-parser/src/parse_keywords.rs | 33 ++++++++-- crates/nu-protocol/src/signature.rs | 37 +++++++++++ 7 files changed, 132 insertions(+), 6 deletions(-) create mode 100644 crates/nu-cmd-lang/src/core_commands/attr/category.rs diff --git a/crates/nu-cli/tests/completions/mod.rs b/crates/nu-cli/tests/completions/mod.rs index dfc8f3d679..d97916f2c9 100644 --- a/crates/nu-cli/tests/completions/mod.rs +++ b/crates/nu-cli/tests/completions/mod.rs @@ -1317,7 +1317,7 @@ fn attribute_completions() { let suggestions = completer.complete("@", 1); // Only checking for the builtins and not the std attributes - let expected: Vec = vec!["example".into(), "search-terms".into()]; + let expected: Vec = vec!["category".into(), "example".into(), "search-terms".into()]; // Match results match_suggestions(&expected, &suggestions); diff --git a/crates/nu-cmd-lang/src/core_commands/attr/category.rs b/crates/nu-cmd-lang/src/core_commands/attr/category.rs new file mode 100644 index 0000000000..1633f43f28 --- /dev/null +++ b/crates/nu-cmd-lang/src/core_commands/attr/category.rs @@ -0,0 +1,61 @@ +use nu_engine::command_prelude::*; + +#[derive(Clone)] +pub struct AttrCategory; + +impl Command for AttrCategory { + fn name(&self) -> &str { + "attr category" + } + + fn signature(&self) -> Signature { + Signature::build("attr category") + .input_output_type(Type::Nothing, Type::list(Type::String)) + .allow_variants_without_examples(true) + .required( + "category", + SyntaxShape::String, + "Category of the custom command.", + ) + .category(Category::Core) + } + + fn description(&self) -> &str { + "Attribute for adding a category to custom commands." + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + _input: PipelineData, + ) -> Result { + let arg: String = call.req(engine_state, stack, 0)?; + Ok(Value::string(arg, call.head).into_pipeline_data()) + } + + fn run_const( + &self, + working_set: &StateWorkingSet, + call: &Call, + _input: PipelineData, + ) -> Result { + let arg: String = call.req_const(working_set, 0)?; + Ok(Value::string(arg, call.head).into_pipeline_data()) + } + + fn is_const(&self) -> bool { + true + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Add a category to a custom command", + example: r###"# Double numbers + @category math + def double []: [number -> number] { $in * 2 }"###, + result: None, + }] + } +} diff --git a/crates/nu-cmd-lang/src/core_commands/attr/mod.rs b/crates/nu-cmd-lang/src/core_commands/attr/mod.rs index 0d2a0e6d3c..6d8c641b35 100644 --- a/crates/nu-cmd-lang/src/core_commands/attr/mod.rs +++ b/crates/nu-cmd-lang/src/core_commands/attr/mod.rs @@ -1,5 +1,7 @@ +mod category; mod example; mod search_terms; +pub use category::AttrCategory; pub use example::AttrExample; pub use search_terms::AttrSearchTerms; diff --git a/crates/nu-cmd-lang/src/default_context.rs b/crates/nu-cmd-lang/src/default_context.rs index f98f7ee39a..8929f75fa0 100644 --- a/crates/nu-cmd-lang/src/default_context.rs +++ b/crates/nu-cmd-lang/src/default_context.rs @@ -16,6 +16,7 @@ pub fn create_default_context() -> EngineState { // Core bind_command! { Alias, + AttrCategory, AttrExample, AttrSearchTerms, Break, diff --git a/crates/nu-lsp/src/lib.rs b/crates/nu-lsp/src/lib.rs index 336496e719..4e1f4d37ff 100644 --- a/crates/nu-lsp/src/lib.rs +++ b/crates/nu-lsp/src/lib.rs @@ -1027,7 +1027,7 @@ mod tests { #[cfg(not(windows))] assert!(hover_text.contains("SLEEP")); #[cfg(windows)] - assert!(hover_text.starts_with("NAME\r\n Start-Sleep")); + assert!(hover_text.contains("Start-Sleep")); } #[test] diff --git a/crates/nu-parser/src/parse_keywords.rs b/crates/nu-parser/src/parse_keywords.rs index dbe8c7292c..31dfbb6ea3 100644 --- a/crates/nu-parser/src/parse_keywords.rs +++ b/crates/nu-parser/src/parse_keywords.rs @@ -12,6 +12,7 @@ use nu_protocol::{ Argument, AttributeBlock, Block, Call, Expr, Expression, ImportPattern, ImportPatternHead, ImportPatternMember, Pipeline, PipelineElement, }, + category_from_string, engine::{StateWorkingSet, DEFAULT_OVERLAY_NAME}, eval_const::eval_constant, parser_path::ParserPath, @@ -520,7 +521,7 @@ fn parse_def_inner( let (desc, extra_desc) = working_set.build_desc(&lite_command.comments); - let (attribute_vals, examples, search_terms) = + let (attribute_vals, examples, search_terms, category) = handle_special_attributes(attributes, working_set); // Checking that the function is used with the correct name @@ -733,6 +734,7 @@ fn parse_def_inner( signature.extra_description = extra_desc; signature.allows_unknown_args = has_wrapped; signature.search_terms = search_terms; + signature.category = category_from_string(&category); *declaration = signature .clone() @@ -786,7 +788,7 @@ fn parse_extern_inner( let (description, extra_description) = working_set.build_desc(&lite_command.comments); - let (attribute_vals, examples, search_terms) = + let (attribute_vals, examples, search_terms, category) = handle_special_attributes(attributes, working_set); // Checking that the function is used with the correct name @@ -891,6 +893,7 @@ fn parse_extern_inner( signature.extra_description = extra_description; signature.search_terms = search_terms; signature.allows_unknown_args = true; + signature.category = category_from_string(&category); if let Some(block_id) = body.and_then(|x| x.as_block()) { if signature.rest_positional.is_none() { @@ -947,13 +950,20 @@ fn parse_extern_inner( Expression::new(working_set, Expr::Call(call), call_span, Type::Any) } +#[allow(clippy::type_complexity)] fn handle_special_attributes( attributes: Vec<(String, Value)>, working_set: &mut StateWorkingSet<'_>, -) -> (Vec<(String, Value)>, Vec, Vec) { +) -> ( + Vec<(String, Value)>, + Vec, + Vec, + String, +) { let mut attribute_vals = vec![]; let mut examples = vec![]; let mut search_terms = vec![]; + let mut category = String::new(); for (name, value) in attributes { let val_span = value.span(); @@ -986,12 +996,27 @@ fn handle_special_attributes( working_set.error(e.wrap(working_set, val_span)); } }, + "category" => match ::from_value(value) { + Ok(term) => { + category.push_str(&term); + } + Err(_) => { + let e = ShellError::GenericError { + error: "nu::shell::invalid_category".into(), + msg: "Value couldn't be converted to category".into(), + span: Some(val_span), + help: Some("Is `attr category` shadowed?".into()), + inner: vec![], + }; + working_set.error(e.wrap(working_set, val_span)); + } + }, _ => { attribute_vals.push((name, value)); } } } - (attribute_vals, examples, search_terms) + (attribute_vals, examples, search_terms, category) } fn check_alias_name<'a>(working_set: &mut StateWorkingSet, spans: &'a [Span]) -> Option<&'a Span> { diff --git a/crates/nu-protocol/src/signature.rs b/crates/nu-protocol/src/signature.rs index bc975848f8..8405846eb2 100644 --- a/crates/nu-protocol/src/signature.rs +++ b/crates/nu-protocol/src/signature.rs @@ -113,6 +113,43 @@ impl std::fmt::Display for Category { } } +pub fn category_from_string(category: &str) -> Category { + match category { + "bits" => Category::Bits, + "bytes" => Category::Bytes, + "chart" => Category::Chart, + "conversions" => Category::Conversions, + // Let's protect our own "core" commands by preventing scripts from having this category. + "core" => Category::Custom("custom_core".to_string()), + "database" => Category::Database, + "date" => Category::Date, + "debug" => Category::Debug, + "default" => Category::Default, + "deprecated" => Category::Deprecated, + "removed" => Category::Removed, + "env" => Category::Env, + "experimental" => Category::Experimental, + "filesystem" => Category::FileSystem, + "filter" => Category::Filters, + "formats" => Category::Formats, + "generators" => Category::Generators, + "hash" => Category::Hash, + "history" => Category::History, + "math" => Category::Math, + "misc" => Category::Misc, + "network" => Category::Network, + "path" => Category::Path, + "platform" => Category::Platform, + "plugin" => Category::Plugin, + "random" => Category::Random, + "shells" => Category::Shells, + "strings" => Category::Strings, + "system" => Category::System, + "viewers" => Category::Viewers, + _ => Category::Custom(category.to_string()), + } +} + /// Signature information of a [`Command`] #[derive(Clone, Debug, Serialize, Deserialize)] pub struct Signature {