tinymist_query/analysis/completion/
field_access.rs

1//! Completion for field access on nodes.
2
3use typst::syntax::ast::MathTextKind;
4
5use crate::analysis::completion::typst_specific::ValueCompletionInfo;
6
7use super::*;
8impl CompletionPair<'_, '_, '_> {
9    /// Add completions for all dot targets on a node.
10    pub fn doc_access_completions(&mut self, target: &LinkedNode) -> Option<()> {
11        self.value_dot_access_completions(target)
12            .or_else(|| self.type_dot_access_completions(target))
13    }
14
15    /// Dot-access can sit inside a math equation while still targeting a code
16    /// interpolation like `$ #calc. $`. In that case, the accessed expression's
17    /// mode is the one that matters for completion behavior.
18    fn dot_access_mode(&self, target: &LinkedNode) -> InterpretMode {
19        let mode = self.cursor.leaf_mode();
20        let target_mode = interpret_mode_at(Some(target));
21
22        if matches!(mode, InterpretMode::Math) && matches!(target_mode, InterpretMode::Code) {
23            return target_mode;
24        }
25
26        mode
27    }
28
29    /// Add completions for all fields on a type.
30    fn type_dot_access_completions(&mut self, target: &LinkedNode) -> Option<()> {
31        let mode = self.dot_access_mode(target);
32
33        if matches!(mode, InterpretMode::Math) {
34            return None;
35        }
36
37        self.type_field_access_completions(target);
38        Some(())
39    }
40
41    /// Add completions for all fields on a type.
42    fn type_field_access_completions(&mut self, target: &LinkedNode) -> Option<()> {
43        let ty = self
44            .worker
45            .ctx
46            .post_type_of_node(target.clone())
47            .filter(|ty| !matches!(ty, Ty::Any));
48        crate::log_debug_ct!("type_field_access_completions_on: {target:?} -> {ty:?}");
49        let mut defines = Defines {
50            types: self.worker.ctx.type_check(&self.cursor.source),
51            defines: Default::default(),
52            docs: Default::default(),
53        };
54        ty?.iface_surface(
55            true,
56            &mut CompletionScopeChecker {
57                check_kind: ScopeCheckKind::FieldAccess,
58                defines: &mut defines,
59                ctx: self.worker.ctx,
60            },
61        );
62
63        self.def_completions(defines, true);
64        Some(())
65    }
66
67    /// Add completions for all fields on a value.
68    fn value_dot_access_completions(&mut self, target: &LinkedNode) -> Option<()> {
69        let (value, styles) = self.worker.ctx.analyze_expr(target).into_iter().next()?;
70
71        let mode = self.dot_access_mode(target);
72        let valid_field_access_syntax =
73            !matches!(mode, InterpretMode::Math) || is_valid_math_field_access(target);
74        let valid_postfix_target =
75            !matches!(mode, InterpretMode::Math) || is_valid_math_postfix(target);
76
77        if !valid_field_access_syntax && !valid_postfix_target {
78            return None;
79        }
80
81        if valid_field_access_syntax {
82            self.value_field_access_completions(&value, mode);
83        }
84        if valid_postfix_target {
85            self.postfix_completions(target, Ty::Value(InsTy::new(value.clone())));
86        }
87
88        match value {
89            Value::Symbol(symbol) => {
90                self.symbol_var_completions(&symbol, None);
91
92                if valid_postfix_target {
93                    self.ufcs_completions(target);
94                }
95            }
96            Value::Content(content) => {
97                if valid_field_access_syntax {
98                    for (name, value) in content.fields() {
99                        self.value_completion(Some(name.into()), &value, false, None);
100                    }
101                }
102                if valid_postfix_target {
103                    self.ufcs_completions(target);
104                }
105            }
106            Value::Dict(dict) if valid_field_access_syntax => {
107                for (name, value) in dict.iter() {
108                    self.value_completion(Some(name.clone().into()), value, false, None);
109                }
110            }
111            Value::Func(func) if valid_field_access_syntax => {
112                // Autocomplete get rules.
113                if let Some((elem, styles)) = func.element().zip(styles.as_ref()) {
114                    for param in elem.params().iter().filter(|param| !param.required) {
115                        if let Some(value) = elem
116                            .field_id(param.name)
117                            .map(|id| elem.field_from_styles(id, StyleChain::new(styles)))
118                        {
119                            self.value_completion(
120                                Some(param.name.into()),
121                                &value.unwrap(),
122                                false,
123                                None,
124                            );
125                        }
126                    }
127                }
128            }
129            _ => {}
130        }
131
132        Some(())
133    }
134
135    fn value_field_access_completions(&mut self, value: &Value, mode: InterpretMode) {
136        let elem_parens = !matches!(mode, InterpretMode::Math);
137        for (name, bind) in value.ty().scope().iter() {
138            if matches!(mode, InterpretMode::Math) && is_func(bind.read()) {
139                continue;
140            }
141
142            self.value_completion_(
143                bind.read(),
144                ValueCompletionInfo {
145                    label: Some(name.clone()),
146                    parens: elem_parens,
147                    docs: None,
148                    label_details: None,
149                    bound_self: true,
150                },
151            );
152        }
153
154        if let Some(scope) = value.scope() {
155            for (name, bind) in scope.iter() {
156                if matches!(mode, InterpretMode::Math) && is_func(bind.read()) {
157                    continue;
158                }
159
160                self.value_completion_(
161                    bind.read(),
162                    ValueCompletionInfo {
163                        label: Some(name.clone()),
164                        parens: elem_parens,
165                        docs: None,
166                        label_details: None,
167                        bound_self: false,
168                    },
169                );
170            }
171        }
172
173        for &field in fields_on(value.ty()) {
174            // Complete the field name along with its value. Notes:
175            // 1. No parentheses since function fields cannot currently be called
176            // with method syntax;
177            // 2. We can unwrap the field's value since it's a field belonging to
178            // this value's type, so accessing it should not fail.
179            self.value_completion_(
180                &value.field(field, ()).unwrap(),
181                ValueCompletionInfo {
182                    label: Some(field.into()),
183                    parens: false,
184                    docs: None,
185                    label_details: None,
186                    bound_self: true,
187                },
188            );
189        }
190    }
191}
192
193fn is_func(read: &Value) -> bool {
194    matches!(read, Value::Func(func) if func.element().is_none())
195}
196
197fn is_valid_math_field_access(target: &SyntaxNode) -> bool {
198    if let Some(field_access) = target.cast::<ast::FieldAccess>() {
199        return is_valid_math_field_access(field_access.target().to_untyped());
200    }
201    if matches!(target.kind(), SyntaxKind::Ident | SyntaxKind::MathIdent) {
202        return true;
203    }
204
205    false
206}
207
208fn is_valid_math_postfix(target: &SyntaxNode) -> bool {
209    fn bad_punc_text(punc: char) -> bool {
210        punc.is_ascii_punctuation() || punc.is_ascii_whitespace()
211    }
212
213    if let Some(target) = target.cast::<ast::MathText>() {
214        return match target.get() {
215            MathTextKind::Character(ch) => !bad_punc_text(ch),
216            MathTextKind::Number(..) => true,
217        };
218    }
219
220    if let Some(target) = target.cast::<ast::Text>() {
221        let target = target.get();
222        return !target.is_empty() && target.chars().all(|ch| !bad_punc_text(ch));
223    }
224
225    true
226}