mirror of
https://github.com/nushell/nushell.git
synced 2025-03-13 15:08:43 +01:00
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:
parent
fcd1d59abd
commit
be508cbd7f
@ -17,23 +17,15 @@ impl Completer for AttributeCompletion {
|
||||
&mut self,
|
||||
working_set: &StateWorkingSet,
|
||||
_stack: &Stack,
|
||||
_prefix: &[u8],
|
||||
prefix: impl AsRef<str>,
|
||||
span: Span,
|
||||
offset: usize,
|
||||
_pos: usize,
|
||||
options: &CompletionOptions,
|
||||
) -> Vec<SemanticSuggestion> {
|
||||
let partial = working_set.get_span_contents(span);
|
||||
let mut matcher = NuMatcher::new(String::from_utf8_lossy(partial), options.clone());
|
||||
let mut matcher = NuMatcher::new(prefix, options);
|
||||
|
||||
let attr_commands = working_set.find_commands_by_predicate(
|
||||
|s| {
|
||||
s.strip_prefix(b"attr ")
|
||||
.map(String::from_utf8_lossy)
|
||||
.is_some_and(|name| matcher.matches(&name))
|
||||
},
|
||||
true,
|
||||
);
|
||||
let attr_commands =
|
||||
working_set.find_commands_by_predicate(|s| s.starts_with(b"attr "), true);
|
||||
|
||||
for (name, desc, ty) in attr_commands {
|
||||
let name = name.strip_prefix(b"attr ").unwrap_or(&name);
|
||||
@ -62,14 +54,12 @@ impl Completer for AttributableCompletion {
|
||||
&mut self,
|
||||
working_set: &StateWorkingSet,
|
||||
_stack: &Stack,
|
||||
_prefix: &[u8],
|
||||
prefix: impl AsRef<str>,
|
||||
span: Span,
|
||||
offset: usize,
|
||||
_pos: usize,
|
||||
options: &CompletionOptions,
|
||||
) -> Vec<SemanticSuggestion> {
|
||||
let partial = working_set.get_span_contents(span);
|
||||
let mut matcher = NuMatcher::new(String::from_utf8_lossy(partial), options.clone());
|
||||
let mut matcher = NuMatcher::new(prefix, options);
|
||||
|
||||
for s in ["def", "extern", "export def", "export extern"] {
|
||||
let decl_id = working_set
|
||||
|
@ -12,10 +12,9 @@ pub trait Completer {
|
||||
&mut self,
|
||||
working_set: &StateWorkingSet,
|
||||
stack: &Stack,
|
||||
prefix: &[u8],
|
||||
prefix: impl AsRef<str>,
|
||||
span: Span,
|
||||
offset: usize,
|
||||
pos: usize,
|
||||
options: &CompletionOptions,
|
||||
) -> Vec<SemanticSuggestion>;
|
||||
}
|
||||
|
@ -19,10 +19,9 @@ impl Completer for CellPathCompletion<'_> {
|
||||
&mut self,
|
||||
working_set: &StateWorkingSet,
|
||||
stack: &Stack,
|
||||
_prefix: &[u8],
|
||||
_prefix: impl AsRef<str>,
|
||||
_span: Span,
|
||||
offset: usize,
|
||||
_pos: usize,
|
||||
options: &CompletionOptions,
|
||||
) -> Vec<SemanticSuggestion> {
|
||||
// empty tail is already handled as variable names completion
|
||||
@ -42,7 +41,7 @@ impl Completer for CellPathCompletion<'_> {
|
||||
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
|
||||
let value = if let Expr::Var(var_id) = self.full_cell_path.head.expr {
|
||||
|
@ -4,9 +4,8 @@ use crate::{
|
||||
completions::{Completer, CompletionOptions},
|
||||
SuggestionKind,
|
||||
};
|
||||
use nu_parser::FlatShape;
|
||||
use nu_protocol::{
|
||||
engine::{CachedFile, Stack, StateWorkingSet},
|
||||
engine::{Stack, StateWorkingSet},
|
||||
Span,
|
||||
};
|
||||
use reedline::Suggestion;
|
||||
@ -14,24 +13,13 @@ use reedline::Suggestion;
|
||||
use super::{completion_options::NuMatcher, SemanticSuggestion};
|
||||
|
||||
pub struct CommandCompletion {
|
||||
flattened: Vec<(Span, FlatShape)>,
|
||||
flat_shape: FlatShape,
|
||||
force_completion_after_space: bool,
|
||||
/// Whether to include internal commands
|
||||
pub internals: bool,
|
||||
/// Whether to include external commands
|
||||
pub externals: bool,
|
||||
}
|
||||
|
||||
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(
|
||||
&self,
|
||||
working_set: &StateWorkingSet,
|
||||
@ -71,6 +59,9 @@ impl CommandCompletion {
|
||||
if suggs.contains_key(&value) {
|
||||
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 there's an internal command with the same name, adds ^cmd to the
|
||||
// matcher so that both the internal and external command are included
|
||||
@ -97,46 +88,50 @@ impl CommandCompletion {
|
||||
|
||||
suggs
|
||||
}
|
||||
}
|
||||
|
||||
fn complete_commands(
|
||||
&self,
|
||||
impl Completer for CommandCompletion {
|
||||
fn fetch(
|
||||
&mut self,
|
||||
working_set: &StateWorkingSet,
|
||||
_stack: &Stack,
|
||||
prefix: impl AsRef<str>,
|
||||
span: Span,
|
||||
offset: usize,
|
||||
find_externals: bool,
|
||||
options: &CompletionOptions,
|
||||
) -> Vec<SemanticSuggestion> {
|
||||
let partial = working_set.get_span_contents(span);
|
||||
let mut matcher = NuMatcher::new(String::from_utf8_lossy(partial), options.clone());
|
||||
let mut matcher = NuMatcher::new(prefix, options);
|
||||
|
||||
let sugg_span = reedline::Span::new(span.start - offset, span.end - offset);
|
||||
|
||||
let mut internal_suggs = HashMap::new();
|
||||
let filtered_commands = working_set.find_commands_by_predicate(
|
||||
|name| {
|
||||
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)),
|
||||
if self.internals {
|
||||
let filtered_commands = working_set.find_commands_by_predicate(
|
||||
|name| {
|
||||
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)),
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let mut external_suggs = if find_externals {
|
||||
let mut external_suggs = if self.externals {
|
||||
self.external_command_completion(
|
||||
working_set,
|
||||
sugg_span,
|
||||
@ -159,179 +154,3 @@ impl CommandCompletion {
|
||||
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
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,12 +3,11 @@ use crate::completions::{
|
||||
CompletionOptions, CustomCompletion, DirectoryCompletion, DotNuCompletion, FileCompletion,
|
||||
FlagCompletion, OperatorCompletion, VariableCompletion,
|
||||
};
|
||||
use log::debug;
|
||||
use nu_color_config::{color_record_to_nustyle, lookup_ansi_color_style};
|
||||
use nu_engine::eval_block;
|
||||
use nu_parser::{flatten_expression, parse, FlatShape};
|
||||
use nu_parser::{flatten_expression, parse};
|
||||
use nu_protocol::{
|
||||
ast::{Expr, Expression, FindMapResult, Traverse},
|
||||
ast::{Argument, Expr, Expression, FindMapResult, Traverse},
|
||||
debugger::WithoutDebug,
|
||||
engine::{Closure, EngineState, Stack, StateWorkingSet},
|
||||
PipelineData, Span, Value,
|
||||
@ -22,7 +21,7 @@ use super::base::{SemanticSuggestion, SuggestionKind};
|
||||
///
|
||||
/// returns the inner-most pipeline_element of interest
|
||||
/// 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,
|
||||
working_set: &'a StateWorkingSet,
|
||||
pos: usize,
|
||||
@ -41,7 +40,6 @@ fn find_pipeline_element_by_position<'a>(
|
||||
.or(Some(expr))
|
||||
.map(FindMapResult::Found)
|
||||
.unwrap_or_default(),
|
||||
// TODO: clear separation of internal/external completion logic
|
||||
Expr::ExternalCall(head, arguments) => arguments
|
||||
.iter()
|
||||
.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)
|
||||
}
|
||||
|
||||
/// 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)]
|
||||
pub struct NuCompleter {
|
||||
engine_state: Arc<EngineState>,
|
||||
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 {
|
||||
pub fn new(engine_state: Arc<EngineState>, stack: Arc<Stack>) -> Self {
|
||||
Self {
|
||||
@ -100,7 +143,245 @@ impl NuCompleter {
|
||||
}
|
||||
|
||||
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(
|
||||
@ -113,27 +394,68 @@ impl NuCompleter {
|
||||
if !prefix.starts_with(b"$") {
|
||||
return vec![];
|
||||
}
|
||||
let mut variable_names_completer = VariableCompletion {};
|
||||
self.process_completion(
|
||||
&mut variable_names_completer,
|
||||
working_set,
|
||||
prefix,
|
||||
new_span,
|
||||
offset,
|
||||
// pos is not required
|
||||
0,
|
||||
)
|
||||
let ctx = Context::new(working_set, new_span, prefix, offset);
|
||||
self.process_completion(&mut VariableCompletion, &ctx)
|
||||
}
|
||||
|
||||
fn command_completion_helper(
|
||||
&self,
|
||||
working_set: &StateWorkingSet,
|
||||
span: Span,
|
||||
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
|
||||
fn process_completion<T: Completer>(
|
||||
&self,
|
||||
completer: &mut T,
|
||||
working_set: &StateWorkingSet,
|
||||
prefix: &[u8],
|
||||
new_span: Span,
|
||||
offset: usize,
|
||||
pos: usize,
|
||||
ctx: &Context,
|
||||
) -> Vec<SemanticSuggestion> {
|
||||
let config = self.engine_state.get_config();
|
||||
|
||||
@ -144,18 +466,12 @@ impl NuCompleter {
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
debug!(
|
||||
"process_completion: prefix: {}, new_span: {new_span:?}, offset: {offset}, pos: {pos}",
|
||||
String::from_utf8_lossy(prefix)
|
||||
);
|
||||
|
||||
completer.fetch(
|
||||
working_set,
|
||||
ctx.working_set,
|
||||
&self.stack,
|
||||
prefix,
|
||||
new_span,
|
||||
offset,
|
||||
pos,
|
||||
String::from_utf8_lossy(ctx.prefix),
|
||||
ctx.span,
|
||||
ctx.offset,
|
||||
&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 = ¤t_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 {
|
||||
fn complete(&mut self, line: &str, pos: usize) -> Vec<Suggestion> {
|
||||
self.completion_helper(line, pos)
|
||||
self.fetch_completions_at(line, pos)
|
||||
.into_iter()
|
||||
.map(|s| s.suggestion)
|
||||
.collect()
|
||||
@ -656,7 +658,7 @@ mod completer_tests {
|
||||
("ls | sudo m", true, "m", vec!["mv", "mut", "move"]),
|
||||
];
|
||||
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
|
||||
assert_eq!(!result.is_empty(), has_result, "line: {}", line);
|
||||
|
||||
|
@ -51,7 +51,7 @@ fn complete_rec(
|
||||
}
|
||||
|
||||
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 {
|
||||
let mut path = built.cwd.clone();
|
||||
@ -315,12 +315,12 @@ pub struct AdjustView {
|
||||
}
|
||||
|
||||
pub fn adjust_if_intermediate(
|
||||
prefix: &[u8],
|
||||
prefix: &str,
|
||||
working_set: &StateWorkingSet,
|
||||
mut span: nu_protocol::Span,
|
||||
) -> AdjustView {
|
||||
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.
|
||||
// Using .chars().count() because unicode and Windows.
|
||||
|
@ -25,8 +25,8 @@ pub enum MatchAlgorithm {
|
||||
Fuzzy,
|
||||
}
|
||||
|
||||
pub struct NuMatcher<T> {
|
||||
options: CompletionOptions,
|
||||
pub struct NuMatcher<'a, T> {
|
||||
options: &'a CompletionOptions,
|
||||
needle: String,
|
||||
state: State<T>,
|
||||
}
|
||||
@ -45,11 +45,11 @@ enum State<T> {
|
||||
}
|
||||
|
||||
/// Filters and sorts suggestions
|
||||
impl<T> NuMatcher<T> {
|
||||
impl<T> NuMatcher<'_, T> {
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `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());
|
||||
match options.match_algorithm {
|
||||
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 {
|
||||
let value = sugg.suggestion.value.to_string();
|
||||
self.add(value, sugg)
|
||||
@ -271,7 +271,7 @@ mod test {
|
||||
match_algorithm,
|
||||
..Default::default()
|
||||
};
|
||||
let mut matcher = NuMatcher::new(needle, options);
|
||||
let mut matcher = NuMatcher::new(needle, &options);
|
||||
matcher.add(haystack, haystack);
|
||||
if should_match {
|
||||
assert_eq!(vec![haystack], matcher.results());
|
||||
@ -286,7 +286,7 @@ mod test {
|
||||
match_algorithm: MatchAlgorithm::Fuzzy,
|
||||
..Default::default()
|
||||
};
|
||||
let mut matcher = NuMatcher::new("fob", options);
|
||||
let mut matcher = NuMatcher::new("fob", &options);
|
||||
for item in ["foo/bar", "fob", "foo bar"] {
|
||||
matcher.add(item, item);
|
||||
}
|
||||
@ -300,7 +300,7 @@ mod test {
|
||||
match_algorithm: MatchAlgorithm::Fuzzy,
|
||||
..Default::default()
|
||||
};
|
||||
let mut matcher = NuMatcher::new("'love spaces' ", options);
|
||||
let mut matcher = NuMatcher::new("'love spaces' ", &options);
|
||||
for item in [
|
||||
"'i love spaces'",
|
||||
"'i love spaces' so much",
|
||||
|
@ -13,18 +13,18 @@ use std::collections::HashMap;
|
||||
use super::completion_options::NuMatcher;
|
||||
|
||||
pub struct CustomCompletion<T: Completer> {
|
||||
stack: Stack,
|
||||
decl_id: DeclId,
|
||||
line: String,
|
||||
line_pos: usize,
|
||||
fallback: 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 {
|
||||
stack,
|
||||
decl_id,
|
||||
line,
|
||||
line_pos,
|
||||
fallback,
|
||||
}
|
||||
}
|
||||
@ -35,19 +35,16 @@ impl<T: Completer> Completer for CustomCompletion<T> {
|
||||
&mut self,
|
||||
working_set: &StateWorkingSet,
|
||||
stack: &Stack,
|
||||
prefix: &[u8],
|
||||
prefix: impl AsRef<str>,
|
||||
span: Span,
|
||||
offset: usize,
|
||||
pos: usize,
|
||||
orig_options: &CompletionOptions,
|
||||
) -> Vec<SemanticSuggestion> {
|
||||
// Line position
|
||||
let line_pos = pos - offset;
|
||||
|
||||
// Call custom declaration
|
||||
let mut stack_mut = stack.clone();
|
||||
let result = eval_call::<WithoutDebug>(
|
||||
working_set.permanent_state,
|
||||
&mut self.stack,
|
||||
&mut stack_mut,
|
||||
&Call {
|
||||
decl_id: self.decl_id,
|
||||
head: span,
|
||||
@ -58,7 +55,7 @@ impl<T: Completer> Completer for CustomCompletion<T> {
|
||||
Type::String,
|
||||
)),
|
||||
Argument::Positional(Expression::new_unknown(
|
||||
Expr::Int(line_pos as i64),
|
||||
Expr::Int(self.line_pos as i64),
|
||||
Span::unknown(),
|
||||
Type::Int,
|
||||
)),
|
||||
@ -120,7 +117,6 @@ impl<T: Completer> Completer for CustomCompletion<T> {
|
||||
prefix,
|
||||
span,
|
||||
offset,
|
||||
pos,
|
||||
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 {
|
||||
for sugg in suggestions {
|
||||
|
@ -11,27 +11,20 @@ use std::path::Path;
|
||||
|
||||
use super::{completion_common::FileSuggestion, SemanticSuggestion};
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
pub struct DirectoryCompletion {}
|
||||
|
||||
impl DirectoryCompletion {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
}
|
||||
pub struct DirectoryCompletion;
|
||||
|
||||
impl Completer for DirectoryCompletion {
|
||||
fn fetch(
|
||||
&mut self,
|
||||
working_set: &StateWorkingSet,
|
||||
stack: &Stack,
|
||||
prefix: &[u8],
|
||||
prefix: impl AsRef<str>,
|
||||
span: Span,
|
||||
offset: usize,
|
||||
_pos: usize,
|
||||
options: &CompletionOptions,
|
||||
) -> 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
|
||||
#[allow(deprecated)]
|
||||
|
@ -12,27 +12,19 @@ use std::{
|
||||
|
||||
use super::{SemanticSuggestion, SuggestionKind};
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
pub struct DotNuCompletion {}
|
||||
|
||||
impl DotNuCompletion {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
}
|
||||
pub struct DotNuCompletion;
|
||||
|
||||
impl Completer for DotNuCompletion {
|
||||
fn fetch(
|
||||
&mut self,
|
||||
working_set: &StateWorkingSet,
|
||||
stack: &Stack,
|
||||
prefix: &[u8],
|
||||
prefix: impl AsRef<str>,
|
||||
span: Span,
|
||||
offset: usize,
|
||||
_pos: usize,
|
||||
options: &CompletionOptions,
|
||||
) -> 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 end_with_backquote = prefix_str.ends_with('`');
|
||||
let prefix_str = prefix_str.replace('`', "");
|
||||
|
@ -11,31 +11,23 @@ use std::path::Path;
|
||||
|
||||
use super::{completion_common::FileSuggestion, SemanticSuggestion};
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
pub struct FileCompletion {}
|
||||
|
||||
impl FileCompletion {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
}
|
||||
pub struct FileCompletion;
|
||||
|
||||
impl Completer for FileCompletion {
|
||||
fn fetch(
|
||||
&mut self,
|
||||
working_set: &StateWorkingSet,
|
||||
stack: &Stack,
|
||||
prefix: &[u8],
|
||||
prefix: impl AsRef<str>,
|
||||
span: Span,
|
||||
offset: usize,
|
||||
_pos: usize,
|
||||
options: &CompletionOptions,
|
||||
) -> Vec<SemanticSuggestion> {
|
||||
let AdjustView {
|
||||
prefix,
|
||||
span,
|
||||
readjusted,
|
||||
} = adjust_if_intermediate(prefix, working_set, span);
|
||||
} = adjust_if_intermediate(prefix.as_ref(), working_set, span);
|
||||
|
||||
#[allow(deprecated)]
|
||||
let items: Vec<_> = complete_item(
|
||||
|
@ -1,8 +1,7 @@
|
||||
use crate::completions::{completion_options::NuMatcher, Completer, CompletionOptions};
|
||||
use nu_protocol::{
|
||||
ast::{Expr, Expression},
|
||||
engine::{Stack, StateWorkingSet},
|
||||
Span,
|
||||
DeclId, Span,
|
||||
};
|
||||
use reedline::Suggestion;
|
||||
|
||||
@ -10,13 +9,7 @@ use super::SemanticSuggestion;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct FlagCompletion {
|
||||
expression: Expression,
|
||||
}
|
||||
|
||||
impl FlagCompletion {
|
||||
pub fn new(expression: Expression) -> Self {
|
||||
Self { expression }
|
||||
}
|
||||
pub decl_id: DeclId,
|
||||
}
|
||||
|
||||
impl Completer for FlagCompletion {
|
||||
@ -24,69 +17,43 @@ impl Completer for FlagCompletion {
|
||||
&mut self,
|
||||
working_set: &StateWorkingSet,
|
||||
_stack: &Stack,
|
||||
prefix: &[u8],
|
||||
prefix: impl AsRef<str>,
|
||||
span: Span,
|
||||
offset: usize,
|
||||
_pos: usize,
|
||||
options: &CompletionOptions,
|
||||
) -> Vec<SemanticSuggestion> {
|
||||
// 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 matcher = NuMatcher::new(String::from_utf8_lossy(prefix), options.clone());
|
||||
|
||||
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'-');
|
||||
|
||||
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()
|
||||
let mut matcher = NuMatcher::new(prefix, options);
|
||||
let mut add_suggestion = |value: String, description: String| {
|
||||
matcher.add_semantic_suggestion(SemanticSuggestion {
|
||||
suggestion: Suggestion {
|
||||
value,
|
||||
description: Some(description),
|
||||
span: reedline::Span {
|
||||
start: span.start - offset,
|
||||
end: span.end - offset,
|
||||
},
|
||||
// TODO????
|
||||
kind: None,
|
||||
});
|
||||
append_whitespace: true,
|
||||
..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());
|
||||
}
|
||||
|
||||
vec![]
|
||||
matcher.results()
|
||||
}
|
||||
}
|
||||
|
@ -9,36 +9,22 @@ use nu_protocol::{
|
||||
use reedline::Suggestion;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct OperatorCompletion {
|
||||
previous_expr: Expression,
|
||||
pub struct OperatorCompletion<'a> {
|
||||
pub left_hand_side: &'a Expression,
|
||||
}
|
||||
|
||||
impl OperatorCompletion {
|
||||
pub fn new(previous_expr: Expression) -> Self {
|
||||
OperatorCompletion { previous_expr }
|
||||
}
|
||||
}
|
||||
|
||||
impl Completer for OperatorCompletion {
|
||||
impl Completer for OperatorCompletion<'_> {
|
||||
fn fetch(
|
||||
&mut self,
|
||||
working_set: &StateWorkingSet,
|
||||
_stack: &Stack,
|
||||
_prefix: &[u8],
|
||||
prefix: impl AsRef<str>,
|
||||
span: Span,
|
||||
offset: usize,
|
||||
_pos: usize,
|
||||
options: &CompletionOptions,
|
||||
) -> Vec<SemanticSuggestion> {
|
||||
//Check if int, float, or string
|
||||
let partial = std::str::from_utf8(working_set.get_span_contents(span)).unwrap_or("");
|
||||
let op = match &self.previous_expr.expr {
|
||||
Expr::BinaryOp(x, _, _) => &x.expr,
|
||||
_ => {
|
||||
return vec![];
|
||||
}
|
||||
};
|
||||
let possible_operations = match op {
|
||||
let possible_operations = match &self.left_hand_side.expr {
|
||||
Expr::Int(_) => vec![
|
||||
("+", "Add (Plus)"),
|
||||
("-", "Subtract (Minus)"),
|
||||
@ -121,7 +107,7 @@ impl Completer for OperatorCompletion {
|
||||
_ => vec![],
|
||||
};
|
||||
|
||||
let mut matcher = NuMatcher::new(partial, options.clone());
|
||||
let mut matcher = NuMatcher::new(prefix, options);
|
||||
for (symbol, desc) in possible_operations.into_iter() {
|
||||
matcher.add_semantic_suggestion(SemanticSuggestion {
|
||||
suggestion: Suggestion {
|
||||
|
@ -7,21 +7,19 @@ use reedline::Suggestion;
|
||||
|
||||
use super::completion_options::NuMatcher;
|
||||
|
||||
pub struct VariableCompletion {}
|
||||
pub struct VariableCompletion;
|
||||
|
||||
impl Completer for VariableCompletion {
|
||||
fn fetch(
|
||||
&mut self,
|
||||
working_set: &StateWorkingSet,
|
||||
_stack: &Stack,
|
||||
prefix: &[u8],
|
||||
prefix: impl AsRef<str>,
|
||||
span: Span,
|
||||
offset: usize,
|
||||
_pos: usize,
|
||||
options: &CompletionOptions,
|
||||
) -> Vec<SemanticSuggestion> {
|
||||
let prefix_str = String::from_utf8_lossy(prefix);
|
||||
let mut matcher = NuMatcher::new(prefix_str, options.clone());
|
||||
let mut matcher = NuMatcher::new(prefix, options);
|
||||
let current_span = reedline::Span {
|
||||
start: span.start - offset,
|
||||
end: span.end - offset,
|
||||
|
@ -14,7 +14,9 @@ use nu_protocol::{debugger::WithoutDebug, engine::StateWorkingSet, PipelineData}
|
||||
use reedline::{Completer, Suggestion};
|
||||
use rstest::{fixture, rstest};
|
||||
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,
|
||||
};
|
||||
|
||||
@ -292,6 +294,105 @@ fn customcompletions_fallback() {
|
||||
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
|
||||
#[test]
|
||||
fn customcompletions_invalid() {
|
||||
@ -307,6 +408,25 @@ fn customcompletions_invalid() {
|
||||
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]
|
||||
fn dotnu_completions() {
|
||||
// Create a new engine
|
||||
@ -315,6 +435,15 @@ fn dotnu_completions() {
|
||||
// Instantiate a new completer
|
||||
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
|
||||
#[cfg(windows)]
|
||||
let completion_str = "use `.\\dir_module\\".to_string();
|
||||
@ -486,6 +615,17 @@ fn external_completer_fallback() {
|
||||
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
|
||||
#[test]
|
||||
fn external_completer_invalid() {
|
||||
|
@ -14,7 +14,7 @@ fn create_default_context() -> EngineState {
|
||||
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) {
|
||||
// Target folder inside assets
|
||||
let dir = fs::fixtures().join("completions");
|
||||
@ -69,7 +69,26 @@ pub fn new_engine() -> (AbsolutePathBuf, String, EngineState, 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) {
|
||||
// Target folder inside assets
|
||||
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)
|
||||
}
|
||||
|
||||
// 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>) {
|
||||
let expected_len = expected.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()
|
||||
.map(|it| it.value.clone())
|
||||
.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 {
|
||||
let mut converted_path = file(path);
|
||||
converted_path.push(MAIN_SEPARATOR);
|
||||
converted_path
|
||||
}
|
||||
|
||||
// convert a given path to string
|
||||
/// convert a given path to string
|
||||
pub fn file(path: impl Into<PathBuf>) -> String {
|
||||
path.into().into_os_string().into_string().unwrap()
|
||||
}
|
||||
|
||||
// merge_input executes the given input into the engine
|
||||
// and merges the state
|
||||
/// merge_input executes the given input into the engine
|
||||
/// and merges the state
|
||||
pub fn merge_input(
|
||||
input: &[u8],
|
||||
engine_state: &mut EngineState,
|
||||
|
0
tests/fixtures/dotnu_completions/dir_module/plain.txt
vendored
Normal file
0
tests/fixtures/dotnu_completions/dir_module/plain.txt
vendored
Normal file
0
tests/fixtures/external_completions/path/sleep
vendored
Executable file
0
tests/fixtures/external_completions/path/sleep
vendored
Executable file
0
tests/fixtures/external_completions/path/sleep.exe
vendored
Normal file
0
tests/fixtures/external_completions/path/sleep.exe
vendored
Normal file
Loading…
Reference in New Issue
Block a user