From 61dbcf3de69e5793bb8335bea6f0232404ceb332 Mon Sep 17 00:00:00 2001 From: vansh284 <65393463+vansh284@users.noreply.github.com> Date: Fri, 11 Apr 2025 11:15:36 +0200 Subject: [PATCH] Substring Match Algorithm (#15511) # Description This PR should close #15474 . # User-Facing Changes When users set the match algorithm to 'substring' by modifying `$env.config` to `$env.config.completions.algorithm = "substring"``), completions are done based on substring matches. This was previously possible by setting `positional` to be false in custom completers, but doing so now logs a warning as this feature is set to be deprecated and replaced by the new way of setting the matching algorithm to substring based. --- crates/nu-cli/src/completions/completer.rs | 1 - .../src/completions/completion_options.rs | 47 ++++++++++++++++--- .../src/completions/custom_completions.rs | 16 +++++-- crates/nu-cli/tests/completions/mod.rs | 4 +- crates/nu-protocol/src/config/completions.rs | 4 +- .../nu-utils/src/default_files/doc_config.nu | 3 +- 6 files changed, 58 insertions(+), 17 deletions(-) diff --git a/crates/nu-cli/src/completions/completer.rs b/crates/nu-cli/src/completions/completer.rs index 0e010054bc..2da8c35f20 100644 --- a/crates/nu-cli/src/completions/completer.rs +++ b/crates/nu-cli/src/completions/completer.rs @@ -646,7 +646,6 @@ impl NuCompleter { case_sensitive: config.completions.case_sensitive, match_algorithm: config.completions.algorithm.into(), sort: config.completions.sort, - ..Default::default() }; completer.fetch( diff --git a/crates/nu-cli/src/completions/completion_options.rs b/crates/nu-cli/src/completions/completion_options.rs index 1d33ea26a9..136129e56c 100644 --- a/crates/nu-cli/src/completions/completion_options.rs +++ b/crates/nu-cli/src/completions/completion_options.rs @@ -18,6 +18,12 @@ pub enum MatchAlgorithm { /// "git switch" is matched by "git sw" Prefix, + /// Only show suggestions which have a substring matching with the given input + /// + /// Example: + /// "git checkout" is matched by "checkout" + Substring, + /// Only show suggestions which contain the input chars at any place /// /// Example: @@ -36,6 +42,10 @@ enum State { /// Holds (haystack, item) items: Vec<(String, T)>, }, + Substring { + /// Holds (haystack, item) + items: Vec<(String, T)>, + }, Fuzzy { matcher: Matcher, atom: Atom, @@ -64,6 +74,18 @@ impl NuMatcher<'_, T> { state: State::Prefix { items: Vec::new() }, } } + MatchAlgorithm::Substring => { + let lowercase_needle = if options.case_sensitive { + needle.to_owned() + } else { + needle.to_folded_case() + }; + NuMatcher { + options, + needle: lowercase_needle, + state: State::Substring { items: Vec::new() }, + } + } MatchAlgorithm::Fuzzy => { let atom = Atom::new( needle, @@ -102,11 +124,21 @@ impl NuMatcher<'_, T> { } else { Cow::Owned(haystack.to_folded_case()) }; - let matches = if self.options.positional { - haystack_folded.starts_with(self.needle.as_str()) + let matches = haystack_folded.starts_with(self.needle.as_str()); + if matches { + if let Some(item) = item { + items.push((haystack.to_string(), item)); + } + } + matches + } + State::Substring { items } => { + let haystack_folded = if self.options.case_sensitive { + Cow::Borrowed(haystack) } else { - haystack_folded.contains(self.needle.as_str()) + Cow::Owned(haystack.to_folded_case()) }; + let matches = haystack_folded.contains(self.needle.as_str()); if matches { if let Some(item) = item { items.push((haystack.to_string(), item)); @@ -148,7 +180,7 @@ impl NuMatcher<'_, T> { /// Get all the items that matched (sorted) pub fn results(self) -> Vec { match self.state { - State::Prefix { mut items, .. } => { + State::Prefix { mut items, .. } | State::Substring { mut items, .. } => { items.sort_by(|(haystack1, _), (haystack2, _)| { let cmp_sensitive = haystack1.cmp(haystack2); if self.options.case_sensitive { @@ -195,6 +227,7 @@ impl From for MatchAlgorithm { fn from(value: CompletionAlgorithm) -> Self { match value { CompletionAlgorithm::Prefix => MatchAlgorithm::Prefix, + CompletionAlgorithm::Substring => MatchAlgorithm::Substring, CompletionAlgorithm::Fuzzy => MatchAlgorithm::Fuzzy, } } @@ -206,6 +239,7 @@ impl TryFrom for MatchAlgorithm { fn try_from(value: String) -> Result { match value.as_str() { "prefix" => Ok(Self::Prefix), + "substring" => Ok(Self::Substring), "fuzzy" => Ok(Self::Fuzzy), _ => Err(InvalidMatchAlgorithm::Unknown), } @@ -230,7 +264,6 @@ impl std::error::Error for InvalidMatchAlgorithm {} #[derive(Clone)] pub struct CompletionOptions { pub case_sensitive: bool, - pub positional: bool, pub match_algorithm: MatchAlgorithm, pub sort: CompletionSort, } @@ -239,7 +272,6 @@ impl Default for CompletionOptions { fn default() -> Self { Self { case_sensitive: true, - positional: true, match_algorithm: MatchAlgorithm::Prefix, sort: Default::default(), } @@ -256,6 +288,9 @@ mod test { #[case(MatchAlgorithm::Prefix, "example text", "", true)] #[case(MatchAlgorithm::Prefix, "example text", "examp", true)] #[case(MatchAlgorithm::Prefix, "example text", "text", false)] + #[case(MatchAlgorithm::Substring, "example text", "", true)] + #[case(MatchAlgorithm::Substring, "example text", "text", true)] + #[case(MatchAlgorithm::Substring, "example text", "mplxt", false)] #[case(MatchAlgorithm::Fuzzy, "example text", "", true)] #[case(MatchAlgorithm::Fuzzy, "example text", "examp", true)] #[case(MatchAlgorithm::Fuzzy, "example text", "ext", true)] diff --git a/crates/nu-cli/src/completions/custom_completions.rs b/crates/nu-cli/src/completions/custom_completions.rs index 32f088db52..a03a7150b1 100644 --- a/crates/nu-cli/src/completions/custom_completions.rs +++ b/crates/nu-cli/src/completions/custom_completions.rs @@ -1,5 +1,6 @@ use crate::completions::{ - completer::map_value_completions, Completer, CompletionOptions, SemanticSuggestion, + completer::map_value_completions, Completer, CompletionOptions, MatchAlgorithm, + SemanticSuggestion, }; use nu_engine::eval_call; use nu_protocol::{ @@ -102,10 +103,10 @@ impl Completer for CustomCompletion { { completion_options.case_sensitive = case_sensitive; } - if let Some(positional) = - options.get("positional").and_then(|val| val.as_bool().ok()) - { - completion_options.positional = positional; + let positional = + options.get("positional").and_then(|val| val.as_bool().ok()); + if positional.is_some() { + log::warn!("Use of the positional option is deprecated. Use the substring match algorithm instead."); } if let Some(algorithm) = options .get("completion_algorithm") @@ -113,6 +114,11 @@ impl Completer for CustomCompletion { .and_then(|option| option.try_into().ok()) { completion_options.match_algorithm = algorithm; + if let Some(false) = positional { + if completion_options.match_algorithm == MatchAlgorithm::Prefix { + completion_options.match_algorithm = MatchAlgorithm::Substring + } + } } } diff --git a/crates/nu-cli/tests/completions/mod.rs b/crates/nu-cli/tests/completions/mod.rs index 9792f2b19b..8aba4337a8 100644 --- a/crates/nu-cli/tests/completions/mod.rs +++ b/crates/nu-cli/tests/completions/mod.rs @@ -228,14 +228,12 @@ 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, + r#"completion_algorithm: "substring", 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<_> = vec!["Abcdef", "Foo Abcdef"]; let suggestions = completer.complete("my-command Abcd", 15); diff --git a/crates/nu-protocol/src/config/completions.rs b/crates/nu-protocol/src/config/completions.rs index 74c4567d17..5544c2c12f 100644 --- a/crates/nu-protocol/src/config/completions.rs +++ b/crates/nu-protocol/src/config/completions.rs @@ -6,6 +6,7 @@ use crate::engine::Closure; pub enum CompletionAlgorithm { #[default] Prefix, + Substring, Fuzzy, } @@ -15,8 +16,9 @@ impl FromStr for CompletionAlgorithm { fn from_str(s: &str) -> Result { match s.to_ascii_lowercase().as_str() { "prefix" => Ok(Self::Prefix), + "substring" => Ok(Self::Substring), "fuzzy" => Ok(Self::Fuzzy), - _ => Err("'prefix' or 'fuzzy'"), + _ => Err("'prefix' or 'fuzzy' or 'substring'"), } } } diff --git a/crates/nu-utils/src/default_files/doc_config.nu b/crates/nu-utils/src/default_files/doc_config.nu index 874628cf2c..5b53d67de2 100644 --- a/crates/nu-utils/src/default_files/doc_config.nu +++ b/crates/nu-utils/src/default_files/doc_config.nu @@ -115,12 +115,13 @@ $env.config.cursor_shape.vi_normal = "underscore" # Cursor shape in normal vi m # $env.config.completions.* # Apply to the Nushell completion system -# algorithm (string): Either "prefix" or "fuzzy" +# algorithm (string): "prefix", "substring" or "fuzzy" $env.config.completions.algorithm = "prefix" # sort (string): One of "smart" or "alphabetical" # In "smart" mode sort order is based on the "algorithm" setting. # When using the "prefix" algorithm, results are alphabetically sorted. +# When using the "substring" algorithm, results are alphabetically sorted. # When using the "fuzzy" algorithm, results are sorted based on their fuzzy score. $env.config.completions.sort = "smart"