mirror of
https://github.com/nushell/nushell.git
synced 2025-05-03 09:34:27 +02:00
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:
parent
5f7082f053
commit
671640b0a9
@ -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,68 +105,59 @@ 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);
|
||||||
suggestion: Suggestion {
|
matcher.add(&name, name.to_string())
|
||||||
value: String::from_utf8_lossy(&x.0).to_string(),
|
},
|
||||||
description: x.1,
|
true,
|
||||||
span: reedline::Span::new(span.start - offset, span.end - offset),
|
);
|
||||||
append_whitespace: true,
|
for (name, description, typ) in filtered_commands {
|
||||||
..Suggestion::default()
|
let name = String::from_utf8_lossy(&name);
|
||||||
},
|
internal_suggs.insert(
|
||||||
kind: Some(SuggestionKind::Command(x.2)),
|
name.to_string(),
|
||||||
})
|
SemanticSuggestion {
|
||||||
.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 {
|
suggestion: Suggestion {
|
||||||
value: x,
|
value: name.to_string(),
|
||||||
span: reedline::Span::new(span.start - offset, span.end - offset),
|
description,
|
||||||
|
span: sugg_span,
|
||||||
append_whitespace: true,
|
append_whitespace: true,
|
||||||
..Suggestion::default()
|
..Suggestion::default()
|
||||||
},
|
},
|
||||||
// TODO: is there a way to create a test?
|
kind: Some(SuggestionKind::Command(typ)),
|
||||||
kind: None,
|
},
|
||||||
});
|
);
|
||||||
|
|
||||||
let results_strings: Vec<String> =
|
|
||||||
results.iter().map(|x| x.suggestion.value.clone()).collect();
|
|
||||||
|
|
||||||
for external in results_external {
|
|
||||||
if results_strings.contains(&external.suggestion.value) {
|
|
||||||
results.push(SemanticSuggestion {
|
|
||||||
suggestion: Suggestion {
|
|
||||||
value: format!("^{}", external.suggestion.value),
|
|
||||||
span: external.suggestion.span,
|
|
||||||
append_whitespace: true,
|
|
||||||
..Suggestion::default()
|
|
||||||
},
|
|
||||||
kind: external.kind,
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
results.push(external)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
results
|
|
||||||
} else {
|
|
||||||
results
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let mut external_suggs = if find_externals {
|
||||||
|
self.external_command_completion(
|
||||||
|
working_set,
|
||||||
|
sugg_span,
|
||||||
|
|name| internal_suggs.contains_key(name),
|
||||||
|
&mut matcher,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
HashMap::new()
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut res = Vec::new();
|
||||||
|
for cmd_name in matcher.results() {
|
||||||
|
if let Some(sugg) = internal_suggs
|
||||||
|
.remove(&cmd_name)
|
||||||
|
.or_else(|| external_suggs.remove(&cmd_name))
|
||||||
|
{
|
||||||
|
res.push(sugg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
res
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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,76 +28,84 @@ 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 mut built = built.clone();
|
let built_paths: Vec<_> = built_paths
|
||||||
built.parts.push(base.to_string());
|
.iter()
|
||||||
built.isdir = true;
|
.map(|built| {
|
||||||
return complete_rec(rest, &built, cwd, options, want_directory, isdir);
|
let mut built = built.clone();
|
||||||
}
|
built.parts.push(base.to_string());
|
||||||
}
|
built.isdir = true;
|
||||||
|
built
|
||||||
let mut built_path = cwd.to_path_buf();
|
})
|
||||||
for part in &built.parts {
|
.collect();
|
||||||
built_path.push(part);
|
return complete_rec(rest, &built_paths, options, want_directory, isdir);
|
||||||
}
|
|
||||||
|
|
||||||
let Ok(result) = built_path.read_dir() else {
|
|
||||||
return completions;
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut entries = Vec::new();
|
|
||||||
for entry in result.filter_map(|e| e.ok()) {
|
|
||||||
let entry_name = entry.file_name().to_string_lossy().into_owned();
|
|
||||||
let entry_isdir = entry.path().is_dir();
|
|
||||||
let mut built = built.clone();
|
|
||||||
built.parts.push(entry_name.clone());
|
|
||||||
built.isdir = entry_isdir;
|
|
||||||
|
|
||||||
if !want_directory || entry_isdir {
|
|
||||||
entries.push((entry_name, built));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let prefix = partial.first().unwrap_or(&"");
|
let prefix = partial.first().unwrap_or(&"");
|
||||||
let sorted_entries = sort_completions(prefix, entries, options, |(entry, _)| entry);
|
let mut matcher = NuMatcher::new(prefix, options.clone());
|
||||||
|
|
||||||
for (entry_name, built) in sorted_entries {
|
for built in built_paths {
|
||||||
|
let mut path = built.cwd.clone();
|
||||||
|
for part in &built.parts {
|
||||||
|
path.push(part);
|
||||||
|
}
|
||||||
|
|
||||||
|
let Ok(result) = path.read_dir() else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
for entry in result.filter_map(|e| e.ok()) {
|
||||||
|
let entry_name = entry.file_name().to_string_lossy().into_owned();
|
||||||
|
let entry_isdir = entry.path().is_dir();
|
||||||
|
let mut built = built.clone();
|
||||||
|
built.parts.push(entry_name.clone());
|
||||||
|
built.isdir = entry_isdir;
|
||||||
|
|
||||||
|
if !want_directory || entry_isdir {
|
||||||
|
matcher.add(entry_name.clone(), (entry_name, built));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut completions = vec![];
|
||||||
|
for (entry_name, built) in matcher.results() {
|
||||||
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
|
// components in between.
|
||||||
// components in between.
|
if !rest.is_empty() || isdir {
|
||||||
if !rest.is_empty() || isdir {
|
completions.extend(complete_rec(
|
||||||
completions.extend(complete_rec(
|
rest,
|
||||||
rest,
|
&[built],
|
||||||
&built,
|
options,
|
||||||
cwd,
|
want_directory,
|
||||||
options,
|
isdir,
|
||||||
want_directory,
|
));
|
||||||
isdir,
|
} else {
|
||||||
));
|
completions.push(built);
|
||||||
} else {
|
|
||||||
completions.push(built);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if entry_name.eq(base)
|
|
||||||
&& matches!(options.match_algorithm, MatchAlgorithm::Prefix)
|
// For https://github.com/nushell/nushell/issues/13204
|
||||||
&& isdir
|
if isdir && options.match_algorithm == MatchAlgorithm::Prefix {
|
||||||
{
|
let exact_match = if options.case_sensitive {
|
||||||
break;
|
entry_name.eq(base)
|
||||||
|
} else {
|
||||||
|
entry_name.to_folded_case().eq(&base.to_folded_case())
|
||||||
|
};
|
||||||
|
if exact_match {
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
@ -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;
|
||||||
|
@ -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,33 +22,154 @@ 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,
|
||||||
let haystack = trim_quotes_str(haystack);
|
state: State<T>,
|
||||||
let needle = trim_quotes_str(needle);
|
}
|
||||||
match *self {
|
|
||||||
MatchAlgorithm::Prefix => haystack.starts_with(needle),
|
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 => {
|
MatchAlgorithm::Fuzzy => {
|
||||||
let matcher = SkimMatcherV2::default();
|
let mut matcher = SkimMatcherV2::default();
|
||||||
matcher.fuzzy_match(haystack, needle).is_some()
|
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 the `needle` search text matches the given `haystack`.
|
/// Returns whether or not the haystack matches the needle. If it does, `item` is added
|
||||||
pub fn matches_u8(&self, haystack: &[u8], needle: &[u8]) -> bool {
|
/// to the list of matches (if given).
|
||||||
match *self {
|
///
|
||||||
MatchAlgorithm::Prefix => haystack.starts_with(needle),
|
/// Helper to avoid code duplication between [NuMatcher::add] and [NuMatcher::matches].
|
||||||
MatchAlgorithm::Fuzzy => {
|
fn matches_aux(&mut self, haystack: &str, item: Option<T>) -> bool {
|
||||||
let haystack_str = String::from_utf8_lossy(haystack);
|
let haystack = trim_quotes_str(haystack);
|
||||||
let needle_str = String::from_utf8_lossy(needle);
|
match &mut self.state {
|
||||||
|
State::Prefix { items } => {
|
||||||
let matcher = SkimMatcherV2::default();
|
let haystack_folded = if self.options.case_sensitive {
|
||||||
matcher.fuzzy_match(&haystack_str, &needle_str).is_some()
|
Cow::Borrowed(haystack)
|
||||||
|
} else {
|
||||||
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Add the given item if the given haystack matches the needle.
|
||||||
|
///
|
||||||
|
/// Returns whether the item was added.
|
||||||
|
pub fn add(&mut self, haystack: impl AsRef<str>, item: T) -> bool {
|
||||||
|
self.matches_aux(haystack.as_ref(), Some(item))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns whether the haystack matches the needle.
|
||||||
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<CompletionAlgorithm> for MatchAlgorithm {
|
impl From<CompletionAlgorithm> for MatchAlgorithm {
|
||||||
@ -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]));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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()
|
|
||||||
}
|
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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,49 +87,44 @@ 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(
|
|
||||||
span,
|
|
||||||
&partial,
|
|
||||||
&search_dir,
|
|
||||||
options,
|
|
||||||
working_set.permanent_state,
|
|
||||||
stack,
|
|
||||||
);
|
|
||||||
completions
|
|
||||||
.into_iter()
|
|
||||||
.filter(move |it| {
|
|
||||||
// Different base dir, so we list the .nu files or folders
|
|
||||||
if !is_current_folder {
|
|
||||||
it.1.ends_with(".nu") || it.1.ends_with(SEP)
|
|
||||||
} else {
|
|
||||||
// Lib dirs, so we filter only the .nu files or directory modules
|
|
||||||
if it.1.ends_with(SEP) {
|
|
||||||
Path::new(&search_dir).join(&it.1).join("mod.nu").exists()
|
|
||||||
} else {
|
|
||||||
it.1.ends_with(".nu")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.map(move |x| SemanticSuggestion {
|
|
||||||
suggestion: Suggestion {
|
|
||||||
value: x.1,
|
|
||||||
style: x.2,
|
|
||||||
span: reedline::Span {
|
|
||||||
start: x.0.start - offset,
|
|
||||||
end: x.0.end - offset,
|
|
||||||
},
|
|
||||||
append_whitespace: true,
|
|
||||||
..Suggestion::default()
|
|
||||||
},
|
|
||||||
// TODO????
|
|
||||||
kind: None,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
sort_suggestions(&prefix_str, output, options)
|
let completions = file_path_completion(
|
||||||
|
span,
|
||||||
|
&partial,
|
||||||
|
&search_dirs.iter().map(|d| d.as_str()).collect::<Vec<_>>(),
|
||||||
|
options,
|
||||||
|
working_set.permanent_state,
|
||||||
|
stack,
|
||||||
|
);
|
||||||
|
completions
|
||||||
|
.into_iter()
|
||||||
|
.filter(move |it| {
|
||||||
|
// Different base dir, so we list the .nu files or folders
|
||||||
|
if !is_current_folder {
|
||||||
|
it.path.ends_with(".nu") || it.path.ends_with(SEP)
|
||||||
|
} else {
|
||||||
|
// Lib dirs, so we filter only the .nu files or directory modules
|
||||||
|
if it.path.ends_with(SEP) {
|
||||||
|
Path::new(&it.cwd).join(&it.path).join("mod.nu").exists()
|
||||||
|
} else {
|
||||||
|
it.path.ends_with(".nu")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.map(move |x| SemanticSuggestion {
|
||||||
|
suggestion: Suggestion {
|
||||||
|
value: x.path,
|
||||||
|
style: x.style,
|
||||||
|
span: reedline::Span {
|
||||||
|
start: x.span.start - offset,
|
||||||
|
end: x.span.end - offset,
|
||||||
|
},
|
||||||
|
append_whitespace: true,
|
||||||
|
..Suggestion::default()
|
||||||
|
},
|
||||||
|
// TODO????
|
||||||
|
kind: None,
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)
|
|
||||||
}
|
}
|
||||||
|
@ -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,34 +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 {
|
|
||||||
value: String::from_utf8_lossy(&named).to_string(),
|
|
||||||
description: Some(flag_desc.to_string()),
|
|
||||||
span: reedline::Span {
|
|
||||||
start: span.start - offset,
|
|
||||||
end: span.end - offset,
|
|
||||||
},
|
|
||||||
append_whitespace: true,
|
|
||||||
..Suggestion::default()
|
|
||||||
},
|
|
||||||
// TODO????
|
|
||||||
kind: None,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if named.long.is_empty() {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut named = named.long.as_bytes().to_vec();
|
|
||||||
named.insert(0, b'-');
|
|
||||||
named.insert(0, b'-');
|
|
||||||
|
|
||||||
if options.match_algorithm.matches_u8(&named, prefix) {
|
|
||||||
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 +59,32 @@ impl Completer for FlagCompletion {
|
|||||||
kind: None,
|
kind: None,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if named.long.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut named = named.long.as_bytes().to_vec();
|
||||||
|
named.insert(0, b'-');
|
||||||
|
named.insert(0, b'-');
|
||||||
|
|
||||||
|
matcher.add_semantic_suggestion(SemanticSuggestion {
|
||||||
|
suggestion: Suggestion {
|
||||||
|
value: String::from_utf8_lossy(&named).to_string(),
|
||||||
|
description: Some(flag_desc.to_string()),
|
||||||
|
span: reedline::Span {
|
||||||
|
start: span.start - offset,
|
||||||
|
end: span.end - offset,
|
||||||
|
},
|
||||||
|
append_whitespace: true,
|
||||||
|
..Suggestion::default()
|
||||||
|
},
|
||||||
|
// TODO????
|
||||||
|
kind: None,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return sort_suggestions(&String::from_utf8_lossy(prefix), output, options);
|
return matcher.results();
|
||||||
}
|
}
|
||||||
|
|
||||||
vec![]
|
vec![]
|
||||||
|
@ -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;
|
||||||
|
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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,37 +61,25 @@ 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,
|
suggestion: Suggestion {
|
||||||
env_var.0.as_bytes(),
|
value: env_var.0,
|
||||||
prefix,
|
span: current_span,
|
||||||
) {
|
..Suggestion::default()
|
||||||
output.push(SemanticSuggestion {
|
},
|
||||||
suggestion: Suggestion {
|
kind: Some(SuggestionKind::Type(env_var.1.get_type())),
|
||||||
value: env_var.0,
|
});
|
||||||
span: current_span,
|
|
||||||
..Suggestion::default()
|
|
||||||
},
|
|
||||||
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,37 +110,25 @@ 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,
|
suggestion: Suggestion {
|
||||||
builtin.as_bytes(),
|
value: builtin.to_string(),
|
||||||
prefix,
|
span: current_span,
|
||||||
) {
|
..Suggestion::default()
|
||||||
output.push(SemanticSuggestion {
|
},
|
||||||
suggestion: Suggestion {
|
// TODO is there a way to get the VarId to get the type???
|
||||||
value: builtin.to_string(),
|
kind: None,
|
||||||
span: current_span,
|
});
|
||||||
..Suggestion::default()
|
|
||||||
},
|
|
||||||
// TODO is there a way to get the VarId to get the type???
|
|
||||||
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
|
||||||
@ -170,40 +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 {
|
|
||||||
value: String::from_utf8_lossy(v.0).to_string(),
|
|
||||||
span: current_span,
|
|
||||||
..Suggestion::default()
|
|
||||||
},
|
|
||||||
kind: Some(SuggestionKind::Type(
|
|
||||||
working_set.get_variable(*v.1).ty.clone(),
|
|
||||||
)),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Permanent state vars
|
|
||||||
// for scope in &self.engine_state.scope {
|
|
||||||
for overlay_frame in working_set
|
|
||||||
.permanent_state
|
|
||||||
.active_overlays(&removed_overlays)
|
|
||||||
.rev()
|
|
||||||
{
|
|
||||||
for v in &overlay_frame.vars {
|
|
||||||
if options.match_algorithm.matches_u8_insensitive(
|
|
||||||
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,
|
||||||
@ -217,11 +152,28 @@ impl Completer for VariableCompletion {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
output = sort_suggestions(&prefix_str, output, options);
|
// Permanent state vars
|
||||||
|
// for scope in &self.engine_state.scope {
|
||||||
|
for overlay_frame in working_set
|
||||||
|
.permanent_state
|
||||||
|
.active_overlays(&removed_overlays)
|
||||||
|
.rev()
|
||||||
|
{
|
||||||
|
for v in &overlay_frame.vars {
|
||||||
|
matcher.add_semantic_suggestion(SemanticSuggestion {
|
||||||
|
suggestion: Suggestion {
|
||||||
|
value: String::from_utf8_lossy(v.0).to_string(),
|
||||||
|
span: current_span,
|
||||||
|
..Suggestion::default()
|
||||||
|
},
|
||||||
|
kind: Some(SuggestionKind::Type(
|
||||||
|
working_set.get_variable(*v.1).ty.clone(),
|
||||||
|
)),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
output.dedup(); // TODO: Removes only consecutive duplicates, is it intended?
|
matcher.results()
|
||||||
|
|
||||||
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())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -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() {
|
||||||
|
@ -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![];
|
||||||
|
@ -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![];
|
||||||
|
Loading…
Reference in New Issue
Block a user