mirror of
https://github.com/nushell/nushell.git
synced 2025-08-09 13:26:01 +02:00
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:
@ -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(
|
||||
|
@ -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)]
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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'"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
|
||||
|
Reference in New Issue
Block a user