nu_cli/completions/
completer.rs

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