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