From 9ec2aca86f3c33685370866ec4ce73569b72a638 Mon Sep 17 00:00:00 2001 From: Kevin Del Castillo Date: Wed, 22 Apr 2020 23:17:38 -0500 Subject: [PATCH] Support completion for paths with multiple dots (#1640) * refactor: expand_path and expand_ndots now work for any string. * refactor: refactor test and add new ones. * refactor: convert expanded to owned string * feat: pub export of expand_ndots * feat: add completion for ndots in fs-shell --- crates/nu-cli/src/shell/filesystem_shell.rs | 23 ++++- crates/nu-parser/src/lib.rs | 1 + crates/nu-parser/src/parse.rs | 6 +- crates/nu-parser/src/path.rs | 105 ++++++++++++++------ 4 files changed, 100 insertions(+), 35 deletions(-) diff --git a/crates/nu-cli/src/shell/filesystem_shell.rs b/crates/nu-cli/src/shell/filesystem_shell.rs index 39b94e546..6c4eef497 100644 --- a/crates/nu-cli/src/shell/filesystem_shell.rs +++ b/crates/nu-cli/src/shell/filesystem_shell.rs @@ -11,6 +11,7 @@ use crate::shell::completer::NuCompleter; use crate::shell::shell::Shell; use crate::utils::FileStructure; use nu_errors::ShellError; +use nu_parser::expand_ndots; use nu_protocol::{Primitive, ReturnSuccess, UntaggedValue}; use rustyline::completion::FilenameCompleter; use rustyline::hint::{Hinter, HistoryHinter}; @@ -994,7 +995,27 @@ impl Shell for FilesystemShell { pos: usize, ctx: &rustyline::Context<'_>, ) -> Result<(usize, Vec), rustyline::error::ReadlineError> { - self.completer.complete(line, pos, ctx) + let expanded = expand_ndots(&line); + + // Find the first not-matching char position, if there is one + let differ_pos = line + .chars() + .zip(expanded.chars()) + .enumerate() + .find(|(_index, (a, b))| a != b) + .map(|(differ_pos, _)| differ_pos); + + let pos = if let Some(differ_pos) = differ_pos { + if differ_pos < pos { + pos + (expanded.len() - line.len()) + } else { + pos + } + } else { + pos + }; + + self.completer.complete(&expanded, pos, ctx) } fn hint(&self, line: &str, pos: usize, ctx: &rustyline::Context<'_>) -> Option { diff --git a/crates/nu-parser/src/lib.rs b/crates/nu-parser/src/lib.rs index 8b789f338..2a0def3ca 100644 --- a/crates/nu-parser/src/lib.rs +++ b/crates/nu-parser/src/lib.rs @@ -8,5 +8,6 @@ mod signature; pub use crate::files::Files; pub use crate::lite_parse::{lite_parse, LiteBlock}; pub use crate::parse::{classify_block, garbage}; +pub use crate::path::expand_ndots; pub use crate::shapes::shapes; pub use crate::signature::{Signature, SignatureRegistry}; diff --git a/crates/nu-parser/src/parse.rs b/crates/nu-parser/src/parse.rs index 3b0c5e2c1..9f145c6c4 100644 --- a/crates/nu-parser/src/parse.rs +++ b/crates/nu-parser/src/parse.rs @@ -366,7 +366,7 @@ fn parse_arg( } SyntaxShape::Pattern => { let trimmed = trim_quotes(&lite_arg.item); - let expanded = expand_path(&trimmed); + let expanded = expand_path(&trimmed).to_string(); ( SpannedExpression::new(Expression::pattern(expanded), lite_arg.span), None, @@ -378,7 +378,7 @@ fn parse_arg( SyntaxShape::Unit => parse_unit(&lite_arg), SyntaxShape::Path => { let trimmed = trim_quotes(&lite_arg.item); - let expanded = expand_path(&trimmed); + let expanded = expand_path(&trimmed).to_string(); let path = Path::new(&expanded); ( SpannedExpression::new(Expression::FilePath(path.to_path_buf()), lite_arg.span), @@ -1068,7 +1068,7 @@ fn classify_pipeline( } else { let name = lite_cmd.name.clone().map(|v| { let trimmed = trim_quotes(&v); - expand_path(&trimmed) + expand_path(&trimmed).to_string() }); let mut args = vec![]; diff --git a/crates/nu-parser/src/path.rs b/crates/nu-parser/src/path.rs index 2aaccb3d2..7c018d9fb 100644 --- a/crates/nu-parser/src/path.rs +++ b/crates/nu-parser/src/path.rs @@ -1,35 +1,69 @@ -use std::path::{Component, Path, PathBuf}; +use std::borrow::Cow; -fn expand_ndots(path: &str) -> String { - let path = Path::new(path); - let mut expanded = PathBuf::new(); +fn handle_dots_push(string: &mut String, count: u8) { + if count < 1 { + return; + } - for component in path.components() { - match component { - Component::Normal(normal) => { - if let Some(normal) = normal.to_str() { - if normal.chars().all(|c| c == '.') { - for _ in 0..(normal.len() - 1) { - expanded.push(".."); - } - } else { - expanded.push(normal); - } - } else { - expanded.push(normal); + if count == 1 { + string.push('.'); + return; + } + + for _ in 0..(count - 1) { + string.push_str("../"); + } + + string.pop(); // remove last '/' +} + +pub fn expand_ndots(path: &str) -> Cow<'_, str> { + let mut dots_count = 0u8; + let ndots_present = { + for chr in path.chars() { + if chr == '.' { + dots_count += 1; + } else { + if dots_count > 2 { + break; } - } - c => expanded.push(c.as_os_str()), + dots_count = 0; + } + } + + dots_count > 2 + }; + + if !ndots_present { + return path.into(); + } + + let mut dots_count = 0u8; + let mut expanded = String::new(); + for chr in path.chars() { + if chr != '.' { + handle_dots_push(&mut expanded, dots_count); + dots_count = 0; + expanded.push(chr); + } else { + dots_count += 1; } } - expanded.to_string_lossy().to_string() + handle_dots_push(&mut expanded, dots_count); + + expanded.into() } -pub fn expand_path(path: &str) -> String { - let tilde_expansion = shellexpand::tilde(path); - expand_ndots(&tilde_expansion) +pub fn expand_path<'a>(path: &'a str) -> Cow<'a, str> { + let tilde_expansion: Cow<'a, str> = shellexpand::tilde(path); + let ndots_expansion: Cow<'a, str> = match tilde_expansion { + Cow::Borrowed(b) => expand_ndots(b), + Cow::Owned(o) => expand_ndots(&o).to_string().into(), + }; + + ndots_expansion } #[cfg(test)] @@ -37,16 +71,25 @@ mod tests { use super::*; #[test] - fn expand_in_relative_path() { - let expected = Path::new("../.."); - let expanded = PathBuf::from(expand_path("...")); - assert_eq!(expected, &expanded); + fn string_without_ndots() { + assert_eq!("../hola", &expand_ndots("../hola").to_string()); } #[test] - fn expand_in_absolute_path() { - let expected = Path::new("/foo/../.."); - let expanded = PathBuf::from(expand_path("/foo/...")); - assert_eq!(expected, &expanded); + fn string_with_three_ndots() { + assert_eq!("../..", &expand_ndots("...").to_string()); + } + + #[test] + fn string_with_three_ndots_and_final_slash() { + assert_eq!("../../", &expand_ndots(".../").to_string()); + } + + #[test] + fn string_with_three_ndots_and_garbage() { + assert_eq!( + "ls ../../ garbage.*[", + &expand_ndots("ls .../ garbage.*[").to_string(), + ); } }