Add MatchAlgorithm for completion suggestions (#5244)

* Pass completion options to each fetch() call

* Add MatchAlgorithm to CompletionOptions

* Add unit test for MatchAlgorithm

* Pass completion options to directory completer
This commit is contained in:
Richard 2022-04-23 17:01:19 +02:00 committed by GitHub
parent 667eb27d1b
commit e6a70f9846
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 151 additions and 64 deletions

View file

@ -1,4 +1,4 @@
use crate::completions::SortBy;
use crate::completions::{CompletionOptions, SortBy};
use nu_protocol::{engine::StateWorkingSet, levenshtein_distance, Span};
use reedline::Suggestion;
@ -12,6 +12,7 @@ pub trait Completer {
span: Span,
offset: usize,
pos: usize,
options: &CompletionOptions,
) -> Vec<Suggestion>;
fn get_sort_by(&self) -> SortBy {

View file

@ -1,4 +1,6 @@
use crate::completions::{file_completions::file_path_completion, Completer, SortBy};
use crate::completions::{
file_completions::file_path_completion, Completer, CompletionOptions, MatchAlgorithm, SortBy,
};
use nu_parser::{trim_quotes, FlatShape};
use nu_protocol::{
engine::{EngineState, StateWorkingSet},
@ -30,7 +32,11 @@ impl CommandCompletion {
}
}
fn external_command_completion(&self, prefix: &str) -> Vec<String> {
fn external_command_completion(
&self,
prefix: &str,
match_algorithm: MatchAlgorithm,
) -> Vec<String> {
let mut executables = vec![];
let paths = self.engine_state.env_vars.get("PATH");
@ -51,7 +57,8 @@ impl CommandCompletion {
) && matches!(
item.path()
.file_name()
.map(|x| x.to_string_lossy().starts_with(prefix)),
.map(|x| match_algorithm
.matches_str(&x.to_string_lossy(), prefix)),
Some(true)
) && is_executable::is_executable(&item.path())
{
@ -74,11 +81,14 @@ impl CommandCompletion {
span: Span,
offset: usize,
find_externals: bool,
match_algorithm: MatchAlgorithm,
) -> Vec<Suggestion> {
let prefix = working_set.get_span_contents(span);
let partial = working_set.get_span_contents(span);
let filter_predicate = |command: &[u8]| match_algorithm.matches_u8(command, partial);
let results = working_set
.find_commands_by_prefix(prefix)
.find_commands_by_predicate(filter_predicate)
.into_iter()
.map(move |x| Suggestion {
value: String::from_utf8_lossy(&x.0).to_string(),
@ -90,12 +100,29 @@ impl CommandCompletion {
},
});
let results_aliases =
working_set
.find_aliases_by_prefix(prefix)
let results_aliases = working_set
.find_aliases_by_predicate(filter_predicate)
.into_iter()
.map(move |x| Suggestion {
value: String::from_utf8_lossy(&x).to_string(),
description: None,
extra: None,
span: reedline::Span {
start: span.start - offset,
end: span.end - offset,
},
});
let mut results = results.chain(results_aliases).collect::<Vec<_>>();
let partial = working_set.get_span_contents(span);
let partial = String::from_utf8_lossy(partial).to_string();
let results = if find_externals {
let results_external = self
.external_command_completion(&partial, match_algorithm)
.into_iter()
.map(move |x| Suggestion {
value: String::from_utf8_lossy(&x).to_string(),
value: x,
description: None,
extra: None,
span: reedline::Span {
@ -104,24 +131,6 @@ impl CommandCompletion {
},
});
let mut results = results.chain(results_aliases).collect::<Vec<_>>();
let prefix = working_set.get_span_contents(span);
let prefix = String::from_utf8_lossy(prefix).to_string();
let results = if find_externals {
let results_external =
self.external_command_completion(&prefix)
.into_iter()
.map(move |x| Suggestion {
value: x,
description: None,
extra: None,
span: reedline::Span {
start: span.start - offset,
end: span.end - offset,
},
});
for external in results_external {
if results.contains(&external) {
results.push(Suggestion {
@ -152,6 +161,7 @@ impl Completer for CommandCompletion {
span: Span,
offset: usize,
pos: usize,
options: &CompletionOptions,
) -> Vec<Suggestion> {
let last = self
.flattened
@ -180,6 +190,7 @@ impl Completer for CommandCompletion {
},
offset,
false,
options.match_algorithm,
)
} else {
vec![]
@ -194,7 +205,7 @@ impl Completer for CommandCompletion {
|| ((span.end - span.start) == 0)
{
// we're in a gap or at a command
self.complete_commands(working_set, span, offset, true)
self.complete_commands(working_set, span, offset, true, options.match_algorithm)
} else {
vec![]
};
@ -221,7 +232,7 @@ impl Completer for CommandCompletion {
// let prefix = working_set.get_span_contents(flat.0);
let prefix = String::from_utf8_lossy(&prefix).to_string();
file_path_completion(span, &prefix, &cwd)
file_path_completion(span, &prefix, &cwd, options.match_algorithm)
.into_iter()
.map(move |x| {
if self.flat_idx == 0 {

View file

@ -1,6 +1,6 @@
use crate::completions::{
CommandCompletion, Completer, CustomCompletion, DirectoryCompletion, DotNuCompletion,
FileCompletion, FlagCompletion, VariableCompletion,
CommandCompletion, Completer, CompletionOptions, CustomCompletion, DirectoryCompletion,
DotNuCompletion, FileCompletion, FlagCompletion, VariableCompletion,
};
use nu_parser::{flatten_expression, parse, FlatShape};
use nu_protocol::{
@ -35,8 +35,11 @@ impl NuCompleter {
offset: usize,
pos: usize,
) -> Vec<Suggestion> {
let options = CompletionOptions::default();
// Fetch
let mut suggestions = completer.fetch(working_set, prefix.clone(), new_span, offset, pos);
let mut suggestions =
completer.fetch(working_set, prefix.clone(), new_span, offset, pos, &options);
// Sort
suggestions = completer.sort(suggestions, prefix);

View file

@ -5,11 +5,38 @@ pub enum SortBy {
None,
}
/// Describes how suggestions should be matched.
#[derive(Copy, Clone)]
pub enum MatchAlgorithm {
/// Only show suggestions which begin with the given input
///
/// Example:
/// "git switch" is matched by "git sw"
Prefix,
}
impl MatchAlgorithm {
/// Returns whether the `needle` search text matches the given `haystack`.
pub fn matches_str(&self, haystack: &str, needle: &str) -> bool {
match *self {
MatchAlgorithm::Prefix => haystack.starts_with(needle),
}
}
/// Returns whether the `needle` search text matches the given `haystack`.
pub fn matches_u8(&self, haystack: &[u8], needle: &[u8]) -> bool {
match *self {
MatchAlgorithm::Prefix => haystack.starts_with(needle),
}
}
}
#[derive(Clone)]
pub struct CompletionOptions {
pub case_sensitive: bool,
pub positional: bool,
pub sort_by: SortBy,
pub match_algorithm: MatchAlgorithm,
}
impl Default for CompletionOptions {
@ -18,6 +45,25 @@ impl Default for CompletionOptions {
case_sensitive: true,
positional: true,
sort_by: SortBy::Ascending,
match_algorithm: MatchAlgorithm::Prefix,
}
}
}
#[cfg(test)]
mod test {
use super::MatchAlgorithm;
#[test]
fn match_algorithm_prefix() {
let algorithm = MatchAlgorithm::Prefix;
assert!(algorithm.matches_str("example text", ""));
assert!(algorithm.matches_str("example text", "examp"));
assert!(!algorithm.matches_str("example text", "text"));
assert!(algorithm.matches_u8(&[1, 2, 3], &[]));
assert!(algorithm.matches_u8(&[1, 2, 3], &[1, 2]));
assert!(!algorithm.matches_u8(&[1, 2, 3], &[2, 3]));
}
}

View file

@ -1,4 +1,4 @@
use crate::completions::{Completer, CompletionOptions, SortBy};
use crate::completions::{Completer, CompletionOptions, MatchAlgorithm, SortBy};
use nu_engine::eval_call;
use nu_protocol::{
ast::{Argument, Call, Expr, Expression},
@ -97,6 +97,7 @@ impl Completer for CustomCompletion {
span: Span,
offset: usize,
pos: usize,
_options: &CompletionOptions,
) -> Vec<Suggestion> {
// Line position
let line_pos = pos - offset;
@ -169,6 +170,7 @@ impl Completer for CustomCompletion {
} else {
SortBy::None
},
match_algorithm: MatchAlgorithm::Prefix,
}
} else {
CompletionOptions::default()

View file

@ -1,4 +1,4 @@
use crate::completions::{file_path_completion, Completer};
use crate::completions::{file_path_completion, Completer, CompletionOptions};
use nu_protocol::{
engine::{EngineState, StateWorkingSet},
levenshtein_distance, Span,
@ -28,6 +28,7 @@ impl Completer for DirectoryCompletion {
span: Span,
offset: usize,
_: usize,
options: &CompletionOptions,
) -> Vec<Suggestion> {
let cwd = if let Some(d) = self.engine_state.env_vars.get("PWD") {
match d.as_string() {
@ -37,10 +38,10 @@ impl Completer for DirectoryCompletion {
} else {
"".to_string()
};
let prefix = String::from_utf8_lossy(&prefix).to_string();
let partial = String::from_utf8_lossy(&prefix).to_string();
// Filter only the folders
let output: Vec<_> = file_path_completion(span, &prefix, &cwd)
let output: Vec<_> = file_path_completion(span, &partial, &cwd, options.match_algorithm)
.into_iter()
.filter_map(move |x| {
if x.1.ends_with(SEP) {

View file

@ -1,4 +1,6 @@
use crate::completions::{file_path_completion, partial_from, Completer, SortBy};
use crate::completions::{
file_path_completion, partial_from, Completer, CompletionOptions, SortBy,
};
use nu_protocol::{
engine::{EngineState, StateWorkingSet},
Span,
@ -26,6 +28,7 @@ impl Completer for DotNuCompletion {
span: Span,
offset: usize,
_: usize,
options: &CompletionOptions,
) -> Vec<Suggestion> {
let prefix_str = String::from_utf8_lossy(&prefix).to_string();
let mut search_dirs: Vec<String> = vec![];
@ -88,7 +91,7 @@ impl Completer for DotNuCompletion {
let output: Vec<Suggestion> = search_dirs
.into_iter()
.flat_map(|it| {
file_path_completion(span, &partial, &it)
file_path_completion(span, &partial, &it, options.match_algorithm)
.into_iter()
.filter(|it| {
// Different base dir, so we list the .nu files or folders

View file

@ -1,4 +1,4 @@
use crate::completions::Completer;
use crate::completions::{Completer, CompletionOptions, MatchAlgorithm};
use nu_protocol::{
engine::{EngineState, StateWorkingSet},
levenshtein_distance, Span,
@ -28,6 +28,7 @@ impl Completer for FileCompletion {
span: Span,
offset: usize,
_: usize,
options: &CompletionOptions,
) -> Vec<Suggestion> {
let cwd = if let Some(d) = self.engine_state.env_vars.get("PWD") {
match d.as_string() {
@ -38,7 +39,7 @@ impl Completer for FileCompletion {
"".to_string()
};
let prefix = String::from_utf8_lossy(&prefix).to_string();
let output: Vec<_> = file_path_completion(span, &prefix, &cwd)
let output: Vec<_> = file_path_completion(span, &prefix, &cwd, options.match_algorithm)
.into_iter()
.map(move |x| Suggestion {
value: x.1,
@ -110,6 +111,7 @@ pub fn file_path_completion(
span: nu_protocol::Span,
partial: &str,
cwd: &str,
match_algorithm: MatchAlgorithm,
) -> Vec<(nu_protocol::Span, String)> {
let (base_dir_name, partial) = partial_from(partial);
@ -125,7 +127,7 @@ pub fn file_path_completion(
.filter_map(|entry| {
entry.ok().and_then(|entry| {
let mut file_name = entry.file_name().to_string_lossy().into_owned();
if matches(&partial, &file_name) {
if matches(&partial, &file_name, match_algorithm) {
let mut path = format!("{}{}", base_dir_name, file_name);
if entry.path().is_dir() {
path.push(SEP);
@ -153,7 +155,6 @@ pub fn file_path_completion(
Vec::new()
}
pub fn matches(partial: &str, from: &str) -> bool {
from.to_ascii_lowercase()
.starts_with(&partial.to_ascii_lowercase())
pub fn matches(partial: &str, from: &str, match_algorithm: MatchAlgorithm) -> bool {
match_algorithm.matches_str(&from.to_ascii_lowercase(), &partial.to_ascii_lowercase())
}

View file

@ -1,4 +1,4 @@
use crate::completions::Completer;
use crate::completions::{Completer, CompletionOptions};
use nu_protocol::{
ast::{Expr, Expression},
engine::StateWorkingSet,
@ -26,6 +26,7 @@ impl Completer for FlagCompletion {
span: Span,
offset: usize,
_: usize,
options: &CompletionOptions,
) -> Vec<Suggestion> {
// Check if it's a flag
if let Expr::Call(call) = &self.expression.expr {
@ -40,7 +41,8 @@ impl Completer for FlagCompletion {
let mut named = vec![0; short.len_utf8()];
short.encode_utf8(&mut named);
named.insert(0, b'-');
if named.starts_with(&prefix) {
if options.match_algorithm.matches_u8(&named, &prefix) {
output.push(Suggestion {
value: String::from_utf8_lossy(&named).to_string(),
description: Some(flag_desc.to_string()),
@ -60,7 +62,8 @@ impl Completer for FlagCompletion {
let mut named = named.long.as_bytes().to_vec();
named.insert(0, b'-');
named.insert(0, b'-');
if named.starts_with(&prefix) {
if options.match_algorithm.matches_u8(&named, &prefix) {
output.push(Suggestion {
value: String::from_utf8_lossy(&named).to_string(),
description: Some(flag_desc.to_string()),

View file

@ -12,7 +12,7 @@ mod variable_completions;
pub use base::Completer;
pub use command_completions::CommandCompletion;
pub use completer::NuCompleter;
pub use completion_options::{CompletionOptions, SortBy};
pub use completion_options::{CompletionOptions, MatchAlgorithm, SortBy};
pub use custom_completions::CustomCompletion;
pub use directory_completions::DirectoryCompletion;
pub use dotnu_completions::DotNuCompletion;

View file

@ -1,4 +1,4 @@
use crate::completions::Completer;
use crate::completions::{Completer, CompletionOptions};
use nu_engine::eval_variable;
use nu_protocol::{
engine::{EngineState, Stack, StateWorkingSet},
@ -37,6 +37,7 @@ impl Completer for VariableCompletion {
span: Span,
offset: usize,
_: usize,
options: &CompletionOptions,
) -> Vec<Suggestion> {
let mut output = vec![];
let builtins = ["$nu", "$in", "$config", "$env", "$nothing"];
@ -54,7 +55,10 @@ impl Completer for VariableCompletion {
// Completion for $env.<tab>
if var_str.as_str() == "$env" {
for env_var in self.stack.get_env_vars(&self.engine_state) {
if env_var.0.as_bytes().starts_with(&prefix) {
if options
.match_algorithm
.matches_u8(env_var.0.as_bytes(), &prefix)
{
output.push(Suggestion {
value: env_var.0,
description: None,
@ -155,7 +159,10 @@ impl Completer for VariableCompletion {
// Variable completion (e.g: $en<tab> to complete $env)
for builtin in builtins {
if builtin.as_bytes().starts_with(&prefix) {
if options
.match_algorithm
.matches_u8(builtin.as_bytes(), &prefix)
{
output.push(Suggestion {
value: builtin.to_string(),
description: None,
@ -168,7 +175,7 @@ impl Completer for VariableCompletion {
// Working set scope vars
for scope in &working_set.delta.scope {
for v in &scope.vars {
if v.0.starts_with(&prefix) {
if options.match_algorithm.matches_u8(v.0, &prefix) {
output.push(Suggestion {
value: String::from_utf8_lossy(v.0).to_string(),
description: None,
@ -182,7 +189,7 @@ impl Completer for VariableCompletion {
// Permanent state vars
for scope in &self.engine_state.scope {
for v in &scope.vars {
if v.0.starts_with(&prefix) {
if options.match_algorithm.matches_u8(v.0, &prefix) {
output.push(Suggestion {
value: String::from_utf8_lossy(v.0).to_string(),
description: None,

View file

@ -444,12 +444,15 @@ impl EngineState {
None
}
pub fn find_commands_by_prefix(&self, name: &[u8]) -> Vec<(Vec<u8>, Option<String>)> {
pub fn find_commands_by_predicate(
&self,
predicate: impl Fn(&[u8]) -> bool,
) -> Vec<(Vec<u8>, Option<String>)> {
let mut output = vec![];
for scope in self.scope.iter().rev() {
for decl in &scope.decls {
if decl.0.starts_with(name) {
if predicate(decl.0) {
let command = self.get_decl(*decl.1);
output.push((decl.0.clone(), Some(command.usage().to_string())));
}
@ -459,12 +462,12 @@ impl EngineState {
output
}
pub fn find_aliases_by_prefix(&self, name: &[u8]) -> Vec<Vec<u8>> {
pub fn find_aliases_by_predicate(&self, predicate: impl Fn(&[u8]) -> bool) -> Vec<Vec<u8>> {
self.scope
.iter()
.rev()
.flat_map(|scope| &scope.aliases)
.filter(|decl| decl.0.starts_with(name))
.filter(|decl| predicate(decl.0))
.map(|decl| decl.0.clone())
.collect()
}
@ -1315,34 +1318,40 @@ impl<'a> StateWorkingSet<'a> {
}
}
pub fn find_commands_by_prefix(&self, name: &[u8]) -> Vec<(Vec<u8>, Option<String>)> {
pub fn find_commands_by_predicate(
&self,
predicate: impl Fn(&[u8]) -> bool,
) -> Vec<(Vec<u8>, Option<String>)> {
let mut output = vec![];
for scope in self.delta.scope.iter().rev() {
for decl in &scope.decls {
if decl.0.starts_with(name) {
if predicate(decl.0) {
let command = self.get_decl(*decl.1);
output.push((decl.0.clone(), Some(command.usage().to_string())));
}
}
}
let mut permanent = self.permanent_state.find_commands_by_prefix(name);
let mut permanent = self.permanent_state.find_commands_by_predicate(predicate);
output.append(&mut permanent);
output
}
pub fn find_aliases_by_prefix(&self, name: &[u8]) -> Vec<Vec<u8>> {
pub fn find_aliases_by_predicate(
&self,
predicate: impl Fn(&[u8]) -> bool + Copy,
) -> Vec<Vec<u8>> {
self.delta
.scope
.iter()
.rev()
.flat_map(|scope| &scope.aliases)
.filter(|decl| decl.0.starts_with(name))
.filter(|decl| predicate(decl.0))
.map(|decl| decl.0.clone())
.chain(self.permanent_state.find_aliases_by_prefix(name))
.chain(self.permanent_state.find_aliases_by_predicate(predicate))
.collect()
}