forked from extern/nushell
implementing case-sensitive & case-insensitive completion matching (#2556)
Co-authored-by: Jonathan Turner <jonathandturner@users.noreply.github.com>
This commit is contained in:
parent
8453261211
commit
df2845a9b4
@ -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) {
|
||||
|
@ -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,
|
||||
|
45
crates/nu-cli/src/completion/matchers/case_insensitive.rs
Normal file
45
crates/nu-cli/src/completion/matchers/case_insensitive.rs
Normal 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"));
|
||||
}
|
||||
}
|
28
crates/nu-cli/src/completion/matchers/case_sensitive.rs
Normal file
28
crates/nu-cli/src/completion/matchers/case_sensitive.rs
Normal 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"));
|
||||
}
|
||||
}
|
6
crates/nu-cli/src/completion/matchers/mod.rs
Normal file
6
crates/nu-cli/src/completion/matchers/mod.rs
Normal 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;
|
||||
}
|
@ -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>;
|
||||
}
|
||||
|
@ -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()
|
||||
|
@ -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,
|
||||
|
Loading…
Reference in New Issue
Block a user