tinymist_query/analysis/completion/
type.rs

1//! Completion by types.
2
3use super::*;
4use typst::foundations::Label;
5use typst::visualize::Color;
6
7pub(crate) struct TypeCompletionWorker<'a, 'b, 'c, 'd> {
8    pub base: &'d mut CompletionPair<'a, 'b, 'c>,
9    pub filter: &'d dyn Fn(&Ty) -> bool,
10}
11
12impl TypeCompletionWorker<'_, '_, '_, '_> {
13    fn snippet_completion(&mut self, label: &str, apply: &str, detail: &str) {
14        if !(self.filter)(&Ty::Any) {
15            return;
16        }
17
18        self.base.snippet_completion(label, apply, detail);
19    }
20
21    pub fn type_completion(&mut self, infer_type: &Ty, docs: Option<&str>) -> Option<()> {
22        // Prevent duplicate completions from appearing.
23        if !self.base.worker.seen_types.insert(infer_type.clone()) {
24            return Some(());
25        }
26
27        crate::log_debug_ct!("type_completion: {infer_type:?}");
28
29        match infer_type {
30            Ty::Any => return None,
31            Ty::Pattern(_) => return None,
32            Ty::Args(_) => return None,
33            Ty::Func(_) => return None,
34            Ty::With(_) => return None,
35            Ty::Select(_) => return None,
36            Ty::Var(_) => return None,
37            Ty::Unary(_) => return None,
38            Ty::Binary(_) => return None,
39            Ty::If(_) => return None,
40            Ty::Union(u) => {
41                for info in u.as_ref() {
42                    self.type_completion(info, docs);
43                }
44            }
45            Ty::Let(bounds) => {
46                for ut in bounds.ubs.iter() {
47                    self.type_completion(ut, docs);
48                }
49                for lt in bounds.lbs.iter() {
50                    self.type_completion(lt, docs);
51                }
52            }
53            Ty::Tuple(..) | Ty::Array(..) => {
54                if !(self.filter)(infer_type) {
55                    return None;
56                }
57                self.snippet_completion("()", "(${})", "An array.");
58            }
59            Ty::Dict(..) => {
60                if !(self.filter)(infer_type) {
61                    return None;
62                }
63                self.snippet_completion("()", "(${})", "A dictionary.");
64            }
65            Ty::Boolean(_b) => {
66                if !(self.filter)(infer_type) {
67                    return None;
68                }
69                self.snippet_completion("false", "false", "No / Disabled.");
70                self.snippet_completion("true", "true", "Yes / Enabled.");
71            }
72            Ty::Builtin(v) => {
73                if !(self.filter)(infer_type) {
74                    return None;
75                }
76                self.builtin_type_completion(v, docs);
77            }
78            Ty::Value(v) => {
79                if !(self.filter)(infer_type) {
80                    return None;
81                }
82                let docs = v.syntax.as_ref().map(|s| s.doc.as_ref()).or(docs);
83
84                if let Value::Type(ty) = &v.val {
85                    self.type_completion(&Ty::Builtin(BuiltinTy::Type(*ty)), docs);
86                } else if v.val.ty() == Type::of::<NoneValue>() {
87                    self.type_completion(&Ty::Builtin(BuiltinTy::None), docs);
88                } else if v.val.ty() == Type::of::<AutoValue>() {
89                    self.type_completion(&Ty::Builtin(BuiltinTy::Auto), docs);
90                } else {
91                    self.base.value_completion(None, &v.val, true, docs);
92                }
93            }
94            Ty::Param(param) => {
95                // todo: variadic
96
97                let resolved_docs;
98                let docs = if docs.is_some() {
99                    docs
100                } else {
101                    resolved_docs = param.docs.as_ref().map(|docs| {
102                        crate::docs::resolve_doc_text(self.base.worker.ctx.shared(), docs)
103                    });
104                    resolved_docs.as_deref()
105                };
106                if param.attrs.positional {
107                    self.type_completion(&param.ty, docs);
108                }
109                if !param.attrs.named {
110                    return Some(());
111                }
112
113                let field = &param.name;
114                if self.base.worker.seen_field(field.clone()) {
115                    return Some(());
116                }
117                if !(self.filter)(infer_type) {
118                    return None;
119                }
120
121                let mut rev_stream = self.base.cursor.before.chars().rev();
122                let ch = rev_stream.find(|ch| !typst::syntax::is_id_continue(*ch));
123                // skip label/ref completion.
124                // todo: more elegant way
125                if matches!(ch, Some('<' | '@')) {
126                    return Some(());
127                }
128
129                self.base.push_completion(Completion {
130                    kind: CompletionKind::Field,
131                    label: field.into(),
132                    apply: Some(eco_format!("{}: ${{}}", field)),
133                    label_details: param.ty.describe(),
134                    detail: docs.map(Into::into),
135                    command: self
136                        .base
137                        .worker
138                        .ctx
139                        .analysis
140                        .trigger_on_snippet_with_param_hint(true)
141                        .map(From::from),
142                    ..Completion::default()
143                });
144            }
145        };
146
147        Some(())
148    }
149
150    pub fn builtin_type_completion(&mut self, v: &BuiltinTy, docs: Option<&str>) -> Option<()> {
151        match v {
152            BuiltinTy::None => self.snippet_completion("none", "none", "Nothing."),
153            BuiltinTy::Auto => {
154                self.snippet_completion("auto", "auto", "A smart default.");
155            }
156            BuiltinTy::Clause => return None,
157            BuiltinTy::Undef => return None,
158            BuiltinTy::Space => return None,
159            BuiltinTy::Break => return None,
160            BuiltinTy::Continue => return None,
161            BuiltinTy::Never => return None,
162            BuiltinTy::Content(..) => return None,
163            BuiltinTy::Infer => return None,
164            BuiltinTy::FlowNone => return None,
165            BuiltinTy::Tag(..) => return None,
166            BuiltinTy::Module(..) => return None,
167
168            BuiltinTy::Path(preference) => {
169                let items = self.base.complete_path(preference);
170                self.base
171                    .worker
172                    .completions
173                    .extend(items.into_iter().flatten());
174            }
175            BuiltinTy::Args => return None,
176            BuiltinTy::Stroke => {
177                self.snippet_completion("stroke()", "stroke(${})", "Stroke type.");
178                self.snippet_completion("()", "(${})", "Stroke dictionary.");
179                self.type_completion(&Ty::Builtin(BuiltinTy::Color), docs);
180                self.type_completion(&Ty::Builtin(BuiltinTy::Length), docs);
181            }
182            BuiltinTy::Color => {
183                self.snippet_completion("luma()", "luma(${v})", "A custom grayscale color.");
184                self.snippet_completion(
185                    "rgb()",
186                    "rgb(${r}, ${g}, ${b}, ${a})",
187                    "A custom RGBA color.",
188                );
189                self.snippet_completion(
190                    "cmyk()",
191                    "cmyk(${c}, ${m}, ${y}, ${k})",
192                    "A custom CMYK color.",
193                );
194                self.snippet_completion(
195                    "oklab()",
196                    "oklab(${l}, ${a}, ${b}, ${alpha})",
197                    "A custom Oklab color.",
198                );
199                self.snippet_completion(
200                    "oklch()",
201                    "oklch(${l}, ${chroma}, ${hue}, ${alpha})",
202                    "A custom Oklch color.",
203                );
204                self.snippet_completion(
205                    "color.linear-rgb()",
206                    "color.linear-rgb(${r}, ${g}, ${b}, ${a})",
207                    "A custom linear RGBA color.",
208                );
209                self.snippet_completion(
210                    "color.hsv()",
211                    "color.hsv(${h}, ${s}, ${v}, ${a})",
212                    "A custom HSVA color.",
213                );
214                self.snippet_completion(
215                    "color.hsl()",
216                    "color.hsl(${h}, ${s}, ${l}, ${a})",
217                    "A custom HSLA color.",
218                );
219            }
220            BuiltinTy::TextSize => return None,
221            #[cfg(not(feature = "lsp"))]
222            BuiltinTy::TextLang => return None,
223            #[cfg(feature = "lsp")]
224            BuiltinTy::TextLang => {
225                for (&key, desc) in rust_iso639::ALL_MAP.entries() {
226                    let detail =
227                        eco_format!("An ISO 639-1/2/3 language code, {}.", desc.get_name());
228                    self.base.push_completion(Completion {
229                        kind: CompletionKind::Syntax,
230                        label: key.to_lowercase().into(),
231                        apply: Some(eco_format!("\"{}\"", key.to_lowercase())),
232                        detail: Some(detail),
233                        label_details: Some(desc.get_name()),
234                        ..Completion::default()
235                    });
236                }
237            }
238            #[cfg(not(feature = "lsp"))]
239            BuiltinTy::TextRegion => return None,
240            #[cfg(feature = "lsp")]
241            BuiltinTy::TextRegion => {
242                for (&key, desc) in rust_iso3166::ALPHA2_MAP.entries() {
243                    let detail =
244                        eco_format!("An ISO 3166-1 alpha-2 region code, {}.", desc.get_name());
245                    self.base.push_completion(Completion {
246                        kind: CompletionKind::Syntax,
247                        label: key.to_lowercase().into(),
248                        apply: Some(eco_format!("\"{}\"", key.to_lowercase())),
249                        detail: Some(detail),
250                        label_details: Some(desc.get_name()),
251                        ..Completion::default()
252                    });
253                }
254            }
255            BuiltinTy::Dir => {}
256            BuiltinTy::TextFont => {
257                self.base.font_completions();
258            }
259            BuiltinTy::TextFeature => {
260                self.base.font_feature_completions();
261            }
262            BuiltinTy::Margin => {
263                self.snippet_completion("()", "(${})", "Margin dictionary.");
264                self.type_completion(&Ty::Builtin(BuiltinTy::Length), docs);
265            }
266            BuiltinTy::Inset => {
267                self.snippet_completion("()", "(${})", "Inset dictionary.");
268                self.type_completion(&Ty::Builtin(BuiltinTy::Length), docs);
269            }
270            BuiltinTy::Outset => {
271                self.snippet_completion("()", "(${})", "Outset dictionary.");
272                self.type_completion(&Ty::Builtin(BuiltinTy::Length), docs);
273            }
274            BuiltinTy::Radius => {
275                self.snippet_completion("()", "(${})", "Radius dictionary.");
276                self.type_completion(&Ty::Builtin(BuiltinTy::Length), docs);
277            }
278            BuiltinTy::Length => {
279                self.snippet_completion("pt", "${1}pt", "Point length unit.");
280                self.snippet_completion("mm", "${1}mm", "Millimeter length unit.");
281                self.snippet_completion("cm", "${1}cm", "Centimeter length unit.");
282                self.snippet_completion("in", "${1}in", "Inch length unit.");
283                self.snippet_completion("em", "${1}em", "Em length unit.");
284                self.type_completion(&Ty::Builtin(BuiltinTy::Auto), docs);
285            }
286            BuiltinTy::Float => {
287                self.snippet_completion(
288                    "exponential notation",
289                    "${1}e${0}",
290                    "Exponential notation",
291                );
292            }
293            BuiltinTy::Label => {
294                self.base.label_completions(false);
295            }
296            BuiltinTy::CiteLabel => {
297                self.base.label_completions(true);
298            }
299            BuiltinTy::RefLabel => {
300                self.base.ref_completions();
301            }
302            BuiltinTy::TypeType(ty) | BuiltinTy::Type(ty) => {
303                if *ty == Type::of::<NoneValue>() {
304                    let docs = docs.or(Some("Nothing."));
305                    self.type_completion(&Ty::Builtin(BuiltinTy::None), docs);
306                } else if *ty == Type::of::<AutoValue>() {
307                    let docs = docs.or(Some("A smart default."));
308                    self.type_completion(&Ty::Builtin(BuiltinTy::Auto), docs);
309                } else if *ty == Type::of::<bool>() {
310                    self.snippet_completion("false", "false", "No / Disabled.");
311                    self.snippet_completion("true", "true", "Yes / Enabled.");
312                } else if *ty == Type::of::<Color>() {
313                    self.type_completion(&Ty::Builtin(BuiltinTy::Color), docs);
314                } else if *ty == Type::of::<Label>() {
315                    self.base.label_completions(false)
316                } else if *ty == Type::of::<Func>() {
317                    self.snippet_completion(
318                        "function",
319                        "(${params}) => ${output}",
320                        "A custom function.",
321                    );
322                } else if let Ok(cons) = ty.constructor() {
323                    let docs = docs.or(cons.docs()).unwrap_or(ty.docs());
324                    self.base.value_completion(
325                        Some(ty.short_name().into()),
326                        &Value::Func(cons),
327                        true,
328                        Some(docs),
329                    );
330                } else if ty.scope().iter().any(|(_, b)| {
331                    if let Value::Func(f) = b.read() {
332                        let pos = f.params().find(|s| s.required());
333                        pos.is_none_or(|pos| pos.name() != Some("self"))
334                    } else {
335                        true
336                    }
337                }) {
338                    let docs = docs.unwrap_or(ty.docs());
339                    self.base.push_completion(Completion {
340                        kind: CompletionKind::Syntax,
341                        label: ty.short_name().into(),
342                        apply: Some(eco_format!("${{{ty}}}")),
343                        detail: Some(docs.into()),
344                        ..Completion::default()
345                    });
346                }
347                // Otherwise, if the type doesn't have constructor and
348                // associated scope, we do nothing here. For example,
349                // - complete `content` doesn't make much sense.
350                // - complete `color` is okay because it has associated
351                //   constructors in scope.`
352            }
353            BuiltinTy::Element(elem) => {
354                self.base.value_completion(
355                    Some(elem.name().into()),
356                    &Value::Func((*elem).into()),
357                    true,
358                    docs,
359                );
360            }
361        };
362
363        Some(())
364    }
365}
366
367#[cfg(feature = "lsp")]
368use iso::*;
369#[cfg(feature = "lsp")]
370mod iso {
371    use ecow::EcoString;
372
373    pub trait GetName {
374        fn get_name(&self) -> EcoString;
375    }
376
377    #[cfg(not(target_arch = "wasm32"))]
378    impl GetName for rust_iso639::LanguageCode<'_> {
379        fn get_name(&self) -> EcoString {
380            self.name.into()
381        }
382    }
383
384    #[cfg(not(all(direct_wasm, target_arch = "wasm32")))]
385    impl GetName for rust_iso3166::CountryCode {
386        fn get_name(&self) -> EcoString {
387            self.name.into()
388        }
389    }
390
391    #[cfg(target_arch = "wasm32")]
392    impl GetName for rust_iso639::LanguageCode {
393        fn get_name(&self) -> EcoString {
394            self.name().into()
395        }
396    }
397
398    #[cfg(all(direct_wasm, target_arch = "wasm32"))]
399    impl GetName for rust_iso3166::CountryCode {
400        fn get_name(&self) -> EcoString {
401            self.name().into()
402        }
403    }
404}