Skip to content

Commit d96dbbd

Browse files
authored
Command-wide completions (nushell#16765)
- closes nushell#14504 - closes nushell#14806 - partially addresses nushell#5035 --- Added `attr complete external`, which enables using the external completer for `extern` and custom command `def`initions: ```nushell @complete external def --wrapped jc [...args] { ^jc ...$args | from json } ``` Added `attr complete` which uses another command (similar to the `arg: string@"command"`) to provide completions for _all arguments_ of a command: ```nushell def fish-completer [spans: list<string>] { ^fish ... } def carapace-completer [spans: list<string>] { ^carapace ... } @complete fish-completer extern git [] @complete carapace-completer def --env get-env [name] { $env | get $name } @complete carapace-completer def --env set-env [name, value] { load-env { $name: $value } } @complete carapace-completer def --env unset-env [name] { hide-env $name } ``` Also includes the following bugfix: - `spans: list<string>` argument provided to the external completer (and completer commands used with `attr complete` with this addition) previously had invalid spans (from `Span::unknown`). These values now have correct spans pointing to their location in the source code.
1 parent e351d6c commit d96dbbd

File tree

10 files changed

+659
-209
lines changed

10 files changed

+659
-209
lines changed

crates/nu-cli/src/completions/command_completions.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ use reedline::Suggestion;
1212

1313
use super::{SemanticSuggestion, completion_options::NuMatcher};
1414

15+
// TODO: Add a toggle for quoting multi word commands. Useful for: `which` and `attr complete`
1516
pub struct CommandCompletion {
1617
/// Whether to include internal commands
1718
pub internals: bool,

crates/nu-cli/src/completions/completer.rs

Lines changed: 214 additions & 204 deletions
Large diffs are not rendered by default.

crates/nu-cli/src/completions/custom_completions.rs

Lines changed: 170 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,12 @@ use crate::completions::{
33
completer::map_value_completions,
44
};
55
use nu_engine::eval_call;
6+
use nu_parser::{FlatShape, flatten_expression};
67
use nu_protocol::{
7-
DeclId, PipelineData, Span, Type, Value,
8+
BlockId, DeclId, IntoSpanned, PipelineData, ShellError, Span, Spanned, Type, Value, VarId,
89
ast::{Argument, Call, Expr, Expression},
910
debugger::WithoutDebug,
10-
engine::{EngineState, Stack, StateWorkingSet},
11+
engine::{Closure, EngineState, Stack, StateWorkingSet},
1112
};
1213
use std::collections::HashMap;
1314

@@ -166,3 +167,170 @@ impl<T: Completer> Completer for CustomCompletion<T> {
166167
}
167168
}
168169
}
170+
171+
pub fn get_command_arguments(
172+
working_set: &StateWorkingSet<'_>,
173+
element_expression: &Expression,
174+
) -> Spanned<Vec<Spanned<String>>> {
175+
let span = element_expression.span(&working_set);
176+
flatten_expression(working_set, element_expression)
177+
.iter()
178+
.map(|(span, shape)| {
179+
let bytes = working_set.get_span_contents(match shape {
180+
// Use expanded alias span
181+
FlatShape::External(span) => **span,
182+
_ => *span,
183+
});
184+
String::from_utf8_lossy(bytes)
185+
.into_owned()
186+
.into_spanned(*span)
187+
})
188+
.collect::<Vec<_>>()
189+
.into_spanned(span)
190+
}
191+
192+
pub struct CommandWideCompletion<'e> {
193+
block_id: BlockId,
194+
captures: Vec<(VarId, Value)>,
195+
expression: &'e Expression,
196+
strip: bool,
197+
pub need_fallback: bool,
198+
}
199+
200+
impl<'a> CommandWideCompletion<'a> {
201+
pub fn command(
202+
working_set: &StateWorkingSet<'_>,
203+
decl_id: DeclId,
204+
expression: &'a Expression,
205+
strip: bool,
206+
) -> Option<Self> {
207+
let block_id = (decl_id.get() < working_set.num_decls())
208+
.then(|| working_set.get_decl(decl_id))
209+
.and_then(|command| command.block_id())?;
210+
211+
Some(Self {
212+
block_id,
213+
captures: vec![],
214+
expression,
215+
strip,
216+
need_fallback: false,
217+
})
218+
}
219+
220+
pub fn closure(closure: &'a Closure, expression: &'a Expression, strip: bool) -> Self {
221+
Self {
222+
block_id: closure.block_id,
223+
captures: closure.captures.clone(),
224+
expression,
225+
strip,
226+
need_fallback: false,
227+
}
228+
}
229+
}
230+
231+
impl<'a> Completer for CommandWideCompletion<'a> {
232+
fn fetch(
233+
&mut self,
234+
working_set: &StateWorkingSet,
235+
stack: &Stack,
236+
_prefix: impl AsRef<str>,
237+
span: Span,
238+
offset: usize,
239+
_options: &CompletionOptions,
240+
) -> Vec<SemanticSuggestion> {
241+
let Spanned {
242+
item: mut args,
243+
span: args_span,
244+
} = get_command_arguments(working_set, self.expression);
245+
246+
let mut new_span = span;
247+
// strip the placeholder
248+
if self.strip
249+
&& let Some(last) = args.last_mut()
250+
{
251+
last.item.pop();
252+
new_span = Span::new(span.start, span.end.saturating_sub(1));
253+
}
254+
255+
let block = working_set.get_block(self.block_id);
256+
let mut callee_stack = stack.captures_to_stack_preserve_out_dest(self.captures.clone());
257+
258+
if let Some(pos_arg) = block.signature.required_positional.first()
259+
&& let Some(var_id) = pos_arg.var_id
260+
{
261+
callee_stack.add_var(
262+
var_id,
263+
Value::list(
264+
args.into_iter()
265+
.map(|Spanned { item, span }| Value::string(item, span))
266+
.collect(),
267+
args_span,
268+
),
269+
);
270+
}
271+
let mut engine_state = working_set.permanent_state.clone();
272+
let _ = engine_state.merge_delta(working_set.delta.clone());
273+
274+
let result = nu_engine::eval_block::<WithoutDebug>(
275+
&engine_state,
276+
&mut callee_stack,
277+
block,
278+
PipelineData::empty(),
279+
)
280+
.map(|p| p.body);
281+
282+
if let Some(results) = convert_whole_command_completion_results(offset, new_span, result) {
283+
results
284+
} else {
285+
self.need_fallback = true;
286+
vec![]
287+
}
288+
}
289+
}
290+
291+
/// Converts the output of the external completion closure and whole command custom completion
292+
/// commands'
293+
fn convert_whole_command_completion_results(
294+
offset: usize,
295+
span: Span,
296+
result: Result<PipelineData, nu_protocol::ShellError>,
297+
) -> Option<Vec<SemanticSuggestion>> {
298+
let value = match result.and_then(|pipeline_data| pipeline_data.into_value(span)) {
299+
Ok(value) => value,
300+
Err(err) => {
301+
log::error!(
302+
"{}",
303+
ShellError::GenericError {
304+
error: "nu::shell::completion".into(),
305+
msg: "failed to eval completer block".into(),
306+
span: None,
307+
help: None,
308+
inner: vec![err],
309+
}
310+
);
311+
return Some(vec![]);
312+
}
313+
};
314+
315+
match value {
316+
Value::List { vals, .. } => Some(map_value_completions(
317+
vals.iter(),
318+
Span::new(span.start, span.end),
319+
offset,
320+
)),
321+
Value::Nothing { .. } => None,
322+
_ => {
323+
log::error!(
324+
"{}",
325+
ShellError::GenericError {
326+
error: "nu::shell::completion".into(),
327+
msg: "completer returned invalid value of type".into(),
328+
span: None,
329+
help: None,
330+
inner: vec![],
331+
},
332+
);
333+
Some(vec![])
334+
}
335+
}
336+
}

crates/nu-cli/tests/completions/mod.rs

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -858,6 +858,98 @@ fn external_completer_invalid() {
858858
assert!(suggestions.is_empty());
859859
}
860860

861+
#[test]
862+
fn command_wide_completion_external() {
863+
let mut completer = custom_completer();
864+
865+
let sample = /* lang=nu */ r#"
866+
@complete external
867+
extern "gh" []
868+
869+
gh alias one two"#;
870+
871+
let suggestions = completer.complete(sample, sample.len());
872+
let expected = vec!["gh", "alias", "one", "two"];
873+
match_suggestions(&expected, &suggestions);
874+
}
875+
876+
#[test]
877+
fn command_wide_completion_custom() {
878+
let mut completer = custom_completer();
879+
880+
let sample = /* lang=nu */ r#"
881+
def "nu-complete foo" [spans: list] {
882+
$spans ++ [some more]
883+
}
884+
885+
@complete "nu-complete foo"
886+
def --wrapped "foo" [...rest] {}
887+
888+
foo bar baz"#;
889+
890+
let suggestions = completer.complete(sample, sample.len());
891+
let expected = vec!["foo", "bar", "baz", "some", "more"];
892+
match_suggestions(&expected, &suggestions);
893+
}
894+
895+
#[test]
896+
fn parameter_completion_overrides_command_wide_completion() {
897+
let mut completer = custom_completer();
898+
899+
let sample = /* lang=nu */ r#"
900+
def "nu-complete cmd" [spans: list] {
901+
[command wide completion]
902+
}
903+
904+
def "nu-complete cmd bar" [] {
905+
[bar specific]
906+
}
907+
908+
@complete "nu-complete cmd"
909+
def --wrapped "cmd" [
910+
foo: string,
911+
bar: string@"nu-complete cmd bar",
912+
...rest
913+
] {}
914+
915+
cmd one "#;
916+
917+
let suggestions = completer.complete(sample, sample.len());
918+
let expected = vec!["bar", "specific"];
919+
match_suggestions(&expected, &suggestions);
920+
}
921+
922+
#[test]
923+
fn command_wide_completion_flag_completion() {
924+
let mut completer = custom_completer();
925+
926+
let sample = /* lang=nu */ r#"
927+
def "nu-complete cmd" [spans: list] {
928+
let last = $spans | last
929+
[command wide --with --external]
930+
| where $it starts-with $last
931+
}
932+
933+
def "nu-complete cmd bar" [] {
934+
[bar specific]
935+
}
936+
937+
@complete "nu-complete cmd"
938+
def --wrapped "cmd" [
939+
--switch(-s)
940+
--flag(-f): string
941+
foo: string,
942+
bar: string@"nu-complete cmd bar",
943+
...rest
944+
] {}
945+
946+
cmd -"#;
947+
948+
let suggestions = completer.complete(sample, sample.len());
949+
let expected = vec!["--flag", "--switch", "-f", "-s", "--with", "--external"];
950+
match_suggestions(&expected, &suggestions);
951+
}
952+
861953
#[test]
862954
fn file_completions() {
863955
// Create a new engine

0 commit comments

Comments
 (0)