tinymist_query/analysis/completion/
field_access.rs1use typst::syntax::ast::MathTextKind;
4
5use crate::analysis::completion::typst_specific::ValueCompletionInfo;
6
7use super::*;
8impl CompletionPair<'_, '_, '_> {
9 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 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 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 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 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 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 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}