add attr category @category to custom command attributes (#15137)

# 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 <a> <b>

Flags:
  -h, --help: Display the help message for this command

Parameters:
  a <int>: First number to add
  b <int>: 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
<!-- List of all changes that impact the user experience here. This
helps us keep track of breaking changes. -->

# Tests + Formatting
<!--
Don't forget to add tests that cover your changes.

Make sure you've run and fixed any issues with these commands:

- `cargo fmt --all -- --check` to check standard code formatting (`cargo
fmt --all` applies these changes)
- `cargo clippy --workspace -- -D warnings -D clippy::unwrap_used` to
check that you're using the standard code style
- `cargo test --workspace` to check that all tests pass (on Windows make
sure to [enable developer
mode](https://learn.microsoft.com/en-us/windows/apps/get-started/developer-mode-features-and-debugging))
- `cargo run -- -c "use toolkit.nu; toolkit test stdlib"` to run the
tests for the standard library

> **Note**
> from `nushell` you can also use the `toolkit` as follows
> ```bash
> use toolkit.nu # or use an `env_change` hook to activate it
automatically
> toolkit check pr
> ```
-->

# After Submitting
<!-- If your PR had any user-facing changes, update [the
documentation](https://github.com/nushell/nushell.github.io) after the
PR is merged, if necessary. This will help us keep the docs up to date.
-->

/cc @Bahex
This commit is contained in:
Darren Schroeder 2025-02-18 15:35:52 -06:00 committed by GitHub
parent 5d1e2b1df1
commit 7636963732
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 132 additions and 6 deletions

View File

@ -1317,7 +1317,7 @@ fn attribute_completions() {
let suggestions = completer.complete("@", 1); let suggestions = completer.complete("@", 1);
// Only checking for the builtins and not the std attributes // Only checking for the builtins and not the std attributes
let expected: Vec<String> = vec!["example".into(), "search-terms".into()]; let expected: Vec<String> = vec!["category".into(), "example".into(), "search-terms".into()];
// Match results // Match results
match_suggestions(&expected, &suggestions); match_suggestions(&expected, &suggestions);

View File

@ -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<PipelineData, ShellError> {
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<PipelineData, ShellError> {
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<Example> {
vec![Example {
description: "Add a category to a custom command",
example: r###"# Double numbers
@category math
def double []: [number -> number] { $in * 2 }"###,
result: None,
}]
}
}

View File

@ -1,5 +1,7 @@
mod category;
mod example; mod example;
mod search_terms; mod search_terms;
pub use category::AttrCategory;
pub use example::AttrExample; pub use example::AttrExample;
pub use search_terms::AttrSearchTerms; pub use search_terms::AttrSearchTerms;

View File

@ -16,6 +16,7 @@ pub fn create_default_context() -> EngineState {
// Core // Core
bind_command! { bind_command! {
Alias, Alias,
AttrCategory,
AttrExample, AttrExample,
AttrSearchTerms, AttrSearchTerms,
Break, Break,

View File

@ -1027,7 +1027,7 @@ mod tests {
#[cfg(not(windows))] #[cfg(not(windows))]
assert!(hover_text.contains("SLEEP")); assert!(hover_text.contains("SLEEP"));
#[cfg(windows)] #[cfg(windows)]
assert!(hover_text.starts_with("NAME\r\n Start-Sleep")); assert!(hover_text.contains("Start-Sleep"));
} }
#[test] #[test]

View File

@ -12,6 +12,7 @@ use nu_protocol::{
Argument, AttributeBlock, Block, Call, Expr, Expression, ImportPattern, ImportPatternHead, Argument, AttributeBlock, Block, Call, Expr, Expression, ImportPattern, ImportPatternHead,
ImportPatternMember, Pipeline, PipelineElement, ImportPatternMember, Pipeline, PipelineElement,
}, },
category_from_string,
engine::{StateWorkingSet, DEFAULT_OVERLAY_NAME}, engine::{StateWorkingSet, DEFAULT_OVERLAY_NAME},
eval_const::eval_constant, eval_const::eval_constant,
parser_path::ParserPath, parser_path::ParserPath,
@ -520,7 +521,7 @@ fn parse_def_inner(
let (desc, extra_desc) = working_set.build_desc(&lite_command.comments); 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); handle_special_attributes(attributes, working_set);
// Checking that the function is used with the correct name // Checking that the function is used with the correct name
@ -733,6 +734,7 @@ fn parse_def_inner(
signature.extra_description = extra_desc; signature.extra_description = extra_desc;
signature.allows_unknown_args = has_wrapped; signature.allows_unknown_args = has_wrapped;
signature.search_terms = search_terms; signature.search_terms = search_terms;
signature.category = category_from_string(&category);
*declaration = signature *declaration = signature
.clone() .clone()
@ -786,7 +788,7 @@ fn parse_extern_inner(
let (description, extra_description) = working_set.build_desc(&lite_command.comments); 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); handle_special_attributes(attributes, working_set);
// Checking that the function is used with the correct name // Checking that the function is used with the correct name
@ -891,6 +893,7 @@ fn parse_extern_inner(
signature.extra_description = extra_description; signature.extra_description = extra_description;
signature.search_terms = search_terms; signature.search_terms = search_terms;
signature.allows_unknown_args = true; signature.allows_unknown_args = true;
signature.category = category_from_string(&category);
if let Some(block_id) = body.and_then(|x| x.as_block()) { if let Some(block_id) = body.and_then(|x| x.as_block()) {
if signature.rest_positional.is_none() { 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) Expression::new(working_set, Expr::Call(call), call_span, Type::Any)
} }
#[allow(clippy::type_complexity)]
fn handle_special_attributes( fn handle_special_attributes(
attributes: Vec<(String, Value)>, attributes: Vec<(String, Value)>,
working_set: &mut StateWorkingSet<'_>, working_set: &mut StateWorkingSet<'_>,
) -> (Vec<(String, Value)>, Vec<CustomExample>, Vec<String>) { ) -> (
Vec<(String, Value)>,
Vec<CustomExample>,
Vec<String>,
String,
) {
let mut attribute_vals = vec![]; let mut attribute_vals = vec![];
let mut examples = vec![]; let mut examples = vec![];
let mut search_terms = vec![]; let mut search_terms = vec![];
let mut category = String::new();
for (name, value) in attributes { for (name, value) in attributes {
let val_span = value.span(); let val_span = value.span();
@ -986,12 +996,27 @@ fn handle_special_attributes(
working_set.error(e.wrap(working_set, val_span)); working_set.error(e.wrap(working_set, val_span));
} }
}, },
"category" => match <String>::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.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> { fn check_alias_name<'a>(working_set: &mut StateWorkingSet, spans: &'a [Span]) -> Option<&'a Span> {

View File

@ -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`] /// Signature information of a [`Command`]
#[derive(Clone, Debug, Serialize, Deserialize)] #[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Signature { pub struct Signature {