refactor(completion, parser): move custom_completion info from Expression to Signature (#15613)

Restricts custom completion from universal to internal arguments only.

Pros:
1. Less memory
2. More flexible for later customizations, e.g. #14923 

Cons:
1. limited customization capabilities, but at least covers all currently
existing features in nushell.

# Description

Mostly vibe coded by [Zed AI](https://zed.dev/ai) with a single prompt.
LGTM, but I'm not so sure @ysthakur 

# User-Facing Changes

Hopefully none.

# Tests + Formatting

+3

# After Submitting

---------

Co-authored-by: Yash Thakur <45539777+ysthakur@users.noreply.github.com>
This commit is contained in:
zc he
2025-07-25 02:21:58 +08:00
committed by GitHub
parent 1b01625e1e
commit 71baeff287
12 changed files with 194 additions and 144 deletions

View File

@ -348,8 +348,43 @@ impl NuCompleter {
for (arg_idx, arg) in call.arguments.iter().enumerate() {
let span = arg.span();
if span.contains(pos) {
// if customized completion specified, it has highest priority
if let Some(decl_id) = arg.expr().and_then(|e| e.custom_completion) {
// Get custom completion from PositionalArg or Flag
let custom_completion_decl_id = {
// Check PositionalArg or Flag from Signature
let signature = working_set.get_decl(call.decl_id).signature();
match arg {
// For named arguments, check Flag
Argument::Named((name, short, value)) => {
if value.as_ref().is_none_or(|e| !e.span.contains(pos)) {
None
} else {
// If we're completing the value of the flag,
// search for the matching custom completion decl_id (long or short)
let flag =
signature.get_long_flag(&name.item).or_else(|| {
short.as_ref().and_then(|s| {
signature.get_short_flag(
s.item.chars().next().unwrap_or('_'),
)
})
});
flag.and_then(|f| f.custom_completion)
}
}
// For positional arguments, check PositionalArg
Argument::Positional(_) => {
// Find the right positional argument by index
let arg_pos = positional_arg_indices.len();
signature
.get_positional(arg_pos)
.and_then(|pos_arg| pos_arg.custom_completion)
}
_ => None,
}
};
if let Some(decl_id) = custom_completion_decl_id {
// for `--foo <tab>` and `--foo=<tab>`, the arg span should be trimmed
let (new_span, prefix) = if matches!(arg, Argument::Named(_)) {
strip_placeholder_with_rsplit(

View File

@ -97,8 +97,11 @@ fn extern_completer() -> NuCompleter {
// Add record value as example
let record = r#"
def animals [] { [ "cat", "dog", "eel" ] }
def fruits [] { [ "apple", "banana" ] }
extern spam [
animal: string@animals
fruit?: string@fruits
...rest: string@animals
--foo (-f): string@animals
-b: string@animals
]
@ -2261,6 +2264,22 @@ fn extern_custom_completion_positional(mut extern_completer: NuCompleter) {
match_suggestions(&expected, &suggestions);
}
#[rstest]
fn extern_custom_completion_optional(mut extern_completer: NuCompleter) {
let suggestions = extern_completer.complete("spam foo -f bar ", 16);
let expected: Vec<_> = vec!["apple", "banana"];
match_suggestions(&expected, &suggestions);
}
#[rstest]
fn extern_custom_completion_rest(mut extern_completer: NuCompleter) {
let suggestions = extern_completer.complete("spam foo -f bar baz ", 20);
let expected: Vec<_> = vec!["cat", "dog", "eel"];
match_suggestions(&expected, &suggestions);
let suggestions = extern_completer.complete("spam foo -f bar baz qux ", 24);
match_suggestions(&expected, &suggestions);
}
#[rstest]
fn extern_custom_completion_long_flag_1(mut extern_completer: NuCompleter) {
let suggestions = extern_completer.complete("spam --foo=", 11);
@ -2289,6 +2308,17 @@ fn extern_custom_completion_short_flag(mut extern_completer: NuCompleter) {
match_suggestions(&expected, &suggestions);
}
/// When we're completing the flag name itself, not its value,
/// custom completions should not be used
#[rstest]
fn custom_completion_flag_name_not_value(mut extern_completer: NuCompleter) {
let suggestions = extern_completer.complete("spam --f", 8);
match_suggestions(&vec!["--foo"], &suggestions);
// Also test with partial short flag
let suggestions = extern_completer.complete("spam -f", 7);
match_suggestions(&vec!["-f"], &suggestions);
}
#[rstest]
fn extern_complete_flags(mut extern_completer: NuCompleter) {
let suggestions = extern_completer.complete("spam -", 6);