diff --git a/crates/nu-cli/src/completions/base.rs b/crates/nu-cli/src/completions/base.rs index 9c00d679ae..a7d492fceb 100644 --- a/crates/nu-cli/src/completions/base.rs +++ b/crates/nu-cli/src/completions/base.rs @@ -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; fn get_sort_by(&self) -> SortBy { diff --git a/crates/nu-cli/src/completions/command_completions.rs b/crates/nu-cli/src/completions/command_completions.rs index 32c8407e0e..1531fc63db 100644 --- a/crates/nu-cli/src/completions/command_completions.rs +++ b/crates/nu-cli/src/completions/command_completions.rs @@ -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 { + fn external_command_completion( + &self, + prefix: &str, + match_algorithm: MatchAlgorithm, + ) -> Vec { 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 { - 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::>(); + + 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::>(); - - 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 { 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 { diff --git a/crates/nu-cli/src/completions/completer.rs b/crates/nu-cli/src/completions/completer.rs index 0b67eba93f..ca14ab4724 100644 --- a/crates/nu-cli/src/completions/completer.rs +++ b/crates/nu-cli/src/completions/completer.rs @@ -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 { + 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); diff --git a/crates/nu-cli/src/completions/completion_options.rs b/crates/nu-cli/src/completions/completion_options.rs index 414c966b39..c03f96d68b 100644 --- a/crates/nu-cli/src/completions/completion_options.rs +++ b/crates/nu-cli/src/completions/completion_options.rs @@ -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])); + } +} diff --git a/crates/nu-cli/src/completions/custom_completions.rs b/crates/nu-cli/src/completions/custom_completions.rs index b7e897d591..e7c5c11f41 100644 --- a/crates/nu-cli/src/completions/custom_completions.rs +++ b/crates/nu-cli/src/completions/custom_completions.rs @@ -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 { // 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() diff --git a/crates/nu-cli/src/completions/directory_completions.rs b/crates/nu-cli/src/completions/directory_completions.rs index 073e375dfd..8f18dea755 100644 --- a/crates/nu-cli/src/completions/directory_completions.rs +++ b/crates/nu-cli/src/completions/directory_completions.rs @@ -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 { 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) { diff --git a/crates/nu-cli/src/completions/dotnu_completions.rs b/crates/nu-cli/src/completions/dotnu_completions.rs index e6310de70b..527bfc818e 100644 --- a/crates/nu-cli/src/completions/dotnu_completions.rs +++ b/crates/nu-cli/src/completions/dotnu_completions.rs @@ -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 { let prefix_str = String::from_utf8_lossy(&prefix).to_string(); let mut search_dirs: Vec = vec![]; @@ -88,7 +91,7 @@ impl Completer for DotNuCompletion { let output: Vec = 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 diff --git a/crates/nu-cli/src/completions/file_completions.rs b/crates/nu-cli/src/completions/file_completions.rs index f880ddc9a0..4c146060fd 100644 --- a/crates/nu-cli/src/completions/file_completions.rs +++ b/crates/nu-cli/src/completions/file_completions.rs @@ -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 { 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()) } diff --git a/crates/nu-cli/src/completions/flag_completions.rs b/crates/nu-cli/src/completions/flag_completions.rs index 34c56bfd6d..6344c7654c 100644 --- a/crates/nu-cli/src/completions/flag_completions.rs +++ b/crates/nu-cli/src/completions/flag_completions.rs @@ -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 { // 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()), diff --git a/crates/nu-cli/src/completions/mod.rs b/crates/nu-cli/src/completions/mod.rs index c08c0c427c..9e7a1d8379 100644 --- a/crates/nu-cli/src/completions/mod.rs +++ b/crates/nu-cli/src/completions/mod.rs @@ -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; diff --git a/crates/nu-cli/src/completions/variable_completions.rs b/crates/nu-cli/src/completions/variable_completions.rs index 5a105a0b94..fa96f5e94e 100644 --- a/crates/nu-cli/src/completions/variable_completions.rs +++ b/crates/nu-cli/src/completions/variable_completions.rs @@ -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 { let mut output = vec![]; let builtins = ["$nu", "$in", "$config", "$env", "$nothing"]; @@ -54,7 +55,10 @@ impl Completer for VariableCompletion { // Completion for $env. 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 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, diff --git a/crates/nu-protocol/src/engine/engine_state.rs b/crates/nu-protocol/src/engine/engine_state.rs index 577b57cace..6aba446161 100644 --- a/crates/nu-protocol/src/engine/engine_state.rs +++ b/crates/nu-protocol/src/engine/engine_state.rs @@ -444,12 +444,15 @@ impl EngineState { None } - pub fn find_commands_by_prefix(&self, name: &[u8]) -> Vec<(Vec, Option)> { + pub fn find_commands_by_predicate( + &self, + predicate: impl Fn(&[u8]) -> bool, + ) -> Vec<(Vec, Option)> { 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> { + pub fn find_aliases_by_predicate(&self, predicate: impl Fn(&[u8]) -> bool) -> Vec> { 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, Option)> { + pub fn find_commands_by_predicate( + &self, + predicate: impl Fn(&[u8]) -> bool, + ) -> Vec<(Vec, Option)> { 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> { + pub fn find_aliases_by_predicate( + &self, + predicate: impl Fn(&[u8]) -> bool + Copy, + ) -> Vec> { 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() }