Avoid recomputing fuzzy match scores (#13700)

<!--
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 makes it so that when using fuzzy matching, the score isn't
recomputed when sorting. Instead, filtering and sorting suggestions is
handled by a new `NuMatcher` struct. This struct accepts suggestions
and, if they match the user's typed text, stores those suggestions
(along with their scores and values). At the end, it returns a sorted
list of suggestions.

This probably won't have a noticeable impact on performance, but it
might be helpful if we start using Nucleo in the future.

Minor change: Makes `find_commands_by_predicate` in `StateWorkingSet`
and `EngineState` take `FnMut` rather than `Fn` for the predicate.

# User-Facing Changes
<!-- List of all changes that impact the user experience here. This
helps us keep track of breaking changes. -->

When using case-insensitive matching, if you have two matches `FOO` and
`abc`, `abc` will be shown before `FOO` rather than the other way
around. I think this way makes more sense than the current behavior.
When I brought this up on Discord, WindSoilder did say it would make
sense to show uppercase matches first if the user typed, say, `F`.
However, that would be a lot more complicated to implement.

# Tests + Formatting
<!--
Don't forget to add tests that cover your changes.

Make sure you've run and fixed any issues with these commands:

- `cargo fmt --all -- --check` to check standard code formatting (`cargo
fmt --all` applies these changes)
- `cargo clippy --workspace -- -D warnings -D clippy::unwrap_used` to
check that you're using the standard code style
- `cargo test --workspace` to check that all tests pass (on Windows make
sure to [enable developer
mode](https://learn.microsoft.com/en-us/windows/apps/get-started/developer-mode-features-and-debugging))
- `cargo run -- -c "use toolkit.nu; toolkit test stdlib"` to run the
tests for the standard library

> **Note**
> from `nushell` you can also use the `toolkit` as follows
> ```bash
> use toolkit.nu # or use an `env_change` hook to activate it
automatically
> toolkit check pr
> ```
-->

Added a test for the changes in
https://github.com/nushell/nushell/pull/13302.

# After Submitting
<!-- If your PR had any user-facing changes, update [the
documentation](https://github.com/nushell/nushell.github.io) after the
PR is merged, if necessary. This will help us keep the docs up to date.
-->
This commit is contained in:
Yash Thakur 2024-11-22 07:29:00 -05:00 committed by GitHub
parent 5f7082f053
commit 671640b0a9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 551 additions and 511 deletions

View File

@ -1,5 +1,7 @@
use std::collections::HashMap;
use crate::{ use crate::{
completions::{Completer, CompletionOptions, MatchAlgorithm}, completions::{Completer, CompletionOptions},
SuggestionKind, SuggestionKind,
}; };
use nu_parser::FlatShape; use nu_parser::FlatShape;
@ -9,7 +11,7 @@ use nu_protocol::{
}; };
use reedline::Suggestion; use reedline::Suggestion;
use super::{completion_common::sort_suggestions, SemanticSuggestion}; use super::{completion_options::NuMatcher, SemanticSuggestion};
pub struct CommandCompletion { pub struct CommandCompletion {
flattened: Vec<(Span, FlatShape)>, flattened: Vec<(Span, FlatShape)>,
@ -33,10 +35,11 @@ impl CommandCompletion {
fn external_command_completion( fn external_command_completion(
&self, &self,
working_set: &StateWorkingSet, working_set: &StateWorkingSet,
prefix: &str, sugg_span: reedline::Span,
match_algorithm: MatchAlgorithm, matched_internal: impl Fn(&str) -> bool,
) -> Vec<String> { matcher: &mut NuMatcher<String>,
let mut executables = vec![]; ) -> HashMap<String, SemanticSuggestion> {
let mut suggs = HashMap::new();
// os agnostic way to get the PATH env var // os agnostic way to get the PATH env var
let paths = working_set.permanent_state.get_path_env_var(); let paths = working_set.permanent_state.get_path_env_var();
@ -54,24 +57,38 @@ impl CommandCompletion {
.completions .completions
.external .external
.max_results .max_results
> executables.len() as i64 <= suggs.len() as i64
&& !executables.contains(
&item
.path()
.file_name()
.map(|x| x.to_string_lossy().to_string())
.unwrap_or_default(),
)
&& matches!(
item.path().file_name().map(|x| match_algorithm
.matches_str(&x.to_string_lossy(), prefix)),
Some(true)
)
&& is_executable::is_executable(item.path())
{ {
if let Ok(name) = item.file_name().into_string() { break;
executables.push(name);
} }
let Ok(name) = item.file_name().into_string() else {
continue;
};
let value = if matched_internal(&name) {
format!("^{}", name)
} else {
name.clone()
};
if suggs.contains_key(&value) {
continue;
}
if matcher.matches(&name) && is_executable::is_executable(item.path()) {
// If there's an internal command with the same name, adds ^cmd to the
// matcher so that both the internal and external command are included
matcher.add(&name, value.clone());
suggs.insert(
value.clone(),
SemanticSuggestion {
suggestion: Suggestion {
value,
span: sugg_span,
append_whitespace: true,
..Default::default()
},
// TODO: is there a way to create a test?
kind: None,
},
);
} }
} }
} }
@ -79,7 +96,7 @@ impl CommandCompletion {
} }
} }
executables suggs
} }
fn complete_commands( fn complete_commands(
@ -88,69 +105,60 @@ impl CommandCompletion {
span: Span, span: Span,
offset: usize, offset: usize,
find_externals: bool, find_externals: bool,
match_algorithm: MatchAlgorithm, options: &CompletionOptions,
) -> Vec<SemanticSuggestion> { ) -> Vec<SemanticSuggestion> {
let partial = working_set.get_span_contents(span); let partial = working_set.get_span_contents(span);
let mut matcher = NuMatcher::new(String::from_utf8_lossy(partial), options.clone());
let filter_predicate = |command: &[u8]| match_algorithm.matches_u8(command, partial); let sugg_span = reedline::Span::new(span.start - offset, span.end - offset);
let mut results = working_set let mut internal_suggs = HashMap::new();
.find_commands_by_predicate(filter_predicate, true) let filtered_commands = working_set.find_commands_by_predicate(
.into_iter() |name| {
.map(move |x| SemanticSuggestion { let name = String::from_utf8_lossy(name);
matcher.add(&name, name.to_string())
},
true,
);
for (name, description, typ) in filtered_commands {
let name = String::from_utf8_lossy(&name);
internal_suggs.insert(
name.to_string(),
SemanticSuggestion {
suggestion: Suggestion { suggestion: Suggestion {
value: String::from_utf8_lossy(&x.0).to_string(), value: name.to_string(),
description: x.1, description,
span: reedline::Span::new(span.start - offset, span.end - offset), span: sugg_span,
append_whitespace: true, append_whitespace: true,
..Suggestion::default() ..Suggestion::default()
}, },
kind: Some(SuggestionKind::Command(x.2)), kind: Some(SuggestionKind::Command(typ)),
})
.collect::<Vec<_>>();
let partial = working_set.get_span_contents(span);
let partial = String::from_utf8_lossy(partial).to_string();
if find_externals {
let results_external = self
.external_command_completion(working_set, &partial, match_algorithm)
.into_iter()
.map(move |x| SemanticSuggestion {
suggestion: Suggestion {
value: x,
span: reedline::Span::new(span.start - offset, span.end - offset),
append_whitespace: true,
..Suggestion::default()
}, },
// TODO: is there a way to create a test? );
kind: None, }
});
let results_strings: Vec<String> = let mut external_suggs = if find_externals {
results.iter().map(|x| x.suggestion.value.clone()).collect(); self.external_command_completion(
working_set,
for external in results_external { sugg_span,
if results_strings.contains(&external.suggestion.value) { |name| internal_suggs.contains_key(name),
results.push(SemanticSuggestion { &mut matcher,
suggestion: Suggestion { )
value: format!("^{}", external.suggestion.value),
span: external.suggestion.span,
append_whitespace: true,
..Suggestion::default()
},
kind: external.kind,
})
} else { } else {
results.push(external) HashMap::new()
} };
}
results let mut res = Vec::new();
} else { for cmd_name in matcher.results() {
results if let Some(sugg) = internal_suggs
.remove(&cmd_name)
.or_else(|| external_suggs.remove(&cmd_name))
{
res.push(sugg);
} }
} }
res
}
} }
impl Completer for CommandCompletion { impl Completer for CommandCompletion {
@ -158,7 +166,7 @@ impl Completer for CommandCompletion {
&mut self, &mut self,
working_set: &StateWorkingSet, working_set: &StateWorkingSet,
_stack: &Stack, _stack: &Stack,
prefix: &[u8], _prefix: &[u8],
span: Span, span: Span,
offset: usize, offset: usize,
pos: usize, pos: usize,
@ -188,18 +196,18 @@ impl Completer for CommandCompletion {
Span::new(last.0.start, pos), Span::new(last.0.start, pos),
offset, offset,
false, false,
options.match_algorithm, options,
) )
} else { } else {
vec![] vec![]
}; };
if !subcommands.is_empty() { if !subcommands.is_empty() {
return sort_suggestions(&String::from_utf8_lossy(prefix), subcommands, options); return subcommands;
} }
let config = working_set.get_config(); let config = working_set.get_config();
let commands = if matches!(self.flat_shape, nu_parser::FlatShape::External) if matches!(self.flat_shape, nu_parser::FlatShape::External)
|| matches!(self.flat_shape, nu_parser::FlatShape::InternalCall(_)) || matches!(self.flat_shape, nu_parser::FlatShape::InternalCall(_))
|| ((span.end - span.start) == 0) || ((span.end - span.start) == 0)
|| is_passthrough_command(working_set.delta.get_file_contents()) || is_passthrough_command(working_set.delta.get_file_contents())
@ -214,13 +222,11 @@ impl Completer for CommandCompletion {
span, span,
offset, offset,
config.completions.external.enable, config.completions.external.enable,
options.match_algorithm, options,
) )
} else { } else {
vec![] vec![]
}; }
sort_suggestions(&String::from_utf8_lossy(prefix), commands, options)
} }
} }

View File

@ -1,22 +1,20 @@
use super::MatchAlgorithm; use super::{completion_options::NuMatcher, MatchAlgorithm};
use crate::{ use crate::completions::CompletionOptions;
completions::{matches, CompletionOptions},
SemanticSuggestion,
};
use fuzzy_matcher::{skim::SkimMatcherV2, FuzzyMatcher};
use nu_ansi_term::Style; use nu_ansi_term::Style;
use nu_engine::env_to_string; use nu_engine::env_to_string;
use nu_path::dots::expand_ndots; use nu_path::dots::expand_ndots;
use nu_path::{expand_to_real_path, home_dir}; use nu_path::{expand_to_real_path, home_dir};
use nu_protocol::{ use nu_protocol::{
engine::{EngineState, Stack, StateWorkingSet}, engine::{EngineState, Stack, StateWorkingSet},
CompletionSort, Span, Span,
}; };
use nu_utils::get_ls_colors; use nu_utils::get_ls_colors;
use nu_utils::IgnoreCaseExt;
use std::path::{is_separator, Component, Path, PathBuf, MAIN_SEPARATOR as SEP}; use std::path::{is_separator, Component, Path, PathBuf, MAIN_SEPARATOR as SEP};
#[derive(Clone, Default)] #[derive(Clone, Default)]
pub struct PathBuiltFromString { pub struct PathBuiltFromString {
cwd: PathBuf,
parts: Vec<String>, parts: Vec<String>,
isdir: bool, isdir: bool,
} }
@ -30,35 +28,41 @@ pub struct PathBuiltFromString {
/// want_directory: Whether we want only directories as completion matches. /// want_directory: Whether we want only directories as completion matches.
/// Some commands like `cd` can only be run on directories whereas others /// Some commands like `cd` can only be run on directories whereas others
/// like `ls` can be run on regular files as well. /// like `ls` can be run on regular files as well.
pub fn complete_rec( fn complete_rec(
partial: &[&str], partial: &[&str],
built: &PathBuiltFromString, built_paths: &[PathBuiltFromString],
cwd: &Path,
options: &CompletionOptions, options: &CompletionOptions,
want_directory: bool, want_directory: bool,
isdir: bool, isdir: bool,
) -> Vec<PathBuiltFromString> { ) -> Vec<PathBuiltFromString> {
let mut completions = vec![];
if let Some((&base, rest)) = partial.split_first() { if let Some((&base, rest)) = partial.split_first() {
if base.chars().all(|c| c == '.') && (isdir || !rest.is_empty()) { if base.chars().all(|c| c == '.') && (isdir || !rest.is_empty()) {
let built_paths: Vec<_> = built_paths
.iter()
.map(|built| {
let mut built = built.clone(); let mut built = built.clone();
built.parts.push(base.to_string()); built.parts.push(base.to_string());
built.isdir = true; built.isdir = true;
return complete_rec(rest, &built, cwd, options, want_directory, isdir); built
})
.collect();
return complete_rec(rest, &built_paths, options, want_directory, isdir);
} }
} }
let mut built_path = cwd.to_path_buf(); let prefix = partial.first().unwrap_or(&"");
let mut matcher = NuMatcher::new(prefix, options.clone());
for built in built_paths {
let mut path = built.cwd.clone();
for part in &built.parts { for part in &built.parts {
built_path.push(part); path.push(part);
} }
let Ok(result) = built_path.read_dir() else { let Ok(result) = path.read_dir() else {
return completions; continue;
}; };
let mut entries = Vec::new();
for entry in result.filter_map(|e| e.ok()) { for entry in result.filter_map(|e| e.ok()) {
let entry_name = entry.file_name().to_string_lossy().into_owned(); let entry_name = entry.file_name().to_string_lossy().into_owned();
let entry_isdir = entry.path().is_dir(); let entry_isdir = entry.path().is_dir();
@ -67,17 +71,15 @@ pub fn complete_rec(
built.isdir = entry_isdir; built.isdir = entry_isdir;
if !want_directory || entry_isdir { if !want_directory || entry_isdir {
entries.push((entry_name, built)); matcher.add(entry_name.clone(), (entry_name, built));
}
} }
} }
let prefix = partial.first().unwrap_or(&""); let mut completions = vec![];
let sorted_entries = sort_completions(prefix, entries, options, |(entry, _)| entry); for (entry_name, built) in matcher.results() {
for (entry_name, built) in sorted_entries {
match partial.split_first() { match partial.split_first() {
Some((base, rest)) => { Some((base, rest)) => {
if matches(base, &entry_name, options) {
// We use `isdir` to confirm that the current component has // We use `isdir` to confirm that the current component has
// at least one next component or a slash. // at least one next component or a slash.
// Serves as confirmation to ignore longer completions for // Serves as confirmation to ignore longer completions for
@ -85,8 +87,7 @@ pub fn complete_rec(
if !rest.is_empty() || isdir { if !rest.is_empty() || isdir {
completions.extend(complete_rec( completions.extend(complete_rec(
rest, rest,
&built, &[built],
cwd,
options, options,
want_directory, want_directory,
isdir, isdir,
@ -94,14 +95,19 @@ pub fn complete_rec(
} else { } else {
completions.push(built); completions.push(built);
} }
}
if entry_name.eq(base) // For https://github.com/nushell/nushell/issues/13204
&& matches!(options.match_algorithm, MatchAlgorithm::Prefix) if isdir && options.match_algorithm == MatchAlgorithm::Prefix {
&& isdir let exact_match = if options.case_sensitive {
{ entry_name.eq(base)
} else {
entry_name.to_folded_case().eq(&base.to_folded_case())
};
if exact_match {
break; break;
} }
} }
}
None => { None => {
completions.push(built); completions.push(built);
} }
@ -147,15 +153,25 @@ fn surround_remove(partial: &str) -> String {
partial.to_string() partial.to_string()
} }
pub struct FileSuggestion {
pub span: nu_protocol::Span,
pub path: String,
pub style: Option<Style>,
pub cwd: PathBuf,
}
/// # Parameters
/// * `cwds` - A list of directories in which to search. The only reason this isn't a single string
/// is because dotnu_completions searches in multiple directories at once
pub fn complete_item( pub fn complete_item(
want_directory: bool, want_directory: bool,
span: nu_protocol::Span, span: nu_protocol::Span,
partial: &str, partial: &str,
cwd: &str, cwds: &[impl AsRef<str>],
options: &CompletionOptions, options: &CompletionOptions,
engine_state: &EngineState, engine_state: &EngineState,
stack: &Stack, stack: &Stack,
) -> Vec<(nu_protocol::Span, String, Option<Style>)> { ) -> Vec<FileSuggestion> {
let cleaned_partial = surround_remove(partial); let cleaned_partial = surround_remove(partial);
let isdir = cleaned_partial.ends_with(is_separator); let isdir = cleaned_partial.ends_with(is_separator);
let expanded_partial = expand_ndots(Path::new(&cleaned_partial)); let expanded_partial = expand_ndots(Path::new(&cleaned_partial));
@ -175,7 +191,10 @@ pub fn complete_item(
partial.push_str(&format!("{path_separator}.")); partial.push_str(&format!("{path_separator}."));
} }
let cwd_pathbuf = Path::new(cwd).to_path_buf(); let cwd_pathbufs: Vec<_> = cwds
.iter()
.map(|cwd| Path::new(cwd.as_ref()).to_path_buf())
.collect();
let ls_colors = (engine_state.config.completions.use_ls_colors let ls_colors = (engine_state.config.completions.use_ls_colors
&& engine_state.config.use_ansi_coloring) && engine_state.config.use_ansi_coloring)
.then(|| { .then(|| {
@ -186,7 +205,7 @@ pub fn complete_item(
get_ls_colors(ls_colors_env_str) get_ls_colors(ls_colors_env_str)
}); });
let mut cwd = cwd_pathbuf.clone(); let mut cwds = cwd_pathbufs.clone();
let mut prefix_len = 0; let mut prefix_len = 0;
let mut original_cwd = OriginalCwd::None; let mut original_cwd = OriginalCwd::None;
@ -194,19 +213,21 @@ pub fn complete_item(
match components.peek().cloned() { match components.peek().cloned() {
Some(c @ Component::Prefix(..)) => { Some(c @ Component::Prefix(..)) => {
// windows only by definition // windows only by definition
cwd = [c, Component::RootDir].iter().collect(); cwds = vec![[c, Component::RootDir].iter().collect()];
prefix_len = c.as_os_str().len(); prefix_len = c.as_os_str().len();
original_cwd = OriginalCwd::Prefix(c.as_os_str().to_string_lossy().into_owned()); original_cwd = OriginalCwd::Prefix(c.as_os_str().to_string_lossy().into_owned());
} }
Some(c @ Component::RootDir) => { Some(c @ Component::RootDir) => {
// This is kind of a hack. When joining an empty string with the rest, // This is kind of a hack. When joining an empty string with the rest,
// we add the slash automagically // we add the slash automagically
cwd = PathBuf::from(c.as_os_str()); cwds = vec![PathBuf::from(c.as_os_str())];
prefix_len = 1; prefix_len = 1;
original_cwd = OriginalCwd::Prefix(String::new()); original_cwd = OriginalCwd::Prefix(String::new());
} }
Some(Component::Normal(home)) if home.to_string_lossy() == "~" => { Some(Component::Normal(home)) if home.to_string_lossy() == "~" => {
cwd = home_dir().map(Into::into).unwrap_or(cwd_pathbuf); cwds = home_dir()
.map(|dir| vec![dir.into()])
.unwrap_or(cwd_pathbufs);
prefix_len = 1; prefix_len = 1;
original_cwd = OriginalCwd::Home; original_cwd = OriginalCwd::Home;
} }
@ -223,8 +244,14 @@ pub fn complete_item(
complete_rec( complete_rec(
partial.as_slice(), partial.as_slice(),
&PathBuiltFromString::default(), &cwds
&cwd, .into_iter()
.map(|cwd| PathBuiltFromString {
cwd,
parts: Vec::new(),
isdir: false,
})
.collect::<Vec<_>>(),
options, options,
want_directory, want_directory,
isdir, isdir,
@ -234,6 +261,7 @@ pub fn complete_item(
if should_collapse_dots { if should_collapse_dots {
p = collapse_ndots(p); p = collapse_ndots(p);
} }
let cwd = p.cwd.clone();
let path = original_cwd.apply(p, path_separator); let path = original_cwd.apply(p, path_separator);
let style = ls_colors.as_ref().map(|lsc| { let style = ls_colors.as_ref().map(|lsc| {
lsc.style_for_path_with_metadata( lsc.style_for_path_with_metadata(
@ -245,7 +273,12 @@ pub fn complete_item(
.map(lscolors::Style::to_nu_ansi_term_style) .map(lscolors::Style::to_nu_ansi_term_style)
.unwrap_or_default() .unwrap_or_default()
}); });
(span, escape_path(path, want_directory), style) FileSuggestion {
span,
path: escape_path(path, want_directory),
style,
cwd,
}
}) })
.collect() .collect()
} }
@ -310,45 +343,6 @@ pub fn adjust_if_intermediate(
} }
} }
/// Convenience function to sort suggestions using [`sort_completions`]
pub fn sort_suggestions(
prefix: &str,
items: Vec<SemanticSuggestion>,
options: &CompletionOptions,
) -> Vec<SemanticSuggestion> {
sort_completions(prefix, items, options, |it| &it.suggestion.value)
}
/// # Arguments
/// * `prefix` - What the user's typed, for sorting by fuzzy matcher score
pub fn sort_completions<T>(
prefix: &str,
mut items: Vec<T>,
options: &CompletionOptions,
get_value: fn(&T) -> &str,
) -> Vec<T> {
// Sort items
if options.sort == CompletionSort::Smart && options.match_algorithm == MatchAlgorithm::Fuzzy {
let mut matcher = SkimMatcherV2::default();
if options.case_sensitive {
matcher = matcher.respect_case();
} else {
matcher = matcher.ignore_case();
};
items.sort_unstable_by(|a, b| {
let a_str = get_value(a);
let b_str = get_value(b);
let a_score = matcher.fuzzy_match(a_str, prefix).unwrap_or_default();
let b_score = matcher.fuzzy_match(b_str, prefix).unwrap_or_default();
b_score.cmp(&a_score).then(a_str.cmp(b_str))
});
} else {
items.sort_unstable_by(|a, b| get_value(a).cmp(get_value(b)));
}
items
}
/// Collapse multiple ".." components into n-dots. /// Collapse multiple ".." components into n-dots.
/// ///
/// It performs the reverse operation of `expand_ndots`, collapsing sequences of ".." into n-dots, /// It performs the reverse operation of `expand_ndots`, collapsing sequences of ".." into n-dots,
@ -359,6 +353,7 @@ fn collapse_ndots(path: PathBuiltFromString) -> PathBuiltFromString {
let mut result = PathBuiltFromString { let mut result = PathBuiltFromString {
parts: Vec::with_capacity(path.parts.len()), parts: Vec::with_capacity(path.parts.len()),
isdir: path.isdir, isdir: path.isdir,
cwd: path.cwd,
}; };
let mut dot_count = 0; let mut dot_count = 0;

View File

@ -1,7 +1,10 @@
use fuzzy_matcher::{skim::SkimMatcherV2, FuzzyMatcher}; use fuzzy_matcher::{skim::SkimMatcherV2, FuzzyMatcher};
use nu_parser::trim_quotes_str; use nu_parser::trim_quotes_str;
use nu_protocol::{CompletionAlgorithm, CompletionSort}; use nu_protocol::{CompletionAlgorithm, CompletionSort};
use std::fmt::Display; use nu_utils::IgnoreCaseExt;
use std::{borrow::Cow, fmt::Display};
use super::SemanticSuggestion;
/// Describes how suggestions should be matched. /// Describes how suggestions should be matched.
#[derive(Copy, Clone, Debug, PartialEq)] #[derive(Copy, Clone, Debug, PartialEq)]
@ -19,32 +22,153 @@ pub enum MatchAlgorithm {
Fuzzy, Fuzzy,
} }
impl MatchAlgorithm { pub struct NuMatcher<T> {
/// Returns whether the `needle` search text matches the given `haystack`. options: CompletionOptions,
pub fn matches_str(&self, haystack: &str, needle: &str) -> bool { needle: String,
state: State<T>,
}
enum State<T> {
Prefix {
/// Holds (haystack, item)
items: Vec<(String, T)>,
},
Fuzzy {
matcher: Box<SkimMatcherV2>,
/// Holds (haystack, item, score)
items: Vec<(String, T, i64)>,
},
}
/// Filters and sorts suggestions
impl<T> NuMatcher<T> {
/// # Arguments
///
/// * `needle` - The text to search for
pub fn new(needle: impl AsRef<str>, options: CompletionOptions) -> NuMatcher<T> {
let orig_needle = trim_quotes_str(needle.as_ref());
let lowercase_needle = if options.case_sensitive {
orig_needle.to_owned()
} else {
orig_needle.to_folded_case()
};
match options.match_algorithm {
MatchAlgorithm::Prefix => NuMatcher {
options,
needle: lowercase_needle,
state: State::Prefix { 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(),
},
}
}
}
}
/// Returns whether or not the haystack matches the needle. If it does, `item` is added
/// to the list of matches (if given).
///
/// Helper to avoid code duplication between [NuMatcher::add] and [NuMatcher::matches].
fn matches_aux(&mut self, haystack: &str, item: Option<T>) -> bool {
let haystack = trim_quotes_str(haystack); let haystack = trim_quotes_str(haystack);
let needle = trim_quotes_str(needle); match &mut self.state {
match *self { State::Prefix { items } => {
MatchAlgorithm::Prefix => haystack.starts_with(needle), let haystack_folded = if self.options.case_sensitive {
MatchAlgorithm::Fuzzy => { Cow::Borrowed(haystack)
let matcher = SkimMatcherV2::default(); } else {
matcher.fuzzy_match(haystack, needle).is_some() Cow::Owned(haystack.to_folded_case())
};
let matches = if self.options.positional {
haystack_folded.starts_with(self.needle.as_str())
} else {
haystack_folded.contains(self.needle.as_str())
};
if matches {
if let Some(item) = item {
items.push((haystack.to_string(), item));
}
}
matches
}
State::Fuzzy { items, matcher } => {
let Some(score) = matcher.fuzzy_match(haystack, &self.needle) else {
return false;
};
if let Some(item) = item {
items.push((haystack.to_string(), item, score));
}
true
} }
} }
} }
/// Returns whether the `needle` search text matches the given `haystack`. /// Add the given item if the given haystack matches the needle.
pub fn matches_u8(&self, haystack: &[u8], needle: &[u8]) -> bool { ///
match *self { /// Returns whether the item was added.
MatchAlgorithm::Prefix => haystack.starts_with(needle), pub fn add(&mut self, haystack: impl AsRef<str>, item: T) -> bool {
MatchAlgorithm::Fuzzy => { self.matches_aux(haystack.as_ref(), Some(item))
let haystack_str = String::from_utf8_lossy(haystack); }
let needle_str = String::from_utf8_lossy(needle);
let matcher = SkimMatcherV2::default(); /// Returns whether the haystack matches the needle.
matcher.fuzzy_match(&haystack_str, &needle_str).is_some() pub fn matches(&mut self, haystack: &str) -> bool {
self.matches_aux(haystack, None)
}
/// Get all the items that matched (sorted)
pub fn results(self) -> Vec<T> {
match self.state {
State::Prefix { mut items, .. } => {
items.sort_by(|(haystack1, _), (haystack2, _)| {
let cmp_sensitive = haystack1.cmp(haystack2);
if self.options.case_sensitive {
cmp_sensitive
} else {
haystack1
.to_folded_case()
.cmp(&haystack2.to_folded_case())
.then(cmp_sensitive)
}
});
items.into_iter().map(|(_, item)| item).collect::<Vec<_>>()
}
State::Fuzzy { mut items, .. } => {
match self.options.sort {
CompletionSort::Alphabetical => {
items.sort_by(|(haystack1, _, _), (haystack2, _, _)| {
haystack1.cmp(haystack2)
});
}
CompletionSort::Smart => {
items.sort_by(|(haystack1, _, score1), (haystack2, _, score2)| {
score2.cmp(score1).then(haystack1.cmp(haystack2))
});
} }
} }
items
.into_iter()
.map(|(_, item, _)| item)
.collect::<Vec<_>>()
}
}
}
}
impl NuMatcher<SemanticSuggestion> {
pub fn add_semantic_suggestion(&mut self, sugg: SemanticSuggestion) -> bool {
let value = sugg.suggestion.value.to_string();
self.add(value, sugg)
} }
} }
@ -105,35 +229,49 @@ impl Default for CompletionOptions {
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use super::MatchAlgorithm; use rstest::rstest;
#[test] use super::{CompletionOptions, MatchAlgorithm, NuMatcher};
fn match_algorithm_prefix() {
let algorithm = MatchAlgorithm::Prefix;
assert!(algorithm.matches_str("example text", "")); #[rstest]
assert!(algorithm.matches_str("example text", "examp")); #[case(MatchAlgorithm::Prefix, "example text", "", true)]
assert!(!algorithm.matches_str("example text", "text")); #[case(MatchAlgorithm::Prefix, "example text", "examp", true)]
#[case(MatchAlgorithm::Prefix, "example text", "text", false)]
assert!(algorithm.matches_u8(&[1, 2, 3], &[])); #[case(MatchAlgorithm::Fuzzy, "example text", "", true)]
assert!(algorithm.matches_u8(&[1, 2, 3], &[1, 2])); #[case(MatchAlgorithm::Fuzzy, "example text", "examp", true)]
assert!(!algorithm.matches_u8(&[1, 2, 3], &[2, 3])); #[case(MatchAlgorithm::Fuzzy, "example text", "ext", true)]
#[case(MatchAlgorithm::Fuzzy, "example text", "mplxt", true)]
#[case(MatchAlgorithm::Fuzzy, "example text", "mpp", false)]
fn match_algorithm_simple(
#[case] match_algorithm: MatchAlgorithm,
#[case] haystack: &str,
#[case] needle: &str,
#[case] should_match: bool,
) {
let options = CompletionOptions {
match_algorithm,
..Default::default()
};
let mut matcher = NuMatcher::new(needle, options);
matcher.add(haystack, haystack);
if should_match {
assert_eq!(vec![haystack], matcher.results());
} else {
assert_ne!(vec![haystack], matcher.results());
}
} }
#[test] #[test]
fn match_algorithm_fuzzy() { fn match_algorithm_fuzzy_sort_score() {
let algorithm = MatchAlgorithm::Fuzzy; let options = CompletionOptions {
match_algorithm: MatchAlgorithm::Fuzzy,
assert!(algorithm.matches_str("example text", "")); ..Default::default()
assert!(algorithm.matches_str("example text", "examp")); };
assert!(algorithm.matches_str("example text", "ext")); let mut matcher = NuMatcher::new("fob", options);
assert!(algorithm.matches_str("example text", "mplxt")); for item in ["foo/bar", "fob", "foo bar"] {
assert!(!algorithm.matches_str("example text", "mpp")); matcher.add(item, item);
}
assert!(algorithm.matches_u8(&[1, 2, 3], &[])); // Sort by score, then in alphabetical order
assert!(algorithm.matches_u8(&[1, 2, 3], &[1, 2])); assert_eq!(vec!["fob", "foo bar", "foo/bar"], matcher.results());
assert!(algorithm.matches_u8(&[1, 2, 3], &[2, 3]));
assert!(algorithm.matches_u8(&[1, 2, 3], &[1, 3]));
assert!(!algorithm.matches_u8(&[1, 2, 3], &[2, 2]));
} }
} }

View File

@ -9,10 +9,9 @@ use nu_protocol::{
engine::{Stack, StateWorkingSet}, engine::{Stack, StateWorkingSet},
CompletionSort, DeclId, PipelineData, Span, Type, Value, CompletionSort, DeclId, PipelineData, Span, Type, Value,
}; };
use nu_utils::IgnoreCaseExt;
use std::collections::HashMap; use std::collections::HashMap;
use super::completion_common::sort_suggestions; use super::completion_options::NuMatcher;
pub struct CustomCompletion { pub struct CustomCompletion {
stack: Stack, stack: Stack,
@ -123,41 +122,11 @@ impl Completer for CustomCompletion {
}) })
.unwrap_or_default(); .unwrap_or_default();
let options = custom_completion_options let options = custom_completion_options.unwrap_or(completion_options.clone());
.as_ref() let mut matcher = NuMatcher::new(String::from_utf8_lossy(prefix), options);
.unwrap_or(completion_options); for sugg in suggestions {
let suggestions = filter(prefix, suggestions, options); matcher.add_semantic_suggestion(sugg);
sort_suggestions(&String::from_utf8_lossy(prefix), suggestions, options) }
matcher.results()
} }
} }
fn filter(
prefix: &[u8],
items: Vec<SemanticSuggestion>,
options: &CompletionOptions,
) -> Vec<SemanticSuggestion> {
items
.into_iter()
.filter(|it| match options.match_algorithm {
MatchAlgorithm::Prefix => match (options.case_sensitive, options.positional) {
(true, true) => it.suggestion.value.as_bytes().starts_with(prefix),
(true, false) => it
.suggestion
.value
.contains(std::str::from_utf8(prefix).unwrap_or("")),
(false, positional) => {
let value = it.suggestion.value.to_folded_case();
let prefix = std::str::from_utf8(prefix).unwrap_or("").to_folded_case();
if positional {
value.starts_with(&prefix)
} else {
value.contains(&prefix)
}
}
},
MatchAlgorithm::Fuzzy => options
.match_algorithm
.matches_u8(it.suggestion.value.as_bytes(), prefix),
})
.collect()
}

View File

@ -2,7 +2,6 @@ use crate::completions::{
completion_common::{adjust_if_intermediate, complete_item, AdjustView}, completion_common::{adjust_if_intermediate, complete_item, AdjustView},
Completer, CompletionOptions, Completer, CompletionOptions,
}; };
use nu_ansi_term::Style;
use nu_protocol::{ use nu_protocol::{
engine::{EngineState, Stack, StateWorkingSet}, engine::{EngineState, Stack, StateWorkingSet},
Span, Span,
@ -10,7 +9,7 @@ use nu_protocol::{
use reedline::Suggestion; use reedline::Suggestion;
use std::path::Path; use std::path::Path;
use super::SemanticSuggestion; use super::{completion_common::FileSuggestion, SemanticSuggestion};
#[derive(Clone, Default)] #[derive(Clone, Default)]
pub struct DirectoryCompletion {} pub struct DirectoryCompletion {}
@ -47,11 +46,11 @@ impl Completer for DirectoryCompletion {
.into_iter() .into_iter()
.map(move |x| SemanticSuggestion { .map(move |x| SemanticSuggestion {
suggestion: Suggestion { suggestion: Suggestion {
value: x.1, value: x.path,
style: x.2, style: x.style,
span: reedline::Span { span: reedline::Span {
start: x.0.start - offset, start: x.span.start - offset,
end: x.0.end - offset, end: x.span.end - offset,
}, },
..Suggestion::default() ..Suggestion::default()
}, },
@ -92,6 +91,6 @@ pub fn directory_completion(
options: &CompletionOptions, options: &CompletionOptions,
engine_state: &EngineState, engine_state: &EngineState,
stack: &Stack, stack: &Stack,
) -> Vec<(nu_protocol::Span, String, Option<Style>)> { ) -> Vec<FileSuggestion> {
complete_item(true, span, partial, cwd, options, engine_state, stack) complete_item(true, span, partial, &[cwd], options, engine_state, stack)
} }

View File

@ -6,7 +6,7 @@ use nu_protocol::{
use reedline::Suggestion; use reedline::Suggestion;
use std::path::{is_separator, Path, MAIN_SEPARATOR as SEP, MAIN_SEPARATOR_STR}; use std::path::{is_separator, Path, MAIN_SEPARATOR as SEP, MAIN_SEPARATOR_STR};
use super::{completion_common::sort_suggestions, SemanticSuggestion}; use super::SemanticSuggestion;
#[derive(Clone, Default)] #[derive(Clone, Default)]
pub struct DotNuCompletion {} pub struct DotNuCompletion {}
@ -87,13 +87,11 @@ impl Completer for DotNuCompletion {
// Fetch the files filtering the ones that ends with .nu // Fetch the files filtering the ones that ends with .nu
// and transform them into suggestions // and transform them into suggestions
let output: Vec<SemanticSuggestion> = search_dirs
.into_iter()
.flat_map(|search_dir| {
let completions = file_path_completion( let completions = file_path_completion(
span, span,
&partial, &partial,
&search_dir, &search_dirs.iter().map(|d| d.as_str()).collect::<Vec<_>>(),
options, options,
working_set.permanent_state, working_set.permanent_state,
stack, stack,
@ -103,23 +101,23 @@ impl Completer for DotNuCompletion {
.filter(move |it| { .filter(move |it| {
// Different base dir, so we list the .nu files or folders // Different base dir, so we list the .nu files or folders
if !is_current_folder { if !is_current_folder {
it.1.ends_with(".nu") || it.1.ends_with(SEP) it.path.ends_with(".nu") || it.path.ends_with(SEP)
} else { } else {
// Lib dirs, so we filter only the .nu files or directory modules // Lib dirs, so we filter only the .nu files or directory modules
if it.1.ends_with(SEP) { if it.path.ends_with(SEP) {
Path::new(&search_dir).join(&it.1).join("mod.nu").exists() Path::new(&it.cwd).join(&it.path).join("mod.nu").exists()
} else { } else {
it.1.ends_with(".nu") it.path.ends_with(".nu")
} }
} }
}) })
.map(move |x| SemanticSuggestion { .map(move |x| SemanticSuggestion {
suggestion: Suggestion { suggestion: Suggestion {
value: x.1, value: x.path,
style: x.2, style: x.style,
span: reedline::Span { span: reedline::Span {
start: x.0.start - offset, start: x.span.start - offset,
end: x.0.end - offset, end: x.span.end - offset,
}, },
append_whitespace: true, append_whitespace: true,
..Suggestion::default() ..Suggestion::default()
@ -127,9 +125,6 @@ impl Completer for DotNuCompletion {
// TODO???? // TODO????
kind: None, kind: None,
}) })
}) .collect::<Vec<_>>()
.collect();
sort_suggestions(&prefix_str, output, options)
} }
} }

View File

@ -2,16 +2,14 @@ use crate::completions::{
completion_common::{adjust_if_intermediate, complete_item, AdjustView}, completion_common::{adjust_if_intermediate, complete_item, AdjustView},
Completer, CompletionOptions, Completer, CompletionOptions,
}; };
use nu_ansi_term::Style;
use nu_protocol::{ use nu_protocol::{
engine::{EngineState, Stack, StateWorkingSet}, engine::{EngineState, Stack, StateWorkingSet},
Span, Span,
}; };
use nu_utils::IgnoreCaseExt;
use reedline::Suggestion; use reedline::Suggestion;
use std::path::Path; use std::path::Path;
use super::SemanticSuggestion; use super::{completion_common::FileSuggestion, SemanticSuggestion};
#[derive(Clone, Default)] #[derive(Clone, Default)]
pub struct FileCompletion {} pub struct FileCompletion {}
@ -44,7 +42,7 @@ impl Completer for FileCompletion {
readjusted, readjusted,
span, span,
&prefix, &prefix,
&working_set.permanent_state.current_work_dir(), &[&working_set.permanent_state.current_work_dir()],
options, options,
working_set.permanent_state, working_set.permanent_state,
stack, stack,
@ -52,11 +50,11 @@ impl Completer for FileCompletion {
.into_iter() .into_iter()
.map(move |x| SemanticSuggestion { .map(move |x| SemanticSuggestion {
suggestion: Suggestion { suggestion: Suggestion {
value: x.1, value: x.path,
style: x.2, style: x.style,
span: reedline::Span { span: reedline::Span {
start: x.0.start - offset, start: x.span.start - offset,
end: x.0.end - offset, end: x.span.end - offset,
}, },
..Suggestion::default() ..Suggestion::default()
}, },
@ -95,21 +93,10 @@ impl Completer for FileCompletion {
pub fn file_path_completion( pub fn file_path_completion(
span: nu_protocol::Span, span: nu_protocol::Span,
partial: &str, partial: &str,
cwd: &str, cwds: &[impl AsRef<str>],
options: &CompletionOptions, options: &CompletionOptions,
engine_state: &EngineState, engine_state: &EngineState,
stack: &Stack, stack: &Stack,
) -> Vec<(nu_protocol::Span, String, Option<Style>)> { ) -> Vec<FileSuggestion> {
complete_item(false, span, partial, cwd, options, engine_state, stack) complete_item(false, span, partial, cwds, options, engine_state, stack)
}
pub fn matches(partial: &str, from: &str, options: &CompletionOptions) -> bool {
// Check for case sensitive
if !options.case_sensitive {
return options
.match_algorithm
.matches_str(&from.to_folded_case(), &partial.to_folded_case());
}
options.match_algorithm.matches_str(from, partial)
} }

View File

@ -1,4 +1,4 @@
use crate::completions::{completion_common::sort_suggestions, Completer, CompletionOptions}; use crate::completions::{completion_options::NuMatcher, Completer, CompletionOptions};
use nu_protocol::{ use nu_protocol::{
ast::{Expr, Expression}, ast::{Expr, Expression},
engine::{Stack, StateWorkingSet}, engine::{Stack, StateWorkingSet},
@ -35,7 +35,7 @@ impl Completer for FlagCompletion {
let decl = working_set.get_decl(call.decl_id); let decl = working_set.get_decl(call.decl_id);
let sig = decl.signature(); let sig = decl.signature();
let mut output = vec![]; let mut matcher = NuMatcher::new(String::from_utf8_lossy(prefix), options.clone());
for named in &sig.named { for named in &sig.named {
let flag_desc = &named.desc; let flag_desc = &named.desc;
@ -44,8 +44,7 @@ impl Completer for FlagCompletion {
short.encode_utf8(&mut named); short.encode_utf8(&mut named);
named.insert(0, b'-'); named.insert(0, b'-');
if options.match_algorithm.matches_u8(&named, prefix) { matcher.add_semantic_suggestion(SemanticSuggestion {
output.push(SemanticSuggestion {
suggestion: Suggestion { suggestion: Suggestion {
value: String::from_utf8_lossy(&named).to_string(), value: String::from_utf8_lossy(&named).to_string(),
description: Some(flag_desc.to_string()), description: Some(flag_desc.to_string()),
@ -60,7 +59,6 @@ impl Completer for FlagCompletion {
kind: None, kind: None,
}); });
} }
}
if named.long.is_empty() { if named.long.is_empty() {
continue; continue;
@ -70,8 +68,7 @@ impl Completer for FlagCompletion {
named.insert(0, b'-'); named.insert(0, b'-');
named.insert(0, b'-'); named.insert(0, b'-');
if options.match_algorithm.matches_u8(&named, prefix) { matcher.add_semantic_suggestion(SemanticSuggestion {
output.push(SemanticSuggestion {
suggestion: Suggestion { suggestion: Suggestion {
value: String::from_utf8_lossy(&named).to_string(), value: String::from_utf8_lossy(&named).to_string(),
description: Some(flag_desc.to_string()), description: Some(flag_desc.to_string()),
@ -86,9 +83,8 @@ impl Completer for FlagCompletion {
kind: None, kind: None,
}); });
} }
}
return sort_suggestions(&String::from_utf8_lossy(prefix), output, options); return matcher.results();
} }
vec![] vec![]

View File

@ -18,7 +18,7 @@ pub use completion_options::{CompletionOptions, MatchAlgorithm};
pub use custom_completions::CustomCompletion; pub use custom_completions::CustomCompletion;
pub use directory_completions::DirectoryCompletion; pub use directory_completions::DirectoryCompletion;
pub use dotnu_completions::DotNuCompletion; pub use dotnu_completions::DotNuCompletion;
pub use file_completions::{file_path_completion, matches, FileCompletion}; pub use file_completions::{file_path_completion, FileCompletion};
pub use flag_completions::FlagCompletion; pub use flag_completions::FlagCompletion;
pub use operator_completions::OperatorCompletion; pub use operator_completions::OperatorCompletion;
pub use variable_completions::VariableCompletion; pub use variable_completions::VariableCompletion;

View File

@ -1,5 +1,5 @@
use crate::completions::{ use crate::completions::{
Completer, CompletionOptions, MatchAlgorithm, SemanticSuggestion, SuggestionKind, completion_options::NuMatcher, Completer, CompletionOptions, SemanticSuggestion, SuggestionKind,
}; };
use nu_protocol::{ use nu_protocol::{
ast::{Expr, Expression}, ast::{Expr, Expression},
@ -28,7 +28,7 @@ impl Completer for OperatorCompletion {
span: Span, span: Span,
offset: usize, offset: usize,
_pos: usize, _pos: usize,
_options: &CompletionOptions, options: &CompletionOptions,
) -> Vec<SemanticSuggestion> { ) -> Vec<SemanticSuggestion> {
//Check if int, float, or string //Check if int, float, or string
let partial = std::str::from_utf8(working_set.get_span_contents(span)).unwrap_or(""); let partial = std::str::from_utf8(working_set.get_span_contents(span)).unwrap_or("");
@ -129,17 +129,12 @@ impl Completer for OperatorCompletion {
_ => vec![], _ => vec![],
}; };
let match_algorithm = MatchAlgorithm::Prefix; let mut matcher = NuMatcher::new(partial, options.clone());
let input_fuzzy_search = for (symbol, desc) in possible_operations.into_iter() {
|(operator, _): &(&str, &str)| match_algorithm.matches_str(operator, partial); matcher.add_semantic_suggestion(SemanticSuggestion {
possible_operations
.into_iter()
.filter(input_fuzzy_search)
.map(move |x| SemanticSuggestion {
suggestion: Suggestion { suggestion: Suggestion {
value: x.0.to_string(), value: symbol.to_string(),
description: Some(x.1.to_string()), description: Some(desc.to_string()),
span: reedline::Span::new(span.start - offset, span.end - offset), span: reedline::Span::new(span.start - offset, span.end - offset),
append_whitespace: true, append_whitespace: true,
..Suggestion::default() ..Suggestion::default()
@ -147,8 +142,9 @@ impl Completer for OperatorCompletion {
kind: Some(SuggestionKind::Command( kind: Some(SuggestionKind::Command(
nu_protocol::engine::CommandType::Builtin, nu_protocol::engine::CommandType::Builtin,
)), )),
}) });
.collect() }
matcher.results()
} }
} }

View File

@ -1,6 +1,4 @@
use crate::completions::{ use crate::completions::{Completer, CompletionOptions, SemanticSuggestion, SuggestionKind};
Completer, CompletionOptions, MatchAlgorithm, SemanticSuggestion, SuggestionKind,
};
use nu_engine::{column::get_columns, eval_variable}; use nu_engine::{column::get_columns, eval_variable};
use nu_protocol::{ use nu_protocol::{
engine::{Stack, StateWorkingSet}, engine::{Stack, StateWorkingSet},
@ -9,7 +7,7 @@ use nu_protocol::{
use reedline::Suggestion; use reedline::Suggestion;
use std::str; use std::str;
use super::completion_common::sort_suggestions; use super::completion_options::NuMatcher;
#[derive(Clone)] #[derive(Clone)]
pub struct VariableCompletion { pub struct VariableCompletion {
@ -33,7 +31,6 @@ impl Completer for VariableCompletion {
_pos: usize, _pos: usize,
options: &CompletionOptions, options: &CompletionOptions,
) -> Vec<SemanticSuggestion> { ) -> Vec<SemanticSuggestion> {
let mut output = vec![];
let builtins = ["$nu", "$in", "$env"]; let builtins = ["$nu", "$in", "$env"];
let var_str = std::str::from_utf8(&self.var_context.0).unwrap_or(""); let var_str = std::str::from_utf8(&self.var_context.0).unwrap_or("");
let var_id = working_set.find_variable(&self.var_context.0); let var_id = working_set.find_variable(&self.var_context.0);
@ -43,6 +40,7 @@ impl Completer for VariableCompletion {
}; };
let sublevels_count = self.var_context.1.len(); let sublevels_count = self.var_context.1.len();
let prefix_str = String::from_utf8_lossy(prefix); let prefix_str = String::from_utf8_lossy(prefix);
let mut matcher = NuMatcher::new(prefix_str, options.clone());
// Completions for the given variable // Completions for the given variable
if !var_str.is_empty() { if !var_str.is_empty() {
@ -63,26 +61,15 @@ impl Completer for VariableCompletion {
if let Some(val) = env_vars.get(&target_var_str) { if let Some(val) = env_vars.get(&target_var_str) {
for suggestion in nested_suggestions(val, &nested_levels, current_span) { for suggestion in nested_suggestions(val, &nested_levels, current_span) {
if options.match_algorithm.matches_u8_insensitive( matcher.add_semantic_suggestion(suggestion);
options.case_sensitive,
suggestion.suggestion.value.as_bytes(),
prefix,
) {
output.push(suggestion);
}
} }
return sort_suggestions(&prefix_str, output, options); return matcher.results();
} }
} else { } else {
// No nesting provided, return all env vars // No nesting provided, return all env vars
for env_var in env_vars { for env_var in env_vars {
if options.match_algorithm.matches_u8_insensitive( matcher.add_semantic_suggestion(SemanticSuggestion {
options.case_sensitive,
env_var.0.as_bytes(),
prefix,
) {
output.push(SemanticSuggestion {
suggestion: Suggestion { suggestion: Suggestion {
value: env_var.0, value: env_var.0,
span: current_span, span: current_span,
@ -91,9 +78,8 @@ impl Completer for VariableCompletion {
kind: Some(SuggestionKind::Type(env_var.1.get_type())), kind: Some(SuggestionKind::Type(env_var.1.get_type())),
}); });
} }
}
return sort_suggestions(&prefix_str, output, options); return matcher.results();
} }
} }
@ -108,16 +94,10 @@ impl Completer for VariableCompletion {
) { ) {
for suggestion in nested_suggestions(&nuval, &self.var_context.1, current_span) for suggestion in nested_suggestions(&nuval, &self.var_context.1, current_span)
{ {
if options.match_algorithm.matches_u8_insensitive( matcher.add_semantic_suggestion(suggestion);
options.case_sensitive,
suggestion.suggestion.value.as_bytes(),
prefix,
) {
output.push(suggestion);
}
} }
return sort_suggestions(&prefix_str, output, options); return matcher.results();
} }
} }
@ -130,28 +110,17 @@ impl Completer for VariableCompletion {
if let Ok(value) = var { if let Ok(value) = var {
for suggestion in nested_suggestions(&value, &self.var_context.1, current_span) for suggestion in nested_suggestions(&value, &self.var_context.1, current_span)
{ {
if options.match_algorithm.matches_u8_insensitive( matcher.add_semantic_suggestion(suggestion);
options.case_sensitive,
suggestion.suggestion.value.as_bytes(),
prefix,
) {
output.push(suggestion);
}
} }
return sort_suggestions(&prefix_str, output, options); return matcher.results();
} }
} }
} }
// Variable completion (e.g: $en<tab> to complete $env) // Variable completion (e.g: $en<tab> to complete $env)
for builtin in builtins { for builtin in builtins {
if options.match_algorithm.matches_u8_insensitive( matcher.add_semantic_suggestion(SemanticSuggestion {
options.case_sensitive,
builtin.as_bytes(),
prefix,
) {
output.push(SemanticSuggestion {
suggestion: Suggestion { suggestion: Suggestion {
value: builtin.to_string(), value: builtin.to_string(),
span: current_span, span: current_span,
@ -161,7 +130,6 @@ impl Completer for VariableCompletion {
kind: None, kind: None,
}); });
} }
}
// TODO: The following can be refactored (see find_commands_by_predicate() used in // TODO: The following can be refactored (see find_commands_by_predicate() used in
// command_completions). // command_completions).
@ -170,12 +138,7 @@ impl Completer for VariableCompletion {
for scope_frame in working_set.delta.scope.iter().rev() { for scope_frame in working_set.delta.scope.iter().rev() {
for overlay_frame in scope_frame.active_overlays(&mut removed_overlays).rev() { for overlay_frame in scope_frame.active_overlays(&mut removed_overlays).rev() {
for v in &overlay_frame.vars { for v in &overlay_frame.vars {
if options.match_algorithm.matches_u8_insensitive( matcher.add_semantic_suggestion(SemanticSuggestion {
options.case_sensitive,
v.0,
prefix,
) {
output.push(SemanticSuggestion {
suggestion: Suggestion { suggestion: Suggestion {
value: String::from_utf8_lossy(v.0).to_string(), value: String::from_utf8_lossy(v.0).to_string(),
span: current_span, span: current_span,
@ -188,7 +151,6 @@ impl Completer for VariableCompletion {
} }
} }
} }
}
// Permanent state vars // Permanent state vars
// for scope in &self.engine_state.scope { // for scope in &self.engine_state.scope {
@ -198,12 +160,7 @@ impl Completer for VariableCompletion {
.rev() .rev()
{ {
for v in &overlay_frame.vars { for v in &overlay_frame.vars {
if options.match_algorithm.matches_u8_insensitive( matcher.add_semantic_suggestion(SemanticSuggestion {
options.case_sensitive,
v.0,
prefix,
) {
output.push(SemanticSuggestion {
suggestion: Suggestion { suggestion: Suggestion {
value: String::from_utf8_lossy(v.0).to_string(), value: String::from_utf8_lossy(v.0).to_string(),
span: current_span, span: current_span,
@ -215,13 +172,8 @@ impl Completer for VariableCompletion {
}); });
} }
} }
}
output = sort_suggestions(&prefix_str, output, options); matcher.results()
output.dedup(); // TODO: Removes only consecutive duplicates, is it intended?
output
} }
} }
@ -302,13 +254,3 @@ fn recursive_value(val: &Value, sublevels: &[Vec<u8>]) -> Result<Value, Span> {
Ok(val.clone()) Ok(val.clone())
} }
} }
impl MatchAlgorithm {
pub fn matches_u8_insensitive(&self, sensitive: bool, haystack: &[u8], needle: &[u8]) -> bool {
if sensitive {
self.matches_u8(haystack, needle)
} else {
self.matches_u8(&haystack.to_ascii_lowercase(), &needle.to_ascii_lowercase())
}
}
}

View File

@ -890,8 +890,8 @@ fn subcommand_completions(mut subcommand_completer: NuCompleter) {
match_suggestions( match_suggestions(
&vec![ &vec![
"foo bar".to_string(), "foo bar".to_string(),
"foo aabcrr".to_string(),
"foo abaz".to_string(), "foo abaz".to_string(),
"foo aabcrr".to_string(),
], ],
&suggestions, &suggestions,
); );
@ -955,8 +955,8 @@ fn flag_completions() {
"--mime-type".into(), "--mime-type".into(),
"--short-names".into(), "--short-names".into(),
"--threads".into(), "--threads".into(),
"-D".into(),
"-a".into(), "-a".into(),
"-D".into(),
"-d".into(), "-d".into(),
"-f".into(), "-f".into(),
"-h".into(), "-h".into(),
@ -1287,7 +1287,7 @@ fn variables_completions() {
assert_eq!(3, suggestions.len()); assert_eq!(3, suggestions.len());
#[cfg(windows)] #[cfg(windows)]
let expected: Vec<String> = vec!["PWD".into(), "Path".into(), "TEST".into()]; let expected: Vec<String> = vec!["Path".into(), "PWD".into(), "TEST".into()];
#[cfg(not(windows))] #[cfg(not(windows))]
let expected: Vec<String> = vec!["PATH".into(), "PWD".into(), "TEST".into()]; let expected: Vec<String> = vec!["PATH".into(), "PWD".into(), "TEST".into()];
@ -1576,6 +1576,23 @@ fn sort_fuzzy_completions_in_alphabetical_order(mut fuzzy_alpha_sort_completer:
); );
} }
#[test]
fn exact_match() {
let (dir, _, engine, stack) = new_partial_engine();
let mut completer = NuCompleter::new(Arc::new(engine), Arc::new(stack));
let target_dir = format!("open {}", folder(dir.join("pArTiAL")));
let suggestions = completer.complete(&target_dir, target_dir.len());
// Since it's an exact match, only 'partial' should be suggested, not
// 'partial-a' and stuff. Implemented in #13302
match_suggestions(
&vec![file(dir.join("partial").join("hello.txt"))],
&suggestions,
);
}
#[ignore = "was reverted, still needs fixing"] #[ignore = "was reverted, still needs fixing"]
#[rstest] #[rstest]
fn alias_offset_bug_7648() { fn alias_offset_bug_7648() {

View File

@ -698,7 +698,7 @@ impl EngineState {
pub fn find_commands_by_predicate( pub fn find_commands_by_predicate(
&self, &self,
predicate: impl Fn(&[u8]) -> bool, mut predicate: impl FnMut(&[u8]) -> bool,
ignore_deprecated: bool, ignore_deprecated: bool,
) -> Vec<(Vec<u8>, Option<String>, CommandType)> { ) -> Vec<(Vec<u8>, Option<String>, CommandType)> {
let mut output = vec![]; let mut output = vec![];

View File

@ -724,7 +724,7 @@ impl<'a> StateWorkingSet<'a> {
pub fn find_commands_by_predicate( pub fn find_commands_by_predicate(
&self, &self,
predicate: impl Fn(&[u8]) -> bool, mut predicate: impl FnMut(&[u8]) -> bool,
ignore_deprecated: bool, ignore_deprecated: bool,
) -> Vec<(Vec<u8>, Option<String>, CommandType)> { ) -> Vec<(Vec<u8>, Option<String>, CommandType)> {
let mut output = vec![]; let mut output = vec![];