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 type_dot_access_completions(&mut self, target: &LinkedNode) -> Option<()> {
17 let mode = self.cursor.leaf_mode();
18
19 if matches!(mode, InterpretMode::Math) {
20 return None;
21 }
22
23 self.type_field_access_completions(target);
24 Some(())
25 }
26
27 fn type_field_access_completions(&mut self, target: &LinkedNode) -> Option<()> {
29 let ty = self
30 .worker
31 .ctx
32 .post_type_of_node(target.clone())
33 .filter(|ty| !matches!(ty, Ty::Any));
34 crate::log_debug_ct!("type_field_access_completions_on: {target:?} -> {ty:?}");
35 let mut defines = Defines {
36 types: self.worker.ctx.type_check(&self.cursor.source),
37 defines: Default::default(),
38 docs: Default::default(),
39 };
40 ty?.iface_surface(
41 true,
42 &mut CompletionScopeChecker {
43 check_kind: ScopeCheckKind::FieldAccess,
44 defines: &mut defines,
45 ctx: self.worker.ctx,
46 },
47 );
48
49 self.def_completions(defines, true);
50 Some(())
51 }
52
53 fn value_dot_access_completions(&mut self, target: &LinkedNode) -> Option<()> {
55 let (value, styles) = self.worker.ctx.analyze_expr(target).into_iter().next()?;
56
57 let mode = self.cursor.leaf_mode();
58 let valid_field_access_syntax =
59 !matches!(mode, InterpretMode::Math) || is_valid_math_field_access(target);
60 let valid_postfix_target =
61 !matches!(mode, InterpretMode::Math) || is_valid_math_postfix(target);
62
63 if !valid_field_access_syntax && !valid_postfix_target {
64 return None;
65 }
66
67 if valid_field_access_syntax {
68 self.value_field_access_completions(&value, mode);
69 }
70 if valid_postfix_target {
71 self.postfix_completions(target, Ty::Value(InsTy::new(value.clone())));
72 }
73
74 match value {
75 Value::Symbol(symbol) => {
76 self.symbol_var_completions(&symbol, None);
77
78 if valid_postfix_target {
79 self.ufcs_completions(target);
80 }
81 }
82 Value::Content(content) => {
83 if valid_field_access_syntax {
84 for (name, value) in content.fields() {
85 self.value_completion(Some(name.into()), &value, false, None);
86 }
87 }
88 if valid_postfix_target {
89 self.ufcs_completions(target);
90 }
91 }
92 Value::Dict(dict) if valid_field_access_syntax => {
93 for (name, value) in dict.iter() {
94 self.value_completion(Some(name.clone().into()), value, false, None);
95 }
96 }
97 Value::Func(func) if valid_field_access_syntax => {
98 if let Some((elem, styles)) = func.element().zip(styles.as_ref()) {
100 for param in elem.params().iter().filter(|param| !param.required) {
101 if let Some(value) = elem
102 .field_id(param.name)
103 .map(|id| elem.field_from_styles(id, StyleChain::new(styles)))
104 {
105 self.value_completion(
106 Some(param.name.into()),
107 &value.unwrap(),
108 false,
109 None,
110 );
111 }
112 }
113 }
114 }
115 _ => {}
116 }
117
118 Some(())
119 }
120
121 fn value_field_access_completions(&mut self, value: &Value, mode: InterpretMode) {
122 let elem_parens = !matches!(mode, InterpretMode::Math);
123 for (name, bind) in value.ty().scope().iter() {
124 if matches!(mode, InterpretMode::Math) && is_func(bind.read()) {
125 continue;
126 }
127
128 self.value_completion_(
129 bind.read(),
130 ValueCompletionInfo {
131 label: Some(name.clone()),
132 parens: elem_parens,
133 docs: None,
134 label_details: None,
135 bound_self: true,
136 },
137 );
138 }
139
140 if let Some(scope) = value.scope() {
141 for (name, bind) in scope.iter() {
142 if matches!(mode, InterpretMode::Math) && is_func(bind.read()) {
143 continue;
144 }
145
146 self.value_completion_(
147 bind.read(),
148 ValueCompletionInfo {
149 label: Some(name.clone()),
150 parens: elem_parens,
151 docs: None,
152 label_details: None,
153 bound_self: false,
154 },
155 );
156 }
157 }
158
159 for &field in fields_on(value.ty()) {
160 self.value_completion_(
166 &value.field(field, ()).unwrap(),
167 ValueCompletionInfo {
168 label: Some(field.into()),
169 parens: false,
170 docs: None,
171 label_details: None,
172 bound_self: true,
173 },
174 );
175 }
176 }
177}
178
179fn is_func(read: &Value) -> bool {
180 matches!(read, Value::Func(func) if func.element().is_none())
181}
182
183fn is_valid_math_field_access(target: &SyntaxNode) -> bool {
184 if let Some(field_access) = target.cast::<ast::FieldAccess>() {
185 return is_valid_math_field_access(field_access.target().to_untyped());
186 }
187 if matches!(target.kind(), SyntaxKind::Ident | SyntaxKind::MathIdent) {
188 return true;
189 }
190
191 false
192}
193
194fn is_valid_math_postfix(target: &SyntaxNode) -> bool {
195 fn bad_punc_text(punc: char) -> bool {
196 punc.is_ascii_punctuation() || punc.is_ascii_whitespace()
197 }
198
199 if let Some(target) = target.cast::<ast::MathText>() {
200 return match target.get() {
201 MathTextKind::Character(ch) => !bad_punc_text(ch),
202 MathTextKind::Number(..) => true,
203 };
204 }
205
206 if let Some(target) = target.cast::<ast::Text>() {
207 let target = target.get();
208 return !target.is_empty() && target.chars().all(|ch| !bad_punc_text(ch));
209 }
210
211 true
212}