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, Span::unknown(), &[], 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 need_externals = !prefix_str.contains(' ');
344                let need_internals = !prefix_str.starts_with('^');
345                let mut span = element_expression.span;
346                if !need_internals {
347                    span.start += 1;
348                };
349                suggestions.extend(self.command_completion_helper(
350                    working_set,
351                    span,
352                    offset,
353                    need_internals,
354                    need_externals,
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 followings 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                                suggestions.splice(
443                                    0..0,
444                                    self.custom_completion_helper(
445                                        custom_completer,
446                                        prefix_str,
447                                        &ctx,
448                                        if strip { pos } else { pos + 1 },
449                                    ),
450                                );
451                                // Fallback completion is already handled in `CustomCompletion`
452                                return suggestions;
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                                suggestions.splice(
529                                    0..0,
530                                    self.custom_completion_helper(
531                                        custom_completer,
532                                        prefix_str,
533                                        &ctx,
534                                        if strip { pos } else { pos + 1 },
535                                    ),
536                                );
537                                // Fallback completion is already handled in `CustomCompletion`
538                                return suggestions;
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                                    true,
592                                    true,
593                                    strip,
594                                );
595                                // flags of sudo/doas can still be completed by external completer
596                                if !commands.is_empty() {
597                                    return commands;
598                                }
599                            }
600                        }
601
602                        // resort to external completer set in config
603                        let completion = self
604                            .engine_state
605                            .get_config()
606                            .completions
607                            .external
608                            .completer
609                            .as_ref()
610                            .map(|closure| {
611                                CommandWideCompletion::closure(closure, element_expression, strip)
612                            });
613
614                        if let Some(mut completion) = completion {
615                            let ctx = Context::new(working_set, span, b"", offset);
616                            let results = self.process_completion(&mut completion, &ctx);
617
618                            // Prioritize external results over (sub)commands
619                            suggestions.splice(0..0, results);
620
621                            if !completion.need_fallback {
622                                return suggestions;
623                            }
624                        }
625
626                        // for external path arguments with spaces, please check issue #15790
627                        if suggestions.is_empty() {
628                            let (new_span, prefix) =
629                                strip_placeholder_if_any(working_set, &span, strip);
630                            let ctx = Context::new(working_set, new_span, prefix, offset);
631                            return self.process_completion(&mut FileCompletion, &ctx);
632                        }
633                        break;
634                    }
635                }
636            }
637            _ => (),
638        }
639
640        // if no suggestions yet, fallback to file completion
641        if suggestions.is_empty() {
642            let (new_span, prefix) =
643                strip_placeholder_if_any(working_set, &element_expression.span, strip);
644            let ctx = Context::new(working_set, new_span, prefix, offset);
645            suggestions.extend(self.process_completion(&mut FileCompletion, &ctx));
646        }
647        suggestions
648    }
649
650    fn variable_names_completion_helper(
651        &self,
652        working_set: &StateWorkingSet,
653        span: Span,
654        offset: usize,
655        strip: bool,
656    ) -> Vec<SemanticSuggestion> {
657        let (new_span, prefix) = strip_placeholder_if_any(working_set, &span, strip);
658        if !prefix.starts_with(b"$") {
659            return vec![];
660        }
661        let ctx = Context::new(working_set, new_span, prefix, offset);
662        self.process_completion(&mut VariableCompletion, &ctx)
663    }
664
665    fn command_completion_helper(
666        &self,
667        working_set: &StateWorkingSet,
668        span: Span,
669        offset: usize,
670        internals: bool,
671        externals: bool,
672        strip: bool,
673    ) -> Vec<SemanticSuggestion> {
674        let config = self.engine_state.get_config();
675        let mut command_completions = CommandCompletion {
676            internals,
677            externals: !internals || (externals && config.completions.external.enable),
678        };
679        let (new_span, prefix) = strip_placeholder_if_any(working_set, &span, strip);
680        let ctx = Context::new(working_set, new_span, prefix, offset);
681        self.process_completion(&mut command_completions, &ctx)
682    }
683
684    fn custom_completion_helper(
685        &self,
686        custom_completion: Completion,
687        input: &str,
688        ctx: &Context,
689        pos: usize,
690    ) -> Vec<SemanticSuggestion> {
691        match custom_completion {
692            Completion::Command(decl_id) => {
693                let mut completer =
694                    CustomCompletion::new(decl_id, input.into(), pos - ctx.offset, FileCompletion);
695                self.process_completion(&mut completer, ctx)
696            }
697            Completion::List(list) => {
698                let mut completer = StaticCompletion::new(list);
699                self.process_completion(&mut completer, ctx)
700            }
701        }
702    }
703
704    fn command_wide_completion_helper(
705        &self,
706        signature: &Signature,
707        element_expression: &Expression,
708        ctx: &Context,
709        strip: bool,
710    ) -> (bool, Vec<SemanticSuggestion>) {
711        let completion = match signature.complete {
712            Some(CommandWideCompleter::Command(decl_id)) => {
713                CommandWideCompletion::command(ctx.working_set, decl_id, element_expression, strip)
714            }
715            Some(CommandWideCompleter::External) => self
716                .engine_state
717                .get_config()
718                .completions
719                .external
720                .completer
721                .as_ref()
722                .map(|closure| CommandWideCompletion::closure(closure, element_expression, strip)),
723            None => None,
724        };
725
726        if let Some(mut completion) = completion {
727            let res = self.process_completion(&mut completion, ctx);
728            (completion.need_fallback, res)
729        } else {
730            (true, vec![])
731        }
732    }
733
734    // Process the completion for a given completer
735    pub(crate) fn process_completion<T: Completer>(
736        &self,
737        completer: &mut T,
738        ctx: &Context,
739    ) -> Vec<SemanticSuggestion> {
740        let config = self.engine_state.get_config();
741
742        let options = CompletionOptions {
743            case_sensitive: config.completions.case_sensitive,
744            match_algorithm: config.completions.algorithm.into(),
745            sort: config.completions.sort,
746        };
747
748        completer.fetch(
749            ctx.working_set,
750            &self.stack,
751            String::from_utf8_lossy(ctx.prefix),
752            ctx.span,
753            ctx.offset,
754            &options,
755        )
756    }
757}
758
759impl ReedlineCompleter for NuCompleter {
760    fn complete(&mut self, line: &str, pos: usize) -> Vec<Suggestion> {
761        self.fetch_completions_at(line, pos)
762            .into_iter()
763            .map(|s| s.suggestion)
764            .collect()
765    }
766}
767
768#[cfg(test)]
769mod completer_tests {
770    use super::*;
771
772    #[test]
773    fn test_completion_helper() {
774        let mut engine_state =
775            nu_command::add_shell_command_context(nu_cmd_lang::create_default_context());
776
777        // Custom additions
778        let delta = {
779            let working_set = nu_protocol::engine::StateWorkingSet::new(&engine_state);
780            working_set.render()
781        };
782
783        let result = engine_state.merge_delta(delta);
784        assert!(
785            result.is_ok(),
786            "Error merging delta: {:?}",
787            result.err().unwrap()
788        );
789
790        let completer = NuCompleter::new(engine_state.into(), Arc::new(Stack::new()));
791        let dataset = [
792            ("1 bit-sh", true, "b", vec!["bit-shl", "bit-shr"]),
793            ("1.0 bit-sh", false, "b", vec![]),
794            ("1 m", true, "m", vec!["mod"]),
795            ("1.0 m", true, "m", vec!["mod"]),
796            ("\"a\" s", true, "s", vec!["starts-with"]),
797            ("sudo", false, "", Vec::new()),
798            ("sudo l", true, "l", vec!["ls", "let", "lines", "loop"]),
799            (" sudo", false, "", Vec::new()),
800            (" sudo le", true, "le", vec!["let", "length"]),
801            (
802                "ls | c",
803                true,
804                "c",
805                vec!["cd", "config", "const", "cp", "cal"],
806            ),
807            ("ls | sudo m", true, "m", vec!["mv", "mut", "move"]),
808        ];
809        for (line, has_result, begins_with, expected_values) in dataset {
810            let result = completer.fetch_completions_at(line, line.len());
811            // Test whether the result is empty or not
812            assert_eq!(!result.is_empty(), has_result, "line: {line}");
813
814            // Test whether the result begins with the expected value
815            result
816                .iter()
817                .for_each(|x| assert!(x.suggestion.value.starts_with(begins_with)));
818
819            // Test whether the result contains all the expected values
820            assert_eq!(
821                result
822                    .iter()
823                    .map(|x| expected_values.contains(&x.suggestion.value.as_str()))
824                    .filter(|x| *x)
825                    .count(),
826                expected_values.len(),
827                "line: {line}"
828            );
829        }
830    }
831}