diff --git a/crates/nu-cli/src/completions.rs b/crates/nu-cli/src/completions.rs deleted file mode 100644 index 23b33e113..000000000 --- a/crates/nu-cli/src/completions.rs +++ /dev/null @@ -1,699 +0,0 @@ -use nu_engine::eval_call; -use nu_parser::{flatten_expression, parse, trim_quotes, FlatShape}; -use nu_protocol::{ - ast::{Call, Expr, Expression}, - engine::{EngineState, Stack, StateWorkingSet}, - levenshtein_distance, PipelineData, Span, Type, Value, CONFIG_VARIABLE_ID, -}; -use reedline::{Completer, Suggestion}; - -const SEP: char = std::path::MAIN_SEPARATOR; - -pub struct CompletionOptions { - case_sensitive: bool, - positional: bool, - sort: bool, -} - -impl Default for CompletionOptions { - fn default() -> Self { - Self { - case_sensitive: true, - positional: true, - sort: true, - } - } -} - -#[derive(Clone)] -pub struct NuCompleter { - engine_state: EngineState, - stack: Stack, - config: Option, -} - -impl NuCompleter { - pub fn new(engine_state: EngineState, stack: Stack, config: Option) -> Self { - Self { - engine_state, - stack, - config, - } - } - - fn external_command_completion(&self, prefix: &str) -> Vec { - let mut executables = vec![]; - - let paths = self.engine_state.env_vars.get("PATH"); - - if let Some(paths) = paths { - if let Ok(paths) = paths.as_list() { - for path in paths { - let path = path.as_string().unwrap_or_default(); - - if let Ok(mut contents) = std::fs::read_dir(path) { - while let Some(Ok(item)) = contents.next() { - if !executables.contains( - &item - .path() - .file_name() - .map(|x| x.to_string_lossy().to_string()) - .unwrap_or_default(), - ) && matches!( - item.path() - .file_name() - .map(|x| x.to_string_lossy().starts_with(prefix)), - Some(true) - ) && is_executable::is_executable(&item.path()) - { - if let Ok(name) = item.file_name().into_string() { - executables.push(name); - } - } - } - } - } - } - } - - executables - } - - fn complete_variables( - &self, - working_set: &StateWorkingSet, - prefix: &[u8], - span: Span, - offset: usize, - ) -> Vec { - let mut output = vec![]; - - let builtins = ["$nu", "$in", "$config", "$env", "$nothing"]; - - for builtin in builtins { - if builtin.as_bytes().starts_with(prefix) { - output.push(Suggestion { - value: builtin.to_string(), - description: None, - extra: None, - span: reedline::Span { - start: span.start - offset, - end: span.end - offset, - }, - }); - } - } - - for scope in &working_set.delta.scope { - for v in &scope.vars { - if v.0.starts_with(prefix) { - output.push(Suggestion { - value: String::from_utf8_lossy(v.0).to_string(), - description: None, - extra: None, - span: reedline::Span { - start: span.start - offset, - end: span.end - offset, - }, - }); - } - } - } - for scope in &self.engine_state.scope { - for v in &scope.vars { - if v.0.starts_with(prefix) { - output.push(Suggestion { - value: String::from_utf8_lossy(v.0).to_string(), - description: None, - extra: None, - span: reedline::Span { - start: span.start - offset, - end: span.end - offset, - }, - }); - } - } - } - - output.dedup(); - - output - } - - fn complete_commands( - &self, - working_set: &StateWorkingSet, - span: Span, - offset: usize, - find_externals: bool, - ) -> Vec { - let prefix = working_set.get_span_contents(span); - - let results = working_set - .find_commands_by_prefix(prefix) - .into_iter() - .map(move |x| Suggestion { - value: String::from_utf8_lossy(&x.0).to_string(), - description: x.1, - extra: None, - span: reedline::Span { - start: span.start - offset, - end: span.end - offset, - }, - }); - - let results_aliases = - working_set - .find_aliases_by_prefix(prefix) - .into_iter() - .map(move |x| Suggestion { - value: String::from_utf8_lossy(&x).to_string(), - description: None, - extra: None, - span: reedline::Span { - start: span.start - offset, - end: span.end - offset, - }, - }); - - let mut results = results.chain(results_aliases).collect::>(); - - let prefix = working_set.get_span_contents(span); - let prefix = String::from_utf8_lossy(prefix).to_string(); - let mut results = if find_externals { - let results_external = - self.external_command_completion(&prefix) - .into_iter() - .map(move |x| Suggestion { - value: x, - description: None, - extra: None, - span: reedline::Span { - start: span.start - offset, - end: span.end - offset, - }, - }); - - for external in results_external { - if results.contains(&external) { - results.push(Suggestion { - value: format!("^{}", external.value), - description: None, - extra: None, - span: external.span, - }) - } else { - results.push(external) - } - } - - results - } else { - results - }; - - results.sort_by(|a, b| { - let a_distance = levenshtein_distance(&prefix, &a.value); - let b_distance = levenshtein_distance(&prefix, &b.value); - a_distance.cmp(&b_distance) - }); - - results - } - - fn completion_helper(&self, line: &str, pos: usize) -> Vec { - let mut working_set = StateWorkingSet::new(&self.engine_state); - let offset = working_set.next_span_start(); - let current_line_str = line.trim().to_string(); - let mut line = line.to_string(); - line.insert(pos, 'a'); - let line_pos = pos; - let pos = offset + pos; - let (output, _err) = parse( - &mut working_set, - Some("completer"), - line.as_bytes(), - false, - &[], - ); - - for pipeline in output.pipelines.into_iter() { - for expr in pipeline.expressions { - let flattened: Vec<_> = flatten_expression(&working_set, &expr); - - for (flat_idx, flat) in flattened.iter().enumerate() { - if pos >= flat.0.start && pos < flat.0.end { - let new_span = Span { - start: flat.0.start, - end: flat.0.end - 1, - }; - - let mut prefix = working_set.get_span_contents(flat.0).to_vec(); - prefix.remove(pos - flat.0.start); - - if prefix.starts_with(b"$") { - let mut output = - self.complete_variables(&working_set, &prefix, new_span, offset); - output.sort_by(|a, b| a.value.cmp(&b.value)); - return output; - } - if prefix.starts_with(b"-") { - // this might be a flag, let's see - if let Expr::Call(call) = &expr.expr { - let decl = working_set.get_decl(call.decl_id); - let sig = decl.signature(); - - let mut output = vec![]; - - for named in &sig.named { - let flag_desc = &named.desc; - if let Some(short) = named.short { - let mut named = vec![0; short.len_utf8()]; - short.encode_utf8(&mut named); - named.insert(0, b'-'); - if named.starts_with(&prefix) { - output.push(Suggestion { - value: String::from_utf8_lossy(&named).to_string(), - description: Some(flag_desc.to_string()), - extra: None, - span: reedline::Span { - start: new_span.start - offset, - end: new_span.end - offset, - }, - }); - } - } - - if named.long.is_empty() { - continue; - } - - let mut named = named.long.as_bytes().to_vec(); - named.insert(0, b'-'); - named.insert(0, b'-'); - if named.starts_with(&prefix) { - output.push(Suggestion { - value: String::from_utf8_lossy(&named).to_string(), - description: Some(flag_desc.to_string()), - extra: None, - span: reedline::Span { - start: new_span.start - offset, - end: new_span.end - offset, - }, - }); - } - } - output.sort_by(|a, b| a.value.cmp(&b.value)); - return output; - } - } - - match &flat.1 { - FlatShape::Custom(decl_id) => { - let mut stack = self.stack.clone(); - - // Set up our initial config to start from - if let Some(conf) = &self.config { - stack.vars.insert(CONFIG_VARIABLE_ID, conf.clone()); - } else { - stack.vars.insert( - CONFIG_VARIABLE_ID, - Value::Record { - cols: vec![], - vals: vec![], - span: Span { start: 0, end: 0 }, - }, - ); - } - - let result = eval_call( - &self.engine_state, - &mut stack, - &Call { - decl_id: *decl_id, - head: new_span, - positional: vec![ - Expression { - span: Span { start: 0, end: 0 }, - ty: Type::String, - expr: Expr::String(current_line_str), - custom_completion: None, - }, - Expression { - span: Span { start: 0, end: 0 }, - ty: Type::Int, - expr: Expr::Int(line_pos as i64), - custom_completion: None, - }, - ], - named: vec![], - redirect_stdout: true, - redirect_stderr: true, - }, - PipelineData::new(new_span), - ); - - fn map_completions<'a>( - list: impl Iterator, - new_span: Span, - offset: usize, - ) -> Vec { - list.filter_map(move |x| { - let s = x.as_string(); - - match s { - Ok(s) => Some(Suggestion { - value: s, - description: None, - extra: None, - span: reedline::Span { - start: new_span.start - offset, - end: new_span.end - offset, - }, - }), - Err(_) => None, - } - }) - .collect() - } - - let (completions, options) = match result { - Ok(pd) => { - let value = pd.into_value(new_span); - match &value { - Value::Record { .. } => { - let completions = value - .get_data_by_key("completions") - .and_then(|val| { - val.as_list().ok().map(|it| { - map_completions( - it.iter(), - new_span, - offset, - ) - }) - }) - .unwrap_or_default(); - let options = value.get_data_by_key("options"); - - let options = - if let Some(Value::Record { .. }) = &options { - let options = options.unwrap_or_default(); - CompletionOptions { - case_sensitive: options - .get_data_by_key("case_sensitive") - .and_then(|val| val.as_bool().ok()) - .unwrap_or(true), - positional: options - .get_data_by_key("positional") - .and_then(|val| val.as_bool().ok()) - .unwrap_or(true), - sort: options - .get_data_by_key("sort") - .and_then(|val| val.as_bool().ok()) - .unwrap_or(true), - } - } else { - CompletionOptions::default() - }; - - (completions, options) - } - Value::List { vals, .. } => { - let completions = - map_completions(vals.iter(), new_span, offset); - (completions, CompletionOptions::default()) - } - _ => (vec![], CompletionOptions::default()), - } - } - _ => (vec![], CompletionOptions::default()), - }; - - let mut completions: Vec = completions - .into_iter() - .filter(|it| { - // Minimise clones for new functionality - match (options.case_sensitive, options.positional) { - (true, true) => { - it.value.as_bytes().starts_with(&prefix) - } - (true, false) => it.value.contains( - std::str::from_utf8(&prefix).unwrap_or(""), - ), - (false, positional) => { - let value = it.value.to_lowercase(); - let prefix = std::str::from_utf8(&prefix) - .unwrap_or("") - .to_lowercase(); - if positional { - value.starts_with(&prefix) - } else { - value.contains(&prefix) - } - } - } - }) - .collect(); - - if options.sort { - completions.sort_by(|a, b| a.value.cmp(&b.value)); - } - - return completions; - } - FlatShape::Filepath | FlatShape::GlobPattern => { - 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(); - let mut output: Vec<_> = - file_path_completion(new_span, &prefix, &cwd) - .into_iter() - .map(move |x| Suggestion { - value: x.1, - description: None, - extra: None, - span: reedline::Span { - start: x.0.start - offset, - end: x.0.end - offset, - }, - }) - .collect(); - // output.sort_by(|a, b| a.value.cmp(&b.value)); - output.sort_by(|a, b| { - let a_distance = levenshtein_distance(&prefix, &a.value); - let b_distance = levenshtein_distance(&prefix, &b.value); - a_distance.cmp(&b_distance) - }); - - return output; - } - flat_shape => { - let last = flattened - .iter() - .rev() - .skip_while(|x| x.0.end > pos) - .take_while(|x| { - matches!( - x.1, - FlatShape::InternalCall - | FlatShape::External - | FlatShape::ExternalArg - | FlatShape::Literal - | FlatShape::String - ) - }) - .last(); - - // The last item here would be the earliest shape that could possible by part of this subcommand - let subcommands = if let Some(last) = last { - self.complete_commands( - &working_set, - Span { - start: last.0.start, - end: pos, - }, - offset, - false, - ) - } else { - vec![] - }; - - if !subcommands.is_empty() { - return subcommands; - } - - let commands = - if matches!(flat_shape, nu_parser::FlatShape::External) - || matches!(flat_shape, nu_parser::FlatShape::InternalCall) - || ((new_span.end - new_span.start) == 0) - { - // we're in a gap or at a command - self.complete_commands(&working_set, new_span, offset, true) - } else { - 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 preceding_byte = if new_span.start > offset { - working_set - .get_span_contents(Span { - start: new_span.start - 1, - end: new_span.start, - }) - .to_vec() - } else { - vec![] - }; - // let prefix = working_set.get_span_contents(flat.0); - let prefix = String::from_utf8_lossy(&prefix).to_string(); - let mut output = file_path_completion(new_span, &prefix, &cwd) - .into_iter() - .map(move |x| { - if flat_idx == 0 { - // We're in the command position - if x.1.starts_with('"') - && !matches!(preceding_byte.get(0), Some(b'^')) - { - let trimmed = trim_quotes(x.1.as_bytes()); - let trimmed = - String::from_utf8_lossy(trimmed).to_string(); - let expanded = - nu_path::canonicalize_with(trimmed, &cwd); - - if let Ok(expanded) = expanded { - if is_executable::is_executable(expanded) { - (x.0, format!("^{}", x.1)) - } else { - (x.0, x.1) - } - } else { - (x.0, x.1) - } - } else { - (x.0, x.1) - } - } else { - (x.0, x.1) - } - }) - .map(move |x| Suggestion { - value: x.1, - description: None, - extra: None, - span: reedline::Span { - start: x.0.start - offset, - end: x.0.end - offset, - }, - }) - .chain(subcommands.into_iter()) - .chain(commands.into_iter()) - .collect::>(); - //output.dedup_by(|a, b| a.1 == b.1); - //output.sort_by(|a, b| a.value.cmp(&b.value)); - output.sort_by(|a, b| { - let a_distance = levenshtein_distance(&prefix, &a.value); - let b_distance = levenshtein_distance(&prefix, &b.value); - a_distance.cmp(&b_distance) - }); - - return output; - } - } - } - } - } - } - - vec![] - } -} - -impl Completer for NuCompleter { - fn complete(&mut self, line: &str, pos: usize) -> Vec { - self.completion_helper(line, pos) - } -} - -fn file_path_completion( - span: nu_protocol::Span, - partial: &str, - cwd: &str, -) -> Vec<(nu_protocol::Span, String)> { - use std::path::{is_separator, Path}; - - let partial = partial.replace('\'', ""); - - 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_with(&base_dir_name, cwd); - // 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(); - } - - let mut results = 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); - } - - if path.contains(' ') { - path = format!("\'{}\'", path); - } - - Some((span, path)) - } else { - None - } - }) - }) - .collect() - } else { - Vec::new() - }; - - results.sort_by(|a, b| { - let a_distance = levenshtein_distance(partial, &a.1); - let b_distance = levenshtein_distance(partial, &b.1); - a_distance.cmp(&b_distance) - }); - - results -} - -fn matches(partial: &str, from: &str) -> bool { - from.to_ascii_lowercase() - .starts_with(&partial.to_ascii_lowercase()) -} diff --git a/crates/nu-cli/src/completions/base.rs b/crates/nu-cli/src/completions/base.rs new file mode 100644 index 000000000..01a49f3d4 --- /dev/null +++ b/crates/nu-cli/src/completions/base.rs @@ -0,0 +1,74 @@ +use crate::completions::{CompletionOptions, SortBy}; +use nu_protocol::{engine::StateWorkingSet, levenshtein_distance, Span}; +use reedline::Suggestion; + +// Completer trait represents the three stages of the completion +// fetch, filter and sort +pub trait Completer { + fn fetch( + &mut self, + working_set: &StateWorkingSet, + prefix: Vec, + span: Span, + offset: usize, + pos: usize, + ) -> (Vec, CompletionOptions); + + // Filter results using the completion options + fn filter( + &self, + prefix: Vec, + items: Vec, + options: CompletionOptions, + ) -> Vec { + items + .into_iter() + .filter(|it| { + // Minimise clones for new functionality + match (options.case_sensitive, options.positional) { + (true, true) => it.value.as_bytes().starts_with(&prefix), + (true, false) => it + .value + .contains(std::str::from_utf8(&prefix).unwrap_or("")), + (false, positional) => { + let value = it.value.to_lowercase(); + let prefix = std::str::from_utf8(&prefix).unwrap_or("").to_lowercase(); + if positional { + value.starts_with(&prefix) + } else { + value.contains(&prefix) + } + } + } + }) + .collect() + } + + // Sort is results using the completion options + fn sort( + &self, + items: Vec, + prefix: Vec, + options: CompletionOptions, + ) -> Vec { + let prefix_str = String::from_utf8_lossy(&prefix).to_string(); + let mut filtered_items = items; + + // Sort items + match options.sort_by { + SortBy::LevenshteinDistance => { + filtered_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) + }); + } + SortBy::Ascending => { + filtered_items.sort_by(|a, b| a.value.cmp(&b.value)); + } + SortBy::None => {} + }; + + filtered_items + } +} diff --git a/crates/nu-cli/src/completions/command_completions.rs b/crates/nu-cli/src/completions/command_completions.rs new file mode 100644 index 000000000..83962ed14 --- /dev/null +++ b/crates/nu-cli/src/completions/command_completions.rs @@ -0,0 +1,268 @@ +use crate::completions::{ + file_completions::file_path_completion, Completer, CompletionOptions, SortBy, +}; +use nu_parser::{trim_quotes, FlatShape}; +use nu_protocol::{ + engine::{EngineState, StateWorkingSet}, + Span, +}; +use reedline::Suggestion; + +pub struct CommandCompletion { + engine_state: EngineState, + flattened: Vec<(Span, FlatShape)>, + flat_idx: usize, + flat_shape: FlatShape, +} + +impl CommandCompletion { + pub fn new( + engine_state: EngineState, + _: &StateWorkingSet, + flattened: Vec<(Span, FlatShape)>, + flat_idx: usize, + flat_shape: FlatShape, + ) -> Self { + Self { + engine_state, + flattened, + flat_idx, + flat_shape, + } + } + + fn external_command_completion(&self, prefix: &str) -> Vec { + let mut executables = vec![]; + + let paths = self.engine_state.env_vars.get("PATH"); + + if let Some(paths) = paths { + if let Ok(paths) = paths.as_list() { + for path in paths { + let path = path.as_string().unwrap_or_default(); + + if let Ok(mut contents) = std::fs::read_dir(path) { + while let Some(Ok(item)) = contents.next() { + if !executables.contains( + &item + .path() + .file_name() + .map(|x| x.to_string_lossy().to_string()) + .unwrap_or_default(), + ) && matches!( + item.path() + .file_name() + .map(|x| x.to_string_lossy().starts_with(prefix)), + Some(true) + ) && is_executable::is_executable(&item.path()) + { + if let Ok(name) = item.file_name().into_string() { + executables.push(name); + } + } + } + } + } + } + } + + executables + } + + fn complete_commands( + &self, + working_set: &StateWorkingSet, + span: Span, + offset: usize, + find_externals: bool, + ) -> Vec { + let prefix = working_set.get_span_contents(span); + + let results = working_set + .find_commands_by_prefix(prefix) + .into_iter() + .map(move |x| Suggestion { + value: String::from_utf8_lossy(&x.0).to_string(), + description: x.1, + extra: None, + span: reedline::Span { + start: span.start - offset, + end: span.end - offset, + }, + }); + + let results_aliases = + working_set + .find_aliases_by_prefix(prefix) + .into_iter() + .map(move |x| Suggestion { + value: String::from_utf8_lossy(&x).to_string(), + description: None, + extra: None, + span: reedline::Span { + start: span.start - offset, + end: span.end - offset, + }, + }); + + let mut results = results.chain(results_aliases).collect::>(); + + let prefix = working_set.get_span_contents(span); + let prefix = String::from_utf8_lossy(prefix).to_string(); + let results = if find_externals { + let results_external = + self.external_command_completion(&prefix) + .into_iter() + .map(move |x| Suggestion { + value: x, + description: None, + extra: None, + span: reedline::Span { + start: span.start - offset, + end: span.end - offset, + }, + }); + + for external in results_external { + if results.contains(&external) { + results.push(Suggestion { + value: format!("^{}", external.value), + description: None, + extra: None, + span: external.span, + }) + } else { + results.push(external) + } + } + + results + } else { + results + }; + + results + } +} + +impl Completer for CommandCompletion { + fn fetch( + &mut self, + working_set: &StateWorkingSet, + prefix: Vec, + span: Span, + offset: usize, + pos: usize, + ) -> (Vec, CompletionOptions) { + let last = self + .flattened + .iter() + .rev() + .skip_while(|x| x.0.end > pos) + .take_while(|x| { + matches!( + x.1, + FlatShape::InternalCall + | FlatShape::External + | FlatShape::ExternalArg + | FlatShape::Literal + | FlatShape::String + ) + }) + .last(); + + // Options + let options = CompletionOptions::new(true, true, SortBy::LevenshteinDistance); + + // The last item here would be the earliest shape that could possible by part of this subcommand + let subcommands = if let Some(last) = last { + self.complete_commands( + working_set, + Span { + start: last.0.start, + end: pos, + }, + offset, + false, + ) + } else { + vec![] + }; + + if !subcommands.is_empty() { + return (subcommands, options); + } + + let commands = if matches!(self.flat_shape, nu_parser::FlatShape::External) + || matches!(self.flat_shape, nu_parser::FlatShape::InternalCall) + || ((span.end - span.start) == 0) + { + // we're in a gap or at a command + self.complete_commands(working_set, span, offset, true) + } else { + 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 preceding_byte = if span.start > offset { + working_set + .get_span_contents(Span { + start: span.start - 1, + end: span.start, + }) + .to_vec() + } else { + vec![] + }; + // let prefix = working_set.get_span_contents(flat.0); + let prefix = String::from_utf8_lossy(&prefix).to_string(); + let output = file_path_completion(span, &prefix, &cwd) + .into_iter() + .map(move |x| { + if self.flat_idx == 0 { + // We're in the command position + if x.1.starts_with('"') && !matches!(preceding_byte.get(0), Some(b'^')) { + let trimmed = trim_quotes(x.1.as_bytes()); + let trimmed = String::from_utf8_lossy(trimmed).to_string(); + let expanded = nu_path::canonicalize_with(trimmed, &cwd); + + if let Ok(expanded) = expanded { + if is_executable::is_executable(expanded) { + (x.0, format!("^{}", x.1)) + } else { + (x.0, x.1) + } + } else { + (x.0, x.1) + } + } else { + (x.0, x.1) + } + } else { + (x.0, x.1) + } + }) + .map(move |x| Suggestion { + value: x.1, + description: None, + extra: None, + span: reedline::Span { + start: x.0.start - offset, + end: x.0.end - offset, + }, + }) + .chain(subcommands.into_iter()) + .chain(commands.into_iter()) + .collect::>(); + + (output, options) + } +} diff --git a/crates/nu-cli/src/completions/completer.rs b/crates/nu-cli/src/completions/completer.rs new file mode 100644 index 000000000..68174c972 --- /dev/null +++ b/crates/nu-cli/src/completions/completer.rs @@ -0,0 +1,173 @@ +use crate::completions::{ + CommandCompletion, Completer, CustomCompletion, FileCompletion, FlagCompletion, + VariableCompletion, +}; +use nu_parser::{flatten_expression, parse, FlatShape}; +use nu_protocol::{ + engine::{EngineState, Stack, StateWorkingSet}, + Span, Value, +}; +use reedline::{Completer as ReedlineCompleter, Suggestion}; + +#[derive(Clone)] +pub struct NuCompleter { + engine_state: EngineState, + stack: Stack, + config: Option, +} + +impl NuCompleter { + pub fn new(engine_state: EngineState, stack: Stack, config: Option) -> Self { + Self { + engine_state, + stack, + config, + } + } + + // Process the completion for a given completer + fn process_completion( + &self, + completer: &mut T, + working_set: &StateWorkingSet, + prefix: Vec, + new_span: Span, + offset: usize, + pos: usize, + ) -> Vec { + // Fetch + let (mut suggestions, options) = + completer.fetch(working_set, prefix.clone(), new_span, offset, pos); + + // Filter + suggestions = completer.filter(prefix.clone(), suggestions, options.clone()); + + // Sort + suggestions = completer.sort(suggestions, prefix, options); + + suggestions + } + + fn completion_helper(&mut self, line: &str, pos: usize) -> Vec { + let mut working_set = StateWorkingSet::new(&self.engine_state); + let offset = working_set.next_span_start(); + let mut line = line.to_string(); + line.insert(pos, 'a'); + let pos = offset + pos; + let (output, _err) = parse( + &mut working_set, + Some("completer"), + line.as_bytes(), + false, + &[], + ); + + for pipeline in output.pipelines.into_iter() { + for expr in pipeline.expressions { + let flattened: Vec<_> = flatten_expression(&working_set, &expr); + + for (flat_idx, flat) in flattened.iter().enumerate() { + if pos >= flat.0.start && pos < flat.0.end { + // Create a new span + let new_span = Span { + start: flat.0.start, + end: flat.0.end - 1, + }; + + // Parses the prefix + let mut prefix = working_set.get_span_contents(flat.0).to_vec(); + prefix.remove(pos - flat.0.start); + + // Variables completion + if prefix.starts_with(b"$") { + let mut completer = VariableCompletion::new(self.engine_state.clone()); + + return self.process_completion( + &mut completer, + &working_set, + prefix, + new_span, + offset, + pos, + ); + } + + // Flags completion + if prefix.starts_with(b"-") { + let mut completer = FlagCompletion::new(expr); + + return self.process_completion( + &mut completer, + &working_set, + prefix, + new_span, + offset, + pos, + ); + } + + // Match other types + match &flat.1 { + FlatShape::Custom(decl_id) => { + let mut completer = CustomCompletion::new( + self.engine_state.clone(), + self.stack.clone(), + self.config.clone(), + *decl_id, + line, + ); + + 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()); + + return self.process_completion( + &mut completer, + &working_set, + prefix, + new_span, + offset, + pos, + ); + } + flat_shape => { + let mut completer = CommandCompletion::new( + self.engine_state.clone(), + &working_set, + flattened.clone(), + flat_idx, + flat_shape.clone(), + ); + + return self.process_completion( + &mut completer, + &working_set, + prefix, + new_span, + offset, + pos, + ); + } + }; + } + } + } + } + + return vec![]; + } +} + +impl ReedlineCompleter for NuCompleter { + fn complete(&mut self, line: &str, pos: usize) -> Vec { + self.completion_helper(line, pos) + } +} diff --git a/crates/nu-cli/src/completions/completion_options.rs b/crates/nu-cli/src/completions/completion_options.rs new file mode 100644 index 000000000..19e2469eb --- /dev/null +++ b/crates/nu-cli/src/completions/completion_options.rs @@ -0,0 +1,33 @@ +#[derive(Clone)] +pub enum SortBy { + LevenshteinDistance, + Ascending, + None, +} + +#[derive(Clone)] +pub struct CompletionOptions { + pub case_sensitive: bool, + pub positional: bool, + pub sort_by: SortBy, +} + +impl CompletionOptions { + pub fn new(case_sensitive: bool, positional: bool, sort_by: SortBy) -> Self { + Self { + case_sensitive, + positional, + sort_by, + } + } +} + +impl Default for CompletionOptions { + fn default() -> Self { + Self { + case_sensitive: true, + positional: true, + sort_by: SortBy::Ascending, + } + } +} diff --git a/crates/nu-cli/src/completions/custom_completions.rs b/crates/nu-cli/src/completions/custom_completions.rs new file mode 100644 index 000000000..8ce43ba85 --- /dev/null +++ b/crates/nu-cli/src/completions/custom_completions.rs @@ -0,0 +1,171 @@ +use crate::completions::{Completer, CompletionOptions, SortBy}; +use nu_engine::eval_call; +use nu_protocol::{ + ast::{Call, Expr, Expression}, + engine::{EngineState, Stack, StateWorkingSet}, + PipelineData, Span, Type, Value, CONFIG_VARIABLE_ID, +}; +use reedline::Suggestion; + +pub struct CustomCompletion { + engine_state: EngineState, + stack: Stack, + config: Option, + decl_id: usize, + line: String, +} + +impl CustomCompletion { + pub fn new( + engine_state: EngineState, + stack: Stack, + config: Option, + decl_id: usize, + line: String, + ) -> Self { + Self { + engine_state, + stack, + config, + decl_id, + line, + } + } + + fn map_completions<'a>( + &self, + list: impl Iterator, + span: Span, + offset: usize, + ) -> Vec { + list.filter_map(move |x| { + let s = x.as_string(); + + match s { + Ok(s) => Some(Suggestion { + value: s, + description: None, + extra: None, + span: reedline::Span { + start: span.start - offset, + end: span.end - offset, + }, + }), + Err(_) => None, + } + }) + .collect() + } +} + +impl Completer for CustomCompletion { + fn fetch( + &mut self, + _: &StateWorkingSet, + _: Vec, + span: Span, + offset: usize, + pos: usize, + ) -> (Vec, CompletionOptions) { + // Line position + let line_pos = pos - offset; + + // Set up our initial config to start from + if let Some(conf) = &self.config { + self.stack.vars.insert(CONFIG_VARIABLE_ID, conf.clone()); + } else { + self.stack.vars.insert( + CONFIG_VARIABLE_ID, + Value::Record { + cols: vec![], + vals: vec![], + span: Span { start: 0, end: 0 }, + }, + ); + } + + // Call custom declaration + let result = eval_call( + &self.engine_state, + &mut self.stack, + &Call { + decl_id: self.decl_id, + head: span, + positional: vec![ + Expression { + span: Span { start: 0, end: 0 }, + ty: Type::String, + expr: Expr::String(self.line.clone()), + custom_completion: None, + }, + Expression { + span: Span { start: 0, end: 0 }, + ty: Type::Int, + expr: Expr::Int(line_pos as i64), + custom_completion: None, + }, + ], + named: vec![], + redirect_stdout: true, + redirect_stderr: true, + }, + PipelineData::new(span), + ); + + // Parse result + let (suggestions, options) = match result { + Ok(pd) => { + let value = pd.into_value(span); + match &value { + Value::Record { .. } => { + let completions = value + .get_data_by_key("completions") + .and_then(|val| { + val.as_list() + .ok() + .map(|it| self.map_completions(it.iter(), span, offset)) + }) + .unwrap_or_default(); + let options = value.get_data_by_key("options"); + + let options = if let Some(Value::Record { .. }) = &options { + let options = options.unwrap_or_default(); + let should_sort = options + .get_data_by_key("sort") + .and_then(|val| val.as_bool().ok()) + .unwrap_or(false); + + CompletionOptions { + case_sensitive: options + .get_data_by_key("case_sensitive") + .and_then(|val| val.as_bool().ok()) + .unwrap_or(true), + positional: options + .get_data_by_key("positional") + .and_then(|val| val.as_bool().ok()) + .unwrap_or(true), + sort_by: if should_sort { + SortBy::Ascending + } else { + SortBy::None + }, + } + } else { + CompletionOptions::default() + }; + + (completions, options) + } + Value::List { vals, .. } => { + let completions = self.map_completions(vals.iter(), span, offset); + (completions, CompletionOptions::default()) + } + _ => (vec![], CompletionOptions::default()), + } + } + _ => (vec![], CompletionOptions::default()), + }; + + (suggestions, options) + } +} diff --git a/crates/nu-cli/src/completions/file_completions.rs b/crates/nu-cli/src/completions/file_completions.rs new file mode 100644 index 000000000..1f7fda70b --- /dev/null +++ b/crates/nu-cli/src/completions/file_completions.rs @@ -0,0 +1,116 @@ +use crate::completions::{Completer, CompletionOptions, SortBy}; +use nu_protocol::{ + engine::{EngineState, StateWorkingSet}, + Span, +}; +use reedline::Suggestion; +use std::path::{is_separator, Path}; + +const SEP: char = std::path::MAIN_SEPARATOR; + +#[derive(Clone)] +pub struct FileCompletion { + engine_state: EngineState, +} + +impl FileCompletion { + pub fn new(engine_state: EngineState) -> Self { + Self { engine_state } + } +} + +impl Completer for FileCompletion { + fn fetch( + &mut self, + _: &StateWorkingSet, + prefix: Vec, + span: Span, + offset: usize, + _: usize, + ) -> (Vec, CompletionOptions) { + 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(); + let output: Vec<_> = file_path_completion(span, &prefix, &cwd) + .into_iter() + .map(move |x| Suggestion { + value: x.1, + description: None, + extra: None, + span: reedline::Span { + start: x.0.start - offset, + end: x.0.end - offset, + }, + }) + .collect(); + + // Options + let options = CompletionOptions::new(true, true, SortBy::LevenshteinDistance); + + (output, options) + } +} + +pub fn file_path_completion( + span: nu_protocol::Span, + partial: &str, + cwd: &str, +) -> Vec<(nu_protocol::Span, String)> { + let partial = partial.replace('\'', ""); + + 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_with(&base_dir_name, cwd); + // 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() { + return 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); + } + + if path.contains(' ') { + path = format!("\'{}\'", path); + } + + Some((span, path)) + } else { + None + } + }) + }) + .collect(); + } + + Vec::new() +} + +pub fn matches(partial: &str, from: &str) -> bool { + from.to_ascii_lowercase() + .starts_with(&partial.to_ascii_lowercase()) +} diff --git a/crates/nu-cli/src/completions/flag_completions.rs b/crates/nu-cli/src/completions/flag_completions.rs new file mode 100644 index 000000000..7d378c417 --- /dev/null +++ b/crates/nu-cli/src/completions/flag_completions.rs @@ -0,0 +1,81 @@ +use crate::completions::{Completer, CompletionOptions}; +use nu_protocol::{ + ast::{Expr, Expression}, + engine::StateWorkingSet, + Span, +}; + +use reedline::Suggestion; + +#[derive(Clone)] +pub struct FlagCompletion { + expression: Expression, +} + +impl FlagCompletion { + pub fn new(expression: Expression) -> Self { + Self { expression } + } +} + +impl Completer for FlagCompletion { + fn fetch( + &mut self, + working_set: &StateWorkingSet, + prefix: Vec, + span: Span, + offset: usize, + _: usize, + ) -> (Vec, CompletionOptions) { + // Check if it's a flag + if let Expr::Call(call) = &self.expression.expr { + let decl = working_set.get_decl(call.decl_id); + let sig = decl.signature(); + + let mut output = vec![]; + + for named in &sig.named { + let flag_desc = &named.desc; + if let Some(short) = named.short { + let mut named = vec![0; short.len_utf8()]; + short.encode_utf8(&mut named); + named.insert(0, b'-'); + if named.starts_with(&prefix) { + output.push(Suggestion { + value: String::from_utf8_lossy(&named).to_string(), + description: Some(flag_desc.to_string()), + extra: None, + span: reedline::Span { + start: span.start - offset, + end: span.end - offset, + }, + }); + } + } + + if named.long.is_empty() { + continue; + } + + let mut named = named.long.as_bytes().to_vec(); + named.insert(0, b'-'); + named.insert(0, b'-'); + if named.starts_with(&prefix) { + output.push(Suggestion { + value: String::from_utf8_lossy(&named).to_string(), + description: Some(flag_desc.to_string()), + extra: None, + span: reedline::Span { + start: span.start - offset, + end: span.end - offset, + }, + }); + } + } + + return (output, CompletionOptions::default()); + } + + (vec![], CompletionOptions::default()) + } +} diff --git a/crates/nu-cli/src/completions/mod.rs b/crates/nu-cli/src/completions/mod.rs new file mode 100644 index 000000000..21c877b55 --- /dev/null +++ b/crates/nu-cli/src/completions/mod.rs @@ -0,0 +1,17 @@ +mod base; +mod command_completions; +mod completer; +mod completion_options; +mod custom_completions; +mod file_completions; +mod flag_completions; +mod variable_completions; + +pub use base::Completer; +pub use command_completions::CommandCompletion; +pub use completer::NuCompleter; +pub use completion_options::{CompletionOptions, SortBy}; +pub use custom_completions::CustomCompletion; +pub use file_completions::{file_path_completion, FileCompletion}; +pub use flag_completions::FlagCompletion; +pub use variable_completions::VariableCompletion; diff --git a/crates/nu-cli/src/completions/variable_completions.rs b/crates/nu-cli/src/completions/variable_completions.rs new file mode 100644 index 000000000..2a06392f3 --- /dev/null +++ b/crates/nu-cli/src/completions/variable_completions.rs @@ -0,0 +1,82 @@ +use crate::completions::{Completer, CompletionOptions}; +use nu_protocol::{ + engine::{EngineState, StateWorkingSet}, + Span, +}; + +use reedline::Suggestion; + +#[derive(Clone)] +pub struct VariableCompletion { + engine_state: EngineState, +} + +impl VariableCompletion { + pub fn new(engine_state: EngineState) -> Self { + Self { engine_state } + } +} + +impl Completer for VariableCompletion { + fn fetch( + &mut self, + working_set: &StateWorkingSet, + prefix: Vec, + span: Span, + offset: usize, + _: usize, + ) -> (Vec, CompletionOptions) { + let mut output = vec![]; + + let builtins = ["$nu", "$in", "$config", "$env", "$nothing"]; + + for builtin in builtins { + if builtin.as_bytes().starts_with(&prefix) { + output.push(Suggestion { + value: builtin.to_string(), + description: None, + extra: None, + span: reedline::Span { + start: span.start - offset, + end: span.end - offset, + }, + }); + } + } + + for scope in &working_set.delta.scope { + for v in &scope.vars { + if v.0.starts_with(&prefix) { + output.push(Suggestion { + value: String::from_utf8_lossy(v.0).to_string(), + description: None, + extra: None, + span: reedline::Span { + start: span.start - offset, + end: span.end - offset, + }, + }); + } + } + } + for scope in &self.engine_state.scope { + for v in &scope.vars { + if v.0.starts_with(&prefix) { + output.push(Suggestion { + value: String::from_utf8_lossy(v.0).to_string(), + description: None, + extra: None, + span: reedline::Span { + start: span.start - offset, + end: span.end - offset, + }, + }); + } + } + } + + output.dedup(); + + (output, CompletionOptions::default()) + } +} diff --git a/crates/nu-cli/src/repl.rs b/crates/nu-cli/src/repl.rs index cc11fb09f..a209d298e 100644 --- a/crates/nu-cli/src/repl.rs +++ b/crates/nu-cli/src/repl.rs @@ -1,10 +1,10 @@ use crate::reedline_config::add_menus; +use crate::{completions::NuCompleter, NuHighlighter, NuValidator, NushellPrompt}; use crate::{prompt_update, reedline_config}; use crate::{ reedline_config::KeybindingsMode, util::{eval_source, report_error}, }; -use crate::{NuCompleter, NuHighlighter, NuValidator, NushellPrompt}; use log::info; use log::trace; use miette::{IntoDiagnostic, Result}; diff --git a/crates/nu-parser/src/flatten.rs b/crates/nu-parser/src/flatten.rs index 07f89948c..84720d70c 100644 --- a/crates/nu-parser/src/flatten.rs +++ b/crates/nu-parser/src/flatten.rs @@ -3,7 +3,7 @@ use nu_protocol::DeclId; use nu_protocol::{engine::StateWorkingSet, Span}; use std::fmt::{Display, Formatter, Result}; -#[derive(Debug, Eq, PartialEq, Ord, PartialOrd)] +#[derive(Debug, Eq, PartialEq, Ord, Clone, PartialOrd)] pub enum FlatShape { Garbage, Nothing,