mirror of
https://github.com/nushell/nushell.git
synced 2025-08-09 12:55:47 +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,
|
case_sensitive: config.completions.case_sensitive,
|
||||||
match_algorithm: config.completions.algorithm.into(),
|
match_algorithm: config.completions.algorithm.into(),
|
||||||
sort: config.completions.sort,
|
sort: config.completions.sort,
|
||||||
..Default::default()
|
|
||||||
};
|
};
|
||||||
|
|
||||||
completer.fetch(
|
completer.fetch(
|
||||||
|
@ -18,6 +18,12 @@ pub enum MatchAlgorithm {
|
|||||||
/// "git switch" is matched by "git sw"
|
/// "git switch" is matched by "git sw"
|
||||||
Prefix,
|
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
|
/// Only show suggestions which contain the input chars at any place
|
||||||
///
|
///
|
||||||
/// Example:
|
/// Example:
|
||||||
@ -36,6 +42,10 @@ enum State<T> {
|
|||||||
/// Holds (haystack, item)
|
/// Holds (haystack, item)
|
||||||
items: Vec<(String, T)>,
|
items: Vec<(String, T)>,
|
||||||
},
|
},
|
||||||
|
Substring {
|
||||||
|
/// Holds (haystack, item)
|
||||||
|
items: Vec<(String, T)>,
|
||||||
|
},
|
||||||
Fuzzy {
|
Fuzzy {
|
||||||
matcher: Matcher,
|
matcher: Matcher,
|
||||||
atom: Atom,
|
atom: Atom,
|
||||||
@ -64,6 +74,18 @@ impl<T> NuMatcher<'_, T> {
|
|||||||
state: State::Prefix { items: Vec::new() },
|
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 => {
|
MatchAlgorithm::Fuzzy => {
|
||||||
let atom = Atom::new(
|
let atom = Atom::new(
|
||||||
needle,
|
needle,
|
||||||
@ -102,11 +124,21 @@ impl<T> NuMatcher<'_, T> {
|
|||||||
} else {
|
} else {
|
||||||
Cow::Owned(haystack.to_folded_case())
|
Cow::Owned(haystack.to_folded_case())
|
||||||
};
|
};
|
||||||
let matches = if self.options.positional {
|
let matches = haystack_folded.starts_with(self.needle.as_str());
|
||||||
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 {
|
} 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 matches {
|
||||||
if let Some(item) = item {
|
if let Some(item) = item {
|
||||||
items.push((haystack.to_string(), item));
|
items.push((haystack.to_string(), item));
|
||||||
@ -148,7 +180,7 @@ impl<T> NuMatcher<'_, T> {
|
|||||||
/// Get all the items that matched (sorted)
|
/// Get all the items that matched (sorted)
|
||||||
pub fn results(self) -> Vec<T> {
|
pub fn results(self) -> Vec<T> {
|
||||||
match self.state {
|
match self.state {
|
||||||
State::Prefix { mut items, .. } => {
|
State::Prefix { mut items, .. } | State::Substring { mut items, .. } => {
|
||||||
items.sort_by(|(haystack1, _), (haystack2, _)| {
|
items.sort_by(|(haystack1, _), (haystack2, _)| {
|
||||||
let cmp_sensitive = haystack1.cmp(haystack2);
|
let cmp_sensitive = haystack1.cmp(haystack2);
|
||||||
if self.options.case_sensitive {
|
if self.options.case_sensitive {
|
||||||
@ -195,6 +227,7 @@ impl From<CompletionAlgorithm> for MatchAlgorithm {
|
|||||||
fn from(value: CompletionAlgorithm) -> Self {
|
fn from(value: CompletionAlgorithm) -> Self {
|
||||||
match value {
|
match value {
|
||||||
CompletionAlgorithm::Prefix => MatchAlgorithm::Prefix,
|
CompletionAlgorithm::Prefix => MatchAlgorithm::Prefix,
|
||||||
|
CompletionAlgorithm::Substring => MatchAlgorithm::Substring,
|
||||||
CompletionAlgorithm::Fuzzy => MatchAlgorithm::Fuzzy,
|
CompletionAlgorithm::Fuzzy => MatchAlgorithm::Fuzzy,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -206,6 +239,7 @@ impl TryFrom<String> for MatchAlgorithm {
|
|||||||
fn try_from(value: String) -> Result<Self, Self::Error> {
|
fn try_from(value: String) -> Result<Self, Self::Error> {
|
||||||
match value.as_str() {
|
match value.as_str() {
|
||||||
"prefix" => Ok(Self::Prefix),
|
"prefix" => Ok(Self::Prefix),
|
||||||
|
"substring" => Ok(Self::Substring),
|
||||||
"fuzzy" => Ok(Self::Fuzzy),
|
"fuzzy" => Ok(Self::Fuzzy),
|
||||||
_ => Err(InvalidMatchAlgorithm::Unknown),
|
_ => Err(InvalidMatchAlgorithm::Unknown),
|
||||||
}
|
}
|
||||||
@ -230,7 +264,6 @@ impl std::error::Error for InvalidMatchAlgorithm {}
|
|||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct CompletionOptions {
|
pub struct CompletionOptions {
|
||||||
pub case_sensitive: bool,
|
pub case_sensitive: bool,
|
||||||
pub positional: bool,
|
|
||||||
pub match_algorithm: MatchAlgorithm,
|
pub match_algorithm: MatchAlgorithm,
|
||||||
pub sort: CompletionSort,
|
pub sort: CompletionSort,
|
||||||
}
|
}
|
||||||
@ -239,7 +272,6 @@ impl Default for CompletionOptions {
|
|||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
case_sensitive: true,
|
case_sensitive: true,
|
||||||
positional: true,
|
|
||||||
match_algorithm: MatchAlgorithm::Prefix,
|
match_algorithm: MatchAlgorithm::Prefix,
|
||||||
sort: Default::default(),
|
sort: Default::default(),
|
||||||
}
|
}
|
||||||
@ -256,6 +288,9 @@ mod test {
|
|||||||
#[case(MatchAlgorithm::Prefix, "example text", "", true)]
|
#[case(MatchAlgorithm::Prefix, "example text", "", true)]
|
||||||
#[case(MatchAlgorithm::Prefix, "example text", "examp", true)]
|
#[case(MatchAlgorithm::Prefix, "example text", "examp", true)]
|
||||||
#[case(MatchAlgorithm::Prefix, "example text", "text", false)]
|
#[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", "", true)]
|
||||||
#[case(MatchAlgorithm::Fuzzy, "example text", "examp", true)]
|
#[case(MatchAlgorithm::Fuzzy, "example text", "examp", true)]
|
||||||
#[case(MatchAlgorithm::Fuzzy, "example text", "ext", true)]
|
#[case(MatchAlgorithm::Fuzzy, "example text", "ext", true)]
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
use crate::completions::{
|
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_engine::eval_call;
|
||||||
use nu_protocol::{
|
use nu_protocol::{
|
||||||
@ -102,10 +103,10 @@ impl<T: Completer> Completer for CustomCompletion<T> {
|
|||||||
{
|
{
|
||||||
completion_options.case_sensitive = case_sensitive;
|
completion_options.case_sensitive = case_sensitive;
|
||||||
}
|
}
|
||||||
if let Some(positional) =
|
let positional =
|
||||||
options.get("positional").and_then(|val| val.as_bool().ok())
|
options.get("positional").and_then(|val| val.as_bool().ok());
|
||||||
{
|
if positional.is_some() {
|
||||||
completion_options.positional = positional;
|
log::warn!("Use of the positional option is deprecated. Use the substring match algorithm instead.");
|
||||||
}
|
}
|
||||||
if let Some(algorithm) = options
|
if let Some(algorithm) = options
|
||||||
.get("completion_algorithm")
|
.get("completion_algorithm")
|
||||||
@ -113,6 +114,11 @@ impl<T: Completer> Completer for CustomCompletion<T> {
|
|||||||
.and_then(|option| option.try_into().ok())
|
.and_then(|option| option.try_into().ok())
|
||||||
{
|
{
|
||||||
completion_options.match_algorithm = algorithm;
|
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(
|
let mut completer = custom_completer_with_options(
|
||||||
r#"$env.config.completions.algorithm = "fuzzy"
|
r#"$env.config.completions.algorithm = "fuzzy"
|
||||||
$env.config.completions.case_sensitive = false"#,
|
$env.config.completions.case_sensitive = false"#,
|
||||||
r#"completion_algorithm: "prefix",
|
r#"completion_algorithm: "substring",
|
||||||
positional: false,
|
|
||||||
case_sensitive: true,
|
case_sensitive: true,
|
||||||
sort: true"#,
|
sort: true"#,
|
||||||
&["Foo Abcdef", "Abcdef", "Acd Bar"],
|
&["Foo Abcdef", "Abcdef", "Acd Bar"],
|
||||||
);
|
);
|
||||||
|
|
||||||
// positional: false should make it do substring matching
|
|
||||||
// sort: true should force sorting
|
// sort: true should force sorting
|
||||||
let expected: Vec<_> = vec!["Abcdef", "Foo Abcdef"];
|
let expected: Vec<_> = vec!["Abcdef", "Foo Abcdef"];
|
||||||
let suggestions = completer.complete("my-command Abcd", 15);
|
let suggestions = completer.complete("my-command Abcd", 15);
|
||||||
|
@ -6,6 +6,7 @@ use crate::engine::Closure;
|
|||||||
pub enum CompletionAlgorithm {
|
pub enum CompletionAlgorithm {
|
||||||
#[default]
|
#[default]
|
||||||
Prefix,
|
Prefix,
|
||||||
|
Substring,
|
||||||
Fuzzy,
|
Fuzzy,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -15,8 +16,9 @@ impl FromStr for CompletionAlgorithm {
|
|||||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
match s.to_ascii_lowercase().as_str() {
|
match s.to_ascii_lowercase().as_str() {
|
||||||
"prefix" => Ok(Self::Prefix),
|
"prefix" => Ok(Self::Prefix),
|
||||||
|
"substring" => Ok(Self::Substring),
|
||||||
"fuzzy" => Ok(Self::Fuzzy),
|
"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.*
|
# $env.config.completions.*
|
||||||
# Apply to the Nushell completion system
|
# Apply to the Nushell completion system
|
||||||
|
|
||||||
# algorithm (string): Either "prefix" or "fuzzy"
|
# algorithm (string): "prefix", "substring" or "fuzzy"
|
||||||
$env.config.completions.algorithm = "prefix"
|
$env.config.completions.algorithm = "prefix"
|
||||||
|
|
||||||
# sort (string): One of "smart" or "alphabetical"
|
# sort (string): One of "smart" or "alphabetical"
|
||||||
# In "smart" mode sort order is based on the "algorithm" setting.
|
# In "smart" mode sort order is based on the "algorithm" setting.
|
||||||
# When using the "prefix" algorithm, results are alphabetically sorted.
|
# 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.
|
# When using the "fuzzy" algorithm, results are sorted based on their fuzzy score.
|
||||||
$env.config.completions.sort = "smart"
|
$env.config.completions.sort = "smart"
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user