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