1use crate::completions::{
2 ArgValueCompletion, AttributableCompletion, AttributeCompletion, CellPathCompletion,
3 CommandCompletion, CommandScope, Completer, CompletionOptions, CustomCompletion,
4 DotNuCompletion, EnvVarCompletion, FileCompletion, FlagCompletion, NuMatcher,
5 OperatorCompletion, VariableCompletion,
6 base::{Fetched, SemanticSuggestion},
7};
8use lru::LruCache;
9use nu_parser::{parse, parse_shorter_head_reading};
10use nu_protocol::{
11 BuiltinCompletion, CommandWideCompleter, Completion, DeclId, Flag, Signature, Span,
12 SuggestionKind,
13 ast::{
14 Argument, AttributeBlock, Block, Call, Expr, Expression, ExternalArgument, FlagRef,
15 FullCellPath, PipelineRedirection, RedirectionTarget, Traverse,
16 },
17 engine::{ArgType, EngineState, Stack, StateWorkingSet},
18};
19use nu_utils::time::Instant;
20use reedline::{
21 Completer as ReedlineCompleter, CompletionOrigin, CompletionResult, CompletionStatus, Partial,
22 Suggestion, Suggestions,
23};
24use std::hash::{DefaultHasher, Hash, Hasher};
25use std::num::NonZeroUsize;
26use std::sync::{Arc, Mutex, mpsc};
27use std::thread;
28use std::time::Duration;
29use std::{borrow::Cow, ops::ControlFlow, path::is_separator};
30
31const DEFAULT_CACHE_SIZE: usize = 100;
34
35use super::{StaticCompletion, custom_completions::CommandWideCompletion};
36
37fn find_pipeline_element_by_position<'a>(
41 expr: &'a Expression,
42 working_set: &'a StateWorkingSet,
43 pos: usize,
44) -> ControlFlow<Option<&'a Expression>> {
45 if !expr.span.contains(pos) && expr.span.end != pos {
46 return ControlFlow::Break(None);
47 }
48
49 let recurse = |e: &'a Expression| find_pipeline_element_by_position(e, working_set, pos);
50 let found = |x| ControlFlow::Break(Some(x));
51 let or_self = |opt: Option<&'a Expression>| opt.map_or(found(expr), found);
52
53 match &expr.expr {
54 Expr::RowCondition(block_id)
55 | Expr::Subexpression(block_id)
56 | Expr::Block(block_id)
57 | Expr::Closure(block_id) => {
58 let block = working_set.get_block(*block_id);
59 check_redirection_in_block(block, pos).map_or(ControlFlow::Continue(()), found)
60 }
61 Expr::Call(call) => or_self(
62 call.arguments
63 .iter()
64 .find_map(|arg| arg.expr().and_then(|e| e.find_map(working_set, &recurse))),
65 ),
66 Expr::ExternalCall(head, arguments) => or_self(
67 arguments
68 .iter()
69 .find_map(|arg| arg.expr().find_map(working_set, &recurse))
70 .or_else(|| {
71 touches(head.span, pos)
74 .then(|| head.as_ref().find_map(working_set, &recurse))
75 .flatten()
76 }),
77 ),
78 Expr::BinaryOp(lhs, _, rhs) => or_self(
79 lhs.find_map(working_set, &recurse)
80 .or_else(|| rhs.find_map(working_set, &recurse)),
81 ),
82 Expr::FullCellPath(fcp) => {
83 if touches(fcp.head.span, pos) && matches!(fcp.head.expr, Expr::List(_)) {
86 return ControlFlow::Continue(());
87 }
88 or_self(fcp.head.find_map(working_set, &recurse))
89 }
90 Expr::Var(_) => found(expr),
91 Expr::AttributeBlock(ab) => or_self(
92 ab.attributes
93 .iter()
94 .map(|attr| &attr.expr)
95 .chain(std::iter::once(ab.item.as_ref()))
96 .find_map(|e| e.find_map(working_set, &recurse)),
97 ),
98 _ => ControlFlow::Continue(()),
99 }
100}
101
102pub(crate) fn touches(span: Span, position: usize) -> bool {
107 span.contains(position) || span.end == position
108}
109
110fn trailing_gap_element<'a>(
114 block: &'a Block,
115 working_set: &StateWorkingSet,
116 absolute_position: usize,
117) -> Option<&'a Expression> {
118 let expression = &block.pipelines.last()?.elements.last()?.expr;
119 let gap = working_set.get_span_contents(Span::new(expression.span.end, absolute_position));
120 gap.iter()
121 .all(u8::is_ascii_whitespace)
122 .then_some(expression)
123}
124
125fn command_name_span(head: Span, element: Span) -> Span {
128 Span::new(head.start, head.end.max(element.end))
129}
130
131fn is_flag_text(token: impl AsRef<[u8]>) -> bool {
136 token.as_ref().starts_with(b"-")
137}
138
139fn is_flag_token(working_set: &StateWorkingSet, span: Span) -> bool {
141 is_flag_text(working_set.get_span_contents(span))
142}
143
144fn is_operator_lhs(expr: &Expr) -> bool {
147 match expr {
148 Expr::Int(_)
149 | Expr::Float(_)
150 | Expr::Binary(_)
151 | Expr::Bool(_)
152 | Expr::String(_)
153 | Expr::RawString(_)
154 | Expr::StringInterpolation(_)
155 | Expr::GlobInterpolation(_, _)
156 | Expr::DateTime(_)
157 | Expr::ValueWithUnit(_)
158 | Expr::Range(_)
159 | Expr::FullCellPath(_)
160 | Expr::CellPath(_)
161 | Expr::Var(_)
162 | Expr::List(_)
163 | Expr::Record(_)
164 | Expr::Table(_)
165 | Expr::Nothing
166 | Expr::Subexpression(_)
167 | Expr::Block(_)
168 | Expr::Closure(_) => true,
169 Expr::AttributeBlock(_)
170 | Expr::VarDecl(_)
171 | Expr::Call(_)
172 | Expr::ExternalCall(_, _)
173 | Expr::Operator(_)
174 | Expr::RowCondition(_)
175 | Expr::UnaryNot(_)
176 | Expr::BinaryOp(_, _, _)
177 | Expr::Collect(_, _)
178 | Expr::MatchBlock(_)
179 | Expr::Keyword(_)
180 | Expr::Filepath(_, _)
181 | Expr::Directory(_, _)
182 | Expr::GlobPattern(_, _)
183 | Expr::ImportPattern(_)
184 | Expr::Overlay(_)
185 | Expr::Signature(_)
186 | Expr::Garbage => false,
187 }
188}
189
190fn find_flag(signature: &Signature, flag: FlagRef<'_>) -> Option<Flag> {
192 match flag {
193 FlagRef::Long(n) => signature.get_long_flag(n),
194 FlagRef::Short(s) => s.chars().next().and_then(|c| signature.get_short_flag(c)),
195 }
196}
197
198fn count_positionals(call: &Call, before_index: usize) -> usize {
200 call.arguments
201 .iter()
202 .take(before_index)
203 .filter(|argument| !matches!(argument, Argument::Named(_)))
204 .count()
205}
206
207fn check_redirection_target(target: &RedirectionTarget, pos: usize) -> Option<&Expression> {
209 let expr = target.expr();
210 expr.and_then(|expression| {
211 if let Expr::String(_) = expression.expr
212 && touches(expression.span, pos)
213 {
214 expr
215 } else {
216 None
217 }
218 })
219}
220
221fn check_redirection_in_block(block: &Block, pos: usize) -> Option<&Expression> {
223 block
224 .pipelines
225 .iter()
226 .flat_map(|p| &p.elements)
227 .filter_map(|e| e.redirection.as_ref())
228 .find_map(|redir| match redir {
229 PipelineRedirection::Single { target, .. } => check_redirection_target(target, pos),
230 PipelineRedirection::Separate { out, err } => {
231 check_redirection_target(out, pos).or_else(|| check_redirection_target(err, pos))
232 }
233 })
234}
235
236#[derive(Debug, Clone, PartialEq, Eq, Hash)]
241pub(crate) struct CompletionQuery {
242 typed: Arc<str>,
244}
245
246impl CompletionQuery {
247 fn new(line: &str, cursor: usize) -> Self {
248 let floored = line.floor_char_boundary(cursor);
249 Self {
250 typed: Arc::from(&line[..floored]),
251 }
252 }
253
254 fn typed(&self) -> &str {
255 &self.typed
256 }
257
258 fn cursor(&self) -> usize {
259 self.typed.len()
260 }
261
262 fn narrows(&self, base: &CompletionQuery, token: reedline::Span) -> bool {
266 let Some(appended) = self.typed().strip_prefix(base.typed()) else {
267 return false;
268 };
269
270 if appended.is_empty() || appended.contains(is_completion_boundary) {
271 return false;
272 }
273
274 let (Some(base_token), Some(narrowed_token)) = (
275 base.typed().get(token.start..),
276 self.typed().get(token.start..),
277 ) else {
278 return false;
279 };
280
281 is_flag_text(base_token) == is_flag_text(narrowed_token)
282 }
283}
284
285fn is_completion_boundary(c: char) -> bool {
286 c.is_whitespace()
287 || is_separator(c)
288 || matches!(
289 c,
290 '|' | ';' | '(' | ')' | '[' | ']' | '{' | '}' | '<' | '>' | '=' | ','
291 )
292}
293
294#[derive(Debug, Clone, Copy, PartialEq, Eq)]
299pub(crate) struct CacheEnv(u64);
300
301impl CacheEnv {
302 fn of(engine_state: &EngineState, stack: &Stack) -> Self {
305 let mut hasher = DefaultHasher::new();
306
307 engine_state.num_decls().hash(&mut hasher);
308 stack
309 .get_env_var(engine_state, "PATH")
310 .map(|path| path.to_expanded_string(":", engine_state.get_config()))
311 .hash(&mut hasher);
312
313 let cwd = engine_state.cwd(Some(stack)).ok();
314 cwd.as_ref()
316 .and_then(|cwd| std::fs::metadata(cwd).ok()?.modified().ok())
317 .hash(&mut hasher);
318 cwd.hash(&mut hasher);
319
320 Self(hasher.finish())
321 }
322}
323
324struct CacheEntry {
325 suggestions: Suggestions,
326 env: CacheEnv,
327}
328
329impl CacheEntry {
330 fn is_usable(&self, env: CacheEnv) -> bool {
332 self.env == env
333 }
334
335 fn reference_span(&self) -> Option<reedline::Span> {
340 self.suggestions.last().map(|suggestion| suggestion.span)
341 }
342}
343
344#[derive(Clone)]
347pub(crate) struct NarrowingCache {
348 entries: Arc<Mutex<Option<LruCache<CompletionQuery, CacheEntry>>>>,
349}
350
351impl Default for NarrowingCache {
352 fn default() -> Self {
353 Self::new(DEFAULT_CACHE_SIZE)
354 }
355}
356
357impl NarrowingCache {
358 pub(crate) fn new(capacity: usize) -> Self {
360 Self {
361 entries: Arc::new(Mutex::new(NonZeroUsize::new(capacity).map(LruCache::new))),
362 }
363 }
364
365 pub(crate) fn set_capacity(&self, capacity: usize) {
368 if let Ok(mut cache_guard) = self.entries.lock() {
369 *cache_guard = NonZeroUsize::new(capacity).map(|new_capacity| {
370 let mut cache = cache_guard
371 .take()
372 .unwrap_or_else(|| LruCache::new(new_capacity));
373 cache.resize(new_capacity);
374 cache
375 });
376 }
377 }
378
379 pub(crate) fn fresh(
380 &self,
381 query: &CompletionQuery,
382 environment: CacheEnv,
383 ) -> Option<Suggestions> {
384 let mut cache_guard = self.entries.lock().ok()?;
385 let entry = cache_guard.as_mut()?.get(query)?;
386
387 entry
388 .is_usable(environment)
389 .then(|| entry.suggestions.clone())
390 }
391
392 pub(crate) fn store(
393 &self,
394 query: CompletionQuery,
395 environment: CacheEnv,
396 suggestions: Suggestions,
397 ) {
398 if let Ok(mut cache_guard) = self.entries.lock()
399 && let Some(cache) = cache_guard.as_mut()
400 {
401 let stale_keys: Vec<_> = cache
402 .iter()
403 .filter(|(_, entry)| !entry.is_usable(environment))
404 .map(|(key, _)| key.clone())
405 .collect();
406
407 for key in stale_keys {
408 cache.pop(&key);
409 }
410
411 cache.put(
412 query,
413 CacheEntry {
414 suggestions,
415 env: environment,
416 },
417 );
418 }
419 }
420
421 pub(crate) fn narrowed_fallback(
422 &self,
423 query: &CompletionQuery,
424 environment: CacheEnv,
425 options: &CompletionOptions,
426 ) -> Suggestions {
427 let Some((base_suggestions, ref_span, search_token)) =
428 self.entries.lock().ok().and_then(|guard| {
429 let (_, entry, span) = guard
430 .as_ref()?
431 .iter()
432 .filter_map(|(bq, e)| {
433 let s = e.reference_span()?;
434 (e.is_usable(environment) && query.narrows(bq, s)).then_some((
435 bq.cursor(),
436 e,
437 s,
438 ))
439 })
440 .max_by_key(|&(c, ..)| c)?;
441
442 let token = query.typed().get(span.start..)?;
443 Some((Arc::clone(&entry.suggestions), span, token))
444 })
445 else {
446 return Suggestions::default();
447 };
448
449 let mut matcher = NuMatcher::new(search_token, options, false);
453
454 base_suggestions
455 .iter()
456 .enumerate()
457 .filter(|(_, s)| s.span == ref_span)
458 .for_each(|(i, s)| {
459 matcher.add(s.display_value(), i);
460 });
461
462 let updated_span = reedline::Span::new(ref_span.start, query.cursor());
463
464 matcher
465 .results()
466 .into_iter()
467 .map(|(index, match_indices)| {
468 let mut suggestion = base_suggestions[index].clone();
469 suggestion.span = updated_span;
470 suggestion.match_indices = Some(match_indices);
471 suggestion
472 })
473 .collect()
474 }
475}
476
477struct Completed {
478 query: CompletionQuery,
479 suggestions: Suggestions,
480 cacheable: bool,
481}
482
483struct CompletionWorker {
484 request_tx: mpsc::Sender<CompletionQuery>,
485 result_rx: mpsc::Receiver<Completed>,
486 pending: Option<CompletionQuery>,
487 latest: Option<Completed>,
488}
489
490fn isolated_stack(parent: Arc<Stack>, suppress_stdin: bool) -> Arc<Stack> {
491 let stack = Stack::with_parent(parent)
492 .reset_out_dest()
493 .suppress_output()
494 .collect_value();
495 Arc::new(if suppress_stdin {
496 stack.suppress_stdin()
497 } else {
498 stack
499 })
500}
501
502#[derive(Debug, Clone)]
504pub(crate) enum SiteKind<'a> {
505 Command { node: Option<&'a Expression> },
507 FlagName {
509 call: &'a Call,
510 element: &'a Expression,
511 },
512 FlagValue {
515 call: &'a Call,
516 element: &'a Expression,
517 flag: FlagRef<'a>,
518 arg_slot: usize,
519 },
520 Positional {
523 call: &'a Call,
524 element: &'a Expression,
525 sig_positional: usize,
526 arg_slot: usize,
527 },
528 Operator { lhs: &'a Expression },
530 CellPath { path: &'a FullCellPath },
532 Variable,
534 AttributeName,
536 AttributableItem,
538 ExternalArg { call: &'a Expression, index: usize },
540 File,
542}
543
544impl<'a> SiteKind<'a> {
545 fn command(node: &'a Expression) -> Self {
547 Self::Command { node: Some(node) }
548 }
549}
550
551#[derive(Debug, Clone)]
557pub(crate) struct CompletionSite<'a> {
558 pub kind: SiteKind<'a>,
559 pub span: Span,
560 pub typed_prefix: Cow<'a, str>,
561 pub cursor: usize,
563}
564
565impl<'a> CompletionSite<'a> {
566 fn new(kind: SiteKind<'a>, span: Span) -> Self {
569 Self {
570 kind,
571 span,
572 typed_prefix: Cow::Borrowed(""),
573 cursor: 0,
574 }
575 }
576}
577
578#[derive(Default)]
580struct Dispatched {
581 suggestions: Vec<SemanticSuggestion>,
582 cacheable: bool,
583}
584
585impl Dispatched {
586 fn merge(&mut self, other: Dispatched) {
588 self.cacheable |= other.cacheable;
589 self.suggestions.extend(other.suggestions);
590 }
591}
592
593impl From<Fetched> for Dispatched {
594 fn from(fetched: Fetched) -> Self {
595 Self {
596 cacheable: fetched.is_cacheable(),
598 suggestions: fetched.into_suggestions(),
599 }
600 }
601}
602
603pub struct CompletionEngine {
604 engine_state: Arc<EngineState>,
605 stack: Arc<Stack>,
606 options: CompletionOptions,
607}
608
609#[derive(Clone, Copy)]
610pub(crate) struct Context<'a> {
611 pub working_set: &'a StateWorkingSet<'a>,
612 pub stack: &'a Stack,
613 pub options: &'a CompletionOptions,
614 pub span: Span,
615 pub prefix: &'a [u8],
616 pub offset: usize,
617}
618
619impl Context<'_> {
620 pub(crate) fn prefix_str(&self) -> Cow<'_, str> {
621 String::from_utf8_lossy(self.prefix)
622 }
623}
624
625impl CompletionEngine {
626 pub fn new(engine_state: Arc<EngineState>, stack: Arc<Stack>) -> Self {
627 Self::with_stack(engine_state, isolated_stack(stack, false))
628 }
629
630 fn for_background(engine_state: Arc<EngineState>, stack: Arc<Stack>) -> Self {
631 Self::with_stack(engine_state, isolated_stack(stack, true))
632 }
633
634 fn with_stack(engine_state: Arc<EngineState>, stack: Arc<Stack>) -> Self {
635 let config = engine_state.get_config();
636 let options = CompletionOptions {
637 case_sensitive: config.completions.case_sensitive,
638 match_algorithm: config.completions.algorithm.into(),
639 sort: config.completions.sort,
640 match_description: false,
641 };
642 Self {
643 engine_state,
644 stack,
645 options,
646 }
647 }
648
649 fn to_background(&self) -> Self {
650 Self::for_background(Arc::clone(&self.engine_state), Arc::clone(&self.stack))
651 }
652
653 fn suggestions_for(&self, query: &CompletionQuery) -> (Suggestions, bool) {
654 let dispatched = self.dispatch_completions_at(query.typed(), query.cursor());
655 let suggestions = dispatched
656 .suggestions
657 .into_iter()
658 .map(|semantic_suggestion| semantic_suggestion.suggestion)
659 .collect();
660
661 (suggestions, dispatched.cacheable)
662 }
663
664 fn with_completion_site<R>(
669 &self,
670 query: &CompletionQuery,
671 f: impl FnOnce(&StateWorkingSet, &Arc<Block>, &CompletionSite, &str, usize) -> R,
672 ) -> R {
673 let line = query.typed();
674 let position = query.cursor();
675 let mut working_set = StateWorkingSet::new(&self.engine_state);
676 let offset = working_set.next_span_start();
677 let block = parse(&mut working_set, Some("completer"), line.as_bytes(), false);
678 let site = self.resolve_completion_site(&block, &working_set, position, offset, line);
679 f(&working_set, &block, &site, line, offset)
680 }
681
682 fn query_runs_user_closure(
683 &self,
684 working_set: &StateWorkingSet,
685 site: &CompletionSite,
686 line: &str,
687 offset: usize,
688 ) -> bool {
689 self.site_runs_user_closure(site, working_set)
690 || self.multiword_argument_runs_user_closure(site, working_set, line, offset)
691 }
692
693 #[cfg(test)]
697 fn should_complete_on_repl_thread(&self, query: &CompletionQuery) -> bool {
698 self.with_completion_site(query, |working_set, _, site, line, offset| {
699 self.query_runs_user_closure(working_set, site, line, offset)
700 })
701 }
702
703 fn complete_user_closure_on_repl_thread(&self, query: &CompletionQuery) -> Option<Suggestions> {
708 self.with_completion_site(query, |working_set, block, site, line, offset| {
709 if !self.query_runs_user_closure(working_set, site, line, offset) {
710 return None;
711 }
712 let _tty = crate::util::ReplTerminalGuard::capture();
713 let dispatched = self.fetch_completions_by_block(
714 Arc::clone(block),
715 working_set,
716 query.cursor(),
717 offset,
718 line,
719 );
720 Some(
721 dispatched
722 .suggestions
723 .into_iter()
724 .map(|semantic_suggestion| semantic_suggestion.suggestion)
725 .collect(),
726 )
727 })
728 }
729
730 fn has_external_completer(&self) -> bool {
731 self.engine_state
732 .get_config()
733 .completions
734 .external
735 .completer
736 .is_some()
737 }
738
739 fn signature_runs_user_closure(&self, signature: &Signature) -> bool {
740 match signature.complete {
741 Some(CommandWideCompleter::Command(_)) => true,
742 Some(CommandWideCompleter::External) => self.has_external_completer(),
743 None => false,
744 }
745 }
746
747 fn site_runs_user_closure(&self, site: &CompletionSite, working_set: &StateWorkingSet) -> bool {
748 match &site.kind {
749 SiteKind::ExternalArg { .. } => self.has_external_completer(),
750 SiteKind::FlagName { call, .. } => {
751 self.signature_runs_user_closure(&working_set.get_decl(call.decl_id).signature())
752 }
753 SiteKind::FlagValue { call, flag, .. } => {
754 let signature = working_set.get_decl(call.decl_id).signature();
755 let resolved = find_flag(&signature, *flag);
756 let custom = resolved.as_ref().and_then(|flag| flag.completion.as_ref());
757 matches!(custom, Some(Completion::Command(_)))
758 || self.signature_runs_user_closure(&signature)
759 }
760 SiteKind::Positional {
761 call,
762 sig_positional,
763 ..
764 } => {
765 let signature = working_set.get_decl(call.decl_id).signature();
766 let custom = signature
767 .get_positional(*sig_positional)
768 .and_then(|positional| positional.completion.as_ref());
769 matches!(custom, Some(Completion::Command(_)))
770 || self.signature_runs_user_closure(&signature)
771 }
772 _ => false,
773 }
774 }
775
776 fn multiword_argument_runs_user_closure(
777 &self,
778 site: &CompletionSite,
779 working_set: &StateWorkingSet,
780 contents: &str,
781 offset: usize,
782 ) -> bool {
783 if !matches!(site.kind, SiteKind::Command { .. })
784 || !working_set
785 .get_span_contents(site.span)
786 .iter()
787 .any(u8::is_ascii_whitespace)
788 {
789 return false;
790 }
791
792 let mut parse_ws = StateWorkingSet::new(&self.engine_state);
793 let _ = parse_ws.add_file("completer", contents.as_bytes());
794 let Some(shorter) = parse_shorter_head_reading(&mut parse_ws, site.span, None) else {
795 return false;
796 };
797 let position = site.cursor.saturating_sub(offset);
798 let shorter_site = self.finalize_site(
799 self.resolve_expression_site(&shorter, site.cursor, &parse_ws),
800 contents,
801 position,
802 offset,
803 );
804 self.site_runs_user_closure(&shorter_site, &parse_ws)
805 }
806
807 pub fn fetch_completions_at(&self, line: &str, position: usize) -> Vec<SemanticSuggestion> {
808 self.dispatch_completions_at(line, position).suggestions
809 }
810
811 fn dispatch_completions_at(&self, line: &str, position: usize) -> Dispatched {
812 let safe_position = line.floor_char_boundary(position);
813 let sliced_line = &line[..safe_position];
816
817 let mut working_set = StateWorkingSet::new(&self.engine_state);
818 let span_offset = working_set.next_span_start();
819
820 let block = parse(
821 &mut working_set,
822 Some("completer"),
823 sliced_line.as_bytes(),
824 false,
825 );
826
827 self.fetch_completions_by_block(
828 block,
829 &working_set,
830 safe_position,
831 span_offset,
832 sliced_line,
833 )
834 }
835
836 pub fn fetch_completions_within_file(
837 &self,
838 filename: &str,
839 position: usize,
840 contents: &str,
841 ) -> Vec<SemanticSuggestion> {
842 let mut working_set = StateWorkingSet::new(&self.engine_state);
843
844 let block = parse(&mut working_set, Some(filename), contents.as_bytes(), false);
846
847 let Some(file_span) = working_set.get_span_for_filename(filename) else {
848 return Vec::new();
849 };
850
851 self.fetch_completions_by_block(block, &working_set, position, file_span.start, contents)
852 .suggestions
853 }
854
855 fn fetch_completions_by_block(
857 &self,
858 block: Arc<Block>,
859 working_set: &StateWorkingSet,
860 position: usize,
861 offset: usize,
862 contents: &str,
863 ) -> Dispatched {
864 let site = self.resolve_completion_site(&block, working_set, position, offset, contents);
865 let mut dispatched = self.dispatch_completion_site(&site, working_set, offset);
866
867 let argument_reading =
870 self.complete_multiword_head_as_argument(&site, working_set, offset, contents);
871 dispatched.cacheable |= argument_reading.cacheable;
872 dispatched
873 .suggestions
874 .splice(..0, argument_reading.suggestions);
875 dispatched
876 }
877
878 fn complete_multiword_head_as_argument(
883 &self,
884 site: &CompletionSite,
885 working_set: &StateWorkingSet,
886 offset: usize,
887 contents: &str,
888 ) -> Dispatched {
889 if !matches!(site.kind, SiteKind::Command { .. })
890 || !working_set
891 .get_span_contents(site.span)
892 .iter()
893 .any(u8::is_ascii_whitespace)
894 {
895 return Dispatched::default();
896 }
897
898 let mut parse_ws = StateWorkingSet::new(&self.engine_state);
899 let _ = parse_ws.add_file("completer", contents.as_bytes());
900 let Some(shorter) = parse_shorter_head_reading(&mut parse_ws, site.span, None) else {
901 return Dispatched::default();
902 };
903
904 let position = site.cursor.saturating_sub(offset);
905 let shorter_site = self.finalize_site(
906 self.resolve_expression_site(&shorter, site.cursor, &parse_ws),
907 contents,
908 position,
909 offset,
910 );
911 if matches!(shorter_site.kind, SiteKind::Command { .. }) {
913 return Dispatched::default();
914 }
915
916 let mut dispatched = self.dispatch_completion_site(&shorter_site, &parse_ws, offset);
917 dispatched
919 .suggestions
920 .retain(|candidate| !matches!(candidate.kind, Some(SuggestionKind::Command(..))));
921 dispatched
922 }
923
924 fn dispatch_completion_site(
926 &self,
927 site: &CompletionSite,
928 working_set: &StateWorkingSet,
929 offset: usize,
930 ) -> Dispatched {
931 let completion_context =
932 self.context(working_set, site.span, site.typed_prefix.as_bytes(), offset);
933
934 match &site.kind {
935 SiteKind::Command { node } => {
936 let completions = self.command_completion_helper(
937 working_set,
938 site.span,
939 offset,
940 self.command_completion_for_head(*node, site.span, working_set),
941 );
942
943 if completions.suggestions.is_empty() {
944 self.suggestions_at(&mut FileCompletion, working_set, site.span, offset)
945 } else {
946 completions
947 }
948 }
949
950 SiteKind::FlagName { .. }
951 | SiteKind::FlagValue { .. }
952 | SiteKind::Positional { .. } => {
953 self.dispatch_call_completion_site(site, working_set, offset, &completion_context)
954 }
955
956 SiteKind::Operator { lhs } => OperatorCompletion {
957 left_hand_side: lhs,
958 }
959 .fetch(&completion_context)
960 .into(),
961
962 SiteKind::CellPath { path } => CellPathCompletion {
963 full_cell_path: path,
964 cursor: site.cursor,
965 }
966 .fetch(&completion_context)
967 .into(),
968
969 SiteKind::Variable => {
970 self.variable_names_completion_helper(working_set, site.span, offset)
971 }
972
973 SiteKind::AttributeName => AttributeCompletion.fetch(&completion_context).into(),
974
975 SiteKind::AttributableItem => AttributableCompletion.fetch(&completion_context).into(),
976
977 SiteKind::ExternalArg { .. } => {
978 self.dispatch_external_arg(site, working_set, offset, &completion_context)
979 }
980
981 SiteKind::File => {
982 self.suggestions_at(&mut FileCompletion, working_set, site.span, offset)
983 }
984 }
985 }
986
987 fn dispatch_external_arg(
990 &self,
991 site: &CompletionSite,
992 working_set: &StateWorkingSet,
993 offset: usize,
994 completion_context: &Context,
995 ) -> Dispatched {
996 let SiteKind::ExternalArg {
997 call: external_call,
998 index,
999 } = &site.kind
1000 else {
1001 return Dispatched::default();
1002 };
1003 let external_call = *external_call;
1004 let Expr::ExternalCall(head, _) = &external_call.expr else {
1005 return Dispatched::default();
1006 };
1007
1008 if *index == 0 {
1010 let head_command = working_set.get_span_contents(head.span);
1011 if head_command == b"sudo" || head_command == b"doas" {
1012 let commands = self.command_completion_helper(
1013 working_set,
1014 site.span,
1015 offset,
1016 CommandCompletion::new(CommandScope::All),
1017 );
1018 if !commands.suggestions.is_empty() {
1019 return commands;
1020 }
1021 }
1022 }
1023
1024 let mut dispatched = Dispatched::default();
1025 let mut external_answered = false;
1026
1027 if let Some(closure) = self
1029 .engine_state
1030 .get_config()
1031 .completions
1032 .external
1033 .completer
1034 .as_ref()
1035 {
1036 let mut completion = CommandWideCompletion::closure(closure, external_call);
1037 let fetched = completion.fetch(completion_context);
1038 external_answered = !fetched.needs_fallback();
1039 dispatched.merge(fetched.into());
1040 }
1041
1042 let subcommands =
1045 self.subcommand_suggestions(working_set, external_call.span.start, site.cursor, offset);
1046
1047 if !external_answered
1049 && dispatched.suggestions.is_empty()
1050 && subcommands.suggestions.is_empty()
1051 {
1052 dispatched.merge(self.suggestions_at(
1053 &mut FileCompletion,
1054 working_set,
1055 site.span,
1056 offset,
1057 ));
1058 }
1059
1060 dispatched.merge(subcommands);
1061 dispatched
1062 }
1063
1064 fn dispatch_call_completion_site(
1066 &self,
1067 site: &CompletionSite,
1068 working_set: &StateWorkingSet,
1069 offset: usize,
1070 completion_context: &Context,
1071 ) -> Dispatched {
1072 let (call, element) = match &site.kind {
1074 SiteKind::FlagName { call, element }
1075 | SiteKind::FlagValue { call, element, .. }
1076 | SiteKind::Positional { call, element, .. } => (*call, *element),
1077 _ => return Dispatched::default(),
1078 };
1079
1080 let signature = working_set.get_decl(call.decl_id).signature();
1081
1082 let subcommands =
1085 self.subcommand_suggestions(working_set, call.head.start, site.cursor, offset);
1086
1087 let argument_value = |engine: &Self, arg_type, custom, arg_slot, declared_shape| {
1090 engine.complete_argument_value(
1091 custom,
1092 ArgValueCompletion {
1093 call,
1094 arg_type,
1095 need_fallback: subcommands.suggestions.is_empty(),
1096 completer: engine,
1097 arg_idx: arg_slot,
1098 declared_shape,
1099 cursor: site.cursor,
1100 },
1101 completion_context,
1102 &signature,
1103 element,
1104 site.cursor,
1105 )
1106 };
1107
1108 let mut results = match &site.kind {
1109 SiteKind::FlagName { .. } => {
1110 self.complete_flag_names(call.decl_id, completion_context, &signature, element)
1111 }
1112 SiteKind::FlagValue { flag, arg_slot, .. } => {
1113 let resolved = find_flag(&signature, *flag);
1114 argument_value(
1115 self,
1116 ArgType::Flag(Cow::Borrowed(flag.name())),
1117 resolved.as_ref().and_then(|flag| flag.completion.clone()),
1118 *arg_slot,
1119 resolved.and_then(|flag| flag.arg),
1120 )
1121 }
1122 SiteKind::Positional {
1123 sig_positional,
1124 arg_slot,
1125 ..
1126 } => {
1127 let positional = signature.get_positional(*sig_positional);
1128 argument_value(
1129 self,
1130 ArgType::Positional(*sig_positional),
1131 positional.and_then(|positional| positional.completion.clone()),
1132 *arg_slot,
1133 positional.map(|positional| positional.shape.clone()),
1134 )
1135 }
1136 _ => Dispatched::default(),
1137 };
1138
1139 results.merge(subcommands);
1140 results
1141 }
1142
1143 pub(crate) fn resolve_completion_site<'a>(
1145 &self,
1146 block: &'a Block,
1147 working_set: &'a StateWorkingSet,
1148 position: usize,
1149 offset: usize,
1150 contents: &'a str,
1151 ) -> CompletionSite<'a> {
1152 let absolute_position = position + offset;
1153
1154 let touched_expression = block
1156 .find_map(working_set, &|expression: &Expression| {
1157 find_pipeline_element_by_position(expression, working_set, absolute_position)
1158 })
1159 .or_else(|| check_redirection_in_block(block, absolute_position))
1160 .or_else(|| trailing_gap_element(block, working_set, absolute_position));
1162
1163 let site = match touched_expression {
1164 Some(expression) => {
1165 self.resolve_expression_site(expression, absolute_position, working_set)
1166 }
1167 None => self.resolve_fallback_site(block, working_set, absolute_position),
1168 };
1169
1170 self.finalize_site(site, contents, position, offset)
1171 }
1172
1173 fn finalize_site<'a>(
1177 &self,
1178 mut site: CompletionSite<'a>,
1179 contents: &'a str,
1180 position: usize,
1181 offset: usize,
1182 ) -> CompletionSite<'a> {
1183 let token_start = site.span.start.saturating_sub(offset);
1184 site.typed_prefix = contents
1185 .get(token_start..position)
1186 .map(Cow::Borrowed)
1187 .unwrap_or(Cow::Borrowed(""));
1188 site.cursor = position + offset;
1189 site
1190 }
1191
1192 fn resolve_expression_site<'a>(
1193 &self,
1194 expression: &'a Expression,
1195 absolute_position: usize,
1196 working_set: &'a StateWorkingSet,
1197 ) -> CompletionSite<'a> {
1198 if absolute_position > expression.span.end && is_operator_lhs(&expression.expr) {
1200 return CompletionSite::new(
1201 SiteKind::Operator { lhs: expression },
1202 Span::point(absolute_position),
1203 );
1204 }
1205
1206 match &expression.expr {
1208 Expr::Call(call) => {
1209 self.resolve_call_site(call, expression, absolute_position, working_set)
1210 }
1211 Expr::ExternalCall(head, arguments) => {
1212 self.resolve_external_call_site(expression, head, arguments, absolute_position)
1213 }
1214 Expr::AttributeBlock(attribute_block) => {
1215 self.resolve_attribute_site(attribute_block, absolute_position)
1216 }
1217 Expr::Var(_) => CompletionSite::new(SiteKind::Variable, expression.span),
1218 Expr::FullCellPath(full_cell_path) => {
1220 let has_dot = working_set
1221 .get_span_contents(expression.span)
1222 .ends_with(b".");
1223
1224 let kind = if full_cell_path.tail.is_empty() && !has_dot {
1225 SiteKind::Variable
1226 } else {
1227 SiteKind::CellPath {
1228 path: full_cell_path,
1229 }
1230 };
1231
1232 CompletionSite::new(kind, expression.span)
1233 }
1234 Expr::BinaryOp(left_hand_side, operator, _) => CompletionSite::new(
1235 SiteKind::Operator {
1236 lhs: left_hand_side.as_ref(),
1237 },
1238 operator.span,
1239 ),
1240 _ => CompletionSite::new(SiteKind::File, expression.span), }
1242 }
1243
1244 fn resolve_external_call_site<'a>(
1247 &self,
1248 expression: &'a Expression,
1249 head: &'a Expression,
1250 arguments: &'a [ExternalArgument],
1251 absolute_position: usize,
1252 ) -> CompletionSite<'a> {
1253 if absolute_position <= head.span.end {
1254 return CompletionSite::new(
1255 SiteKind::command(expression),
1256 command_name_span(head.span, expression.span),
1257 );
1258 }
1259
1260 let (index, span) = arguments
1262 .iter()
1263 .enumerate()
1264 .find_map(|(index, argument)| {
1265 touches(argument.expr().span, absolute_position)
1266 .then_some((index, argument.expr().span))
1267 })
1268 .unwrap_or((arguments.len(), Span::point(absolute_position)));
1269
1270 CompletionSite::new(
1271 SiteKind::ExternalArg {
1272 call: expression,
1273 index,
1274 },
1275 span,
1276 )
1277 }
1278
1279 fn resolve_call_site<'a>(
1280 &self,
1281 call: &'a Call,
1282 expression: &'a Expression,
1283 absolute_position: usize,
1284 working_set: &'a StateWorkingSet,
1285 ) -> CompletionSite<'a> {
1286 if absolute_position <= call.head.end {
1288 return CompletionSite::new(
1289 SiteKind::command(expression),
1290 command_name_span(call.head, expression.span),
1291 );
1292 }
1293
1294 if let Some((argument_index, argument)) = call
1296 .arguments
1297 .iter()
1298 .enumerate()
1299 .find(|(_, argument)| touches(argument.span(), absolute_position))
1300 {
1301 return self.resolve_argument_site(
1302 call,
1303 expression,
1304 argument,
1305 argument_index,
1306 absolute_position,
1307 working_set,
1308 );
1309 }
1310
1311 if let Some(operator_left_hand_side) =
1313 self.row_condition_operator_lhs(call, working_set, absolute_position)
1314 {
1315 return CompletionSite::new(
1316 SiteKind::Operator {
1317 lhs: operator_left_hand_side,
1318 },
1319 Span::point(absolute_position),
1320 );
1321 }
1322
1323 let gap_start = call
1328 .arguments
1329 .last()
1330 .map_or(call.head.end, |argument| argument.span().end);
1331
1332 let gap = working_set.get_span_contents(Span::new(gap_start, absolute_position));
1333
1334 let token_start = gap
1336 .iter()
1337 .rposition(u8::is_ascii_whitespace)
1338 .map_or(gap_start, |index| gap_start + index + 1);
1339
1340 let trailing_token = Span::new(token_start, absolute_position);
1341 let token_is_flag = is_flag_token(working_set, trailing_token);
1342
1343 let point = Span::point(absolute_position);
1344
1345 if let Some(flag_ref) = self.pending_flag_value(call, working_set) {
1346 CompletionSite::new(
1349 SiteKind::FlagValue {
1350 call,
1351 element: expression,
1352 flag: flag_ref,
1353 arg_slot: call.arguments.len(),
1354 },
1355 point,
1356 )
1357 } else if token_is_flag {
1358 CompletionSite::new(
1359 SiteKind::FlagName {
1360 call,
1361 element: expression,
1362 },
1363 trailing_token,
1364 )
1365 } else {
1366 CompletionSite::new(
1367 SiteKind::Positional {
1368 call,
1369 element: expression,
1370 sig_positional: count_positionals(call, call.arguments.len()),
1371 arg_slot: call.arguments.len(),
1372 },
1373 point,
1374 )
1375 }
1376 }
1377
1378 fn row_condition_operator_lhs<'a>(
1381 &self,
1382 call: &'a Call,
1383 working_set: &'a StateWorkingSet,
1384 absolute_position: usize,
1385 ) -> Option<&'a Expression> {
1386 let block_id = call
1387 .arguments
1388 .iter()
1389 .rev()
1390 .find_map(|argument| match argument {
1391 Argument::Positional(Expression {
1392 expr: Expr::RowCondition(block_id),
1393 ..
1394 }) => Some(*block_id),
1395 _ => None,
1396 })?;
1397
1398 let last_term = &working_set
1399 .get_block(block_id)
1400 .pipelines
1401 .last()?
1402 .elements
1403 .last()?
1404 .expr;
1405
1406 if absolute_position <= last_term.span.end || !is_operator_lhs(&last_term.expr) {
1407 return None;
1408 }
1409
1410 let gap = working_set.get_span_contents(Span::new(last_term.span.end, absolute_position));
1411 gap.iter().all(u8::is_ascii_whitespace).then_some(last_term)
1412 }
1413
1414 fn pending_flag_value<'a>(
1416 &self,
1417 call: &'a Call,
1418 working_set: &StateWorkingSet,
1419 ) -> Option<FlagRef<'a>> {
1420 let Argument::Named((name, short, None)) = call.arguments.last()? else {
1421 return None;
1422 };
1423
1424 let flag_ref = FlagRef::from_named(name, short.as_ref());
1425 let signature = working_set.get_decl(call.decl_id).signature();
1426
1427 find_flag(&signature, flag_ref)?
1428 .arg
1429 .is_some()
1430 .then_some(flag_ref)
1431 }
1432
1433 fn resolve_argument_site<'a>(
1434 &self,
1435 call: &'a Call,
1436 expression: &'a Expression,
1437 argument: &'a Argument,
1438 argument_index: usize,
1439 absolute_position: usize,
1440 working_set: &StateWorkingSet,
1441 ) -> CompletionSite<'a> {
1442 let flag_name = SiteKind::FlagName {
1443 call,
1444 element: expression,
1445 };
1446
1447 let (kind, span) = match argument {
1448 Argument::Named((name, short, optional_value)) => {
1449 if let Some(value_expression) = optional_value
1450 .as_ref()
1451 .filter(|value| touches(value.span, absolute_position))
1452 {
1453 (
1454 SiteKind::FlagValue {
1455 call,
1456 element: expression,
1457 flag: FlagRef::from_named(name, short.as_ref()),
1458 arg_slot: argument_index,
1459 },
1460 value_expression.span,
1461 )
1462 } else {
1463 (flag_name, name.span)
1467 }
1468 }
1469 Argument::Positional(_) | Argument::Unknown(_) => {
1471 let kind = if is_flag_token(working_set, argument.span()) {
1472 flag_name
1473 } else {
1474 SiteKind::Positional {
1475 call,
1476 element: expression,
1477 sig_positional: count_positionals(call, argument_index),
1478 arg_slot: argument_index,
1479 }
1480 };
1481 (kind, argument.span())
1482 }
1483 Argument::Spread(_) => (SiteKind::File, argument.span()),
1484 };
1485
1486 CompletionSite::new(kind, span)
1487 }
1488
1489 fn resolve_attribute_site<'a>(
1490 &self,
1491 attribute_block: &'a AttributeBlock,
1492 absolute_position: usize,
1493 ) -> CompletionSite<'a> {
1494 if let Some(attribute) = attribute_block
1495 .attributes
1496 .iter()
1497 .find(|attribute| touches(attribute.expr.span, absolute_position))
1498 {
1499 return CompletionSite::new(SiteKind::AttributeName, attribute.expr.span);
1500 }
1501
1502 if touches(attribute_block.item.span, absolute_position) {
1503 return CompletionSite::new(SiteKind::AttributableItem, attribute_block.item.span);
1504 }
1505
1506 let kind = match attribute_block.attributes.last() {
1510 Some(last) if absolute_position >= last.expr.span.end => SiteKind::AttributableItem,
1511 _ => SiteKind::AttributeName,
1512 };
1513
1514 CompletionSite::new(kind, Span::point(absolute_position))
1515 }
1516
1517 fn resolve_fallback_site<'a>(
1518 &self,
1519 block: &'a Block,
1520 working_set: &'a StateWorkingSet,
1521 absolute_position: usize,
1522 ) -> CompletionSite<'a> {
1523 let last_element = block
1524 .pipelines
1525 .last()
1526 .and_then(|pipeline| pipeline.elements.last())
1527 .map(|element| &element.expr);
1528
1529 let kind = if last_element
1532 .map(|element| working_set.get_span_contents(element.span))
1533 .is_some_and(|bytes| bytes.ends_with(b"@"))
1534 {
1535 SiteKind::AttributeName
1536 } else if matches!(last_element.map(|e| &e.expr), Some(Expr::AttributeBlock(_))) {
1537 SiteKind::AttributableItem
1538 } else {
1539 SiteKind::Command { node: None }
1540 };
1541
1542 CompletionSite::new(kind, Span::point(absolute_position))
1543 }
1544 fn complete_argument_value(
1545 &self,
1546 custom: Option<Completion>,
1547 mut arg_value: ArgValueCompletion,
1548 context: &Context,
1549 signature: &Signature,
1550 element_expression: &Expression,
1551 cursor: usize,
1552 ) -> Dispatched {
1553 let mut results = Dispatched::default();
1554
1555 if let Some(custom) = custom {
1556 let attempt = match custom {
1557 Completion::Builtin(kind) => self.complete_builtin(kind, &arg_value, context),
1559 other => {
1562 let element_line = String::from_utf8_lossy(
1563 context
1564 .working_set
1565 .get_span_contents(Span::new(element_expression.span.start, cursor)),
1566 );
1567 self.custom_completion_helper(other, element_line.as_ref(), context, cursor)
1568 }
1569 };
1570 let need_fallback = attempt.needs_fallback();
1571 results.merge(attempt.into());
1572 if !need_fallback {
1573 return results;
1574 }
1575 }
1576
1577 let attempt = self.command_wide_completion_helper(signature, element_expression, context);
1578 let need_fallback = attempt.needs_fallback();
1579 results.merge(attempt.into());
1580 if !need_fallback {
1581 return results;
1582 }
1583
1584 results.merge(arg_value.fetch(context).into());
1585 results
1586 }
1587
1588 fn complete_builtin(
1590 &self,
1591 kind: BuiltinCompletion,
1592 arg_value: &ArgValueCompletion,
1593 context: &Context,
1594 ) -> Fetched {
1595 match kind {
1596 BuiltinCompletion::NuFile { std_virtual_path } => {
1597 DotNuCompletion { std_virtual_path }.fetch(context)
1598 }
1599 BuiltinCompletion::ModuleExports => {
1600 arg_value.complete_module_exports(context, context.working_set)
1601 }
1602 BuiltinCompletion::EnvVar => EnvVarCompletion.fetch(context),
1603 BuiltinCompletion::Command { internal_only } => {
1604 let scope = if internal_only {
1605 CommandScope::InternalsOnly
1606 } else {
1607 CommandScope::All
1608 };
1609 CommandCompletion::quoted(scope).fetch(context)
1610 }
1611 }
1612 }
1613
1614 fn complete_flag_names(
1615 &self,
1616 decl_id: DeclId,
1617 context: &Context,
1618 signature: &Signature,
1619 element_expression: &Expression,
1620 ) -> Dispatched {
1621 let mut results: Dispatched = FlagCompletion { decl_id }.fetch(context).into();
1622 results.merge(
1623 self.command_wide_completion_helper(signature, element_expression, context)
1624 .into(),
1625 );
1626 results
1627 }
1628
1629 fn suggestions_at<C: Completer>(
1630 &self,
1631 completer: &mut C,
1632 working_set: &StateWorkingSet,
1633 span: Span,
1634 offset: usize,
1635 ) -> Dispatched {
1636 completer
1637 .fetch(&self.context(
1638 working_set,
1639 span,
1640 working_set.get_span_contents(span),
1641 offset,
1642 ))
1643 .into()
1644 }
1645
1646 fn variable_names_completion_helper(
1647 &self,
1648 working_set: &StateWorkingSet,
1649 span: Span,
1650 offset: usize,
1651 ) -> Dispatched {
1652 let prefix = working_set.get_span_contents(span);
1653 if !prefix.starts_with(b"$") {
1654 return Dispatched::default();
1655 }
1656 let ctx = self.context(working_set, span, prefix, offset);
1657 VariableCompletion.fetch(&ctx).into()
1658 }
1659
1660 fn command_completion_helper(
1661 &self,
1662 working_set: &StateWorkingSet,
1663 span: Span,
1664 offset: usize,
1665 mut command_completion: CommandCompletion,
1666 ) -> Dispatched {
1667 let prefix = working_set.get_span_contents(span);
1668 let ctx = self.context(working_set, span, prefix, offset);
1669 command_completion.fetch(&ctx).into()
1670 }
1671
1672 fn command_completion_for_head(
1676 &self,
1677 node: Option<&Expression>,
1678 span: Span,
1679 working_set: &StateWorkingSet,
1680 ) -> CommandCompletion {
1681 let sigil = node
1682 .filter(|node| node.span.start < span.start)
1683 .and_then(|node| working_set.get_span_contents(node.span).first().copied());
1684
1685 CommandCompletion::new(match sigil {
1686 Some(b'^') => CommandScope::ExternalsOnly,
1687 Some(b'%') => CommandScope::BuiltinsOnly,
1688 _ => CommandScope::All,
1689 })
1690 }
1691
1692 fn subcommand_suggestions(
1696 &self,
1697 working_set: &StateWorkingSet,
1698 command_start: usize,
1699 cursor: usize,
1700 offset: usize,
1701 ) -> Dispatched {
1702 if cursor <= command_start {
1703 return Dispatched::default();
1704 }
1705 self.command_completion_helper(
1706 working_set,
1707 Span::new(command_start, cursor),
1708 offset,
1709 CommandCompletion::new(CommandScope::InternalsOnly),
1710 )
1711 }
1712
1713 fn custom_completion_helper(
1714 &self,
1715 custom_completion: Completion,
1716 input: &str,
1717 context: &Context,
1718 pos: usize,
1719 ) -> Fetched {
1720 match custom_completion {
1721 Completion::Command(decl_id) => {
1722 let mut completer =
1723 CustomCompletion::new(decl_id, input.into(), pos - context.offset);
1724 completer.fetch(context)
1725 }
1726 Completion::List(list) => {
1727 let mut completer = StaticCompletion::new(list);
1728 completer.fetch(context)
1729 }
1730 Completion::Builtin(_) => Fetched::Absent,
1733 }
1734 }
1735
1736 fn command_wide_completion_helper(
1737 &self,
1738 signature: &Signature,
1739 element_expression: &Expression,
1740 context: &Context,
1741 ) -> Fetched {
1742 let completion = match signature.complete {
1743 Some(CommandWideCompleter::Command(decl_id)) => {
1744 CommandWideCompletion::command(context.working_set, decl_id, element_expression)
1745 }
1746 Some(CommandWideCompleter::External) => self
1747 .engine_state
1748 .get_config()
1749 .completions
1750 .external
1751 .completer
1752 .as_ref()
1753 .map(|closure| CommandWideCompletion::closure(closure, element_expression)),
1754 None => None,
1755 };
1756
1757 match completion {
1758 Some(mut completion) => {
1759 let context = Context {
1760 prefix: b"",
1761 ..*context
1762 };
1763 completion.fetch(&context)
1764 }
1765 None => Fetched::Absent,
1766 }
1767 }
1768
1769 pub(crate) fn context<'a>(
1770 &'a self,
1771 working_set: &'a StateWorkingSet,
1772 span: Span,
1773 prefix: &'a [u8],
1774 offset: usize,
1775 ) -> Context<'a> {
1776 Context {
1777 working_set,
1778 stack: self.stack.as_ref(),
1779 options: &self.options,
1780 span,
1781 prefix,
1782 offset,
1783 }
1784 }
1785
1786 pub(crate) fn options(&self) -> &CompletionOptions {
1787 &self.options
1788 }
1789}
1790
1791pub struct NuCompleter {
1792 engine: CompletionEngine,
1793 cache: NarrowingCache,
1794 cache_env: CacheEnv,
1797 worker: Option<CompletionWorker>,
1798 background: bool,
1801}
1802
1803impl NuCompleter {
1804 pub fn new(engine_state: Arc<EngineState>, stack: Arc<Stack>) -> Self {
1805 Self::with_cache(engine_state, stack, NarrowingCache::default())
1806 }
1807
1808 pub(crate) fn for_repl(
1811 engine_state: Arc<EngineState>,
1812 stack: Arc<Stack>,
1813 cache: NarrowingCache,
1814 ) -> Self {
1815 let mut completer = Self::with_cache(engine_state, stack, cache);
1816 completer.background = nu_experimental::BACKGROUND_COMPLETIONS.get();
1817 completer
1818 }
1819
1820 pub(crate) fn with_cache(
1821 engine_state: Arc<EngineState>,
1822 stack: Arc<Stack>,
1823 cache: NarrowingCache,
1824 ) -> Self {
1825 let engine = CompletionEngine::new(engine_state, stack);
1826 let cache_env = CacheEnv::of(&engine.engine_state, &engine.stack);
1827 let cache_size = engine.engine_state.get_config().completions.cache_size;
1829 cache.set_capacity(cache_size.try_into().unwrap_or(0));
1830 Self {
1831 engine,
1832 cache,
1833 cache_env,
1834 worker: None,
1835 background: true,
1836 }
1837 }
1838
1839 fn fresh_for(&self, query: &CompletionQuery) -> Option<Suggestions> {
1840 if let Some(worker) = self.worker.as_ref()
1841 && let Some(latest) = &worker.latest
1842 && &latest.query == query
1843 {
1844 return Some(latest.suggestions.clone());
1845 }
1846 self.cache.fresh(query, self.cache_env)
1847 }
1848
1849 fn settle_pending(&mut self, query: &CompletionQuery) {
1850 if let Some(worker) = self.worker.as_mut()
1851 && worker.pending.as_ref() == Some(query)
1852 {
1853 worker.pending = None;
1854 }
1855 }
1856
1857 fn stale_fallback(&self, query: &CompletionQuery) -> Suggestions {
1858 self.cache
1859 .narrowed_fallback(query, self.cache_env, self.engine.options())
1860 }
1861
1862 fn spawn_worker(engine: &CompletionEngine) -> CompletionWorker {
1863 let (request_tx, request_rx) = mpsc::channel::<CompletionQuery>();
1864 let (result_tx, result_rx) = mpsc::channel::<Completed>();
1865
1866 let engine = engine.to_background();
1867 thread::spawn(move || {
1868 while let Ok(mut query) = request_rx.recv() {
1869 while let Ok(newer) = request_rx.try_recv() {
1870 query = newer;
1871 }
1872
1873 let (suggestions, cacheable) = engine.suggestions_for(&query);
1874 let done = Completed {
1875 query,
1876 suggestions,
1877 cacheable,
1878 };
1879 if result_tx.send(done).is_err() {
1880 return;
1881 }
1882 }
1883 });
1884
1885 CompletionWorker {
1886 request_tx,
1887 result_rx,
1888 pending: None,
1889 latest: None,
1890 }
1891 }
1892
1893 fn fold_completed(&mut self, done: Completed) -> bool {
1894 let Self {
1895 cache,
1896 cache_env,
1897 worker,
1898 ..
1899 } = self;
1900 let Some(worker) = worker.as_mut() else {
1901 return false;
1902 };
1903 let settled = worker.pending.as_ref() == Some(&done.query);
1904 if done.cacheable {
1905 cache.store(done.query.clone(), *cache_env, done.suggestions.clone());
1906 }
1907 worker.latest = Some(done);
1908 settled
1909 }
1910
1911 fn try_recv_completed(&self) -> Option<Completed> {
1912 self.worker.as_ref()?.result_rx.try_recv().ok()
1913 }
1914
1915 fn recv_completed(&self, timeout: Duration) -> Option<Completed> {
1916 self.worker.as_ref()?.result_rx.recv_timeout(timeout).ok()
1917 }
1918
1919 fn drain_completed(&mut self) -> bool {
1920 let mut settled = false;
1921 while let Some(done) = self.try_recv_completed() {
1922 settled |= self.fold_completed(done);
1923 }
1924 settled
1925 }
1926
1927 pub fn complete_blocking(&mut self, line: &str, pos: usize) -> Suggestions {
1928 const BLOCKING_TIMEOUT: Duration = Duration::from_secs(30);
1929
1930 let fallback = match self.complete(line, pos) {
1931 CompletionResult::Fresh { suggestions, .. } => return suggestions,
1932 in_flight => in_flight.into_shared().unwrap_or_default(),
1933 };
1934
1935 let deadline = Instant::now() + BLOCKING_TIMEOUT;
1936 while let Some(remaining) = deadline.checked_duration_since(Instant::now()) {
1937 let Some(done) = self.recv_completed(remaining) else {
1938 break;
1939 };
1940 if self.fold_completed(done) {
1941 return self.complete(line, pos).into_shared().unwrap_or_default();
1942 }
1943 }
1944
1945 fallback
1946 }
1947}
1948
1949fn common_prefix_len(a: &str, b: &str) -> usize {
1951 a.char_indices()
1952 .zip(b.chars())
1953 .find_map(|((index, x), y)| (x != y).then_some(index))
1954 .unwrap_or_else(|| a.len().min(b.len()))
1955}
1956
1957fn partial_of(line: &str, suggestions: &[Suggestion]) -> Option<Partial> {
1958 let span = suggestions.first()?.span;
1959
1960 let mut matching_values = suggestions
1961 .iter()
1962 .filter(|suggestion| suggestion.span == span)
1963 .map(|suggestion| suggestion.value.as_str());
1964
1965 let first = matching_values.next()?;
1968 let shared_len = matching_values.try_fold(first.len(), |shared, value| {
1969 let common = common_prefix_len(first.get(..shared)?, value);
1970 (common > 0).then_some(common)
1971 })?;
1972 let shared_prefix = first.get(..shared_len)?;
1973
1974 let entered = line.get(span.start..span.end)?;
1975 let extends = shared_prefix != entered
1976 && shared_prefix
1977 .to_lowercase()
1978 .starts_with(&entered.to_lowercase());
1979
1980 extends.then_some(Partial {
1981 span,
1982 insert: shared_prefix.to_string(),
1983 })
1984}
1985
1986impl ReedlineCompleter for NuCompleter {
1987 fn complete(&mut self, line: &str, pos: usize) -> CompletionResult {
1988 if !self.background {
1989 let suggestions: Suggestions = self
1993 .engine
1994 .fetch_completions_at(line, pos)
1995 .into_iter()
1996 .map(|s| s.suggestion)
1997 .collect();
1998 let partial = partial_of(line, &suggestions);
1999 return CompletionResult::fresh(suggestions).with_partial(partial);
2000 }
2001
2002 let query = CompletionQuery::new(line, pos);
2003 self.drain_completed();
2004
2005 if let Some(suggestions) = self.fresh_for(&query) {
2006 self.settle_pending(&query);
2007 let partial = partial_of(line, &suggestions);
2008 return CompletionResult::fresh(suggestions).with_partial(partial);
2009 }
2010
2011 if let Some(suggestions) = self.engine.complete_user_closure_on_repl_thread(&query) {
2012 self.settle_pending(&query);
2013 let partial = partial_of(line, &suggestions);
2014 return CompletionResult::fresh(suggestions).with_partial(partial);
2015 }
2016
2017 let fallback = self.stale_fallback(&query);
2018 let partial = partial_of(line, &fallback);
2019
2020 let worker = self
2021 .worker
2022 .get_or_insert_with(|| Self::spawn_worker(&self.engine));
2023
2024 if worker.pending.as_ref() != Some(&query) {
2025 if worker.request_tx.send(query.clone()).is_ok() {
2026 worker.pending = Some(query);
2027 } else {
2028 self.worker = None;
2031 }
2032 }
2033
2034 CompletionResult::stale_or_pending(fallback, CompletionOrigin::new(line, pos))
2035 .with_partial(partial)
2036 }
2037
2038 fn poll_completion(&mut self) -> CompletionStatus {
2039 let settled = self.drain_completed();
2040
2041 match self.worker.as_mut() {
2042 Some(worker) if worker.pending.is_some() => {
2043 if settled {
2044 worker.pending = None;
2045 CompletionStatus::Ready
2046 } else {
2047 CompletionStatus::Pending
2048 }
2049 }
2050 _ => CompletionStatus::Idle,
2051 }
2052 }
2053}
2054
2055#[cfg(test)]
2056mod completer_tests {
2057 use super::*;
2058
2059 fn test_engine() -> Arc<EngineState> {
2060 let mut engine =
2061 nu_command::add_shell_command_context(nu_cmd_lang::create_default_context());
2062 let delta = StateWorkingSet::new(&engine).render();
2063 engine.merge_delta(delta).expect("merge_delta");
2064 Arc::new(engine)
2065 }
2066
2067 fn q(s: &str) -> CompletionQuery {
2068 CompletionQuery::new(s, s.len())
2069 }
2070
2071 fn token(start: usize) -> reedline::Span {
2073 reedline::Span::new(start, start)
2074 }
2075
2076 #[test]
2077 fn narrows_stays_within_one_token() {
2078 assert!(q("ls foobar").narrows(&q("ls foo"), token(3)));
2079
2080 assert!(!q("ls foo").narrows(&q("ls foo"), token(3)));
2082 assert!(!q("ls fo").narrows(&q("ls foo"), token(3)));
2083
2084 for narrowed in [
2086 "ls foo|from",
2087 "ls foo;ls",
2088 "ls foo/bar",
2089 "ls foo=1",
2090 "ls foo,2",
2091 ] {
2092 assert!(
2093 !q(narrowed).narrows(&q("ls foo"), token(3)),
2094 "narrowed across a boundary: {narrowed:?}"
2095 );
2096 }
2097 }
2098
2099 #[test]
2100 fn narrows_rejects_a_token_that_becomes_a_flag() {
2101 let base = q("from csv ");
2105 assert!(!q("from csv --sep").narrows(&base, token(base.cursor())));
2106
2107 assert!(q("from csv --sep").narrows(&q("from csv --s"), token(9)));
2109 }
2110
2111 #[test]
2112 fn background_engine_suppresses_stdin() {
2113 let engine = test_engine();
2114 let stack = Arc::new(Stack::new());
2115 let foreground = CompletionEngine::new(engine, stack);
2116 assert!(!foreground.stack.suppress_stdin);
2117 let background = foreground.to_background();
2118 assert!(background.stack.suppress_stdin);
2119 }
2120
2121 #[test]
2122 fn internal_command_completion_stays_on_the_worker() {
2123 let engine = CompletionEngine::new(test_engine(), Arc::new(Stack::new()));
2124 assert!(!engine.should_complete_on_repl_thread(&q("ls | c")));
2125 }
2126
2127 fn engine_with_external_completer() -> Arc<EngineState> {
2128 use nu_engine::eval_block;
2129 use nu_protocol::{PipelineData, debugger::WithoutDebug};
2130
2131 let mut engine = (*test_engine()).clone();
2132 let mut stack = Stack::new();
2133 let mut working_set = StateWorkingSet::new(&engine);
2134 let block = parse(
2135 &mut working_set,
2136 None,
2137 b"$env.config.completions.external.completer = {|spans| $spans}",
2138 false,
2139 );
2140 assert!(working_set.parse_errors.is_empty());
2141 engine
2142 .merge_delta(working_set.render())
2143 .expect("merge_delta");
2144 eval_block::<WithoutDebug>(&engine, &mut stack, &block, PipelineData::empty())
2145 .expect("eval completer config");
2146 engine.merge_env(&mut stack).expect("merge_env");
2147 Arc::new(engine)
2148 }
2149
2150 #[test]
2151 fn external_arg_with_completer_runs_on_the_repl_thread() {
2152 let engine =
2153 CompletionEngine::new(engine_with_external_completer(), Arc::new(Stack::new()));
2154 assert!(engine.should_complete_on_repl_thread(&q("nvim foo")));
2155 assert!(!engine.should_complete_on_repl_thread(&q("ls | c")));
2156 }
2157
2158 #[test]
2160 fn opted_out_completer_settles_inline() {
2161 let mut completer = NuCompleter::new(test_engine(), Arc::new(Stack::new()));
2162 completer.background = false;
2163
2164 let result = completer.complete("ls | c", 6);
2165 assert!(
2166 matches!(result, CompletionResult::Fresh { .. }),
2167 "expected a settled result, got {result:?}"
2168 );
2169 assert!(result.suggestions().iter().any(|s| s.value == "cd"));
2170 assert!(completer.worker.is_none(), "a worker was spawned anyway");
2171 assert_eq!(completer.poll_completion(), CompletionStatus::Idle);
2172 }
2173
2174 fn probe_engine() -> (Arc<EngineState>, Arc<Stack>) {
2178 let mut engine =
2179 nu_command::add_shell_command_context(nu_cmd_lang::create_default_context());
2180 let mut stack = Stack::new();
2181 let cwd = std::env::temp_dir()
2182 .to_string_lossy()
2183 .trim_end_matches(['/', '\\'])
2184 .replace('\\', "/");
2185 stack.add_env_var(
2186 "PWD".to_string(),
2187 nu_protocol::Value::string(&cwd, Span::unknown()),
2188 );
2189 stack.add_env_var(
2191 "PATH".to_string(),
2192 nu_protocol::Value::string(std::env::var("PATH").unwrap_or_default(), Span::unknown()),
2193 );
2194
2195 let setup = r#"$env.config.completions.external = {
2196 enable: true
2197 completer: {|spans|
2198 let piped = ("alpha\nbeta\n" | lines | length)
2199 let direct = ('gamma' | str trim)
2200 [$"piped-($piped)" $"direct-($direct)"]
2201 }
2202 }"#;
2203 let mut working_set = StateWorkingSet::new(&engine);
2204 let block = nu_parser::parse(&mut working_set, None, setup.as_bytes(), false);
2205 assert!(working_set.parse_errors.is_empty(), "setup failed to parse");
2206 engine.merge_delta(working_set.render()).expect("merge");
2207 nu_engine::eval_block::<nu_protocol::debugger::WithoutDebug>(
2208 &engine,
2209 &mut stack,
2210 &block,
2211 nu_protocol::PipelineData::empty(),
2212 )
2213 .expect("eval setup");
2214 engine.merge_env(&mut stack).expect("merge env");
2215
2216 (Arc::new(engine), Arc::new(stack))
2217 }
2218
2219 #[rstest::rstest]
2224 #[case::foreground(false)]
2225 #[case::background(true)]
2226 fn externals_in_a_completer_keep_their_stdout(#[case] suppress_stdin: bool) {
2227 let (engine, stack) = probe_engine();
2228 let engine = CompletionEngine::new(engine, isolated_stack(stack, suppress_stdin));
2229
2230 let values: Vec<String> = engine
2231 .fetch_completions_at("somecmd x", 9)
2232 .into_iter()
2233 .map(|s| s.suggestion.value)
2234 .collect();
2235
2236 assert!(
2237 values.iter().any(|v| v == "piped-2"),
2238 "a piped external lost its stdout: {values:?}"
2239 );
2240 assert!(
2241 values.iter().any(|v| v == "direct-gamma"),
2242 "a final external lost its stdout: {values:?}"
2243 );
2244 }
2245
2246 #[test]
2248 fn background_result_matches_the_synchronous_engine() {
2249 let engine = test_engine();
2250 let mut completer = NuCompleter::new(engine.clone(), Arc::new(Stack::new()));
2251
2252 let sorted = |mut values: Vec<String>| {
2253 values.sort();
2254 values
2255 };
2256 let expected = sorted(
2257 CompletionEngine::new(engine, Arc::new(Stack::new()))
2258 .fetch_completions_at("ls | c", 6)
2259 .into_iter()
2260 .map(|s| s.suggestion.value)
2261 .collect(),
2262 );
2263 assert!(expected.iter().any(|value| value == "cd"));
2264
2265 assert!(completer.complete("ls | c", 6).is_pending());
2267
2268 let settled = sorted(
2269 completer
2270 .complete_blocking("ls | c", 6)
2271 .iter()
2272 .map(|s| s.value.clone())
2273 .collect(),
2274 );
2275 assert_eq!(expected, settled);
2276 }
2277
2278 #[test]
2281 fn cache_outlives_the_completer_that_filled_it() {
2282 let engine = test_engine();
2283 let cache = NarrowingCache::default();
2284
2285 let mut filling_prompt =
2286 NuCompleter::with_cache(engine.clone(), Arc::new(Stack::new()), cache.clone());
2287 let warmed = filling_prompt.complete_blocking("ls | c", 6);
2288 assert!(warmed.iter().any(|s| s.value == "cd"));
2289 drop(filling_prompt);
2290
2291 let mut next_prompt = NuCompleter::with_cache(engine, Arc::new(Stack::new()), cache);
2292 let answer = next_prompt.complete("ls | c", 6);
2293 assert!(
2294 matches!(answer, CompletionResult::Fresh { .. }),
2295 "a carried-over cache entry should answer outright, got {answer:?}"
2296 );
2297 assert!(answer.suggestions().iter().any(|s| s.value == "cd"));
2298 }
2299
2300 #[test]
2302 fn cache_is_not_reused_in_a_different_environment() {
2303 use nu_protocol::Value;
2304
2305 let engine = test_engine();
2306 let cache = NarrowingCache::default();
2307
2308 let mut filling_prompt =
2309 NuCompleter::with_cache(engine.clone(), Arc::new(Stack::new()), cache.clone());
2310 assert!(!filling_prompt.complete_blocking("ls | c", 6).is_empty());
2311
2312 let mut moved = Stack::new();
2313 moved.add_env_var(
2314 "PATH".into(),
2315 Value::string("/somewhere/else", Span::unknown()),
2316 );
2317 let mut next_prompt = NuCompleter::with_cache(engine, Arc::new(moved), cache);
2318 assert!(
2319 next_prompt.complete("ls | c", 6).is_pending(),
2320 "entries from another environment must not answer"
2321 );
2322 }
2323
2324 #[test]
2326 fn cache_size_zero_disables_the_cache() {
2327 let mut engine = test_engine();
2328 {
2329 let state = Arc::make_mut(&mut engine);
2330 Arc::make_mut(&mut state.config).completions.cache_size = 0;
2331 }
2332 let cache = NarrowingCache::default();
2333
2334 let mut filling_prompt =
2335 NuCompleter::with_cache(engine.clone(), Arc::new(Stack::new()), cache.clone());
2336 assert!(!filling_prompt.complete_blocking("ls | c", 6).is_empty());
2337 drop(filling_prompt);
2338
2339 let mut next_prompt = NuCompleter::with_cache(engine, Arc::new(Stack::new()), cache);
2340 assert!(
2341 next_prompt.complete("ls | c", 6).is_pending(),
2342 "a disabled cache must not answer a query it could have answered"
2343 );
2344 }
2345
2346 #[test]
2349 fn a_narrowed_cache_answer_keeps_the_order_it_was_given() {
2350 let cache = NarrowingCache::default();
2351 let env = CacheEnv::of(&test_engine(), &Stack::new());
2352 let span = reedline::Span::new(3, 5);
2353
2354 let cached: Suggestions = ["config/", "config.nu"]
2356 .iter()
2357 .map(|value| Suggestion {
2358 value: (*value).to_string(),
2359 span,
2360 ..Default::default()
2361 })
2362 .collect::<Vec<_>>()
2363 .into();
2364
2365 cache.store(CompletionQuery::new("ls co", 5), env, cached);
2366
2367 let narrowed = cache.narrowed_fallback(
2368 &CompletionQuery::new("ls con", 6),
2369 env,
2370 &CompletionOptions::default(),
2371 );
2372
2373 let values: Vec<&str> = narrowed.iter().map(|s| s.value.as_str()).collect();
2374 assert_eq!(
2375 values,
2376 ["config/", "config.nu"],
2377 "the cached answer must not reorder what it stands in for"
2378 );
2379 }
2380}