diff --git a/crates/nu-cli/src/completions/command_completions.rs b/crates/nu-cli/src/completions/command_completions.rs index e880c7fb43..10d8237393 100644 --- a/crates/nu-cli/src/completions/command_completions.rs +++ b/crates/nu-cli/src/completions/command_completions.rs @@ -59,7 +59,7 @@ impl CommandCompletion { .max_results > executables.len() as i64 { - continue; + break; } let Ok(name) = item.file_name().into_string() else { continue; @@ -72,24 +72,23 @@ impl CommandCompletion { } else { name.clone() }; - if matcher.add( - name.clone(), - SemanticSuggestion { - suggestion: Suggestion { - value, - span: sugg_span, - append_whitespace: true, - ..Default::default() + if matcher.matches(&name) && is_executable::is_executable(item.path()) { + executables.insert(name.clone()); + // This causes the matcher to match the executable name twice, + // but it's likely a cost we can eat + matcher.add( + name, + SemanticSuggestion { + suggestion: Suggestion { + value, + span: sugg_span, + append_whitespace: true, + ..Default::default() + }, + // TODO: is there a way to create a test? + kind: None, }, - // TODO: is there a way to create a test? - kind: None, - }, - ) { - if is_executable::is_executable(item.path()) { - executables.insert(name); - } else { - matcher.remove_last(); - } + ); } } } diff --git a/crates/nu-cli/src/completions/completion_options.rs b/crates/nu-cli/src/completions/completion_options.rs index cf90ac1032..86ead4725b 100644 --- a/crates/nu-cli/src/completions/completion_options.rs +++ b/crates/nu-cli/src/completions/completion_options.rs @@ -34,6 +34,7 @@ enum State { items: Vec<(String, T)>, }, Fuzzy { + matcher: Box, /// Holds (haystack, item, score) items: Vec<(String, T, i64)>, }, @@ -57,11 +58,22 @@ impl NuMatcher { needle: lowercase_needle, state: State::Prefix { items: Vec::new() }, }, - MatchAlgorithm::Fuzzy => NuMatcher { - options, - needle: orig_needle.to_owned(), - state: State::Fuzzy { items: Vec::new() }, - }, + MatchAlgorithm::Fuzzy => { + let mut matcher = SkimMatcherV2::default(); + if options.case_sensitive { + matcher = matcher.respect_case(); + } else { + matcher = matcher.ignore_case(); + }; + NuMatcher { + options, + needle: orig_needle.to_owned(), + state: State::Fuzzy { + matcher: Box::new(matcher), + items: Vec::new(), + }, + } + } } } @@ -69,51 +81,51 @@ impl NuMatcher { /// /// Returns whether the item was added. pub fn add(&mut self, haystack: String, item: T) -> bool { + let haystack = trim_quotes_str(&haystack); match &mut self.state { State::Prefix { items } => { - let haystack = trim_quotes_str(&haystack).to_owned(); - let haystack_lowercased = if self.options.case_sensitive { - Cow::Borrowed(&haystack) + let haystack = if self.options.case_sensitive { + Cow::Borrowed(haystack) } else { Cow::Owned(haystack.to_folded_case()) }; let matches = if self.options.positional { - haystack_lowercased.starts_with(self.needle.as_str()) + haystack.starts_with(self.needle.as_str()) } else { - haystack_lowercased.contains(self.needle.as_str()) + haystack.contains(self.needle.as_str()) }; if matches { - items.push((haystack, item)); + items.push((haystack.to_string(), item)); } matches } - State::Fuzzy { items } => { - let mut matcher = SkimMatcherV2::default(); - if self.options.case_sensitive { - matcher = matcher.respect_case(); - } else { - matcher = matcher.ignore_case(); - }; - let Some(score) = matcher.fuzzy_match(&haystack, &self.needle) else { + State::Fuzzy { items, matcher } => { + let Some(score) = matcher.fuzzy_match(haystack, &self.needle) else { return false; }; - items.push((haystack, item, score)); + items.push((haystack.to_string(), item, score)); true } } } - /// Remove the last added item. This is useful if you want to filter matched - /// completions by some expensive condition. You can call `add`, run the expensive condition, - /// then call `remove_last` if the expensive condition is false. - pub fn remove_last(&mut self) { + /// Returns whether the item was added. + pub fn matches(&mut self, haystack: &str) -> bool { + let haystack = trim_quotes_str(haystack).to_owned(); match &mut self.state { - State::Prefix { items } => { - items.pop(); - } - State::Fuzzy { items } => { - items.pop(); + State::Prefix { .. } => { + let haystack = if self.options.case_sensitive { + Cow::Borrowed(&haystack) + } else { + Cow::Owned(haystack.to_folded_case()) + }; + if self.options.positional { + haystack.starts_with(self.needle.as_str()) + } else { + haystack.contains(self.needle.as_str()) + } } + State::Fuzzy { matcher, .. } => matcher.fuzzy_match(&haystack, &self.needle).is_some(), } }