tinymist_query/analysis/completion/
typst_specific.rs

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