refactor(completion): flatten_shape -> expression for internal/external/operator (#15086)

# Description

Fixes #14852

As the completion rules are somehow intertwined between internals and
externals,
this PR is relatively messy, and has larger probability to break things,
@fdncred @ysthakur @sholderbach
But I strongly believe this is a better direction to go. Edge cases
should be easier to fix in the dedicated branches.

There're no flattened expression based completion rules left.

# User-Facing Changes

# Tests + Formatting
+7
# After Submitting

---------

Co-authored-by: Yash Thakur <45539777+ysthakur@users.noreply.github.com>
This commit is contained in:
zc he 2025-02-24 02:47:49 +08:00 committed by GitHub
parent fcd1d59abd
commit be508cbd7f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 635 additions and 743 deletions

View File

@ -17,23 +17,15 @@ impl Completer for AttributeCompletion {
&mut self, &mut self,
working_set: &StateWorkingSet, working_set: &StateWorkingSet,
_stack: &Stack, _stack: &Stack,
_prefix: &[u8], prefix: impl AsRef<str>,
span: Span, span: Span,
offset: usize, offset: usize,
_pos: usize,
options: &CompletionOptions, options: &CompletionOptions,
) -> Vec<SemanticSuggestion> { ) -> Vec<SemanticSuggestion> {
let partial = working_set.get_span_contents(span); let mut matcher = NuMatcher::new(prefix, options);
let mut matcher = NuMatcher::new(String::from_utf8_lossy(partial), options.clone());
let attr_commands = working_set.find_commands_by_predicate( let attr_commands =
|s| { working_set.find_commands_by_predicate(|s| s.starts_with(b"attr "), true);
s.strip_prefix(b"attr ")
.map(String::from_utf8_lossy)
.is_some_and(|name| matcher.matches(&name))
},
true,
);
for (name, desc, ty) in attr_commands { for (name, desc, ty) in attr_commands {
let name = name.strip_prefix(b"attr ").unwrap_or(&name); let name = name.strip_prefix(b"attr ").unwrap_or(&name);
@ -62,14 +54,12 @@ impl Completer for AttributableCompletion {
&mut self, &mut self,
working_set: &StateWorkingSet, working_set: &StateWorkingSet,
_stack: &Stack, _stack: &Stack,
_prefix: &[u8], prefix: impl AsRef<str>,
span: Span, span: Span,
offset: usize, offset: usize,
_pos: usize,
options: &CompletionOptions, options: &CompletionOptions,
) -> Vec<SemanticSuggestion> { ) -> Vec<SemanticSuggestion> {
let partial = working_set.get_span_contents(span); let mut matcher = NuMatcher::new(prefix, options);
let mut matcher = NuMatcher::new(String::from_utf8_lossy(partial), options.clone());
for s in ["def", "extern", "export def", "export extern"] { for s in ["def", "extern", "export def", "export extern"] {
let decl_id = working_set let decl_id = working_set

View File

@ -12,10 +12,9 @@ pub trait Completer {
&mut self, &mut self,
working_set: &StateWorkingSet, working_set: &StateWorkingSet,
stack: &Stack, stack: &Stack,
prefix: &[u8], prefix: impl AsRef<str>,
span: Span, span: Span,
offset: usize, offset: usize,
pos: usize,
options: &CompletionOptions, options: &CompletionOptions,
) -> Vec<SemanticSuggestion>; ) -> Vec<SemanticSuggestion>;
} }

View File

@ -19,10 +19,9 @@ impl Completer for CellPathCompletion<'_> {
&mut self, &mut self,
working_set: &StateWorkingSet, working_set: &StateWorkingSet,
stack: &Stack, stack: &Stack,
_prefix: &[u8], _prefix: impl AsRef<str>,
_span: Span, _span: Span,
offset: usize, offset: usize,
_pos: usize,
options: &CompletionOptions, options: &CompletionOptions,
) -> Vec<SemanticSuggestion> { ) -> Vec<SemanticSuggestion> {
// empty tail is already handled as variable names completion // empty tail is already handled as variable names completion
@ -42,7 +41,7 @@ impl Completer for CellPathCompletion<'_> {
end: true_end - offset, end: true_end - offset,
}; };
let mut matcher = NuMatcher::new(prefix_str, options.clone()); let mut matcher = NuMatcher::new(prefix_str, options);
// evaluate the head expression to get its value // evaluate the head expression to get its value
let value = if let Expr::Var(var_id) = self.full_cell_path.head.expr { let value = if let Expr::Var(var_id) = self.full_cell_path.head.expr {

View File

@ -4,9 +4,8 @@ use crate::{
completions::{Completer, CompletionOptions}, completions::{Completer, CompletionOptions},
SuggestionKind, SuggestionKind,
}; };
use nu_parser::FlatShape;
use nu_protocol::{ use nu_protocol::{
engine::{CachedFile, Stack, StateWorkingSet}, engine::{Stack, StateWorkingSet},
Span, Span,
}; };
use reedline::Suggestion; use reedline::Suggestion;
@ -14,24 +13,13 @@ use reedline::Suggestion;
use super::{completion_options::NuMatcher, SemanticSuggestion}; use super::{completion_options::NuMatcher, SemanticSuggestion};
pub struct CommandCompletion { pub struct CommandCompletion {
flattened: Vec<(Span, FlatShape)>, /// Whether to include internal commands
flat_shape: FlatShape, pub internals: bool,
force_completion_after_space: bool, /// Whether to include external commands
pub externals: bool,
} }
impl CommandCompletion { impl CommandCompletion {
pub fn new(
flattened: Vec<(Span, FlatShape)>,
flat_shape: FlatShape,
force_completion_after_space: bool,
) -> Self {
Self {
flattened,
flat_shape,
force_completion_after_space,
}
}
fn external_command_completion( fn external_command_completion(
&self, &self,
working_set: &StateWorkingSet, working_set: &StateWorkingSet,
@ -71,6 +59,9 @@ impl CommandCompletion {
if suggs.contains_key(&value) { if suggs.contains_key(&value) {
continue; continue;
} }
// TODO: check name matching before a relative heavy IO involved
// `is_executable` for performance consideration, should avoid
// duplicated `match_aux` call for matched items in the future
if matcher.matches(&name) && is_executable::is_executable(item.path()) { if matcher.matches(&name) && is_executable::is_executable(item.path()) {
// If there's an internal command with the same name, adds ^cmd to the // If there's an internal command with the same name, adds ^cmd to the
// matcher so that both the internal and external command are included // matcher so that both the internal and external command are included
@ -97,46 +88,50 @@ impl CommandCompletion {
suggs suggs
} }
}
fn complete_commands( impl Completer for CommandCompletion {
&self, fn fetch(
&mut self,
working_set: &StateWorkingSet, working_set: &StateWorkingSet,
_stack: &Stack,
prefix: impl AsRef<str>,
span: Span, span: Span,
offset: usize, offset: usize,
find_externals: bool,
options: &CompletionOptions, options: &CompletionOptions,
) -> Vec<SemanticSuggestion> { ) -> Vec<SemanticSuggestion> {
let partial = working_set.get_span_contents(span); let mut matcher = NuMatcher::new(prefix, options);
let mut matcher = NuMatcher::new(String::from_utf8_lossy(partial), options.clone());
let sugg_span = reedline::Span::new(span.start - offset, span.end - offset); let sugg_span = reedline::Span::new(span.start - offset, span.end - offset);
let mut internal_suggs = HashMap::new(); let mut internal_suggs = HashMap::new();
let filtered_commands = working_set.find_commands_by_predicate( if self.internals {
|name| { let filtered_commands = working_set.find_commands_by_predicate(
let name = String::from_utf8_lossy(name); |name| {
matcher.add(&name, name.to_string()) let name = String::from_utf8_lossy(name);
}, matcher.add(&name, name.to_string())
true,
);
for (name, description, typ) in filtered_commands {
let name = String::from_utf8_lossy(&name);
internal_suggs.insert(
name.to_string(),
SemanticSuggestion {
suggestion: Suggestion {
value: name.to_string(),
description,
span: sugg_span,
append_whitespace: true,
..Suggestion::default()
},
kind: Some(SuggestionKind::Command(typ)),
}, },
true,
); );
for (name, description, typ) in filtered_commands {
let name = String::from_utf8_lossy(&name);
internal_suggs.insert(
name.to_string(),
SemanticSuggestion {
suggestion: Suggestion {
value: name.to_string(),
description,
span: sugg_span,
append_whitespace: true,
..Suggestion::default()
},
kind: Some(SuggestionKind::Command(typ)),
},
);
}
} }
let mut external_suggs = if find_externals { let mut external_suggs = if self.externals {
self.external_command_completion( self.external_command_completion(
working_set, working_set,
sugg_span, sugg_span,
@ -159,179 +154,3 @@ impl CommandCompletion {
res res
} }
} }
impl Completer for CommandCompletion {
fn fetch(
&mut self,
working_set: &StateWorkingSet,
_stack: &Stack,
_prefix: &[u8],
span: Span,
offset: usize,
pos: usize,
options: &CompletionOptions,
) -> Vec<SemanticSuggestion> {
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();
// 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::new(last.0.start, pos),
offset,
false,
options,
)
} else {
vec![]
};
if !subcommands.is_empty() {
return subcommands;
}
let config = working_set.get_config();
if matches!(self.flat_shape, nu_parser::FlatShape::External)
|| matches!(self.flat_shape, nu_parser::FlatShape::InternalCall(_))
|| ((span.end - span.start) == 0)
|| is_passthrough_command(working_set.delta.get_file_contents())
{
// we're in a gap or at a command
if working_set.get_span_contents(span).is_empty() && !self.force_completion_after_space
{
return vec![];
}
self.complete_commands(
working_set,
span,
offset,
config.completions.external.enable,
options,
)
} else {
vec![]
}
}
}
pub fn find_non_whitespace_index(contents: &[u8], start: usize) -> usize {
match contents.get(start..) {
Some(contents) => {
contents
.iter()
.take_while(|x| x.is_ascii_whitespace())
.count()
+ start
}
None => start,
}
}
pub fn is_passthrough_command(working_set_file_contents: &[CachedFile]) -> bool {
for cached_file in working_set_file_contents {
let contents = &cached_file.content;
let last_pipe_pos_rev = contents.iter().rev().position(|x| x == &b'|');
let last_pipe_pos = last_pipe_pos_rev.map(|x| contents.len() - x).unwrap_or(0);
let cur_pos = find_non_whitespace_index(contents, last_pipe_pos);
let result = match contents.get(cur_pos..) {
Some(contents) => contents.starts_with(b"sudo ") || contents.starts_with(b"doas "),
None => false,
};
if result {
return true;
}
}
false
}
#[cfg(test)]
mod command_completions_tests {
use super::*;
use nu_protocol::engine::EngineState;
use std::sync::Arc;
#[test]
fn test_find_non_whitespace_index() {
let commands = [
(" hello", 4),
("sudo ", 0),
(" sudo ", 2),
(" sudo ", 2),
(" hello ", 1),
(" hello ", 3),
(" hello | sudo ", 4),
(" sudo|sudo", 5),
("sudo | sudo ", 0),
(" hello sud", 1),
];
for (idx, ele) in commands.iter().enumerate() {
let index = find_non_whitespace_index(ele.0.as_bytes(), 0);
assert_eq!(index, ele.1, "Failed on index {}", idx);
}
}
#[test]
fn test_is_last_command_passthrough() {
let commands = [
(" hello", false),
(" sudo ", true),
("sudo ", true),
(" hello", false),
(" sudo", false),
(" sudo ", true),
(" sudo ", true),
(" sudo ", true),
(" hello ", false),
(" hello | sudo ", true),
(" sudo|sudo", false),
("sudo | sudo ", true),
(" hello sud", false),
(" sudo | sud ", false),
(" sudo|sudo ", true),
(" sudo | sudo ls | sudo ", true),
];
for (idx, ele) in commands.iter().enumerate() {
let input = ele.0.as_bytes();
let mut engine_state = EngineState::new();
engine_state.add_file("test.nu".into(), Arc::new([]));
let delta = {
let mut working_set = StateWorkingSet::new(&engine_state);
let _ = working_set.add_file("child.nu".into(), input);
working_set.render()
};
let result = engine_state.merge_delta(delta);
assert!(
result.is_ok(),
"Merge delta has failed: {}",
result.err().unwrap()
);
let is_passthrough_command = is_passthrough_command(engine_state.get_file_contents());
assert_eq!(
is_passthrough_command, ele.1,
"index for '{}': {}",
ele.0, idx
);
}
}
}

View File

@ -3,12 +3,11 @@ use crate::completions::{
CompletionOptions, CustomCompletion, DirectoryCompletion, DotNuCompletion, FileCompletion, CompletionOptions, CustomCompletion, DirectoryCompletion, DotNuCompletion, FileCompletion,
FlagCompletion, OperatorCompletion, VariableCompletion, FlagCompletion, OperatorCompletion, VariableCompletion,
}; };
use log::debug;
use nu_color_config::{color_record_to_nustyle, lookup_ansi_color_style}; use nu_color_config::{color_record_to_nustyle, lookup_ansi_color_style};
use nu_engine::eval_block; use nu_engine::eval_block;
use nu_parser::{flatten_expression, parse, FlatShape}; use nu_parser::{flatten_expression, parse};
use nu_protocol::{ use nu_protocol::{
ast::{Expr, Expression, FindMapResult, Traverse}, ast::{Argument, Expr, Expression, FindMapResult, Traverse},
debugger::WithoutDebug, debugger::WithoutDebug,
engine::{Closure, EngineState, Stack, StateWorkingSet}, engine::{Closure, EngineState, Stack, StateWorkingSet},
PipelineData, Span, Value, PipelineData, Span, Value,
@ -22,7 +21,7 @@ use super::base::{SemanticSuggestion, SuggestionKind};
/// ///
/// returns the inner-most pipeline_element of interest /// returns the inner-most pipeline_element of interest
/// i.e. the one that contains given position and needs completion /// i.e. the one that contains given position and needs completion
fn find_pipeline_element_by_position<'a>( pub fn find_pipeline_element_by_position<'a>(
expr: &'a Expression, expr: &'a Expression,
working_set: &'a StateWorkingSet, working_set: &'a StateWorkingSet,
pos: usize, pos: usize,
@ -41,7 +40,6 @@ fn find_pipeline_element_by_position<'a>(
.or(Some(expr)) .or(Some(expr))
.map(FindMapResult::Found) .map(FindMapResult::Found)
.unwrap_or_default(), .unwrap_or_default(),
// TODO: clear separation of internal/external completion logic
Expr::ExternalCall(head, arguments) => arguments Expr::ExternalCall(head, arguments) => arguments
.iter() .iter()
.find_map(|arg| arg.expr().find_map(working_set, &closure)) .find_map(|arg| arg.expr().find_map(working_set, &closure))
@ -85,12 +83,57 @@ fn strip_placeholder<'a>(working_set: &'a StateWorkingSet, span: &Span) -> (Span
(new_span, prefix) (new_span, prefix)
} }
/// Given a span with noise,
/// 1. Call `rsplit` to get the last token
/// 2. Strip the last placeholder from the token
fn strip_placeholder_with_rsplit<'a>(
working_set: &'a StateWorkingSet,
span: &Span,
predicate: impl FnMut(&u8) -> bool,
) -> (Span, &'a [u8]) {
let span_content = working_set.get_span_contents(*span);
let mut prefix = span_content
.rsplit(predicate)
.next()
.unwrap_or(span_content);
let start = span.end.saturating_sub(prefix.len());
if !prefix.is_empty() {
prefix = &prefix[..prefix.len() - 1];
}
let end = start + prefix.len();
(Span::new(start, end), prefix)
}
#[derive(Clone)] #[derive(Clone)]
pub struct NuCompleter { pub struct NuCompleter {
engine_state: Arc<EngineState>, engine_state: Arc<EngineState>,
stack: Stack, stack: Stack,
} }
/// Common arguments required for Completer
struct Context<'a> {
working_set: &'a StateWorkingSet<'a>,
span: Span,
prefix: &'a [u8],
offset: usize,
}
impl Context<'_> {
fn new<'a>(
working_set: &'a StateWorkingSet,
span: Span,
prefix: &'a [u8],
offset: usize,
) -> Context<'a> {
Context {
working_set,
span,
prefix,
offset,
}
}
}
impl NuCompleter { impl NuCompleter {
pub fn new(engine_state: Arc<EngineState>, stack: Arc<Stack>) -> Self { pub fn new(engine_state: Arc<EngineState>, stack: Arc<Stack>) -> Self {
Self { Self {
@ -100,7 +143,245 @@ impl NuCompleter {
} }
pub fn fetch_completions_at(&mut self, line: &str, pos: usize) -> Vec<SemanticSuggestion> { pub fn fetch_completions_at(&mut self, line: &str, pos: usize) -> Vec<SemanticSuggestion> {
self.completion_helper(line, pos) let mut working_set = StateWorkingSet::new(&self.engine_state);
let offset = working_set.next_span_start();
// TODO: Callers should be trimming the line themselves
let line = if line.len() > pos { &line[..pos] } else { line };
// Adjust offset so that the spans of the suggestions will start at the right
// place even with `only_buffer_difference: true`
let pos = offset + pos;
let block = parse(
&mut working_set,
Some("completer"),
// Add a placeholder `a` to the end
format!("{}a", line).as_bytes(),
false,
);
let Some(element_expression) = block.find_map(&working_set, &|expr: &Expression| {
find_pipeline_element_by_position(expr, &working_set, pos)
}) else {
return vec![];
};
self.complete_by_expression(&working_set, element_expression, offset, pos, line)
}
/// Complete given the expression of interest
/// Usually, the expression is get from `find_pipeline_element_by_position`
///
/// # Arguments
/// * `offset` - start offset of current working_set span
/// * `pos` - cursor position, should be > offset
/// * `prefix_str` - all the text before the cursor
pub fn complete_by_expression(
&self,
working_set: &StateWorkingSet,
element_expression: &Expression,
offset: usize,
pos: usize,
prefix_str: &str,
) -> Vec<SemanticSuggestion> {
let mut suggestions: Vec<SemanticSuggestion> = vec![];
match &element_expression.expr {
Expr::Var(_) => {
return self.variable_names_completion_helper(
working_set,
element_expression.span,
offset,
);
}
Expr::FullCellPath(full_cell_path) => {
// e.g. `$e<tab>` parsed as FullCellPath
if full_cell_path.tail.is_empty() {
return self.variable_names_completion_helper(
working_set,
element_expression.span,
offset,
);
} else {
let mut cell_path_completer = CellPathCompletion { full_cell_path };
let ctx = Context::new(working_set, Span::unknown(), &[], offset);
return self.process_completion(&mut cell_path_completer, &ctx);
}
}
Expr::BinaryOp(lhs, op, _) => {
if op.span.contains(pos) {
let mut operator_completions = OperatorCompletion {
left_hand_side: lhs.as_ref(),
};
let (new_span, prefix) = strip_placeholder(working_set, &op.span);
let ctx = Context::new(working_set, new_span, prefix, offset);
let results = self.process_completion(&mut operator_completions, &ctx);
if !results.is_empty() {
return results;
}
}
}
Expr::AttributeBlock(ab) => {
if let Some(span) = ab.attributes.iter().find_map(|attr| {
let span = attr.expr.span;
span.contains(pos).then_some(span)
}) {
let (new_span, prefix) = strip_placeholder(working_set, &span);
let ctx = Context::new(working_set, new_span, prefix, offset);
return self.process_completion(&mut AttributeCompletion, &ctx);
};
let span = ab.item.span;
if span.contains(pos) {
let (new_span, prefix) = strip_placeholder(working_set, &span);
let ctx = Context::new(working_set, new_span, prefix, offset);
return self.process_completion(&mut AttributableCompletion, &ctx);
}
}
// NOTE: user defined internal commands can have any length
// e.g. `def "foo -f --ff bar"`, complete by line text
// instead of relying on the parsing result in that case
Expr::Call(_) | Expr::ExternalCall(_, _) => {
let need_externals = !prefix_str.contains(' ');
let need_internals = !prefix_str.starts_with('^');
let mut span = element_expression.span;
if !need_internals {
span = Span::new(span.start + 1, span.end)
};
suggestions.extend(self.command_completion_helper(
working_set,
span,
offset,
need_internals,
need_externals,
))
}
_ => (),
}
// unfinished argument completion for commands
match &element_expression.expr {
Expr::Call(call) => {
// TODO: the argument to complete won't necessarily be the last one in the future
// for lsp completion, we won't trim the text,
// so that `def`s after pos can be completed
for arg in call.arguments.iter() {
let span = arg.span();
if span.contains(pos) {
// if customized completion specified, it has highest priority
if let Some(decl_id) = arg.expr().and_then(|e| e.custom_completion) {
// for `--foo <tab>` and `--foo=<tab>`, the arg span should be trimmed
let (new_span, prefix) = if matches!(arg, Argument::Named(_)) {
strip_placeholder_with_rsplit(working_set, &span, |b| {
*b == b'=' || *b == b' '
})
} else {
strip_placeholder(working_set, &span)
};
let ctx = Context::new(working_set, new_span, prefix, offset);
let mut completer = CustomCompletion::new(
decl_id,
prefix_str.into(),
pos - offset,
FileCompletion,
);
suggestions.extend(self.process_completion(&mut completer, &ctx));
break;
}
// normal arguments completion
let (new_span, prefix) = strip_placeholder(working_set, &span);
let ctx = Context::new(working_set, new_span, prefix, offset);
suggestions.extend(match arg {
// flags
Argument::Named(_) | Argument::Unknown(_)
if prefix.starts_with(b"-") =>
{
let mut flag_completions = FlagCompletion {
decl_id: call.decl_id,
};
self.process_completion(&mut flag_completions, &ctx)
}
// complete according to expression type and command head
Argument::Positional(expr) => {
let command_head = working_set.get_span_contents(call.head);
self.argument_completion_helper(
command_head,
expr,
&ctx,
suggestions.is_empty(),
)
}
_ => vec![],
});
break;
}
}
}
Expr::ExternalCall(head, arguments) => {
for (i, arg) in arguments.iter().enumerate() {
let span = arg.expr().span;
if span.contains(pos) {
// e.g. `sudo l<tab>`
// HACK: judge by index 0 is not accurate
if i == 0 {
let external_cmd = working_set.get_span_contents(head.span);
if external_cmd == b"sudo" || external_cmd == b"doas" {
let commands = self.command_completion_helper(
working_set,
span,
offset,
true,
true,
);
// flags of sudo/doas can still be completed by external completer
if !commands.is_empty() {
return commands;
}
}
}
// resort to external completer set in config
let config = self.engine_state.get_config();
if let Some(closure) = config.completions.external.completer.as_ref() {
let mut text_spans: Vec<String> =
flatten_expression(working_set, element_expression)
.iter()
.map(|(span, _)| {
let bytes = working_set.get_span_contents(*span);
String::from_utf8_lossy(bytes).to_string()
})
.collect();
// strip the placeholder
if let Some(last) = text_spans.last_mut() {
last.pop();
}
if let Some(external_result) = self.external_completion(
closure,
&text_spans,
offset,
Span::new(span.start, span.end.saturating_sub(1)),
) {
suggestions.extend(external_result);
return suggestions;
}
}
break;
}
}
}
_ => (),
}
// if no suggestions yet, fallback to file completion
if suggestions.is_empty() {
let (new_span, prefix) =
strip_placeholder_with_rsplit(working_set, &element_expression.span, |c| {
*c == b' '
});
let ctx = Context::new(working_set, new_span, prefix, offset);
suggestions.extend(self.process_completion(&mut FileCompletion, &ctx));
}
suggestions
} }
fn variable_names_completion_helper( fn variable_names_completion_helper(
@ -113,27 +394,68 @@ impl NuCompleter {
if !prefix.starts_with(b"$") { if !prefix.starts_with(b"$") {
return vec![]; return vec![];
} }
let mut variable_names_completer = VariableCompletion {}; let ctx = Context::new(working_set, new_span, prefix, offset);
self.process_completion( self.process_completion(&mut VariableCompletion, &ctx)
&mut variable_names_completer, }
working_set,
prefix, fn command_completion_helper(
new_span, &self,
offset, working_set: &StateWorkingSet,
// pos is not required span: Span,
0, offset: usize,
) internals: bool,
externals: bool,
) -> Vec<SemanticSuggestion> {
let mut command_completions = CommandCompletion {
internals,
externals,
};
let (new_span, prefix) = strip_placeholder(working_set, &span);
let ctx = Context::new(working_set, new_span, prefix, offset);
self.process_completion(&mut command_completions, &ctx)
}
fn argument_completion_helper(
&self,
command_head: &[u8],
expr: &Expression,
ctx: &Context,
need_fallback: bool,
) -> Vec<SemanticSuggestion> {
// special commands
match command_head {
// complete module file/directory
// TODO: if module file already specified,
// should parse it to get modules/commands/consts to complete
b"use" | b"export use" | b"overlay use" | b"source-env" => {
return self.process_completion(&mut DotNuCompletion, ctx);
}
b"which" => {
let mut completer = CommandCompletion {
internals: true,
externals: true,
};
return self.process_completion(&mut completer, ctx);
}
_ => (),
}
// general positional arguments
let file_completion_helper = || self.process_completion(&mut FileCompletion, ctx);
match &expr.expr {
Expr::Directory(_, _) => self.process_completion(&mut DirectoryCompletion, ctx),
Expr::Filepath(_, _) | Expr::GlobPattern(_, _) => file_completion_helper(),
// fallback to file completion if necessary
_ if need_fallback => file_completion_helper(),
_ => vec![],
}
} }
// Process the completion for a given completer // Process the completion for a given completer
fn process_completion<T: Completer>( fn process_completion<T: Completer>(
&self, &self,
completer: &mut T, completer: &mut T,
working_set: &StateWorkingSet, ctx: &Context,
prefix: &[u8],
new_span: Span,
offset: usize,
pos: usize,
) -> Vec<SemanticSuggestion> { ) -> Vec<SemanticSuggestion> {
let config = self.engine_state.get_config(); let config = self.engine_state.get_config();
@ -144,18 +466,12 @@ impl NuCompleter {
..Default::default() ..Default::default()
}; };
debug!(
"process_completion: prefix: {}, new_span: {new_span:?}, offset: {offset}, pos: {pos}",
String::from_utf8_lossy(prefix)
);
completer.fetch( completer.fetch(
working_set, ctx.working_set,
&self.stack, &self.stack,
prefix, String::from_utf8_lossy(ctx.prefix),
new_span, ctx.span,
offset, ctx.offset,
pos,
&options, &options,
) )
} }
@ -215,325 +531,11 @@ impl NuCompleter {
} }
} }
} }
fn completion_helper(&mut self, line: &str, pos: usize) -> Vec<SemanticSuggestion> {
let mut working_set = StateWorkingSet::new(&self.engine_state);
let offset = working_set.next_span_start();
// TODO: Callers should be trimming the line themselves
let line = if line.len() > pos { &line[..pos] } else { line };
// Adjust offset so that the spans of the suggestions will start at the right
// place even with `only_buffer_difference: true`
let fake_offset = offset + line.len() - pos;
let pos = offset + line.len();
let initial_line = line.to_string();
let mut line = line.to_string();
line.push('a');
let config = self.engine_state.get_config();
let block = parse(&mut working_set, Some("completer"), line.as_bytes(), false);
let Some(element_expression) = block.find_map(&working_set, &|expr: &Expression| {
find_pipeline_element_by_position(expr, &working_set, pos)
}) else {
return vec![];
};
match &element_expression.expr {
Expr::Var(_) => {
return self.variable_names_completion_helper(
&working_set,
element_expression.span,
fake_offset,
);
}
Expr::FullCellPath(full_cell_path) => {
// e.g. `$e<tab>` parsed as FullCellPath
if full_cell_path.tail.is_empty() {
return self.variable_names_completion_helper(
&working_set,
element_expression.span,
fake_offset,
);
} else {
let mut cell_path_completer = CellPathCompletion { full_cell_path };
return self.process_completion(
&mut cell_path_completer,
&working_set,
&[],
element_expression.span,
fake_offset,
pos,
);
}
}
_ => (),
}
let flattened = flatten_expression(&working_set, element_expression);
let mut spans: Vec<String> = vec![];
for (flat_idx, (span, shape)) in flattened.iter().enumerate() {
let is_passthrough_command = spans
.first()
.filter(|content| content.as_str() == "sudo" || content.as_str() == "doas")
.is_some();
// Read the current span to string
let current_span = working_set.get_span_contents(*span);
let current_span_str = String::from_utf8_lossy(current_span);
let is_last_span = span.contains(pos);
// Skip the last 'a' as span item
if is_last_span {
let offset = pos - span.start;
if offset == 0 {
spans.push(String::new())
} else {
let mut current_span_str = current_span_str.to_string();
current_span_str.remove(offset);
spans.push(current_span_str);
}
} else {
spans.push(current_span_str.to_string());
}
// Complete based on the last span
if is_last_span {
// Create a new span
let new_span = Span::new(span.start, span.end - 1);
// Parses the prefix. Completion should look up to the cursor position, not after.
let index = pos - span.start;
let prefix = &current_span[..index];
if let Expr::AttributeBlock(ab) = &element_expression.expr {
let last_attr = ab.attributes.last().expect("at least one attribute");
if let Expr::Garbage = last_attr.expr.expr {
return self.process_completion(
&mut AttributeCompletion,
&working_set,
prefix,
new_span,
fake_offset,
pos,
);
} else {
return self.process_completion(
&mut AttributableCompletion,
&working_set,
prefix,
new_span,
fake_offset,
pos,
);
}
}
// Flags completion
if prefix.starts_with(b"-") {
// Try to complete flag internally
let mut completer = FlagCompletion::new(element_expression.clone());
let result = self.process_completion(
&mut completer,
&working_set,
prefix,
new_span,
fake_offset,
pos,
);
if !result.is_empty() {
return result;
}
// We got no results for internal completion
// now we can check if external completer is set and use it
if let Some(closure) = config.completions.external.completer.as_ref() {
if let Some(external_result) =
self.external_completion(closure, &spans, fake_offset, new_span)
{
return external_result;
}
}
}
// specially check if it is currently empty - always complete commands
if (is_passthrough_command && flat_idx == 1)
|| (flat_idx == 0 && working_set.get_span_contents(new_span).is_empty())
{
let mut completer = CommandCompletion::new(
flattened.clone(),
// flat_idx,
FlatShape::String,
true,
);
return self.process_completion(
&mut completer,
&working_set,
prefix,
new_span,
fake_offset,
pos,
);
}
// Completions that depends on the previous expression (e.g: use, source-env)
if (is_passthrough_command && flat_idx > 1) || flat_idx > 0 {
if let Some(previous_expr) = flattened.get(flat_idx - 1) {
// Read the content for the previous expression
let prev_expr_str = working_set.get_span_contents(previous_expr.0).to_vec();
// Completion for .nu files
if prev_expr_str == b"use"
|| prev_expr_str == b"overlay use"
|| prev_expr_str == b"source-env"
{
let mut completer = DotNuCompletion::new();
return self.process_completion(
&mut completer,
&working_set,
prefix,
new_span,
fake_offset,
pos,
);
} else if prev_expr_str == b"ls" {
let mut completer = FileCompletion::new();
return self.process_completion(
&mut completer,
&working_set,
prefix,
new_span,
fake_offset,
pos,
);
} else if matches!(
previous_expr.1,
FlatShape::Float
| FlatShape::Int
| FlatShape::String
| FlatShape::List
| FlatShape::Bool
| FlatShape::Variable(_)
) {
let mut completer = OperatorCompletion::new(element_expression.clone());
let operator_suggestion = self.process_completion(
&mut completer,
&working_set,
prefix,
new_span,
fake_offset,
pos,
);
if !operator_suggestion.is_empty() {
return operator_suggestion;
}
}
}
}
// Match other types
match shape {
FlatShape::Custom(decl_id) => {
let mut completer = CustomCompletion::new(
self.stack.clone(),
*decl_id,
initial_line,
FileCompletion::new(),
);
return self.process_completion(
&mut completer,
&working_set,
prefix,
new_span,
fake_offset,
pos,
);
}
FlatShape::Directory => {
let mut completer = DirectoryCompletion::new();
return self.process_completion(
&mut completer,
&working_set,
prefix,
new_span,
fake_offset,
pos,
);
}
FlatShape::Filepath | FlatShape::GlobPattern => {
let mut completer = FileCompletion::new();
return self.process_completion(
&mut completer,
&working_set,
prefix,
new_span,
fake_offset,
pos,
);
}
flat_shape => {
let mut completer = CommandCompletion::new(
flattened.clone(),
// flat_idx,
flat_shape.clone(),
false,
);
let mut out: Vec<_> = self.process_completion(
&mut completer,
&working_set,
prefix,
new_span,
fake_offset,
pos,
);
if !out.is_empty() {
return out;
}
// Try to complete using an external completer (if set)
if let Some(closure) = config.completions.external.completer.as_ref() {
if let Some(external_result) =
self.external_completion(closure, &spans, fake_offset, new_span)
{
return external_result;
}
}
// Check for file completion
let mut completer = FileCompletion::new();
out = self.process_completion(
&mut completer,
&working_set,
prefix,
new_span,
fake_offset,
pos,
);
if !out.is_empty() {
return out;
}
}
};
}
}
vec![]
}
} }
impl ReedlineCompleter for NuCompleter { impl ReedlineCompleter for NuCompleter {
fn complete(&mut self, line: &str, pos: usize) -> Vec<Suggestion> { fn complete(&mut self, line: &str, pos: usize) -> Vec<Suggestion> {
self.completion_helper(line, pos) self.fetch_completions_at(line, pos)
.into_iter() .into_iter()
.map(|s| s.suggestion) .map(|s| s.suggestion)
.collect() .collect()
@ -656,7 +658,7 @@ mod completer_tests {
("ls | sudo m", true, "m", vec!["mv", "mut", "move"]), ("ls | sudo m", true, "m", vec!["mv", "mut", "move"]),
]; ];
for (line, has_result, begins_with, expected_values) in dataset { for (line, has_result, begins_with, expected_values) in dataset {
let result = completer.completion_helper(line, line.len()); let result = completer.fetch_completions_at(line, line.len());
// Test whether the result is empty or not // Test whether the result is empty or not
assert_eq!(!result.is_empty(), has_result, "line: {}", line); assert_eq!(!result.is_empty(), has_result, "line: {}", line);

View File

@ -51,7 +51,7 @@ fn complete_rec(
} }
let prefix = partial.first().unwrap_or(&""); let prefix = partial.first().unwrap_or(&"");
let mut matcher = NuMatcher::new(prefix, options.clone()); let mut matcher = NuMatcher::new(prefix, options);
for built in built_paths { for built in built_paths {
let mut path = built.cwd.clone(); let mut path = built.cwd.clone();
@ -315,12 +315,12 @@ pub struct AdjustView {
} }
pub fn adjust_if_intermediate( pub fn adjust_if_intermediate(
prefix: &[u8], prefix: &str,
working_set: &StateWorkingSet, working_set: &StateWorkingSet,
mut span: nu_protocol::Span, mut span: nu_protocol::Span,
) -> AdjustView { ) -> AdjustView {
let span_contents = String::from_utf8_lossy(working_set.get_span_contents(span)).to_string(); let span_contents = String::from_utf8_lossy(working_set.get_span_contents(span)).to_string();
let mut prefix = String::from_utf8_lossy(prefix).to_string(); let mut prefix = prefix.to_string();
// A difference of 1 because of the cursor's unicode code point in between. // A difference of 1 because of the cursor's unicode code point in between.
// Using .chars().count() because unicode and Windows. // Using .chars().count() because unicode and Windows.

View File

@ -25,8 +25,8 @@ pub enum MatchAlgorithm {
Fuzzy, Fuzzy,
} }
pub struct NuMatcher<T> { pub struct NuMatcher<'a, T> {
options: CompletionOptions, options: &'a CompletionOptions,
needle: String, needle: String,
state: State<T>, state: State<T>,
} }
@ -45,11 +45,11 @@ enum State<T> {
} }
/// Filters and sorts suggestions /// Filters and sorts suggestions
impl<T> NuMatcher<T> { impl<T> NuMatcher<'_, T> {
/// # Arguments /// # Arguments
/// ///
/// * `needle` - The text to search for /// * `needle` - The text to search for
pub fn new(needle: impl AsRef<str>, options: CompletionOptions) -> NuMatcher<T> { pub fn new(needle: impl AsRef<str>, options: &CompletionOptions) -> NuMatcher<T> {
let needle = trim_quotes_str(needle.as_ref()); let needle = trim_quotes_str(needle.as_ref());
match options.match_algorithm { match options.match_algorithm {
MatchAlgorithm::Prefix => { MatchAlgorithm::Prefix => {
@ -184,7 +184,7 @@ impl<T> NuMatcher<T> {
} }
} }
impl NuMatcher<SemanticSuggestion> { impl NuMatcher<'_, SemanticSuggestion> {
pub fn add_semantic_suggestion(&mut self, sugg: SemanticSuggestion) -> bool { pub fn add_semantic_suggestion(&mut self, sugg: SemanticSuggestion) -> bool {
let value = sugg.suggestion.value.to_string(); let value = sugg.suggestion.value.to_string();
self.add(value, sugg) self.add(value, sugg)
@ -271,7 +271,7 @@ mod test {
match_algorithm, match_algorithm,
..Default::default() ..Default::default()
}; };
let mut matcher = NuMatcher::new(needle, options); let mut matcher = NuMatcher::new(needle, &options);
matcher.add(haystack, haystack); matcher.add(haystack, haystack);
if should_match { if should_match {
assert_eq!(vec![haystack], matcher.results()); assert_eq!(vec![haystack], matcher.results());
@ -286,7 +286,7 @@ mod test {
match_algorithm: MatchAlgorithm::Fuzzy, match_algorithm: MatchAlgorithm::Fuzzy,
..Default::default() ..Default::default()
}; };
let mut matcher = NuMatcher::new("fob", options); let mut matcher = NuMatcher::new("fob", &options);
for item in ["foo/bar", "fob", "foo bar"] { for item in ["foo/bar", "fob", "foo bar"] {
matcher.add(item, item); matcher.add(item, item);
} }
@ -300,7 +300,7 @@ mod test {
match_algorithm: MatchAlgorithm::Fuzzy, match_algorithm: MatchAlgorithm::Fuzzy,
..Default::default() ..Default::default()
}; };
let mut matcher = NuMatcher::new("'love spaces' ", options); let mut matcher = NuMatcher::new("'love spaces' ", &options);
for item in [ for item in [
"'i love spaces'", "'i love spaces'",
"'i love spaces' so much", "'i love spaces' so much",

View File

@ -13,18 +13,18 @@ use std::collections::HashMap;
use super::completion_options::NuMatcher; use super::completion_options::NuMatcher;
pub struct CustomCompletion<T: Completer> { pub struct CustomCompletion<T: Completer> {
stack: Stack,
decl_id: DeclId, decl_id: DeclId,
line: String, line: String,
line_pos: usize,
fallback: T, fallback: T,
} }
impl<T: Completer> CustomCompletion<T> { impl<T: Completer> CustomCompletion<T> {
pub fn new(stack: Stack, decl_id: DeclId, line: String, fallback: T) -> Self { pub fn new(decl_id: DeclId, line: String, line_pos: usize, fallback: T) -> Self {
Self { Self {
stack,
decl_id, decl_id,
line, line,
line_pos,
fallback, fallback,
} }
} }
@ -35,19 +35,16 @@ impl<T: Completer> Completer for CustomCompletion<T> {
&mut self, &mut self,
working_set: &StateWorkingSet, working_set: &StateWorkingSet,
stack: &Stack, stack: &Stack,
prefix: &[u8], prefix: impl AsRef<str>,
span: Span, span: Span,
offset: usize, offset: usize,
pos: usize,
orig_options: &CompletionOptions, orig_options: &CompletionOptions,
) -> Vec<SemanticSuggestion> { ) -> Vec<SemanticSuggestion> {
// Line position
let line_pos = pos - offset;
// Call custom declaration // Call custom declaration
let mut stack_mut = stack.clone();
let result = eval_call::<WithoutDebug>( let result = eval_call::<WithoutDebug>(
working_set.permanent_state, working_set.permanent_state,
&mut self.stack, &mut stack_mut,
&Call { &Call {
decl_id: self.decl_id, decl_id: self.decl_id,
head: span, head: span,
@ -58,7 +55,7 @@ impl<T: Completer> Completer for CustomCompletion<T> {
Type::String, Type::String,
)), )),
Argument::Positional(Expression::new_unknown( Argument::Positional(Expression::new_unknown(
Expr::Int(line_pos as i64), Expr::Int(self.line_pos as i64),
Span::unknown(), Span::unknown(),
Type::Int, Type::Int,
)), )),
@ -120,7 +117,6 @@ impl<T: Completer> Completer for CustomCompletion<T> {
prefix, prefix,
span, span,
offset, offset,
pos,
orig_options, orig_options,
); );
} }
@ -138,7 +134,7 @@ impl<T: Completer> Completer for CustomCompletion<T> {
} }
}; };
let mut matcher = NuMatcher::new(String::from_utf8_lossy(prefix), completion_options); let mut matcher = NuMatcher::new(prefix, &completion_options);
if should_sort { if should_sort {
for sugg in suggestions { for sugg in suggestions {

View File

@ -11,27 +11,20 @@ use std::path::Path;
use super::{completion_common::FileSuggestion, SemanticSuggestion}; use super::{completion_common::FileSuggestion, SemanticSuggestion};
#[derive(Clone, Default)] pub struct DirectoryCompletion;
pub struct DirectoryCompletion {}
impl DirectoryCompletion {
pub fn new() -> Self {
Self::default()
}
}
impl Completer for DirectoryCompletion { impl Completer for DirectoryCompletion {
fn fetch( fn fetch(
&mut self, &mut self,
working_set: &StateWorkingSet, working_set: &StateWorkingSet,
stack: &Stack, stack: &Stack,
prefix: &[u8], prefix: impl AsRef<str>,
span: Span, span: Span,
offset: usize, offset: usize,
_pos: usize,
options: &CompletionOptions, options: &CompletionOptions,
) -> Vec<SemanticSuggestion> { ) -> Vec<SemanticSuggestion> {
let AdjustView { prefix, span, .. } = adjust_if_intermediate(prefix, working_set, span); let AdjustView { prefix, span, .. } =
adjust_if_intermediate(prefix.as_ref(), working_set, span);
// Filter only the folders // Filter only the folders
#[allow(deprecated)] #[allow(deprecated)]

View File

@ -12,27 +12,19 @@ use std::{
use super::{SemanticSuggestion, SuggestionKind}; use super::{SemanticSuggestion, SuggestionKind};
#[derive(Clone, Default)] pub struct DotNuCompletion;
pub struct DotNuCompletion {}
impl DotNuCompletion {
pub fn new() -> Self {
Self::default()
}
}
impl Completer for DotNuCompletion { impl Completer for DotNuCompletion {
fn fetch( fn fetch(
&mut self, &mut self,
working_set: &StateWorkingSet, working_set: &StateWorkingSet,
stack: &Stack, stack: &Stack,
prefix: &[u8], prefix: impl AsRef<str>,
span: Span, span: Span,
offset: usize, offset: usize,
_pos: usize,
options: &CompletionOptions, options: &CompletionOptions,
) -> Vec<SemanticSuggestion> { ) -> Vec<SemanticSuggestion> {
let prefix_str = String::from_utf8_lossy(prefix); let prefix_str = prefix.as_ref();
let start_with_backquote = prefix_str.starts_with('`'); let start_with_backquote = prefix_str.starts_with('`');
let end_with_backquote = prefix_str.ends_with('`'); let end_with_backquote = prefix_str.ends_with('`');
let prefix_str = prefix_str.replace('`', ""); let prefix_str = prefix_str.replace('`', "");

View File

@ -11,31 +11,23 @@ use std::path::Path;
use super::{completion_common::FileSuggestion, SemanticSuggestion}; use super::{completion_common::FileSuggestion, SemanticSuggestion};
#[derive(Clone, Default)] pub struct FileCompletion;
pub struct FileCompletion {}
impl FileCompletion {
pub fn new() -> Self {
Self::default()
}
}
impl Completer for FileCompletion { impl Completer for FileCompletion {
fn fetch( fn fetch(
&mut self, &mut self,
working_set: &StateWorkingSet, working_set: &StateWorkingSet,
stack: &Stack, stack: &Stack,
prefix: &[u8], prefix: impl AsRef<str>,
span: Span, span: Span,
offset: usize, offset: usize,
_pos: usize,
options: &CompletionOptions, options: &CompletionOptions,
) -> Vec<SemanticSuggestion> { ) -> Vec<SemanticSuggestion> {
let AdjustView { let AdjustView {
prefix, prefix,
span, span,
readjusted, readjusted,
} = adjust_if_intermediate(prefix, working_set, span); } = adjust_if_intermediate(prefix.as_ref(), working_set, span);
#[allow(deprecated)] #[allow(deprecated)]
let items: Vec<_> = complete_item( let items: Vec<_> = complete_item(

View File

@ -1,8 +1,7 @@
use crate::completions::{completion_options::NuMatcher, Completer, CompletionOptions}; use crate::completions::{completion_options::NuMatcher, Completer, CompletionOptions};
use nu_protocol::{ use nu_protocol::{
ast::{Expr, Expression},
engine::{Stack, StateWorkingSet}, engine::{Stack, StateWorkingSet},
Span, DeclId, Span,
}; };
use reedline::Suggestion; use reedline::Suggestion;
@ -10,13 +9,7 @@ use super::SemanticSuggestion;
#[derive(Clone)] #[derive(Clone)]
pub struct FlagCompletion { pub struct FlagCompletion {
expression: Expression, pub decl_id: DeclId,
}
impl FlagCompletion {
pub fn new(expression: Expression) -> Self {
Self { expression }
}
} }
impl Completer for FlagCompletion { impl Completer for FlagCompletion {
@ -24,69 +17,43 @@ impl Completer for FlagCompletion {
&mut self, &mut self,
working_set: &StateWorkingSet, working_set: &StateWorkingSet,
_stack: &Stack, _stack: &Stack,
prefix: &[u8], prefix: impl AsRef<str>,
span: Span, span: Span,
offset: usize, offset: usize,
_pos: usize,
options: &CompletionOptions, options: &CompletionOptions,
) -> Vec<SemanticSuggestion> { ) -> Vec<SemanticSuggestion> {
// Check if it's a flag let mut matcher = NuMatcher::new(prefix, options);
if let Expr::Call(call) = &self.expression.expr { let mut add_suggestion = |value: String, description: String| {
let decl = working_set.get_decl(call.decl_id); matcher.add_semantic_suggestion(SemanticSuggestion {
let sig = decl.signature(); suggestion: Suggestion {
value,
let mut matcher = NuMatcher::new(String::from_utf8_lossy(prefix), options.clone()); description: Some(description),
span: reedline::Span {
for named in &sig.named { start: span.start - offset,
let flag_desc = &named.desc; end: span.end - offset,
if let Some(short) = named.short {
let mut named = vec![0; short.len_utf8()];
short.encode_utf8(&mut named);
named.insert(0, b'-');
matcher.add_semantic_suggestion(SemanticSuggestion {
suggestion: Suggestion {
value: String::from_utf8_lossy(&named).to_string(),
description: Some(flag_desc.to_string()),
span: reedline::Span {
start: span.start - offset,
end: span.end - offset,
},
append_whitespace: true,
..Suggestion::default()
},
// TODO????
kind: None,
});
}
if named.long.is_empty() {
continue;
}
let mut named = named.long.as_bytes().to_vec();
named.insert(0, b'-');
named.insert(0, b'-');
matcher.add_semantic_suggestion(SemanticSuggestion {
suggestion: Suggestion {
value: String::from_utf8_lossy(&named).to_string(),
description: Some(flag_desc.to_string()),
span: reedline::Span {
start: span.start - offset,
end: span.end - offset,
},
append_whitespace: true,
..Suggestion::default()
}, },
// TODO???? append_whitespace: true,
kind: None, ..Suggestion::default()
}); },
// TODO????
kind: None,
});
};
let decl = working_set.get_decl(self.decl_id);
let sig = decl.signature();
for named in &sig.named {
if let Some(short) = named.short {
let mut name = String::from("-");
name.push(short);
add_suggestion(name, named.desc.clone());
} }
return matcher.results(); if named.long.is_empty() {
continue;
}
add_suggestion(format!("--{}", named.long), named.desc.clone());
} }
matcher.results()
vec![]
} }
} }

View File

@ -9,36 +9,22 @@ use nu_protocol::{
use reedline::Suggestion; use reedline::Suggestion;
#[derive(Clone)] #[derive(Clone)]
pub struct OperatorCompletion { pub struct OperatorCompletion<'a> {
previous_expr: Expression, pub left_hand_side: &'a Expression,
} }
impl OperatorCompletion { impl Completer for OperatorCompletion<'_> {
pub fn new(previous_expr: Expression) -> Self {
OperatorCompletion { previous_expr }
}
}
impl Completer for OperatorCompletion {
fn fetch( fn fetch(
&mut self, &mut self,
working_set: &StateWorkingSet, working_set: &StateWorkingSet,
_stack: &Stack, _stack: &Stack,
_prefix: &[u8], prefix: impl AsRef<str>,
span: Span, span: Span,
offset: usize, offset: usize,
_pos: usize,
options: &CompletionOptions, options: &CompletionOptions,
) -> Vec<SemanticSuggestion> { ) -> Vec<SemanticSuggestion> {
//Check if int, float, or string //Check if int, float, or string
let partial = std::str::from_utf8(working_set.get_span_contents(span)).unwrap_or(""); let possible_operations = match &self.left_hand_side.expr {
let op = match &self.previous_expr.expr {
Expr::BinaryOp(x, _, _) => &x.expr,
_ => {
return vec![];
}
};
let possible_operations = match op {
Expr::Int(_) => vec![ Expr::Int(_) => vec![
("+", "Add (Plus)"), ("+", "Add (Plus)"),
("-", "Subtract (Minus)"), ("-", "Subtract (Minus)"),
@ -121,7 +107,7 @@ impl Completer for OperatorCompletion {
_ => vec![], _ => vec![],
}; };
let mut matcher = NuMatcher::new(partial, options.clone()); let mut matcher = NuMatcher::new(prefix, options);
for (symbol, desc) in possible_operations.into_iter() { for (symbol, desc) in possible_operations.into_iter() {
matcher.add_semantic_suggestion(SemanticSuggestion { matcher.add_semantic_suggestion(SemanticSuggestion {
suggestion: Suggestion { suggestion: Suggestion {

View File

@ -7,21 +7,19 @@ use reedline::Suggestion;
use super::completion_options::NuMatcher; use super::completion_options::NuMatcher;
pub struct VariableCompletion {} pub struct VariableCompletion;
impl Completer for VariableCompletion { impl Completer for VariableCompletion {
fn fetch( fn fetch(
&mut self, &mut self,
working_set: &StateWorkingSet, working_set: &StateWorkingSet,
_stack: &Stack, _stack: &Stack,
prefix: &[u8], prefix: impl AsRef<str>,
span: Span, span: Span,
offset: usize, offset: usize,
_pos: usize,
options: &CompletionOptions, options: &CompletionOptions,
) -> Vec<SemanticSuggestion> { ) -> Vec<SemanticSuggestion> {
let prefix_str = String::from_utf8_lossy(prefix); let mut matcher = NuMatcher::new(prefix, options);
let mut matcher = NuMatcher::new(prefix_str, options.clone());
let current_span = reedline::Span { let current_span = reedline::Span {
start: span.start - offset, start: span.start - offset,
end: span.end - offset, end: span.end - offset,

View File

@ -14,7 +14,9 @@ use nu_protocol::{debugger::WithoutDebug, engine::StateWorkingSet, PipelineData}
use reedline::{Completer, Suggestion}; use reedline::{Completer, Suggestion};
use rstest::{fixture, rstest}; use rstest::{fixture, rstest};
use support::{ use support::{
completions_helpers::{new_dotnu_engine, new_partial_engine, new_quote_engine}, completions_helpers::{
new_dotnu_engine, new_external_engine, new_partial_engine, new_quote_engine,
},
file, folder, match_suggestions, new_engine, file, folder, match_suggestions, new_engine,
}; };
@ -292,6 +294,105 @@ fn customcompletions_fallback() {
match_suggestions(&expected, &suggestions); match_suggestions(&expected, &suggestions);
} }
/// Custom function arguments mixed with subcommands
#[test]
fn custom_arguments_and_subcommands() {
let (_, _, mut engine, mut stack) = new_engine();
let command = r#"
def foo [i: directory] {}
def "foo test bar" [] {}"#;
assert!(support::merge_input(command.as_bytes(), &mut engine, &mut stack).is_ok());
let mut completer = NuCompleter::new(Arc::new(engine), Arc::new(stack));
let completion_str = "foo test";
let suggestions = completer.complete(completion_str, completion_str.len());
// including both subcommand and directory completions
let expected: Vec<String> = vec!["foo test bar".into(), folder("test_a"), folder("test_b")];
match_suggestions(&expected, &suggestions);
}
/// Custom function flags mixed with subcommands
#[test]
fn custom_flags_and_subcommands() {
let (_, _, mut engine, mut stack) = new_engine();
let command = r#"
def foo [--test: directory] {}
def "foo --test bar" [] {}"#;
assert!(support::merge_input(command.as_bytes(), &mut engine, &mut stack).is_ok());
let mut completer = NuCompleter::new(Arc::new(engine), Arc::new(stack));
let completion_str = "foo --test";
let suggestions = completer.complete(completion_str, completion_str.len());
// including both flag and directory completions
let expected: Vec<String> = vec!["foo --test bar".into(), "--test".into()];
match_suggestions(&expected, &suggestions);
}
/// If argument type is something like int/string, complete only subcommands
#[test]
fn custom_arguments_vs_subcommands() {
let (_, _, mut engine, mut stack) = new_engine();
let command = r#"
def foo [i: string] {}
def "foo test bar" [] {}"#;
assert!(support::merge_input(command.as_bytes(), &mut engine, &mut stack).is_ok());
let mut completer = NuCompleter::new(Arc::new(engine), Arc::new(stack));
let completion_str = "foo test";
let suggestions = completer.complete(completion_str, completion_str.len());
// including only subcommand completions
let expected: Vec<String> = vec!["foo test bar".into()];
match_suggestions(&expected, &suggestions);
}
/// External command only if starts with `^`
#[test]
fn external_commands_only() {
let engine = new_external_engine();
let mut completer = NuCompleter::new(
Arc::new(engine),
Arc::new(nu_protocol::engine::Stack::new()),
);
let completion_str = "^sleep";
let suggestions = completer.complete(completion_str, completion_str.len());
#[cfg(windows)]
let expected: Vec<String> = vec!["sleep.exe".into()];
#[cfg(not(windows))]
let expected: Vec<String> = vec!["sleep".into()];
match_suggestions(&expected, &suggestions);
let completion_str = "sleep";
let suggestions = completer.complete(completion_str, completion_str.len());
#[cfg(windows)]
let expected: Vec<String> = vec!["sleep".into(), "sleep.exe".into()];
#[cfg(not(windows))]
let expected: Vec<String> = vec!["sleep".into(), "^sleep".into()];
match_suggestions(&expected, &suggestions);
}
/// Which completes both internals and externals
#[test]
fn which_command_completions() {
let engine = new_external_engine();
let mut completer = NuCompleter::new(
Arc::new(engine),
Arc::new(nu_protocol::engine::Stack::new()),
);
// flags
let completion_str = "which --all";
let suggestions = completer.complete(completion_str, completion_str.len());
let expected: Vec<String> = vec!["--all".into()];
match_suggestions(&expected, &suggestions);
// commands
let completion_str = "which sleep";
let suggestions = completer.complete(completion_str, completion_str.len());
#[cfg(windows)]
let expected: Vec<String> = vec!["sleep".into(), "sleep.exe".into()];
#[cfg(not(windows))]
let expected: Vec<String> = vec!["sleep".into(), "^sleep".into()];
match_suggestions(&expected, &suggestions);
}
/// Suppress completions for invalid values /// Suppress completions for invalid values
#[test] #[test]
fn customcompletions_invalid() { fn customcompletions_invalid() {
@ -307,6 +408,25 @@ fn customcompletions_invalid() {
assert!(suggestions.is_empty()); assert!(suggestions.is_empty());
} }
#[test]
fn dont_use_dotnu_completions() {
// Create a new engine
let (_, _, engine, stack) = new_dotnu_engine();
// Instantiate a new completer
let mut completer = NuCompleter::new(Arc::new(engine), Arc::new(stack));
// Test nested nu script
let completion_str = "go work use `./dir_module/".to_string();
let suggestions = completer.complete(&completion_str, completion_str.len());
// including a plaintext file
let expected: Vec<String> = vec![
"./dir_module/mod.nu".into(),
"./dir_module/plain.txt".into(),
"`./dir_module/sub module/`".into(),
];
match_suggestions(&expected, &suggestions);
}
#[test] #[test]
fn dotnu_completions() { fn dotnu_completions() {
// Create a new engine // Create a new engine
@ -315,6 +435,15 @@ fn dotnu_completions() {
// Instantiate a new completer // Instantiate a new completer
let mut completer = NuCompleter::new(Arc::new(engine), Arc::new(stack)); let mut completer = NuCompleter::new(Arc::new(engine), Arc::new(stack));
// Flags should still be working
let completion_str = "overlay use --".to_string();
let suggestions = completer.complete(&completion_str, completion_str.len());
match_suggestions(
&vec!["--help".into(), "--prefix".into(), "--reload".into()],
&suggestions,
);
// Test nested nu script // Test nested nu script
#[cfg(windows)] #[cfg(windows)]
let completion_str = "use `.\\dir_module\\".to_string(); let completion_str = "use `.\\dir_module\\".to_string();
@ -486,6 +615,17 @@ fn external_completer_fallback() {
match_suggestions(&expected, &suggestions); match_suggestions(&expected, &suggestions);
} }
/// Fallback to external completions for flags of `sudo`
#[test]
fn external_completer_sudo() {
let block = "{|spans| ['--background']}";
let input = "sudo --back".to_string();
let expected = vec!["--background".into()];
let suggestions = run_external_completion(block, &input);
match_suggestions(&expected, &suggestions);
}
/// Suppress completions when external completer returns invalid value /// Suppress completions when external completer returns invalid value
#[test] #[test]
fn external_completer_invalid() { fn external_completer_invalid() {

View File

@ -14,7 +14,7 @@ fn create_default_context() -> EngineState {
nu_command::add_shell_command_context(nu_cmd_lang::create_default_context()) nu_command::add_shell_command_context(nu_cmd_lang::create_default_context())
} }
// creates a new engine with the current path into the completions fixtures folder /// creates a new engine with the current path into the completions fixtures folder
pub fn new_engine() -> (AbsolutePathBuf, String, EngineState, Stack) { pub fn new_engine() -> (AbsolutePathBuf, String, EngineState, Stack) {
// Target folder inside assets // Target folder inside assets
let dir = fs::fixtures().join("completions"); let dir = fs::fixtures().join("completions");
@ -69,7 +69,26 @@ pub fn new_engine() -> (AbsolutePathBuf, String, EngineState, Stack) {
(dir, dir_str, engine_state, stack) (dir, dir_str, engine_state, stack)
} }
// creates a new engine with the current path into the completions fixtures folder /// Adds pseudo PATH env for external completion tests
pub fn new_external_engine() -> EngineState {
let mut engine = create_default_context();
let dir = fs::fixtures().join("external_completions").join("path");
let dir_str = dir.to_string_lossy().to_string();
let internal_span = nu_protocol::Span::new(0, dir_str.len());
engine.add_env_var(
"PATH".to_string(),
Value::List {
vals: vec![Value::String {
val: dir_str,
internal_span,
}],
internal_span,
},
);
engine
}
/// creates a new engine with the current path into the completions fixtures folder
pub fn new_dotnu_engine() -> (AbsolutePathBuf, String, EngineState, Stack) { pub fn new_dotnu_engine() -> (AbsolutePathBuf, String, EngineState, Stack) {
// Target folder inside assets // Target folder inside assets
let dir = fs::fixtures().join("dotnu_completions"); let dir = fs::fixtures().join("dotnu_completions");
@ -197,7 +216,7 @@ pub fn new_partial_engine() -> (AbsolutePathBuf, String, EngineState, Stack) {
(dir, dir_str, engine_state, stack) (dir, dir_str, engine_state, stack)
} }
// match a list of suggestions with the expected values /// match a list of suggestions with the expected values
pub fn match_suggestions(expected: &Vec<String>, suggestions: &Vec<Suggestion>) { pub fn match_suggestions(expected: &Vec<String>, suggestions: &Vec<Suggestion>) {
let expected_len = expected.len(); let expected_len = expected.len();
let suggestions_len = suggestions.len(); let suggestions_len = suggestions.len();
@ -209,28 +228,28 @@ pub fn match_suggestions(expected: &Vec<String>, suggestions: &Vec<Suggestion>)
) )
} }
let suggestoins_str = suggestions let suggestions_str = suggestions
.iter() .iter()
.map(|it| it.value.clone()) .map(|it| it.value.clone())
.collect::<Vec<_>>(); .collect::<Vec<_>>();
assert_eq!(expected, &suggestoins_str); assert_eq!(expected, &suggestions_str);
} }
// append the separator to the converted path /// append the separator to the converted path
pub fn folder(path: impl Into<PathBuf>) -> String { pub fn folder(path: impl Into<PathBuf>) -> String {
let mut converted_path = file(path); let mut converted_path = file(path);
converted_path.push(MAIN_SEPARATOR); converted_path.push(MAIN_SEPARATOR);
converted_path converted_path
} }
// convert a given path to string /// convert a given path to string
pub fn file(path: impl Into<PathBuf>) -> String { pub fn file(path: impl Into<PathBuf>) -> String {
path.into().into_os_string().into_string().unwrap() path.into().into_os_string().into_string().unwrap()
} }
// merge_input executes the given input into the engine /// merge_input executes the given input into the engine
// and merges the state /// and merges the state
pub fn merge_input( pub fn merge_input(
input: &[u8], input: &[u8],
engine_state: &mut EngineState, engine_state: &mut EngineState,

View File

View File

View File