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
This commit is contained in:
Kevin Del Castillo 2020-04-22 23:17:38 -05:00 committed by GitHub
parent 818171cc2c
commit 9ec2aca86f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 100 additions and 35 deletions

View File

@ -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::completion::Pair>), 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<String> {

View File

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

View File

@ -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![];

View File

@ -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();
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("..");
fn handle_dots_push(string: &mut String, count: u8) {
if count < 1 {
return;
}
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 {
expanded.push(normal);
if dots_count > 2 {
break;
}
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 {
expanded.push(normal);
dots_count += 1;
}
}
c => expanded.push(c.as_os_str()),
}
handle_dots_push(&mut expanded, dots_count);
expanded.into()
}
expanded.to_string_lossy().to_string()
}
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(),
};
pub fn expand_path(path: &str) -> String {
let tilde_expansion = shellexpand::tilde(path);
expand_ndots(&tilde_expansion)
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(),
);
}
}