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