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