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_engine::eval_block;
9use nu_parser::{flatten_expression, parse, parse_module_file_or_dir};
10use nu_protocol::{
11 PipelineData, Span, Type, Value,
12 ast::{Argument, Block, Expr, Expression, FindMapResult, ListItem, Traverse},
13 debugger::WithoutDebug,
14 engine::{Closure, EngineState, Stack, StateWorkingSet},
15};
16use reedline::{Completer as ReedlineCompleter, Suggestion};
17use std::sync::Arc;
18
19fn find_pipeline_element_by_position<'a>(
24 expr: &'a Expression,
25 working_set: &'a StateWorkingSet,
26 pos: usize,
27) -> FindMapResult<&'a Expression> {
28 if !expr.span.contains(pos) {
30 return FindMapResult::Stop;
31 }
32 let closure = |expr: &'a Expression| find_pipeline_element_by_position(expr, working_set, pos);
33 match &expr.expr {
34 Expr::Call(call) => call
35 .arguments
36 .iter()
37 .find_map(|arg| arg.expr().and_then(|e| e.find_map(working_set, &closure)))
38 .or(Some(expr))
40 .map(FindMapResult::Found)
41 .unwrap_or_default(),
42 Expr::ExternalCall(head, arguments) => arguments
43 .iter()
44 .find_map(|arg| arg.expr().find_map(working_set, &closure))
45 .or(head.as_ref().find_map(working_set, &closure))
46 .or(Some(expr))
47 .map(FindMapResult::Found)
48 .unwrap_or_default(),
49 Expr::BinaryOp(lhs, _, rhs) => lhs
51 .find_map(working_set, &closure)
52 .or(rhs.find_map(working_set, &closure))
53 .or(Some(expr))
54 .map(FindMapResult::Found)
55 .unwrap_or_default(),
56 Expr::FullCellPath(fcp) => fcp
57 .head
58 .find_map(working_set, &closure)
59 .map(FindMapResult::Found)
60 .or_else(|| {
62 (fcp.head.span.contains(pos) && matches!(fcp.head.expr, Expr::List(_)))
63 .then_some(FindMapResult::Continue)
64 })
65 .or(Some(FindMapResult::Found(expr)))
66 .unwrap_or_default(),
67 Expr::Var(_) => FindMapResult::Found(expr),
68 Expr::AttributeBlock(ab) => ab
69 .attributes
70 .iter()
71 .map(|attr| &attr.expr)
72 .chain(Some(ab.item.as_ref()))
73 .find_map(|expr| expr.find_map(working_set, &closure))
74 .or(Some(expr))
75 .map(FindMapResult::Found)
76 .unwrap_or_default(),
77 _ => FindMapResult::Continue,
78 }
79}
80
81fn strip_placeholder_if_any<'a>(
84 working_set: &'a StateWorkingSet,
85 span: &Span,
86 strip: bool,
87) -> (Span, &'a [u8]) {
88 let new_span = if strip {
89 let new_end = std::cmp::max(span.end - 1, span.start);
90 Span::new(span.start, new_end)
91 } else {
92 span.to_owned()
93 };
94 let prefix = working_set.get_span_contents(new_span);
95 (new_span, prefix)
96}
97
98fn strip_placeholder_with_rsplit<'a>(
102 working_set: &'a StateWorkingSet,
103 span: &Span,
104 predicate: impl FnMut(&u8) -> bool,
105 strip: bool,
106) -> (Span, &'a [u8]) {
107 let span_content = working_set.get_span_contents(*span);
108 let mut prefix = span_content
109 .rsplit(predicate)
110 .next()
111 .unwrap_or(span_content);
112 let start = span.end.saturating_sub(prefix.len());
113 if strip && !prefix.is_empty() {
114 prefix = &prefix[..prefix.len() - 1];
115 }
116 let end = start + prefix.len();
117 (Span::new(start, end), prefix)
118}
119
120#[derive(Clone)]
121pub struct NuCompleter {
122 engine_state: Arc<EngineState>,
123 stack: Stack,
124}
125
126struct Context<'a> {
128 working_set: &'a StateWorkingSet<'a>,
129 span: Span,
130 prefix: &'a [u8],
131 offset: usize,
132}
133
134struct PositionalArguments<'a> {
136 command_head: &'a str,
138 positional_arg_indices: Vec<usize>,
140 arguments: &'a [Argument],
142 expr: &'a Expression,
144}
145
146impl Context<'_> {
147 fn new<'a>(
148 working_set: &'a StateWorkingSet,
149 span: Span,
150 prefix: &'a [u8],
151 offset: usize,
152 ) -> Context<'a> {
153 Context {
154 working_set,
155 span,
156 prefix,
157 offset,
158 }
159 }
160}
161
162impl NuCompleter {
163 pub fn new(engine_state: Arc<EngineState>, stack: Arc<Stack>) -> Self {
164 Self {
165 engine_state,
166 stack: Stack::with_parent(stack).reset_out_dest().collect_value(),
167 }
168 }
169
170 pub fn fetch_completions_at(&self, line: &str, pos: usize) -> Vec<SemanticSuggestion> {
171 let mut working_set = StateWorkingSet::new(&self.engine_state);
172 let offset = working_set.next_span_start();
173 let line = if line.len() > pos { &line[..pos] } else { line };
175 let block = parse(
176 &mut working_set,
177 Some("completer"),
178 format!("{line}a").as_bytes(),
180 false,
181 );
182 self.fetch_completions_by_block(block, &working_set, pos, offset, line, true)
183 }
184
185 pub fn fetch_completions_within_file(
192 &self,
193 filename: &str,
194 pos: usize,
195 contents: &str,
196 ) -> Vec<SemanticSuggestion> {
197 let mut working_set = StateWorkingSet::new(&self.engine_state);
198 let block = parse(&mut working_set, Some(filename), contents.as_bytes(), false);
199 let Some(file_span) = working_set.get_span_for_filename(filename) else {
200 return vec![];
201 };
202 let offset = file_span.start;
203 self.fetch_completions_by_block(block.clone(), &working_set, pos, offset, contents, false)
204 }
205
206 fn fetch_completions_by_block(
207 &self,
208 block: Arc<Block>,
209 working_set: &StateWorkingSet,
210 pos: usize,
211 offset: usize,
212 contents: &str,
213 extra_placeholder: bool,
214 ) -> Vec<SemanticSuggestion> {
215 let mut pos_to_search = pos + offset;
218 if !extra_placeholder {
219 pos_to_search = pos_to_search.saturating_sub(1);
220 }
221 let Some(element_expression) = block.find_map(working_set, &|expr: &Expression| {
222 find_pipeline_element_by_position(expr, working_set, pos_to_search)
223 }) else {
224 return vec![];
225 };
226
227 let start_offset = element_expression.span.start - offset;
229 let Some(text) = contents.get(start_offset..pos) else {
230 return vec![];
231 };
232 self.complete_by_expression(
233 working_set,
234 element_expression,
235 offset,
236 pos_to_search,
237 text,
238 extra_placeholder,
239 )
240 }
241
242 fn complete_by_expression(
251 &self,
252 working_set: &StateWorkingSet,
253 element_expression: &Expression,
254 offset: usize,
255 pos: usize,
256 prefix_str: &str,
257 strip: bool,
258 ) -> Vec<SemanticSuggestion> {
259 let mut suggestions: Vec<SemanticSuggestion> = vec![];
260
261 match &element_expression.expr {
262 Expr::Var(_) => {
263 return self.variable_names_completion_helper(
264 working_set,
265 element_expression.span,
266 offset,
267 strip,
268 );
269 }
270 Expr::FullCellPath(full_cell_path) => {
271 if full_cell_path.tail.is_empty() && !prefix_str.ends_with('.') {
274 return self.variable_names_completion_helper(
275 working_set,
276 element_expression.span,
277 offset,
278 strip,
279 );
280 } else {
281 let mut cell_path_completer = CellPathCompletion {
282 full_cell_path,
283 position: if strip { pos - 1 } else { pos },
284 };
285 let ctx = Context::new(working_set, Span::unknown(), &[], offset);
286 return self.process_completion(&mut cell_path_completer, &ctx);
287 }
288 }
289 Expr::BinaryOp(lhs, op, _) => {
290 if op.span.contains(pos) {
291 let mut operator_completions = OperatorCompletion {
292 left_hand_side: lhs.as_ref(),
293 };
294 let (new_span, prefix) = strip_placeholder_if_any(working_set, &op.span, strip);
295 let ctx = Context::new(working_set, new_span, prefix, offset);
296 let results = self.process_completion(&mut operator_completions, &ctx);
297 if !results.is_empty() {
298 return results;
299 }
300 }
301 }
302 Expr::AttributeBlock(ab) => {
303 if let Some(span) = ab.attributes.iter().find_map(|attr| {
304 let span = attr.expr.span;
305 span.contains(pos).then_some(span)
306 }) {
307 let (new_span, prefix) = strip_placeholder_if_any(working_set, &span, strip);
308 let ctx = Context::new(working_set, new_span, prefix, offset);
309 return self.process_completion(&mut AttributeCompletion, &ctx);
310 };
311 let span = ab.item.span;
312 if span.contains(pos) {
313 let (new_span, prefix) = strip_placeholder_if_any(working_set, &span, strip);
314 let ctx = Context::new(working_set, new_span, prefix, offset);
315 return self.process_completion(&mut AttributableCompletion, &ctx);
316 }
317 }
318
319 Expr::Call(_) | Expr::ExternalCall(_, _) => {
323 let need_externals = !prefix_str.contains(' ');
324 let need_internals = !prefix_str.starts_with('^');
325 let mut span = element_expression.span;
326 if !need_internals {
327 span.start += 1;
328 };
329 suggestions.extend(self.command_completion_helper(
330 working_set,
331 span,
332 offset,
333 need_internals,
334 need_externals,
335 strip,
336 ))
337 }
338 _ => (),
339 }
340
341 match &element_expression.expr {
343 Expr::Call(call) => {
344 let mut positional_arg_indices = Vec::new();
348 for (arg_idx, arg) in call.arguments.iter().enumerate() {
349 let span = arg.span();
350 if span.contains(pos) {
351 if let Some(decl_id) = arg.expr().and_then(|e| e.custom_completion) {
353 let (new_span, prefix) = if matches!(arg, Argument::Named(_)) {
355 strip_placeholder_with_rsplit(
356 working_set,
357 &span,
358 |b| *b == b'=' || *b == b' ',
359 strip,
360 )
361 } else {
362 strip_placeholder_if_any(working_set, &span, strip)
363 };
364 let ctx = Context::new(working_set, new_span, prefix, offset);
365
366 let mut completer = CustomCompletion::new(
367 decl_id,
368 prefix_str.into(),
369 pos - offset,
370 FileCompletion,
371 );
372
373 suggestions.splice(0..0, self.process_completion(&mut completer, &ctx));
375 break;
376 }
377
378 let (new_span, prefix) =
380 strip_placeholder_if_any(working_set, &span, strip);
381 let ctx = Context::new(working_set, new_span, prefix, offset);
382 let flag_completion_helper = || {
383 let mut flag_completions = FlagCompletion {
384 decl_id: call.decl_id,
385 };
386 self.process_completion(&mut flag_completions, &ctx)
387 };
388 suggestions.splice(
390 0..0,
391 match arg {
392 Argument::Named(_) | Argument::Unknown(_)
394 if prefix.starts_with(b"-") =>
395 {
396 flag_completion_helper()
397 }
398 Argument::Positional(_) if prefix == b"-" => {
400 flag_completion_helper()
401 }
402 Argument::Positional(expr) => {
404 let command_head = working_set.get_decl(call.decl_id).name();
405 positional_arg_indices.push(arg_idx);
406 self.argument_completion_helper(
407 PositionalArguments {
408 command_head,
409 positional_arg_indices,
410 arguments: &call.arguments,
411 expr,
412 },
413 pos,
414 &ctx,
415 suggestions.is_empty(),
416 )
417 }
418 _ => vec![],
419 },
420 );
421 break;
422 } else if !matches!(arg, Argument::Named(_)) {
423 positional_arg_indices.push(arg_idx);
424 }
425 }
426 }
427 Expr::ExternalCall(head, arguments) => {
428 for (i, arg) in arguments.iter().enumerate() {
429 let span = arg.expr().span;
430 if span.contains(pos) {
431 if i == 0 {
434 let external_cmd = working_set.get_span_contents(head.span);
435 if external_cmd == b"sudo" || external_cmd == b"doas" {
436 let commands = self.command_completion_helper(
437 working_set,
438 span,
439 offset,
440 true,
441 true,
442 strip,
443 );
444 if !commands.is_empty() {
446 return commands;
447 }
448 }
449 }
450 let config = self.engine_state.get_config();
452 if let Some(closure) = config.completions.external.completer.as_ref() {
453 let mut text_spans: Vec<String> =
454 flatten_expression(working_set, element_expression)
455 .iter()
456 .map(|(span, _)| {
457 let bytes = working_set.get_span_contents(*span);
458 String::from_utf8_lossy(bytes).to_string()
459 })
460 .collect();
461 let mut new_span = span;
462 if strip {
464 if let Some(last) = text_spans.last_mut() {
465 last.pop();
466 new_span = Span::new(span.start, span.end.saturating_sub(1));
467 }
468 }
469 if let Some(external_result) =
470 self.external_completion(closure, &text_spans, offset, new_span)
471 {
472 suggestions.splice(0..0, external_result);
474 return suggestions;
475 }
476 }
477 if suggestions.is_empty() {
479 let (new_span, prefix) =
480 strip_placeholder_if_any(working_set, &span, strip);
481 let ctx = Context::new(working_set, new_span, prefix, offset);
482 return self.process_completion(&mut FileCompletion, &ctx);
483 }
484 break;
485 }
486 }
487 }
488 _ => (),
489 }
490
491 if suggestions.is_empty() {
493 let (new_span, prefix) = strip_placeholder_with_rsplit(
494 working_set,
495 &element_expression.span,
496 |c| *c == b' ',
497 strip,
498 );
499 let ctx = Context::new(working_set, new_span, prefix, offset);
500 suggestions.extend(self.process_completion(&mut FileCompletion, &ctx));
501 }
502 suggestions
503 }
504
505 fn variable_names_completion_helper(
506 &self,
507 working_set: &StateWorkingSet,
508 span: Span,
509 offset: usize,
510 strip: bool,
511 ) -> Vec<SemanticSuggestion> {
512 let (new_span, prefix) = strip_placeholder_if_any(working_set, &span, strip);
513 if !prefix.starts_with(b"$") {
514 return vec![];
515 }
516 let ctx = Context::new(working_set, new_span, prefix, offset);
517 self.process_completion(&mut VariableCompletion, &ctx)
518 }
519
520 fn command_completion_helper(
521 &self,
522 working_set: &StateWorkingSet,
523 span: Span,
524 offset: usize,
525 internals: bool,
526 externals: bool,
527 strip: bool,
528 ) -> Vec<SemanticSuggestion> {
529 let config = self.engine_state.get_config();
530 let mut command_completions = CommandCompletion {
531 internals,
532 externals: !internals || (externals && config.completions.external.enable),
533 };
534 let (new_span, prefix) = strip_placeholder_if_any(working_set, &span, strip);
535 let ctx = Context::new(working_set, new_span, prefix, offset);
536 self.process_completion(&mut command_completions, &ctx)
537 }
538
539 fn argument_completion_helper(
540 &self,
541 argument_info: PositionalArguments,
542 pos: usize,
543 ctx: &Context,
544 need_fallback: bool,
545 ) -> Vec<SemanticSuggestion> {
546 let PositionalArguments {
547 command_head,
548 positional_arg_indices,
549 arguments,
550 expr,
551 } = argument_info;
552 match command_head {
554 "use" | "export use" | "overlay use" | "source-env"
556 if positional_arg_indices.len() == 1 =>
557 {
558 return self.process_completion(
559 &mut DotNuCompletion {
560 std_virtual_path: command_head != "source-env",
561 },
562 ctx,
563 );
564 }
565 "use" | "export use" => {
568 let Some(Argument::Positional(Expression {
569 expr: Expr::String(module_name),
570 span,
571 ..
572 })) = positional_arg_indices
573 .first()
574 .and_then(|i| arguments.get(*i))
575 else {
576 return vec![];
577 };
578 let module_name = module_name.as_bytes();
579 let (module_id, temp_working_set) = match ctx.working_set.find_module(module_name) {
580 Some(module_id) => (module_id, None),
581 None => {
582 let mut temp_working_set =
583 StateWorkingSet::new(ctx.working_set.permanent_state);
584 let Some(module_id) = parse_module_file_or_dir(
585 &mut temp_working_set,
586 module_name,
587 *span,
588 None,
589 ) else {
590 return vec![];
591 };
592 (module_id, Some(temp_working_set))
593 }
594 };
595 let mut exportable_completion = ExportableCompletion {
596 module_id,
597 temp_working_set,
598 };
599 let mut complete_on_list_items = |items: &[ListItem]| -> Vec<SemanticSuggestion> {
600 for item in items {
601 let span = item.expr().span;
602 if span.contains(pos) {
603 let offset = span.start.saturating_sub(ctx.span.start);
604 let end_offset =
605 ctx.prefix.len().min(pos.min(span.end) - ctx.span.start + 1);
606 let new_ctx = Context::new(
607 ctx.working_set,
608 Span::new(span.start, ctx.span.end.min(span.end)),
609 ctx.prefix.get(offset..end_offset).unwrap_or_default(),
610 ctx.offset,
611 );
612 return self.process_completion(&mut exportable_completion, &new_ctx);
613 }
614 }
615 vec![]
616 };
617
618 match &expr.expr {
619 Expr::String(_) => {
620 return self.process_completion(&mut exportable_completion, ctx);
621 }
622 Expr::FullCellPath(fcp) => match &fcp.head.expr {
623 Expr::List(items) => {
624 return complete_on_list_items(items);
625 }
626 _ => return vec![],
627 },
628 _ => return vec![],
629 }
630 }
631 "which" => {
632 let mut completer = CommandCompletion {
633 internals: true,
634 externals: true,
635 };
636 return self.process_completion(&mut completer, ctx);
637 }
638 _ => (),
639 }
640
641 let file_completion_helper = || self.process_completion(&mut FileCompletion, ctx);
643 match &expr.expr {
644 Expr::Directory(_, _) => self.process_completion(&mut DirectoryCompletion, ctx),
645 Expr::Filepath(_, _) | Expr::GlobPattern(_, _) => file_completion_helper(),
646 _ if need_fallback => file_completion_helper(),
648 _ => vec![],
649 }
650 }
651
652 fn process_completion<T: Completer>(
654 &self,
655 completer: &mut T,
656 ctx: &Context,
657 ) -> Vec<SemanticSuggestion> {
658 let config = self.engine_state.get_config();
659
660 let options = CompletionOptions {
661 case_sensitive: config.completions.case_sensitive,
662 match_algorithm: config.completions.algorithm.into(),
663 sort: config.completions.sort,
664 };
665
666 completer.fetch(
667 ctx.working_set,
668 &self.stack,
669 String::from_utf8_lossy(ctx.prefix),
670 ctx.span,
671 ctx.offset,
672 &options,
673 )
674 }
675
676 fn external_completion(
677 &self,
678 closure: &Closure,
679 spans: &[String],
680 offset: usize,
681 span: Span,
682 ) -> Option<Vec<SemanticSuggestion>> {
683 let block = self.engine_state.get_block(closure.block_id);
684 let mut callee_stack = self
685 .stack
686 .captures_to_stack_preserve_out_dest(closure.captures.clone());
687
688 if let Some(pos_arg) = block.signature.required_positional.first() {
690 if let Some(var_id) = pos_arg.var_id {
691 callee_stack.add_var(
692 var_id,
693 Value::list(
694 spans
695 .iter()
696 .map(|it| Value::string(it, Span::unknown()))
697 .collect(),
698 Span::unknown(),
699 ),
700 );
701 }
702 }
703
704 let result = eval_block::<WithoutDebug>(
705 &self.engine_state,
706 &mut callee_stack,
707 block,
708 PipelineData::empty(),
709 );
710
711 match result.and_then(|data| data.into_value(span)) {
712 Ok(Value::List { vals, .. }) => {
713 let result =
714 map_value_completions(vals.iter(), Span::new(span.start, span.end), offset);
715 Some(result)
716 }
717 Ok(Value::Nothing { .. }) => None,
718 Ok(value) => {
719 log::error!(
720 "External completer returned invalid value of type {}",
721 value.get_type().to_string()
722 );
723 Some(vec![])
724 }
725 Err(err) => {
726 log::error!("failed to eval completer block: {err}");
727 Some(vec![])
728 }
729 }
730 }
731}
732
733impl ReedlineCompleter for NuCompleter {
734 fn complete(&mut self, line: &str, pos: usize) -> Vec<Suggestion> {
735 self.fetch_completions_at(line, pos)
736 .into_iter()
737 .map(|s| s.suggestion)
738 .collect()
739 }
740}
741
742pub fn map_value_completions<'a>(
743 list: impl Iterator<Item = &'a Value>,
744 span: Span,
745 offset: usize,
746) -> Vec<SemanticSuggestion> {
747 list.filter_map(move |x| {
748 if let Ok(s) = x.coerce_string() {
750 return Some(SemanticSuggestion {
751 suggestion: Suggestion {
752 value: s,
753 span: reedline::Span {
754 start: span.start - offset,
755 end: span.end - offset,
756 },
757 ..Suggestion::default()
758 },
759 kind: Some(SuggestionKind::Value(x.get_type())),
760 });
761 }
762
763 if let Ok(record) = x.as_record() {
765 let mut suggestion = Suggestion {
766 value: String::from(""), span: reedline::Span {
768 start: span.start - offset,
769 end: span.end - offset,
770 },
771 ..Suggestion::default()
772 };
773 let mut value_type = Type::String;
774
775 record.iter().for_each(|(key, value)| {
777 match key.as_str() {
778 "value" => {
779 value_type = value.get_type();
780 if let Ok(val_str) = value.coerce_string() {
782 suggestion.value = val_str;
784 }
785 }
786 "description" => {
787 if let Ok(desc_str) = value.coerce_string() {
789 suggestion.description = Some(desc_str);
791 }
792 }
793 "style" => {
794 suggestion.style = match value {
796 Value::String { val, .. } => Some(lookup_ansi_color_style(val)),
797 Value::Record { .. } => Some(color_record_to_nustyle(value)),
798 _ => None,
799 };
800 }
801 _ => (),
802 }
803 });
804
805 return Some(SemanticSuggestion {
806 suggestion,
807 kind: Some(SuggestionKind::Value(value_type)),
808 });
809 }
810
811 None
812 })
813 .collect()
814}
815
816#[cfg(test)]
817mod completer_tests {
818 use super::*;
819
820 #[test]
821 fn test_completion_helper() {
822 let mut engine_state =
823 nu_command::add_shell_command_context(nu_cmd_lang::create_default_context());
824
825 let delta = {
827 let working_set = nu_protocol::engine::StateWorkingSet::new(&engine_state);
828 working_set.render()
829 };
830
831 let result = engine_state.merge_delta(delta);
832 assert!(
833 result.is_ok(),
834 "Error merging delta: {:?}",
835 result.err().unwrap()
836 );
837
838 let completer = NuCompleter::new(engine_state.into(), Arc::new(Stack::new()));
839 let dataset = [
840 ("1 bit-sh", true, "b", vec!["bit-shl", "bit-shr"]),
841 ("1.0 bit-sh", false, "b", vec![]),
842 ("1 m", true, "m", vec!["mod"]),
843 ("1.0 m", true, "m", vec!["mod"]),
844 ("\"a\" s", true, "s", vec!["starts-with"]),
845 ("sudo", false, "", Vec::new()),
846 ("sudo l", true, "l", vec!["ls", "let", "lines", "loop"]),
847 (" sudo", false, "", Vec::new()),
848 (" sudo le", true, "le", vec!["let", "length"]),
849 (
850 "ls | c",
851 true,
852 "c",
853 vec!["cd", "config", "const", "cp", "cal"],
854 ),
855 ("ls | sudo m", true, "m", vec!["mv", "mut", "move"]),
856 ];
857 for (line, has_result, begins_with, expected_values) in dataset {
858 let result = completer.fetch_completions_at(line, line.len());
859 assert_eq!(!result.is_empty(), has_result, "line: {line}");
861
862 result
864 .iter()
865 .for_each(|x| assert!(x.suggestion.value.starts_with(begins_with)));
866
867 assert_eq!(
869 result
870 .iter()
871 .map(|x| expected_values.contains(&x.suggestion.value.as_str()))
872 .filter(|x| *x)
873 .count(),
874 expected_values.len(),
875 "line: {line}"
876 );
877 }
878 }
879}