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                    },
354                    strip,
355                ))
356            }
357            _ => (),
358        }
359
360        // unfinished argument completion for commands
361        match &element_expression.expr {
362            Expr::Call(call) => {
363                let signature = working_set.get_decl(call.decl_id).signature();
364                // NOTE: the argument to complete is not necessarily the last one
365                // for lsp completion, we don't trim the text,
366                // so that `def`s after pos can be completed
367                let mut positional_arg_index = 0;
368
369                for (arg_idx, arg) in call.arguments.iter().enumerate() {
370                    let span = arg.span();
371
372                    // Skip arguments before the cursor
373                    if !span.contains(pos) {
374                        match arg {
375                            Argument::Named(_) => (),
376                            _ => positional_arg_index += 1,
377                        }
378                        continue;
379                    }
380
381                    // Context defaults to the whole argument, needs adjustments for specific situations
382                    let (new_span, prefix) = strip_placeholder_if_any(working_set, &span, strip);
383                    let ctx = Context::new(working_set, new_span, prefix, offset);
384                    let flag_completion_helper = |ctx: Context| {
385                        let mut flag_completions = FlagCompletion {
386                            decl_id: call.decl_id,
387                        };
388                        let mut res = self.process_completion(&mut flag_completions, &ctx);
389                        // For external command wrappers, which are parsed as internal calls,
390                        // also try command-wide completion for flag names
391                        // TODO: duplication?
392                        let command_wide_ctx = Context::new(working_set, span, b"", offset);
393                        res.extend(
394                            self.command_wide_completion_helper(
395                                &signature,
396                                element_expression,
397                                &command_wide_ctx,
398                                strip,
399                            )
400                            .1,
401                        );
402                        res
403                    };
404
405                    // Basically 2 kinds of argument completions for now:
406                    // 1. Flag name: 2 sources combined:
407                    //    * Signature based internal flags
408                    //    * Command-wide external flags
409                    // 2. Flag value/positional: try the following in order:
410                    //    1. Custom completion
411                    //    2. Command-wide completion
412                    //    3. Dynamic completion defined in trait `Command`
413                    //    4. Type-based default completion
414                    //    5. Fallback(file) completion
415                    match arg {
416                        // Flag value completion
417                        Argument::Named((name, short, Some(val))) if val.span.contains(pos) => {
418                            // for `--foo ..a|` and `--foo=..a|` (`|` represents the cursor), the
419                            // arg span should be trimmed:
420                            // - split the given span with `predicate` (b == '=' || b == ' '), and
421                            //   take the rightmost part:
422                            //   - "--foo ..a" => ["--foo", "..a"] => "..a"
423                            //   - "--foo=..a" => ["--foo", "..a"] => "..a"
424                            // - strip placeholder (`a`) if present
425                            let mut new_span = val.span;
426                            if strip {
427                                new_span.end = new_span.end.saturating_sub(1);
428                            }
429                            let prefix = working_set.get_span_contents(new_span);
430                            let ctx = Context::new(working_set, new_span, prefix, offset);
431
432                            // If we're completing the value of the flag,
433                            // search for the matching custom completion decl_id (long or short)
434                            let flag = signature.get_long_flag(&name.item).or_else(|| {
435                                short.as_ref().and_then(|s| {
436                                    signature.get_short_flag(s.item.chars().next().unwrap_or('_'))
437                                })
438                            });
439                            // Prioritize custom completion results over everything else
440                            if let Some(custom_completer) = flag.and_then(|f| f.completion) {
441                                suggestions.splice(
442                                    0..0,
443                                    self.custom_completion_helper(
444                                        custom_completer,
445                                        prefix_str,
446                                        &ctx,
447                                        if strip { pos } else { pos + 1 },
448                                    ),
449                                );
450                                // Fallback completion is already handled in `CustomCompletion`
451                                return suggestions;
452                            }
453
454                            // Try command-wide completion if specified by attributes
455                            // NOTE: `CommandWideCompletion` handles placeholder stripping internally
456                            let command_wide_ctx = Context::new(working_set, val.span, b"", offset);
457                            let (need_fallback, command_wide_res) = self
458                                .command_wide_completion_helper(
459                                    &signature,
460                                    element_expression,
461                                    &command_wide_ctx,
462                                    strip,
463                                );
464                            suggestions.splice(0..0, command_wide_res);
465                            if !need_fallback {
466                                return suggestions;
467                            }
468
469                            let mut flag_value_completion = ArgValueCompletion {
470                                arg_type: ArgType::Flag(Cow::from(name.as_ref().item.as_str())),
471                                // flag value doesn't need to fallback, just fill a
472                                // temp value false.
473                                need_fallback: false,
474                                completer: self,
475                                call,
476                                arg_idx,
477                                pos,
478                                strip,
479                            };
480                            suggestions.splice(
481                                0..0,
482                                self.process_completion(&mut flag_value_completion, &ctx),
483                            );
484                            return suggestions;
485                        }
486                        // Flag name completion
487                        Argument::Named((_, _, None)) => {
488                            suggestions.splice(0..0, flag_completion_helper(ctx));
489                        }
490                        // Edge case of lsp completion where the cursor is at the flag name,
491                        // with a flag value next to it.
492                        Argument::Named((_, _, Some(val))) => {
493                            // Span/prefix calibration
494                            let mut new_span = Span::new(span.start, val.span.start);
495                            let raw_prefix = working_set.get_span_contents(new_span);
496                            let prefix = raw_prefix.trim_ascii_end();
497                            let mut prefix = prefix.strip_suffix(b"=").unwrap_or(prefix);
498                            new_span.end = new_span
499                                .end
500                                .saturating_sub(raw_prefix.len() - prefix.len())
501                                .max(span.start);
502
503                            // Currently never reachable
504                            if strip {
505                                new_span.end = new_span.end.saturating_sub(1).max(span.start);
506                                prefix = prefix[..prefix.len() - 1].as_ref();
507                            }
508
509                            let ctx = Context::new(working_set, new_span, prefix, offset);
510                            suggestions.splice(0..0, flag_completion_helper(ctx));
511                        }
512                        Argument::Unknown(_) if prefix.starts_with(b"-") => {
513                            suggestions.splice(0..0, flag_completion_helper(ctx));
514                        }
515                        // only when `strip` == false
516                        Argument::Positional(_) if prefix == b"-" => {
517                            suggestions.splice(0..0, flag_completion_helper(ctx));
518                        }
519                        Argument::Positional(_) => {
520                            // Prioritize custom completion results over everything else
521                            if let Some(custom_completer) = signature
522                                // For positional arguments, check PositionalArg
523                                // Find the right positional argument by index
524                                .get_positional(positional_arg_index)
525                                .and_then(|pos_arg| pos_arg.completion.clone())
526                            {
527                                suggestions.splice(
528                                    0..0,
529                                    self.custom_completion_helper(
530                                        custom_completer,
531                                        prefix_str,
532                                        &ctx,
533                                        if strip { pos } else { pos + 1 },
534                                    ),
535                                );
536                                // Fallback completion is already handled in `CustomCompletion`
537                                return suggestions;
538                            }
539
540                            // Try command-wide completion if specified by attributes
541                            let command_wide_ctx = Context::new(working_set, span, b"", offset);
542                            let (need_fallback, command_wide_res) = self
543                                .command_wide_completion_helper(
544                                    &signature,
545                                    element_expression,
546                                    &command_wide_ctx,
547                                    strip,
548                                );
549                            suggestions.splice(0..0, command_wide_res);
550                            if !need_fallback {
551                                return suggestions;
552                            }
553
554                            // Default argument value completion
555                            let mut positional_value_completion = ArgValueCompletion {
556                                // arg_type: ArgType::Positional(positional_arg_index - 1),
557                                arg_type: ArgType::Positional(positional_arg_index),
558                                need_fallback: suggestions.is_empty(),
559                                completer: self,
560                                call,
561                                arg_idx,
562                                pos,
563                                strip,
564                            };
565
566                            suggestions.splice(
567                                0..0,
568                                self.process_completion(&mut positional_value_completion, &ctx),
569                            );
570                            return suggestions;
571                        }
572                        _ => (),
573                    }
574                    break;
575                }
576            }
577            Expr::ExternalCall(head, arguments) => {
578                for (i, arg) in arguments.iter().enumerate() {
579                    let span = arg.expr().span;
580                    if span.contains(pos) {
581                        // e.g. `sudo l<tab>`
582                        // HACK: judge by index 0 is not accurate
583                        if i == 0 {
584                            let external_cmd = working_set.get_span_contents(head.span);
585                            if external_cmd == b"sudo" || external_cmd == b"doas" {
586                                let commands = self.command_completion_helper(
587                                    working_set,
588                                    span,
589                                    offset,
590                                    CommandCompletionOptions {
591                                        internals: true,
592                                        externals: true,
593                                        builtins_only: false,
594                                    },
595                                    strip,
596                                );
597                                // flags of sudo/doas can still be completed by external completer
598                                if !commands.is_empty() {
599                                    return commands;
600                                }
601                            }
602                        }
603
604                        // resort to external completer set in config
605                        let completion = self
606                            .engine_state
607                            .get_config()
608                            .completions
609                            .external
610                            .completer
611                            .as_ref()
612                            .map(|closure| {
613                                CommandWideCompletion::closure(closure, element_expression, strip)
614                            });
615
616                        if let Some(mut completion) = completion {
617                            let ctx = Context::new(working_set, span, b"", offset);
618                            let results = self.process_completion(&mut completion, &ctx);
619
620                            // Prioritize external results over (sub)commands
621                            suggestions.splice(0..0, results);
622
623                            if !completion.need_fallback {
624                                return suggestions;
625                            }
626                        }
627
628                        // for external path arguments with spaces, please check issue #15790
629                        if suggestions.is_empty() {
630                            let (new_span, prefix) =
631                                strip_placeholder_if_any(working_set, &span, strip);
632                            let ctx = Context::new(working_set, new_span, prefix, offset);
633                            return self.process_completion(&mut FileCompletion, &ctx);
634                        }
635                        break;
636                    }
637                }
638            }
639            _ => (),
640        }
641
642        // if no suggestions yet, fallback to file completion
643        if suggestions.is_empty() {
644            let (new_span, prefix) =
645                strip_placeholder_if_any(working_set, &element_expression.span, strip);
646            let ctx = Context::new(working_set, new_span, prefix, offset);
647            suggestions.extend(self.process_completion(&mut FileCompletion, &ctx));
648        }
649        suggestions
650    }
651
652    fn variable_names_completion_helper(
653        &self,
654        working_set: &StateWorkingSet,
655        span: Span,
656        offset: usize,
657        strip: bool,
658    ) -> Vec<SemanticSuggestion> {
659        let (new_span, prefix) = strip_placeholder_if_any(working_set, &span, strip);
660        if !prefix.starts_with(b"$") {
661            return vec![];
662        }
663        let ctx = Context::new(working_set, new_span, prefix, offset);
664        self.process_completion(&mut VariableCompletion, &ctx)
665    }
666
667    fn command_completion_helper(
668        &self,
669        working_set: &StateWorkingSet,
670        span: Span,
671        offset: usize,
672        options: CommandCompletionOptions,
673        strip: bool,
674    ) -> Vec<SemanticSuggestion> {
675        let config = self.engine_state.get_config();
676        let mut command_completions = CommandCompletion {
677            internals: options.internals,
678            externals: !options.internals
679                || (options.externals && config.completions.external.enable),
680            builtins_only: options.builtins_only,
681        };
682        let (new_span, prefix) = strip_placeholder_if_any(working_set, &span, strip);
683        let ctx = Context::new(working_set, new_span, prefix, offset);
684        self.process_completion(&mut command_completions, &ctx)
685    }
686
687    fn custom_completion_helper(
688        &self,
689        custom_completion: Completion,
690        input: &str,
691        ctx: &Context,
692        pos: usize,
693    ) -> Vec<SemanticSuggestion> {
694        match custom_completion {
695            Completion::Command(decl_id) => {
696                let mut completer =
697                    CustomCompletion::new(decl_id, input.into(), pos - ctx.offset, FileCompletion);
698                self.process_completion(&mut completer, ctx)
699            }
700            Completion::List(list) => {
701                let mut completer = StaticCompletion::new(list);
702                self.process_completion(&mut completer, ctx)
703            }
704        }
705    }
706
707    fn command_wide_completion_helper(
708        &self,
709        signature: &Signature,
710        element_expression: &Expression,
711        ctx: &Context,
712        strip: bool,
713    ) -> (bool, Vec<SemanticSuggestion>) {
714        let completion = match signature.complete {
715            Some(CommandWideCompleter::Command(decl_id)) => {
716                CommandWideCompletion::command(ctx.working_set, decl_id, element_expression, strip)
717            }
718            Some(CommandWideCompleter::External) => self
719                .engine_state
720                .get_config()
721                .completions
722                .external
723                .completer
724                .as_ref()
725                .map(|closure| CommandWideCompletion::closure(closure, element_expression, strip)),
726            None => None,
727        };
728
729        if let Some(mut completion) = completion {
730            let res = self.process_completion(&mut completion, ctx);
731            (completion.need_fallback, res)
732        } else {
733            (true, vec![])
734        }
735    }
736
737    // Process the completion for a given completer
738    pub(crate) fn process_completion<T: Completer>(
739        &self,
740        completer: &mut T,
741        ctx: &Context,
742    ) -> Vec<SemanticSuggestion> {
743        let config = self.engine_state.get_config();
744
745        let options = CompletionOptions {
746            case_sensitive: config.completions.case_sensitive,
747            match_algorithm: config.completions.algorithm.into(),
748            sort: config.completions.sort,
749            match_description: false,
750        };
751
752        completer.fetch(
753            ctx.working_set,
754            &self.stack,
755            String::from_utf8_lossy(ctx.prefix),
756            ctx.span,
757            ctx.offset,
758            &options,
759        )
760    }
761}
762
763struct CommandCompletionOptions {
764    internals: bool,
765    externals: bool,
766    builtins_only: bool,
767}
768
769impl ReedlineCompleter for NuCompleter {
770    fn complete(&mut self, line: &str, pos: usize) -> Vec<Suggestion> {
771        self.fetch_completions_at(line, pos)
772            .into_iter()
773            .map(|s| s.suggestion)
774            .collect()
775    }
776}
777
778#[cfg(test)]
779mod completer_tests {
780    use super::*;
781
782    #[test]
783    fn test_completion_helper() {
784        let mut engine_state =
785            nu_command::add_shell_command_context(nu_cmd_lang::create_default_context());
786
787        // Custom additions
788        let delta = {
789            let working_set = nu_protocol::engine::StateWorkingSet::new(&engine_state);
790            working_set.render()
791        };
792
793        let result = engine_state.merge_delta(delta);
794        assert!(
795            result.is_ok(),
796            "Error merging delta: {:?}",
797            result.err().unwrap()
798        );
799
800        let completer = NuCompleter::new(engine_state.into(), Arc::new(Stack::new()));
801        let dataset = [
802            ("1 bit-sh", true, "b", vec!["bit-shl", "bit-shr"]),
803            ("1.0 bit-sh", false, "b", vec![]),
804            ("1 m", true, "m", vec!["mod"]),
805            ("1.0 m", true, "m", vec!["mod"]),
806            ("\"a\" s", true, "s", vec!["starts-with"]),
807            ("sudo", false, "", Vec::new()),
808            ("sudo l", true, "l", vec!["ls", "let", "lines", "loop"]),
809            (" sudo", false, "", Vec::new()),
810            (" sudo le", true, "le", vec!["let", "length"]),
811            (
812                "ls | c",
813                true,
814                "c",
815                vec!["cd", "config", "const", "cp", "cal"],
816            ),
817            ("ls | sudo m", true, "m", vec!["mv", "mut", "move"]),
818        ];
819        for (line, has_result, begins_with, expected_values) in dataset {
820            let result = completer.fetch_completions_at(line, line.len());
821            // Test whether the result is empty or not
822            assert_eq!(!result.is_empty(), has_result, "line: {line}");
823
824            // Test whether the result begins with the expected value
825            result
826                .iter()
827                .for_each(|x| assert!(x.suggestion.value.starts_with(begins_with)));
828
829            // Test whether the result contains all the expected values
830            assert_eq!(
831                result
832                    .iter()
833                    .map(|x| expected_values.contains(&x.suggestion.value.as_str()))
834                    .filter(|x| *x)
835                    .count(),
836                expected_values.len(),
837                "line: {line}"
838            );
839        }
840    }
841}