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!("{line}a").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                            // Prioritize argument completions over (sub)commands
374                            suggestions.splice(0..0, self.process_completion(&mut completer, &ctx));
375                            break;
376                        }
377
378                        // normal arguments completion
379                        let (new_span, prefix) =
380                            strip_placeholder_if_any(working_set, &span, strip);
381                        let ctx = Context::new(working_set, new_span, prefix, offset);
382                        let flag_completion_helper = || {
383                            let mut flag_completions = FlagCompletion {
384                                decl_id: call.decl_id,
385                            };
386                            self.process_completion(&mut flag_completions, &ctx)
387                        };
388                        // Prioritize argument completions over (sub)commands
389                        suggestions.splice(
390                            0..0,
391                            match arg {
392                                // flags
393                                Argument::Named(_) | Argument::Unknown(_)
394                                    if prefix.starts_with(b"-") =>
395                                {
396                                    flag_completion_helper()
397                                }
398                                // only when `strip` == false
399                                Argument::Positional(_) if prefix == b"-" => {
400                                    flag_completion_helper()
401                                }
402                                // complete according to expression type and command head
403                                Argument::Positional(expr) => {
404                                    let command_head = working_set.get_decl(call.decl_id).name();
405                                    positional_arg_indices.push(arg_idx);
406                                    self.argument_completion_helper(
407                                        PositionalArguments {
408                                            command_head,
409                                            positional_arg_indices,
410                                            arguments: &call.arguments,
411                                            expr,
412                                        },
413                                        pos,
414                                        &ctx,
415                                        suggestions.is_empty(),
416                                    )
417                                }
418                                _ => vec![],
419                            },
420                        );
421                        break;
422                    } else if !matches!(arg, Argument::Named(_)) {
423                        positional_arg_indices.push(arg_idx);
424                    }
425                }
426            }
427            Expr::ExternalCall(head, arguments) => {
428                for (i, arg) in arguments.iter().enumerate() {
429                    let span = arg.expr().span;
430                    if span.contains(pos) {
431                        // e.g. `sudo l<tab>`
432                        // HACK: judge by index 0 is not accurate
433                        if i == 0 {
434                            let external_cmd = working_set.get_span_contents(head.span);
435                            if external_cmd == b"sudo" || external_cmd == b"doas" {
436                                let commands = self.command_completion_helper(
437                                    working_set,
438                                    span,
439                                    offset,
440                                    true,
441                                    true,
442                                    strip,
443                                );
444                                // flags of sudo/doas can still be completed by external completer
445                                if !commands.is_empty() {
446                                    return commands;
447                                }
448                            }
449                        }
450                        // resort to external completer set in config
451                        let config = self.engine_state.get_config();
452                        if let Some(closure) = config.completions.external.completer.as_ref() {
453                            let mut text_spans: Vec<String> =
454                                flatten_expression(working_set, element_expression)
455                                    .iter()
456                                    .map(|(span, _)| {
457                                        let bytes = working_set.get_span_contents(*span);
458                                        String::from_utf8_lossy(bytes).to_string()
459                                    })
460                                    .collect();
461                            let mut new_span = span;
462                            // strip the placeholder
463                            if strip {
464                                if let Some(last) = text_spans.last_mut() {
465                                    last.pop();
466                                    new_span = Span::new(span.start, span.end.saturating_sub(1));
467                                }
468                            }
469                            if let Some(external_result) =
470                                self.external_completion(closure, &text_spans, offset, new_span)
471                            {
472                                // Prioritize external results over (sub)commands
473                                suggestions.splice(0..0, external_result);
474                                return suggestions;
475                            }
476                        }
477                        // for external path arguments with spaces, please check issue #15790
478                        if suggestions.is_empty() {
479                            let (new_span, prefix) =
480                                strip_placeholder_if_any(working_set, &span, strip);
481                            let ctx = Context::new(working_set, new_span, prefix, offset);
482                            return self.process_completion(&mut FileCompletion, &ctx);
483                        }
484                        break;
485                    }
486                }
487            }
488            _ => (),
489        }
490
491        // if no suggestions yet, fallback to file completion
492        if suggestions.is_empty() {
493            let (new_span, prefix) = strip_placeholder_with_rsplit(
494                working_set,
495                &element_expression.span,
496                |c| *c == b' ',
497                strip,
498            );
499            let ctx = Context::new(working_set, new_span, prefix, offset);
500            suggestions.extend(self.process_completion(&mut FileCompletion, &ctx));
501        }
502        suggestions
503    }
504
505    fn variable_names_completion_helper(
506        &self,
507        working_set: &StateWorkingSet,
508        span: Span,
509        offset: usize,
510        strip: bool,
511    ) -> Vec<SemanticSuggestion> {
512        let (new_span, prefix) = strip_placeholder_if_any(working_set, &span, strip);
513        if !prefix.starts_with(b"$") {
514            return vec![];
515        }
516        let ctx = Context::new(working_set, new_span, prefix, offset);
517        self.process_completion(&mut VariableCompletion, &ctx)
518    }
519
520    fn command_completion_helper(
521        &self,
522        working_set: &StateWorkingSet,
523        span: Span,
524        offset: usize,
525        internals: bool,
526        externals: bool,
527        strip: bool,
528    ) -> Vec<SemanticSuggestion> {
529        let config = self.engine_state.get_config();
530        let mut command_completions = CommandCompletion {
531            internals,
532            externals: !internals || (externals && config.completions.external.enable),
533        };
534        let (new_span, prefix) = strip_placeholder_if_any(working_set, &span, strip);
535        let ctx = Context::new(working_set, new_span, prefix, offset);
536        self.process_completion(&mut command_completions, &ctx)
537    }
538
539    fn argument_completion_helper(
540        &self,
541        argument_info: PositionalArguments,
542        pos: usize,
543        ctx: &Context,
544        need_fallback: bool,
545    ) -> Vec<SemanticSuggestion> {
546        let PositionalArguments {
547            command_head,
548            positional_arg_indices,
549            arguments,
550            expr,
551        } = argument_info;
552        // special commands
553        match command_head {
554            // complete module file/directory
555            "use" | "export use" | "overlay use" | "source-env"
556                if positional_arg_indices.len() == 1 =>
557            {
558                return self.process_completion(
559                    &mut DotNuCompletion {
560                        std_virtual_path: command_head != "source-env",
561                    },
562                    ctx,
563                );
564            }
565            // NOTE: if module file already specified,
566            // should parse it to get modules/commands/consts to complete
567            "use" | "export use" => {
568                let Some(Argument::Positional(Expression {
569                    expr: Expr::String(module_name),
570                    span,
571                    ..
572                })) = positional_arg_indices
573                    .first()
574                    .and_then(|i| arguments.get(*i))
575                else {
576                    return vec![];
577                };
578                let module_name = module_name.as_bytes();
579                let (module_id, temp_working_set) = match ctx.working_set.find_module(module_name) {
580                    Some(module_id) => (module_id, None),
581                    None => {
582                        let mut temp_working_set =
583                            StateWorkingSet::new(ctx.working_set.permanent_state);
584                        let Some(module_id) = parse_module_file_or_dir(
585                            &mut temp_working_set,
586                            module_name,
587                            *span,
588                            None,
589                        ) else {
590                            return vec![];
591                        };
592                        (module_id, Some(temp_working_set))
593                    }
594                };
595                let mut exportable_completion = ExportableCompletion {
596                    module_id,
597                    temp_working_set,
598                };
599                let mut complete_on_list_items = |items: &[ListItem]| -> Vec<SemanticSuggestion> {
600                    for item in items {
601                        let span = item.expr().span;
602                        if span.contains(pos) {
603                            let offset = span.start.saturating_sub(ctx.span.start);
604                            let end_offset =
605                                ctx.prefix.len().min(pos.min(span.end) - ctx.span.start + 1);
606                            let new_ctx = Context::new(
607                                ctx.working_set,
608                                Span::new(span.start, ctx.span.end.min(span.end)),
609                                ctx.prefix.get(offset..end_offset).unwrap_or_default(),
610                                ctx.offset,
611                            );
612                            return self.process_completion(&mut exportable_completion, &new_ctx);
613                        }
614                    }
615                    vec![]
616                };
617
618                match &expr.expr {
619                    Expr::String(_) => {
620                        return self.process_completion(&mut exportable_completion, ctx);
621                    }
622                    Expr::FullCellPath(fcp) => match &fcp.head.expr {
623                        Expr::List(items) => {
624                            return complete_on_list_items(items);
625                        }
626                        _ => return vec![],
627                    },
628                    _ => return vec![],
629                }
630            }
631            "which" => {
632                let mut completer = CommandCompletion {
633                    internals: true,
634                    externals: true,
635                };
636                return self.process_completion(&mut completer, ctx);
637            }
638            _ => (),
639        }
640
641        // general positional arguments
642        let file_completion_helper = || self.process_completion(&mut FileCompletion, ctx);
643        match &expr.expr {
644            Expr::Directory(_, _) => self.process_completion(&mut DirectoryCompletion, ctx),
645            Expr::Filepath(_, _) | Expr::GlobPattern(_, _) => file_completion_helper(),
646            // fallback to file completion if necessary
647            _ if need_fallback => file_completion_helper(),
648            _ => vec![],
649        }
650    }
651
652    // Process the completion for a given completer
653    fn process_completion<T: Completer>(
654        &self,
655        completer: &mut T,
656        ctx: &Context,
657    ) -> Vec<SemanticSuggestion> {
658        let config = self.engine_state.get_config();
659
660        let options = CompletionOptions {
661            case_sensitive: config.completions.case_sensitive,
662            match_algorithm: config.completions.algorithm.into(),
663            sort: config.completions.sort,
664        };
665
666        completer.fetch(
667            ctx.working_set,
668            &self.stack,
669            String::from_utf8_lossy(ctx.prefix),
670            ctx.span,
671            ctx.offset,
672            &options,
673        )
674    }
675
676    fn external_completion(
677        &self,
678        closure: &Closure,
679        spans: &[String],
680        offset: usize,
681        span: Span,
682    ) -> Option<Vec<SemanticSuggestion>> {
683        let block = self.engine_state.get_block(closure.block_id);
684        let mut callee_stack = self
685            .stack
686            .captures_to_stack_preserve_out_dest(closure.captures.clone());
687
688        // Line
689        if let Some(pos_arg) = block.signature.required_positional.first() {
690            if let Some(var_id) = pos_arg.var_id {
691                callee_stack.add_var(
692                    var_id,
693                    Value::list(
694                        spans
695                            .iter()
696                            .map(|it| Value::string(it, Span::unknown()))
697                            .collect(),
698                        Span::unknown(),
699                    ),
700                );
701            }
702        }
703
704        let result = eval_block::<WithoutDebug>(
705            &self.engine_state,
706            &mut callee_stack,
707            block,
708            PipelineData::empty(),
709        );
710
711        match result.and_then(|data| data.into_value(span)) {
712            Ok(Value::List { vals, .. }) => {
713                let result =
714                    map_value_completions(vals.iter(), Span::new(span.start, span.end), offset);
715                Some(result)
716            }
717            Ok(Value::Nothing { .. }) => None,
718            Ok(value) => {
719                log::error!(
720                    "External completer returned invalid value of type {}",
721                    value.get_type().to_string()
722                );
723                Some(vec![])
724            }
725            Err(err) => {
726                log::error!("failed to eval completer block: {err}");
727                Some(vec![])
728            }
729        }
730    }
731}
732
733impl ReedlineCompleter for NuCompleter {
734    fn complete(&mut self, line: &str, pos: usize) -> Vec<Suggestion> {
735        self.fetch_completions_at(line, pos)
736            .into_iter()
737            .map(|s| s.suggestion)
738            .collect()
739    }
740}
741
742pub fn map_value_completions<'a>(
743    list: impl Iterator<Item = &'a Value>,
744    span: Span,
745    offset: usize,
746) -> Vec<SemanticSuggestion> {
747    list.filter_map(move |x| {
748        // Match for string values
749        if let Ok(s) = x.coerce_string() {
750            return Some(SemanticSuggestion {
751                suggestion: Suggestion {
752                    value: s,
753                    span: reedline::Span {
754                        start: span.start - offset,
755                        end: span.end - offset,
756                    },
757                    ..Suggestion::default()
758                },
759                kind: Some(SuggestionKind::Value(x.get_type())),
760            });
761        }
762
763        // Match for record values
764        if let Ok(record) = x.as_record() {
765            let mut suggestion = Suggestion {
766                value: String::from(""), // Initialize with empty string
767                span: reedline::Span {
768                    start: span.start - offset,
769                    end: span.end - offset,
770                },
771                ..Suggestion::default()
772            };
773            let mut value_type = Type::String;
774
775            // Iterate the cols looking for `value` and `description`
776            record.iter().for_each(|(key, value)| {
777                match key.as_str() {
778                    "value" => {
779                        value_type = value.get_type();
780                        // Convert the value to string
781                        if let Ok(val_str) = value.coerce_string() {
782                            // Update the suggestion value
783                            suggestion.value = val_str;
784                        }
785                    }
786                    "description" => {
787                        // Convert the value to string
788                        if let Ok(desc_str) = value.coerce_string() {
789                            // Update the suggestion value
790                            suggestion.description = Some(desc_str);
791                        }
792                    }
793                    "style" => {
794                        // Convert the value to string
795                        suggestion.style = match value {
796                            Value::String { val, .. } => Some(lookup_ansi_color_style(val)),
797                            Value::Record { .. } => Some(color_record_to_nustyle(value)),
798                            _ => None,
799                        };
800                    }
801                    _ => (),
802                }
803            });
804
805            return Some(SemanticSuggestion {
806                suggestion,
807                kind: Some(SuggestionKind::Value(value_type)),
808            });
809        }
810
811        None
812    })
813    .collect()
814}
815
816#[cfg(test)]
817mod completer_tests {
818    use super::*;
819
820    #[test]
821    fn test_completion_helper() {
822        let mut engine_state =
823            nu_command::add_shell_command_context(nu_cmd_lang::create_default_context());
824
825        // Custom additions
826        let delta = {
827            let working_set = nu_protocol::engine::StateWorkingSet::new(&engine_state);
828            working_set.render()
829        };
830
831        let result = engine_state.merge_delta(delta);
832        assert!(
833            result.is_ok(),
834            "Error merging delta: {:?}",
835            result.err().unwrap()
836        );
837
838        let completer = NuCompleter::new(engine_state.into(), Arc::new(Stack::new()));
839        let dataset = [
840            ("1 bit-sh", true, "b", vec!["bit-shl", "bit-shr"]),
841            ("1.0 bit-sh", false, "b", vec![]),
842            ("1 m", true, "m", vec!["mod"]),
843            ("1.0 m", true, "m", vec!["mod"]),
844            ("\"a\" s", true, "s", vec!["starts-with"]),
845            ("sudo", false, "", Vec::new()),
846            ("sudo l", true, "l", vec!["ls", "let", "lines", "loop"]),
847            (" sudo", false, "", Vec::new()),
848            (" sudo le", true, "le", vec!["let", "length"]),
849            (
850                "ls | c",
851                true,
852                "c",
853                vec!["cd", "config", "const", "cp", "cal"],
854            ),
855            ("ls | sudo m", true, "m", vec!["mv", "mut", "move"]),
856        ];
857        for (line, has_result, begins_with, expected_values) in dataset {
858            let result = completer.fetch_completions_at(line, line.len());
859            // Test whether the result is empty or not
860            assert_eq!(!result.is_empty(), has_result, "line: {line}");
861
862            // Test whether the result begins with the expected value
863            result
864                .iter()
865                .for_each(|x| assert!(x.suggestion.value.starts_with(begins_with)));
866
867            // Test whether the result contains all the expected values
868            assert_eq!(
869                result
870                    .iter()
871                    .map(|x| expected_values.contains(&x.suggestion.value.as_str()))
872                    .filter(|x| *x)
873                    .count(),
874                expected_values.len(),
875                "line: {line}"
876            );
877        }
878    }
879}