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_parser::{parse, parse_module_file_or_dir};
9use nu_protocol::{
10 CommandWideCompleter, Completion, Span, Type, Value,
11 ast::{
12 Argument, Block, Expr, Expression, FindMapResult, ListItem, PipelineRedirection,
13 RedirectionTarget, Traverse,
14 },
15 engine::{EngineState, Stack, StateWorkingSet},
16};
17use reedline::{Completer as ReedlineCompleter, Suggestion};
18use std::sync::Arc;
19
20use super::{StaticCompletion, custom_completions::CommandWideCompletion};
21
22fn find_pipeline_element_by_position<'a>(
27 expr: &'a Expression,
28 working_set: &'a StateWorkingSet,
29 pos: usize,
30) -> FindMapResult<&'a Expression> {
31 if !expr.span.contains(pos) {
33 return FindMapResult::Stop;
34 }
35 let closure = |expr: &'a Expression| find_pipeline_element_by_position(expr, working_set, pos);
36 match &expr.expr {
37 Expr::RowCondition(block_id)
38 | Expr::Subexpression(block_id)
39 | Expr::Block(block_id)
40 | Expr::Closure(block_id) => {
41 let block = working_set.get_block(*block_id);
42 check_redirection_in_block(block.as_ref(), pos)
44 .map(FindMapResult::Found)
45 .unwrap_or_default()
46 }
47 Expr::Call(call) => call
48 .arguments
49 .iter()
50 .find_map(|arg| arg.expr().and_then(|e| e.find_map(working_set, &closure)))
51 .or(Some(expr))
53 .map(FindMapResult::Found)
54 .unwrap_or_default(),
55 Expr::ExternalCall(head, arguments) => arguments
56 .iter()
57 .find_map(|arg| arg.expr().find_map(working_set, &closure))
58 .or(head.as_ref().find_map(working_set, &closure))
59 .or(Some(expr))
60 .map(FindMapResult::Found)
61 .unwrap_or_default(),
62 Expr::BinaryOp(lhs, _, rhs) => lhs
64 .find_map(working_set, &closure)
65 .or(rhs.find_map(working_set, &closure))
66 .or(Some(expr))
67 .map(FindMapResult::Found)
68 .unwrap_or_default(),
69 Expr::FullCellPath(fcp) => fcp
70 .head
71 .find_map(working_set, &closure)
72 .map(FindMapResult::Found)
73 .or_else(|| {
75 (fcp.head.span.contains(pos) && matches!(fcp.head.expr, Expr::List(_)))
76 .then_some(FindMapResult::Continue)
77 })
78 .or(Some(FindMapResult::Found(expr)))
79 .unwrap_or_default(),
80 Expr::Var(_) => FindMapResult::Found(expr),
81 Expr::AttributeBlock(ab) => ab
82 .attributes
83 .iter()
84 .map(|attr| &attr.expr)
85 .chain(Some(ab.item.as_ref()))
86 .find_map(|expr| expr.find_map(working_set, &closure))
87 .or(Some(expr))
88 .map(FindMapResult::Found)
89 .unwrap_or_default(),
90 _ => FindMapResult::Continue,
91 }
92}
93
94fn check_redirection_target(target: &RedirectionTarget, pos: usize) -> Option<&Expression> {
96 let expr = target.expr();
97 expr.and_then(|expression| {
98 if let Expr::String(_) = expression.expr
99 && expression.span.contains(pos)
100 {
101 expr
102 } else {
103 None
104 }
105 })
106}
107
108fn check_redirection_in_block(block: &Block, pos: usize) -> Option<&Expression> {
111 block.pipelines.iter().find_map(|pipeline| {
112 pipeline.elements.iter().find_map(|element| {
113 element.redirection.as_ref().and_then(|redir| match redir {
114 PipelineRedirection::Single { target, .. } => check_redirection_target(target, pos),
115 PipelineRedirection::Separate { out, err } => check_redirection_target(out, pos)
116 .or_else(|| check_redirection_target(err, pos)),
117 })
118 })
119 })
120}
121
122fn strip_placeholder_if_any<'a>(
125 working_set: &'a StateWorkingSet,
126 span: &Span,
127 strip: bool,
128) -> (Span, &'a [u8]) {
129 let new_span = if strip {
130 let new_end = std::cmp::max(span.end - 1, span.start);
131 Span::new(span.start, new_end)
132 } else {
133 span.to_owned()
134 };
135 let prefix = working_set.get_span_contents(new_span);
136 (new_span, prefix)
137}
138
139fn strip_placeholder_with_rsplit<'a>(
143 working_set: &'a StateWorkingSet,
144 span: &Span,
145 predicate: impl FnMut(&u8) -> bool,
146 strip: bool,
147) -> (Span, &'a [u8]) {
148 let span_content = working_set.get_span_contents(*span);
149 let mut prefix = span_content
150 .rsplit(predicate)
151 .next()
152 .unwrap_or(span_content);
153 let start = span.end.saturating_sub(prefix.len());
154 if strip && !prefix.is_empty() {
155 prefix = &prefix[..prefix.len() - 1];
156 }
157 let end = start + prefix.len();
158 (Span::new(start, end), prefix)
159}
160
161#[derive(Clone)]
162pub struct NuCompleter {
163 engine_state: Arc<EngineState>,
164 stack: Stack,
165}
166
167struct Context<'a> {
169 working_set: &'a StateWorkingSet<'a>,
170 span: Span,
171 prefix: &'a [u8],
172 offset: usize,
173}
174
175struct PositionalArguments<'a> {
177 command_head: &'a str,
179 positional_arg_indices: Vec<usize>,
181 arguments: &'a [Argument],
183 expr: &'a Expression,
185}
186
187impl Context<'_> {
188 fn new<'a>(
189 working_set: &'a StateWorkingSet,
190 span: Span,
191 prefix: &'a [u8],
192 offset: usize,
193 ) -> Context<'a> {
194 Context {
195 working_set,
196 span,
197 prefix,
198 offset,
199 }
200 }
201}
202
203impl NuCompleter {
204 pub fn new(engine_state: Arc<EngineState>, stack: Arc<Stack>) -> Self {
205 Self {
206 engine_state,
207 stack: Stack::with_parent(stack).reset_out_dest().collect_value(),
208 }
209 }
210
211 pub fn fetch_completions_at(&self, line: &str, pos: usize) -> Vec<SemanticSuggestion> {
212 let mut working_set = StateWorkingSet::new(&self.engine_state);
213 let offset = working_set.next_span_start();
214 let line = if line.len() > pos { &line[..pos] } else { line };
216 let block = parse(
217 &mut working_set,
218 Some("completer"),
219 format!("{line}a").as_bytes(),
221 false,
222 );
223 self.fetch_completions_by_block(block, &working_set, pos, offset, line, true)
224 }
225
226 pub fn fetch_completions_within_file(
233 &self,
234 filename: &str,
235 pos: usize,
236 contents: &str,
237 ) -> Vec<SemanticSuggestion> {
238 let mut working_set = StateWorkingSet::new(&self.engine_state);
239 let block = parse(&mut working_set, Some(filename), contents.as_bytes(), false);
240 let Some(file_span) = working_set.get_span_for_filename(filename) else {
241 return vec![];
242 };
243 let offset = file_span.start;
244 self.fetch_completions_by_block(block.clone(), &working_set, pos, offset, contents, false)
245 }
246
247 fn fetch_completions_by_block(
248 &self,
249 block: Arc<Block>,
250 working_set: &StateWorkingSet,
251 pos: usize,
252 offset: usize,
253 contents: &str,
254 extra_placeholder: bool,
255 ) -> Vec<SemanticSuggestion> {
256 let mut pos_to_search = pos + offset;
259 if !extra_placeholder {
260 pos_to_search = pos_to_search.saturating_sub(1);
261 }
262 let Some(element_expression) = block
263 .find_map(working_set, &|expr: &Expression| {
264 find_pipeline_element_by_position(expr, working_set, pos_to_search)
265 })
266 .or_else(|| check_redirection_in_block(block.as_ref(), pos_to_search))
267 else {
268 return vec![];
269 };
270
271 let start_offset = element_expression.span.start - offset;
273 let Some(text) = contents.get(start_offset..pos) else {
274 return vec![];
275 };
276 self.complete_by_expression(
277 working_set,
278 element_expression,
279 offset,
280 pos_to_search,
281 text,
282 extra_placeholder,
283 )
284 }
285
286 fn complete_by_expression(
295 &self,
296 working_set: &StateWorkingSet,
297 element_expression: &Expression,
298 offset: usize,
299 pos: usize,
300 prefix_str: &str,
301 strip: bool,
302 ) -> Vec<SemanticSuggestion> {
303 let mut suggestions: Vec<SemanticSuggestion> = vec![];
304
305 match &element_expression.expr {
306 Expr::Var(_) => {
307 return self.variable_names_completion_helper(
308 working_set,
309 element_expression.span,
310 offset,
311 strip,
312 );
313 }
314 Expr::FullCellPath(full_cell_path) => {
315 if full_cell_path.tail.is_empty() && !prefix_str.ends_with('.') {
318 return self.variable_names_completion_helper(
319 working_set,
320 element_expression.span,
321 offset,
322 strip,
323 );
324 } else {
325 let mut cell_path_completer = CellPathCompletion {
326 full_cell_path,
327 position: if strip { pos - 1 } else { pos },
328 };
329 let ctx = Context::new(working_set, Span::unknown(), &[], offset);
330 return self.process_completion(&mut cell_path_completer, &ctx);
331 }
332 }
333 Expr::BinaryOp(lhs, op, _) => {
334 if op.span.contains(pos) {
335 let mut operator_completions = OperatorCompletion {
336 left_hand_side: lhs.as_ref(),
337 };
338 let (new_span, prefix) = strip_placeholder_if_any(working_set, &op.span, strip);
339 let ctx = Context::new(working_set, new_span, prefix, offset);
340 let results = self.process_completion(&mut operator_completions, &ctx);
341 if !results.is_empty() {
342 return results;
343 }
344 }
345 }
346 Expr::AttributeBlock(ab) => {
347 if let Some(span) = ab.attributes.iter().find_map(|attr| {
348 let span = attr.expr.span;
349 span.contains(pos).then_some(span)
350 }) {
351 let (new_span, prefix) = strip_placeholder_if_any(working_set, &span, strip);
352 let ctx = Context::new(working_set, new_span, prefix, offset);
353 return self.process_completion(&mut AttributeCompletion, &ctx);
354 };
355 let span = ab.item.span;
356 if span.contains(pos) {
357 let (new_span, prefix) = strip_placeholder_if_any(working_set, &span, strip);
358 let ctx = Context::new(working_set, new_span, prefix, offset);
359 return self.process_completion(&mut AttributableCompletion, &ctx);
360 }
361 }
362
363 Expr::Call(_) | Expr::ExternalCall(_, _) => {
367 let need_externals = !prefix_str.contains(' ');
368 let need_internals = !prefix_str.starts_with('^');
369 let mut span = element_expression.span;
370 if !need_internals {
371 span.start += 1;
372 };
373 suggestions.extend(self.command_completion_helper(
374 working_set,
375 span,
376 offset,
377 need_internals,
378 need_externals,
379 strip,
380 ))
381 }
382 _ => (),
383 }
384
385 match &element_expression.expr {
387 Expr::Call(call) => {
388 let mut positional_arg_indices = Vec::new();
392 for (arg_idx, arg) in call.arguments.iter().enumerate() {
393 let span = arg.span();
394
395 if !span.contains(pos) {
396 match arg {
397 Argument::Named(_) => (),
398 _ => positional_arg_indices.push(arg_idx),
399 }
400 continue;
401 }
402
403 let signature = working_set.get_decl(call.decl_id).signature();
404
405 let completion = {
407 match arg {
409 Argument::Named((name, short, value)) => {
411 if value.as_ref().is_none_or(|e| !e.span.contains(pos)) {
412 None
413 } else {
414 let flag = signature.get_long_flag(&name.item).or_else(|| {
417 short.as_ref().and_then(|s| {
418 signature.get_short_flag(
419 s.item.chars().next().unwrap_or('_'),
420 )
421 })
422 });
423 flag.and_then(|f| f.completion)
424 }
425 }
426 Argument::Positional(_) => {
428 let arg_pos = positional_arg_indices.len();
430 signature
431 .get_positional(arg_pos)
432 .and_then(|pos_arg| pos_arg.completion.clone())
433 }
434 _ => None,
435 }
436 };
437
438 if let Some(completion) = completion {
439 let (new_span, prefix) = match arg {
447 Argument::Named(_) => strip_placeholder_with_rsplit(
448 working_set,
449 &span,
450 |b| *b == b'=' || *b == b' ',
451 strip,
452 ),
453 _ => strip_placeholder_if_any(working_set, &span, strip),
454 };
455
456 let ctx = Context::new(working_set, new_span, prefix, offset);
457
458 match completion {
459 Completion::Command(decl_id) => {
460 let mut completer = CustomCompletion::new(
461 decl_id,
462 prefix_str.into(),
463 pos - offset,
464 FileCompletion,
465 );
466 suggestions
468 .splice(0..0, self.process_completion(&mut completer, &ctx));
469 break;
470 }
471 Completion::List(list) => {
472 let mut completer = StaticCompletion::new(list);
473 suggestions
475 .splice(0..0, self.process_completion(&mut completer, &ctx));
476 return suggestions;
478 }
479 }
480 } else if let Some(command_wide_completer) = signature.complete {
481 let flag_completions = {
482 let (new_span, prefix) =
483 strip_placeholder_if_any(working_set, &span, strip);
484 let ctx = Context::new(working_set, new_span, prefix, offset);
485 let flag_completion_helper = || {
486 let mut flag_completions = FlagCompletion {
487 decl_id: call.decl_id,
488 };
489 self.process_completion(&mut flag_completions, &ctx)
490 };
491
492 match arg {
493 Argument::Named(_) | Argument::Unknown(_)
495 if prefix.starts_with(b"-") =>
496 {
497 flag_completion_helper()
498 }
499 Argument::Positional(_) if prefix == b"-" => {
501 flag_completion_helper()
502 }
503 _ => vec![],
504 }
505 };
506
507 let completion = match command_wide_completer {
508 CommandWideCompleter::Command(decl_id) => {
509 CommandWideCompletion::command(
510 working_set,
511 decl_id,
512 element_expression,
513 strip,
514 )
515 }
516 CommandWideCompleter::External => self
517 .engine_state
518 .get_config()
519 .completions
520 .external
521 .completer
522 .as_ref()
523 .map(|closure| {
524 CommandWideCompletion::closure(
525 closure,
526 element_expression,
527 strip,
528 )
529 }),
530 };
531
532 if let Some(mut completion) = completion {
533 let ctx = Context::new(working_set, span, b"", offset);
534 let results = self.process_completion(&mut completion, &ctx);
535
536 let flags_length = flag_completions.len();
538 suggestions.splice(0..0, flag_completions);
539
540 suggestions.splice(flags_length..flags_length, results);
542
543 if !completion.need_fallback {
544 return suggestions;
545 }
546 }
547 }
548
549 let (new_span, prefix) = strip_placeholder_if_any(working_set, &span, strip);
551 let ctx = Context::new(working_set, new_span, prefix, offset);
552 let flag_completion_helper = || {
553 let mut flag_completions = FlagCompletion {
554 decl_id: call.decl_id,
555 };
556 self.process_completion(&mut flag_completions, &ctx)
557 };
558 suggestions.splice(
560 0..0,
561 match arg {
562 Argument::Named(_) | Argument::Unknown(_)
564 if prefix.starts_with(b"-") =>
565 {
566 flag_completion_helper()
567 }
568 Argument::Positional(_) if prefix == b"-" => flag_completion_helper(),
570 Argument::Positional(expr) => {
572 let command_head = working_set.get_decl(call.decl_id).name();
573 positional_arg_indices.push(arg_idx);
574 let mut need_fallback = suggestions.is_empty();
575 let results = self.argument_completion_helper(
576 PositionalArguments {
577 command_head,
578 positional_arg_indices,
579 arguments: &call.arguments,
580 expr,
581 },
582 pos,
583 &ctx,
584 &mut need_fallback,
585 );
586 if !need_fallback && suggestions.is_empty() {
588 return results;
589 }
590 results
591 }
592 _ => vec![],
593 },
594 );
595 break;
596 }
597 }
598 Expr::ExternalCall(head, arguments) => {
599 for (i, arg) in arguments.iter().enumerate() {
600 let span = arg.expr().span;
601 if span.contains(pos) {
602 if i == 0 {
605 let external_cmd = working_set.get_span_contents(head.span);
606 if external_cmd == b"sudo" || external_cmd == b"doas" {
607 let commands = self.command_completion_helper(
608 working_set,
609 span,
610 offset,
611 true,
612 true,
613 strip,
614 );
615 if !commands.is_empty() {
617 return commands;
618 }
619 }
620 }
621
622 let completion = self
624 .engine_state
625 .get_config()
626 .completions
627 .external
628 .completer
629 .as_ref()
630 .map(|closure| {
631 CommandWideCompletion::closure(closure, element_expression, strip)
632 });
633
634 if let Some(mut completion) = completion {
635 let ctx = Context::new(working_set, span, b"", offset);
636 let results = self.process_completion(&mut completion, &ctx);
637
638 suggestions.splice(0..0, results);
640
641 if !completion.need_fallback {
642 return suggestions;
643 }
644 }
645
646 if suggestions.is_empty() {
648 let (new_span, prefix) =
649 strip_placeholder_if_any(working_set, &span, strip);
650 let ctx = Context::new(working_set, new_span, prefix, offset);
651 return self.process_completion(&mut FileCompletion, &ctx);
652 }
653 break;
654 }
655 }
656 }
657 _ => (),
658 }
659
660 if suggestions.is_empty() {
662 let (new_span, prefix) =
663 strip_placeholder_if_any(working_set, &element_expression.span, strip);
664 let ctx = Context::new(working_set, new_span, prefix, offset);
665 suggestions.extend(self.process_completion(&mut FileCompletion, &ctx));
666 }
667 suggestions
668 }
669
670 fn variable_names_completion_helper(
671 &self,
672 working_set: &StateWorkingSet,
673 span: Span,
674 offset: usize,
675 strip: bool,
676 ) -> Vec<SemanticSuggestion> {
677 let (new_span, prefix) = strip_placeholder_if_any(working_set, &span, strip);
678 if !prefix.starts_with(b"$") {
679 return vec![];
680 }
681 let ctx = Context::new(working_set, new_span, prefix, offset);
682 self.process_completion(&mut VariableCompletion, &ctx)
683 }
684
685 fn command_completion_helper(
686 &self,
687 working_set: &StateWorkingSet,
688 span: Span,
689 offset: usize,
690 internals: bool,
691 externals: bool,
692 strip: bool,
693 ) -> Vec<SemanticSuggestion> {
694 let config = self.engine_state.get_config();
695 let mut command_completions = CommandCompletion {
696 internals,
697 externals: !internals || (externals && config.completions.external.enable),
698 };
699 let (new_span, prefix) = strip_placeholder_if_any(working_set, &span, strip);
700 let ctx = Context::new(working_set, new_span, prefix, offset);
701 self.process_completion(&mut command_completions, &ctx)
702 }
703
704 fn argument_completion_helper(
705 &self,
706 argument_info: PositionalArguments,
707 pos: usize,
708 ctx: &Context,
709 need_fallback: &mut bool,
710 ) -> Vec<SemanticSuggestion> {
711 let PositionalArguments {
712 command_head,
713 positional_arg_indices,
714 arguments,
715 expr,
716 } = argument_info;
717 match command_head {
719 "use" | "export use" | "overlay use" | "source-env"
721 if positional_arg_indices.len() <= 1 =>
722 {
723 *need_fallback = false;
724
725 return self.process_completion(
726 &mut DotNuCompletion {
727 std_virtual_path: command_head != "source-env",
728 },
729 ctx,
730 );
731 }
732 "use" | "export use" => {
735 *need_fallback = false;
736
737 let Some(Argument::Positional(Expression {
738 expr: Expr::String(module_name),
739 span,
740 ..
741 })) = positional_arg_indices
742 .first()
743 .and_then(|i| arguments.get(*i))
744 else {
745 return vec![];
746 };
747 let module_name = module_name.as_bytes();
748 let (module_id, temp_working_set) = match ctx.working_set.find_module(module_name) {
749 Some(module_id) => (module_id, None),
750 None => {
751 let mut temp_working_set =
752 StateWorkingSet::new(ctx.working_set.permanent_state);
753 let Some(module_id) = parse_module_file_or_dir(
754 &mut temp_working_set,
755 module_name,
756 *span,
757 None,
758 ) else {
759 return vec![];
760 };
761 (module_id, Some(temp_working_set))
762 }
763 };
764 let mut exportable_completion = ExportableCompletion {
765 module_id,
766 temp_working_set,
767 };
768 let mut complete_on_list_items = |items: &[ListItem]| -> Vec<SemanticSuggestion> {
769 for item in items {
770 let span = item.expr().span;
771 if span.contains(pos) {
772 let offset = span.start.saturating_sub(ctx.span.start);
773 let end_offset =
774 ctx.prefix.len().min(pos.min(span.end) - ctx.span.start + 1);
775 let new_ctx = Context::new(
776 ctx.working_set,
777 Span::new(span.start, ctx.span.end.min(span.end)),
778 ctx.prefix.get(offset..end_offset).unwrap_or_default(),
779 ctx.offset,
780 );
781 return self.process_completion(&mut exportable_completion, &new_ctx);
782 }
783 }
784 vec![]
785 };
786
787 match &expr.expr {
788 Expr::String(_) => {
789 return self.process_completion(&mut exportable_completion, ctx);
790 }
791 Expr::FullCellPath(fcp) => match &fcp.head.expr {
792 Expr::List(items) => {
793 return complete_on_list_items(items);
794 }
795 _ => return vec![],
796 },
797 _ => return vec![],
798 }
799 }
800 "which" => {
801 *need_fallback = false;
802
803 let mut completer = CommandCompletion {
804 internals: true,
805 externals: true,
806 };
807 return self.process_completion(&mut completer, ctx);
808 }
809 "attr complete" => {
810 *need_fallback = false;
811
812 let mut completer = CommandCompletion {
813 internals: true,
814 externals: false,
815 };
816 return self.process_completion(&mut completer, ctx);
817 }
818 _ => (),
819 }
820
821 let file_completion_helper = || self.process_completion(&mut FileCompletion, ctx);
823 match &expr.expr {
824 Expr::Directory(_, _) => {
825 *need_fallback = false;
826 self.process_completion(&mut DirectoryCompletion, ctx)
827 }
828 Expr::Filepath(_, _) | Expr::GlobPattern(_, _) => file_completion_helper(),
829 _ if *need_fallback => file_completion_helper(),
831 _ => vec![],
832 }
833 }
834
835 fn process_completion<T: Completer>(
837 &self,
838 completer: &mut T,
839 ctx: &Context,
840 ) -> Vec<SemanticSuggestion> {
841 let config = self.engine_state.get_config();
842
843 let options = CompletionOptions {
844 case_sensitive: config.completions.case_sensitive,
845 match_algorithm: config.completions.algorithm.into(),
846 sort: config.completions.sort,
847 };
848
849 completer.fetch(
850 ctx.working_set,
851 &self.stack,
852 String::from_utf8_lossy(ctx.prefix),
853 ctx.span,
854 ctx.offset,
855 &options,
856 )
857 }
858}
859
860impl ReedlineCompleter for NuCompleter {
861 fn complete(&mut self, line: &str, pos: usize) -> Vec<Suggestion> {
862 self.fetch_completions_at(line, pos)
863 .into_iter()
864 .map(|s| s.suggestion)
865 .collect()
866 }
867}
868
869pub fn map_value_completions<'a>(
870 list: impl Iterator<Item = &'a Value>,
871 span: Span,
872 offset: usize,
873) -> Vec<SemanticSuggestion> {
874 list.filter_map(move |x| {
875 if let Ok(s) = x.coerce_string() {
877 return Some(SemanticSuggestion {
878 suggestion: Suggestion {
879 value: s,
880 span: reedline::Span {
881 start: span.start - offset,
882 end: span.end - offset,
883 },
884 ..Suggestion::default()
885 },
886 kind: Some(SuggestionKind::Value(x.get_type())),
887 });
888 }
889
890 if let Ok(record) = x.as_record() {
892 let mut suggestion = Suggestion {
893 value: String::from(""), span: reedline::Span {
895 start: span.start - offset,
896 end: span.end - offset,
897 },
898 ..Suggestion::default()
899 };
900 let mut value_type = Type::String;
901
902 record.iter().for_each(|(key, value)| {
904 match key.as_str() {
905 "value" => {
906 value_type = value.get_type();
907 if let Ok(val_str) = value.coerce_string() {
909 suggestion.value = val_str;
911 }
912 }
913 "description" => {
914 if let Ok(desc_str) = value.coerce_string() {
916 suggestion.description = Some(desc_str);
918 }
919 }
920 "style" => {
921 suggestion.style = match value {
923 Value::String { val, .. } => Some(lookup_ansi_color_style(val)),
924 Value::Record { .. } => Some(color_record_to_nustyle(value)),
925 _ => None,
926 };
927 }
928 _ => (),
929 }
930 });
931
932 return Some(SemanticSuggestion {
933 suggestion,
934 kind: Some(SuggestionKind::Value(value_type)),
935 });
936 }
937
938 None
939 })
940 .collect()
941}
942
943#[cfg(test)]
944mod completer_tests {
945 use super::*;
946
947 #[test]
948 fn test_completion_helper() {
949 let mut engine_state =
950 nu_command::add_shell_command_context(nu_cmd_lang::create_default_context());
951
952 let delta = {
954 let working_set = nu_protocol::engine::StateWorkingSet::new(&engine_state);
955 working_set.render()
956 };
957
958 let result = engine_state.merge_delta(delta);
959 assert!(
960 result.is_ok(),
961 "Error merging delta: {:?}",
962 result.err().unwrap()
963 );
964
965 let completer = NuCompleter::new(engine_state.into(), Arc::new(Stack::new()));
966 let dataset = [
967 ("1 bit-sh", true, "b", vec!["bit-shl", "bit-shr"]),
968 ("1.0 bit-sh", false, "b", vec![]),
969 ("1 m", true, "m", vec!["mod"]),
970 ("1.0 m", true, "m", vec!["mod"]),
971 ("\"a\" s", true, "s", vec!["starts-with"]),
972 ("sudo", false, "", Vec::new()),
973 ("sudo l", true, "l", vec!["ls", "let", "lines", "loop"]),
974 (" sudo", false, "", Vec::new()),
975 (" sudo le", true, "le", vec!["let", "length"]),
976 (
977 "ls | c",
978 true,
979 "c",
980 vec!["cd", "config", "const", "cp", "cal"],
981 ),
982 ("ls | sudo m", true, "m", vec!["mv", "mut", "move"]),
983 ];
984 for (line, has_result, begins_with, expected_values) in dataset {
985 let result = completer.fetch_completions_at(line, line.len());
986 assert_eq!(!result.is_empty(), has_result, "line: {line}");
988
989 result
991 .iter()
992 .for_each(|x| assert!(x.suggestion.value.starts_with(begins_with)));
993
994 assert_eq!(
996 result
997 .iter()
998 .map(|x| expected_values.contains(&x.suggestion.value.as_str()))
999 .filter(|x| *x)
1000 .count(),
1001 expected_values.len(),
1002 "line: {line}"
1003 );
1004 }
1005 }
1006}