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 },
354 strip,
355 ))
356 }
357 _ => (),
358 }
359
360 match &element_expression.expr {
362 Expr::Call(call) => {
363 let signature = working_set.get_decl(call.decl_id).signature();
364 let mut positional_arg_index = 0;
368
369 for (arg_idx, arg) in call.arguments.iter().enumerate() {
370 let span = arg.span();
371
372 if !span.contains(pos) {
374 match arg {
375 Argument::Named(_) => (),
376 _ => positional_arg_index += 1,
377 }
378 continue;
379 }
380
381 let (new_span, prefix) = strip_placeholder_if_any(working_set, &span, strip);
383 let ctx = Context::new(working_set, new_span, prefix, offset);
384 let flag_completion_helper = |ctx: Context| {
385 let mut flag_completions = FlagCompletion {
386 decl_id: call.decl_id,
387 };
388 let mut res = self.process_completion(&mut flag_completions, &ctx);
389 let command_wide_ctx = Context::new(working_set, span, b"", offset);
393 res.extend(
394 self.command_wide_completion_helper(
395 &signature,
396 element_expression,
397 &command_wide_ctx,
398 strip,
399 )
400 .1,
401 );
402 res
403 };
404
405 match arg {
416 Argument::Named((name, short, Some(val))) if val.span.contains(pos) => {
418 let mut new_span = val.span;
426 if strip {
427 new_span.end = new_span.end.saturating_sub(1);
428 }
429 let prefix = working_set.get_span_contents(new_span);
430 let ctx = Context::new(working_set, new_span, prefix, offset);
431
432 let flag = signature.get_long_flag(&name.item).or_else(|| {
435 short.as_ref().and_then(|s| {
436 signature.get_short_flag(s.item.chars().next().unwrap_or('_'))
437 })
438 });
439 if let Some(custom_completer) = flag.and_then(|f| f.completion) {
441 suggestions.splice(
442 0..0,
443 self.custom_completion_helper(
444 custom_completer,
445 prefix_str,
446 &ctx,
447 if strip { pos } else { pos + 1 },
448 ),
449 );
450 return suggestions;
452 }
453
454 let command_wide_ctx = Context::new(working_set, val.span, b"", offset);
457 let (need_fallback, command_wide_res) = self
458 .command_wide_completion_helper(
459 &signature,
460 element_expression,
461 &command_wide_ctx,
462 strip,
463 );
464 suggestions.splice(0..0, command_wide_res);
465 if !need_fallback {
466 return suggestions;
467 }
468
469 let mut flag_value_completion = ArgValueCompletion {
470 arg_type: ArgType::Flag(Cow::from(name.as_ref().item.as_str())),
471 need_fallback: false,
474 completer: self,
475 call,
476 arg_idx,
477 pos,
478 strip,
479 };
480 suggestions.splice(
481 0..0,
482 self.process_completion(&mut flag_value_completion, &ctx),
483 );
484 return suggestions;
485 }
486 Argument::Named((_, _, None)) => {
488 suggestions.splice(0..0, flag_completion_helper(ctx));
489 }
490 Argument::Named((_, _, Some(val))) => {
493 let mut new_span = Span::new(span.start, val.span.start);
495 let raw_prefix = working_set.get_span_contents(new_span);
496 let prefix = raw_prefix.trim_ascii_end();
497 let mut prefix = prefix.strip_suffix(b"=").unwrap_or(prefix);
498 new_span.end = new_span
499 .end
500 .saturating_sub(raw_prefix.len() - prefix.len())
501 .max(span.start);
502
503 if strip {
505 new_span.end = new_span.end.saturating_sub(1).max(span.start);
506 prefix = prefix[..prefix.len() - 1].as_ref();
507 }
508
509 let ctx = Context::new(working_set, new_span, prefix, offset);
510 suggestions.splice(0..0, flag_completion_helper(ctx));
511 }
512 Argument::Unknown(_) if prefix.starts_with(b"-") => {
513 suggestions.splice(0..0, flag_completion_helper(ctx));
514 }
515 Argument::Positional(_) if prefix == b"-" => {
517 suggestions.splice(0..0, flag_completion_helper(ctx));
518 }
519 Argument::Positional(_) => {
520 if let Some(custom_completer) = signature
522 .get_positional(positional_arg_index)
525 .and_then(|pos_arg| pos_arg.completion.clone())
526 {
527 suggestions.splice(
528 0..0,
529 self.custom_completion_helper(
530 custom_completer,
531 prefix_str,
532 &ctx,
533 if strip { pos } else { pos + 1 },
534 ),
535 );
536 return suggestions;
538 }
539
540 let command_wide_ctx = Context::new(working_set, span, b"", offset);
542 let (need_fallback, command_wide_res) = self
543 .command_wide_completion_helper(
544 &signature,
545 element_expression,
546 &command_wide_ctx,
547 strip,
548 );
549 suggestions.splice(0..0, command_wide_res);
550 if !need_fallback {
551 return suggestions;
552 }
553
554 let mut positional_value_completion = ArgValueCompletion {
556 arg_type: ArgType::Positional(positional_arg_index),
558 need_fallback: suggestions.is_empty(),
559 completer: self,
560 call,
561 arg_idx,
562 pos,
563 strip,
564 };
565
566 suggestions.splice(
567 0..0,
568 self.process_completion(&mut positional_value_completion, &ctx),
569 );
570 return suggestions;
571 }
572 _ => (),
573 }
574 break;
575 }
576 }
577 Expr::ExternalCall(head, arguments) => {
578 for (i, arg) in arguments.iter().enumerate() {
579 let span = arg.expr().span;
580 if span.contains(pos) {
581 if i == 0 {
584 let external_cmd = working_set.get_span_contents(head.span);
585 if external_cmd == b"sudo" || external_cmd == b"doas" {
586 let commands = self.command_completion_helper(
587 working_set,
588 span,
589 offset,
590 CommandCompletionOptions {
591 internals: true,
592 externals: true,
593 builtins_only: false,
594 },
595 strip,
596 );
597 if !commands.is_empty() {
599 return commands;
600 }
601 }
602 }
603
604 let completion = self
606 .engine_state
607 .get_config()
608 .completions
609 .external
610 .completer
611 .as_ref()
612 .map(|closure| {
613 CommandWideCompletion::closure(closure, element_expression, strip)
614 });
615
616 if let Some(mut completion) = completion {
617 let ctx = Context::new(working_set, span, b"", offset);
618 let results = self.process_completion(&mut completion, &ctx);
619
620 suggestions.splice(0..0, results);
622
623 if !completion.need_fallback {
624 return suggestions;
625 }
626 }
627
628 if suggestions.is_empty() {
630 let (new_span, prefix) =
631 strip_placeholder_if_any(working_set, &span, strip);
632 let ctx = Context::new(working_set, new_span, prefix, offset);
633 return self.process_completion(&mut FileCompletion, &ctx);
634 }
635 break;
636 }
637 }
638 }
639 _ => (),
640 }
641
642 if suggestions.is_empty() {
644 let (new_span, prefix) =
645 strip_placeholder_if_any(working_set, &element_expression.span, strip);
646 let ctx = Context::new(working_set, new_span, prefix, offset);
647 suggestions.extend(self.process_completion(&mut FileCompletion, &ctx));
648 }
649 suggestions
650 }
651
652 fn variable_names_completion_helper(
653 &self,
654 working_set: &StateWorkingSet,
655 span: Span,
656 offset: usize,
657 strip: bool,
658 ) -> Vec<SemanticSuggestion> {
659 let (new_span, prefix) = strip_placeholder_if_any(working_set, &span, strip);
660 if !prefix.starts_with(b"$") {
661 return vec![];
662 }
663 let ctx = Context::new(working_set, new_span, prefix, offset);
664 self.process_completion(&mut VariableCompletion, &ctx)
665 }
666
667 fn command_completion_helper(
668 &self,
669 working_set: &StateWorkingSet,
670 span: Span,
671 offset: usize,
672 options: CommandCompletionOptions,
673 strip: bool,
674 ) -> Vec<SemanticSuggestion> {
675 let config = self.engine_state.get_config();
676 let mut command_completions = CommandCompletion {
677 internals: options.internals,
678 externals: !options.internals
679 || (options.externals && config.completions.external.enable),
680 builtins_only: options.builtins_only,
681 };
682 let (new_span, prefix) = strip_placeholder_if_any(working_set, &span, strip);
683 let ctx = Context::new(working_set, new_span, prefix, offset);
684 self.process_completion(&mut command_completions, &ctx)
685 }
686
687 fn custom_completion_helper(
688 &self,
689 custom_completion: Completion,
690 input: &str,
691 ctx: &Context,
692 pos: usize,
693 ) -> Vec<SemanticSuggestion> {
694 match custom_completion {
695 Completion::Command(decl_id) => {
696 let mut completer =
697 CustomCompletion::new(decl_id, input.into(), pos - ctx.offset, FileCompletion);
698 self.process_completion(&mut completer, ctx)
699 }
700 Completion::List(list) => {
701 let mut completer = StaticCompletion::new(list);
702 self.process_completion(&mut completer, ctx)
703 }
704 }
705 }
706
707 fn command_wide_completion_helper(
708 &self,
709 signature: &Signature,
710 element_expression: &Expression,
711 ctx: &Context,
712 strip: bool,
713 ) -> (bool, Vec<SemanticSuggestion>) {
714 let completion = match signature.complete {
715 Some(CommandWideCompleter::Command(decl_id)) => {
716 CommandWideCompletion::command(ctx.working_set, decl_id, element_expression, strip)
717 }
718 Some(CommandWideCompleter::External) => self
719 .engine_state
720 .get_config()
721 .completions
722 .external
723 .completer
724 .as_ref()
725 .map(|closure| CommandWideCompletion::closure(closure, element_expression, strip)),
726 None => None,
727 };
728
729 if let Some(mut completion) = completion {
730 let res = self.process_completion(&mut completion, ctx);
731 (completion.need_fallback, res)
732 } else {
733 (true, vec![])
734 }
735 }
736
737 pub(crate) fn process_completion<T: Completer>(
739 &self,
740 completer: &mut T,
741 ctx: &Context,
742 ) -> Vec<SemanticSuggestion> {
743 let config = self.engine_state.get_config();
744
745 let options = CompletionOptions {
746 case_sensitive: config.completions.case_sensitive,
747 match_algorithm: config.completions.algorithm.into(),
748 sort: config.completions.sort,
749 match_description: false,
750 };
751
752 completer.fetch(
753 ctx.working_set,
754 &self.stack,
755 String::from_utf8_lossy(ctx.prefix),
756 ctx.span,
757 ctx.offset,
758 &options,
759 )
760 }
761}
762
763struct CommandCompletionOptions {
764 internals: bool,
765 externals: bool,
766 builtins_only: bool,
767}
768
769impl ReedlineCompleter for NuCompleter {
770 fn complete(&mut self, line: &str, pos: usize) -> Vec<Suggestion> {
771 self.fetch_completions_at(line, pos)
772 .into_iter()
773 .map(|s| s.suggestion)
774 .collect()
775 }
776}
777
778#[cfg(test)]
779mod completer_tests {
780 use super::*;
781
782 #[test]
783 fn test_completion_helper() {
784 let mut engine_state =
785 nu_command::add_shell_command_context(nu_cmd_lang::create_default_context());
786
787 let delta = {
789 let working_set = nu_protocol::engine::StateWorkingSet::new(&engine_state);
790 working_set.render()
791 };
792
793 let result = engine_state.merge_delta(delta);
794 assert!(
795 result.is_ok(),
796 "Error merging delta: {:?}",
797 result.err().unwrap()
798 );
799
800 let completer = NuCompleter::new(engine_state.into(), Arc::new(Stack::new()));
801 let dataset = [
802 ("1 bit-sh", true, "b", vec!["bit-shl", "bit-shr"]),
803 ("1.0 bit-sh", false, "b", vec![]),
804 ("1 m", true, "m", vec!["mod"]),
805 ("1.0 m", true, "m", vec!["mod"]),
806 ("\"a\" s", true, "s", vec!["starts-with"]),
807 ("sudo", false, "", Vec::new()),
808 ("sudo l", true, "l", vec!["ls", "let", "lines", "loop"]),
809 (" sudo", false, "", Vec::new()),
810 (" sudo le", true, "le", vec!["let", "length"]),
811 (
812 "ls | c",
813 true,
814 "c",
815 vec!["cd", "config", "const", "cp", "cal"],
816 ),
817 ("ls | sudo m", true, "m", vec!["mv", "mut", "move"]),
818 ];
819 for (line, has_result, begins_with, expected_values) in dataset {
820 let result = completer.fetch_completions_at(line, line.len());
821 assert_eq!(!result.is_empty(), has_result, "line: {line}");
823
824 result
826 .iter()
827 .for_each(|x| assert!(x.suggestion.value.starts_with(begins_with)));
828
829 assert_eq!(
831 result
832 .iter()
833 .map(|x| expected_values.contains(&x.suggestion.value.as_str()))
834 .filter(|x| *x)
835 .count(),
836 expected_values.len(),
837 "line: {line}"
838 );
839 }
840 }
841}