Skip to main content

nu_cli/completions/
completer.rs

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
31/// Max cache entries before evicting the least recently used; overridden per completer by
32/// `$env.config.completions.cache_size` (`0` disables the cache).
33const DEFAULT_CACHE_SIZE: usize = 100;
34
35use super::{StaticCompletion, custom_completions::CommandWideCompletion};
36
37/// Used as the function `f` in find_map Traverse
38///
39/// returns the inner-most pipeline_element of interest that reaches the given position
40fn 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`, not `contains`: the cursor sits at the head's trailing
72                    // edge (issue #7648).
73                    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            // `use std/util [E, T⌶`: the import list is a `List` in a `FullCellPath`; leave it
84            // to the enclosing call, which knows the module, to complete its members.
85            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
102/// Whether `position` is inside `span` or exactly at its trailing edge.
103///
104/// Completion happens at a token's trailing edge, which the end-exclusive
105/// [`Span::contains`] would miss.
106pub(crate) fn touches(span: Span, position: usize) -> bool {
107    span.contains(position) || span.end == position
108}
109
110/// The last element when the cursor trails it over whitespace only (`ls ⌶`) — an empty
111/// new slot for that element. Non-whitespace gaps fall through to
112/// [`CompletionEngine::resolve_fallback_site`].
113fn 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
125/// The span a command-name completion replaces, given the parsed `head` and the whole
126/// `element` it heads.
127fn command_name_span(head: Span, element: Span) -> Span {
128    Span::new(head.start, head.end.max(element.end))
129}
130
131/// Whether `token` is a flag being typed — i.e. it begins with `-`.
132///
133/// The parser stores in-progress flags as positionals, so the leading dash is the only
134/// reliable flag/positional test; the cache relies on it too.
135fn is_flag_text(token: impl AsRef<[u8]>) -> bool {
136    token.as_ref().starts_with(b"-")
137}
138
139/// [`is_flag_text`] for the token occupying `span`.
140fn is_flag_token(working_set: &StateWorkingSet, span: Span) -> bool {
141    is_flag_text(working_set.get_span_contents(span))
142}
143
144/// Whether `expr` is a value an operator can trail (`1 ⌶`, `'str' ⌶`). Exhaustive, so a
145/// new [`Expr`] variant must be classified rather than defaulting.
146fn 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
190/// The flag a [`FlagRef`] refers to, preserving the long/short distinction.
191fn 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
198/// Non-named arguments before `before_index` — the positional index of that slot.
199fn 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
207/// Helper function to extract file-path expression from redirection target
208fn 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
221/// For redirection target completion
222fn 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/// Cache key and worker message identity: the text typed up to the cursor.
237///
238/// Excludes trailing text and derives `cursor()` from `typed.len()` so the two never
239/// disagree.
240#[derive(Debug, Clone, PartialEq, Eq, Hash)]
241pub(crate) struct CompletionQuery {
242    /// The prefix of the line buffer up to the (floored) cursor position.
243    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    /// Whether `self` is `base` with more characters typed into the same `token`. The
263    /// appended text must stay within one token and must not turn it into a flag — a
264    /// different completion site than the cached result came from.
265    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/// The environment a cached completion was computed against.
295///
296/// Results depend on cwd, `PATH`, and known declarations, which change between prompts
297/// while the query text does not — so the query alone is not a sound cache key.
298#[derive(Debug, Clone, Copy, PartialEq, Eq)]
299pub(crate) struct CacheEnv(u64);
300
301impl CacheEnv {
302    /// Fingerprint the completion-relevant parts of `engine_state`/`stack`, once per
303    /// completer.
304    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        // The cwd mtime, so adding/removing files invalidates stale file completions.
315        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    /// Whether this entry may still answer a query: produced in the same environment.
331    fn is_usable(&self, env: CacheEnv) -> bool {
332        self.env == env
333    }
334
335    /// The span the cursor extends: the range the *last* suggestion replaces.
336    ///
337    /// `fetch_completions_by_block` keeps the cursor-anchored family last, so reading the
338    /// last span is the correct one to extend.
339    fn reference_span(&self) -> Option<reedline::Span> {
340        self.suggestions.last().map(|suggestion| suggestion.span)
341    }
342}
343
344/// Cross-prompt completion cache bounded by entry count (`$env.config.completions.cache_size`),
345/// evicting least recently used entries. Capacity `0` disables the cache.
346#[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    /// `0` isn't a valid `LruCache` capacity; it means the cache is disabled.
359    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    /// Resizes the cache in place, dropping LRU entries when shrinking. Capacity `0`
366    /// disables it. Called once per prompt so `cache_size` config changes take effect.
367    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        // Don't re-sort: the producing completer ranks a directory by its bare name and
450        // appends the separator afterwards, so sorting here would rank it `config/` and
451        // land it after `config.nu`. Filtering alone preserves the order it chose.
452        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/// What the cursor is completing; each variant carries exactly the AST it needs.
503#[derive(Debug, Clone)]
504pub(crate) enum SiteKind<'a> {
505    /// A command head. `node` is the whole call expression, used to detect a `^`/`%` sigil.
506    Command { node: Option<&'a Expression> },
507    /// A flag name being typed (`--`, `-x`).
508    FlagName {
509        call: &'a Call,
510        element: &'a Expression,
511    },
512    /// The value of a flag (`--opt <tab>`). `flag` preserves long/short identity;
513    /// `arg_slot` indexes `call.arguments`.
514    FlagValue {
515        call: &'a Call,
516        element: &'a Expression,
517        flag: FlagRef<'a>,
518        arg_slot: usize,
519    },
520    /// A positional argument. `sig_positional` indexes the signature's positionals,
521    /// `arg_slot` indexes `call.arguments`.
522    Positional {
523        call: &'a Call,
524        element: &'a Expression,
525        sig_positional: usize,
526        arg_slot: usize,
527    },
528    /// A binary-operator position trailing `lhs`.
529    Operator { lhs: &'a Expression },
530    /// A cell path into `path`.
531    CellPath { path: &'a FullCellPath },
532    /// A `$var` name.
533    Variable,
534    /// An attribute name (`@<tab>`).
535    AttributeName,
536    /// The item an attribute block decorates (`def`, `extern`, …).
537    AttributableItem,
538    /// An argument of a bare external call; `index` is the argument slot.
539    ExternalArg { call: &'a Expression, index: usize },
540    /// A file path — the base/fallback completion.
541    File,
542}
543
544impl<'a> SiteKind<'a> {
545    /// A command head backed by an existing call expression (used for sigil detection).
546    fn command(node: &'a Expression) -> Self {
547        Self::Command { node: Some(node) }
548    }
549}
550
551/// A fully resolved completion site: the span to replace, the typed text, the cursor, and
552/// the [`SiteKind`].
553///
554/// `typed_prefix`/`cursor` are derived centrally in [`CompletionEngine::finalize_site`] so
555/// they can never disagree with the span.
556#[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    /// The cursor, in absolute working-set (span) coordinates.
562    pub cursor: usize,
563}
564
565impl<'a> CompletionSite<'a> {
566    /// A site with the given kind and span; `typed_prefix`/`cursor` are filled later by
567    /// [`CompletionEngine::finalize_site`].
568    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/// Engine dispatch output: suggestions plus whether an impure source ran (worth caching).
579#[derive(Default)]
580struct Dispatched {
581    suggestions: Vec<SemanticSuggestion>,
582    cacheable: bool,
583}
584
585impl Dispatched {
586    /// Append another dispatch's suggestions, propagating its cacheability.
587    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        // Parse only up to the cursor, so the last pipeline element is always the token (or
670        // gap) being edited; trailing whitespace is kept to distinguish a gap from the token.
671        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        // `parse` must run first: it registers the file and its spans in `working_set`.
701        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    /// `position` is the cursor as a buffer-relative byte offset into `contents`.
712    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        // A multi-word head is ambiguous: also recover the argument reading of the shorter
724        // command and offer it before the subcommand name.
725        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    /// A multi-word head is ambiguous: `baz --test bar` is also `bar`, the value of
735    /// `baz --test`'s flag. Recover that argument reading via [`parse_shorter_head_reading`]
736    /// over the real buffer spans (avoiding the stale-span hazard of #5127), dropping
737    /// command-kind results the primary dispatch already offers.
738    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        // A command-head result means no distinct argument; leave it to the primary dispatch.
768        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        // Drop command-kind results; only the argument value is contributed here.
774        dispatched
775            .suggestions
776            .retain(|candidate| !matches!(candidate.kind, Some(SuggestionKind::Command(..))));
777        dispatched
778    }
779
780    /// Dispatches the completion site to the appropriate specialized completer.
781    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    /// Complete an external call argument: `sudo`/`doas` special-case, the configured
844    /// external completer, then file completion as a fallback.
845    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        // The first argument of `sudo`/`doas` is a command run under the wrapper.
865        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        // The user's configured external completer (`$env.config.completions.external.completer`).
884        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        // Internal subcommands extending this call (e.g. `fod br` → `food bar`), which
899        // suppress the file fallback like an internal call's arguments do.
900        let subcommands =
901            self.subcommand_suggestions(working_set, external_call.span.start, site.cursor, offset);
902
903        // File completion for path arguments, only when nothing more specific answered.
904        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    /// Dispatch completions for call-bound sites (FlagName, FlagValue, Positional).
921    fn dispatch_call_completion_site(
922        &self,
923        site: &CompletionSite,
924        working_set: &StateWorkingSet,
925        offset: usize,
926        completion_context: &Context,
927    ) -> Dispatched {
928        // Only call-bound kinds carry a call and element; anything else is an error here.
929        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        // Subcommands extending this command line are always offered, and suppress the
939        // file-path fallback: a matched subcommand shouldn't also dump the whole directory.
940        let subcommands =
941            self.subcommand_suggestions(working_set, call.head.start, site.cursor, offset);
942
943        // The value kinds share one shape; only the `ArgType` and custom-completer lookup differ.
944        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    /// Resolves the contextual state and constraints at the cursor's location.
992    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        // The token whose span the cursor is inside of, or at the trailing edge of.
1003        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            // Otherwise the cursor is in a whitespace gap after the element it trails.
1009            .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    /// Fill the centrally-derived `typed_prefix`/`cursor` fields from the final `site.span`,
1022    /// so the prefix and replacement span can never disagree. Point spans yield an empty
1023    /// prefix.
1024    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        // Cursor in whitespace after a completed value (`1 ⌶`) is an operator position.
1047        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        // Base case: file completion; overridden below where the expression warrants it.
1055        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            // `$foo` alone is the variable; `$foo.bar` or `$foo.` is a cell path.
1067            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), // The default `File` setup holds
1089        }
1090    }
1091
1092    /// Resolve a bare external call (`git checkout`). The head completes as a command;
1093    /// other positions are [`SiteKind::ExternalArg`].
1094    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        // An existing argument the cursor touches, or else the trailing empty slot.
1109        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        // Cursor in (or right after) the command head: complete the command name.
1135        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        // Cursor on an existing argument.
1143        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        // A trailing gap after a row condition (`where name ⌶`) is an operator position.
1160        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        // Classify the slot the cursor trails after the last argument: a pending flag value,
1172        // a new flag name, or a new positional. Looking only at the non-whitespace token
1173        // ending at the cursor keeps `cmd -f val ⌶` (positional) and `cmd --⌶` (flag name)
1174        // distinct, and its span preserves the `-`/`--` prefix.
1175        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        // Start just past the last whitespace in the gap.
1183        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            // Intentionally out-of-range `arg_slot`: there is no in-progress argument node
1195            // yet, and `ArgValueCompletion` reads `None` as exactly that.
1196            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    /// The last row-condition term when the cursor trails it (`where name ⌶`): the LHS of
1227    /// an operator the user is about to type.
1228    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    /// The [`FlagRef`] of a last-argument flag still awaiting its value (`cmd --opt ⌶`).
1263    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                    // Only the name is being completed: `Argument::span` would also cover
1312                    // the value written after it (`--endian big`), and `name.span` is the
1313                    // flag token itself for both spellings.
1314                    (flag_name, name.span)
1315                }
1316            }
1317            // A positional/unknown token starting with `-` is a flag name being typed.
1318            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        // Past the last attribute is the decorated item's slot, even when the parser found
1355        // no item to give a span to (`@complete "c"⏎⌶`). Earlier gaps sit between two
1356        // attributes, where another attribute name is what's being typed.
1357        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        // A bare `@` opens an attribute name; trailing a completed attribute block completes
1378        // the attributable item itself; otherwise a fresh command position.
1379        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                // A command declared an engine-provided completion for this argument.
1406                Completion::Builtin(kind) => self.complete_builtin(kind, &arg_value, context),
1407                // A custom completer receives the element text up to the cursor
1408                // (`my-command foobar`), so its spans are anchored to the element's start.
1409                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    /// Dispatch a [`BuiltinCompletion`] a command declared for its argument.
1438    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    /// Command-completion scope for a command head, honouring a leading sigil: `^` →
1522    /// externals only, `%` → built-ins only, otherwise everything. The sigil is the byte
1523    /// between the call's own span and its head span.
1524    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    /// Internal commands whose name extends the command line typed so far (`foo test⌶`
1542    /// also offers `foo test bar`). Externals are excluded: a multi-word line names only
1543    /// internal subcommands.
1544    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            // Engine-provided completions are handled in `complete_argument_value`; decline
1580            // if one arrives by another path.
1581            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    /// The [`CacheEnv`] of every entry this completer stores/reads; computed once per
1644    /// completer, not on [`CompletionEngine`] (which non-caching callers also build).
1645    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        // Read fresh each prompt so `cache_size` config changes take effect.
1662        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
1782/// Byte length of the longest prefix `a` and `b` share. Always a char boundary in both.
1783fn 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    // Narrow a window into the first value rather than allocating a `String`; runs every
1799    // keystroke.
1800    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                // Worker died (a panic in a user completer closure kills it); drop it so the
1842                // next request spawns a replacement.
1843                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    /// The token being extended starts at `start`; suggestions replace from there.
1885    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        // Not narrowing: no new text, or text removed.
1894        assert!(!q("ls foo").narrows(&q("ls foo"), token(3)));
1895        assert!(!q("ls fo").narrows(&q("ls foo"), token(3)));
1896
1897        // Each boundary character starts a new token, which a cached entry cannot answer.
1898        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        // The empty positional slot after `from csv ` is answered with file names at a point
1915        // span; typing `--sep` appends no boundary, so only the flag check keeps them from
1916        // following it.
1917        let base = q("from csv ");
1918        assert!(!q("from csv --sep").narrows(&base, token(base.cursor())));
1919
1920        // Extending a flag the user was already typing stays sound.
1921        assert!(q("from csv --sep").narrows(&q("from csv --s"), token(9)));
1922    }
1923
1924    /// The worker runs on an isolated stack and must still produce identical results.
1925    #[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        // Nothing is cached yet, so the first non-blocking call can only be pending.
1944        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    /// A cache handed to a new per-prompt completer must still answer the previous prompt's
1957    /// queries.
1958    #[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    /// …but not across a `cd`/`$env.PATH` change — the reason [`CacheEnv`] exists.
1979    #[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    /// `cache_size = 0` must disable the cache entirely, even a carried-over one.
2003    #[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    /// A cached answer stands in for the computed one, so the two must agree on order.
2025    /// Re-sorting the cache put `config/` after `config.nu`, inverting every keystroke.
2026    #[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        // The order file completion produces: the directory first, ranked as `config`.
2033        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}