1use crate::completions::{
2 AttributableCompletion, AttributeCompletion, CellPathCompletion, CommandCompletion, Completer,
3 CompletionOptions, CustomCompletion, DirectoryCompletion, DotNuCompletion,
4 ExportableCompletion, FileCompletion, FlagCompletion, OperatorCompletion, VariableCompletion,
5 base::{SemanticSuggestion, SuggestionKind},
6};
7use nu_color_config::{color_record_to_nustyle, lookup_ansi_color_style};
8use nu_engine::eval_block;
9use nu_parser::{flatten_expression, parse, parse_module_file_or_dir};
10use nu_protocol::{
11 PipelineData, Span, Type, Value,
12 ast::{Argument, Block, Expr, Expression, FindMapResult, ListItem, Traverse},
13 debugger::WithoutDebug,
14 engine::{Closure, EngineState, Stack, StateWorkingSet},
15};
16use reedline::{Completer as ReedlineCompleter, Suggestion};
17use std::sync::Arc;
18
19fn find_pipeline_element_by_position<'a>(
24 expr: &'a Expression,
25 working_set: &'a StateWorkingSet,
26 pos: usize,
27) -> FindMapResult<&'a Expression> {
28 if !expr.span.contains(pos) {
30 return FindMapResult::Stop;
31 }
32 let closure = |expr: &'a Expression| find_pipeline_element_by_position(expr, working_set, pos);
33 match &expr.expr {
34 Expr::Call(call) => call
35 .arguments
36 .iter()
37 .find_map(|arg| arg.expr().and_then(|e| e.find_map(working_set, &closure)))
38 .or(Some(expr))
40 .map(FindMapResult::Found)
41 .unwrap_or_default(),
42 Expr::ExternalCall(head, arguments) => arguments
43 .iter()
44 .find_map(|arg| arg.expr().find_map(working_set, &closure))
45 .or(head.as_ref().find_map(working_set, &closure))
46 .or(Some(expr))
47 .map(FindMapResult::Found)
48 .unwrap_or_default(),
49 Expr::BinaryOp(lhs, _, rhs) => lhs
51 .find_map(working_set, &closure)
52 .or(rhs.find_map(working_set, &closure))
53 .or(Some(expr))
54 .map(FindMapResult::Found)
55 .unwrap_or_default(),
56 Expr::FullCellPath(fcp) => fcp
57 .head
58 .find_map(working_set, &closure)
59 .map(FindMapResult::Found)
60 .or_else(|| {
62 (fcp.head.span.contains(pos) && matches!(fcp.head.expr, Expr::List(_)))
63 .then_some(FindMapResult::Continue)
64 })
65 .or(Some(FindMapResult::Found(expr)))
66 .unwrap_or_default(),
67 Expr::Var(_) => FindMapResult::Found(expr),
68 Expr::AttributeBlock(ab) => ab
69 .attributes
70 .iter()
71 .map(|attr| &attr.expr)
72 .chain(Some(ab.item.as_ref()))
73 .find_map(|expr| expr.find_map(working_set, &closure))
74 .or(Some(expr))
75 .map(FindMapResult::Found)
76 .unwrap_or_default(),
77 _ => FindMapResult::Continue,
78 }
79}
80
81fn strip_placeholder_if_any<'a>(
84 working_set: &'a StateWorkingSet,
85 span: &Span,
86 strip: bool,
87) -> (Span, &'a [u8]) {
88 let new_span = if strip {
89 let new_end = std::cmp::max(span.end - 1, span.start);
90 Span::new(span.start, new_end)
91 } else {
92 span.to_owned()
93 };
94 let prefix = working_set.get_span_contents(new_span);
95 (new_span, prefix)
96}
97
98fn strip_placeholder_with_rsplit<'a>(
102 working_set: &'a StateWorkingSet,
103 span: &Span,
104 predicate: impl FnMut(&u8) -> bool,
105 strip: bool,
106) -> (Span, &'a [u8]) {
107 let span_content = working_set.get_span_contents(*span);
108 let mut prefix = span_content
109 .rsplit(predicate)
110 .next()
111 .unwrap_or(span_content);
112 let start = span.end.saturating_sub(prefix.len());
113 if strip && !prefix.is_empty() {
114 prefix = &prefix[..prefix.len() - 1];
115 }
116 let end = start + prefix.len();
117 (Span::new(start, end), prefix)
118}
119
120#[derive(Clone)]
121pub struct NuCompleter {
122 engine_state: Arc<EngineState>,
123 stack: Stack,
124}
125
126struct Context<'a> {
128 working_set: &'a StateWorkingSet<'a>,
129 span: Span,
130 prefix: &'a [u8],
131 offset: usize,
132}
133
134struct PositionalArguments<'a> {
136 command_head: &'a str,
138 positional_arg_indices: Vec<usize>,
140 arguments: &'a [Argument],
142 expr: &'a Expression,
144}
145
146impl Context<'_> {
147 fn new<'a>(
148 working_set: &'a StateWorkingSet,
149 span: Span,
150 prefix: &'a [u8],
151 offset: usize,
152 ) -> Context<'a> {
153 Context {
154 working_set,
155 span,
156 prefix,
157 offset,
158 }
159 }
160}
161
162impl NuCompleter {
163 pub fn new(engine_state: Arc<EngineState>, stack: Arc<Stack>) -> Self {
164 Self {
165 engine_state,
166 stack: Stack::with_parent(stack).reset_out_dest().collect_value(),
167 }
168 }
169
170 pub fn fetch_completions_at(&self, line: &str, pos: usize) -> Vec<SemanticSuggestion> {
171 let mut working_set = StateWorkingSet::new(&self.engine_state);
172 let offset = working_set.next_span_start();
173 let line = if line.len() > pos { &line[..pos] } else { line };
175 let block = parse(
176 &mut working_set,
177 Some("completer"),
178 format!("{}a", line).as_bytes(),
180 false,
181 );
182 self.fetch_completions_by_block(block, &working_set, pos, offset, line, true)
183 }
184
185 pub fn fetch_completions_within_file(
192 &self,
193 filename: &str,
194 pos: usize,
195 contents: &str,
196 ) -> Vec<SemanticSuggestion> {
197 let mut working_set = StateWorkingSet::new(&self.engine_state);
198 let block = parse(&mut working_set, Some(filename), contents.as_bytes(), false);
199 let Some(file_span) = working_set.get_span_for_filename(filename) else {
200 return vec![];
201 };
202 let offset = file_span.start;
203 self.fetch_completions_by_block(block.clone(), &working_set, pos, offset, contents, false)
204 }
205
206 fn fetch_completions_by_block(
207 &self,
208 block: Arc<Block>,
209 working_set: &StateWorkingSet,
210 pos: usize,
211 offset: usize,
212 contents: &str,
213 extra_placeholder: bool,
214 ) -> Vec<SemanticSuggestion> {
215 let mut pos_to_search = pos + offset;
218 if !extra_placeholder {
219 pos_to_search = pos_to_search.saturating_sub(1);
220 }
221 let Some(element_expression) = block.find_map(working_set, &|expr: &Expression| {
222 find_pipeline_element_by_position(expr, working_set, pos_to_search)
223 }) else {
224 return vec![];
225 };
226
227 let start_offset = element_expression.span.start - offset;
229 let Some(text) = contents.get(start_offset..pos) else {
230 return vec![];
231 };
232 self.complete_by_expression(
233 working_set,
234 element_expression,
235 offset,
236 pos_to_search,
237 text,
238 extra_placeholder,
239 )
240 }
241
242 fn complete_by_expression(
251 &self,
252 working_set: &StateWorkingSet,
253 element_expression: &Expression,
254 offset: usize,
255 pos: usize,
256 prefix_str: &str,
257 strip: bool,
258 ) -> Vec<SemanticSuggestion> {
259 let mut suggestions: Vec<SemanticSuggestion> = vec![];
260
261 match &element_expression.expr {
262 Expr::Var(_) => {
263 return self.variable_names_completion_helper(
264 working_set,
265 element_expression.span,
266 offset,
267 strip,
268 );
269 }
270 Expr::FullCellPath(full_cell_path) => {
271 if full_cell_path.tail.is_empty() && !prefix_str.ends_with('.') {
274 return self.variable_names_completion_helper(
275 working_set,
276 element_expression.span,
277 offset,
278 strip,
279 );
280 } else {
281 let mut cell_path_completer = CellPathCompletion {
282 full_cell_path,
283 position: if strip { pos - 1 } else { pos },
284 };
285 let ctx = Context::new(working_set, Span::unknown(), &[], offset);
286 return self.process_completion(&mut cell_path_completer, &ctx);
287 }
288 }
289 Expr::BinaryOp(lhs, op, _) => {
290 if op.span.contains(pos) {
291 let mut operator_completions = OperatorCompletion {
292 left_hand_side: lhs.as_ref(),
293 };
294 let (new_span, prefix) = strip_placeholder_if_any(working_set, &op.span, strip);
295 let ctx = Context::new(working_set, new_span, prefix, offset);
296 let results = self.process_completion(&mut operator_completions, &ctx);
297 if !results.is_empty() {
298 return results;
299 }
300 }
301 }
302 Expr::AttributeBlock(ab) => {
303 if let Some(span) = ab.attributes.iter().find_map(|attr| {
304 let span = attr.expr.span;
305 span.contains(pos).then_some(span)
306 }) {
307 let (new_span, prefix) = strip_placeholder_if_any(working_set, &span, strip);
308 let ctx = Context::new(working_set, new_span, prefix, offset);
309 return self.process_completion(&mut AttributeCompletion, &ctx);
310 };
311 let span = ab.item.span;
312 if span.contains(pos) {
313 let (new_span, prefix) = strip_placeholder_if_any(working_set, &span, strip);
314 let ctx = Context::new(working_set, new_span, prefix, offset);
315 return self.process_completion(&mut AttributableCompletion, &ctx);
316 }
317 }
318
319 Expr::Call(_) | Expr::ExternalCall(_, _) => {
323 let need_externals = !prefix_str.contains(' ');
324 let need_internals = !prefix_str.starts_with('^');
325 let mut span = element_expression.span;
326 if !need_internals {
327 span.start += 1;
328 };
329 suggestions.extend(self.command_completion_helper(
330 working_set,
331 span,
332 offset,
333 need_internals,
334 need_externals,
335 strip,
336 ))
337 }
338 _ => (),
339 }
340
341 match &element_expression.expr {
343 Expr::Call(call) => {
344 let mut positional_arg_indices = Vec::new();
348 for (arg_idx, arg) in call.arguments.iter().enumerate() {
349 let span = arg.span();
350 if span.contains(pos) {
351 if let Some(decl_id) = arg.expr().and_then(|e| e.custom_completion) {
353 let (new_span, prefix) = if matches!(arg, Argument::Named(_)) {
355 strip_placeholder_with_rsplit(
356 working_set,
357 &span,
358 |b| *b == b'=' || *b == b' ',
359 strip,
360 )
361 } else {
362 strip_placeholder_if_any(working_set, &span, strip)
363 };
364 let ctx = Context::new(working_set, new_span, prefix, offset);
365
366 let mut completer = CustomCompletion::new(
367 decl_id,
368 prefix_str.into(),
369 pos - offset,
370 FileCompletion,
371 );
372
373 suggestions.extend(self.process_completion(&mut completer, &ctx));
374 break;
375 }
376
377 let (new_span, prefix) =
379 strip_placeholder_if_any(working_set, &span, strip);
380 let ctx = Context::new(working_set, new_span, prefix, offset);
381 let flag_completion_helper = || {
382 let mut flag_completions = FlagCompletion {
383 decl_id: call.decl_id,
384 };
385 self.process_completion(&mut flag_completions, &ctx)
386 };
387 suggestions.extend(match arg {
388 Argument::Named(_) | Argument::Unknown(_)
390 if prefix.starts_with(b"-") =>
391 {
392 flag_completion_helper()
393 }
394 Argument::Positional(_) if prefix == b"-" => flag_completion_helper(),
396 Argument::Positional(expr) => {
398 let command_head = working_set.get_decl(call.decl_id).name();
399 positional_arg_indices.push(arg_idx);
400 self.argument_completion_helper(
401 PositionalArguments {
402 command_head,
403 positional_arg_indices,
404 arguments: &call.arguments,
405 expr,
406 },
407 pos,
408 &ctx,
409 suggestions.is_empty(),
410 )
411 }
412 _ => vec![],
413 });
414 break;
415 } else if !matches!(arg, Argument::Named(_)) {
416 positional_arg_indices.push(arg_idx);
417 }
418 }
419 }
420 Expr::ExternalCall(head, arguments) => {
421 for (i, arg) in arguments.iter().enumerate() {
422 let span = arg.expr().span;
423 if span.contains(pos) {
424 if i == 0 {
427 let external_cmd = working_set.get_span_contents(head.span);
428 if external_cmd == b"sudo" || external_cmd == b"doas" {
429 let commands = self.command_completion_helper(
430 working_set,
431 span,
432 offset,
433 true,
434 true,
435 strip,
436 );
437 if !commands.is_empty() {
439 return commands;
440 }
441 }
442 }
443 let config = self.engine_state.get_config();
445 if let Some(closure) = config.completions.external.completer.as_ref() {
446 let mut text_spans: Vec<String> =
447 flatten_expression(working_set, element_expression)
448 .iter()
449 .map(|(span, _)| {
450 let bytes = working_set.get_span_contents(*span);
451 String::from_utf8_lossy(bytes).to_string()
452 })
453 .collect();
454 let mut new_span = span;
455 if strip {
457 if let Some(last) = text_spans.last_mut() {
458 last.pop();
459 new_span = Span::new(span.start, span.end.saturating_sub(1));
460 }
461 }
462 if let Some(external_result) =
463 self.external_completion(closure, &text_spans, offset, new_span)
464 {
465 suggestions.extend(external_result);
466 return suggestions;
467 }
468 }
469 break;
470 }
471 }
472 }
473 _ => (),
474 }
475
476 if suggestions.is_empty() {
478 let (new_span, prefix) = strip_placeholder_with_rsplit(
479 working_set,
480 &element_expression.span,
481 |c| *c == b' ',
482 strip,
483 );
484 let ctx = Context::new(working_set, new_span, prefix, offset);
485 suggestions.extend(self.process_completion(&mut FileCompletion, &ctx));
486 }
487 suggestions
488 }
489
490 fn variable_names_completion_helper(
491 &self,
492 working_set: &StateWorkingSet,
493 span: Span,
494 offset: usize,
495 strip: bool,
496 ) -> Vec<SemanticSuggestion> {
497 let (new_span, prefix) = strip_placeholder_if_any(working_set, &span, strip);
498 if !prefix.starts_with(b"$") {
499 return vec![];
500 }
501 let ctx = Context::new(working_set, new_span, prefix, offset);
502 self.process_completion(&mut VariableCompletion, &ctx)
503 }
504
505 fn command_completion_helper(
506 &self,
507 working_set: &StateWorkingSet,
508 span: Span,
509 offset: usize,
510 internals: bool,
511 externals: bool,
512 strip: bool,
513 ) -> Vec<SemanticSuggestion> {
514 let config = self.engine_state.get_config();
515 let mut command_completions = CommandCompletion {
516 internals,
517 externals: !internals || (externals && config.completions.external.enable),
518 };
519 let (new_span, prefix) = strip_placeholder_if_any(working_set, &span, strip);
520 let ctx = Context::new(working_set, new_span, prefix, offset);
521 self.process_completion(&mut command_completions, &ctx)
522 }
523
524 fn argument_completion_helper(
525 &self,
526 argument_info: PositionalArguments,
527 pos: usize,
528 ctx: &Context,
529 need_fallback: bool,
530 ) -> Vec<SemanticSuggestion> {
531 let PositionalArguments {
532 command_head,
533 positional_arg_indices,
534 arguments,
535 expr,
536 } = argument_info;
537 match command_head {
539 "use" | "export use" | "overlay use" | "source-env"
541 if positional_arg_indices.len() == 1 =>
542 {
543 return self.process_completion(
544 &mut DotNuCompletion {
545 std_virtual_path: command_head != "source-env",
546 },
547 ctx,
548 );
549 }
550 "use" | "export use" => {
553 let Some(Argument::Positional(Expression {
554 expr: Expr::String(module_name),
555 span,
556 ..
557 })) = positional_arg_indices
558 .first()
559 .and_then(|i| arguments.get(*i))
560 else {
561 return vec![];
562 };
563 let module_name = module_name.as_bytes();
564 let (module_id, temp_working_set) = match ctx.working_set.find_module(module_name) {
565 Some(module_id) => (module_id, None),
566 None => {
567 let mut temp_working_set =
568 StateWorkingSet::new(ctx.working_set.permanent_state);
569 let Some(module_id) = parse_module_file_or_dir(
570 &mut temp_working_set,
571 module_name,
572 *span,
573 None,
574 ) else {
575 return vec![];
576 };
577 (module_id, Some(temp_working_set))
578 }
579 };
580 let mut exportable_completion = ExportableCompletion {
581 module_id,
582 temp_working_set,
583 };
584 let mut complete_on_list_items = |items: &[ListItem]| -> Vec<SemanticSuggestion> {
585 for item in items {
586 let span = item.expr().span;
587 if span.contains(pos) {
588 let offset = span.start.saturating_sub(ctx.span.start);
589 let end_offset =
590 ctx.prefix.len().min(pos.min(span.end) - ctx.span.start + 1);
591 let new_ctx = Context::new(
592 ctx.working_set,
593 Span::new(span.start, ctx.span.end.min(span.end)),
594 ctx.prefix.get(offset..end_offset).unwrap_or_default(),
595 ctx.offset,
596 );
597 return self.process_completion(&mut exportable_completion, &new_ctx);
598 }
599 }
600 vec![]
601 };
602
603 match &expr.expr {
604 Expr::String(_) => {
605 return self.process_completion(&mut exportable_completion, ctx);
606 }
607 Expr::FullCellPath(fcp) => match &fcp.head.expr {
608 Expr::List(items) => {
609 return complete_on_list_items(items);
610 }
611 _ => return vec![],
612 },
613 _ => return vec![],
614 }
615 }
616 "which" => {
617 let mut completer = CommandCompletion {
618 internals: true,
619 externals: true,
620 };
621 return self.process_completion(&mut completer, ctx);
622 }
623 _ => (),
624 }
625
626 let file_completion_helper = || self.process_completion(&mut FileCompletion, ctx);
628 match &expr.expr {
629 Expr::Directory(_, _) => self.process_completion(&mut DirectoryCompletion, ctx),
630 Expr::Filepath(_, _) | Expr::GlobPattern(_, _) => file_completion_helper(),
631 _ if need_fallback => file_completion_helper(),
633 _ => vec![],
634 }
635 }
636
637 fn process_completion<T: Completer>(
639 &self,
640 completer: &mut T,
641 ctx: &Context,
642 ) -> Vec<SemanticSuggestion> {
643 let config = self.engine_state.get_config();
644
645 let options = CompletionOptions {
646 case_sensitive: config.completions.case_sensitive,
647 match_algorithm: config.completions.algorithm.into(),
648 sort: config.completions.sort,
649 };
650
651 completer.fetch(
652 ctx.working_set,
653 &self.stack,
654 String::from_utf8_lossy(ctx.prefix),
655 ctx.span,
656 ctx.offset,
657 &options,
658 )
659 }
660
661 fn external_completion(
662 &self,
663 closure: &Closure,
664 spans: &[String],
665 offset: usize,
666 span: Span,
667 ) -> Option<Vec<SemanticSuggestion>> {
668 let block = self.engine_state.get_block(closure.block_id);
669 let mut callee_stack = self
670 .stack
671 .captures_to_stack_preserve_out_dest(closure.captures.clone());
672
673 if let Some(pos_arg) = block.signature.required_positional.first() {
675 if let Some(var_id) = pos_arg.var_id {
676 callee_stack.add_var(
677 var_id,
678 Value::list(
679 spans
680 .iter()
681 .map(|it| Value::string(it, Span::unknown()))
682 .collect(),
683 Span::unknown(),
684 ),
685 );
686 }
687 }
688
689 let result = eval_block::<WithoutDebug>(
690 &self.engine_state,
691 &mut callee_stack,
692 block,
693 PipelineData::empty(),
694 );
695
696 match result.and_then(|data| data.into_value(span)) {
697 Ok(Value::List { vals, .. }) => {
698 let result =
699 map_value_completions(vals.iter(), Span::new(span.start, span.end), offset);
700 Some(result)
701 }
702 Ok(Value::Nothing { .. }) => None,
703 Ok(value) => {
704 log::error!(
705 "External completer returned invalid value of type {}",
706 value.get_type().to_string()
707 );
708 Some(vec![])
709 }
710 Err(err) => {
711 log::error!("failed to eval completer block: {err}");
712 Some(vec![])
713 }
714 }
715 }
716}
717
718impl ReedlineCompleter for NuCompleter {
719 fn complete(&mut self, line: &str, pos: usize) -> Vec<Suggestion> {
720 self.fetch_completions_at(line, pos)
721 .into_iter()
722 .map(|s| s.suggestion)
723 .collect()
724 }
725}
726
727pub fn map_value_completions<'a>(
728 list: impl Iterator<Item = &'a Value>,
729 span: Span,
730 offset: usize,
731) -> Vec<SemanticSuggestion> {
732 list.filter_map(move |x| {
733 if let Ok(s) = x.coerce_string() {
735 return Some(SemanticSuggestion {
736 suggestion: Suggestion {
737 value: s,
738 span: reedline::Span {
739 start: span.start - offset,
740 end: span.end - offset,
741 },
742 ..Suggestion::default()
743 },
744 kind: Some(SuggestionKind::Value(x.get_type())),
745 });
746 }
747
748 if let Ok(record) = x.as_record() {
750 let mut suggestion = Suggestion {
751 value: String::from(""), span: reedline::Span {
753 start: span.start - offset,
754 end: span.end - offset,
755 },
756 ..Suggestion::default()
757 };
758 let mut value_type = Type::String;
759
760 record.iter().for_each(|(key, value)| {
762 match key.as_str() {
763 "value" => {
764 value_type = value.get_type();
765 if let Ok(val_str) = value.coerce_string() {
767 suggestion.value = val_str;
769 }
770 }
771 "description" => {
772 if let Ok(desc_str) = value.coerce_string() {
774 suggestion.description = Some(desc_str);
776 }
777 }
778 "style" => {
779 suggestion.style = match value {
781 Value::String { val, .. } => Some(lookup_ansi_color_style(val)),
782 Value::Record { .. } => Some(color_record_to_nustyle(value)),
783 _ => None,
784 };
785 }
786 _ => (),
787 }
788 });
789
790 return Some(SemanticSuggestion {
791 suggestion,
792 kind: Some(SuggestionKind::Value(value_type)),
793 });
794 }
795
796 None
797 })
798 .collect()
799}
800
801#[cfg(test)]
802mod completer_tests {
803 use super::*;
804
805 #[test]
806 fn test_completion_helper() {
807 let mut engine_state =
808 nu_command::add_shell_command_context(nu_cmd_lang::create_default_context());
809
810 let delta = {
812 let working_set = nu_protocol::engine::StateWorkingSet::new(&engine_state);
813 working_set.render()
814 };
815
816 let result = engine_state.merge_delta(delta);
817 assert!(
818 result.is_ok(),
819 "Error merging delta: {:?}",
820 result.err().unwrap()
821 );
822
823 let completer = NuCompleter::new(engine_state.into(), Arc::new(Stack::new()));
824 let dataset = [
825 ("1 bit-sh", true, "b", vec!["bit-shl", "bit-shr"]),
826 ("1.0 bit-sh", false, "b", vec![]),
827 ("1 m", true, "m", vec!["mod"]),
828 ("1.0 m", true, "m", vec!["mod"]),
829 ("\"a\" s", true, "s", vec!["starts-with"]),
830 ("sudo", false, "", Vec::new()),
831 ("sudo l", true, "l", vec!["ls", "let", "lines", "loop"]),
832 (" sudo", false, "", Vec::new()),
833 (" sudo le", true, "le", vec!["let", "length"]),
834 (
835 "ls | c",
836 true,
837 "c",
838 vec!["cd", "config", "const", "cp", "cal"],
839 ),
840 ("ls | sudo m", true, "m", vec!["mv", "mut", "move"]),
841 ];
842 for (line, has_result, begins_with, expected_values) in dataset {
843 let result = completer.fetch_completions_at(line, line.len());
844 assert_eq!(!result.is_empty(), has_result, "line: {}", line);
846
847 result
849 .iter()
850 .for_each(|x| assert!(x.suggestion.value.starts_with(begins_with)));
851
852 assert_eq!(
854 result
855 .iter()
856 .map(|x| expected_values.contains(&x.suggestion.value.as_str()))
857 .filter(|x| *x)
858 .count(),
859 expected_values.len(),
860 "line: {}",
861 line
862 );
863 }
864 }
865}