From a88058006a770678f55c59a13c7bdbb0e3b27e59 Mon Sep 17 00:00:00 2001 From: JT Date: Tue, 5 Oct 2021 08:21:31 +1300 Subject: [PATCH] Add path completions --- Cargo.lock | 1 + crates/nu-cli/Cargo.toml | 2 + crates/nu-cli/src/completions.rs | 76 ++++++++++++++++++++++++++ crates/nu-cli/src/syntax_highlight.rs | 8 +++ crates/nu-command/src/filesystem/cd.rs | 2 +- crates/nu-command/src/filesystem/ls.rs | 9 ++- crates/nu-engine/src/eval.rs | 8 +++ crates/nu-parser/src/flatten.rs | 9 ++- crates/nu-parser/src/parser.rs | 70 ++++++++++++++++++++++-- crates/nu-protocol/src/ast/expr.rs | 2 + crates/nu-protocol/src/syntax_shape.rs | 4 +- crates/nu-protocol/src/ty.rs | 2 - 12 files changed, 182 insertions(+), 11 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index cf1052648a..af84ca6676 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -487,6 +487,7 @@ dependencies = [ "nu-ansi-term", "nu-engine", "nu-parser", + "nu-path", "nu-protocol", "reedline", "thiserror", diff --git a/crates/nu-cli/Cargo.toml b/crates/nu-cli/Cargo.toml index 117fe77d98..5b47189196 100644 --- a/crates/nu-cli/Cargo.toml +++ b/crates/nu-cli/Cargo.toml @@ -5,8 +5,10 @@ edition = "2018" [dependencies] nu-engine = { path = "../nu-engine" } +nu-path = { path = "../nu-path" } nu-parser = { path = "../nu-parser" } nu-protocol = { path = "../nu-protocol" } + miette = { version = "3.0.0", features = ["fancy"] } thiserror = "1.0.29" nu-ansi-term = "0.36.0" diff --git a/crates/nu-cli/src/completions.rs b/crates/nu-cli/src/completions.rs index 2140ca0da9..f0820f0807 100644 --- a/crates/nu-cli/src/completions.rs +++ b/crates/nu-cli/src/completions.rs @@ -8,6 +8,8 @@ use nu_protocol::{ }; use reedline::Completer; +const SEP: char = std::path::MAIN_SEPARATOR; + pub struct NuCompleter { engine_state: Rc>, } @@ -28,6 +30,8 @@ impl Completer for NuCompleter { let flattened = flatten_block(&working_set, &output); + // println!("flattened: {:?}", flattened); + for flat in flattened { if pos >= flat.0.start && pos <= flat.0.end { match &flat.1 { @@ -80,6 +84,25 @@ impl Completer for NuCompleter { }) .collect(); } + nu_parser::FlatShape::Filepath | nu_parser::FlatShape::GlobPattern => { + let prefix = working_set.get_span_contents(flat.0); + let prefix = String::from_utf8_lossy(prefix).to_string(); + + let results = file_path_completion(flat.0, &prefix); + + return results + .into_iter() + .map(move |x| { + ( + reedline::Span { + start: x.0.start - offset, + end: x.0.end - offset, + }, + x.1, + ) + }) + .collect(); + } _ => {} } } @@ -88,3 +111,56 @@ impl Completer for NuCompleter { vec![] } } + +fn file_path_completion( + span: nu_protocol::Span, + partial: &str, +) -> Vec<(nu_protocol::Span, String)> { + use std::path::{is_separator, Path}; + + let (base_dir_name, partial) = { + // 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)); + // On windows, this standardizes paths to use \ + let mut base = base.replace(is_separator, &SEP.to_string()); + + // rsplit_once removes the separator + base.push(SEP); + (base, rest) + }; + + let base_dir = nu_path::expand_path(&base_dir_name); + // 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() { + result + .filter_map(|entry| { + entry.ok().and_then(|entry| { + let mut file_name = entry.file_name().to_string_lossy().into_owned(); + if matches(partial, &file_name) { + let mut path = format!("{}{}", base_dir_name, file_name); + if entry.path().is_dir() { + path.push(SEP); + file_name.push(SEP); + } + + Some((span, path)) + } else { + None + } + }) + }) + .collect() + } else { + Vec::new() + } +} + +fn matches(partial: &str, from: &str) -> bool { + from.to_ascii_lowercase() + .starts_with(&partial.to_ascii_lowercase()) +} diff --git a/crates/nu-cli/src/syntax_highlight.rs b/crates/nu-cli/src/syntax_highlight.rs index 87ec535ed0..10e997054d 100644 --- a/crates/nu-cli/src/syntax_highlight.rs +++ b/crates/nu-cli/src/syntax_highlight.rs @@ -81,6 +81,14 @@ impl Highlighter for NuHighlighter { Style::new().fg(nu_ansi_term::Color::Yellow).bold(), next_token, )), + FlatShape::Filepath => output.push(( + Style::new().fg(nu_ansi_term::Color::Yellow).bold(), + next_token, + )), + FlatShape::GlobPattern => output.push(( + Style::new().fg(nu_ansi_term::Color::Yellow).bold(), + next_token, + )), FlatShape::Variable => output.push(( Style::new().fg(nu_ansi_term::Color::Blue).bold(), next_token, diff --git a/crates/nu-command/src/filesystem/cd.rs b/crates/nu-command/src/filesystem/cd.rs index 94282c75e2..c7a5ce0b80 100644 --- a/crates/nu-command/src/filesystem/cd.rs +++ b/crates/nu-command/src/filesystem/cd.rs @@ -15,7 +15,7 @@ impl Command for Cd { } fn signature(&self) -> nu_protocol::Signature { - Signature::build("cd").optional("path", SyntaxShape::FilePath, "the path to change to") + Signature::build("cd").optional("path", SyntaxShape::Filepath, "the path to change to") } fn run( diff --git a/crates/nu-command/src/filesystem/ls.rs b/crates/nu-command/src/filesystem/ls.rs index 2e243e05f4..77b9f1c602 100644 --- a/crates/nu-command/src/filesystem/ls.rs +++ b/crates/nu-command/src/filesystem/ls.rs @@ -31,7 +31,14 @@ impl Command for Ls { ) -> Result { let pattern = if let Some(expr) = call.positional.get(0) { let result = eval_expression(context, expr)?; - result.as_string()? + let mut result = result.as_string()?; + + let path = std::path::Path::new(&result); + if path.is_dir() { + result.push('*'); + } + + result } else { "*".into() }; diff --git a/crates/nu-engine/src/eval.rs b/crates/nu-engine/src/eval.rs index 04057e540e..5882ca2445 100644 --- a/crates/nu-engine/src/eval.rs +++ b/crates/nu-engine/src/eval.rs @@ -240,6 +240,14 @@ pub fn eval_expression( val: s.clone(), span: expr.span, }), + Expr::Filepath(s) => Ok(Value::String { + val: s.clone(), + span: expr.span, + }), + Expr::GlobPattern(s) => Ok(Value::String { + val: s.clone(), + span: expr.span, + }), Expr::Signature(_) => Ok(Value::Nothing { span: expr.span }), Expr::Garbage => Ok(Value::Nothing { span: expr.span }), } diff --git a/crates/nu-parser/src/flatten.rs b/crates/nu-parser/src/flatten.rs index fffb5626ec..c2d3ade9a4 100644 --- a/crates/nu-parser/src/flatten.rs +++ b/crates/nu-parser/src/flatten.rs @@ -15,6 +15,8 @@ pub enum FlatShape { Operator, Signature, String, + Filepath, + GlobPattern, Variable, Custom(String), } @@ -118,7 +120,12 @@ pub fn flatten_expression( Expr::Bool(_) => { vec![(expr.span, FlatShape::Bool)] } - + Expr::Filepath(_) => { + vec![(expr.span, FlatShape::Filepath)] + } + Expr::GlobPattern(_) => { + vec![(expr.span, FlatShape::GlobPattern)] + } Expr::List(list) => { let mut output = vec![]; for l in list { diff --git a/crates/nu-parser/src/parser.rs b/crates/nu-parser/src/parser.rs index fa9703a73f..925cc76bb9 100644 --- a/crates/nu-parser/src/parser.rs +++ b/crates/nu-parser/src/parser.rs @@ -1320,6 +1320,68 @@ pub fn parse_full_cell_path( } } +pub fn parse_filepath( + working_set: &mut StateWorkingSet, + span: Span, +) -> (Expression, Option) { + let bytes = working_set.get_span_contents(span); + let bytes = if (bytes.starts_with(b"\"") && bytes.ends_with(b"\"") && bytes.len() > 1) + || (bytes.starts_with(b"\'") && bytes.ends_with(b"\'") && bytes.len() > 1) + { + &bytes[1..(bytes.len() - 1)] + } else { + bytes + }; + + if let Ok(token) = String::from_utf8(bytes.into()) { + ( + Expression { + expr: Expr::Filepath(token), + span, + ty: Type::String, + custom_completion: None, + }, + None, + ) + } else { + ( + garbage(span), + Some(ParseError::Expected("string".into(), span)), + ) + } +} + +pub fn parse_glob_pattern( + working_set: &mut StateWorkingSet, + span: Span, +) -> (Expression, Option) { + let bytes = working_set.get_span_contents(span); + let bytes = if (bytes.starts_with(b"\"") && bytes.ends_with(b"\"") && bytes.len() > 1) + || (bytes.starts_with(b"\'") && bytes.ends_with(b"\'") && bytes.len() > 1) + { + &bytes[1..(bytes.len() - 1)] + } else { + bytes + }; + + if let Ok(token) = String::from_utf8(bytes.into()) { + ( + Expression { + expr: Expr::GlobPattern(token), + span, + ty: Type::String, + custom_completion: None, + }, + None, + ) + } else { + ( + garbage(span), + Some(ParseError::Expected("string".into(), span)), + ) + } +} + pub fn parse_string( working_set: &mut StateWorkingSet, span: Span, @@ -1364,7 +1426,7 @@ pub fn parse_shape_name( b"number" => SyntaxShape::Number, b"range" => SyntaxShape::Range, b"int" => SyntaxShape::Int, - b"path" => SyntaxShape::FilePath, + b"path" => SyntaxShape::Filepath, b"glob" => SyntaxShape::GlobPattern, b"block" => SyntaxShape::Block(None), //FIXME b"cond" => SyntaxShape::RowCondition, @@ -2320,9 +2382,9 @@ pub fn parse_value( SyntaxShape::Number => parse_number(bytes, span), SyntaxShape::Int => parse_int(bytes, span), SyntaxShape::Range => parse_range(working_set, span), - SyntaxShape::String | SyntaxShape::GlobPattern | SyntaxShape::FilePath => { - parse_string(working_set, span) - } + SyntaxShape::Filepath => parse_filepath(working_set, span), + SyntaxShape::GlobPattern => parse_glob_pattern(working_set, span), + SyntaxShape::String => parse_string(working_set, span), SyntaxShape::Block(_) => { if bytes.starts_with(b"{") { parse_block_expression(working_set, shape, span) diff --git a/crates/nu-protocol/src/ast/expr.rs b/crates/nu-protocol/src/ast/expr.rs index 449f4687df..45955da946 100644 --- a/crates/nu-protocol/src/ast/expr.rs +++ b/crates/nu-protocol/src/ast/expr.rs @@ -23,6 +23,8 @@ pub enum Expr { List(Vec), Table(Vec, Vec>), Keyword(Vec, Span, Box), + Filepath(String), + GlobPattern(String), String(String), // FIXME: improve this in the future? CellPath(CellPath), FullCellPath(Box), diff --git a/crates/nu-protocol/src/syntax_shape.rs b/crates/nu-protocol/src/syntax_shape.rs index 8b752461e7..d1202528ac 100644 --- a/crates/nu-protocol/src/syntax_shape.rs +++ b/crates/nu-protocol/src/syntax_shape.rs @@ -28,7 +28,7 @@ pub enum SyntaxShape { Int, /// A filepath is allowed - FilePath, + Filepath, /// A glob pattern is allowed, eg `foo*` GlobPattern, @@ -86,7 +86,7 @@ impl SyntaxShape { SyntaxShape::Custom(custom, _) => custom.to_type(), SyntaxShape::Duration => Type::Duration, SyntaxShape::Expression => Type::Unknown, - SyntaxShape::FilePath => Type::String, + SyntaxShape::Filepath => Type::String, SyntaxShape::Filesize => Type::Filesize, SyntaxShape::FullCellPath => Type::Unknown, SyntaxShape::GlobPattern => Type::String, diff --git a/crates/nu-protocol/src/ty.rs b/crates/nu-protocol/src/ty.rs index c11bafaca8..e3015b7c05 100644 --- a/crates/nu-protocol/src/ty.rs +++ b/crates/nu-protocol/src/ty.rs @@ -12,7 +12,6 @@ pub enum Type { Block, CellPath, Duration, - FilePath, Filesize, List(Box), Number, @@ -32,7 +31,6 @@ impl Display for Type { Type::Bool => write!(f, "bool"), Type::CellPath => write!(f, "cell path"), Type::Duration => write!(f, "duration"), - Type::FilePath => write!(f, "filepath"), Type::Filesize => write!(f, "filesize"), Type::Float => write!(f, "float"), Type::Int => write!(f, "int"),