Variable completions. (#3666)

In Nu we have variables (E.g. $var-name) and these contain `Value` types.
This means we can bind to variables any structured data and column path syntax
(E.g. `$variable.path.to`) allows flexibility for "querying" said structures.

Here we offer completions for these. For example, in a Nushell session the
variable `$nu` contains environment values among other things. If we wanted to
see in the screen some environment variable (say the var `SHELL`) we do:

```
> echo $nu.env.SHELL
```

with completions we can now do: `echo $nu.env.S[\TAB]` and we get suggestions
that start at the column path `$nu.env` with vars starting with the letter `S`
in this case `SHELL` appears in the suggestions.
This commit is contained in:
Andrés N. Robalino
2021-06-23 02:21:39 -05:00
committed by GitHub
parent 2b021472d6
commit 03c9eaf005
26 changed files with 546 additions and 134 deletions

View File

@@ -10,6 +10,7 @@ version = "0.33.0"
doctest = false
[dependencies]
nu-engine = { version="0.33.0", path="../nu-engine" }
nu-data = { version="0.33.0", path="../nu-data" }
nu-errors = { version="0.33.0", path="../nu-errors" }
nu-parser = { version="0.33.0", path="../nu-parser" }
@@ -19,5 +20,8 @@ nu-source = { version="0.33.0", path="../nu-source" }
nu-test-support = { version="0.33.0", path="../nu-test-support" }
dirs-next = "2.0.0"
indexmap = { version="1.6.1", features=["serde-1"] }
indexmap = { version = "1.6.1", features = ["serde-1"] }
is_executable = { version="1.0.1", optional=true }
[dev-dependencies]
parking_lot = "0.11.1"

View File

@@ -16,7 +16,7 @@ where
{
fn complete(&self, ctx: &Context, partial: &str, matcher: &dyn Matcher) -> Vec<Suggestion> {
let registry = ctx.signature_registry();
let mut commands: IndexSet<String> = IndexSet::from_iter(registry.get_names());
let mut commands: IndexSet<String> = IndexSet::from_iter(registry.names());
// Command suggestions can come from three possible sets:
// 1. internal command names,

View File

@@ -8,6 +8,7 @@ use crate::flag::FlagCompleter;
use crate::matchers;
use crate::matchers::Matcher;
use crate::path::{PathCompleter, PathSuggestion};
use crate::variable::VariableCompleter;
use crate::{Completer, CompletionContext, Suggestion};
pub struct NuCompleter {}
@@ -26,7 +27,7 @@ impl NuCompleter {
let tokens = nu_parser::lex(line, 0).0;
let locations = Some(nu_parser::parse_block(tokens).0)
.map(|block| nu_parser::classify_block(&block, context.signature_registry()))
.map(|block| nu_parser::classify_block(&block, context.scope()))
.map(|(block, _)| engine::completion_location(line, &block, pos))
.unwrap_or_default();
@@ -121,7 +122,10 @@ impl NuCompleter {
.collect()
}
LocationType::Variable => Vec::new(),
LocationType::Variable => {
let variable_completer = VariableCompleter;
variable_completer.complete(context, &partial, matcher.to_owned())
}
}
})
.collect();

View File

@@ -301,10 +301,6 @@ mod tests {
}
impl ParserScope for VecRegistry {
fn get_names(&self) -> Vec<String> {
self.0.iter().cloned().map(|s| s.name).collect()
}
fn has_signature(&self, name: &str) -> bool {
self.0.iter().any(|v| v.name == name)
}

View File

@@ -10,7 +10,7 @@ where
Context: CompletionContext,
{
fn complete(&self, ctx: &Context, partial: &str, matcher: &dyn Matcher) -> Vec<Suggestion> {
if let Some(sig) = ctx.signature_registry().get_signature(&self.cmd) {
if let Some(sig) = ctx.signature_registry().get(&self.cmd) {
let mut suggestions = Vec::new();
for (name, (named_type, _desc)) in sig.named.iter() {
suggestions.push(format!("--{}", name));

View File

@@ -4,6 +4,10 @@ pub(crate) mod engine;
pub(crate) mod flag;
pub(crate) mod matchers;
pub(crate) mod path;
pub(crate) mod variable;
use nu_engine::EvaluationContext;
use nu_protocol::{SignatureRegistry, VariableRegistry};
use matchers::Matcher;
@@ -15,8 +19,20 @@ pub struct Suggestion {
pub replacement: String,
}
impl Suggestion {
fn new(display: impl Into<String>, replacement: impl Into<String>) -> Self {
Self {
display: display.into(),
replacement: replacement.into(),
}
}
}
pub trait CompletionContext {
fn signature_registry(&self) -> &dyn nu_parser::ParserScope;
fn signature_registry(&self) -> &dyn SignatureRegistry;
fn scope(&self) -> &dyn nu_parser::ParserScope;
fn source(&self) -> &EvaluationContext;
fn variable_registry(&self) -> &dyn VariableRegistry;
}
pub trait Completer<Context: CompletionContext> {

View File

@@ -0,0 +1,182 @@
use nu_engine::value_shell::ValueShell;
use nu_protocol::ColumnPath;
use nu_source::SpannedItem;
use super::matchers::Matcher;
use crate::{Completer, CompletionContext, Suggestion};
use std::path::{Path, PathBuf};
fn build_path(head: &str, members: &Path, entry: &str) -> String {
let mut full_path = head.to_string();
full_path.push_str(
&members
.join(entry)
.display()
.to_string()
.replace(std::path::MAIN_SEPARATOR, "."),
);
full_path
}
fn collect_entries(value_fs: &ValueShell, head: &str, path: &Path) -> Vec<String> {
value_fs
.members_under(&path)
.iter()
.flat_map(|entry| {
entry
.row_entries()
.map(|(entry_name, _)| build_path(&head, &path, entry_name))
})
.collect()
}
pub struct VariableCompleter;
impl<Context> Completer<Context> for VariableCompleter
where
Context: CompletionContext,
{
fn complete(&self, ctx: &Context, partial: &str, matcher: &dyn Matcher) -> Vec<Suggestion> {
let registry = ctx.variable_registry();
let variables_available = registry.variables();
let partial_column_path = ColumnPath::with_head(&partial.to_string().spanned_unknown());
partial_column_path
.map(|(head, members)| {
variables_available
.iter()
.filter(|candidate| matcher.matches(&head, candidate))
.into_iter()
.filter_map(|candidate| {
if !partial.ends_with('.') && members.is_empty() {
Some(vec![candidate.to_string()])
} else {
let value = registry.get_variable(&candidate[..].spanned_unknown());
let path = PathBuf::from(members.path());
value.map(|candidate| {
let fs = ValueShell::new(candidate);
fs.find(&path)
.map(|fs| collect_entries(fs, &head, &path))
.or_else(|| {
path.parent().map(|parent| {
fs.find(parent)
.map(|fs| collect_entries(fs, &head, &parent))
.unwrap_or_default()
})
})
.unwrap_or_default()
})
}
})
.flatten()
.filter_map(|candidate| {
if matcher.matches(&partial, &candidate) {
Some(Suggestion::new(&candidate, &candidate))
} else {
None
}
})
.collect()
})
.unwrap_or_default()
}
}
#[cfg(test)]
mod tests {
use super::{Completer, Suggestion as S, VariableCompleter};
use crate::matchers::case_insensitive::Matcher as CaseInsensitiveMatcher;
use indexmap::IndexMap;
use nu_engine::{
evaluation_context::EngineState, ConfigHolder, EvaluationContext, FakeHost, Host, Scope,
ShellManager,
};
use nu_protocol::{SignatureRegistry, VariableRegistry};
use parking_lot::Mutex;
use std::ffi::OsString;
use std::sync::{atomic::AtomicBool, Arc};
struct CompletionContext<'a>(&'a EvaluationContext);
impl<'a> crate::CompletionContext for CompletionContext<'a> {
fn signature_registry(&self) -> &dyn SignatureRegistry {
&self.0.scope
}
fn source(&self) -> &nu_engine::EvaluationContext {
&self.0
}
fn scope(&self) -> &dyn nu_parser::ParserScope {
&self.0.scope
}
fn variable_registry(&self) -> &dyn VariableRegistry {
self.0
}
}
fn create_context_with_host(host: Box<dyn Host>) -> EvaluationContext {
let scope = Scope::new();
let env_vars = host.vars().iter().cloned().collect::<IndexMap<_, _>>();
scope.add_env(env_vars);
EvaluationContext {
scope,
engine_state: Arc::new(EngineState {
host: Arc::new(parking_lot::Mutex::new(host)),
current_errors: Arc::new(Mutex::new(vec![])),
ctrl_c: Arc::new(AtomicBool::new(false)),
configs: Arc::new(Mutex::new(ConfigHolder::new())),
shell_manager: ShellManager::basic(),
windows_drives_previous_cwd: Arc::new(Mutex::new(std::collections::HashMap::new())),
}),
}
}
fn set_envs(host: &mut FakeHost, values: Vec<(&str, &str)>) {
values.iter().for_each(|(key, value)| {
host.env_set(OsString::from(key), OsString::from(value));
});
}
#[test]
fn structure() {
let mut host = nu_engine::FakeHost::new();
set_envs(&mut host, vec![("COMPLETER", "VARIABLE"), ("SHELL", "NU")]);
let context = create_context_with_host(Box::new(host));
assert_eq!(
VariableCompleter {}.complete(
&CompletionContext(&context),
"$nu.env.",
&CaseInsensitiveMatcher
),
vec![
S::new("$nu.env.COMPLETER", "$nu.env.COMPLETER"),
S::new("$nu.env.SHELL", "$nu.env.SHELL")
]
);
assert_eq!(
VariableCompleter {}.complete(
&CompletionContext(&context),
"$nu.env.CO",
&CaseInsensitiveMatcher
),
vec![S::new("$nu.env.COMPLETER", "$nu.env.COMPLETER"),]
);
assert_eq!(
VariableCompleter {}.complete(
&CompletionContext(&context),
"$nu.en",
&CaseInsensitiveMatcher
),
vec![S::new("$nu.env", "$nu.env"),]
);
}
}