Skip to main content

nu_cli/completions/
base.rs

1use crate::completions::completer::Context;
2use nu_color_config::NuStyle;
3use nu_protocol::{DynamicSuggestion, IntoValue, Record, Span, SuggestionKind, Value};
4use reedline::Suggestion;
5
6pub trait Completer {
7    /// Fetch, filter, and sort completions for the token described by `ctx`.
8    fn fetch(&mut self, ctx: &Context) -> Fetched;
9}
10
11/// The outcome of one source's [`Completer::fetch`], plus two flags the machinery cannot
12/// infer from the suggestions: `cacheable` (impure sources worth reusing between keystrokes)
13/// and `need_fallback` (try the next source). Fields are private, so a declining result can
14/// never also carry suggestions.
15#[derive(Debug, Default)]
16pub struct Fetched {
17    pub(crate) suggestions: Vec<SemanticSuggestion>,
18    pub(crate) cacheable: bool,
19    pub(crate) need_fallback: bool,
20}
21
22impl Fetched {
23    /// A cheap engine-state result: never cached, never falls back.
24    pub(crate) fn pure(suggestions: Vec<SemanticSuggestion>) -> Self {
25        Self {
26            suggestions,
27            cacheable: false,
28            need_fallback: false,
29        }
30    }
31
32    /// An impure source's result (filesystem, `PATH`, user/plugin code); worth caching.
33    pub(crate) fn cacheable(suggestions: Vec<SemanticSuggestion>) -> Self {
34        Self {
35            suggestions,
36            cacheable: true,
37            need_fallback: false,
38        }
39    }
40
41    /// An impure source that declined: fall back, but stay `cacheable` since the
42    /// expensive attempt ran.
43    pub(crate) fn fallback() -> Self {
44        Self {
45            suggestions: vec![],
46            cacheable: true,
47            need_fallback: true,
48        }
49    }
50
51    /// Like [`Self::fallback`] but cheap — no source ran, so nothing to cache.
52    pub(crate) fn absent() -> Self {
53        Self {
54            suggestions: vec![],
55            cacheable: false,
56            need_fallback: true,
57        }
58    }
59
60    /// Force [`Self::cacheable`] on when the caller did expensive work (e.g. parsing a
61    /// module off disk) around a cheap lookup.
62    pub(crate) fn caching(mut self) -> Self {
63        self.cacheable = true;
64        self
65    }
66}
67
68/// Convert an engine [`Span`] to reedline coordinates by subtracting the working-set
69/// `offset`. Both ends saturate so spans before `offset` can't underflow into an index that
70/// would panic (`is_char_boundary`); callers may pass untrusted spans.
71pub(crate) fn to_reedline_span(span: Span, offset: usize) -> reedline::Span {
72    reedline::Span::new(
73        span.start.saturating_sub(offset),
74        span.end.saturating_sub(offset),
75    )
76}
77
78#[derive(Debug, Default, PartialEq)]
79pub struct SemanticSuggestion {
80    pub suggestion: Suggestion,
81    pub kind: Option<SuggestionKind>,
82}
83
84impl SemanticSuggestion {
85    pub fn from_dynamic_suggestion(
86        suggestion: DynamicSuggestion,
87        span: reedline::Span,
88        style: Option<nu_ansi_term::Style>,
89    ) -> Self {
90        SemanticSuggestion {
91            suggestion: Suggestion {
92                value: suggestion.value,
93                display_override: suggestion.display_override,
94                description: suggestion.description,
95                extra: suggestion.extra,
96                append_whitespace: suggestion.append_whitespace,
97                match_indices: suggestion.match_indices,
98                style,
99                span,
100            },
101            kind: suggestion.kind,
102        }
103    }
104}
105
106impl IntoValue for SemanticSuggestion {
107    fn into_value(self, span: Span) -> Value {
108        let mut record = Record::new();
109        record.insert("value", Value::string(self.suggestion.value, span));
110
111        if let Some(span_rec) = span_record(self.suggestion.span, span) {
112            record.insert("span", span_rec);
113        }
114
115        if let Some(display) = self.suggestion.display_override {
116            record.insert("display_override", Value::string(display, span));
117        }
118
119        if let Some(style) = self.suggestion.style.map(NuStyle::from) {
120            record.insert("style", style.into_value(span));
121        }
122
123        if let Some(description) = self.suggestion.description {
124            record.insert("description", description.into_value(span));
125        }
126
127        if let Some(kind) = self.kind {
128            let (kind_str, ty) = match kind {
129                SuggestionKind::Command(ty, _) => ("command", Some(ty.to_string())),
130                SuggestionKind::Value(ty) => ("value", Some(ty.to_string())),
131                SuggestionKind::CellPath => ("cell-path", None),
132                SuggestionKind::Directory => ("directory", None),
133                SuggestionKind::File => ("file", None),
134                SuggestionKind::Flag => ("flag", None),
135                SuggestionKind::Module => ("module", None),
136                SuggestionKind::Operator => ("operator", None),
137                SuggestionKind::Variable => ("variable", None),
138            };
139            record.insert("kind", kind_str.into_value(span));
140
141            if let Some(ty) = ty {
142                record.insert("type", ty.into_value(span));
143            }
144        }
145
146        Value::record(record, span)
147    }
148}
149
150fn span_record(span: reedline::Span, src_span: Span) -> Option<Value> {
151    let (Ok(start), Ok(end)) = (span.start.try_into(), span.end.try_into()) else {
152        log::error!("failed to convert span to i64s");
153        return None;
154    };
155
156    Some(Value::record(
157        Record::from_iter([
158            ("start".into(), Value::int(start, src_span)),
159            ("end".into(), Value::int(end, src_span)),
160        ]),
161        src_span,
162    ))
163}
164
165impl From<Suggestion> for SemanticSuggestion {
166    fn from(suggestion: Suggestion) -> Self {
167        Self {
168            suggestion,
169            ..Default::default()
170        }
171    }
172}