1use crate::completions::{
2 ArgValueCompletion, AttributableCompletion, AttributeCompletion, CellPathCompletion,
3 CommandCompletion, Completer, CompletionOptions, CustomCompletion, FileCompletion,
4 FlagCompletion, OperatorCompletion, VariableCompletion, base::SemanticSuggestion,
5};
6use nu_parser::parse;
7use nu_protocol::{
8 CommandWideCompleter, Completion, GetSpan, Signature, Span,
9 ast::{Argument, Block, Expr, Expression, PipelineRedirection, RedirectionTarget, Traverse},
10 engine::{ArgType, EngineState, Stack, StateWorkingSet},
11};
12use reedline::{Completer as ReedlineCompleter, Suggestion};
13use std::sync::Arc;
14use std::{borrow::Cow, ops::ControlFlow};
15
16use super::{StaticCompletion, custom_completions::CommandWideCompletion};
17
18fn find_pipeline_element_by_position<'a>(
23 expr: &'a Expression,
24 working_set: &'a StateWorkingSet,
25 pos: usize,
26) -> ControlFlow<Option<&'a Expression>> {
27 if !expr.span.contains(pos) {
29 return ControlFlow::Break(None);
30 }
31 let closure = |expr: &'a Expression| find_pipeline_element_by_position(expr, working_set, pos);
32 let found = |x| ControlFlow::Break(Some(x));
33 match &expr.expr {
34 Expr::RowCondition(block_id)
35 | Expr::Subexpression(block_id)
36 | Expr::Block(block_id)
37 | Expr::Closure(block_id) => {
38 let block = working_set.get_block(*block_id);
39 check_redirection_in_block(block.as_ref(), pos)
41 .map(found)
42 .unwrap_or(ControlFlow::Continue(()))
43 }
44 Expr::Call(call) => call
45 .arguments
46 .iter()
47 .find_map(|arg| arg.expr().and_then(|e| e.find_map(working_set, &closure)))
48 .map(found)
49 .unwrap_or(found(expr)),
50 Expr::ExternalCall(head, arguments) => arguments
51 .iter()
52 .find_map(|arg| arg.expr().find_map(working_set, &closure))
53 .or_else(|| {
54 let span = working_set.get_span(head.span_id);
58 if span.contains(pos) {
59 head.as_ref().find_map(working_set, &closure)
61 } else {
62 None
63 }
64 })
65 .map(found)
66 .unwrap_or(found(expr)),
67 Expr::BinaryOp(lhs, _, rhs) => lhs
69 .find_map(working_set, &closure)
70 .or_else(|| rhs.find_map(working_set, &closure))
71 .map(found)
72 .unwrap_or(found(expr)),
73 Expr::FullCellPath(fcp) => fcp
74 .head
75 .find_map(working_set, &closure)
76 .map(found)
77 .or_else(|| {
79 (fcp.head.span.contains(pos) && matches!(fcp.head.expr, Expr::List(_)))
80 .then_some(ControlFlow::Continue(()))
81 })
82 .unwrap_or(found(expr)),
83 Expr::Var(_) => found(expr),
84 Expr::AttributeBlock(ab) => ab
85 .attributes
86 .iter()
87 .map(|attr| &attr.expr)
88 .chain(Some(ab.item.as_ref()))
89 .find_map(|expr| expr.find_map(working_set, &closure))
90 .map(found)
91 .unwrap_or(found(expr)),
92 _ => ControlFlow::Continue(()),
93 }
94}
95
96fn check_redirection_target(target: &RedirectionTarget, pos: usize) -> Option<&Expression> {
98 let expr = target.expr();
99 expr.and_then(|expression| {
100 if let Expr::String(_) = expression.expr
101 && expression.span.contains(pos)
102 {
103 expr
104 } else {
105 None
106 }
107 })
108}
109
110fn check_redirection_in_block(block: &Block, pos: usize) -> Option<&Expression> {
113 block.pipelines.iter().find_map(|pipeline| {
114 pipeline.elements.iter().find_map(|element| {
115 element.redirection.as_ref().and_then(|redir| match redir {
116 PipelineRedirection::Single { target, .. } => check_redirection_target(target, pos),
117 PipelineRedirection::Separate { out, err } => check_redirection_target(out, pos)
118 .or_else(|| check_redirection_target(err, pos)),
119 })
120 })
121 })
122}
123
124fn strip_placeholder_if_any<'a>(
127 working_set: &'a StateWorkingSet,
128 span: &Span,
129 strip: bool,
130) -> (Span, &'a [u8]) {
131 let new_span = if strip {
132 let new_end = std::cmp::max(span.end - 1, span.start);
133 Span::new(span.start, new_end)
134 } else {
135 span.to_owned()
136 };
137 let prefix = working_set.get_span_contents(new_span);
138 (new_span, prefix)
139}
140
141#[derive(Clone)]
142pub struct NuCompleter {
143 engine_state: Arc<EngineState>,
144 stack: Stack,
145}
146
147pub(crate) struct Context<'a> {
149 pub working_set: &'a StateWorkingSet<'a>,
150 pub span: Span,
151 pub prefix: &'a [u8],
152 pub offset: usize,
153}
154
155impl Context<'_> {
156 pub(crate) fn new<'a>(
157 working_set: &'a StateWorkingSet,
158 span: Span,
159 prefix: &'a [u8],
160 offset: usize,
161 ) -> Context<'a> {
162 Context {
163 working_set,
164 span,
165 prefix,
166 offset,
167 }
168 }
169}
170
171impl NuCompleter {
172 pub fn new(engine_state: Arc<EngineState>, stack: Arc<Stack>) -> Self {
173 Self {
174 engine_state,
175 stack: Stack::with_parent(stack).reset_out_dest().collect_value(),
176 }
177 }
178
179 pub fn fetch_completions_at(&self, line: &str, pos: usize) -> Vec<SemanticSuggestion> {
180 let mut working_set = StateWorkingSet::new(&self.engine_state);
181 let offset = working_set.next_span_start();
182 let line = if line.len() > pos { &line[..pos] } else { line };
184 let block = parse(
185 &mut working_set,
186 Some("completer"),
187 format!("{line}a").as_bytes(),
189 false,
190 );
191 self.fetch_completions_by_block(block, &working_set, pos, offset, line, true)
192 }
193
194 pub fn fetch_completions_within_file(
201 &self,
202 filename: &str,
203 pos: usize,
204 contents: &str,
205 ) -> Vec<SemanticSuggestion> {
206 let mut working_set = StateWorkingSet::new(&self.engine_state);
207 let block = parse(&mut working_set, Some(filename), contents.as_bytes(), false);
208 let Some(file_span) = working_set.get_span_for_filename(filename) else {
209 return vec![];
210 };
211 let offset = file_span.start;
212 self.fetch_completions_by_block(block.clone(), &working_set, pos, offset, contents, false)
213 }
214
215 fn fetch_completions_by_block(
216 &self,
217 block: Arc<Block>,
218 working_set: &StateWorkingSet,
219 pos: usize,
220 offset: usize,
221 contents: &str,
222 extra_placeholder: bool,
223 ) -> Vec<SemanticSuggestion> {
224 let mut pos_to_search = pos + offset;
227 if !extra_placeholder {
228 pos_to_search = pos_to_search.saturating_sub(1);
229 }
230 let Some(element_expression) = block
231 .find_map(working_set, &|expr: &Expression| {
232 find_pipeline_element_by_position(expr, working_set, pos_to_search)
233 })
234 .or_else(|| check_redirection_in_block(block.as_ref(), pos_to_search))
235 else {
236 return vec![];
237 };
238
239 let start_offset = element_expression.span.start - offset;
241 let Some(text) = contents.get(start_offset..pos) else {
242 return vec![];
243 };
244 self.complete_by_expression(
245 working_set,
246 element_expression,
247 offset,
248 pos_to_search,
249 text,
250 extra_placeholder,
251 )
252 }
253
254 fn complete_by_expression(
263 &self,
264 working_set: &StateWorkingSet,
265 element_expression: &Expression,
266 offset: usize,
267 pos: usize,
268 prefix_str: &str,
269 strip: bool,
270 ) -> Vec<SemanticSuggestion> {
271 let mut suggestions: Vec<SemanticSuggestion> = vec![];
272
273 match &element_expression.expr {
274 Expr::Var(_) => {
275 return self.variable_names_completion_helper(
276 working_set,
277 element_expression.span,
278 offset,
279 strip,
280 );
281 }
282 Expr::FullCellPath(full_cell_path) => {
283 if full_cell_path.tail.is_empty() && !prefix_str.ends_with('.') {
286 return self.variable_names_completion_helper(
287 working_set,
288 element_expression.span,
289 offset,
290 strip,
291 );
292 } else {
293 let mut cell_path_completer = CellPathCompletion {
294 full_cell_path,
295 position: if strip { pos - 1 } else { pos },
296 };
297 let ctx = Context::new(working_set, element_expression.span, &[], offset);
298 return self.process_completion(&mut cell_path_completer, &ctx);
299 }
300 }
301 Expr::BinaryOp(lhs, op, _) => {
302 if op.span.contains(pos) {
303 let mut operator_completions = OperatorCompletion {
304 left_hand_side: lhs.as_ref(),
305 };
306 let (new_span, prefix) = strip_placeholder_if_any(working_set, &op.span, strip);
307 let ctx = Context::new(working_set, new_span, prefix, offset);
308 let results = self.process_completion(&mut operator_completions, &ctx);
309 if !results.is_empty() {
310 return results;
311 }
312 }
313 }
314 Expr::AttributeBlock(ab) => {
315 if let Some(span) = ab.attributes.iter().find_map(|attr| {
316 let span = attr.expr.span;
317 span.contains(pos).then_some(span)
318 }) {
319 let (new_span, prefix) = strip_placeholder_if_any(working_set, &span, strip);
320 let ctx = Context::new(working_set, new_span, prefix, offset);
321 return self.process_completion(&mut AttributeCompletion, &ctx);
322 };
323 let span = ab.item.span;
324 if span.contains(pos) {
325 let (new_span, prefix) = strip_placeholder_if_any(working_set, &span, strip);
326 let ctx = Context::new(working_set, new_span, prefix, offset);
327 return self.process_completion(&mut AttributableCompletion, &ctx);
328 }
329 }
330
331 Expr::Call(_) | Expr::ExternalCall(_, _) => {
335 let force_external = prefix_str.starts_with('^');
336 let force_internal = prefix_str.starts_with('%');
337 let force_builtins_only = force_internal;
338
339 let need_externals = !prefix_str.contains(' ') && !force_internal;
340 let need_internals = !force_external;
341 let mut span = element_expression.span;
342 if force_external || force_internal {
343 span.start += 1;
344 };
345 suggestions.extend(self.command_completion_helper(
346 working_set,
347 span,
348 offset,
349 CommandCompletionOptions {
350 internals: need_internals,
351 externals: need_externals,
352 builtins_only: force_builtins_only,
353 quote_internals: false,
354 },
355 strip,
356 ))
357 }
358 _ => (),
359 }
360
361 match &element_expression.expr {
363 Expr::Call(call) => {
364 let signature = working_set.get_decl(call.decl_id).signature();
365 let mut positional_arg_index = 0;
369
370 for (arg_idx, arg) in call.arguments.iter().enumerate() {
371 let span = arg.span();
372
373 if !span.contains(pos) {
375 match arg {
376 Argument::Named(_) => (),
377 _ => positional_arg_index += 1,
378 }
379 continue;
380 }
381
382 let (new_span, prefix) = strip_placeholder_if_any(working_set, &span, strip);
384 let ctx = Context::new(working_set, new_span, prefix, offset);
385 let flag_completion_helper = |ctx: Context| {
386 let mut flag_completions = FlagCompletion {
387 decl_id: call.decl_id,
388 };
389 let mut res = self.process_completion(&mut flag_completions, &ctx);
390 let command_wide_ctx = Context::new(working_set, span, b"", offset);
394 res.extend(
395 self.command_wide_completion_helper(
396 &signature,
397 element_expression,
398 &command_wide_ctx,
399 strip,
400 )
401 .1,
402 );
403 res
404 };
405
406 match arg {
417 Argument::Named((name, short, Some(val))) if val.span.contains(pos) => {
419 let mut new_span = val.span;
427 if strip {
428 new_span.end = new_span.end.saturating_sub(1);
429 }
430 let prefix = working_set.get_span_contents(new_span);
431 let ctx = Context::new(working_set, new_span, prefix, offset);
432
433 let flag = signature.get_long_flag(&name.item).or_else(|| {
436 short.as_ref().and_then(|s| {
437 signature.get_short_flag(s.item.chars().next().unwrap_or('_'))
438 })
439 });
440 if let Some(custom_completer) = flag.and_then(|f| f.completion) {
442 let (need_fallback, new_suggestions) = self
443 .custom_completion_helper(
444 custom_completer,
445 prefix_str,
446 &ctx,
447 if strip { pos } else { pos + 1 },
448 );
449 suggestions.splice(0..0, new_suggestions);
450 if !need_fallback {
451 return suggestions;
452 }
453 }
454
455 let command_wide_ctx = Context::new(working_set, val.span, b"", offset);
458 let (need_fallback, command_wide_res) = self
459 .command_wide_completion_helper(
460 &signature,
461 element_expression,
462 &command_wide_ctx,
463 strip,
464 );
465 suggestions.splice(0..0, command_wide_res);
466 if !need_fallback {
467 return suggestions;
468 }
469
470 let mut flag_value_completion = ArgValueCompletion {
471 arg_type: ArgType::Flag(Cow::from(name.as_ref().item.as_str())),
472 need_fallback: false,
475 completer: self,
476 call,
477 arg_idx,
478 pos,
479 strip,
480 };
481 suggestions.splice(
482 0..0,
483 self.process_completion(&mut flag_value_completion, &ctx),
484 );
485 return suggestions;
486 }
487 Argument::Named((_, _, None)) => {
489 suggestions.splice(0..0, flag_completion_helper(ctx));
490 }
491 Argument::Named((_, _, Some(val))) => {
494 let mut new_span = Span::new(span.start, val.span.start);
496 let raw_prefix = working_set.get_span_contents(new_span);
497 let prefix = raw_prefix.trim_ascii_end();
498 let mut prefix = prefix.strip_suffix(b"=").unwrap_or(prefix);
499 new_span.end = new_span
500 .end
501 .saturating_sub(raw_prefix.len() - prefix.len())
502 .max(span.start);
503
504 if strip {
506 new_span.end = new_span.end.saturating_sub(1).max(span.start);
507 prefix = prefix[..prefix.len() - 1].as_ref();
508 }
509
510 let ctx = Context::new(working_set, new_span, prefix, offset);
511 suggestions.splice(0..0, flag_completion_helper(ctx));
512 }
513 Argument::Unknown(_) if prefix.starts_with(b"-") => {
514 suggestions.splice(0..0, flag_completion_helper(ctx));
515 }
516 Argument::Positional(_) if prefix == b"-" => {
518 suggestions.splice(0..0, flag_completion_helper(ctx));
519 }
520 Argument::Positional(_) => {
521 if let Some(custom_completer) = signature
523 .get_positional(positional_arg_index)
526 .and_then(|pos_arg| pos_arg.completion.clone())
527 {
528 let (need_fallback, new_suggestions) = self
529 .custom_completion_helper(
530 custom_completer,
531 prefix_str,
532 &ctx,
533 if strip { pos } else { pos + 1 },
534 );
535 suggestions.splice(0..0, new_suggestions);
536 if !need_fallback {
537 return suggestions;
538 }
539 }
540
541 let command_wide_ctx = Context::new(working_set, span, b"", offset);
543 let (need_fallback, command_wide_res) = self
544 .command_wide_completion_helper(
545 &signature,
546 element_expression,
547 &command_wide_ctx,
548 strip,
549 );
550 suggestions.splice(0..0, command_wide_res);
551 if !need_fallback {
552 return suggestions;
553 }
554
555 let mut positional_value_completion = ArgValueCompletion {
557 arg_type: ArgType::Positional(positional_arg_index),
559 need_fallback: suggestions.is_empty(),
560 completer: self,
561 call,
562 arg_idx,
563 pos,
564 strip,
565 };
566
567 suggestions.splice(
568 0..0,
569 self.process_completion(&mut positional_value_completion, &ctx),
570 );
571 return suggestions;
572 }
573 _ => (),
574 }
575 break;
576 }
577 }
578 Expr::ExternalCall(head, arguments) => {
579 for (i, arg) in arguments.iter().enumerate() {
580 let span = arg.expr().span;
581 if span.contains(pos) {
582 if i == 0 {
585 let external_cmd = working_set.get_span_contents(head.span);
586 if external_cmd == b"sudo" || external_cmd == b"doas" {
587 let commands = self.command_completion_helper(
588 working_set,
589 span,
590 offset,
591 CommandCompletionOptions {
592 internals: true,
593 externals: true,
594 builtins_only: false,
595 quote_internals: false,
596 },
597 strip,
598 );
599 if !commands.is_empty() {
601 return commands;
602 }
603 }
604 }
605
606 let completion = self
608 .engine_state
609 .get_config()
610 .completions
611 .external
612 .completer
613 .as_ref()
614 .map(|closure| {
615 CommandWideCompletion::closure(closure, element_expression, strip)
616 });
617
618 if let Some(mut completion) = completion {
619 let ctx = Context::new(working_set, span, b"", offset);
620 let results = self.process_completion(&mut completion, &ctx);
621
622 suggestions.splice(0..0, results);
624
625 if !completion.need_fallback {
626 return suggestions;
627 }
628 }
629
630 if suggestions.is_empty() {
632 let (new_span, prefix) =
633 strip_placeholder_if_any(working_set, &span, strip);
634 let ctx = Context::new(working_set, new_span, prefix, offset);
635 return self.process_completion(&mut FileCompletion, &ctx);
636 }
637 break;
638 }
639 }
640 }
641 _ => (),
642 }
643
644 if suggestions.is_empty() {
646 let (new_span, prefix) =
647 strip_placeholder_if_any(working_set, &element_expression.span, strip);
648 let ctx = Context::new(working_set, new_span, prefix, offset);
649 suggestions.extend(self.process_completion(&mut FileCompletion, &ctx));
650 }
651 suggestions
652 }
653
654 fn variable_names_completion_helper(
655 &self,
656 working_set: &StateWorkingSet,
657 span: Span,
658 offset: usize,
659 strip: bool,
660 ) -> Vec<SemanticSuggestion> {
661 let (new_span, prefix) = strip_placeholder_if_any(working_set, &span, strip);
662 if !prefix.starts_with(b"$") {
663 return vec![];
664 }
665 let ctx = Context::new(working_set, new_span, prefix, offset);
666 self.process_completion(&mut VariableCompletion, &ctx)
667 }
668
669 fn command_completion_helper(
670 &self,
671 working_set: &StateWorkingSet,
672 span: Span,
673 offset: usize,
674 options: CommandCompletionOptions,
675 strip: bool,
676 ) -> Vec<SemanticSuggestion> {
677 let config = self.engine_state.get_config();
678 let mut command_completions = CommandCompletion {
679 internals: options.internals,
680 externals: !options.internals
681 || (options.externals && config.completions.external.enable),
682 builtins_only: options.builtins_only,
683 quote_internals: options.quote_internals,
684 };
685 let (new_span, prefix) = strip_placeholder_if_any(working_set, &span, strip);
686 let ctx = Context::new(working_set, new_span, prefix, offset);
687 self.process_completion(&mut command_completions, &ctx)
688 }
689
690 fn custom_completion_helper(
691 &self,
692 custom_completion: Completion,
693 input: &str,
694 ctx: &Context,
695 pos: usize,
696 ) -> (bool, Vec<SemanticSuggestion>) {
697 match custom_completion {
698 Completion::Command(decl_id) => {
699 let mut completer =
700 CustomCompletion::new(decl_id, input.into(), pos - ctx.offset, false);
701 let suggestions = self.process_completion(&mut completer, ctx);
702 (completer.need_fallback, suggestions)
703 }
704 Completion::List(list) => {
705 let mut completer = StaticCompletion::new(list);
706 (false, self.process_completion(&mut completer, ctx))
707 }
708 }
709 }
710
711 fn command_wide_completion_helper(
712 &self,
713 signature: &Signature,
714 element_expression: &Expression,
715 ctx: &Context,
716 strip: bool,
717 ) -> (bool, Vec<SemanticSuggestion>) {
718 let completion = match signature.complete {
719 Some(CommandWideCompleter::Command(decl_id)) => {
720 CommandWideCompletion::command(ctx.working_set, decl_id, element_expression, strip)
721 }
722 Some(CommandWideCompleter::External) => self
723 .engine_state
724 .get_config()
725 .completions
726 .external
727 .completer
728 .as_ref()
729 .map(|closure| CommandWideCompletion::closure(closure, element_expression, strip)),
730 None => None,
731 };
732
733 if let Some(mut completion) = completion {
734 let res = self.process_completion(&mut completion, ctx);
735 (completion.need_fallback, res)
736 } else {
737 (true, vec![])
738 }
739 }
740
741 pub(crate) fn process_completion<T: Completer>(
743 &self,
744 completer: &mut T,
745 ctx: &Context,
746 ) -> Vec<SemanticSuggestion> {
747 let config = self.engine_state.get_config();
748
749 let options = CompletionOptions {
750 case_sensitive: config.completions.case_sensitive,
751 match_algorithm: config.completions.algorithm.into(),
752 sort: config.completions.sort,
753 match_description: false,
754 };
755
756 completer.fetch(
757 ctx.working_set,
758 &self.stack,
759 String::from_utf8_lossy(ctx.prefix),
760 ctx.span,
761 ctx.offset,
762 &options,
763 )
764 }
765}
766
767struct CommandCompletionOptions {
768 internals: bool,
769 externals: bool,
770 builtins_only: bool,
771 quote_internals: bool,
772}
773
774impl ReedlineCompleter for NuCompleter {
775 fn complete(&mut self, line: &str, pos: usize) -> Vec<Suggestion> {
776 self.fetch_completions_at(line, pos)
777 .into_iter()
778 .map(|s| s.suggestion)
779 .collect()
780 }
781}
782
783#[cfg(test)]
784mod completer_tests {
785 use super::*;
786
787 #[test]
788 fn test_completion_helper() {
789 let mut engine_state =
790 nu_command::add_shell_command_context(nu_cmd_lang::create_default_context());
791
792 let delta = {
794 let working_set = nu_protocol::engine::StateWorkingSet::new(&engine_state);
795 working_set.render()
796 };
797
798 let result = engine_state.merge_delta(delta);
799 assert!(
800 result.is_ok(),
801 "Error merging delta: {:?}",
802 result.err().unwrap()
803 );
804
805 let completer = NuCompleter::new(engine_state.into(), Arc::new(Stack::new()));
806 let dataset = [
807 ("1 bit-sh", true, "b", vec!["bit-shl", "bit-shr"]),
808 ("1.0 bit-sh", false, "b", vec![]),
809 ("1 m", true, "m", vec!["mod"]),
810 ("1.0 m", true, "m", vec!["mod"]),
811 ("\"a\" s", true, "s", vec!["starts-with"]),
812 ("sudo", false, "", Vec::new()),
813 ("sudo l", true, "l", vec!["ls", "let", "lines", "loop"]),
814 (" sudo", false, "", Vec::new()),
815 (" sudo le", true, "le", vec!["let", "length"]),
816 (
817 "ls | c",
818 true,
819 "c",
820 vec!["cd", "config", "const", "cp", "cal"],
821 ),
822 ("ls | sudo m", true, "m", vec!["mv", "mut", "move"]),
823 ];
824 for (line, has_result, begins_with, expected_values) in dataset {
825 let result = completer.fetch_completions_at(line, line.len());
826 assert_eq!(!result.is_empty(), has_result, "line: {line}");
828
829 result
831 .iter()
832 .for_each(|x| assert!(x.suggestion.value.starts_with(begins_with)));
833
834 assert_eq!(
836 result
837 .iter()
838 .map(|x| expected_values.contains(&x.suggestion.value.as_str()))
839 .filter(|x| *x)
840 .count(),
841 expected_values.len(),
842 "line: {line}"
843 );
844 }
845 }
846}