From e36649f74b37ee183a2b2999b5b3ee4762cb921a Mon Sep 17 00:00:00 2001 From: JT <547158+jntrnr@users.noreply.github.com> Date: Tue, 3 May 2022 12:37:38 +1200 Subject: [PATCH] Update path completions to handle spaces (#5419) --- .../src/completions/completion_options.rs | 3 + .../src/completions/directory_completions.rs | 90 +++++++++++++++---- .../src/completions/file_completions.rs | 10 +-- crates/nu-cli/src/completions/mod.rs | 4 +- crates/nu-parser/src/lib.rs | 2 +- crates/nu-parser/src/parser.rs | 11 +++ 6 files changed, 94 insertions(+), 26 deletions(-) diff --git a/crates/nu-cli/src/completions/completion_options.rs b/crates/nu-cli/src/completions/completion_options.rs index 8e5c08b52e..e5e506c91e 100644 --- a/crates/nu-cli/src/completions/completion_options.rs +++ b/crates/nu-cli/src/completions/completion_options.rs @@ -1,6 +1,7 @@ use std::fmt::Display; use fuzzy_matcher::{skim::SkimMatcherV2, FuzzyMatcher}; +use nu_parser::trim_quotes_str; #[derive(Copy, Clone)] pub enum SortBy { @@ -28,6 +29,8 @@ pub enum MatchAlgorithm { impl MatchAlgorithm { /// Returns whether the `needle` search text matches the given `haystack`. pub fn matches_str(&self, haystack: &str, needle: &str) -> bool { + let haystack = trim_quotes_str(haystack); + let needle = trim_quotes_str(needle); match *self { MatchAlgorithm::Prefix => haystack.starts_with(needle), MatchAlgorithm::Fuzzy => { diff --git a/crates/nu-cli/src/completions/directory_completions.rs b/crates/nu-cli/src/completions/directory_completions.rs index 0156132d07..4efc2dd955 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, CompletionOptions}; +use crate::completions::{matches, Completer, CompletionOptions}; use nu_protocol::{ engine::{EngineState, StateWorkingSet}, levenshtein_distance, Span, @@ -7,6 +7,8 @@ use reedline::Suggestion; use std::path::Path; use std::sync::Arc; +use super::{partial_from, prepend_base_dir, MatchAlgorithm}; + const SEP: char = std::path::MAIN_SEPARATOR; #[derive(Clone)] @@ -41,23 +43,17 @@ impl Completer for DirectoryCompletion { let partial = String::from_utf8_lossy(&prefix).to_string(); // Filter only the folders - let output: Vec<_> = file_path_completion(span, &partial, &cwd, options.match_algorithm) + let output: Vec<_> = directory_completion(span, &partial, &cwd, options.match_algorithm) .into_iter() - .filter_map(move |x| { - if x.1.ends_with(SEP) { - return Some(Suggestion { - value: x.1, - description: None, - extra: None, - span: reedline::Span { - start: x.0.start - offset, - end: x.0.end - offset, - }, - append_whitespace: false, - }); - } - - None + .map(move |x| Suggestion { + value: x.1, + description: None, + extra: None, + span: reedline::Span { + start: x.0.start - offset, + end: x.0.end - offset, + }, + append_whitespace: false, }) .collect(); @@ -101,3 +97,63 @@ impl Completer for DirectoryCompletion { non_hidden } } + +pub fn directory_completion( + span: nu_protocol::Span, + partial: &str, + cwd: &str, + match_algorithm: MatchAlgorithm, +) -> Vec<(nu_protocol::Span, String)> { + let original_input = partial; + + let (base_dir_name, partial) = partial_from(partial); + + let base_dir = nu_path::expand_path_with(&base_dir_name, cwd); + + // This check is here as base_dir.read_dir() with base_dir == "" will open the current dir + // which we don't want in this case (if we did, base_dir would already be ".") + if base_dir == Path::new("") { + return Vec::new(); + } + + if let Ok(result) = base_dir.read_dir() { + return result + .filter_map(|entry| { + entry.ok().and_then(|entry| { + if let Ok(metadata) = entry.metadata() { + if metadata.is_dir() { + let mut file_name = entry.file_name().to_string_lossy().into_owned(); + if matches(&partial, &file_name, match_algorithm) { + let mut path = if prepend_base_dir(original_input, &base_dir_name) { + format!("{}{}", base_dir_name, file_name) + } else { + file_name.to_string() + }; + + if entry.path().is_dir() { + path.push(SEP); + file_name.push(SEP); + } + + // Fix files or folders with quotes + if path.contains('\'') || path.contains('"') || path.contains(' ') { + path = format!("`{}`", path); + } + + Some((span, path)) + } else { + None + } + } else { + None + } + } else { + None + } + }) + }) + .collect(); + } + + Vec::new() +} diff --git a/crates/nu-cli/src/completions/file_completions.rs b/crates/nu-cli/src/completions/file_completions.rs index 5de5db30fe..a24139a894 100644 --- a/crates/nu-cli/src/completions/file_completions.rs +++ b/crates/nu-cli/src/completions/file_completions.rs @@ -95,7 +95,7 @@ impl Completer for FileCompletion { } pub fn partial_from(input: &str) -> (String, String) { - let partial = input.replace('\'', ""); + let partial = input.replace('`', ""); // If partial is only a word we want to search in the current dir let (base, rest) = partial.rsplit_once(is_separator).unwrap_or((".", &partial)); @@ -141,12 +141,8 @@ pub fn file_path_completion( file_name.push(SEP); } - if path.contains(' ') { - path = format!("\'{}\'", path); - } - // Fix files or folders with quotes - if path.contains('\'') || path.contains('"') { + if path.contains('\'') || path.contains('"') || path.contains(' ') { path = format!("`{}`", path); } @@ -167,7 +163,7 @@ pub fn matches(partial: &str, from: &str, match_algorithm: MatchAlgorithm) -> bo } /// Returns whether the base_dir should be prepended to the file path -fn prepend_base_dir(input: &str, base_dir: &str) -> bool { +pub fn prepend_base_dir(input: &str, base_dir: &str) -> bool { if base_dir == format!(".{}", SEP) { // if the current base_dir path is the local folder we only add a "./" prefix if the user // input already includes a local folder prefix. diff --git a/crates/nu-cli/src/completions/mod.rs b/crates/nu-cli/src/completions/mod.rs index 9e7a1d8379..a710aa3d74 100644 --- a/crates/nu-cli/src/completions/mod.rs +++ b/crates/nu-cli/src/completions/mod.rs @@ -16,6 +16,8 @@ pub use completion_options::{CompletionOptions, MatchAlgorithm, SortBy}; pub use custom_completions::CustomCompletion; pub use directory_completions::DirectoryCompletion; pub use dotnu_completions::DotNuCompletion; -pub use file_completions::{file_path_completion, partial_from, FileCompletion}; +pub use file_completions::{ + file_path_completion, matches, partial_from, prepend_base_dir, FileCompletion, +}; pub use flag_completions::FlagCompletion; pub use variable_completions::VariableCompletion; diff --git a/crates/nu-parser/src/lib.rs b/crates/nu-parser/src/lib.rs index ae9cb8f07e..c9997cf0ce 100644 --- a/crates/nu-parser/src/lib.rs +++ b/crates/nu-parser/src/lib.rs @@ -18,7 +18,7 @@ pub use parse_keywords::*; pub use parser::{ is_math_expression_like, parse, parse_block, parse_duration_bytes, parse_external_call, - trim_quotes, unescape_unquote_string, Import, + trim_quotes, trim_quotes_str, unescape_unquote_string, Import, }; #[cfg(feature = "plugin")] diff --git a/crates/nu-parser/src/parser.rs b/crates/nu-parser/src/parser.rs index 9708a2f516..57b47cc50a 100644 --- a/crates/nu-parser/src/parser.rs +++ b/crates/nu-parser/src/parser.rs @@ -126,6 +126,17 @@ pub fn trim_quotes(bytes: &[u8]) -> &[u8] { } } +pub fn trim_quotes_str(s: &str) -> &str { + if (s.starts_with('"') && s.ends_with('"') && s.len() > 1) + || (s.starts_with('\'') && s.ends_with('\'') && s.len() > 1) + || (s.starts_with('`') && s.ends_with('`') && s.len() > 1) + { + &s[1..(s.len() - 1)] + } else { + s + } +} + pub fn check_call(command: Span, sig: &Signature, call: &Call) -> Option { // Allow the call to pass if they pass in the help flag if call.named_iter().any(|(n, _, _)| n.item == "help") {