nu-cli: directory syntax shape + completions (#5299)

This commit is contained in:
Herlon Aguiar 2022-04-22 22:18:51 +02:00 committed by GitHub
parent 661283c4d2
commit 5ff2ae628b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 211 additions and 9 deletions

View File

@ -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());

View File

@ -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<EngineState>,
}
impl DirectoryCompletion {
pub fn new(engine_state: Arc<EngineState>) -> Self {
Self { engine_state }
}
}
impl Completer for DirectoryCompletion {
fn fetch(
&mut self,
_: &StateWorkingSet,
prefix: Vec<u8>,
span: Span,
offset: usize,
_: usize,
) -> Vec<Suggestion> {
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<Suggestion>, prefix: Vec<u8>) -> Vec<Suggestion> {
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<Suggestion> = vec![];
let mut non_hidden: Vec<Suggestion> = 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
}
}

View File

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

View File

@ -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),

View File

@ -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<String> = 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<String>, suggestions: Vec<Suggestion>) {
pub fn match_suggestions(expected: Vec<String>, suggestions: Vec<Suggestion>) {
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()
}

View File

@ -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(),

View File

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

View File

@ -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'))

View File

@ -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() {

View File

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

View File

@ -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)]
}

View File

@ -1915,6 +1915,33 @@ pub fn parse_full_cell_path(
}
}
pub fn parse_directory(
working_set: &mut StateWorkingSet,
span: Span,
) -> (Expression, Option<ParseError>) {
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);

View File

@ -33,6 +33,7 @@ pub enum Expr {
ValueWithUnit(Box<Expression>, Spanned<Unit>),
DateTime(chrono::DateTime<FixedOffset>),
Filepath(String),
Directory(String),
GlobPattern(String),
String(String),
CellPath(CellPath),

View File

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

View File

@ -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"),