nu_cli/completions/
completer.rs

1use crate::completions::{
2    AttributableCompletion, AttributeCompletion, CellPathCompletion, CommandCompletion, Completer,
3    CompletionOptions, CustomCompletion, DirectoryCompletion, DotNuCompletion, FileCompletion,
4    FlagCompletion, OperatorCompletion, VariableCompletion,
5};
6use nu_color_config::{color_record_to_nustyle, lookup_ansi_color_style};
7use nu_engine::eval_block;
8use nu_parser::{flatten_expression, parse};
9use nu_protocol::{
10    ast::{Argument, Block, Expr, Expression, FindMapResult, Traverse},
11    debugger::WithoutDebug,
12    engine::{Closure, EngineState, Stack, StateWorkingSet},
13    PipelineData, Span, Type, Value,
14};
15use reedline::{Completer as ReedlineCompleter, Suggestion};
16use std::{str, sync::Arc};
17
18use super::base::{SemanticSuggestion, SuggestionKind};
19
20/// Used as the function `f` in find_map Traverse
21///
22/// returns the inner-most pipeline_element of interest
23/// i.e. the one that contains given position and needs completion
24fn find_pipeline_element_by_position<'a>(
25    expr: &'a Expression,
26    working_set: &'a StateWorkingSet,
27    pos: usize,
28) -> FindMapResult<&'a Expression> {
29    // skip the entire expression if the position is not in it
30    if !expr.span.contains(pos) {
31        return FindMapResult::Stop;
32    }
33    let closure = |expr: &'a Expression| find_pipeline_element_by_position(expr, working_set, pos);
34    match &expr.expr {
35        Expr::Call(call) => call
36            .arguments
37            .iter()
38            .find_map(|arg| arg.expr().and_then(|e| e.find_map(working_set, &closure)))
39            // if no inner call/external_call found, then this is the inner-most one
40            .or(Some(expr))
41            .map(FindMapResult::Found)
42            .unwrap_or_default(),
43        Expr::ExternalCall(head, arguments) => arguments
44            .iter()
45            .find_map(|arg| arg.expr().find_map(working_set, &closure))
46            .or(head.as_ref().find_map(working_set, &closure))
47            .or(Some(expr))
48            .map(FindMapResult::Found)
49            .unwrap_or_default(),
50        // complete the operator
51        Expr::BinaryOp(lhs, _, rhs) => lhs
52            .find_map(working_set, &closure)
53            .or(rhs.find_map(working_set, &closure))
54            .or(Some(expr))
55            .map(FindMapResult::Found)
56            .unwrap_or_default(),
57        Expr::FullCellPath(fcp) => fcp
58            .head
59            .find_map(working_set, &closure)
60            .or(Some(expr))
61            .map(FindMapResult::Found)
62            .unwrap_or_default(),
63        Expr::Var(_) => FindMapResult::Found(expr),
64        Expr::AttributeBlock(ab) => ab
65            .attributes
66            .iter()
67            .map(|attr| &attr.expr)
68            .chain(Some(ab.item.as_ref()))
69            .find_map(|expr| expr.find_map(working_set, &closure))
70            .or(Some(expr))
71            .map(FindMapResult::Found)
72            .unwrap_or_default(),
73        _ => FindMapResult::Continue,
74    }
75}
76
77/// Before completion, an additional character `a` is added to the source as a placeholder for correct parsing results.
78/// This function helps to strip it
79fn strip_placeholder_if_any<'a>(
80    working_set: &'a StateWorkingSet,
81    span: &Span,
82    strip: bool,
83) -> (Span, &'a [u8]) {
84    let new_span = if strip {
85        let new_end = std::cmp::max(span.end - 1, span.start);
86        Span::new(span.start, new_end)
87    } else {
88        span.to_owned()
89    };
90    let prefix = working_set.get_span_contents(new_span);
91    (new_span, prefix)
92}
93
94/// Given a span with noise,
95/// 1. Call `rsplit` to get the last token
96/// 2. Strip the last placeholder from the token
97fn strip_placeholder_with_rsplit<'a>(
98    working_set: &'a StateWorkingSet,
99    span: &Span,
100    predicate: impl FnMut(&u8) -> bool,
101    strip: bool,
102) -> (Span, &'a [u8]) {
103    let span_content = working_set.get_span_contents(*span);
104    let mut prefix = span_content
105        .rsplit(predicate)
106        .next()
107        .unwrap_or(span_content);
108    let start = span.end.saturating_sub(prefix.len());
109    if strip && !prefix.is_empty() {
110        prefix = &prefix[..prefix.len() - 1];
111    }
112    let end = start + prefix.len();
113    (Span::new(start, end), prefix)
114}
115
116#[derive(Clone)]
117pub struct NuCompleter {
118    engine_state: Arc<EngineState>,
119    stack: Stack,
120}
121
122/// Common arguments required for Completer
123struct Context<'a> {
124    working_set: &'a StateWorkingSet<'a>,
125    span: Span,
126    prefix: &'a [u8],
127    offset: usize,
128}
129
130impl Context<'_> {
131    fn new<'a>(
132        working_set: &'a StateWorkingSet,
133        span: Span,
134        prefix: &'a [u8],
135        offset: usize,
136    ) -> Context<'a> {
137        Context {
138            working_set,
139            span,
140            prefix,
141            offset,
142        }
143    }
144}
145
146impl NuCompleter {
147    pub fn new(engine_state: Arc<EngineState>, stack: Arc<Stack>) -> Self {
148        Self {
149            engine_state,
150            stack: Stack::with_parent(stack).reset_out_dest().collect_value(),
151        }
152    }
153
154    pub fn fetch_completions_at(&self, line: &str, pos: usize) -> Vec<SemanticSuggestion> {
155        let mut working_set = StateWorkingSet::new(&self.engine_state);
156        let offset = working_set.next_span_start();
157        // TODO: Callers should be trimming the line themselves
158        let line = if line.len() > pos { &line[..pos] } else { line };
159        let block = parse(
160            &mut working_set,
161            Some("completer"),
162            // Add a placeholder `a` to the end
163            format!("{}a", line).as_bytes(),
164            false,
165        );
166        self.fetch_completions_by_block(block, &working_set, pos, offset, line, true)
167    }
168
169    /// For completion in LSP server.
170    /// We don't truncate the contents in order
171    /// to complete the definitions after the cursor.
172    ///
173    /// And we avoid the placeholder to reuse the parsed blocks
174    /// cached while handling other LSP requests, e.g. diagnostics
175    pub fn fetch_completions_within_file(
176        &self,
177        filename: &str,
178        pos: usize,
179        contents: &str,
180    ) -> Vec<SemanticSuggestion> {
181        let mut working_set = StateWorkingSet::new(&self.engine_state);
182        let block = parse(&mut working_set, Some(filename), contents.as_bytes(), false);
183        let Some(file_span) = working_set.get_span_for_filename(filename) else {
184            return vec![];
185        };
186        let offset = file_span.start;
187        self.fetch_completions_by_block(block.clone(), &working_set, pos, offset, contents, false)
188    }
189
190    fn fetch_completions_by_block(
191        &self,
192        block: Arc<Block>,
193        working_set: &StateWorkingSet,
194        pos: usize,
195        offset: usize,
196        contents: &str,
197        extra_placeholder: bool,
198    ) -> Vec<SemanticSuggestion> {
199        // Adjust offset so that the spans of the suggestions will start at the right
200        // place even with `only_buffer_difference: true`
201        let mut pos_to_search = pos + offset;
202        if !extra_placeholder {
203            pos_to_search = pos_to_search.saturating_sub(1);
204        }
205        let Some(element_expression) = block.find_map(working_set, &|expr: &Expression| {
206            find_pipeline_element_by_position(expr, working_set, pos_to_search)
207        }) else {
208            return vec![];
209        };
210
211        // text of element_expression
212        let start_offset = element_expression.span.start - offset;
213        let Some(text) = contents.get(start_offset..pos) else {
214            return vec![];
215        };
216        self.complete_by_expression(
217            working_set,
218            element_expression,
219            offset,
220            pos_to_search,
221            text,
222            extra_placeholder,
223        )
224    }
225
226    /// Complete given the expression of interest
227    /// Usually, the expression is get from `find_pipeline_element_by_position`
228    ///
229    /// # Arguments
230    /// * `offset` - start offset of current working_set span
231    /// * `pos` - cursor position, should be > offset
232    /// * `prefix_str` - all the text before the cursor, within the `element_expression`
233    /// * `strip` - whether to strip the extra placeholder from a span
234    fn complete_by_expression(
235        &self,
236        working_set: &StateWorkingSet,
237        element_expression: &Expression,
238        offset: usize,
239        pos: usize,
240        prefix_str: &str,
241        strip: bool,
242    ) -> Vec<SemanticSuggestion> {
243        let mut suggestions: Vec<SemanticSuggestion> = vec![];
244
245        match &element_expression.expr {
246            Expr::Var(_) => {
247                return self.variable_names_completion_helper(
248                    working_set,
249                    element_expression.span,
250                    offset,
251                    strip,
252                );
253            }
254            Expr::FullCellPath(full_cell_path) => {
255                // e.g. `$e<tab>` parsed as FullCellPath
256                // but `$e.<tab>` without placeholder should be taken as cell_path
257                if full_cell_path.tail.is_empty() && !prefix_str.ends_with('.') {
258                    return self.variable_names_completion_helper(
259                        working_set,
260                        element_expression.span,
261                        offset,
262                        strip,
263                    );
264                } else {
265                    let mut cell_path_completer = CellPathCompletion {
266                        full_cell_path,
267                        position: if strip { pos - 1 } else { pos },
268                    };
269                    let ctx = Context::new(working_set, Span::unknown(), &[], offset);
270                    return self.process_completion(&mut cell_path_completer, &ctx);
271                }
272            }
273            Expr::BinaryOp(lhs, op, _) => {
274                if op.span.contains(pos) {
275                    let mut operator_completions = OperatorCompletion {
276                        left_hand_side: lhs.as_ref(),
277                    };
278                    let (new_span, prefix) = strip_placeholder_if_any(working_set, &op.span, strip);
279                    let ctx = Context::new(working_set, new_span, prefix, offset);
280                    let results = self.process_completion(&mut operator_completions, &ctx);
281                    if !results.is_empty() {
282                        return results;
283                    }
284                }
285            }
286            Expr::AttributeBlock(ab) => {
287                if let Some(span) = ab.attributes.iter().find_map(|attr| {
288                    let span = attr.expr.span;
289                    span.contains(pos).then_some(span)
290                }) {
291                    let (new_span, prefix) = strip_placeholder_if_any(working_set, &span, strip);
292                    let ctx = Context::new(working_set, new_span, prefix, offset);
293                    return self.process_completion(&mut AttributeCompletion, &ctx);
294                };
295                let span = ab.item.span;
296                if span.contains(pos) {
297                    let (new_span, prefix) = strip_placeholder_if_any(working_set, &span, strip);
298                    let ctx = Context::new(working_set, new_span, prefix, offset);
299                    return self.process_completion(&mut AttributableCompletion, &ctx);
300                }
301            }
302
303            // NOTE: user defined internal commands can have any length
304            // e.g. `def "foo -f --ff bar"`, complete by line text
305            // instead of relying on the parsing result in that case
306            Expr::Call(_) | Expr::ExternalCall(_, _) => {
307                let need_externals = !prefix_str.contains(' ');
308                let need_internals = !prefix_str.starts_with('^');
309                let mut span = element_expression.span;
310                if !need_internals {
311                    span.start += 1;
312                };
313                suggestions.extend(self.command_completion_helper(
314                    working_set,
315                    span,
316                    offset,
317                    need_internals,
318                    need_externals,
319                    strip,
320                ))
321            }
322            _ => (),
323        }
324
325        // unfinished argument completion for commands
326        match &element_expression.expr {
327            Expr::Call(call) => {
328                // NOTE: the argument to complete is not necessarily the last one
329                // for lsp completion, we don't trim the text,
330                // so that `def`s after pos can be completed
331                for arg in call.arguments.iter() {
332                    let span = arg.span();
333                    if span.contains(pos) {
334                        // if customized completion specified, it has highest priority
335                        if let Some(decl_id) = arg.expr().and_then(|e| e.custom_completion) {
336                            // for `--foo <tab>` and `--foo=<tab>`, the arg span should be trimmed
337                            let (new_span, prefix) = if matches!(arg, Argument::Named(_)) {
338                                strip_placeholder_with_rsplit(
339                                    working_set,
340                                    &span,
341                                    |b| *b == b'=' || *b == b' ',
342                                    strip,
343                                )
344                            } else {
345                                strip_placeholder_if_any(working_set, &span, strip)
346                            };
347                            let ctx = Context::new(working_set, new_span, prefix, offset);
348
349                            let mut completer = CustomCompletion::new(
350                                decl_id,
351                                prefix_str.into(),
352                                pos - offset,
353                                FileCompletion,
354                            );
355
356                            suggestions.extend(self.process_completion(&mut completer, &ctx));
357                            break;
358                        }
359
360                        // normal arguments completion
361                        let (new_span, prefix) =
362                            strip_placeholder_if_any(working_set, &span, strip);
363                        let ctx = Context::new(working_set, new_span, prefix, offset);
364                        let flag_completion_helper = || {
365                            let mut flag_completions = FlagCompletion {
366                                decl_id: call.decl_id,
367                            };
368                            self.process_completion(&mut flag_completions, &ctx)
369                        };
370                        suggestions.extend(match arg {
371                            // flags
372                            Argument::Named(_) | Argument::Unknown(_)
373                                if prefix.starts_with(b"-") =>
374                            {
375                                flag_completion_helper()
376                            }
377                            // only when `strip` == false
378                            Argument::Positional(_) if prefix == b"-" => flag_completion_helper(),
379                            // complete according to expression type and command head
380                            Argument::Positional(expr) => {
381                                let command_head = working_set.get_span_contents(call.head);
382                                self.argument_completion_helper(
383                                    command_head,
384                                    expr,
385                                    &ctx,
386                                    suggestions.is_empty(),
387                                )
388                            }
389                            _ => vec![],
390                        });
391                        break;
392                    }
393                }
394            }
395            Expr::ExternalCall(head, arguments) => {
396                for (i, arg) in arguments.iter().enumerate() {
397                    let span = arg.expr().span;
398                    if span.contains(pos) {
399                        // e.g. `sudo l<tab>`
400                        // HACK: judge by index 0 is not accurate
401                        if i == 0 {
402                            let external_cmd = working_set.get_span_contents(head.span);
403                            if external_cmd == b"sudo" || external_cmd == b"doas" {
404                                let commands = self.command_completion_helper(
405                                    working_set,
406                                    span,
407                                    offset,
408                                    true,
409                                    true,
410                                    strip,
411                                );
412                                // flags of sudo/doas can still be completed by external completer
413                                if !commands.is_empty() {
414                                    return commands;
415                                }
416                            }
417                        }
418                        // resort to external completer set in config
419                        let config = self.engine_state.get_config();
420                        if let Some(closure) = config.completions.external.completer.as_ref() {
421                            let mut text_spans: Vec<String> =
422                                flatten_expression(working_set, element_expression)
423                                    .iter()
424                                    .map(|(span, _)| {
425                                        let bytes = working_set.get_span_contents(*span);
426                                        String::from_utf8_lossy(bytes).to_string()
427                                    })
428                                    .collect();
429                            let mut new_span = span;
430                            // strip the placeholder
431                            if strip {
432                                if let Some(last) = text_spans.last_mut() {
433                                    last.pop();
434                                    new_span = Span::new(span.start, span.end.saturating_sub(1));
435                                }
436                            }
437                            if let Some(external_result) =
438                                self.external_completion(closure, &text_spans, offset, new_span)
439                            {
440                                suggestions.extend(external_result);
441                                return suggestions;
442                            }
443                        }
444                        break;
445                    }
446                }
447            }
448            _ => (),
449        }
450
451        // if no suggestions yet, fallback to file completion
452        if suggestions.is_empty() {
453            let (new_span, prefix) = strip_placeholder_with_rsplit(
454                working_set,
455                &element_expression.span,
456                |c| *c == b' ',
457                strip,
458            );
459            let ctx = Context::new(working_set, new_span, prefix, offset);
460            suggestions.extend(self.process_completion(&mut FileCompletion, &ctx));
461        }
462        suggestions
463    }
464
465    fn variable_names_completion_helper(
466        &self,
467        working_set: &StateWorkingSet,
468        span: Span,
469        offset: usize,
470        strip: bool,
471    ) -> Vec<SemanticSuggestion> {
472        let (new_span, prefix) = strip_placeholder_if_any(working_set, &span, strip);
473        if !prefix.starts_with(b"$") {
474            return vec![];
475        }
476        let ctx = Context::new(working_set, new_span, prefix, offset);
477        self.process_completion(&mut VariableCompletion, &ctx)
478    }
479
480    fn command_completion_helper(
481        &self,
482        working_set: &StateWorkingSet,
483        span: Span,
484        offset: usize,
485        internals: bool,
486        externals: bool,
487        strip: bool,
488    ) -> Vec<SemanticSuggestion> {
489        let mut command_completions = CommandCompletion {
490            internals,
491            externals,
492        };
493        let (new_span, prefix) = strip_placeholder_if_any(working_set, &span, strip);
494        let ctx = Context::new(working_set, new_span, prefix, offset);
495        self.process_completion(&mut command_completions, &ctx)
496    }
497
498    fn argument_completion_helper(
499        &self,
500        command_head: &[u8],
501        expr: &Expression,
502        ctx: &Context,
503        need_fallback: bool,
504    ) -> Vec<SemanticSuggestion> {
505        // special commands
506        match command_head {
507            // complete module file/directory
508            // TODO: if module file already specified,
509            // should parse it to get modules/commands/consts to complete
510            b"use" | b"export use" | b"overlay use" | b"source-env" => {
511                return self.process_completion(&mut DotNuCompletion, ctx);
512            }
513            b"which" => {
514                let mut completer = CommandCompletion {
515                    internals: true,
516                    externals: true,
517                };
518                return self.process_completion(&mut completer, ctx);
519            }
520            _ => (),
521        }
522
523        // general positional arguments
524        let file_completion_helper = || self.process_completion(&mut FileCompletion, ctx);
525        match &expr.expr {
526            Expr::Directory(_, _) => self.process_completion(&mut DirectoryCompletion, ctx),
527            Expr::Filepath(_, _) | Expr::GlobPattern(_, _) => file_completion_helper(),
528            // fallback to file completion if necessary
529            _ if need_fallback => file_completion_helper(),
530            _ => vec![],
531        }
532    }
533
534    // Process the completion for a given completer
535    fn process_completion<T: Completer>(
536        &self,
537        completer: &mut T,
538        ctx: &Context,
539    ) -> Vec<SemanticSuggestion> {
540        let config = self.engine_state.get_config();
541
542        let options = CompletionOptions {
543            case_sensitive: config.completions.case_sensitive,
544            match_algorithm: config.completions.algorithm.into(),
545            sort: config.completions.sort,
546            ..Default::default()
547        };
548
549        completer.fetch(
550            ctx.working_set,
551            &self.stack,
552            String::from_utf8_lossy(ctx.prefix),
553            ctx.span,
554            ctx.offset,
555            &options,
556        )
557    }
558
559    fn external_completion(
560        &self,
561        closure: &Closure,
562        spans: &[String],
563        offset: usize,
564        span: Span,
565    ) -> Option<Vec<SemanticSuggestion>> {
566        let block = self.engine_state.get_block(closure.block_id);
567        let mut callee_stack = self
568            .stack
569            .captures_to_stack_preserve_out_dest(closure.captures.clone());
570
571        // Line
572        if let Some(pos_arg) = block.signature.required_positional.first() {
573            if let Some(var_id) = pos_arg.var_id {
574                callee_stack.add_var(
575                    var_id,
576                    Value::list(
577                        spans
578                            .iter()
579                            .map(|it| Value::string(it, Span::unknown()))
580                            .collect(),
581                        Span::unknown(),
582                    ),
583                );
584            }
585        }
586
587        let result = eval_block::<WithoutDebug>(
588            &self.engine_state,
589            &mut callee_stack,
590            block,
591            PipelineData::empty(),
592        );
593
594        match result.and_then(|data| data.into_value(span)) {
595            Ok(Value::List { vals, .. }) => {
596                let result =
597                    map_value_completions(vals.iter(), Span::new(span.start, span.end), offset);
598                Some(result)
599            }
600            Ok(Value::Nothing { .. }) => None,
601            Ok(value) => {
602                log::error!(
603                    "External completer returned invalid value of type {}",
604                    value.get_type().to_string()
605                );
606                Some(vec![])
607            }
608            Err(err) => {
609                log::error!("failed to eval completer block: {err}");
610                Some(vec![])
611            }
612        }
613    }
614}
615
616impl ReedlineCompleter for NuCompleter {
617    fn complete(&mut self, line: &str, pos: usize) -> Vec<Suggestion> {
618        self.fetch_completions_at(line, pos)
619            .into_iter()
620            .map(|s| s.suggestion)
621            .collect()
622    }
623}
624
625pub fn map_value_completions<'a>(
626    list: impl Iterator<Item = &'a Value>,
627    span: Span,
628    offset: usize,
629) -> Vec<SemanticSuggestion> {
630    list.filter_map(move |x| {
631        // Match for string values
632        if let Ok(s) = x.coerce_string() {
633            return Some(SemanticSuggestion {
634                suggestion: Suggestion {
635                    value: s,
636                    span: reedline::Span {
637                        start: span.start - offset,
638                        end: span.end - offset,
639                    },
640                    ..Suggestion::default()
641                },
642                kind: Some(SuggestionKind::Value(x.get_type())),
643            });
644        }
645
646        // Match for record values
647        if let Ok(record) = x.as_record() {
648            let mut suggestion = Suggestion {
649                value: String::from(""), // Initialize with empty string
650                span: reedline::Span {
651                    start: span.start - offset,
652                    end: span.end - offset,
653                },
654                ..Suggestion::default()
655            };
656            let mut value_type = Type::String;
657
658            // Iterate the cols looking for `value` and `description`
659            record.iter().for_each(|(key, value)| {
660                match key.as_str() {
661                    "value" => {
662                        value_type = value.get_type();
663                        // Convert the value to string
664                        if let Ok(val_str) = value.coerce_string() {
665                            // Update the suggestion value
666                            suggestion.value = val_str;
667                        }
668                    }
669                    "description" => {
670                        // Convert the value to string
671                        if let Ok(desc_str) = value.coerce_string() {
672                            // Update the suggestion value
673                            suggestion.description = Some(desc_str);
674                        }
675                    }
676                    "style" => {
677                        // Convert the value to string
678                        suggestion.style = match value {
679                            Value::String { val, .. } => Some(lookup_ansi_color_style(val)),
680                            Value::Record { .. } => Some(color_record_to_nustyle(value)),
681                            _ => None,
682                        };
683                    }
684                    _ => (),
685                }
686            });
687
688            return Some(SemanticSuggestion {
689                suggestion,
690                kind: Some(SuggestionKind::Value(value_type)),
691            });
692        }
693
694        None
695    })
696    .collect()
697}
698
699#[cfg(test)]
700mod completer_tests {
701    use super::*;
702
703    #[test]
704    fn test_completion_helper() {
705        let mut engine_state =
706            nu_command::add_shell_command_context(nu_cmd_lang::create_default_context());
707
708        // Custom additions
709        let delta = {
710            let working_set = nu_protocol::engine::StateWorkingSet::new(&engine_state);
711            working_set.render()
712        };
713
714        let result = engine_state.merge_delta(delta);
715        assert!(
716            result.is_ok(),
717            "Error merging delta: {:?}",
718            result.err().unwrap()
719        );
720
721        let completer = NuCompleter::new(engine_state.into(), Arc::new(Stack::new()));
722        let dataset = [
723            ("1 bit-sh", true, "b", vec!["bit-shl", "bit-shr"]),
724            ("1.0 bit-sh", false, "b", vec![]),
725            ("1 m", true, "m", vec!["mod"]),
726            ("1.0 m", true, "m", vec!["mod"]),
727            ("\"a\" s", true, "s", vec!["starts-with"]),
728            ("sudo", false, "", Vec::new()),
729            ("sudo l", true, "l", vec!["ls", "let", "lines", "loop"]),
730            (" sudo", false, "", Vec::new()),
731            (" sudo le", true, "le", vec!["let", "length"]),
732            (
733                "ls | c",
734                true,
735                "c",
736                vec!["cd", "config", "const", "cp", "cal"],
737            ),
738            ("ls | sudo m", true, "m", vec!["mv", "mut", "move"]),
739        ];
740        for (line, has_result, begins_with, expected_values) in dataset {
741            let result = completer.fetch_completions_at(line, line.len());
742            // Test whether the result is empty or not
743            assert_eq!(!result.is_empty(), has_result, "line: {}", line);
744
745            // Test whether the result begins with the expected value
746            result
747                .iter()
748                .for_each(|x| assert!(x.suggestion.value.starts_with(begins_with)));
749
750            // Test whether the result contains all the expected values
751            assert_eq!(
752                result
753                    .iter()
754                    .map(|x| expected_values.contains(&x.suggestion.value.as_str()))
755                    .filter(|x| *x)
756                    .count(),
757                expected_values.len(),
758                "line: {}",
759                line
760            );
761        }
762    }
763}