diff --git a/crates/nu-cli/src/completions/completer.rs b/crates/nu-cli/src/completions/completer.rs index 0309e08ab1..0b67eba93f 100644 --- a/crates/nu-cli/src/completions/completer.rs +++ b/crates/nu-cli/src/completions/completer.rs @@ -1,6 +1,6 @@ use crate::completions::{ - CommandCompletion, Completer, CustomCompletion, DotNuCompletion, FileCompletion, - FlagCompletion, VariableCompletion, + CommandCompletion, Completer, CustomCompletion, DirectoryCompletion, DotNuCompletion, + FileCompletion, FlagCompletion, VariableCompletion, }; use nu_parser::{flatten_expression, parse, FlatShape}; use nu_protocol::{ @@ -153,6 +153,19 @@ impl NuCompleter { pos, ); } + FlatShape::Directory => { + let mut completer = + DirectoryCompletion::new(self.engine_state.clone()); + + return self.process_completion( + &mut completer, + &working_set, + prefix, + new_span, + offset, + pos, + ); + } FlatShape::Filepath | FlatShape::GlobPattern => { let mut completer = FileCompletion::new(self.engine_state.clone()); diff --git a/crates/nu-cli/src/completions/directory_completions.rs b/crates/nu-cli/src/completions/directory_completions.rs new file mode 100644 index 0000000000..073e375dfd --- /dev/null +++ b/crates/nu-cli/src/completions/directory_completions.rs @@ -0,0 +1,101 @@ +use crate::completions::{file_path_completion, Completer}; +use nu_protocol::{ + engine::{EngineState, StateWorkingSet}, + levenshtein_distance, Span, +}; +use reedline::Suggestion; +use std::path::Path; +use std::sync::Arc; + +const SEP: char = std::path::MAIN_SEPARATOR; + +#[derive(Clone)] +pub struct DirectoryCompletion { + engine_state: Arc, +} + +impl DirectoryCompletion { + pub fn new(engine_state: Arc) -> Self { + Self { engine_state } + } +} + +impl Completer for DirectoryCompletion { + fn fetch( + &mut self, + _: &StateWorkingSet, + prefix: Vec, + span: Span, + offset: usize, + _: usize, + ) -> Vec { + let cwd = if let Some(d) = self.engine_state.env_vars.get("PWD") { + match d.as_string() { + Ok(s) => s, + Err(_) => "".to_string(), + } + } else { + "".to_string() + }; + let prefix = String::from_utf8_lossy(&prefix).to_string(); + + // Filter only the folders + let output: Vec<_> = file_path_completion(span, &prefix, &cwd) + .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, + }, + }); + } + + None + }) + .collect(); + + output + } + + // Sort results prioritizing the non hidden folders + fn sort(&self, items: Vec, prefix: Vec) -> Vec { + let prefix_str = String::from_utf8_lossy(&prefix).to_string(); + + // Sort items + let mut sorted_items = items; + sorted_items.sort_by(|a, b| a.value.cmp(&b.value)); + sorted_items.sort_by(|a, b| { + let a_distance = levenshtein_distance(&prefix_str, &a.value); + let b_distance = levenshtein_distance(&prefix_str, &b.value); + a_distance.cmp(&b_distance) + }); + + // Separate the results between hidden and non hidden + let mut hidden: Vec = vec![]; + let mut non_hidden: Vec = vec![]; + + for item in sorted_items.into_iter() { + let item_path = Path::new(&item.value); + + if let Some(value) = item_path.file_name() { + if let Some(value) = value.to_str() { + if value.starts_with('.') { + hidden.push(item); + } else { + non_hidden.push(item); + } + } + } + } + + // Append the hidden folders to the non hidden vec to avoid creating a new vec + non_hidden.append(&mut hidden); + + non_hidden + } +} diff --git a/crates/nu-cli/src/completions/mod.rs b/crates/nu-cli/src/completions/mod.rs index 544276fe70..c08c0c427c 100644 --- a/crates/nu-cli/src/completions/mod.rs +++ b/crates/nu-cli/src/completions/mod.rs @@ -3,6 +3,7 @@ mod command_completions; mod completer; mod completion_options; mod custom_completions; +mod directory_completions; mod dotnu_completions; mod file_completions; mod flag_completions; @@ -13,6 +14,7 @@ pub use command_completions::CommandCompletion; pub use completer::NuCompleter; pub use completion_options::{CompletionOptions, 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 flag_completions::FlagCompletion; diff --git a/crates/nu-cli/src/syntax_highlight.rs b/crates/nu-cli/src/syntax_highlight.rs index 0416a76601..cd9612f194 100644 --- a/crates/nu-cli/src/syntax_highlight.rs +++ b/crates/nu-cli/src/syntax_highlight.rs @@ -178,6 +178,11 @@ impl Highlighter for NuHighlighter { get_shape_color(shape.1.to_string(), &self.config), next_token, )), + FlatShape::Directory => output.push(( + // nushell Directory + get_shape_color(shape.1.to_string(), &self.config), + next_token, + )), FlatShape::GlobPattern => output.push(( // nushell GlobPattern get_shape_color(shape.1.to_string(), &self.config), diff --git a/crates/nu-cli/tests/test_completions.rs b/crates/nu-cli/tests/test_completions.rs index 243f4daec4..7ea7b1c93a 100644 --- a/crates/nu-cli/tests/test_completions.rs +++ b/crates/nu-cli/tests/test_completions.rs @@ -18,7 +18,7 @@ fn file_completions() { let mut completer = NuCompleter::new(std::sync::Arc::new(engine), stack); // Test completions for the current folder - let target_dir = format!("cd {}", dir_str); + let target_dir = format!("cp {}", dir_str); let suggestions = completer.complete(&target_dir, target_dir.len()); // Create the expected values @@ -45,8 +45,34 @@ fn file_completions() { match_suggestions(expected_paths, suggestions); } +#[test] +fn folder_completions() { + // Create a new engine + let (dir, dir_str, engine) = new_engine(); + + let stack = Stack::new(); + + // Instatiate a new completer + let mut completer = NuCompleter::new(std::sync::Arc::new(engine), stack); + + // Test completions for the current folder + let target_dir = format!("cd {}", dir_str); + let suggestions = completer.complete(&target_dir, target_dir.len()); + + // Create the expected values + let expected_paths: Vec = vec![ + folder(dir.join("test_a")), + folder(dir.join("test_b")), + folder(dir.join("another")), + folder(dir.join(".hidden_folder")), + ]; + + // Match the results + match_suggestions(expected_paths, suggestions); +} + // creates a new engine with the current path into the completions fixtures folder -fn new_engine() -> (PathBuf, String, EngineState) { +pub fn new_engine() -> (PathBuf, String, EngineState) { // Target folder inside assets let dir = fs::fixtures().join("completions"); let mut dir_str = dir @@ -61,14 +87,14 @@ fn new_engine() -> (PathBuf, String, EngineState) { } // match a list of suggestions with the expected values -fn match_suggestions(expected: Vec, suggestions: Vec) { +pub fn match_suggestions(expected: Vec, suggestions: Vec) { expected.iter().zip(suggestions).for_each(|it| { assert_eq!(it.0, &it.1.value); }); } // append the separator to the converted path -fn folder(path: PathBuf) -> String { +pub fn folder(path: PathBuf) -> String { let mut converted_path = file(path); converted_path.push(SEP); @@ -76,6 +102,6 @@ fn folder(path: PathBuf) -> String { } // convert a given path to string -fn file(path: PathBuf) -> String { +pub fn file(path: PathBuf) -> String { path.into_os_string().into_string().unwrap_or_default() } diff --git a/crates/nu-color-config/src/shape_color.rs b/crates/nu-color-config/src/shape_color.rs index 11a4f0bcf4..94e4565d1e 100644 --- a/crates/nu-color-config/src/shape_color.rs +++ b/crates/nu-color-config/src/shape_color.rs @@ -29,6 +29,7 @@ pub fn get_shape_color(shape: String, conf: &Config) -> Style { "shape_record" => Style::new().fg(Color::Cyan).bold(), "shape_block" => Style::new().fg(Color::Blue).bold(), "shape_filepath" => Style::new().fg(Color::Cyan), + "shape_directory" => Style::new().fg(Color::Cyan), "shape_globpattern" => Style::new().fg(Color::Cyan).bold(), "shape_variable" => Style::new().fg(Color::Purple), "shape_flag" => Style::new().fg(Color::Blue).bold(), diff --git a/crates/nu-command/src/filesystem/cd.rs b/crates/nu-command/src/filesystem/cd.rs index 2f24700b4a..fe753e0cca 100644 --- a/crates/nu-command/src/filesystem/cd.rs +++ b/crates/nu-command/src/filesystem/cd.rs @@ -17,7 +17,7 @@ impl Command for Cd { fn signature(&self) -> nu_protocol::Signature { Signature::build("cd") - .optional("path", SyntaxShape::Filepath, "the path to change to") + .optional("path", SyntaxShape::Directory, "the path to change to") .category(Category::FileSystem) } diff --git a/crates/nu-command/src/filesystem/mkdir.rs b/crates/nu-command/src/filesystem/mkdir.rs index 25650ae68c..cfbe72b993 100644 --- a/crates/nu-command/src/filesystem/mkdir.rs +++ b/crates/nu-command/src/filesystem/mkdir.rs @@ -21,7 +21,7 @@ impl Command for Mkdir { Signature::build("mkdir") .rest( "rest", - SyntaxShape::Filepath, + SyntaxShape::Directory, "the name(s) of the path(s) to create", ) .switch("show-created-paths", "show the path(s) created.", Some('s')) diff --git a/crates/nu-command/src/formats/from/nuon.rs b/crates/nu-command/src/formats/from/nuon.rs index 2cdffe3208..e45ee5884a 100644 --- a/crates/nu-command/src/formats/from/nuon.rs +++ b/crates/nu-command/src/formats/from/nuon.rs @@ -237,6 +237,7 @@ fn convert_to_value( expr.span, )), Expr::Filepath(val) => Ok(Value::String { val, span }), + Expr::Directory(val) => Ok(Value::String { val, span }), Expr::Float(val) => Ok(Value::Float { val, span }), Expr::FullCellPath(full_cell_path) => { if !full_cell_path.tail.is_empty() { diff --git a/crates/nu-engine/src/eval.rs b/crates/nu-engine/src/eval.rs index cf6828d69c..8ac589debf 100644 --- a/crates/nu-engine/src/eval.rs +++ b/crates/nu-engine/src/eval.rs @@ -530,6 +530,15 @@ pub fn eval_expression( span: expr.span, }) } + Expr::Directory(s) => { + let cwd = current_dir_str(engine_state, stack)?; + let path = expand_path_with(s, cwd); + + Ok(Value::String { + val: path.to_string_lossy().to_string(), + span: expr.span, + }) + } Expr::GlobPattern(s) => { let cwd = current_dir_str(engine_state, stack)?; let path = expand_path_with(s, cwd); diff --git a/crates/nu-parser/src/flatten.rs b/crates/nu-parser/src/flatten.rs index 63184bf107..2584d18429 100644 --- a/crates/nu-parser/src/flatten.rs +++ b/crates/nu-parser/src/flatten.rs @@ -25,6 +25,7 @@ pub enum FlatShape { Record, Block, Filepath, + Directory, DateTime, GlobPattern, Variable, @@ -56,6 +57,7 @@ impl Display for FlatShape { FlatShape::Record => write!(f, "shape_record"), FlatShape::Block => write!(f, "shape_block"), FlatShape::Filepath => write!(f, "shape_filepath"), + FlatShape::Directory => write!(f, "shape_directory"), FlatShape::GlobPattern => write!(f, "shape_globpattern"), FlatShape::Variable => write!(f, "shape_variable"), FlatShape::Flag => write!(f, "shape_flag"), @@ -279,6 +281,9 @@ pub fn flatten_expression( Expr::Filepath(_) => { vec![(expr.span, FlatShape::Filepath)] } + Expr::Directory(_) => { + vec![(expr.span, FlatShape::Directory)] + } Expr::GlobPattern(_) => { vec![(expr.span, FlatShape::GlobPattern)] } diff --git a/crates/nu-parser/src/parser.rs b/crates/nu-parser/src/parser.rs index d84da91973..ba4dc21cb6 100644 --- a/crates/nu-parser/src/parser.rs +++ b/crates/nu-parser/src/parser.rs @@ -1915,6 +1915,33 @@ pub fn parse_full_cell_path( } } +pub fn parse_directory( + working_set: &mut StateWorkingSet, + span: Span, +) -> (Expression, Option) { + let bytes = working_set.get_span_contents(span); + let bytes = trim_quotes(bytes); + trace!("parsing: directory"); + + if let Ok(token) = String::from_utf8(bytes.into()) { + trace!("-- found {}", token); + ( + Expression { + expr: Expr::Directory(token), + span, + ty: Type::String, + custom_completion: None, + }, + None, + ) + } else { + ( + garbage(span), + Some(ParseError::Expected("directory".into(), span)), + ) + } +} + pub fn parse_filepath( working_set: &mut StateWorkingSet, span: Span, @@ -2551,6 +2578,7 @@ pub fn parse_shape_name( b"cell-path" => SyntaxShape::CellPath, b"duration" => SyntaxShape::Duration, b"path" => SyntaxShape::Filepath, + b"directory" => SyntaxShape::Directory, b"expr" => SyntaxShape::Expression, b"filesize" => SyntaxShape::Filesize, b"glob" => SyntaxShape::GlobPattern, @@ -3869,6 +3897,7 @@ pub fn parse_value( SyntaxShape::Filesize => parse_filesize(working_set, span), SyntaxShape::Range => parse_range(working_set, span, expand_aliases_denylist), SyntaxShape::Filepath => parse_filepath(working_set, span), + SyntaxShape::Directory => parse_directory(working_set, span), SyntaxShape::GlobPattern => parse_glob_pattern(working_set, span), SyntaxShape::String => parse_string(working_set, span), SyntaxShape::Binary => parse_binary(working_set, span), @@ -4868,6 +4897,7 @@ pub fn discover_captures_in_expr( } } Expr::Filepath(_) => {} + Expr::Directory(_) => {} Expr::Float(_) => {} Expr::FullCellPath(cell_path) => { let result = discover_captures_in_expr(working_set, &cell_path.head, seen, seen_blocks); diff --git a/crates/nu-protocol/src/ast/expr.rs b/crates/nu-protocol/src/ast/expr.rs index 3f1597476a..d3640aff45 100644 --- a/crates/nu-protocol/src/ast/expr.rs +++ b/crates/nu-protocol/src/ast/expr.rs @@ -33,6 +33,7 @@ pub enum Expr { ValueWithUnit(Box, Spanned), DateTime(chrono::DateTime), Filepath(String), + Directory(String), GlobPattern(String), String(String), CellPath(CellPath), diff --git a/crates/nu-protocol/src/ast/expression.rs b/crates/nu-protocol/src/ast/expression.rs index f29fac8275..c6ceeac41e 100644 --- a/crates/nu-protocol/src/ast/expression.rs +++ b/crates/nu-protocol/src/ast/expression.rs @@ -162,6 +162,7 @@ impl Expression { } Expr::ImportPattern(_) => false, Expr::Filepath(_) => false, + Expr::Directory(_) => false, Expr::Float(_) => false, Expr::FullCellPath(full_cell_path) => { if full_cell_path.head.has_in_variable(working_set) { @@ -320,6 +321,7 @@ impl Expression { } } Expr::Filepath(_) => {} + Expr::Directory(_) => {} Expr::Float(_) => {} Expr::FullCellPath(full_cell_path) => { full_cell_path @@ -467,6 +469,7 @@ impl Expression { } } Expr::Filepath(_) => {} + Expr::Directory(_) => {} Expr::Float(_) => {} Expr::FullCellPath(full_cell_path) => { full_cell_path diff --git a/crates/nu-protocol/src/syntax_shape.rs b/crates/nu-protocol/src/syntax_shape.rs index 645bc9ca9e..8626157b3b 100644 --- a/crates/nu-protocol/src/syntax_shape.rs +++ b/crates/nu-protocol/src/syntax_shape.rs @@ -34,6 +34,9 @@ pub enum SyntaxShape { /// A filepath is allowed Filepath, + /// A directory is allowed + Directory, + /// A glob pattern is allowed, eg `foo*` GlobPattern, @@ -105,6 +108,7 @@ impl SyntaxShape { SyntaxShape::Duration => Type::Duration, SyntaxShape::Expression => Type::Any, SyntaxShape::Filepath => Type::String, + SyntaxShape::Directory => Type::String, SyntaxShape::Filesize => Type::Filesize, SyntaxShape::FullCellPath => Type::Any, SyntaxShape::GlobPattern => Type::String, @@ -145,6 +149,7 @@ impl Display for SyntaxShape { SyntaxShape::Range => write!(f, "range"), SyntaxShape::Int => write!(f, "int"), SyntaxShape::Filepath => write!(f, "path"), + SyntaxShape::Directory => write!(f, "directory"), SyntaxShape::GlobPattern => write!(f, "glob"), SyntaxShape::ImportPattern => write!(f, "import"), SyntaxShape::Block(_) => write!(f, "block"),