Skip to main content

nu_cli/completions/
completer.rs

1use crate::completions::{
2    ArgValueCompletion, AttributableCompletion, AttributeCompletion, CellPathCompletion,
3    CommandCompletion, Completer, CompletionOptions, CustomCompletion, FileCompletion,
4    FlagCompletion, OperatorCompletion, VariableCompletion, base::SemanticSuggestion,
5};
6use nu_parser::parse;
7use nu_protocol::{
8    CommandWideCompleter, Completion, GetSpan, Signature, Span,
9    ast::{Argument, Block, Expr, Expression, PipelineRedirection, RedirectionTarget, Traverse},
10    engine::{ArgType, EngineState, Stack, StateWorkingSet},
11};
12use reedline::{Completer as ReedlineCompleter, Suggestion};
13use std::sync::Arc;
14use std::{borrow::Cow, ops::ControlFlow};
15
16use super::{StaticCompletion, custom_completions::CommandWideCompletion};
17
18/// Used as the function `f` in find_map Traverse
19///
20/// returns the inner-most pipeline_element of interest
21/// i.e. the one that contains given position and needs completion
22fn find_pipeline_element_by_position<'a>(
23    expr: &'a Expression,
24    working_set: &'a StateWorkingSet,
25    pos: usize,
26) -> ControlFlow<Option<&'a Expression>> {
27    // skip the entire expression if the position is not in it
28    if !expr.span.contains(pos) {
29        return ControlFlow::Break(None);
30    }
31    let closure = |expr: &'a Expression| find_pipeline_element_by_position(expr, working_set, pos);
32    let found = |x| ControlFlow::Break(Some(x));
33    match &expr.expr {
34        Expr::RowCondition(block_id)
35        | Expr::Subexpression(block_id)
36        | Expr::Block(block_id)
37        | Expr::Closure(block_id) => {
38            let block = working_set.get_block(*block_id);
39            // check redirection target for sub blocks before diving recursively into them
40            check_redirection_in_block(block.as_ref(), pos)
41                .map(found)
42                .unwrap_or(ControlFlow::Continue(()))
43        }
44        Expr::Call(call) => call
45            .arguments
46            .iter()
47            .find_map(|arg| arg.expr().and_then(|e| e.find_map(working_set, &closure)))
48            .map(found)
49            .unwrap_or(found(expr)),
50        Expr::ExternalCall(head, arguments) => arguments
51            .iter()
52            .find_map(|arg| arg.expr().find_map(working_set, &closure))
53            .or_else(|| {
54                // For aliased external_call, the span of original external command head should fail the
55                // contains(pos) check, thus avoiding recursion into its head expression.
56                // See issue #7648 for details.
57                let span = working_set.get_span(head.span_id);
58                if span.contains(pos) {
59                    // This is for complicated external head expressions, e.g. `^(echo<tab> foo)`
60                    head.as_ref().find_map(working_set, &closure)
61                } else {
62                    None
63                }
64            })
65            .map(found)
66            .unwrap_or(found(expr)),
67        // complete the operator
68        Expr::BinaryOp(lhs, _, rhs) => lhs
69            .find_map(working_set, &closure)
70            .or_else(|| rhs.find_map(working_set, &closure))
71            .map(found)
72            .unwrap_or(found(expr)),
73        Expr::FullCellPath(fcp) => fcp
74            .head
75            .find_map(working_set, &closure)
76            .map(found)
77            // e.g. use std/util [<tab>
78            .or_else(|| {
79                (fcp.head.span.contains(pos) && matches!(fcp.head.expr, Expr::List(_)))
80                    .then_some(ControlFlow::Continue(()))
81            })
82            .unwrap_or(found(expr)),
83        Expr::Var(_) => found(expr),
84        Expr::AttributeBlock(ab) => ab
85            .attributes
86            .iter()
87            .map(|attr| &attr.expr)
88            .chain(Some(ab.item.as_ref()))
89            .find_map(|expr| expr.find_map(working_set, &closure))
90            .map(found)
91            .unwrap_or(found(expr)),
92        _ => ControlFlow::Continue(()),
93    }
94}
95
96/// Helper function to extract file-path expression from redirection target
97fn check_redirection_target(target: &RedirectionTarget, pos: usize) -> Option<&Expression> {
98    let expr = target.expr();
99    expr.and_then(|expression| {
100        if let Expr::String(_) = expression.expr
101            && expression.span.contains(pos)
102        {
103            expr
104        } else {
105            None
106        }
107    })
108}
109
110/// For redirection target completion
111/// https://github.com/nushell/nushell/issues/16827
112fn check_redirection_in_block(block: &Block, pos: usize) -> Option<&Expression> {
113    block.pipelines.iter().find_map(|pipeline| {
114        pipeline.elements.iter().find_map(|element| {
115            element.redirection.as_ref().and_then(|redir| match redir {
116                PipelineRedirection::Single { target, .. } => check_redirection_target(target, pos),
117                PipelineRedirection::Separate { out, err } => check_redirection_target(out, pos)
118                    .or_else(|| check_redirection_target(err, pos)),
119            })
120        })
121    })
122}
123
124/// Before completion, an additional character `a` is added to the source as a placeholder for correct parsing results.
125/// This function helps to strip it
126fn strip_placeholder_if_any<'a>(
127    working_set: &'a StateWorkingSet,
128    span: &Span,
129    strip: bool,
130) -> (Span, &'a [u8]) {
131    let new_span = if strip {
132        let new_end = std::cmp::max(span.end - 1, span.start);
133        Span::new(span.start, new_end)
134    } else {
135        span.to_owned()
136    };
137    let prefix = working_set.get_span_contents(new_span);
138    (new_span, prefix)
139}
140
141#[derive(Clone)]
142pub struct NuCompleter {
143    engine_state: Arc<EngineState>,
144    stack: Stack,
145}
146
147/// Common arguments required for Completer
148pub(crate) struct Context<'a> {
149    pub working_set: &'a StateWorkingSet<'a>,
150    pub span: Span,
151    pub prefix: &'a [u8],
152    pub offset: usize,
153}
154
155impl Context<'_> {
156    pub(crate) fn new<'a>(
157        working_set: &'a StateWorkingSet,
158        span: Span,
159        prefix: &'a [u8],
160        offset: usize,
161    ) -> Context<'a> {
162        Context {
163            working_set,
164            span,
165            prefix,
166            offset,
167        }
168    }
169}
170
171impl NuCompleter {
172    pub fn new(engine_state: Arc<EngineState>, stack: Arc<Stack>) -> Self {
173        Self {
174            engine_state,
175            stack: Stack::with_parent(stack).reset_out_dest().collect_value(),
176        }
177    }
178
179    pub fn fetch_completions_at(&self, line: &str, pos: usize) -> Vec<SemanticSuggestion> {
180        let mut working_set = StateWorkingSet::new(&self.engine_state);
181        let offset = working_set.next_span_start();
182        // TODO: Callers should be trimming the line themselves
183        let line = if line.len() > pos { &line[..pos] } else { line };
184        let block = parse(
185            &mut working_set,
186            Some("completer"),
187            // Add a placeholder `a` to the end
188            format!("{line}a").as_bytes(),
189            false,
190        );
191        self.fetch_completions_by_block(block, &working_set, pos, offset, line, true)
192    }
193
194    /// For completion in LSP server.
195    /// We don't truncate the contents in order
196    /// to complete the definitions after the cursor.
197    ///
198    /// And we avoid the placeholder to reuse the parsed blocks
199    /// cached while handling other LSP requests, e.g. diagnostics
200    pub fn fetch_completions_within_file(
201        &self,
202        filename: &str,
203        pos: usize,
204        contents: &str,
205    ) -> Vec<SemanticSuggestion> {
206        let mut working_set = StateWorkingSet::new(&self.engine_state);
207        let block = parse(&mut working_set, Some(filename), contents.as_bytes(), false);
208        let Some(file_span) = working_set.get_span_for_filename(filename) else {
209            return vec![];
210        };
211        let offset = file_span.start;
212        self.fetch_completions_by_block(block.clone(), &working_set, pos, offset, contents, false)
213    }
214
215    fn fetch_completions_by_block(
216        &self,
217        block: Arc<Block>,
218        working_set: &StateWorkingSet,
219        pos: usize,
220        offset: usize,
221        contents: &str,
222        extra_placeholder: bool,
223    ) -> Vec<SemanticSuggestion> {
224        // Adjust offset so that the spans of the suggestions will start at the right
225        // place even with `only_buffer_difference: true`
226        let mut pos_to_search = pos + offset;
227        if !extra_placeholder {
228            pos_to_search = pos_to_search.saturating_sub(1);
229        }
230        let Some(element_expression) = block
231            .find_map(working_set, &|expr: &Expression| {
232                find_pipeline_element_by_position(expr, working_set, pos_to_search)
233            })
234            .or_else(|| check_redirection_in_block(block.as_ref(), pos_to_search))
235        else {
236            return vec![];
237        };
238
239        // text of element_expression
240        let start_offset = element_expression.span.start - offset;
241        let Some(text) = contents.get(start_offset..pos) else {
242            return vec![];
243        };
244        self.complete_by_expression(
245            working_set,
246            element_expression,
247            offset,
248            pos_to_search,
249            text,
250            extra_placeholder,
251        )
252    }
253
254    /// Complete given the expression of interest
255    /// Usually, the expression is get from `find_pipeline_element_by_position`
256    ///
257    /// # Arguments
258    /// * `offset` - start offset of current working_set span
259    /// * `pos` - cursor position, should be > offset
260    /// * `prefix_str` - all the text before the cursor, within the `element_expression`
261    /// * `strip` - whether to strip the extra placeholder from a span
262    fn complete_by_expression(
263        &self,
264        working_set: &StateWorkingSet,
265        element_expression: &Expression,
266        offset: usize,
267        pos: usize,
268        prefix_str: &str,
269        strip: bool,
270    ) -> Vec<SemanticSuggestion> {
271        let mut suggestions: Vec<SemanticSuggestion> = vec![];
272
273        match &element_expression.expr {
274            Expr::Var(_) => {
275                return self.variable_names_completion_helper(
276                    working_set,
277                    element_expression.span,
278                    offset,
279                    strip,
280                );
281            }
282            Expr::FullCellPath(full_cell_path) => {
283                // e.g. `$e<tab>` parsed as FullCellPath
284                // but `$e.<tab>` without placeholder should be taken as cell_path
285                if full_cell_path.tail.is_empty() && !prefix_str.ends_with('.') {
286                    return self.variable_names_completion_helper(
287                        working_set,
288                        element_expression.span,
289                        offset,
290                        strip,
291                    );
292                } else {
293                    let mut cell_path_completer = CellPathCompletion {
294                        full_cell_path,
295                        position: if strip { pos - 1 } else { pos },
296                    };
297                    let ctx = Context::new(working_set, element_expression.span, &[], offset);
298                    return self.process_completion(&mut cell_path_completer, &ctx);
299                }
300            }
301            Expr::BinaryOp(lhs, op, _) => {
302                if op.span.contains(pos) {
303                    let mut operator_completions = OperatorCompletion {
304                        left_hand_side: lhs.as_ref(),
305                    };
306                    let (new_span, prefix) = strip_placeholder_if_any(working_set, &op.span, strip);
307                    let ctx = Context::new(working_set, new_span, prefix, offset);
308                    let results = self.process_completion(&mut operator_completions, &ctx);
309                    if !results.is_empty() {
310                        return results;
311                    }
312                }
313            }
314            Expr::AttributeBlock(ab) => {
315                if let Some(span) = ab.attributes.iter().find_map(|attr| {
316                    let span = attr.expr.span;
317                    span.contains(pos).then_some(span)
318                }) {
319                    let (new_span, prefix) = strip_placeholder_if_any(working_set, &span, strip);
320                    let ctx = Context::new(working_set, new_span, prefix, offset);
321                    return self.process_completion(&mut AttributeCompletion, &ctx);
322                };
323                let span = ab.item.span;
324                if span.contains(pos) {
325                    let (new_span, prefix) = strip_placeholder_if_any(working_set, &span, strip);
326                    let ctx = Context::new(working_set, new_span, prefix, offset);
327                    return self.process_completion(&mut AttributableCompletion, &ctx);
328                }
329            }
330
331            // NOTE: user defined internal commands can have any length
332            // e.g. `def "foo -f --ff bar"`, complete by line text
333            // instead of relying on the parsing result in that case
334            Expr::Call(_) | Expr::ExternalCall(_, _) => {
335                let force_external = prefix_str.starts_with('^');
336                let force_internal = prefix_str.starts_with('%');
337                let force_builtins_only = force_internal;
338
339                let need_externals = !prefix_str.contains(' ') && !force_internal;
340                let need_internals = !force_external;
341                let mut span = element_expression.span;
342                if force_external || force_internal {
343                    span.start += 1;
344                };
345                suggestions.extend(self.command_completion_helper(
346                    working_set,
347                    span,
348                    offset,
349                    CommandCompletionOptions {
350                        internals: need_internals,
351                        externals: need_externals,
352                        builtins_only: force_builtins_only,
353                        quote_internals: false,
354                    },
355                    strip,
356                ))
357            }
358            _ => (),
359        }
360
361        // unfinished argument completion for commands
362        match &element_expression.expr {
363            Expr::Call(call) => {
364                let signature = working_set.get_decl(call.decl_id).signature();
365                // NOTE: the argument to complete is not necessarily the last one
366                // for lsp completion, we don't trim the text,
367                // so that `def`s after pos can be completed
368                let mut positional_arg_index = 0;
369
370                for (arg_idx, arg) in call.arguments.iter().enumerate() {
371                    let span = arg.span();
372
373                    // Skip arguments before the cursor
374                    if !span.contains(pos) {
375                        match arg {
376                            Argument::Named(_) => (),
377                            _ => positional_arg_index += 1,
378                        }
379                        continue;
380                    }
381
382                    // Context defaults to the whole argument, needs adjustments for specific situations
383                    let (new_span, prefix) = strip_placeholder_if_any(working_set, &span, strip);
384                    let ctx = Context::new(working_set, new_span, prefix, offset);
385                    let flag_completion_helper = |ctx: Context| {
386                        let mut flag_completions = FlagCompletion {
387                            decl_id: call.decl_id,
388                        };
389                        let mut res = self.process_completion(&mut flag_completions, &ctx);
390                        // For external command wrappers, which are parsed as internal calls,
391                        // also try command-wide completion for flag names
392                        // TODO: duplication?
393                        let command_wide_ctx = Context::new(working_set, span, b"", offset);
394                        res.extend(
395                            self.command_wide_completion_helper(
396                                &signature,
397                                element_expression,
398                                &command_wide_ctx,
399                                strip,
400                            )
401                            .1,
402                        );
403                        res
404                    };
405
406                    // Basically 2 kinds of argument completions for now:
407                    // 1. Flag name: 2 sources combined:
408                    //    * Signature based internal flags
409                    //    * Command-wide external flags
410                    // 2. Flag value/positional: try the following in order:
411                    //    1. Custom completion
412                    //    2. Command-wide completion
413                    //    3. Dynamic completion defined in trait `Command`
414                    //    4. Type-based default completion
415                    //    5. Fallback(file) completion
416                    match arg {
417                        // Flag value completion
418                        Argument::Named((name, short, Some(val))) if val.span.contains(pos) => {
419                            // for `--foo ..a|` and `--foo=..a|` (`|` represents the cursor), the
420                            // arg span should be trimmed:
421                            // - split the given span with `predicate` (b == '=' || b == ' '), and
422                            //   take the rightmost part:
423                            //   - "--foo ..a" => ["--foo", "..a"] => "..a"
424                            //   - "--foo=..a" => ["--foo", "..a"] => "..a"
425                            // - strip placeholder (`a`) if present
426                            let mut new_span = val.span;
427                            if strip {
428                                new_span.end = new_span.end.saturating_sub(1);
429                            }
430                            let prefix = working_set.get_span_contents(new_span);
431                            let ctx = Context::new(working_set, new_span, prefix, offset);
432
433                            // If we're completing the value of the flag,
434                            // search for the matching custom completion decl_id (long or short)
435                            let flag = signature.get_long_flag(&name.item).or_else(|| {
436                                short.as_ref().and_then(|s| {
437                                    signature.get_short_flag(s.item.chars().next().unwrap_or('_'))
438                                })
439                            });
440                            // Prioritize custom completion results over everything else
441                            if let Some(custom_completer) = flag.and_then(|f| f.completion) {
442                                let (need_fallback, new_suggestions) = self
443                                    .custom_completion_helper(
444                                        custom_completer,
445                                        prefix_str,
446                                        &ctx,
447                                        if strip { pos } else { pos + 1 },
448                                    );
449                                suggestions.splice(0..0, new_suggestions);
450                                if !need_fallback {
451                                    return suggestions;
452                                }
453                            }
454
455                            // Try command-wide completion if specified by attributes
456                            // NOTE: `CommandWideCompletion` handles placeholder stripping internally
457                            let command_wide_ctx = Context::new(working_set, val.span, b"", offset);
458                            let (need_fallback, command_wide_res) = self
459                                .command_wide_completion_helper(
460                                    &signature,
461                                    element_expression,
462                                    &command_wide_ctx,
463                                    strip,
464                                );
465                            suggestions.splice(0..0, command_wide_res);
466                            if !need_fallback {
467                                return suggestions;
468                            }
469
470                            let mut flag_value_completion = ArgValueCompletion {
471                                arg_type: ArgType::Flag(Cow::from(name.as_ref().item.as_str())),
472                                // flag value doesn't need to fallback, just fill a
473                                // temp value false.
474                                need_fallback: false,
475                                completer: self,
476                                call,
477                                arg_idx,
478                                pos,
479                                strip,
480                            };
481                            suggestions.splice(
482                                0..0,
483                                self.process_completion(&mut flag_value_completion, &ctx),
484                            );
485                            return suggestions;
486                        }
487                        // Flag name completion
488                        Argument::Named((_, _, None)) => {
489                            suggestions.splice(0..0, flag_completion_helper(ctx));
490                        }
491                        // Edge case of lsp completion where the cursor is at the flag name,
492                        // with a flag value next to it.
493                        Argument::Named((_, _, Some(val))) => {
494                            // Span/prefix calibration
495                            let mut new_span = Span::new(span.start, val.span.start);
496                            let raw_prefix = working_set.get_span_contents(new_span);
497                            let prefix = raw_prefix.trim_ascii_end();
498                            let mut prefix = prefix.strip_suffix(b"=").unwrap_or(prefix);
499                            new_span.end = new_span
500                                .end
501                                .saturating_sub(raw_prefix.len() - prefix.len())
502                                .max(span.start);
503
504                            // Currently never reachable
505                            if strip {
506                                new_span.end = new_span.end.saturating_sub(1).max(span.start);
507                                prefix = prefix[..prefix.len() - 1].as_ref();
508                            }
509
510                            let ctx = Context::new(working_set, new_span, prefix, offset);
511                            suggestions.splice(0..0, flag_completion_helper(ctx));
512                        }
513                        Argument::Unknown(_) if prefix.starts_with(b"-") => {
514                            suggestions.splice(0..0, flag_completion_helper(ctx));
515                        }
516                        // only when `strip` == false
517                        Argument::Positional(_) if prefix == b"-" => {
518                            suggestions.splice(0..0, flag_completion_helper(ctx));
519                        }
520                        Argument::Positional(_) => {
521                            // Prioritize custom completion results over everything else
522                            if let Some(custom_completer) = signature
523                                // For positional arguments, check PositionalArg
524                                // Find the right positional argument by index
525                                .get_positional(positional_arg_index)
526                                .and_then(|pos_arg| pos_arg.completion.clone())
527                            {
528                                let (need_fallback, new_suggestions) = self
529                                    .custom_completion_helper(
530                                        custom_completer,
531                                        prefix_str,
532                                        &ctx,
533                                        if strip { pos } else { pos + 1 },
534                                    );
535                                suggestions.splice(0..0, new_suggestions);
536                                if !need_fallback {
537                                    return suggestions;
538                                }
539                            }
540
541                            // Try command-wide completion if specified by attributes
542                            let command_wide_ctx = Context::new(working_set, span, b"", offset);
543                            let (need_fallback, command_wide_res) = self
544                                .command_wide_completion_helper(
545                                    &signature,
546                                    element_expression,
547                                    &command_wide_ctx,
548                                    strip,
549                                );
550                            suggestions.splice(0..0, command_wide_res);
551                            if !need_fallback {
552                                return suggestions;
553                            }
554
555                            // Default argument value completion
556                            let mut positional_value_completion = ArgValueCompletion {
557                                // arg_type: ArgType::Positional(positional_arg_index - 1),
558                                arg_type: ArgType::Positional(positional_arg_index),
559                                need_fallback: suggestions.is_empty(),
560                                completer: self,
561                                call,
562                                arg_idx,
563                                pos,
564                                strip,
565                            };
566
567                            suggestions.splice(
568                                0..0,
569                                self.process_completion(&mut positional_value_completion, &ctx),
570                            );
571                            return suggestions;
572                        }
573                        _ => (),
574                    }
575                    break;
576                }
577            }
578            Expr::ExternalCall(head, arguments) => {
579                for (i, arg) in arguments.iter().enumerate() {
580                    let span = arg.expr().span;
581                    if span.contains(pos) {
582                        // e.g. `sudo l<tab>`
583                        // HACK: judge by index 0 is not accurate
584                        if i == 0 {
585                            let external_cmd = working_set.get_span_contents(head.span);
586                            if external_cmd == b"sudo" || external_cmd == b"doas" {
587                                let commands = self.command_completion_helper(
588                                    working_set,
589                                    span,
590                                    offset,
591                                    CommandCompletionOptions {
592                                        internals: true,
593                                        externals: true,
594                                        builtins_only: false,
595                                        quote_internals: false,
596                                    },
597                                    strip,
598                                );
599                                // flags of sudo/doas can still be completed by external completer
600                                if !commands.is_empty() {
601                                    return commands;
602                                }
603                            }
604                        }
605
606                        // resort to external completer set in config
607                        let completion = self
608                            .engine_state
609                            .get_config()
610                            .completions
611                            .external
612                            .completer
613                            .as_ref()
614                            .map(|closure| {
615                                CommandWideCompletion::closure(closure, element_expression, strip)
616                            });
617
618                        if let Some(mut completion) = completion {
619                            let ctx = Context::new(working_set, span, b"", offset);
620                            let results = self.process_completion(&mut completion, &ctx);
621
622                            // Prioritize external results over (sub)commands
623                            suggestions.splice(0..0, results);
624
625                            if !completion.need_fallback {
626                                return suggestions;
627                            }
628                        }
629
630                        // for external path arguments with spaces, please check issue #15790
631                        if suggestions.is_empty() {
632                            let (new_span, prefix) =
633                                strip_placeholder_if_any(working_set, &span, strip);
634                            let ctx = Context::new(working_set, new_span, prefix, offset);
635                            return self.process_completion(&mut FileCompletion, &ctx);
636                        }
637                        break;
638                    }
639                }
640            }
641            _ => (),
642        }
643
644        // if no suggestions yet, fallback to file completion
645        if suggestions.is_empty() {
646            let (new_span, prefix) =
647                strip_placeholder_if_any(working_set, &element_expression.span, strip);
648            let ctx = Context::new(working_set, new_span, prefix, offset);
649            suggestions.extend(self.process_completion(&mut FileCompletion, &ctx));
650        }
651        suggestions
652    }
653
654    fn variable_names_completion_helper(
655        &self,
656        working_set: &StateWorkingSet,
657        span: Span,
658        offset: usize,
659        strip: bool,
660    ) -> Vec<SemanticSuggestion> {
661        let (new_span, prefix) = strip_placeholder_if_any(working_set, &span, strip);
662        if !prefix.starts_with(b"$") {
663            return vec![];
664        }
665        let ctx = Context::new(working_set, new_span, prefix, offset);
666        self.process_completion(&mut VariableCompletion, &ctx)
667    }
668
669    fn command_completion_helper(
670        &self,
671        working_set: &StateWorkingSet,
672        span: Span,
673        offset: usize,
674        options: CommandCompletionOptions,
675        strip: bool,
676    ) -> Vec<SemanticSuggestion> {
677        let config = self.engine_state.get_config();
678        let mut command_completions = CommandCompletion {
679            internals: options.internals,
680            externals: !options.internals
681                || (options.externals && config.completions.external.enable),
682            builtins_only: options.builtins_only,
683            quote_internals: options.quote_internals,
684        };
685        let (new_span, prefix) = strip_placeholder_if_any(working_set, &span, strip);
686        let ctx = Context::new(working_set, new_span, prefix, offset);
687        self.process_completion(&mut command_completions, &ctx)
688    }
689
690    fn custom_completion_helper(
691        &self,
692        custom_completion: Completion,
693        input: &str,
694        ctx: &Context,
695        pos: usize,
696    ) -> (bool, Vec<SemanticSuggestion>) {
697        match custom_completion {
698            Completion::Command(decl_id) => {
699                let mut completer =
700                    CustomCompletion::new(decl_id, input.into(), pos - ctx.offset, false);
701                let suggestions = self.process_completion(&mut completer, ctx);
702                (completer.need_fallback, suggestions)
703            }
704            Completion::List(list) => {
705                let mut completer = StaticCompletion::new(list);
706                (false, self.process_completion(&mut completer, ctx))
707            }
708        }
709    }
710
711    fn command_wide_completion_helper(
712        &self,
713        signature: &Signature,
714        element_expression: &Expression,
715        ctx: &Context,
716        strip: bool,
717    ) -> (bool, Vec<SemanticSuggestion>) {
718        let completion = match signature.complete {
719            Some(CommandWideCompleter::Command(decl_id)) => {
720                CommandWideCompletion::command(ctx.working_set, decl_id, element_expression, strip)
721            }
722            Some(CommandWideCompleter::External) => self
723                .engine_state
724                .get_config()
725                .completions
726                .external
727                .completer
728                .as_ref()
729                .map(|closure| CommandWideCompletion::closure(closure, element_expression, strip)),
730            None => None,
731        };
732
733        if let Some(mut completion) = completion {
734            let res = self.process_completion(&mut completion, ctx);
735            (completion.need_fallback, res)
736        } else {
737            (true, vec![])
738        }
739    }
740
741    // Process the completion for a given completer
742    pub(crate) fn process_completion<T: Completer>(
743        &self,
744        completer: &mut T,
745        ctx: &Context,
746    ) -> Vec<SemanticSuggestion> {
747        let config = self.engine_state.get_config();
748
749        let options = CompletionOptions {
750            case_sensitive: config.completions.case_sensitive,
751            match_algorithm: config.completions.algorithm.into(),
752            sort: config.completions.sort,
753            match_description: false,
754        };
755
756        completer.fetch(
757            ctx.working_set,
758            &self.stack,
759            String::from_utf8_lossy(ctx.prefix),
760            ctx.span,
761            ctx.offset,
762            &options,
763        )
764    }
765}
766
767struct CommandCompletionOptions {
768    internals: bool,
769    externals: bool,
770    builtins_only: bool,
771    quote_internals: bool,
772}
773
774impl ReedlineCompleter for NuCompleter {
775    fn complete(&mut self, line: &str, pos: usize) -> Vec<Suggestion> {
776        self.fetch_completions_at(line, pos)
777            .into_iter()
778            .map(|s| s.suggestion)
779            .collect()
780    }
781}
782
783#[cfg(test)]
784mod completer_tests {
785    use super::*;
786
787    #[test]
788    fn test_completion_helper() {
789        let mut engine_state =
790            nu_command::add_shell_command_context(nu_cmd_lang::create_default_context());
791
792        // Custom additions
793        let delta = {
794            let working_set = nu_protocol::engine::StateWorkingSet::new(&engine_state);
795            working_set.render()
796        };
797
798        let result = engine_state.merge_delta(delta);
799        assert!(
800            result.is_ok(),
801            "Error merging delta: {:?}",
802            result.err().unwrap()
803        );
804
805        let completer = NuCompleter::new(engine_state.into(), Arc::new(Stack::new()));
806        let dataset = [
807            ("1 bit-sh", true, "b", vec!["bit-shl", "bit-shr"]),
808            ("1.0 bit-sh", false, "b", vec![]),
809            ("1 m", true, "m", vec!["mod"]),
810            ("1.0 m", true, "m", vec!["mod"]),
811            ("\"a\" s", true, "s", vec!["starts-with"]),
812            ("sudo", false, "", Vec::new()),
813            ("sudo l", true, "l", vec!["ls", "let", "lines", "loop"]),
814            (" sudo", false, "", Vec::new()),
815            (" sudo le", true, "le", vec!["let", "length"]),
816            (
817                "ls | c",
818                true,
819                "c",
820                vec!["cd", "config", "const", "cp", "cal"],
821            ),
822            ("ls | sudo m", true, "m", vec!["mv", "mut", "move"]),
823        ];
824        for (line, has_result, begins_with, expected_values) in dataset {
825            let result = completer.fetch_completions_at(line, line.len());
826            // Test whether the result is empty or not
827            assert_eq!(!result.is_empty(), has_result, "line: {line}");
828
829            // Test whether the result begins with the expected value
830            result
831                .iter()
832                .for_each(|x| assert!(x.suggestion.value.starts_with(begins_with)));
833
834            // Test whether the result contains all the expected values
835            assert_eq!(
836                result
837                    .iter()
838                    .map(|x| expected_values.contains(&x.suggestion.value.as_str()))
839                    .filter(|x| *x)
840                    .count(),
841                expected_values.len(),
842                "line: {line}"
843            );
844        }
845    }
846}