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`]. Caching and fallback are encoded in
12/// the variant, so a declining result cannot carry suggestions.
13#[derive(Debug, Default)]
14pub enum Fetched {
15    /// Cheap engine-state result: never cached, never falls back.
16    Pure(Vec<SemanticSuggestion>),
17    /// Impure source result (filesystem, `PATH`, user/plugin code); worth caching.
18    Cacheable(Vec<SemanticSuggestion>),
19    /// Impure source declined: fall back, but cache the attempt.
20    Declined,
21    /// No source ran: fall back cheaply.
22    #[default]
23    Absent,
24}
25
26impl Fetched {
27    /// The suggestions this outcome carries; declining outcomes carry none.
28    pub(crate) fn into_suggestions(self) -> Vec<SemanticSuggestion> {
29        match self {
30            Self::Pure(suggestions) | Self::Cacheable(suggestions) => suggestions,
31            Self::Declined | Self::Absent => Vec::new(),
32        }
33    }
34
35    /// Impure source ran; result worth reusing between keystrokes.
36    pub(crate) fn is_cacheable(&self) -> bool {
37        matches!(self, Self::Cacheable(_) | Self::Declined)
38    }
39
40    /// Whether this source declined, so the next one should be tried.
41    pub(crate) fn needs_fallback(&self) -> bool {
42        matches!(self, Self::Declined | Self::Absent)
43    }
44
45    /// Mark cheap results cacheable when the caller did expensive work.
46    pub(crate) fn caching(self) -> Self {
47        match self {
48            Self::Pure(suggestions) => Self::Cacheable(suggestions),
49            Self::Absent => Self::Declined,
50            already => already,
51        }
52    }
53}
54
55/// An engine [`Span`] in reedline coordinates: subtract `offset`, saturating so spans
56/// before it can't underflow into an index that would panic (`is_char_boundary`); callers
57/// may pass untrusted spans.
58pub(crate) fn to_reedline_span(span: Span, offset: usize) -> reedline::Span {
59    reedline::Span::new(
60        span.start.saturating_sub(offset),
61        span.end.saturating_sub(offset),
62    )
63}
64
65#[derive(Debug, Default, PartialEq)]
66pub struct SemanticSuggestion {
67    pub suggestion: Suggestion,
68    pub kind: Option<SuggestionKind>,
69}
70
71impl SemanticSuggestion {
72    pub fn from_dynamic_suggestion(
73        suggestion: DynamicSuggestion,
74        span: reedline::Span,
75        style: Option<nu_ansi_term::Style>,
76    ) -> Self {
77        SemanticSuggestion {
78            suggestion: Suggestion {
79                value: suggestion.value,
80                display_override: suggestion.display_override,
81                description: suggestion.description,
82                extra: suggestion.extra,
83                append_whitespace: suggestion.append_whitespace,
84                match_indices: suggestion.match_indices,
85                style,
86                span,
87            },
88            kind: suggestion.kind,
89        }
90    }
91}
92
93impl IntoValue for SemanticSuggestion {
94    fn into_value(self, span: Span) -> Value {
95        let mut record = Record::new();
96        record.insert("value", Value::string(self.suggestion.value, span));
97
98        if let Some(span_rec) = span_record(self.suggestion.span, span) {
99            record.insert("span", span_rec);
100        }
101
102        if let Some(display) = self.suggestion.display_override {
103            record.insert("display_override", Value::string(display, span));
104        }
105
106        if let Some(style) = self.suggestion.style.map(NuStyle::from) {
107            record.insert("style", style.into_value(span));
108        }
109
110        if let Some(description) = self.suggestion.description {
111            record.insert("description", description.into_value(span));
112        }
113
114        if let Some(kind) = self.kind {
115            let (kind_str, ty) = match kind {
116                SuggestionKind::Command(ty, _) => ("command", Some(ty.to_string())),
117                SuggestionKind::Value(ty) => ("value", Some(ty.to_string())),
118                SuggestionKind::CellPath => ("cell-path", None),
119                SuggestionKind::Directory => ("directory", None),
120                SuggestionKind::File => ("file", None),
121                SuggestionKind::Flag => ("flag", None),
122                SuggestionKind::Module => ("module", None),
123                SuggestionKind::Operator => ("operator", None),
124                SuggestionKind::Variable => ("variable", None),
125            };
126            record.insert("kind", kind_str.into_value(span));
127
128            // Always a column: kinds without a type report `null`.
129            record.insert(
130                "type",
131                ty.map_or_else(|| Value::nothing(span), |ty| ty.into_value(span)),
132            );
133        }
134
135        Value::record(record, span)
136    }
137}
138
139fn span_record(span: reedline::Span, src_span: Span) -> Option<Value> {
140    let (Ok(start), Ok(end)) = (span.start.try_into(), span.end.try_into()) else {
141        log::error!("failed to convert span to i64s");
142        return None;
143    };
144
145    Some(Value::record(
146        Record::from_iter([
147            ("start".into(), Value::int(start, src_span)),
148            ("end".into(), Value::int(end, src_span)),
149        ]),
150        src_span,
151    ))
152}
153
154impl From<Suggestion> for SemanticSuggestion {
155    fn from(suggestion: Suggestion) -> Self {
156        Self {
157            suggestion,
158            ..Default::default()
159        }
160    }
161}
162
163#[cfg(test)]
164mod tests {
165    use super::*;
166
167    /// `complete_argument_value` relies on this to treat `need_fallback`-requesting
168    /// outcomes as always empty (a dead check was removed on that assumption).
169    #[test]
170    fn fallback_variants_carry_no_suggestions() {
171        assert!(Fetched::Declined.into_suggestions().is_empty());
172        assert!(Fetched::Absent.into_suggestions().is_empty());
173    }
174}