tinymist_query/analysis/completion/
typst_specific.rs

1//! Completion by typst specific semantics, like `font`, `package`, `label`, or
2//! `typst::foundations::Value`.
3
4use tinymist_std::time::yyyy_mm_dd;
5use typst::foundations::Symbol;
6use typst::syntax::is_valid_label_literal_id;
7
8use super::*;
9impl CompletionPair<'_, '_, '_> {
10    /// Add completions for all font families.
11    pub fn font_completions(&mut self) {
12        let equation = self.cursor.before_window(25).contains("equation");
13        for (family, iter) in self.worker.world().clone().book().families() {
14            let detail = summarize_font_family(iter);
15            if !equation || family.contains("Math") {
16                self.value_completion(
17                    None,
18                    &Value::Str(family.into()),
19                    false,
20                    Some(detail.as_str()),
21                );
22            }
23        }
24    }
25
26    /// Add completions for current font features.
27    pub fn font_feature_completions(&mut self) {
28        // todo: add me
29    }
30
31    /// Add completions for all available packages.
32    pub fn package_completions(&mut self, all_versions: bool) {
33        let w = self.worker.world().clone();
34        // Finds packages that are in `@preview`
35        let mut packages = w.packages().iter().collect_vec();
36
37        // Finds packages that are not in `@preview`
38        #[cfg(feature = "local-registry")]
39        let other_packages_refs = self.worker.ctx.non_preview_packages();
40        #[cfg(feature = "local-registry")]
41        packages.extend(other_packages_refs.iter());
42
43        packages.sort_by_key(|entry| {
44            (
45                &entry.namespace,
46                &entry.package.name,
47                Reverse(entry.package.version),
48            )
49        });
50        if !all_versions {
51            packages.dedup_by_key(|entry| (&entry.namespace, &entry.package.name));
52        }
53        let completion_info = packages
54            .iter()
55            .map(|entry| {
56                let mut docs = String::new();
57                if let Some(desc) = &entry.package.description {
58                    docs.push_str(desc);
59                    docs.push_str("\n\n");
60                }
61                docs.push_str(&format!("Authors: {}\n", entry.package.authors.join(", ")));
62                docs.push_str(&format!("Version: {}\n", entry.package.version));
63                if let Some(updated_at) = &entry.updated_at
64                    && let Ok(formatted) = updated_at.format(&yyyy_mm_dd())
65                {
66                    docs.push_str(&format!("Updated: {formatted}\n"));
67                }
68                (format_str!("{}", entry.spec()), docs)
69            })
70            .collect_vec(); // avoid self borrow
71        for (value, docs) in completion_info {
72            self.value_completion(None, &Value::Str(value), false, Some(&docs));
73        }
74    }
75
76    /// Add completions for raw block tags.
77    pub fn raw_completions(&mut self) {
78        for (name, mut tags) in RawElem::languages() {
79            let lower = name.to_lowercase();
80            if !tags.contains(&lower.as_str()) {
81                tags.push(lower.as_str());
82            }
83
84            tags.retain(|tag| is_ident(tag));
85            if tags.is_empty() {
86                continue;
87            }
88
89            self.push_completion(Completion {
90                kind: CompletionKind::Constant,
91                label: name.into(),
92                apply: Some(tags[0].into()),
93                detail: Some(repr::separated_list(&tags, " or ").into()),
94                ..Completion::default()
95            });
96        }
97    }
98
99    /// Add completions for labels and references.
100    pub fn ref_completions(&mut self) {
101        self.label_completions_(false, true);
102    }
103
104    /// Add completions for labels and references.
105    pub fn label_completions(&mut self, only_citation: bool) {
106        self.label_completions_(only_citation, false);
107    }
108
109    fn label_completion_apply(
110        &mut self,
111        label: &EcoString,
112        open: bool,
113        close: bool,
114    ) -> (EcoString, Option<Vec<EcoTextEdit>>) {
115        if is_valid_label_literal_id(label.as_str()) {
116            return (
117                eco_format!(
118                    "{}{}{}",
119                    if open { "<" } else { "" },
120                    label.as_str(),
121                    if close { ">" } else { "" }
122                ),
123                None,
124            );
125        }
126        let mut edits = vec![];
127
128        let (remove_open, remove_close) = match self.cursor.selected_node() {
129            Some(SelectedNode::Label(node)) => {
130                let range = node.range();
131                let remove_open = node
132                    .text()
133                    .starts_with('<')
134                    .then_some(range.start..range.start + 1);
135                let remove_close = node
136                    .text()
137                    .ends_with('>')
138                    .then_some(range.end - 1..range.end);
139                (remove_open, remove_close)
140            }
141            _ => (
142                (self.cursor.from > 0 && self.cursor.text[..self.cursor.from].ends_with('<'))
143                    .then_some(self.cursor.from - 1..self.cursor.from),
144                self.cursor
145                    .after
146                    .starts_with('>')
147                    .then_some(self.cursor.cursor..self.cursor.cursor + 1),
148            ),
149        };
150
151        if let Some(range) = remove_open {
152            edits.push(EcoTextEdit::new(
153                self.cursor.lsp_range_of(range),
154                EcoString::new(),
155            ));
156        }
157        if let Some(range) = remove_close {
158            edits.push(EcoTextEdit::new(
159                self.cursor.lsp_range_of(range),
160                EcoString::new(),
161            ));
162        }
163
164        (
165            eco_format!("label({})", label.as_str().repr()),
166            (!edits.is_empty()).then_some(edits),
167        )
168    }
169
170    /// Add completions for labels and references.
171    pub fn label_completions_(&mut self, only_citation: bool, ref_label: bool) {
172        let Some(document) = self.worker.document else {
173            return;
174        };
175        let (labels, split) = analyze_labels(document);
176
177        let head = &self.cursor.text[..self.cursor.from];
178        let at = head.ends_with('@');
179        let open = !at && !head.ends_with('<');
180        let close = !at && !self.cursor.after.starts_with('>');
181        let citation = !at && only_citation;
182
183        let (skip, take) = if at || ref_label {
184            (0, usize::MAX)
185        } else if citation {
186            (split, usize::MAX)
187        } else {
188            (0, split)
189        };
190
191        for DynLabel {
192            label,
193            label_desc,
194            detail,
195            bib_title,
196        } in labels.into_iter().skip(skip).take(take)
197        {
198            if !self.worker.seen_casts.insert(hash128(&label)) {
199                continue;
200            }
201            let label: EcoString = label.resolve().as_str().into();
202            let (apply, additional_text_edits) = self.label_completion_apply(&label, open, close);
203            let completion = Completion {
204                kind: CompletionKind::Reference,
205                apply: Some(apply),
206                additional_text_edits,
207                label: label.clone(),
208                label_details: label_desc.clone(),
209                filter_text: Some(label.clone()),
210                detail: detail.clone(),
211                ..Completion::default()
212            };
213
214            if let Some(bib_title) = bib_title {
215                // Note that this completion re-uses the above `apply` field to
216                // alter the `bib_title` to the corresponding label.
217                self.push_completion(Completion {
218                    kind: CompletionKind::Constant,
219                    label: bib_title.clone(),
220                    label_details: Some(label),
221                    filter_text: Some(bib_title),
222                    detail,
223                    ..completion.clone()
224                });
225            }
226
227            self.push_completion(completion);
228        }
229    }
230
231    /// Add a completion for a specific value.
232    pub fn value_completion(
233        &mut self,
234        label: Option<EcoString>,
235        value: &Value,
236        parens: bool,
237        docs: Option<&str>,
238    ) {
239        self.value_completion_(
240            value,
241            ValueCompletionInfo {
242                label,
243                parens,
244                label_details: None,
245                docs,
246                bound_self: false,
247            },
248        );
249    }
250
251    /// Add a completion for a specific value.
252    pub fn value_completion_(&mut self, value: &Value, extras: ValueCompletionInfo) {
253        let ValueCompletionInfo {
254            label,
255            parens,
256            label_details,
257            docs,
258            bound_self,
259        } = extras;
260
261        // Prevent duplicate completions from appearing.
262        if !self.worker.seen_casts.insert(hash128(&(&label, &value))) {
263            return;
264        }
265
266        let at = label.as_deref().is_some_and(|field| !is_ident(field));
267        let label = label.unwrap_or_else(|| value.repr());
268
269        let detail = docs.map(Into::into).or_else(|| match value {
270            Value::Symbol(symbol) => Some(symbol_detail(symbol.get())),
271            Value::Func(func) => func.docs().map(plain_docs_sentence),
272            Value::Type(ty) => Some(plain_docs_sentence(ty.docs())),
273            v => {
274                let repr = v.repr();
275                (repr.as_str() != label).then_some(repr)
276            }
277        });
278        let label_details = label_details.or_else(|| match value {
279            Value::Symbol(s) => Some(symbol_label_detail(s.get())),
280            _ => None,
281        });
282
283        let mut apply = None;
284        if parens && matches!(value, Value::Func(_)) {
285            let mode = self.cursor.leaf_mode();
286            let ty = Ty::Value(InsTy::new(value.clone()));
287            let kind_checker = CompletionKindChecker {
288                symbols: HashSet::default(),
289                functions: HashSet::from_iter([&ty]),
290            };
291            let mut fn_feat = FnCompletionFeat::default();
292            // todo: unify bound self checking
293            fn_feat.bound_self = bound_self;
294            let fn_feat = fn_feat.check(kind_checker.functions.iter().copied());
295            self.func_completion(mode, fn_feat, label, label_details, detail, parens);
296            return;
297        } else if at {
298            apply = Some(eco_format!("at(\"{label}\")"));
299        } else {
300            let apply_label = &mut label.as_str();
301            if apply_label.ends_with('"')
302                && self.cursor.after.starts_with('"')
303                && let Some(trimmed) = apply_label.strip_suffix('"')
304            {
305                *apply_label = trimmed;
306            }
307            let from_before = slice_at(self.cursor.text, 0..self.cursor.from);
308            if apply_label.starts_with('"')
309                && from_before.ends_with('"')
310                && let Some(trimmed) = apply_label.strip_prefix('"')
311            {
312                *apply_label = trimmed;
313            }
314
315            if apply_label.len() != label.len() {
316                apply = Some((*apply_label).into());
317            }
318        }
319
320        self.push_completion(Completion {
321            kind: value_to_completion_kind(value),
322            label,
323            apply,
324            detail,
325            label_details,
326            ..Completion::default()
327        });
328    }
329
330    pub fn symbol_completions(&mut self, label: EcoString, symbol: &Symbol) {
331        let sym_val = symbol.get();
332        let kind = CompletionKind::Symbol(sym_val.into());
333        self.push_completion(Completion {
334            kind,
335            label: label.clone(),
336            label_details: Some(symbol_label_detail(sym_val)),
337            detail: Some(symbol_detail(sym_val)),
338            ..Completion::default()
339        });
340
341        let is_stepless = self.cursor.ctx.analysis.completion_feat.is_stepless();
342        if is_stepless {
343            self.symbol_var_completions(symbol, Some(&label));
344        }
345    }
346
347    pub fn symbol_var_completions(&mut self, symbol: &Symbol, prefix: Option<&str>) {
348        for modifier in symbol.modifiers() {
349            if let Ok(modified) = symbol.clone().modified((), modifier) {
350                let label = match &prefix {
351                    Some(prefix) => eco_format!("{prefix}.{modifier}"),
352                    None => modifier.into(),
353                };
354
355                self.symbol_completions(label, &modified);
356            }
357        }
358    }
359}
360
361#[derive(Debug, Clone, Default)]
362pub struct ValueCompletionInfo<'a> {
363    pub label: Option<EcoString>,
364    pub parens: bool,
365    pub label_details: Option<EcoString>,
366    pub docs: Option<&'a str>,
367    pub bound_self: bool,
368}