nu-cli: refactor completions (#5102)

This commit is contained in:
Herlon Aguiar 2022-04-06 09:58:55 +02:00 committed by GitHub
parent 121a4f06fb
commit 13869e7d52
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 1017 additions and 701 deletions

View file

@ -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<Value>,
}
impl NuCompleter {
pub fn new(engine_state: EngineState, stack: Stack, config: Option<Value>) -> Self {
Self {
engine_state,
stack,
config,
}
}
fn external_command_completion(&self, prefix: &str) -> Vec<String> {
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<Suggestion> {
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<Suggestion> {
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::<Vec<_>>();
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<Suggestion> {
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<Item = &'a Value>,
new_span: Span,
offset: usize,
) -> Vec<Suggestion> {
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<Suggestion> = 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::<Vec<_>>();
//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<Suggestion> {
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())
}

View file

@ -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<u8>,
span: Span,
offset: usize,
pos: usize,
) -> (Vec<Suggestion>, CompletionOptions);
// Filter results using the completion options
fn filter(
&self,
prefix: Vec<u8>,
items: Vec<Suggestion>,
options: CompletionOptions,
) -> Vec<Suggestion> {
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<Suggestion>,
prefix: Vec<u8>,
options: CompletionOptions,
) -> Vec<Suggestion> {
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
}
}

View file

@ -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<String> {
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<Suggestion> {
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::<Vec<_>>();
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<u8>,
span: Span,
offset: usize,
pos: usize,
) -> (Vec<Suggestion>, 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::<Vec<_>>();
(output, options)
}
}

View file

@ -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<Value>,
}
impl NuCompleter {
pub fn new(engine_state: EngineState, stack: Stack, config: Option<Value>) -> Self {
Self {
engine_state,
stack,
config,
}
}
// Process the completion for a given completer
fn process_completion<T: Completer>(
&self,
completer: &mut T,
working_set: &StateWorkingSet,
prefix: Vec<u8>,
new_span: Span,
offset: usize,
pos: usize,
) -> Vec<Suggestion> {
// 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<Suggestion> {
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<Suggestion> {
self.completion_helper(line, pos)
}
}

View file

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

View file

@ -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<Value>,
decl_id: usize,
line: String,
}
impl CustomCompletion {
pub fn new(
engine_state: EngineState,
stack: Stack,
config: Option<Value>,
decl_id: usize,
line: String,
) -> Self {
Self {
engine_state,
stack,
config,
decl_id,
line,
}
}
fn map_completions<'a>(
&self,
list: impl Iterator<Item = &'a Value>,
span: Span,
offset: usize,
) -> Vec<Suggestion> {
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<u8>,
span: Span,
offset: usize,
pos: usize,
) -> (Vec<Suggestion>, 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)
}
}

View file

@ -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<u8>,
span: Span,
offset: usize,
_: usize,
) -> (Vec<Suggestion>, 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())
}

View file

@ -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<u8>,
span: Span,
offset: usize,
_: usize,
) -> (Vec<Suggestion>, 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())
}
}

View file

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

View file

@ -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<u8>,
span: Span,
offset: usize,
_: usize,
) -> (Vec<Suggestion>, 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())
}
}

View file

@ -1,10 +1,10 @@
use crate::reedline_config::add_menus; use crate::reedline_config::add_menus;
use crate::{completions::NuCompleter, NuHighlighter, NuValidator, NushellPrompt};
use crate::{prompt_update, reedline_config}; use crate::{prompt_update, reedline_config};
use crate::{ use crate::{
reedline_config::KeybindingsMode, reedline_config::KeybindingsMode,
util::{eval_source, report_error}, util::{eval_source, report_error},
}; };
use crate::{NuCompleter, NuHighlighter, NuValidator, NushellPrompt};
use log::info; use log::info;
use log::trace; use log::trace;
use miette::{IntoDiagnostic, Result}; use miette::{IntoDiagnostic, Result};

View file

@ -3,7 +3,7 @@ use nu_protocol::DeclId;
use nu_protocol::{engine::StateWorkingSet, Span}; use nu_protocol::{engine::StateWorkingSet, Span};
use std::fmt::{Display, Formatter, Result}; use std::fmt::{Display, Formatter, Result};
#[derive(Debug, Eq, PartialEq, Ord, PartialOrd)] #[derive(Debug, Eq, PartialEq, Ord, Clone, PartialOrd)]
pub enum FlatShape { pub enum FlatShape {
Garbage, Garbage,
Nothing, Nothing,