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 indexmap::set::IndexSet;
|
||||||
|
|
||||||
|
use super::matchers::Matcher;
|
||||||
use crate::completion::{Completer, Context, Suggestion};
|
use crate::completion::{Completer, Context, Suggestion};
|
||||||
use crate::context;
|
use crate::context;
|
||||||
|
|
||||||
pub struct CommandCompleter;
|
pub struct CommandCompleter;
|
||||||
|
|
||||||
impl Completer for 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 context: &context::Context = ctx.as_ref();
|
||||||
let mut commands: IndexSet<String> = IndexSet::from_iter(context.registry.names());
|
let mut commands: IndexSet<String> = IndexSet::from_iter(context.registry.names());
|
||||||
|
|
||||||
|
@ -25,7 +26,7 @@ impl Completer for CommandCompleter {
|
||||||
|
|
||||||
let mut suggestions: Vec<_> = commands
|
let mut suggestions: Vec<_> = commands
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter(|v| v.starts_with(partial))
|
.filter(|v| matcher.matches(partial, v))
|
||||||
.map(|v| Suggestion {
|
.map(|v| Suggestion {
|
||||||
replacement: v.clone(),
|
replacement: v.clone(),
|
||||||
display: v,
|
display: v,
|
||||||
|
@ -34,7 +35,7 @@ impl Completer for CommandCompleter {
|
||||||
|
|
||||||
if partial != "" {
|
if partial != "" {
|
||||||
let path_completer = crate::completion::path::PathCompleter;
|
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 iter = path_results.into_iter().filter_map(|path_suggestion| {
|
||||||
let path = path_suggestion.path;
|
let path = path_suggestion.path;
|
||||||
if path.is_dir() || is_executable(&path) {
|
if path.is_dir() || is_executable(&path) {
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
use super::matchers::Matcher;
|
||||||
use crate::completion::{Completer, Context, Suggestion};
|
use crate::completion::{Completer, Context, Suggestion};
|
||||||
use crate::context;
|
use crate::context;
|
||||||
|
|
||||||
|
@ -6,7 +7,7 @@ pub struct FlagCompleter {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Completer for 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();
|
let context: &context::Context = ctx.as_ref();
|
||||||
|
|
||||||
if let Some(cmd) = context.registry.get_command(&self.cmd) {
|
if let Some(cmd) = context.registry.get_command(&self.cmd) {
|
||||||
|
@ -22,7 +23,7 @@ impl Completer for FlagCompleter {
|
||||||
|
|
||||||
suggestions
|
suggestions
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter(|v| v.starts_with(partial))
|
.filter(|v| matcher.matches(partial, v))
|
||||||
.map(|v| Suggestion {
|
.map(|v| Suggestion {
|
||||||
replacement: format!("{} ", v),
|
replacement: format!("{} ", v),
|
||||||
display: 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 command;
|
||||||
pub(crate) mod engine;
|
pub(crate) mod engine;
|
||||||
pub(crate) mod flag;
|
pub(crate) mod flag;
|
||||||
|
pub(crate) mod matchers;
|
||||||
pub(crate) mod path;
|
pub(crate) mod path;
|
||||||
|
|
||||||
use crate::context;
|
use crate::context;
|
||||||
|
use matchers::Matcher;
|
||||||
|
|
||||||
#[derive(Debug, Eq, PartialEq)]
|
#[derive(Debug, Eq, PartialEq)]
|
||||||
pub struct Suggestion {
|
pub struct Suggestion {
|
||||||
|
@ -26,5 +28,5 @@ impl<'a> AsRef<context::Context> for Context<'a> {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait Completer {
|
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 std::path::PathBuf;
|
||||||
|
|
||||||
|
use super::matchers::Matcher;
|
||||||
use crate::completion::{Completer, Context, Suggestion};
|
use crate::completion::{Completer, Context, Suggestion};
|
||||||
|
|
||||||
const SEP: char = std::path::MAIN_SEPARATOR;
|
const SEP: char = std::path::MAIN_SEPARATOR;
|
||||||
|
@ -12,7 +13,7 @@ pub struct PathSuggestion {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PathCompleter {
|
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 = nu_parser::expand_ndots(partial);
|
||||||
let expanded = expanded.as_ref();
|
let expanded = expanded.as_ref();
|
||||||
|
|
||||||
|
@ -46,7 +47,7 @@ impl PathCompleter {
|
||||||
.filter_map(|entry| {
|
.filter_map(|entry| {
|
||||||
entry.ok().and_then(|entry| {
|
entry.ok().and_then(|entry| {
|
||||||
let mut file_name = entry.file_name().to_string_lossy().into_owned();
|
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);
|
let mut path = format!("{}{}", base_dir_name, file_name);
|
||||||
if entry.file_type().map(|ft| ft.is_dir()).unwrap_or(false) {
|
if entry.file_type().map(|ft| ft.is_dir()).unwrap_or(false) {
|
||||||
path.push(std::path::MAIN_SEPARATOR);
|
path.push(std::path::MAIN_SEPARATOR);
|
||||||
|
@ -73,8 +74,13 @@ impl PathCompleter {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Completer for PathCompleter {
|
impl Completer for PathCompleter {
|
||||||
fn complete(&self, _ctx: &Context<'_>, partial: &str) -> Vec<Suggestion> {
|
fn complete(
|
||||||
self.path_suggestions(partial)
|
&self,
|
||||||
|
_ctx: &Context<'_>,
|
||||||
|
partial: &str,
|
||||||
|
matcher: &dyn Matcher,
|
||||||
|
) -> Vec<Suggestion> {
|
||||||
|
self.path_suggestions(partial, matcher)
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|ps| ps.suggestion)
|
.map(|ps| ps.suggestion)
|
||||||
.collect()
|
.collect()
|
||||||
|
|
|
@ -1,9 +1,14 @@
|
||||||
use crate::completion::command::CommandCompleter;
|
use crate::completion::command::CommandCompleter;
|
||||||
use crate::completion::flag::FlagCompleter;
|
use crate::completion::flag::FlagCompleter;
|
||||||
|
use crate::completion::matchers;
|
||||||
|
use crate::completion::matchers::Matcher;
|
||||||
use crate::completion::path::{PathCompleter, PathSuggestion};
|
use crate::completion::path::{PathCompleter, PathSuggestion};
|
||||||
use crate::completion::{self, Completer, Suggestion};
|
use crate::completion::{self, Completer, Suggestion};
|
||||||
use crate::context;
|
use crate::context;
|
||||||
|
use nu_source::Tag;
|
||||||
|
|
||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
|
|
||||||
pub(crate) struct NuCompleter {}
|
pub(crate) struct NuCompleter {}
|
||||||
|
|
||||||
impl NuCompleter {}
|
impl NuCompleter {}
|
||||||
|
@ -28,6 +33,22 @@ impl NuCompleter {
|
||||||
.map(|block| completion::engine::completion_location(line, &block.block, pos))
|
.map(|block| completion::engine::completion_location(line, &block.block, pos))
|
||||||
.unwrap_or_default();
|
.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() {
|
if locations.is_empty() {
|
||||||
(pos, Vec::new())
|
(pos, Vec::new())
|
||||||
} else {
|
} else {
|
||||||
|
@ -39,12 +60,12 @@ impl NuCompleter {
|
||||||
match location.item {
|
match location.item {
|
||||||
LocationType::Command => {
|
LocationType::Command => {
|
||||||
let command_completer = CommandCompleter;
|
let command_completer = CommandCompleter;
|
||||||
command_completer.complete(context, partial)
|
command_completer.complete(context, partial, matcher.to_owned())
|
||||||
}
|
}
|
||||||
|
|
||||||
LocationType::Flag(cmd) => {
|
LocationType::Flag(cmd) => {
|
||||||
let flag_completer = FlagCompleter { cmd };
|
let flag_completer = FlagCompleter { cmd };
|
||||||
flag_completer.complete(context, partial)
|
flag_completer.complete(context, partial, matcher.to_owned())
|
||||||
}
|
}
|
||||||
|
|
||||||
LocationType::Argument(cmd, _arg_name) => {
|
LocationType::Argument(cmd, _arg_name) => {
|
||||||
|
@ -73,7 +94,7 @@ impl NuCompleter {
|
||||||
partial
|
partial
|
||||||
};
|
};
|
||||||
|
|
||||||
let completed_paths = path_completer.path_suggestions(partial);
|
let completed_paths = path_completer.path_suggestions(partial, matcher);
|
||||||
match cmd.as_deref().unwrap_or("") {
|
match cmd.as_deref().unwrap_or("") {
|
||||||
"cd" => select_directory_suggestions(completed_paths),
|
"cd" => select_directory_suggestions(completed_paths),
|
||||||
_ => completed_paths,
|
_ => completed_paths,
|
||||||
|
|
Loading…
Reference in New Issue
Block a user