mirror of
https://github.com/nushell/nushell.git
synced 2025-05-05 15:32:56 +00: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,
|
&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
|
||||||
|
@ -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>;
|
||||||
}
|
}
|
||||||
|
@ -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 {
|
||||||
|
@ -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
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -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 = ¤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 {
|
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);
|
||||||
|
|
||||||
|
@ -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.
|
||||||
|
@ -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",
|
||||||
|
@ -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 {
|
||||||
|
@ -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)]
|
||||||
|
@ -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('`', "");
|
||||||
|
@ -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(
|
||||||
|
@ -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![]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 {
|
||||||
|
@ -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,
|
||||||
|
@ -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() {
|
||||||
|
@ -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,
|
||||||
|
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…
x
Reference in New Issue
Block a user