nu_cli/completions/
completer.rs

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
19/// Used as the function `f` in find_map Traverse
20///
21/// returns the inner-most pipeline_element of interest
22/// i.e. the one that contains given position and needs completion
23fn find_pipeline_element_by_position<'a>(
24    expr: &'a Expression,
25    working_set: &'a StateWorkingSet,
26    pos: usize,
27) -> FindMapResult<&'a Expression> {
28    // skip the entire expression if the position is not in it
29    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            // if no inner call/external_call found, then this is the inner-most one
39            .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        // complete the operator
50        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            // e.g. use std/util [<tab>
61            .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
81/// Before completion, an additional character `a` is added to the source as a placeholder for correct parsing results.
82/// This function helps to strip it
83fn 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
98/// Given a span with noise,
99/// 1. Call `rsplit` to get the last token
100/// 2. Strip the last placeholder from the token
101fn 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
126/// Common arguments required for Completer
127struct Context<'a> {
128    working_set: &'a StateWorkingSet<'a>,
129    span: Span,
130    prefix: &'a [u8],
131    offset: usize,
132}
133
134/// For argument completion
135struct PositionalArguments<'a> {
136    /// command name
137    command_head: &'a str,
138    /// indices of positional arguments
139    positional_arg_indices: Vec<usize>,
140    /// argument list
141    arguments: &'a [Argument],
142    /// expression of current argument
143    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        // TODO: Callers should be trimming the line themselves
174        let line = if line.len() > pos { &line[..pos] } else { line };
175        let block = parse(
176            &mut working_set,
177            Some("completer"),
178            // Add a placeholder `a` to the end
179            format!("{}a", line).as_bytes(),
180            false,
181        );
182        self.fetch_completions_by_block(block, &working_set, pos, offset, line, true)
183    }
184
185    /// For completion in LSP server.
186    /// We don't truncate the contents in order
187    /// to complete the definitions after the cursor.
188    ///
189    /// And we avoid the placeholder to reuse the parsed blocks
190    /// cached while handling other LSP requests, e.g. diagnostics
191    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        // Adjust offset so that the spans of the suggestions will start at the right
216        // place even with `only_buffer_difference: true`
217        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        // text of element_expression
228        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    /// Complete given the expression of interest
243    /// Usually, the expression is get from `find_pipeline_element_by_position`
244    ///
245    /// # Arguments
246    /// * `offset` - start offset of current working_set span
247    /// * `pos` - cursor position, should be > offset
248    /// * `prefix_str` - all the text before the cursor, within the `element_expression`
249    /// * `strip` - whether to strip the extra placeholder from a span
250    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                // e.g. `$e<tab>` parsed as FullCellPath
272                // but `$e.<tab>` without placeholder should be taken as cell_path
273                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            // NOTE: user defined internal commands can have any length
320            // e.g. `def "foo -f --ff bar"`, complete by line text
321            // instead of relying on the parsing result in that case
322            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        // unfinished argument completion for commands
342        match &element_expression.expr {
343            Expr::Call(call) => {
344                // NOTE: the argument to complete is not necessarily the last one
345                // for lsp completion, we don't trim the text,
346                // so that `def`s after pos can be completed
347                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 customized completion specified, it has highest priority
352                        if let Some(decl_id) = arg.expr().and_then(|e| e.custom_completion) {
353                            // for `--foo <tab>` and `--foo=<tab>`, the arg span should be trimmed
354                            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                        // normal arguments completion
378                        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                            // flags
389                            Argument::Named(_) | Argument::Unknown(_)
390                                if prefix.starts_with(b"-") =>
391                            {
392                                flag_completion_helper()
393                            }
394                            // only when `strip` == false
395                            Argument::Positional(_) if prefix == b"-" => flag_completion_helper(),
396                            // complete according to expression type and command head
397                            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                        // e.g. `sudo l<tab>`
425                        // HACK: judge by index 0 is not accurate
426                        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                                // flags of sudo/doas can still be completed by external completer
438                                if !commands.is_empty() {
439                                    return commands;
440                                }
441                            }
442                        }
443                        // resort to external completer set in config
444                        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                            // strip the placeholder
456                            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 no suggestions yet, fallback to file completion
477        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        // special commands
538        match command_head {
539            // complete module file/directory
540            "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            // NOTE: if module file already specified,
551            // should parse it to get modules/commands/consts to complete
552            "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        // general positional arguments
627        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            // fallback to file completion if necessary
632            _ if need_fallback => file_completion_helper(),
633            _ => vec![],
634        }
635    }
636
637    // Process the completion for a given completer
638    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        // Line
674        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        // Match for string values
734        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        // Match for record values
749        if let Ok(record) = x.as_record() {
750            let mut suggestion = Suggestion {
751                value: String::from(""), // Initialize with empty string
752                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            // Iterate the cols looking for `value` and `description`
761            record.iter().for_each(|(key, value)| {
762                match key.as_str() {
763                    "value" => {
764                        value_type = value.get_type();
765                        // Convert the value to string
766                        if let Ok(val_str) = value.coerce_string() {
767                            // Update the suggestion value
768                            suggestion.value = val_str;
769                        }
770                    }
771                    "description" => {
772                        // Convert the value to string
773                        if let Ok(desc_str) = value.coerce_string() {
774                            // Update the suggestion value
775                            suggestion.description = Some(desc_str);
776                        }
777                    }
778                    "style" => {
779                        // Convert the value to string
780                        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        // Custom additions
811        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            // Test whether the result is empty or not
845            assert_eq!(!result.is_empty(), has_result, "line: {}", line);
846
847            // Test whether the result begins with the expected value
848            result
849                .iter()
850                .for_each(|x| assert!(x.suggestion.value.starts_with(begins_with)));
851
852            // Test whether the result contains all the expected values
853            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}