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