Substring Match Algorithm (#15511)

<!--
if this PR closes one or more issues, you can automatically link the PR
with
them by using one of the [*linking
keywords*](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword),
e.g.
- this PR should close #xxxx
- fixes #xxxx

you can also mention related issues, PRs or discussions!
-->

# Description
<!--
Thank you for improving Nushell. Please, check our [contributing
guide](../CONTRIBUTING.md) and talk to the core team before making major
changes.

Description of your pull request goes here. **Provide examples and/or
screenshots** if your changes affect the user experience.
-->
This PR should close #15474 .

# User-Facing Changes
<!-- List of all changes that impact the user experience here. This
helps us keep track of breaking 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.
This commit is contained in:
vansh284
2025-04-11 11:15:36 +02:00
committed by GitHub
parent f8ed4b45fd
commit 61dbcf3de6
6 changed files with 58 additions and 17 deletions

View File

@ -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(

View File

@ -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<T> {
/// 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<T> 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<T> 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<T> NuMatcher<'_, T> {
/// Get all the items that matched (sorted)
pub fn results(self) -> Vec<T> {
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<CompletionAlgorithm> 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<String> for MatchAlgorithm {
fn try_from(value: String) -> Result<Self, Self::Error> {
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)]

View File

@ -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<T: Completer> Completer for CustomCompletion<T> {
{
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<T: Completer> Completer for CustomCompletion<T> {
.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
}
}
}
}

View File

@ -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);

View File

@ -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<Self, Self::Err> {
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'"),
}
}
}

View File

@ -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"