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::{
10 Argument, Block, Expr, Expression, FindMapResult, PipelineRedirection, RedirectionTarget,
11 Traverse,
12 },
13 engine::{ArgType, EngineState, Stack, StateWorkingSet},
14};
15use reedline::{Completer as ReedlineCompleter, Suggestion};
16use std::borrow::Cow;
17use std::sync::Arc;
18
19use super::{StaticCompletion, custom_completions::CommandWideCompletion};
20
21fn find_pipeline_element_by_position<'a>(
26 expr: &'a Expression,
27 working_set: &'a StateWorkingSet,
28 pos: usize,
29) -> FindMapResult<&'a Expression> {
30 if !expr.span.contains(pos) {
32 return FindMapResult::Stop;
33 }
34 let closure = |expr: &'a Expression| find_pipeline_element_by_position(expr, working_set, pos);
35 match &expr.expr {
36 Expr::RowCondition(block_id)
37 | Expr::Subexpression(block_id)
38 | Expr::Block(block_id)
39 | Expr::Closure(block_id) => {
40 let block = working_set.get_block(*block_id);
41 check_redirection_in_block(block.as_ref(), pos)
43 .map(FindMapResult::Found)
44 .unwrap_or_default()
45 }
46 Expr::Call(call) => call
47 .arguments
48 .iter()
49 .find_map(|arg| arg.expr().and_then(|e| e.find_map(working_set, &closure)))
50 .or(Some(expr))
52 .map(FindMapResult::Found)
53 .unwrap_or_default(),
54 Expr::ExternalCall(head, arguments) => arguments
55 .iter()
56 .find_map(|arg| arg.expr().find_map(working_set, &closure))
57 .or_else(|| {
58 let span = working_set.get_span(head.span_id);
62 if span.contains(pos) {
63 head.as_ref().find_map(working_set, &closure)
65 } else {
66 None
67 }
68 })
69 .or(Some(expr))
70 .map(FindMapResult::Found)
71 .unwrap_or_default(),
72 Expr::BinaryOp(lhs, _, rhs) => lhs
74 .find_map(working_set, &closure)
75 .or_else(|| rhs.find_map(working_set, &closure))
76 .or(Some(expr))
77 .map(FindMapResult::Found)
78 .unwrap_or_default(),
79 Expr::FullCellPath(fcp) => fcp
80 .head
81 .find_map(working_set, &closure)
82 .map(FindMapResult::Found)
83 .or_else(|| {
85 (fcp.head.span.contains(pos) && matches!(fcp.head.expr, Expr::List(_)))
86 .then_some(FindMapResult::Continue)
87 })
88 .or(Some(FindMapResult::Found(expr)))
89 .unwrap_or_default(),
90 Expr::Var(_) => FindMapResult::Found(expr),
91 Expr::AttributeBlock(ab) => ab
92 .attributes
93 .iter()
94 .map(|attr| &attr.expr)
95 .chain(Some(ab.item.as_ref()))
96 .find_map(|expr| expr.find_map(working_set, &closure))
97 .or(Some(expr))
98 .map(FindMapResult::Found)
99 .unwrap_or_default(),
100 _ => FindMapResult::Continue,
101 }
102}
103
104fn check_redirection_target(target: &RedirectionTarget, pos: usize) -> Option<&Expression> {
106 let expr = target.expr();
107 expr.and_then(|expression| {
108 if let Expr::String(_) = expression.expr
109 && expression.span.contains(pos)
110 {
111 expr
112 } else {
113 None
114 }
115 })
116}
117
118fn check_redirection_in_block(block: &Block, pos: usize) -> Option<&Expression> {
121 block.pipelines.iter().find_map(|pipeline| {
122 pipeline.elements.iter().find_map(|element| {
123 element.redirection.as_ref().and_then(|redir| match redir {
124 PipelineRedirection::Single { target, .. } => check_redirection_target(target, pos),
125 PipelineRedirection::Separate { out, err } => check_redirection_target(out, pos)
126 .or_else(|| check_redirection_target(err, pos)),
127 })
128 })
129 })
130}
131
132fn strip_placeholder_if_any<'a>(
135 working_set: &'a StateWorkingSet,
136 span: &Span,
137 strip: bool,
138) -> (Span, &'a [u8]) {
139 let new_span = if strip {
140 let new_end = std::cmp::max(span.end - 1, span.start);
141 Span::new(span.start, new_end)
142 } else {
143 span.to_owned()
144 };
145 let prefix = working_set.get_span_contents(new_span);
146 (new_span, prefix)
147}
148
149#[derive(Clone)]
150pub struct NuCompleter {
151 engine_state: Arc<EngineState>,
152 stack: Stack,
153}
154
155pub(crate) struct Context<'a> {
157 pub working_set: &'a StateWorkingSet<'a>,
158 pub span: Span,
159 pub prefix: &'a [u8],
160 pub offset: usize,
161}
162
163impl Context<'_> {
164 pub(crate) fn new<'a>(
165 working_set: &'a StateWorkingSet,
166 span: Span,
167 prefix: &'a [u8],
168 offset: usize,
169 ) -> Context<'a> {
170 Context {
171 working_set,
172 span,
173 prefix,
174 offset,
175 }
176 }
177}
178
179impl NuCompleter {
180 pub fn new(engine_state: Arc<EngineState>, stack: Arc<Stack>) -> Self {
181 Self {
182 engine_state,
183 stack: Stack::with_parent(stack).reset_out_dest().collect_value(),
184 }
185 }
186
187 pub fn fetch_completions_at(&self, line: &str, pos: usize) -> Vec<SemanticSuggestion> {
188 let mut working_set = StateWorkingSet::new(&self.engine_state);
189 let offset = working_set.next_span_start();
190 let line = if line.len() > pos { &line[..pos] } else { line };
192 let block = parse(
193 &mut working_set,
194 Some("completer"),
195 format!("{line}a").as_bytes(),
197 false,
198 );
199 self.fetch_completions_by_block(block, &working_set, pos, offset, line, true)
200 }
201
202 pub fn fetch_completions_within_file(
209 &self,
210 filename: &str,
211 pos: usize,
212 contents: &str,
213 ) -> Vec<SemanticSuggestion> {
214 let mut working_set = StateWorkingSet::new(&self.engine_state);
215 let block = parse(&mut working_set, Some(filename), contents.as_bytes(), false);
216 let Some(file_span) = working_set.get_span_for_filename(filename) else {
217 return vec![];
218 };
219 let offset = file_span.start;
220 self.fetch_completions_by_block(block.clone(), &working_set, pos, offset, contents, false)
221 }
222
223 fn fetch_completions_by_block(
224 &self,
225 block: Arc<Block>,
226 working_set: &StateWorkingSet,
227 pos: usize,
228 offset: usize,
229 contents: &str,
230 extra_placeholder: bool,
231 ) -> Vec<SemanticSuggestion> {
232 let mut pos_to_search = pos + offset;
235 if !extra_placeholder {
236 pos_to_search = pos_to_search.saturating_sub(1);
237 }
238 let Some(element_expression) = block
239 .find_map(working_set, &|expr: &Expression| {
240 find_pipeline_element_by_position(expr, working_set, pos_to_search)
241 })
242 .or_else(|| check_redirection_in_block(block.as_ref(), pos_to_search))
243 else {
244 return vec![];
245 };
246
247 let start_offset = element_expression.span.start - offset;
249 let Some(text) = contents.get(start_offset..pos) else {
250 return vec![];
251 };
252 self.complete_by_expression(
253 working_set,
254 element_expression,
255 offset,
256 pos_to_search,
257 text,
258 extra_placeholder,
259 )
260 }
261
262 fn complete_by_expression(
271 &self,
272 working_set: &StateWorkingSet,
273 element_expression: &Expression,
274 offset: usize,
275 pos: usize,
276 prefix_str: &str,
277 strip: bool,
278 ) -> Vec<SemanticSuggestion> {
279 let mut suggestions: Vec<SemanticSuggestion> = vec![];
280
281 match &element_expression.expr {
282 Expr::Var(_) => {
283 return self.variable_names_completion_helper(
284 working_set,
285 element_expression.span,
286 offset,
287 strip,
288 );
289 }
290 Expr::FullCellPath(full_cell_path) => {
291 if full_cell_path.tail.is_empty() && !prefix_str.ends_with('.') {
294 return self.variable_names_completion_helper(
295 working_set,
296 element_expression.span,
297 offset,
298 strip,
299 );
300 } else {
301 let mut cell_path_completer = CellPathCompletion {
302 full_cell_path,
303 position: if strip { pos - 1 } else { pos },
304 };
305 let ctx = Context::new(working_set, Span::unknown(), &[], offset);
306 return self.process_completion(&mut cell_path_completer, &ctx);
307 }
308 }
309 Expr::BinaryOp(lhs, op, _) => {
310 if op.span.contains(pos) {
311 let mut operator_completions = OperatorCompletion {
312 left_hand_side: lhs.as_ref(),
313 };
314 let (new_span, prefix) = strip_placeholder_if_any(working_set, &op.span, strip);
315 let ctx = Context::new(working_set, new_span, prefix, offset);
316 let results = self.process_completion(&mut operator_completions, &ctx);
317 if !results.is_empty() {
318 return results;
319 }
320 }
321 }
322 Expr::AttributeBlock(ab) => {
323 if let Some(span) = ab.attributes.iter().find_map(|attr| {
324 let span = attr.expr.span;
325 span.contains(pos).then_some(span)
326 }) {
327 let (new_span, prefix) = strip_placeholder_if_any(working_set, &span, strip);
328 let ctx = Context::new(working_set, new_span, prefix, offset);
329 return self.process_completion(&mut AttributeCompletion, &ctx);
330 };
331 let span = ab.item.span;
332 if span.contains(pos) {
333 let (new_span, prefix) = strip_placeholder_if_any(working_set, &span, strip);
334 let ctx = Context::new(working_set, new_span, prefix, offset);
335 return self.process_completion(&mut AttributableCompletion, &ctx);
336 }
337 }
338
339 Expr::Call(_) | Expr::ExternalCall(_, _) => {
343 let need_externals = !prefix_str.contains(' ');
344 let need_internals = !prefix_str.starts_with('^');
345 let mut span = element_expression.span;
346 if !need_internals {
347 span.start += 1;
348 };
349 suggestions.extend(self.command_completion_helper(
350 working_set,
351 span,
352 offset,
353 need_internals,
354 need_externals,
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 suggestions.splice(
443 0..0,
444 self.custom_completion_helper(
445 custom_completer,
446 prefix_str,
447 &ctx,
448 if strip { pos } else { pos + 1 },
449 ),
450 );
451 return suggestions;
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 suggestions.splice(
529 0..0,
530 self.custom_completion_helper(
531 custom_completer,
532 prefix_str,
533 &ctx,
534 if strip { pos } else { pos + 1 },
535 ),
536 );
537 return suggestions;
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 true,
592 true,
593 strip,
594 );
595 if !commands.is_empty() {
597 return commands;
598 }
599 }
600 }
601
602 let completion = self
604 .engine_state
605 .get_config()
606 .completions
607 .external
608 .completer
609 .as_ref()
610 .map(|closure| {
611 CommandWideCompletion::closure(closure, element_expression, strip)
612 });
613
614 if let Some(mut completion) = completion {
615 let ctx = Context::new(working_set, span, b"", offset);
616 let results = self.process_completion(&mut completion, &ctx);
617
618 suggestions.splice(0..0, results);
620
621 if !completion.need_fallback {
622 return suggestions;
623 }
624 }
625
626 if suggestions.is_empty() {
628 let (new_span, prefix) =
629 strip_placeholder_if_any(working_set, &span, strip);
630 let ctx = Context::new(working_set, new_span, prefix, offset);
631 return self.process_completion(&mut FileCompletion, &ctx);
632 }
633 break;
634 }
635 }
636 }
637 _ => (),
638 }
639
640 if suggestions.is_empty() {
642 let (new_span, prefix) =
643 strip_placeholder_if_any(working_set, &element_expression.span, strip);
644 let ctx = Context::new(working_set, new_span, prefix, offset);
645 suggestions.extend(self.process_completion(&mut FileCompletion, &ctx));
646 }
647 suggestions
648 }
649
650 fn variable_names_completion_helper(
651 &self,
652 working_set: &StateWorkingSet,
653 span: Span,
654 offset: usize,
655 strip: bool,
656 ) -> Vec<SemanticSuggestion> {
657 let (new_span, prefix) = strip_placeholder_if_any(working_set, &span, strip);
658 if !prefix.starts_with(b"$") {
659 return vec![];
660 }
661 let ctx = Context::new(working_set, new_span, prefix, offset);
662 self.process_completion(&mut VariableCompletion, &ctx)
663 }
664
665 fn command_completion_helper(
666 &self,
667 working_set: &StateWorkingSet,
668 span: Span,
669 offset: usize,
670 internals: bool,
671 externals: bool,
672 strip: bool,
673 ) -> Vec<SemanticSuggestion> {
674 let config = self.engine_state.get_config();
675 let mut command_completions = CommandCompletion {
676 internals,
677 externals: !internals || (externals && config.completions.external.enable),
678 };
679 let (new_span, prefix) = strip_placeholder_if_any(working_set, &span, strip);
680 let ctx = Context::new(working_set, new_span, prefix, offset);
681 self.process_completion(&mut command_completions, &ctx)
682 }
683
684 fn custom_completion_helper(
685 &self,
686 custom_completion: Completion,
687 input: &str,
688 ctx: &Context,
689 pos: usize,
690 ) -> Vec<SemanticSuggestion> {
691 match custom_completion {
692 Completion::Command(decl_id) => {
693 let mut completer =
694 CustomCompletion::new(decl_id, input.into(), pos - ctx.offset, FileCompletion);
695 self.process_completion(&mut completer, ctx)
696 }
697 Completion::List(list) => {
698 let mut completer = StaticCompletion::new(list);
699 self.process_completion(&mut completer, ctx)
700 }
701 }
702 }
703
704 fn command_wide_completion_helper(
705 &self,
706 signature: &Signature,
707 element_expression: &Expression,
708 ctx: &Context,
709 strip: bool,
710 ) -> (bool, Vec<SemanticSuggestion>) {
711 let completion = match signature.complete {
712 Some(CommandWideCompleter::Command(decl_id)) => {
713 CommandWideCompletion::command(ctx.working_set, decl_id, element_expression, strip)
714 }
715 Some(CommandWideCompleter::External) => self
716 .engine_state
717 .get_config()
718 .completions
719 .external
720 .completer
721 .as_ref()
722 .map(|closure| CommandWideCompletion::closure(closure, element_expression, strip)),
723 None => None,
724 };
725
726 if let Some(mut completion) = completion {
727 let res = self.process_completion(&mut completion, ctx);
728 (completion.need_fallback, res)
729 } else {
730 (true, vec![])
731 }
732 }
733
734 pub(crate) fn process_completion<T: Completer>(
736 &self,
737 completer: &mut T,
738 ctx: &Context,
739 ) -> Vec<SemanticSuggestion> {
740 let config = self.engine_state.get_config();
741
742 let options = CompletionOptions {
743 case_sensitive: config.completions.case_sensitive,
744 match_algorithm: config.completions.algorithm.into(),
745 sort: config.completions.sort,
746 };
747
748 completer.fetch(
749 ctx.working_set,
750 &self.stack,
751 String::from_utf8_lossy(ctx.prefix),
752 ctx.span,
753 ctx.offset,
754 &options,
755 )
756 }
757}
758
759impl ReedlineCompleter for NuCompleter {
760 fn complete(&mut self, line: &str, pos: usize) -> Vec<Suggestion> {
761 self.fetch_completions_at(line, pos)
762 .into_iter()
763 .map(|s| s.suggestion)
764 .collect()
765 }
766}
767
768#[cfg(test)]
769mod completer_tests {
770 use super::*;
771
772 #[test]
773 fn test_completion_helper() {
774 let mut engine_state =
775 nu_command::add_shell_command_context(nu_cmd_lang::create_default_context());
776
777 let delta = {
779 let working_set = nu_protocol::engine::StateWorkingSet::new(&engine_state);
780 working_set.render()
781 };
782
783 let result = engine_state.merge_delta(delta);
784 assert!(
785 result.is_ok(),
786 "Error merging delta: {:?}",
787 result.err().unwrap()
788 );
789
790 let completer = NuCompleter::new(engine_state.into(), Arc::new(Stack::new()));
791 let dataset = [
792 ("1 bit-sh", true, "b", vec!["bit-shl", "bit-shr"]),
793 ("1.0 bit-sh", false, "b", vec![]),
794 ("1 m", true, "m", vec!["mod"]),
795 ("1.0 m", true, "m", vec!["mod"]),
796 ("\"a\" s", true, "s", vec!["starts-with"]),
797 ("sudo", false, "", Vec::new()),
798 ("sudo l", true, "l", vec!["ls", "let", "lines", "loop"]),
799 (" sudo", false, "", Vec::new()),
800 (" sudo le", true, "le", vec!["let", "length"]),
801 (
802 "ls | c",
803 true,
804 "c",
805 vec!["cd", "config", "const", "cp", "cal"],
806 ),
807 ("ls | sudo m", true, "m", vec!["mv", "mut", "move"]),
808 ];
809 for (line, has_result, begins_with, expected_values) in dataset {
810 let result = completer.fetch_completions_at(line, line.len());
811 assert_eq!(!result.is_empty(), has_result, "line: {line}");
813
814 result
816 .iter()
817 .for_each(|x| assert!(x.suggestion.value.starts_with(begins_with)));
818
819 assert_eq!(
821 result
822 .iter()
823 .map(|x| expected_values.contains(&x.suggestion.value.as_str()))
824 .filter(|x| *x)
825 .count(),
826 expected_values.len(),
827 "line: {line}"
828 );
829 }
830 }
831}