From df2845a9b4d4ff4adb12e69e6ecdc47079c9b985 Mon Sep 17 00:00:00 2001 From: rezural <69941255+rezural@users.noreply.github.com> Date: Fri, 18 Sep 2020 00:52:58 +1000 Subject: [PATCH] implementing case-sensitive & case-insensitive completion matching (#2556) Co-authored-by: Jonathan Turner --- crates/nu-cli/src/completion/command.rs | 7 +-- crates/nu-cli/src/completion/flag.rs | 5 ++- .../completion/matchers/case_insensitive.rs | 45 +++++++++++++++++++ .../src/completion/matchers/case_sensitive.rs | 28 ++++++++++++ crates/nu-cli/src/completion/matchers/mod.rs | 6 +++ crates/nu-cli/src/completion/mod.rs | 4 +- crates/nu-cli/src/completion/path.rs | 14 ++++-- crates/nu-cli/src/shell/completer.rs | 27 +++++++++-- 8 files changed, 123 insertions(+), 13 deletions(-) create mode 100644 crates/nu-cli/src/completion/matchers/case_insensitive.rs create mode 100644 crates/nu-cli/src/completion/matchers/case_sensitive.rs create mode 100644 crates/nu-cli/src/completion/matchers/mod.rs diff --git a/crates/nu-cli/src/completion/command.rs b/crates/nu-cli/src/completion/command.rs index 855148d50..c87a82402 100644 --- a/crates/nu-cli/src/completion/command.rs +++ b/crates/nu-cli/src/completion/command.rs @@ -3,13 +3,14 @@ use std::path::Path; use indexmap::set::IndexSet; +use super::matchers::Matcher; use crate::completion::{Completer, Context, Suggestion}; use crate::context; pub struct CommandCompleter; impl Completer for CommandCompleter { - fn complete(&self, ctx: &Context<'_>, partial: &str) -> Vec { + fn complete(&self, ctx: &Context<'_>, partial: &str, matcher: &dyn Matcher) -> Vec { let context: &context::Context = ctx.as_ref(); let mut commands: IndexSet = IndexSet::from_iter(context.registry.names()); @@ -25,7 +26,7 @@ impl Completer for CommandCompleter { let mut suggestions: Vec<_> = commands .into_iter() - .filter(|v| v.starts_with(partial)) + .filter(|v| matcher.matches(partial, v)) .map(|v| Suggestion { replacement: v.clone(), display: v, @@ -34,7 +35,7 @@ impl Completer for CommandCompleter { if partial != "" { let path_completer = crate::completion::path::PathCompleter; - let path_results = path_completer.path_suggestions(partial); + let path_results = path_completer.path_suggestions(partial, matcher); let iter = path_results.into_iter().filter_map(|path_suggestion| { let path = path_suggestion.path; if path.is_dir() || is_executable(&path) { diff --git a/crates/nu-cli/src/completion/flag.rs b/crates/nu-cli/src/completion/flag.rs index 8b2ffd183..25c0ab6fa 100644 --- a/crates/nu-cli/src/completion/flag.rs +++ b/crates/nu-cli/src/completion/flag.rs @@ -1,3 +1,4 @@ +use super::matchers::Matcher; use crate::completion::{Completer, Context, Suggestion}; use crate::context; @@ -6,7 +7,7 @@ pub struct FlagCompleter { } impl Completer for FlagCompleter { - fn complete(&self, ctx: &Context<'_>, partial: &str) -> Vec { + fn complete(&self, ctx: &Context<'_>, partial: &str, matcher: &dyn Matcher) -> Vec { let context: &context::Context = ctx.as_ref(); if let Some(cmd) = context.registry.get_command(&self.cmd) { @@ -22,7 +23,7 @@ impl Completer for FlagCompleter { suggestions .into_iter() - .filter(|v| v.starts_with(partial)) + .filter(|v| matcher.matches(partial, v)) .map(|v| Suggestion { replacement: format!("{} ", v), display: v, diff --git a/crates/nu-cli/src/completion/matchers/case_insensitive.rs b/crates/nu-cli/src/completion/matchers/case_insensitive.rs new file mode 100644 index 000000000..020bcb2f0 --- /dev/null +++ b/crates/nu-cli/src/completion/matchers/case_insensitive.rs @@ -0,0 +1,45 @@ +use crate::completion::matchers; +pub struct Matcher; + +impl matchers::Matcher for Matcher { + fn matches(&self, partial: &str, from: &str) -> bool { + from.to_ascii_lowercase() + .starts_with(partial.to_ascii_lowercase().as_str()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + // TODO: check some unicode matches if this becomes relevant + + // FIXME: could work exhaustively through ['-', '--'. ''] in a loop for each test + #[test] + fn completes_exact_matches() { + let matcher: Box = Box::new(Matcher); + + assert!(matcher.matches("shouldmatch", "shouldmatch")); + assert!(matcher.matches("shouldm", "shouldmatch")); + assert!(matcher.matches("--also-should-m", "--also-should-match")); + assert!(matcher.matches("-also-should-m", "-also-should-match")); + } + + #[test] + fn completes_case_insensitive_matches() { + let matcher: Box = Box::new(Matcher); + + assert!(matcher.matches("thisshould", "Thisshouldmatch")); + assert!(matcher.matches("--Shouldm", "--shouldmatch")); + assert!(matcher.matches("-Shouldm", "-shouldmatch")); + } + + #[test] + fn should_not_match_when_unequal() { + let matcher: Box = Box::new(Matcher); + + assert!(!matcher.matches("ashouldmatch", "Shouldnotmatch")); + assert!(!matcher.matches("--ashouldnotmatch", "--Shouldnotmatch")); + assert!(!matcher.matches("-ashouldnotmatch", "-Shouldnotmatch")); + } +} diff --git a/crates/nu-cli/src/completion/matchers/case_sensitive.rs b/crates/nu-cli/src/completion/matchers/case_sensitive.rs new file mode 100644 index 000000000..552afba0a --- /dev/null +++ b/crates/nu-cli/src/completion/matchers/case_sensitive.rs @@ -0,0 +1,28 @@ +use crate::completion::matchers; + +pub struct Matcher; + +impl matchers::Matcher for Matcher { + fn matches(&self, partial: &str, from: &str) -> bool { + from.starts_with(partial) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn completes_case_sensitive() { + let matcher: Box = Box::new(Matcher); + + //Should match + assert!(matcher.matches("shouldmatch", "shouldmatch")); + assert!(matcher.matches("shouldm", "shouldmatch")); + assert!(matcher.matches("--also-should-m", "--also-should-match")); + assert!(matcher.matches("-also-should-m", "-also-should-match")); + + // Should not match + assert!(!matcher.matches("--Shouldnot", "--shouldnotmatch")); + } +} diff --git a/crates/nu-cli/src/completion/matchers/mod.rs b/crates/nu-cli/src/completion/matchers/mod.rs new file mode 100644 index 000000000..c14b0d50c --- /dev/null +++ b/crates/nu-cli/src/completion/matchers/mod.rs @@ -0,0 +1,6 @@ +pub(crate) mod case_insensitive; +pub(crate) mod case_sensitive; + +pub trait Matcher { + fn matches(&self, partial: &str, from: &str) -> bool; +} diff --git a/crates/nu-cli/src/completion/mod.rs b/crates/nu-cli/src/completion/mod.rs index 6b0131ac5..2836f9018 100644 --- a/crates/nu-cli/src/completion/mod.rs +++ b/crates/nu-cli/src/completion/mod.rs @@ -1,9 +1,11 @@ pub(crate) mod command; pub(crate) mod engine; pub(crate) mod flag; +pub(crate) mod matchers; pub(crate) mod path; use crate::context; +use matchers::Matcher; #[derive(Debug, Eq, PartialEq)] pub struct Suggestion { @@ -26,5 +28,5 @@ impl<'a> AsRef for Context<'a> { } pub trait Completer { - fn complete(&self, ctx: &Context<'_>, partial: &str) -> Vec; + fn complete(&self, ctx: &Context<'_>, partial: &str, matcher: &dyn Matcher) -> Vec; } diff --git a/crates/nu-cli/src/completion/path.rs b/crates/nu-cli/src/completion/path.rs index ab58151df..c7c47d77e 100644 --- a/crates/nu-cli/src/completion/path.rs +++ b/crates/nu-cli/src/completion/path.rs @@ -1,5 +1,6 @@ use std::path::PathBuf; +use super::matchers::Matcher; use crate::completion::{Completer, Context, Suggestion}; const SEP: char = std::path::MAIN_SEPARATOR; @@ -12,7 +13,7 @@ pub struct PathSuggestion { } impl PathCompleter { - pub fn path_suggestions(&self, partial: &str) -> Vec { + pub fn path_suggestions(&self, partial: &str, matcher: &dyn Matcher) -> Vec { let expanded = nu_parser::expand_ndots(partial); let expanded = expanded.as_ref(); @@ -46,7 +47,7 @@ impl PathCompleter { .filter_map(|entry| { entry.ok().and_then(|entry| { let mut file_name = entry.file_name().to_string_lossy().into_owned(); - if file_name.starts_with(partial) { + if matcher.matches(partial, file_name.as_str()) { let mut path = format!("{}{}", base_dir_name, file_name); if entry.file_type().map(|ft| ft.is_dir()).unwrap_or(false) { path.push(std::path::MAIN_SEPARATOR); @@ -73,8 +74,13 @@ impl PathCompleter { } impl Completer for PathCompleter { - fn complete(&self, _ctx: &Context<'_>, partial: &str) -> Vec { - self.path_suggestions(partial) + fn complete( + &self, + _ctx: &Context<'_>, + partial: &str, + matcher: &dyn Matcher, + ) -> Vec { + self.path_suggestions(partial, matcher) .into_iter() .map(|ps| ps.suggestion) .collect() diff --git a/crates/nu-cli/src/shell/completer.rs b/crates/nu-cli/src/shell/completer.rs index 58bc3686d..95fe6f369 100644 --- a/crates/nu-cli/src/shell/completer.rs +++ b/crates/nu-cli/src/shell/completer.rs @@ -1,9 +1,14 @@ use crate::completion::command::CommandCompleter; use crate::completion::flag::FlagCompleter; +use crate::completion::matchers; +use crate::completion::matchers::Matcher; use crate::completion::path::{PathCompleter, PathSuggestion}; use crate::completion::{self, Completer, Suggestion}; use crate::context; +use nu_source::Tag; + use std::borrow::Cow; + pub(crate) struct NuCompleter {} impl NuCompleter {} @@ -28,6 +33,22 @@ impl NuCompleter { .map(|block| completion::engine::completion_location(line, &block.block, pos)) .unwrap_or_default(); + let matcher = nu_data::config::config(Tag::unknown()) + .ok() + .and_then(|cfg| cfg.get("line_editor").cloned()) + .and_then(|le| { + le.row_entries() + .find(|(idx, _value)| idx.as_str() == "completion_match_method") + .and_then(|(_idx, value)| value.as_string().ok()) + }) + .unwrap_or_else(String::new); + + let matcher = matcher.as_str(); + let matcher: &dyn Matcher = match matcher { + "case-insensitive" => &matchers::case_insensitive::Matcher, + _ => &matchers::case_sensitive::Matcher, + }; + if locations.is_empty() { (pos, Vec::new()) } else { @@ -39,12 +60,12 @@ impl NuCompleter { match location.item { LocationType::Command => { let command_completer = CommandCompleter; - command_completer.complete(context, partial) + command_completer.complete(context, partial, matcher.to_owned()) } LocationType::Flag(cmd) => { let flag_completer = FlagCompleter { cmd }; - flag_completer.complete(context, partial) + flag_completer.complete(context, partial, matcher.to_owned()) } LocationType::Argument(cmd, _arg_name) => { @@ -73,7 +94,7 @@ impl NuCompleter { partial }; - let completed_paths = path_completer.path_suggestions(partial); + let completed_paths = path_completer.path_suggestions(partial, matcher); match cmd.as_deref().unwrap_or("") { "cd" => select_directory_suggestions(completed_paths), _ => completed_paths,