Custom completions: Inherit case_sensitive option from $env.config (#14738)

# Description

Currently, if a custom completer returns a record containing an
`options` field, but these options don't specify `case_sensitive`,
`case_sensitive` will be true. This PR instead makes the default value
whatever the user specified in `$env.config.completions.case_sensitive`.

The match algorithm option already does this. `positional` is also
inherited from the global config, although user's can't actually specify
that one themselves in `$env.config` (I'm planning on getting rid of
`positional` in a separate PR).

# User-Facing Changes

For those making custom completions, if they need matching to be done
case-sensitively and:
- their completer returns a record rather than a list,
- and the record contains an `options` field,
- and the `options` field is a record,
- and the record doesn't contain a `case_sensitive` option,

then they will need to specify `case_sensitive: true` in their custom
completer's options. Otherwise, if the user sets
`$env.config.completions.case_sensitive = false`, their custom completer
will also use case-insensitive matching.

Others shouldn't have to make any changes.

# Tests + Formatting

Updated tests to check if `case_sensitive`. Basically rewrote them,
actually. I figured it'd be better to make a single helper function that
takes completer options and completion suggestions and generates a
completer from that rather than having multiple fixtures providing
different completers.

# After Submitting

Probably needs to be noted in the release notes, but I don't think the
[docs](https://www.nushell.sh/book/custom_completions.html#options-for-custom-completions)
need to be updated.
This commit is contained in:
Yash Thakur 2025-01-07 12:52:31 -05:00 committed by GitHub
parent dad956b2ee
commit 787f292ca7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 89 additions and 76 deletions

View File

@ -66,7 +66,7 @@ impl Completer for CustomCompletion {
PipelineData::empty(), PipelineData::empty(),
); );
let mut custom_completion_options = None; let mut completion_options = completion_options.clone();
let mut should_sort = true; let mut should_sort = true;
// Parse result // Parse result
@ -89,25 +89,24 @@ impl Completer for CustomCompletion {
should_sort = sort; should_sort = sort;
} }
custom_completion_options = Some(CompletionOptions { if let Some(case_sensitive) = options
case_sensitive: options .get("case_sensitive")
.get("case_sensitive") .and_then(|val| val.as_bool().ok())
.and_then(|val| val.as_bool().ok()) {
.unwrap_or(true), completion_options.case_sensitive = case_sensitive;
positional: options }
.get("positional") if let Some(positional) =
.and_then(|val| val.as_bool().ok()) options.get("positional").and_then(|val| val.as_bool().ok())
.unwrap_or(completion_options.positional), {
match_algorithm: match options.get("completion_algorithm") { completion_options.positional = positional;
Some(option) => option }
.coerce_string() if let Some(algorithm) = options
.ok() .get("completion_algorithm")
.and_then(|option| option.try_into().ok()) .and_then(|option| option.coerce_string().ok())
.unwrap_or(completion_options.match_algorithm), .and_then(|option| option.try_into().ok())
None => completion_options.match_algorithm, {
}, completion_options.match_algorithm = algorithm;
sort: completion_options.sort, }
});
} }
completions completions
@ -117,8 +116,7 @@ impl Completer for CustomCompletion {
}) })
.unwrap_or_default(); .unwrap_or_default();
let options = custom_completion_options.unwrap_or(completion_options.clone()); let mut matcher = NuMatcher::new(String::from_utf8_lossy(prefix), completion_options);
let mut matcher = NuMatcher::new(String::from_utf8_lossy(prefix), options);
if should_sort { if should_sort {
for sugg in suggestions { for sugg in suggestions {

View File

@ -62,50 +62,29 @@ fn extern_completer() -> NuCompleter {
NuCompleter::new(Arc::new(engine), Arc::new(stack)) NuCompleter::new(Arc::new(engine), Arc::new(stack))
} }
#[fixture] fn custom_completer_with_options(
fn completer_strings_with_options() -> NuCompleter { global_opts: &str,
// Create a new engine completer_opts: &str,
completions: &[&str],
) -> NuCompleter {
let (_, _, mut engine, mut stack) = new_engine(); let (_, _, mut engine, mut stack) = new_engine();
// Add record value as example let command = format!(
let record = r#" r#"
# To test that the config setting has no effect on the custom completions {}
$env.config.completions.algorithm = "fuzzy" def comp [] {{
def animals [] { {{ completions: [{}], options: {{ {} }} }}
{ }}
# Very rare and totally real animals def my-command [arg: string@comp] {{}}"#,
completions: ["Abcdef", "Foo Abcdef", "Acd Bar" ], global_opts,
options: { completions
completion_algorithm: "prefix", .iter()
positional: false, .map(|comp| format!("'{}'", comp))
case_sensitive: false, .collect::<Vec<_>>()
} .join(", "),
} completer_opts,
} );
def my-command [animal: string@animals] { print $animal }"#;
assert!(support::merge_input(record.as_bytes(), &mut engine, &mut stack).is_ok());
// Instantiate a new completer
NuCompleter::new(Arc::new(engine), Arc::new(stack))
}
#[fixture]
fn completer_strings_no_sort() -> NuCompleter {
// Create a new engine
let (_, _, mut engine, mut stack) = new_engine();
let command = r#"
def animals [] {
{
completions: ["zzzfoo", "foo", "not matched", "abcfoo" ],
options: {
completion_algorithm: "fuzzy",
sort: false,
}
}
}
def my-command [animal: string@animals] { print $animal }"#;
assert!(support::merge_input(command.as_bytes(), &mut engine, &mut stack).is_ok()); assert!(support::merge_input(command.as_bytes(), &mut engine, &mut stack).is_ok());
// Instantiate a new completer
NuCompleter::new(Arc::new(engine), Arc::new(stack)) NuCompleter::new(Arc::new(engine), Arc::new(stack))
} }
@ -217,23 +196,59 @@ fn variables_customcompletion_subcommands_with_customcompletion_2(
match_suggestions(&expected, &suggestions); match_suggestions(&expected, &suggestions);
} }
#[rstest] /// $env.config should be overridden by the custom completer's options
fn customcompletions_substring_matching(mut completer_strings_with_options: NuCompleter) { #[test]
let suggestions = completer_strings_with_options.complete("my-command Abcd", 15); fn customcompletions_override_options() {
let mut completer = custom_completer_with_options(
r#"$env.config.completions.algorithm = "fuzzy"
$env.config.completions.case_sensitive = false"#,
r#"completion_algorithm: "prefix",
positional: false,
case_sensitive: true,
sort: true"#,
&["Foo Abcdef", "Abcdef", "Acd Bar"],
);
// positional: false should make it do substring matching
// sort: true should force sorting
let expected: Vec<String> = vec!["Abcdef".into(), "Foo Abcdef".into()]; let expected: Vec<String> = vec!["Abcdef".into(), "Foo Abcdef".into()];
let suggestions = completer.complete("my-command Abcd", 15);
match_suggestions(&expected, &suggestions);
// Custom options should make case-sensitive
let suggestions = completer.complete("my-command aBcD", 15);
assert!(suggestions.is_empty());
}
/// $env.config should be inherited by the custom completer's options
#[test]
fn customcompletions_inherit_options() {
let mut completer = custom_completer_with_options(
r#"$env.config.completions.algorithm = "fuzzy"
$env.config.completions.case_sensitive = false"#,
"",
&["Foo Abcdef", "Abcdef", "Acd Bar"],
);
// Make sure matching is fuzzy
let suggestions = completer.complete("my-command Acd", 14);
let expected: Vec<String> = vec!["Acd Bar".into(), "Abcdef".into(), "Foo Abcdef".into()];
match_suggestions(&expected, &suggestions);
// Custom options should make matching case insensitive
let suggestions = completer.complete("my-command acd", 14);
match_suggestions(&expected, &suggestions); match_suggestions(&expected, &suggestions);
} }
#[rstest] #[test]
fn customcompletions_case_insensitive(mut completer_strings_with_options: NuCompleter) { fn customcompletions_no_sort() {
let suggestions = completer_strings_with_options.complete("my-command foo", 14); let mut completer = custom_completer_with_options(
let expected: Vec<String> = vec!["Foo Abcdef".into()]; "",
match_suggestions(&expected, &suggestions); r#"completion_algorithm: "fuzzy",
} sort: false"#,
&["zzzfoo", "foo", "not matched", "abcfoo"],
#[rstest] );
fn customcompletions_no_sort(mut completer_strings_no_sort: NuCompleter) { let suggestions = completer.complete("my-command foo", 14);
let suggestions = completer_strings_no_sort.complete("my-command foo", 14);
let expected: Vec<String> = vec!["zzzfoo".into(), "foo".into(), "abcfoo".into()]; let expected: Vec<String> = vec!["zzzfoo".into(), "foo".into(), "abcfoo".into()];
match_suggestions(&expected, &suggestions); match_suggestions(&expected, &suggestions);
} }