implementing case-sensitive & case-insensitive completion matching (#2556)

Co-authored-by: Jonathan Turner <jonathandturner@users.noreply.github.com>
This commit is contained in:
rezural 2020-09-18 00:52:58 +10:00 committed by GitHub
parent 8453261211
commit df2845a9b4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 123 additions and 13 deletions

View File

@ -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<Suggestion> {
fn complete(&self, ctx: &Context<'_>, partial: &str, matcher: &dyn Matcher) -> Vec<Suggestion> {
let context: &context::Context = ctx.as_ref();
let mut commands: IndexSet<String> = 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) {

View File

@ -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<Suggestion> {
fn complete(&self, ctx: &Context<'_>, partial: &str, matcher: &dyn Matcher) -> Vec<Suggestion> {
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,

View File

@ -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<dyn matchers::Matcher> = 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<dyn matchers::Matcher> = 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<dyn matchers::Matcher> = Box::new(Matcher);
assert!(!matcher.matches("ashouldmatch", "Shouldnotmatch"));
assert!(!matcher.matches("--ashouldnotmatch", "--Shouldnotmatch"));
assert!(!matcher.matches("-ashouldnotmatch", "-Shouldnotmatch"));
}
}

View File

@ -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<dyn matchers::Matcher> = 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"));
}
}

View File

@ -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;
}

View File

@ -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<context::Context> for Context<'a> {
}
pub trait Completer {
fn complete(&self, ctx: &Context<'_>, partial: &str) -> Vec<Suggestion>;
fn complete(&self, ctx: &Context<'_>, partial: &str, matcher: &dyn Matcher) -> Vec<Suggestion>;
}

View File

@ -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<PathSuggestion> {
pub fn path_suggestions(&self, partial: &str, matcher: &dyn Matcher) -> Vec<PathSuggestion> {
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<Suggestion> {
self.path_suggestions(partial)
fn complete(
&self,
_ctx: &Context<'_>,
partial: &str,
matcher: &dyn Matcher,
) -> Vec<Suggestion> {
self.path_suggestions(partial, matcher)
.into_iter()
.map(|ps| ps.suggestion)
.collect()

View File

@ -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,