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
20fn find_pipeline_element_by_position<'a>(
25 expr: &'a Expression,
26 working_set: &'a StateWorkingSet,
27 pos: usize,
28) -> FindMapResult<&'a Expression> {
29 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 .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 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
77fn 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
94fn 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
122struct 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 let line = if line.len() > pos { &line[..pos] } else { line };
159 let block = parse(
160 &mut working_set,
161 Some("completer"),
162 format!("{}a", line).as_bytes(),
164 false,
165 );
166 self.fetch_completions_by_block(block, &working_set, pos, offset, line, true)
167 }
168
169 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 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 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 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 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 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 match &element_expression.expr {
327 Expr::Call(call) => {
328 for arg in call.arguments.iter() {
332 let span = arg.span();
333 if span.contains(pos) {
334 if let Some(decl_id) = arg.expr().and_then(|e| e.custom_completion) {
336 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 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 Argument::Named(_) | Argument::Unknown(_)
373 if prefix.starts_with(b"-") =>
374 {
375 flag_completion_helper()
376 }
377 Argument::Positional(_) if prefix == b"-" => flag_completion_helper(),
379 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 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 if !commands.is_empty() {
414 return commands;
415 }
416 }
417 }
418 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 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 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 match command_head {
507 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 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 _ if need_fallback => file_completion_helper(),
530 _ => vec![],
531 }
532 }
533
534 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 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 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 if let Ok(record) = x.as_record() {
648 let mut suggestion = Suggestion {
649 value: String::from(""), 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 record.iter().for_each(|(key, value)| {
660 match key.as_str() {
661 "value" => {
662 value_type = value.get_type();
663 if let Ok(val_str) = value.coerce_string() {
665 suggestion.value = val_str;
667 }
668 }
669 "description" => {
670 if let Ok(desc_str) = value.coerce_string() {
672 suggestion.description = Some(desc_str);
674 }
675 }
676 "style" => {
677 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 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 assert_eq!(!result.is_empty(), has_result, "line: {}", line);
744
745 result
747 .iter()
748 .for_each(|x| assert!(x.suggestion.value.starts_with(begins_with)));
749
750 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}