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 let custom_completion_decl_id = {
353 let signature = working_set.get_decl(call.decl_id).signature();
355
356 match arg {
357 Argument::Named((name, short, value)) => {
359 if value.as_ref().is_none_or(|e| !e.span.contains(pos)) {
360 None
361 } else {
362 let flag =
365 signature.get_long_flag(&name.item).or_else(|| {
366 short.as_ref().and_then(|s| {
367 signature.get_short_flag(
368 s.item.chars().next().unwrap_or('_'),
369 )
370 })
371 });
372 flag.and_then(|f| f.custom_completion)
373 }
374 }
375 Argument::Positional(_) => {
377 let arg_pos = positional_arg_indices.len();
379 signature
380 .get_positional(arg_pos)
381 .and_then(|pos_arg| pos_arg.custom_completion)
382 }
383 _ => None,
384 }
385 };
386
387 if let Some(decl_id) = custom_completion_decl_id {
388 let (new_span, prefix) = if matches!(arg, Argument::Named(_)) {
390 strip_placeholder_with_rsplit(
391 working_set,
392 &span,
393 |b| *b == b'=' || *b == b' ',
394 strip,
395 )
396 } else {
397 strip_placeholder_if_any(working_set, &span, strip)
398 };
399 let ctx = Context::new(working_set, new_span, prefix, offset);
400
401 let mut completer = CustomCompletion::new(
402 decl_id,
403 prefix_str.into(),
404 pos - offset,
405 FileCompletion,
406 );
407
408 suggestions.splice(0..0, self.process_completion(&mut completer, &ctx));
410 break;
411 }
412
413 let (new_span, prefix) =
415 strip_placeholder_if_any(working_set, &span, strip);
416 let ctx = Context::new(working_set, new_span, prefix, offset);
417 let flag_completion_helper = || {
418 let mut flag_completions = FlagCompletion {
419 decl_id: call.decl_id,
420 };
421 self.process_completion(&mut flag_completions, &ctx)
422 };
423 suggestions.splice(
425 0..0,
426 match arg {
427 Argument::Named(_) | Argument::Unknown(_)
429 if prefix.starts_with(b"-") =>
430 {
431 flag_completion_helper()
432 }
433 Argument::Positional(_) if prefix == b"-" => {
435 flag_completion_helper()
436 }
437 Argument::Positional(expr) => {
439 let command_head = working_set.get_decl(call.decl_id).name();
440 positional_arg_indices.push(arg_idx);
441 self.argument_completion_helper(
442 PositionalArguments {
443 command_head,
444 positional_arg_indices,
445 arguments: &call.arguments,
446 expr,
447 },
448 pos,
449 &ctx,
450 suggestions.is_empty(),
451 )
452 }
453 _ => vec![],
454 },
455 );
456 break;
457 } else if !matches!(arg, Argument::Named(_)) {
458 positional_arg_indices.push(arg_idx);
459 }
460 }
461 }
462 Expr::ExternalCall(head, arguments) => {
463 for (i, arg) in arguments.iter().enumerate() {
464 let span = arg.expr().span;
465 if span.contains(pos) {
466 if i == 0 {
469 let external_cmd = working_set.get_span_contents(head.span);
470 if external_cmd == b"sudo" || external_cmd == b"doas" {
471 let commands = self.command_completion_helper(
472 working_set,
473 span,
474 offset,
475 true,
476 true,
477 strip,
478 );
479 if !commands.is_empty() {
481 return commands;
482 }
483 }
484 }
485 let config = self.engine_state.get_config();
487 if let Some(closure) = config.completions.external.completer.as_ref() {
488 let mut text_spans: Vec<String> =
489 flatten_expression(working_set, element_expression)
490 .iter()
491 .map(|(span, _)| {
492 let bytes = working_set.get_span_contents(*span);
493 String::from_utf8_lossy(bytes).to_string()
494 })
495 .collect();
496 let mut new_span = span;
497 if strip {
499 if let Some(last) = text_spans.last_mut() {
500 last.pop();
501 new_span = Span::new(span.start, span.end.saturating_sub(1));
502 }
503 }
504 if let Some(external_result) =
505 self.external_completion(closure, &text_spans, offset, new_span)
506 {
507 suggestions.splice(0..0, external_result);
509 return suggestions;
510 }
511 }
512 if suggestions.is_empty() {
514 let (new_span, prefix) =
515 strip_placeholder_if_any(working_set, &span, strip);
516 let ctx = Context::new(working_set, new_span, prefix, offset);
517 return self.process_completion(&mut FileCompletion, &ctx);
518 }
519 break;
520 }
521 }
522 }
523 _ => (),
524 }
525
526 if suggestions.is_empty() {
528 let (new_span, prefix) = strip_placeholder_with_rsplit(
529 working_set,
530 &element_expression.span,
531 |c| *c == b' ',
532 strip,
533 );
534 let ctx = Context::new(working_set, new_span, prefix, offset);
535 suggestions.extend(self.process_completion(&mut FileCompletion, &ctx));
536 }
537 suggestions
538 }
539
540 fn variable_names_completion_helper(
541 &self,
542 working_set: &StateWorkingSet,
543 span: Span,
544 offset: usize,
545 strip: bool,
546 ) -> Vec<SemanticSuggestion> {
547 let (new_span, prefix) = strip_placeholder_if_any(working_set, &span, strip);
548 if !prefix.starts_with(b"$") {
549 return vec![];
550 }
551 let ctx = Context::new(working_set, new_span, prefix, offset);
552 self.process_completion(&mut VariableCompletion, &ctx)
553 }
554
555 fn command_completion_helper(
556 &self,
557 working_set: &StateWorkingSet,
558 span: Span,
559 offset: usize,
560 internals: bool,
561 externals: bool,
562 strip: bool,
563 ) -> Vec<SemanticSuggestion> {
564 let config = self.engine_state.get_config();
565 let mut command_completions = CommandCompletion {
566 internals,
567 externals: !internals || (externals && config.completions.external.enable),
568 };
569 let (new_span, prefix) = strip_placeholder_if_any(working_set, &span, strip);
570 let ctx = Context::new(working_set, new_span, prefix, offset);
571 self.process_completion(&mut command_completions, &ctx)
572 }
573
574 fn argument_completion_helper(
575 &self,
576 argument_info: PositionalArguments,
577 pos: usize,
578 ctx: &Context,
579 need_fallback: bool,
580 ) -> Vec<SemanticSuggestion> {
581 let PositionalArguments {
582 command_head,
583 positional_arg_indices,
584 arguments,
585 expr,
586 } = argument_info;
587 match command_head {
589 "use" | "export use" | "overlay use" | "source-env"
591 if positional_arg_indices.len() == 1 =>
592 {
593 return self.process_completion(
594 &mut DotNuCompletion {
595 std_virtual_path: command_head != "source-env",
596 },
597 ctx,
598 );
599 }
600 "use" | "export use" => {
603 let Some(Argument::Positional(Expression {
604 expr: Expr::String(module_name),
605 span,
606 ..
607 })) = positional_arg_indices
608 .first()
609 .and_then(|i| arguments.get(*i))
610 else {
611 return vec![];
612 };
613 let module_name = module_name.as_bytes();
614 let (module_id, temp_working_set) = match ctx.working_set.find_module(module_name) {
615 Some(module_id) => (module_id, None),
616 None => {
617 let mut temp_working_set =
618 StateWorkingSet::new(ctx.working_set.permanent_state);
619 let Some(module_id) = parse_module_file_or_dir(
620 &mut temp_working_set,
621 module_name,
622 *span,
623 None,
624 ) else {
625 return vec![];
626 };
627 (module_id, Some(temp_working_set))
628 }
629 };
630 let mut exportable_completion = ExportableCompletion {
631 module_id,
632 temp_working_set,
633 };
634 let mut complete_on_list_items = |items: &[ListItem]| -> Vec<SemanticSuggestion> {
635 for item in items {
636 let span = item.expr().span;
637 if span.contains(pos) {
638 let offset = span.start.saturating_sub(ctx.span.start);
639 let end_offset =
640 ctx.prefix.len().min(pos.min(span.end) - ctx.span.start + 1);
641 let new_ctx = Context::new(
642 ctx.working_set,
643 Span::new(span.start, ctx.span.end.min(span.end)),
644 ctx.prefix.get(offset..end_offset).unwrap_or_default(),
645 ctx.offset,
646 );
647 return self.process_completion(&mut exportable_completion, &new_ctx);
648 }
649 }
650 vec![]
651 };
652
653 match &expr.expr {
654 Expr::String(_) => {
655 return self.process_completion(&mut exportable_completion, ctx);
656 }
657 Expr::FullCellPath(fcp) => match &fcp.head.expr {
658 Expr::List(items) => {
659 return complete_on_list_items(items);
660 }
661 _ => return vec![],
662 },
663 _ => return vec![],
664 }
665 }
666 "which" => {
667 let mut completer = CommandCompletion {
668 internals: true,
669 externals: true,
670 };
671 return self.process_completion(&mut completer, ctx);
672 }
673 _ => (),
674 }
675
676 let file_completion_helper = || self.process_completion(&mut FileCompletion, ctx);
678 match &expr.expr {
679 Expr::Directory(_, _) => self.process_completion(&mut DirectoryCompletion, ctx),
680 Expr::Filepath(_, _) | Expr::GlobPattern(_, _) => file_completion_helper(),
681 _ if need_fallback => file_completion_helper(),
683 _ => vec![],
684 }
685 }
686
687 fn process_completion<T: Completer>(
689 &self,
690 completer: &mut T,
691 ctx: &Context,
692 ) -> Vec<SemanticSuggestion> {
693 let config = self.engine_state.get_config();
694
695 let options = CompletionOptions {
696 case_sensitive: config.completions.case_sensitive,
697 match_algorithm: config.completions.algorithm.into(),
698 sort: config.completions.sort,
699 };
700
701 completer.fetch(
702 ctx.working_set,
703 &self.stack,
704 String::from_utf8_lossy(ctx.prefix),
705 ctx.span,
706 ctx.offset,
707 &options,
708 )
709 }
710
711 fn external_completion(
712 &self,
713 closure: &Closure,
714 spans: &[String],
715 offset: usize,
716 span: Span,
717 ) -> Option<Vec<SemanticSuggestion>> {
718 let block = self.engine_state.get_block(closure.block_id);
719 let mut callee_stack = self
720 .stack
721 .captures_to_stack_preserve_out_dest(closure.captures.clone());
722
723 if let Some(pos_arg) = block.signature.required_positional.first() {
725 if let Some(var_id) = pos_arg.var_id {
726 callee_stack.add_var(
727 var_id,
728 Value::list(
729 spans
730 .iter()
731 .map(|it| Value::string(it, Span::unknown()))
732 .collect(),
733 Span::unknown(),
734 ),
735 );
736 }
737 }
738
739 let result = eval_block::<WithoutDebug>(
740 &self.engine_state,
741 &mut callee_stack,
742 block,
743 PipelineData::empty(),
744 );
745
746 match result.and_then(|data| data.into_value(span)) {
747 Ok(Value::List { vals, .. }) => {
748 let result =
749 map_value_completions(vals.iter(), Span::new(span.start, span.end), offset);
750 Some(result)
751 }
752 Ok(Value::Nothing { .. }) => None,
753 Ok(value) => {
754 log::error!(
755 "External completer returned invalid value of type {}",
756 value.get_type()
757 );
758 Some(vec![])
759 }
760 Err(err) => {
761 log::error!("failed to eval completer block: {err}");
762 Some(vec![])
763 }
764 }
765 }
766}
767
768impl ReedlineCompleter for NuCompleter {
769 fn complete(&mut self, line: &str, pos: usize) -> Vec<Suggestion> {
770 self.fetch_completions_at(line, pos)
771 .into_iter()
772 .map(|s| s.suggestion)
773 .collect()
774 }
775}
776
777pub fn map_value_completions<'a>(
778 list: impl Iterator<Item = &'a Value>,
779 span: Span,
780 offset: usize,
781) -> Vec<SemanticSuggestion> {
782 list.filter_map(move |x| {
783 if let Ok(s) = x.coerce_string() {
785 return Some(SemanticSuggestion {
786 suggestion: Suggestion {
787 value: s,
788 span: reedline::Span {
789 start: span.start - offset,
790 end: span.end - offset,
791 },
792 ..Suggestion::default()
793 },
794 kind: Some(SuggestionKind::Value(x.get_type())),
795 });
796 }
797
798 if let Ok(record) = x.as_record() {
800 let mut suggestion = Suggestion {
801 value: String::from(""), span: reedline::Span {
803 start: span.start - offset,
804 end: span.end - offset,
805 },
806 ..Suggestion::default()
807 };
808 let mut value_type = Type::String;
809
810 record.iter().for_each(|(key, value)| {
812 match key.as_str() {
813 "value" => {
814 value_type = value.get_type();
815 if let Ok(val_str) = value.coerce_string() {
817 suggestion.value = val_str;
819 }
820 }
821 "description" => {
822 if let Ok(desc_str) = value.coerce_string() {
824 suggestion.description = Some(desc_str);
826 }
827 }
828 "style" => {
829 suggestion.style = match value {
831 Value::String { val, .. } => Some(lookup_ansi_color_style(val)),
832 Value::Record { .. } => Some(color_record_to_nustyle(value)),
833 _ => None,
834 };
835 }
836 _ => (),
837 }
838 });
839
840 return Some(SemanticSuggestion {
841 suggestion,
842 kind: Some(SuggestionKind::Value(value_type)),
843 });
844 }
845
846 None
847 })
848 .collect()
849}
850
851#[cfg(test)]
852mod completer_tests {
853 use super::*;
854
855 #[test]
856 fn test_completion_helper() {
857 let mut engine_state =
858 nu_command::add_shell_command_context(nu_cmd_lang::create_default_context());
859
860 let delta = {
862 let working_set = nu_protocol::engine::StateWorkingSet::new(&engine_state);
863 working_set.render()
864 };
865
866 let result = engine_state.merge_delta(delta);
867 assert!(
868 result.is_ok(),
869 "Error merging delta: {:?}",
870 result.err().unwrap()
871 );
872
873 let completer = NuCompleter::new(engine_state.into(), Arc::new(Stack::new()));
874 let dataset = [
875 ("1 bit-sh", true, "b", vec!["bit-shl", "bit-shr"]),
876 ("1.0 bit-sh", false, "b", vec![]),
877 ("1 m", true, "m", vec!["mod"]),
878 ("1.0 m", true, "m", vec!["mod"]),
879 ("\"a\" s", true, "s", vec!["starts-with"]),
880 ("sudo", false, "", Vec::new()),
881 ("sudo l", true, "l", vec!["ls", "let", "lines", "loop"]),
882 (" sudo", false, "", Vec::new()),
883 (" sudo le", true, "le", vec!["let", "length"]),
884 (
885 "ls | c",
886 true,
887 "c",
888 vec!["cd", "config", "const", "cp", "cal"],
889 ),
890 ("ls | sudo m", true, "m", vec!["mv", "mut", "move"]),
891 ];
892 for (line, has_result, begins_with, expected_values) in dataset {
893 let result = completer.fetch_completions_at(line, line.len());
894 assert_eq!(!result.is_empty(), has_result, "line: {line}");
896
897 result
899 .iter()
900 .for_each(|x| assert!(x.suggestion.value.starts_with(begins_with)));
901
902 assert_eq!(
904 result
905 .iter()
906 .map(|x| expected_values.contains(&x.suggestion.value.as_str()))
907 .filter(|x| *x)
908 .count(),
909 expected_values.len(),
910 "line: {line}"
911 );
912 }
913 }
914}