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 suggestions: fetched.suggestions,
597 cacheable: fetched.cacheable,
598 }
599 }
600}
601
602pub struct CompletionEngine {
603 engine_state: Arc<EngineState>,
604 stack: Arc<Stack>,
605 options: CompletionOptions,
606}
607
608#[derive(Clone, Copy)]
609pub(crate) struct Context<'a> {
610 pub working_set: &'a StateWorkingSet<'a>,
611 pub stack: &'a Stack,
612 pub options: &'a CompletionOptions,
613 pub span: Span,
614 pub prefix: &'a [u8],
615 pub offset: usize,
616}
617
618impl Context<'_> {
619 pub(crate) fn prefix_str(&self) -> Cow<'_, str> {
620 String::from_utf8_lossy(self.prefix)
621 }
622}
623
624impl CompletionEngine {
625 pub fn new(engine_state: Arc<EngineState>, stack: Arc<Stack>) -> Self {
626 Self::with_stack(engine_state, isolated_stack(stack, false))
627 }
628
629 fn for_background(engine_state: Arc<EngineState>, stack: Arc<Stack>) -> Self {
630 Self::with_stack(engine_state, isolated_stack(stack, true))
631 }
632
633 fn with_stack(engine_state: Arc<EngineState>, stack: Arc<Stack>) -> Self {
634 let config = engine_state.get_config();
635 let options = CompletionOptions {
636 case_sensitive: config.completions.case_sensitive,
637 match_algorithm: config.completions.algorithm.into(),
638 sort: config.completions.sort,
639 match_description: false,
640 };
641 Self {
642 engine_state,
643 stack,
644 options,
645 }
646 }
647
648 fn to_background(&self) -> Self {
649 Self::for_background(Arc::clone(&self.engine_state), Arc::clone(&self.stack))
650 }
651
652 fn suggestions_for(&self, query: &CompletionQuery) -> (Suggestions, bool) {
653 let dispatched = self.dispatch_completions_at(query.typed(), query.cursor());
654 let suggestions = dispatched
655 .suggestions
656 .into_iter()
657 .map(|semantic_suggestion| semantic_suggestion.suggestion)
658 .collect();
659
660 (suggestions, dispatched.cacheable)
661 }
662
663 pub fn fetch_completions_at(&self, line: &str, position: usize) -> Vec<SemanticSuggestion> {
664 self.dispatch_completions_at(line, position).suggestions
665 }
666
667 fn dispatch_completions_at(&self, line: &str, position: usize) -> Dispatched {
668 let safe_position = line.floor_char_boundary(position);
669 let sliced_line = &line[..safe_position];
672
673 let mut working_set = StateWorkingSet::new(&self.engine_state);
674 let span_offset = working_set.next_span_start();
675
676 let block = parse(
677 &mut working_set,
678 Some("completer"),
679 sliced_line.as_bytes(),
680 false,
681 );
682
683 self.fetch_completions_by_block(
684 block,
685 &working_set,
686 safe_position,
687 span_offset,
688 sliced_line,
689 )
690 }
691
692 pub fn fetch_completions_within_file(
693 &self,
694 filename: &str,
695 position: usize,
696 contents: &str,
697 ) -> Vec<SemanticSuggestion> {
698 let mut working_set = StateWorkingSet::new(&self.engine_state);
699
700 let block = parse(&mut working_set, Some(filename), contents.as_bytes(), false);
702
703 let Some(file_span) = working_set.get_span_for_filename(filename) else {
704 return Vec::new();
705 };
706
707 self.fetch_completions_by_block(block, &working_set, position, file_span.start, contents)
708 .suggestions
709 }
710
711 fn fetch_completions_by_block(
713 &self,
714 block: Arc<Block>,
715 working_set: &StateWorkingSet,
716 position: usize,
717 offset: usize,
718 contents: &str,
719 ) -> Dispatched {
720 let site = self.resolve_completion_site(&block, working_set, position, offset, contents);
721 let mut dispatched = self.dispatch_completion_site(&site, working_set, offset);
722
723 let argument_reading =
726 self.complete_multiword_head_as_argument(&site, working_set, offset, contents);
727 dispatched.cacheable |= argument_reading.cacheable;
728 dispatched
729 .suggestions
730 .splice(..0, argument_reading.suggestions);
731 dispatched
732 }
733
734 fn complete_multiword_head_as_argument(
739 &self,
740 site: &CompletionSite,
741 working_set: &StateWorkingSet,
742 offset: usize,
743 contents: &str,
744 ) -> Dispatched {
745 if !matches!(site.kind, SiteKind::Command { .. })
746 || !working_set
747 .get_span_contents(site.span)
748 .iter()
749 .any(u8::is_ascii_whitespace)
750 {
751 return Dispatched::default();
752 }
753
754 let mut parse_ws = StateWorkingSet::new(&self.engine_state);
755 let _ = parse_ws.add_file("completer", contents.as_bytes());
756 let Some(shorter) = parse_shorter_head_reading(&mut parse_ws, site.span, None) else {
757 return Dispatched::default();
758 };
759
760 let position = site.cursor.saturating_sub(offset);
761 let shorter_site = self.finalize_site(
762 self.resolve_expression_site(&shorter, site.cursor, &parse_ws),
763 contents,
764 position,
765 offset,
766 );
767 if matches!(shorter_site.kind, SiteKind::Command { .. }) {
769 return Dispatched::default();
770 }
771
772 let mut dispatched = self.dispatch_completion_site(&shorter_site, &parse_ws, offset);
773 dispatched
775 .suggestions
776 .retain(|candidate| !matches!(candidate.kind, Some(SuggestionKind::Command(..))));
777 dispatched
778 }
779
780 fn dispatch_completion_site(
782 &self,
783 site: &CompletionSite,
784 working_set: &StateWorkingSet,
785 offset: usize,
786 ) -> Dispatched {
787 let completion_context =
788 self.context(working_set, site.span, site.typed_prefix.as_bytes(), offset);
789
790 match &site.kind {
791 SiteKind::Command { node } => {
792 let completions = self.command_completion_helper(
793 working_set,
794 site.span,
795 offset,
796 self.command_completion_for_head(*node, site.span, working_set),
797 );
798
799 if completions.suggestions.is_empty() {
800 self.suggestions_at(&mut FileCompletion, working_set, site.span, offset)
801 } else {
802 completions
803 }
804 }
805
806 SiteKind::FlagName { .. }
807 | SiteKind::FlagValue { .. }
808 | SiteKind::Positional { .. } => {
809 self.dispatch_call_completion_site(site, working_set, offset, &completion_context)
810 }
811
812 SiteKind::Operator { lhs } => OperatorCompletion {
813 left_hand_side: lhs,
814 }
815 .fetch(&completion_context)
816 .into(),
817
818 SiteKind::CellPath { path } => CellPathCompletion {
819 full_cell_path: path,
820 cursor: site.cursor,
821 }
822 .fetch(&completion_context)
823 .into(),
824
825 SiteKind::Variable => {
826 self.variable_names_completion_helper(working_set, site.span, offset)
827 }
828
829 SiteKind::AttributeName => AttributeCompletion.fetch(&completion_context).into(),
830
831 SiteKind::AttributableItem => AttributableCompletion.fetch(&completion_context).into(),
832
833 SiteKind::ExternalArg { .. } => {
834 self.dispatch_external_arg(site, working_set, offset, &completion_context)
835 }
836
837 SiteKind::File => {
838 self.suggestions_at(&mut FileCompletion, working_set, site.span, offset)
839 }
840 }
841 }
842
843 fn dispatch_external_arg(
846 &self,
847 site: &CompletionSite,
848 working_set: &StateWorkingSet,
849 offset: usize,
850 completion_context: &Context,
851 ) -> Dispatched {
852 let SiteKind::ExternalArg {
853 call: external_call,
854 index,
855 } = &site.kind
856 else {
857 return Dispatched::default();
858 };
859 let external_call = *external_call;
860 let Expr::ExternalCall(head, _) = &external_call.expr else {
861 return Dispatched::default();
862 };
863
864 if *index == 0 {
866 let head_command = working_set.get_span_contents(head.span);
867 if head_command == b"sudo" || head_command == b"doas" {
868 let commands = self.command_completion_helper(
869 working_set,
870 site.span,
871 offset,
872 CommandCompletion::new(CommandScope::All),
873 );
874 if !commands.suggestions.is_empty() {
875 return commands;
876 }
877 }
878 }
879
880 let mut dispatched = Dispatched::default();
881 let mut external_answered = false;
882
883 if let Some(closure) = self
885 .engine_state
886 .get_config()
887 .completions
888 .external
889 .completer
890 .as_ref()
891 {
892 let mut completion = CommandWideCompletion::closure(closure, external_call);
893 let fetched = completion.fetch(completion_context);
894 external_answered = !fetched.need_fallback;
895 dispatched.merge(fetched.into());
896 }
897
898 let subcommands =
901 self.subcommand_suggestions(working_set, external_call.span.start, site.cursor, offset);
902
903 if !external_answered
905 && dispatched.suggestions.is_empty()
906 && subcommands.suggestions.is_empty()
907 {
908 dispatched.merge(self.suggestions_at(
909 &mut FileCompletion,
910 working_set,
911 site.span,
912 offset,
913 ));
914 }
915
916 dispatched.merge(subcommands);
917 dispatched
918 }
919
920 fn dispatch_call_completion_site(
922 &self,
923 site: &CompletionSite,
924 working_set: &StateWorkingSet,
925 offset: usize,
926 completion_context: &Context,
927 ) -> Dispatched {
928 let (call, element) = match &site.kind {
930 SiteKind::FlagName { call, element }
931 | SiteKind::FlagValue { call, element, .. }
932 | SiteKind::Positional { call, element, .. } => (*call, *element),
933 _ => return Dispatched::default(),
934 };
935
936 let signature = working_set.get_decl(call.decl_id).signature();
937
938 let subcommands =
941 self.subcommand_suggestions(working_set, call.head.start, site.cursor, offset);
942
943 let argument_value = |engine: &Self, arg_type, custom, arg_slot| {
945 engine.complete_argument_value(
946 custom,
947 ArgValueCompletion {
948 call,
949 arg_type,
950 need_fallback: subcommands.suggestions.is_empty(),
951 completer: engine,
952 arg_idx: arg_slot,
953 cursor: site.cursor,
954 },
955 completion_context,
956 &signature,
957 element,
958 site.cursor,
959 )
960 };
961
962 let mut results = match &site.kind {
963 SiteKind::FlagName { .. } => {
964 self.complete_flag_names(call.decl_id, completion_context, &signature, element)
965 }
966 SiteKind::FlagValue { flag, arg_slot, .. } => argument_value(
967 self,
968 ArgType::Flag(Cow::Borrowed(flag.name())),
969 find_flag(&signature, *flag).and_then(|flag| flag.completion),
970 *arg_slot,
971 ),
972 SiteKind::Positional {
973 sig_positional,
974 arg_slot,
975 ..
976 } => argument_value(
977 self,
978 ArgType::Positional(*sig_positional),
979 signature
980 .get_positional(*sig_positional)
981 .and_then(|positional| positional.completion.clone()),
982 *arg_slot,
983 ),
984 _ => Dispatched::default(),
985 };
986
987 results.merge(subcommands);
988 results
989 }
990
991 pub(crate) fn resolve_completion_site<'a>(
993 &self,
994 block: &'a Block,
995 working_set: &'a StateWorkingSet,
996 position: usize,
997 offset: usize,
998 contents: &'a str,
999 ) -> CompletionSite<'a> {
1000 let absolute_position = position + offset;
1001
1002 let touched_expression = block
1004 .find_map(working_set, &|expression: &Expression| {
1005 find_pipeline_element_by_position(expression, working_set, absolute_position)
1006 })
1007 .or_else(|| check_redirection_in_block(block, absolute_position))
1008 .or_else(|| trailing_gap_element(block, working_set, absolute_position));
1010
1011 let site = match touched_expression {
1012 Some(expression) => {
1013 self.resolve_expression_site(expression, absolute_position, working_set)
1014 }
1015 None => self.resolve_fallback_site(block, working_set, absolute_position),
1016 };
1017
1018 self.finalize_site(site, contents, position, offset)
1019 }
1020
1021 fn finalize_site<'a>(
1025 &self,
1026 mut site: CompletionSite<'a>,
1027 contents: &'a str,
1028 position: usize,
1029 offset: usize,
1030 ) -> CompletionSite<'a> {
1031 let token_start = site.span.start.saturating_sub(offset);
1032 site.typed_prefix = contents
1033 .get(token_start..position)
1034 .map(Cow::Borrowed)
1035 .unwrap_or(Cow::Borrowed(""));
1036 site.cursor = position + offset;
1037 site
1038 }
1039
1040 fn resolve_expression_site<'a>(
1041 &self,
1042 expression: &'a Expression,
1043 absolute_position: usize,
1044 working_set: &'a StateWorkingSet,
1045 ) -> CompletionSite<'a> {
1046 if absolute_position > expression.span.end && is_operator_lhs(&expression.expr) {
1048 return CompletionSite::new(
1049 SiteKind::Operator { lhs: expression },
1050 Span::point(absolute_position),
1051 );
1052 }
1053
1054 match &expression.expr {
1056 Expr::Call(call) => {
1057 self.resolve_call_site(call, expression, absolute_position, working_set)
1058 }
1059 Expr::ExternalCall(head, arguments) => {
1060 self.resolve_external_call_site(expression, head, arguments, absolute_position)
1061 }
1062 Expr::AttributeBlock(attribute_block) => {
1063 self.resolve_attribute_site(attribute_block, absolute_position)
1064 }
1065 Expr::Var(_) => CompletionSite::new(SiteKind::Variable, expression.span),
1066 Expr::FullCellPath(full_cell_path) => {
1068 let has_dot = working_set
1069 .get_span_contents(expression.span)
1070 .ends_with(b".");
1071
1072 let kind = if full_cell_path.tail.is_empty() && !has_dot {
1073 SiteKind::Variable
1074 } else {
1075 SiteKind::CellPath {
1076 path: full_cell_path,
1077 }
1078 };
1079
1080 CompletionSite::new(kind, expression.span)
1081 }
1082 Expr::BinaryOp(left_hand_side, operator, _) => CompletionSite::new(
1083 SiteKind::Operator {
1084 lhs: left_hand_side.as_ref(),
1085 },
1086 operator.span,
1087 ),
1088 _ => CompletionSite::new(SiteKind::File, expression.span), }
1090 }
1091
1092 fn resolve_external_call_site<'a>(
1095 &self,
1096 expression: &'a Expression,
1097 head: &'a Expression,
1098 arguments: &'a [ExternalArgument],
1099 absolute_position: usize,
1100 ) -> CompletionSite<'a> {
1101 if absolute_position <= head.span.end {
1102 return CompletionSite::new(
1103 SiteKind::command(expression),
1104 command_name_span(head.span, expression.span),
1105 );
1106 }
1107
1108 let (index, span) = arguments
1110 .iter()
1111 .enumerate()
1112 .find_map(|(index, argument)| {
1113 touches(argument.expr().span, absolute_position)
1114 .then_some((index, argument.expr().span))
1115 })
1116 .unwrap_or((arguments.len(), Span::point(absolute_position)));
1117
1118 CompletionSite::new(
1119 SiteKind::ExternalArg {
1120 call: expression,
1121 index,
1122 },
1123 span,
1124 )
1125 }
1126
1127 fn resolve_call_site<'a>(
1128 &self,
1129 call: &'a Call,
1130 expression: &'a Expression,
1131 absolute_position: usize,
1132 working_set: &'a StateWorkingSet,
1133 ) -> CompletionSite<'a> {
1134 if absolute_position <= call.head.end {
1136 return CompletionSite::new(
1137 SiteKind::command(expression),
1138 command_name_span(call.head, expression.span),
1139 );
1140 }
1141
1142 if let Some((argument_index, argument)) = call
1144 .arguments
1145 .iter()
1146 .enumerate()
1147 .find(|(_, argument)| touches(argument.span(), absolute_position))
1148 {
1149 return self.resolve_argument_site(
1150 call,
1151 expression,
1152 argument,
1153 argument_index,
1154 absolute_position,
1155 working_set,
1156 );
1157 }
1158
1159 if let Some(operator_left_hand_side) =
1161 self.row_condition_operator_lhs(call, working_set, absolute_position)
1162 {
1163 return CompletionSite::new(
1164 SiteKind::Operator {
1165 lhs: operator_left_hand_side,
1166 },
1167 Span::point(absolute_position),
1168 );
1169 }
1170
1171 let gap_start = call
1176 .arguments
1177 .last()
1178 .map_or(call.head.end, |argument| argument.span().end);
1179
1180 let gap = working_set.get_span_contents(Span::new(gap_start, absolute_position));
1181
1182 let token_start = gap
1184 .iter()
1185 .rposition(u8::is_ascii_whitespace)
1186 .map_or(gap_start, |index| gap_start + index + 1);
1187
1188 let trailing_token = Span::new(token_start, absolute_position);
1189 let token_is_flag = is_flag_token(working_set, trailing_token);
1190
1191 let point = Span::point(absolute_position);
1192
1193 if let Some(flag_ref) = self.pending_flag_value(call, working_set) {
1194 CompletionSite::new(
1197 SiteKind::FlagValue {
1198 call,
1199 element: expression,
1200 flag: flag_ref,
1201 arg_slot: call.arguments.len(),
1202 },
1203 point,
1204 )
1205 } else if token_is_flag {
1206 CompletionSite::new(
1207 SiteKind::FlagName {
1208 call,
1209 element: expression,
1210 },
1211 trailing_token,
1212 )
1213 } else {
1214 CompletionSite::new(
1215 SiteKind::Positional {
1216 call,
1217 element: expression,
1218 sig_positional: count_positionals(call, call.arguments.len()),
1219 arg_slot: call.arguments.len(),
1220 },
1221 point,
1222 )
1223 }
1224 }
1225
1226 fn row_condition_operator_lhs<'a>(
1229 &self,
1230 call: &'a Call,
1231 working_set: &'a StateWorkingSet,
1232 absolute_position: usize,
1233 ) -> Option<&'a Expression> {
1234 let block_id = call
1235 .arguments
1236 .iter()
1237 .rev()
1238 .find_map(|argument| match argument {
1239 Argument::Positional(Expression {
1240 expr: Expr::RowCondition(block_id),
1241 ..
1242 }) => Some(*block_id),
1243 _ => None,
1244 })?;
1245
1246 let last_term = &working_set
1247 .get_block(block_id)
1248 .pipelines
1249 .last()?
1250 .elements
1251 .last()?
1252 .expr;
1253
1254 if absolute_position <= last_term.span.end || !is_operator_lhs(&last_term.expr) {
1255 return None;
1256 }
1257
1258 let gap = working_set.get_span_contents(Span::new(last_term.span.end, absolute_position));
1259 gap.iter().all(u8::is_ascii_whitespace).then_some(last_term)
1260 }
1261
1262 fn pending_flag_value<'a>(
1264 &self,
1265 call: &'a Call,
1266 working_set: &StateWorkingSet,
1267 ) -> Option<FlagRef<'a>> {
1268 let Argument::Named((name, short, None)) = call.arguments.last()? else {
1269 return None;
1270 };
1271
1272 let flag_ref = FlagRef::from_named(name, short.as_ref());
1273 let signature = working_set.get_decl(call.decl_id).signature();
1274
1275 find_flag(&signature, flag_ref)?
1276 .arg
1277 .is_some()
1278 .then_some(flag_ref)
1279 }
1280
1281 fn resolve_argument_site<'a>(
1282 &self,
1283 call: &'a Call,
1284 expression: &'a Expression,
1285 argument: &'a Argument,
1286 argument_index: usize,
1287 absolute_position: usize,
1288 working_set: &StateWorkingSet,
1289 ) -> CompletionSite<'a> {
1290 let flag_name = SiteKind::FlagName {
1291 call,
1292 element: expression,
1293 };
1294
1295 let (kind, span) = match argument {
1296 Argument::Named((name, short, optional_value)) => {
1297 if let Some(value_expression) = optional_value
1298 .as_ref()
1299 .filter(|value| touches(value.span, absolute_position))
1300 {
1301 (
1302 SiteKind::FlagValue {
1303 call,
1304 element: expression,
1305 flag: FlagRef::from_named(name, short.as_ref()),
1306 arg_slot: argument_index,
1307 },
1308 value_expression.span,
1309 )
1310 } else {
1311 (flag_name, name.span)
1315 }
1316 }
1317 Argument::Positional(_) | Argument::Unknown(_) => {
1319 let kind = if is_flag_token(working_set, argument.span()) {
1320 flag_name
1321 } else {
1322 SiteKind::Positional {
1323 call,
1324 element: expression,
1325 sig_positional: count_positionals(call, argument_index),
1326 arg_slot: argument_index,
1327 }
1328 };
1329 (kind, argument.span())
1330 }
1331 Argument::Spread(_) => (SiteKind::File, argument.span()),
1332 };
1333
1334 CompletionSite::new(kind, span)
1335 }
1336
1337 fn resolve_attribute_site<'a>(
1338 &self,
1339 attribute_block: &'a AttributeBlock,
1340 absolute_position: usize,
1341 ) -> CompletionSite<'a> {
1342 if let Some(attribute) = attribute_block
1343 .attributes
1344 .iter()
1345 .find(|attribute| touches(attribute.expr.span, absolute_position))
1346 {
1347 return CompletionSite::new(SiteKind::AttributeName, attribute.expr.span);
1348 }
1349
1350 if touches(attribute_block.item.span, absolute_position) {
1351 return CompletionSite::new(SiteKind::AttributableItem, attribute_block.item.span);
1352 }
1353
1354 let kind = match attribute_block.attributes.last() {
1358 Some(last) if absolute_position >= last.expr.span.end => SiteKind::AttributableItem,
1359 _ => SiteKind::AttributeName,
1360 };
1361
1362 CompletionSite::new(kind, Span::point(absolute_position))
1363 }
1364
1365 fn resolve_fallback_site<'a>(
1366 &self,
1367 block: &'a Block,
1368 working_set: &'a StateWorkingSet,
1369 absolute_position: usize,
1370 ) -> CompletionSite<'a> {
1371 let last_element = block
1372 .pipelines
1373 .last()
1374 .and_then(|pipeline| pipeline.elements.last())
1375 .map(|element| &element.expr);
1376
1377 let kind = if last_element
1380 .map(|element| working_set.get_span_contents(element.span))
1381 .is_some_and(|bytes| bytes.ends_with(b"@"))
1382 {
1383 SiteKind::AttributeName
1384 } else if matches!(last_element.map(|e| &e.expr), Some(Expr::AttributeBlock(_))) {
1385 SiteKind::AttributableItem
1386 } else {
1387 SiteKind::Command { node: None }
1388 };
1389
1390 CompletionSite::new(kind, Span::point(absolute_position))
1391 }
1392 fn complete_argument_value(
1393 &self,
1394 custom: Option<Completion>,
1395 mut arg_value: ArgValueCompletion,
1396 context: &Context,
1397 signature: &Signature,
1398 element_expression: &Expression,
1399 cursor: usize,
1400 ) -> Dispatched {
1401 let mut results = Dispatched::default();
1402
1403 if let Some(custom) = custom {
1404 let attempt = match custom {
1405 Completion::Builtin(kind) => self.complete_builtin(kind, &arg_value, context),
1407 other => {
1410 let element_line = String::from_utf8_lossy(
1411 context
1412 .working_set
1413 .get_span_contents(Span::new(element_expression.span.start, cursor)),
1414 );
1415 self.custom_completion_helper(other, element_line.as_ref(), context, cursor)
1416 }
1417 };
1418 let need_fallback = attempt.need_fallback;
1419 results.merge(attempt.into());
1420 if !need_fallback {
1421 return results;
1422 }
1423 }
1424
1425 let attempt = self.command_wide_completion_helper(signature, element_expression, context);
1426 let need_fallback = attempt.need_fallback;
1427 results.merge(attempt.into());
1428 if !need_fallback {
1429 return results;
1430 }
1431
1432 arg_value.need_fallback &= results.suggestions.is_empty();
1433 results.merge(arg_value.fetch(context).into());
1434 results
1435 }
1436
1437 fn complete_builtin(
1439 &self,
1440 kind: BuiltinCompletion,
1441 arg_value: &ArgValueCompletion,
1442 context: &Context,
1443 ) -> Fetched {
1444 match kind {
1445 BuiltinCompletion::NuFile { std_virtual_path } => {
1446 DotNuCompletion { std_virtual_path }.fetch(context)
1447 }
1448 BuiltinCompletion::ModuleExports => {
1449 arg_value.complete_module_exports(context, context.working_set)
1450 }
1451 BuiltinCompletion::EnvVar => EnvVarCompletion.fetch(context),
1452 BuiltinCompletion::Command { internal_only } => {
1453 let scope = if internal_only {
1454 CommandScope::InternalsOnly
1455 } else {
1456 CommandScope::All
1457 };
1458 CommandCompletion::quoted(scope).fetch(context)
1459 }
1460 }
1461 }
1462
1463 fn complete_flag_names(
1464 &self,
1465 decl_id: DeclId,
1466 context: &Context,
1467 signature: &Signature,
1468 element_expression: &Expression,
1469 ) -> Dispatched {
1470 let mut results: Dispatched = FlagCompletion { decl_id }.fetch(context).into();
1471 results.merge(
1472 self.command_wide_completion_helper(signature, element_expression, context)
1473 .into(),
1474 );
1475 results
1476 }
1477
1478 fn suggestions_at<C: Completer>(
1479 &self,
1480 completer: &mut C,
1481 working_set: &StateWorkingSet,
1482 span: Span,
1483 offset: usize,
1484 ) -> Dispatched {
1485 completer
1486 .fetch(&self.context(
1487 working_set,
1488 span,
1489 working_set.get_span_contents(span),
1490 offset,
1491 ))
1492 .into()
1493 }
1494
1495 fn variable_names_completion_helper(
1496 &self,
1497 working_set: &StateWorkingSet,
1498 span: Span,
1499 offset: usize,
1500 ) -> Dispatched {
1501 let prefix = working_set.get_span_contents(span);
1502 if !prefix.starts_with(b"$") {
1503 return Dispatched::default();
1504 }
1505 let ctx = self.context(working_set, span, prefix, offset);
1506 VariableCompletion.fetch(&ctx).into()
1507 }
1508
1509 fn command_completion_helper(
1510 &self,
1511 working_set: &StateWorkingSet,
1512 span: Span,
1513 offset: usize,
1514 mut command_completion: CommandCompletion,
1515 ) -> Dispatched {
1516 let prefix = working_set.get_span_contents(span);
1517 let ctx = self.context(working_set, span, prefix, offset);
1518 command_completion.fetch(&ctx).into()
1519 }
1520
1521 fn command_completion_for_head(
1525 &self,
1526 node: Option<&Expression>,
1527 span: Span,
1528 working_set: &StateWorkingSet,
1529 ) -> CommandCompletion {
1530 let sigil = node
1531 .filter(|node| node.span.start < span.start)
1532 .and_then(|node| working_set.get_span_contents(node.span).first().copied());
1533
1534 CommandCompletion::new(match sigil {
1535 Some(b'^') => CommandScope::ExternalsOnly,
1536 Some(b'%') => CommandScope::BuiltinsOnly,
1537 _ => CommandScope::All,
1538 })
1539 }
1540
1541 fn subcommand_suggestions(
1545 &self,
1546 working_set: &StateWorkingSet,
1547 command_start: usize,
1548 cursor: usize,
1549 offset: usize,
1550 ) -> Dispatched {
1551 if cursor <= command_start {
1552 return Dispatched::default();
1553 }
1554 self.command_completion_helper(
1555 working_set,
1556 Span::new(command_start, cursor),
1557 offset,
1558 CommandCompletion::new(CommandScope::InternalsOnly),
1559 )
1560 }
1561
1562 fn custom_completion_helper(
1563 &self,
1564 custom_completion: Completion,
1565 input: &str,
1566 context: &Context,
1567 pos: usize,
1568 ) -> Fetched {
1569 match custom_completion {
1570 Completion::Command(decl_id) => {
1571 let mut completer =
1572 CustomCompletion::new(decl_id, input.into(), pos - context.offset);
1573 completer.fetch(context)
1574 }
1575 Completion::List(list) => {
1576 let mut completer = StaticCompletion::new(list);
1577 completer.fetch(context)
1578 }
1579 Completion::Builtin(_) => Fetched::absent(),
1582 }
1583 }
1584
1585 fn command_wide_completion_helper(
1586 &self,
1587 signature: &Signature,
1588 element_expression: &Expression,
1589 context: &Context,
1590 ) -> Fetched {
1591 let completion = match signature.complete {
1592 Some(CommandWideCompleter::Command(decl_id)) => {
1593 CommandWideCompletion::command(context.working_set, decl_id, element_expression)
1594 }
1595 Some(CommandWideCompleter::External) => self
1596 .engine_state
1597 .get_config()
1598 .completions
1599 .external
1600 .completer
1601 .as_ref()
1602 .map(|closure| CommandWideCompletion::closure(closure, element_expression)),
1603 None => None,
1604 };
1605
1606 match completion {
1607 Some(mut completion) => {
1608 let context = Context {
1609 prefix: b"",
1610 ..*context
1611 };
1612 completion.fetch(&context)
1613 }
1614 None => Fetched::absent(),
1615 }
1616 }
1617
1618 pub(crate) fn context<'a>(
1619 &'a self,
1620 working_set: &'a StateWorkingSet,
1621 span: Span,
1622 prefix: &'a [u8],
1623 offset: usize,
1624 ) -> Context<'a> {
1625 Context {
1626 working_set,
1627 stack: self.stack.as_ref(),
1628 options: &self.options,
1629 span,
1630 prefix,
1631 offset,
1632 }
1633 }
1634
1635 pub(crate) fn options(&self) -> &CompletionOptions {
1636 &self.options
1637 }
1638}
1639
1640pub struct NuCompleter {
1641 engine: CompletionEngine,
1642 cache: NarrowingCache,
1643 cache_env: CacheEnv,
1646 worker: Option<CompletionWorker>,
1647}
1648
1649impl NuCompleter {
1650 pub fn new(engine_state: Arc<EngineState>, stack: Arc<Stack>) -> Self {
1651 Self::with_cache(engine_state, stack, NarrowingCache::default())
1652 }
1653
1654 pub(crate) fn with_cache(
1655 engine_state: Arc<EngineState>,
1656 stack: Arc<Stack>,
1657 cache: NarrowingCache,
1658 ) -> Self {
1659 let engine = CompletionEngine::new(engine_state, stack);
1660 let cache_env = CacheEnv::of(&engine.engine_state, &engine.stack);
1661 let cache_size = engine.engine_state.get_config().completions.cache_size;
1663 cache.set_capacity(cache_size.try_into().unwrap_or(0));
1664 Self {
1665 engine,
1666 cache,
1667 cache_env,
1668 worker: None,
1669 }
1670 }
1671
1672 fn fresh_for(&self, query: &CompletionQuery) -> Option<Suggestions> {
1673 if let Some(worker) = self.worker.as_ref()
1674 && let Some(latest) = &worker.latest
1675 && &latest.query == query
1676 {
1677 return Some(latest.suggestions.clone());
1678 }
1679 self.cache.fresh(query, self.cache_env)
1680 }
1681
1682 fn settle_pending(&mut self, query: &CompletionQuery) {
1683 if let Some(worker) = self.worker.as_mut()
1684 && worker.pending.as_ref() == Some(query)
1685 {
1686 worker.pending = None;
1687 }
1688 }
1689
1690 fn stale_fallback(&self, query: &CompletionQuery) -> Suggestions {
1691 self.cache
1692 .narrowed_fallback(query, self.cache_env, self.engine.options())
1693 }
1694
1695 fn spawn_worker(engine: &CompletionEngine) -> CompletionWorker {
1696 let (request_tx, request_rx) = mpsc::channel::<CompletionQuery>();
1697 let (result_tx, result_rx) = mpsc::channel::<Completed>();
1698
1699 let engine = engine.to_background();
1700 thread::spawn(move || {
1701 while let Ok(mut query) = request_rx.recv() {
1702 while let Ok(newer) = request_rx.try_recv() {
1703 query = newer;
1704 }
1705
1706 let (suggestions, cacheable) = engine.suggestions_for(&query);
1707 let done = Completed {
1708 query,
1709 suggestions,
1710 cacheable,
1711 };
1712 if result_tx.send(done).is_err() {
1713 return;
1714 }
1715 }
1716 });
1717
1718 CompletionWorker {
1719 request_tx,
1720 result_rx,
1721 pending: None,
1722 latest: None,
1723 }
1724 }
1725
1726 fn fold_completed(&mut self, done: Completed) -> bool {
1727 let Self {
1728 cache,
1729 cache_env,
1730 worker,
1731 ..
1732 } = self;
1733 let Some(worker) = worker.as_mut() else {
1734 return false;
1735 };
1736 let settled = worker.pending.as_ref() == Some(&done.query);
1737 if done.cacheable {
1738 cache.store(done.query.clone(), *cache_env, done.suggestions.clone());
1739 }
1740 worker.latest = Some(done);
1741 settled
1742 }
1743
1744 fn try_recv_completed(&self) -> Option<Completed> {
1745 self.worker.as_ref()?.result_rx.try_recv().ok()
1746 }
1747
1748 fn recv_completed(&self, timeout: Duration) -> Option<Completed> {
1749 self.worker.as_ref()?.result_rx.recv_timeout(timeout).ok()
1750 }
1751
1752 fn drain_completed(&mut self) -> bool {
1753 let mut settled = false;
1754 while let Some(done) = self.try_recv_completed() {
1755 settled |= self.fold_completed(done);
1756 }
1757 settled
1758 }
1759
1760 pub fn complete_blocking(&mut self, line: &str, pos: usize) -> Suggestions {
1761 const BLOCKING_TIMEOUT: Duration = Duration::from_secs(30);
1762
1763 let fallback = match self.complete(line, pos) {
1764 CompletionResult::Fresh { suggestions, .. } => return suggestions,
1765 in_flight => in_flight.into_shared().unwrap_or_default(),
1766 };
1767
1768 let deadline = Instant::now() + BLOCKING_TIMEOUT;
1769 while let Some(remaining) = deadline.checked_duration_since(Instant::now()) {
1770 let Some(done) = self.recv_completed(remaining) else {
1771 break;
1772 };
1773 if self.fold_completed(done) {
1774 return self.complete(line, pos).into_shared().unwrap_or_default();
1775 }
1776 }
1777
1778 fallback
1779 }
1780}
1781
1782fn common_prefix_len(a: &str, b: &str) -> usize {
1784 a.char_indices()
1785 .zip(b.chars())
1786 .find_map(|((index, x), y)| (x != y).then_some(index))
1787 .unwrap_or_else(|| a.len().min(b.len()))
1788}
1789
1790fn partial_of(line: &str, suggestions: &[Suggestion]) -> Option<Partial> {
1791 let span = suggestions.first()?.span;
1792
1793 let mut matching_values = suggestions
1794 .iter()
1795 .filter(|suggestion| suggestion.span == span)
1796 .map(|suggestion| suggestion.value.as_str());
1797
1798 let first = matching_values.next()?;
1801 let shared_len = matching_values.try_fold(first.len(), |shared, value| {
1802 let common = common_prefix_len(first.get(..shared)?, value);
1803 (common > 0).then_some(common)
1804 })?;
1805 let shared_prefix = first.get(..shared_len)?;
1806
1807 let entered = line.get(span.start..span.end)?;
1808 let extends = shared_prefix != entered
1809 && shared_prefix
1810 .to_lowercase()
1811 .starts_with(&entered.to_lowercase());
1812
1813 extends.then_some(Partial {
1814 span,
1815 insert: shared_prefix.to_string(),
1816 })
1817}
1818
1819impl ReedlineCompleter for NuCompleter {
1820 fn complete(&mut self, line: &str, pos: usize) -> CompletionResult {
1821 let query = CompletionQuery::new(line, pos);
1822 self.drain_completed();
1823
1824 if let Some(suggestions) = self.fresh_for(&query) {
1825 self.settle_pending(&query);
1826 let partial = partial_of(line, &suggestions);
1827 return CompletionResult::fresh(suggestions).with_partial(partial);
1828 }
1829
1830 let fallback = self.stale_fallback(&query);
1831 let partial = partial_of(line, &fallback);
1832
1833 let worker = self
1834 .worker
1835 .get_or_insert_with(|| Self::spawn_worker(&self.engine));
1836
1837 if worker.pending.as_ref() != Some(&query) {
1838 if worker.request_tx.send(query.clone()).is_ok() {
1839 worker.pending = Some(query);
1840 } else {
1841 self.worker = None;
1844 }
1845 }
1846
1847 CompletionResult::stale_or_pending(fallback, CompletionOrigin::new(line, pos))
1848 .with_partial(partial)
1849 }
1850
1851 fn poll_completion(&mut self) -> CompletionStatus {
1852 let settled = self.drain_completed();
1853
1854 match self.worker.as_mut() {
1855 Some(worker) if worker.pending.is_some() => {
1856 if settled {
1857 worker.pending = None;
1858 CompletionStatus::Ready
1859 } else {
1860 CompletionStatus::Pending
1861 }
1862 }
1863 _ => CompletionStatus::Idle,
1864 }
1865 }
1866}
1867
1868#[cfg(test)]
1869mod completer_tests {
1870 use super::*;
1871
1872 fn test_engine() -> Arc<EngineState> {
1873 let mut engine =
1874 nu_command::add_shell_command_context(nu_cmd_lang::create_default_context());
1875 let delta = StateWorkingSet::new(&engine).render();
1876 engine.merge_delta(delta).expect("merge_delta");
1877 Arc::new(engine)
1878 }
1879
1880 fn q(s: &str) -> CompletionQuery {
1881 CompletionQuery::new(s, s.len())
1882 }
1883
1884 fn token(start: usize) -> reedline::Span {
1886 reedline::Span::new(start, start)
1887 }
1888
1889 #[test]
1890 fn narrows_stays_within_one_token() {
1891 assert!(q("ls foobar").narrows(&q("ls foo"), token(3)));
1892
1893 assert!(!q("ls foo").narrows(&q("ls foo"), token(3)));
1895 assert!(!q("ls fo").narrows(&q("ls foo"), token(3)));
1896
1897 for narrowed in [
1899 "ls foo|from",
1900 "ls foo;ls",
1901 "ls foo/bar",
1902 "ls foo=1",
1903 "ls foo,2",
1904 ] {
1905 assert!(
1906 !q(narrowed).narrows(&q("ls foo"), token(3)),
1907 "narrowed across a boundary: {narrowed:?}"
1908 );
1909 }
1910 }
1911
1912 #[test]
1913 fn narrows_rejects_a_token_that_becomes_a_flag() {
1914 let base = q("from csv ");
1918 assert!(!q("from csv --sep").narrows(&base, token(base.cursor())));
1919
1920 assert!(q("from csv --sep").narrows(&q("from csv --s"), token(9)));
1922 }
1923
1924 #[test]
1926 fn background_result_matches_the_synchronous_engine() {
1927 let engine = test_engine();
1928 let mut completer = NuCompleter::new(engine.clone(), Arc::new(Stack::new()));
1929
1930 let sorted = |mut values: Vec<String>| {
1931 values.sort();
1932 values
1933 };
1934 let expected = sorted(
1935 CompletionEngine::new(engine, Arc::new(Stack::new()))
1936 .fetch_completions_at("ls | c", 6)
1937 .into_iter()
1938 .map(|s| s.suggestion.value)
1939 .collect(),
1940 );
1941 assert!(expected.iter().any(|value| value == "cd"));
1942
1943 assert!(completer.complete("ls | c", 6).is_pending());
1945
1946 let settled = sorted(
1947 completer
1948 .complete_blocking("ls | c", 6)
1949 .iter()
1950 .map(|s| s.value.clone())
1951 .collect(),
1952 );
1953 assert_eq!(expected, settled);
1954 }
1955
1956 #[test]
1959 fn cache_outlives_the_completer_that_filled_it() {
1960 let engine = test_engine();
1961 let cache = NarrowingCache::default();
1962
1963 let mut filling_prompt =
1964 NuCompleter::with_cache(engine.clone(), Arc::new(Stack::new()), cache.clone());
1965 let warmed = filling_prompt.complete_blocking("ls | c", 6);
1966 assert!(warmed.iter().any(|s| s.value == "cd"));
1967 drop(filling_prompt);
1968
1969 let mut next_prompt = NuCompleter::with_cache(engine, Arc::new(Stack::new()), cache);
1970 let answer = next_prompt.complete("ls | c", 6);
1971 assert!(
1972 matches!(answer, CompletionResult::Fresh { .. }),
1973 "a carried-over cache entry should answer outright, got {answer:?}"
1974 );
1975 assert!(answer.suggestions().iter().any(|s| s.value == "cd"));
1976 }
1977
1978 #[test]
1980 fn cache_is_not_reused_in_a_different_environment() {
1981 use nu_protocol::Value;
1982
1983 let engine = test_engine();
1984 let cache = NarrowingCache::default();
1985
1986 let mut filling_prompt =
1987 NuCompleter::with_cache(engine.clone(), Arc::new(Stack::new()), cache.clone());
1988 assert!(!filling_prompt.complete_blocking("ls | c", 6).is_empty());
1989
1990 let mut moved = Stack::new();
1991 moved.add_env_var(
1992 "PATH".into(),
1993 Value::string("/somewhere/else", Span::unknown()),
1994 );
1995 let mut next_prompt = NuCompleter::with_cache(engine, Arc::new(moved), cache);
1996 assert!(
1997 next_prompt.complete("ls | c", 6).is_pending(),
1998 "entries from another environment must not answer"
1999 );
2000 }
2001
2002 #[test]
2004 fn cache_size_zero_disables_the_cache() {
2005 let mut engine = test_engine();
2006 {
2007 let state = Arc::make_mut(&mut engine);
2008 Arc::make_mut(&mut state.config).completions.cache_size = 0;
2009 }
2010 let cache = NarrowingCache::default();
2011
2012 let mut filling_prompt =
2013 NuCompleter::with_cache(engine.clone(), Arc::new(Stack::new()), cache.clone());
2014 assert!(!filling_prompt.complete_blocking("ls | c", 6).is_empty());
2015 drop(filling_prompt);
2016
2017 let mut next_prompt = NuCompleter::with_cache(engine, Arc::new(Stack::new()), cache);
2018 assert!(
2019 next_prompt.complete("ls | c", 6).is_pending(),
2020 "a disabled cache must not answer a query it could have answered"
2021 );
2022 }
2023
2024 #[test]
2027 fn a_narrowed_cache_answer_keeps_the_order_it_was_given() {
2028 let cache = NarrowingCache::default();
2029 let env = CacheEnv::of(&test_engine(), &Stack::new());
2030 let span = reedline::Span::new(3, 5);
2031
2032 let cached: Suggestions = ["config/", "config.nu"]
2034 .iter()
2035 .map(|value| Suggestion {
2036 value: (*value).to_string(),
2037 span,
2038 ..Default::default()
2039 })
2040 .collect::<Vec<_>>()
2041 .into();
2042
2043 cache.store(CompletionQuery::new("ls co", 5), env, cached);
2044
2045 let narrowed = cache.narrowed_fallback(
2046 &CompletionQuery::new("ls con", 6),
2047 env,
2048 &CompletionOptions::default(),
2049 );
2050
2051 let values: Vec<&str> = narrowed.iter().map(|s| s.value.as_str()).collect();
2052 assert_eq!(
2053 values,
2054 ["config/", "config.nu"],
2055 "the cached answer must not reorder what it stands in for"
2056 );
2057 }
2058}