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