diff --git a/Cargo.lock b/Cargo.lock index 8a1506e45..7cc597db1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3233,6 +3233,7 @@ dependencies = [ "itertools", "nu-cli", "nu-command", + "nu-completion", "nu-data", "nu-engine", "nu-errors", @@ -3318,6 +3319,7 @@ dependencies = [ "meval", "nu-ansi-term", "nu-command", + "nu-completion", "nu-data", "nu-engine", "nu-errors", @@ -3476,6 +3478,23 @@ dependencies = [ "zip", ] +[[package]] +name = "nu-completion" +version = "0.32.1" +dependencies = [ + "directories-next", + "dirs-next", + "indexmap", + "nu-data", + "nu-engine", + "nu-errors", + "nu-parser", + "nu-protocol", + "nu-source", + "nu-test-support", + "rustyline", +] + [[package]] name = "nu-data" version = "0.32.1" diff --git a/Cargo.toml b/Cargo.toml index 5dde0ebe4..39dfb3ff4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,6 +20,7 @@ members = ["crates/*/"] [dependencies] nu-cli = { version = "0.32.1", path = "./crates/nu-cli", default-features = false } nu-command = { version = "0.32.1", path = "./crates/nu-command" } +nu-completion = { version = "0.32.1", path = "./crates/nu-completion" } nu-data = { version = "0.32.1", path = "./crates/nu-data" } nu-engine = { version = "0.32.1", path = "./crates/nu-engine" } nu-errors = { version = "0.32.1", path = "./crates/nu-errors" } diff --git a/crates/nu-cli/Cargo.toml b/crates/nu-cli/Cargo.toml index 027f50f56..fd8d5ffeb 100644 --- a/crates/nu-cli/Cargo.toml +++ b/crates/nu-cli/Cargo.toml @@ -11,6 +11,7 @@ version = "0.32.1" doctest = false [dependencies] +nu-completion = { version = "0.32.1", path = "../nu-completion" } nu-command = { version = "0.32.1", path = "../nu-command" } nu-data = { version = "0.32.1", path = "../nu-data" } nu-engine = { version = "0.32.1", path = "../nu-engine" } diff --git a/crates/nu-cli/src/completion/mod.rs b/crates/nu-cli/src/completion/mod.rs deleted file mode 100644 index c66d959f1..000000000 --- a/crates/nu-cli/src/completion/mod.rs +++ /dev/null @@ -1,37 +0,0 @@ -pub(crate) mod command; -pub(crate) mod engine; -pub(crate) mod flag; -pub(crate) mod matchers; -pub(crate) mod path; - -use matchers::Matcher; -use nu_engine::EvaluationContext; - -#[derive(Debug, Eq, PartialEq)] -pub struct Suggestion { - pub display: String, - pub replacement: String, -} - -pub struct CompletionContext<'a>(&'a EvaluationContext); - -impl<'a> CompletionContext<'a> { - pub fn new(a: &'a EvaluationContext) -> CompletionContext<'a> { - CompletionContext(a) - } -} - -impl<'a> AsRef for CompletionContext<'a> { - fn as_ref(&self) -> &EvaluationContext { - self.0 - } -} - -pub trait Completer { - fn complete( - &self, - ctx: &CompletionContext<'_>, - partial: &str, - matcher: &dyn Matcher, - ) -> Vec; -} diff --git a/crates/nu-cli/src/lib.rs b/crates/nu-cli/src/lib.rs index e1acc17d0..e918ef5f4 100644 --- a/crates/nu-cli/src/lib.rs +++ b/crates/nu-cli/src/lib.rs @@ -11,8 +11,6 @@ extern crate quickcheck_macros; mod app; mod cli; -#[cfg(feature = "rustyline-support")] -mod completion; mod format; #[cfg(feature = "rustyline-support")] mod keybinding; diff --git a/crates/nu-cli/src/shell.rs b/crates/nu-cli/src/shell.rs index 3c8930606..58cc406de 100644 --- a/crates/nu-cli/src/shell.rs +++ b/crates/nu-cli/src/shell.rs @@ -1,7 +1,5 @@ #![allow(clippy::module_inception)] -#[cfg(feature = "rustyline-support")] -pub(crate) mod completer; #[cfg(feature = "rustyline-support")] pub(crate) mod helper; diff --git a/crates/nu-cli/src/shell/helper.rs b/crates/nu-cli/src/shell/helper.rs index 5f47bc409..1bdd63300 100644 --- a/crates/nu-cli/src/shell/helper.rs +++ b/crates/nu-cli/src/shell/helper.rs @@ -1,6 +1,5 @@ -use crate::completion; -use crate::shell::completer::NuCompleter; use nu_ansi_term::Color; +use nu_completion::NuCompleter; use nu_engine::{DefaultPalette, EvaluationContext, Painter}; use nu_source::{Tag, Tagged}; use std::borrow::Cow::{self, Owned}; @@ -28,18 +27,28 @@ impl Helper { } } -impl rustyline::completion::Candidate for completion::Suggestion { +struct CompletionContext<'a>(&'a EvaluationContext); + +impl<'a> nu_completion::CompletionContext for CompletionContext<'a> { + fn signature_registry(&self) -> &dyn nu_parser::ParserScope { + &self.0.scope + } +} + +pub struct CompletionSuggestion(nu_completion::Suggestion); + +impl rustyline::completion::Candidate for CompletionSuggestion { fn display(&self) -> &str { - &self.display + &self.0.display } fn replacement(&self) -> &str { - &self.replacement + &self.0.replacement } } impl rustyline::completion::Completer for Helper { - type Candidate = completion::Suggestion; + type Candidate = CompletionSuggestion; fn complete( &self, @@ -47,8 +56,10 @@ impl rustyline::completion::Completer for Helper { pos: usize, _ctx: &rustyline::Context<'_>, ) -> Result<(usize, Vec), rustyline::error::ReadlineError> { - let ctx = completion::CompletionContext::new(&self.context); - Ok(self.completer.complete(line, pos, &ctx)) + let ctx = CompletionContext(&self.context); + let (position, suggestions) = self.completer.complete(line, pos, &ctx); + let suggestions = suggestions.into_iter().map(CompletionSuggestion).collect(); + Ok((position, suggestions)) } fn update(&self, line: &mut rustyline::line_buffer::LineBuffer, start: usize, elected: &str) { diff --git a/crates/nu-completion/Cargo.toml b/crates/nu-completion/Cargo.toml new file mode 100644 index 000000000..0de1bbd63 --- /dev/null +++ b/crates/nu-completion/Cargo.toml @@ -0,0 +1,29 @@ +[package] +authors = ["The Nu Project Contributors"] +description = "Completions for nushell" +edition = "2018" +license = "MIT" +name = "nu-completion" +version = "0.32.1" + +[lib] +doctest = false + +[dependencies] +nu-data = { version = "0.32.1", path = "../nu-data" } +nu-engine = { version = "0.32.1", path = "../nu-engine" } +nu-errors = { version = "0.32.1", path = "../nu-errors" } +nu-parser = { version = "0.32.1", path = "../nu-parser" } +nu-protocol = { version = "0.32.1", path = "../nu-protocol" } +nu-source = { version = "0.32.1", path = "../nu-source" } +nu-test-support = { version = "0.32.1", path = "../nu-test-support" } + +directories-next = { version = "2.0.0", optional = true } +dirs-next = { version = "2.0.0", optional = true } +indexmap = { version = "1.6.1", features = ["serde-1"] } +rustyline = { version = "8.1.0", optional = true } + +[features] +rustyline-support = ["rustyline", "nu-engine/rustyline-support"] +dirs = ["dirs-next"] +directories = ["directories-next"] diff --git a/crates/nu-cli/src/completion/command.rs b/crates/nu-completion/src/command.rs similarity index 89% rename from crates/nu-cli/src/completion/command.rs rename to crates/nu-completion/src/command.rs index 242508407..da67ad935 100644 --- a/crates/nu-cli/src/completion/command.rs +++ b/crates/nu-completion/src/command.rs @@ -1,24 +1,22 @@ +use nu_test_support::NATIVE_PATH_ENV_VAR; + use std::iter::FromIterator; use std::path::Path; use indexmap::set::IndexSet; use super::matchers::Matcher; -use crate::completion::{Completer, CompletionContext, Suggestion}; -use nu_engine::EvaluationContext; -use nu_test_support::NATIVE_PATH_ENV_VAR; +use crate::{Completer, CompletionContext, Suggestion}; pub struct CommandCompleter; -impl Completer for CommandCompleter { - fn complete( - &self, - ctx: &CompletionContext<'_>, - partial: &str, - matcher: &dyn Matcher, - ) -> Vec { - let context: &EvaluationContext = ctx.as_ref(); - let mut commands: IndexSet = IndexSet::from_iter(context.scope.get_command_names()); +impl Completer for CommandCompleter +where + Context: CompletionContext, +{ + fn complete(&self, ctx: &Context, partial: &str, matcher: &dyn Matcher) -> Vec { + let registry = ctx.signature_registry(); + let mut commands: IndexSet = IndexSet::from_iter(registry.get_names()); // Command suggestions can come from three possible sets: // 1. internal command names, @@ -40,7 +38,7 @@ impl Completer for CommandCompleter { .collect(); if !partial.is_empty() { - let path_completer = crate::completion::path::PathCompleter; + let path_completer = crate::path::PathCompleter; let path_results = path_completer.path_suggestions(partial, matcher); let iter = path_results.into_iter().filter_map(|path_suggestion| { let path = path_suggestion.path; diff --git a/crates/nu-cli/src/shell/completer.rs b/crates/nu-completion/src/completer.rs similarity index 86% rename from crates/nu-cli/src/shell/completer.rs rename to crates/nu-completion/src/completer.rs index f7f9d338a..90182c557 100644 --- a/crates/nu-cli/src/shell/completer.rs +++ b/crates/nu-completion/src/completer.rs @@ -1,35 +1,34 @@ -use crate::completion::command::CommandCompleter; -use crate::completion::flag::FlagCompleter; -use crate::completion::matchers; -use crate::completion::matchers::Matcher; -use crate::completion::path::{PathCompleter, PathSuggestion}; -use crate::completion::{self, Completer, Suggestion}; -use nu_engine::EvaluationContext; -use nu_parser::ParserScope; -use nu_source::{Span, Tag}; - use std::borrow::Cow; -pub(crate) struct NuCompleter {} +use nu_source::{Span, Tag}; + +use crate::command::CommandCompleter; +use crate::engine; +use crate::flag::FlagCompleter; +use crate::matchers; +use crate::matchers::Matcher; +use crate::path::{PathCompleter, PathSuggestion}; +use crate::{Completer, CompletionContext, Suggestion}; + +pub struct NuCompleter {} impl NuCompleter {} impl NuCompleter { - pub fn complete( + pub fn complete( &self, line: &str, pos: usize, - context: &completion::CompletionContext, + context: &Context, ) -> (usize, Vec) { - use completion::engine::LocationType; + use engine::LocationType; - let nu_context: &EvaluationContext = context.as_ref(); + let tokens = nu_parser::lex(line, 0).0; - nu_context.scope.enter_scope(); - let (block, _) = nu_parser::parse(line, 0, &nu_context.scope); - nu_context.scope.exit_scope(); - - let locations = completion::engine::completion_location(line, &block, pos); + let locations = Some(nu_parser::parse_block(tokens).0) + .map(|block| nu_parser::classify_block(&block, context.signature_registry())) + .map(|(block, _)| engine::completion_location(line, &block, pos)) + .unwrap_or_default(); let matcher = nu_data::config::config(Tag::unknown()) .ok() @@ -61,7 +60,6 @@ impl NuCompleter { pos = location.span.start(); } } - let suggestions = locations .into_iter() .flat_map(|location| { @@ -147,7 +145,16 @@ fn select_directory_suggestions(completed_paths: Vec) -> Vec String { - let value: Cow = rustyline::completion::unescape(&orig_value, Some('\\')); + let value: Cow = { + #[cfg(feature = "rustyline-support")] + { + rustyline::completion::unescape(&orig_value, Some('\\')) + } + #[cfg(not(feature = "rustyline-support"))] + { + orig_value.into() + } + }; let mut quotes = vec!['"', '\'']; let mut should_quote = false; diff --git a/crates/nu-cli/src/completion/engine.rs b/crates/nu-completion/src/engine.rs similarity index 99% rename from crates/nu-cli/src/completion/engine.rs rename to crates/nu-completion/src/engine.rs index da790d042..69eb72f23 100644 --- a/crates/nu-cli/src/completion/engine.rs +++ b/crates/nu-completion/src/engine.rs @@ -301,6 +301,10 @@ mod tests { } impl ParserScope for VecRegistry { + fn get_names(&self) -> Vec { + self.0.iter().cloned().map(|s| s.name).collect() + } + fn has_signature(&self, name: &str) -> bool { self.0.iter().any(|v| v.name == name) } @@ -331,8 +335,6 @@ mod tests { mod completion_location { use super::*; - use nu_parser::ParserScope; - fn completion_location( line: &str, scope: &dyn ParserScope, diff --git a/crates/nu-cli/src/completion/flag.rs b/crates/nu-completion/src/flag.rs similarity index 62% rename from crates/nu-cli/src/completion/flag.rs rename to crates/nu-completion/src/flag.rs index e4d707ef2..1458b724c 100644 --- a/crates/nu-cli/src/completion/flag.rs +++ b/crates/nu-completion/src/flag.rs @@ -1,22 +1,16 @@ use super::matchers::Matcher; -use crate::completion::{Completer, CompletionContext, Suggestion}; -use nu_engine::EvaluationContext; +use crate::{Completer, CompletionContext, Suggestion}; pub struct FlagCompleter { pub(crate) cmd: String, } -impl Completer for FlagCompleter { - fn complete( - &self, - ctx: &CompletionContext<'_>, - partial: &str, - matcher: &dyn Matcher, - ) -> Vec { - let context: &EvaluationContext = ctx.as_ref(); - - if let Some(cmd) = context.scope.get_command(&self.cmd) { - let sig = cmd.signature(); +impl Completer for FlagCompleter +where + Context: CompletionContext, +{ + fn complete(&self, ctx: &Context, partial: &str, matcher: &dyn Matcher) -> Vec { + if let Some(sig) = ctx.signature_registry().get_signature(&self.cmd) { let mut suggestions = Vec::new(); for (name, (named_type, _desc)) in sig.named.iter() { suggestions.push(format!("--{}", name)); diff --git a/crates/nu-completion/src/lib.rs b/crates/nu-completion/src/lib.rs new file mode 100644 index 000000000..590c978e7 --- /dev/null +++ b/crates/nu-completion/src/lib.rs @@ -0,0 +1,24 @@ +pub(crate) mod command; +pub(crate) mod completer; +pub(crate) mod engine; +pub(crate) mod flag; +pub(crate) mod matchers; +pub(crate) mod path; + +use matchers::Matcher; + +pub use completer::NuCompleter; + +#[derive(Debug, Eq, PartialEq)] +pub struct Suggestion { + pub display: String, + pub replacement: String, +} + +pub trait CompletionContext { + fn signature_registry(&self) -> &dyn nu_parser::ParserScope; +} + +pub trait Completer { + fn complete(&self, ctx: &Context, partial: &str, matcher: &dyn Matcher) -> Vec; +} diff --git a/crates/nu-cli/src/completion/matchers/case_insensitive.rs b/crates/nu-completion/src/matchers/case_insensitive.rs similarity index 93% rename from crates/nu-cli/src/completion/matchers/case_insensitive.rs rename to crates/nu-completion/src/matchers/case_insensitive.rs index c240bb9f2..e9f3d9551 100644 --- a/crates/nu-cli/src/completion/matchers/case_insensitive.rs +++ b/crates/nu-completion/src/matchers/case_insensitive.rs @@ -1,4 +1,5 @@ -use crate::completion::matchers; +use crate::matchers; + pub struct Matcher; impl matchers::Matcher for Matcher { @@ -12,7 +13,7 @@ impl matchers::Matcher for Matcher { mod tests { use super::*; - // TODO: check some Unicode matches if this becomes relevant + // TODO: check some unicode matches if this becomes relevant // FIXME: could work exhaustively through ['-', '--'. ''] in a loop for each test #[test] diff --git a/crates/nu-cli/src/completion/matchers/case_sensitive.rs b/crates/nu-completion/src/matchers/case_sensitive.rs similarity index 95% rename from crates/nu-cli/src/completion/matchers/case_sensitive.rs rename to crates/nu-completion/src/matchers/case_sensitive.rs index 552afba0a..6a00aac6d 100644 --- a/crates/nu-cli/src/completion/matchers/case_sensitive.rs +++ b/crates/nu-completion/src/matchers/case_sensitive.rs @@ -1,4 +1,4 @@ -use crate::completion::matchers; +use crate::matchers; pub struct Matcher; diff --git a/crates/nu-cli/src/completion/matchers/mod.rs b/crates/nu-completion/src/matchers/mod.rs similarity index 100% rename from crates/nu-cli/src/completion/matchers/mod.rs rename to crates/nu-completion/src/matchers/mod.rs diff --git a/crates/nu-cli/src/completion/path.rs b/crates/nu-completion/src/path.rs similarity index 91% rename from crates/nu-cli/src/completion/path.rs rename to crates/nu-completion/src/path.rs index 0d0866b95..b5247f765 100644 --- a/crates/nu-cli/src/completion/path.rs +++ b/crates/nu-completion/src/path.rs @@ -1,7 +1,7 @@ use std::path::PathBuf; use super::matchers::Matcher; -use crate::completion::{Completer, CompletionContext, Suggestion}; +use crate::{Completer, CompletionContext, Suggestion}; const SEP: char = std::path::MAIN_SEPARATOR; @@ -74,13 +74,11 @@ impl PathCompleter { } } -impl Completer for PathCompleter { - fn complete( - &self, - _ctx: &CompletionContext<'_>, - partial: &str, - matcher: &dyn Matcher, - ) -> Vec { +impl Completer for PathCompleter +where + Context: CompletionContext, +{ + fn complete(&self, _ctx: &Context, partial: &str, matcher: &dyn Matcher) -> Vec { self.path_suggestions(partial, matcher) .into_iter() .map(|ps| ps.suggestion) diff --git a/crates/nu-engine/src/evaluate/scope.rs b/crates/nu-engine/src/evaluate/scope.rs index 028de8d14..c4b0d8eef 100644 --- a/crates/nu-engine/src/evaluate/scope.rs +++ b/crates/nu-engine/src/evaluate/scope.rs @@ -319,6 +319,10 @@ impl Scope { } impl ParserScope for Scope { + fn get_names(&self) -> Vec { + self.get_command_names() + } + fn get_signature(&self, name: &str) -> Option { self.get_command(name).map(|x| x.signature()) } diff --git a/crates/nu-parser/src/scope.rs b/crates/nu-parser/src/scope.rs index a112e163c..cc8a4564a 100644 --- a/crates/nu-parser/src/scope.rs +++ b/crates/nu-parser/src/scope.rs @@ -3,6 +3,8 @@ use nu_source::Spanned; use std::{fmt::Debug, sync::Arc}; pub trait ParserScope: Debug { + fn get_names(&self) -> Vec; + fn get_signature(&self, name: &str) -> Option; fn has_signature(&self, name: &str) -> bool;