tinymist_query/analysis/completion/
snippet.rs1use core::fmt;
10
11use super::*;
12
13impl CompletionPair<'_, '_, '_> {
14 pub fn snippet_completion(&mut self, label: &str, snippet: &str, docs: &str) {
16 self.push_completion(Completion {
17 kind: CompletionKind::Syntax,
18 label: label.into(),
19 apply: Some(snippet.into()),
20 detail: Some(docs.into()),
21 command: self
22 .worker
23 .ctx
24 .analysis
25 .trigger_on_snippet(snippet.contains("${"))
26 .map(From::from),
27 ..Completion::default()
28 });
29 }
30
31 pub fn snippet_completions(
32 &mut self,
33 mode: Option<InterpretMode>,
34 surrounding_syntax: Option<SurroundingSyntax>,
35 ) {
36 let mut keys = vec![CompletionContextKey::new(mode, surrounding_syntax)];
37 if mode.is_some() {
38 keys.push(CompletionContextKey::new(None, surrounding_syntax));
39 }
40 if surrounding_syntax.is_some() {
41 keys.push(CompletionContextKey::new(mode, None));
42 if mode.is_some() {
43 keys.push(CompletionContextKey::new(None, None));
44 }
45 }
46 let applies_to = |snippet: &PrefixSnippet| keys.iter().any(|key| snippet.applies_to(key));
47
48 for snippet in DEFAULT_PREFIX_SNIPPET.iter() {
49 if !applies_to(snippet) {
50 continue;
51 }
52
53 let analysis = &self.worker.ctx.analysis;
54 let command = match snippet.command {
55 Some(CompletionCommand::TriggerSuggest) => analysis.trigger_suggest(true),
56 None => analysis.trigger_on_snippet(snippet.snippet.contains("${")),
57 };
58
59 self.push_completion(Completion {
60 kind: CompletionKind::Syntax,
61 label: snippet.label.as_ref().into(),
62 apply: Some(snippet.snippet.as_ref().into()),
63 detail: Some(snippet.description.as_ref().into()),
64 command: command.map(From::from),
65 ..Completion::default()
66 });
67 }
68 }
69
70 pub fn postfix_completions(&mut self, node: &LinkedNode, ty: Ty) -> Option<()> {
71 if !self.worker.ctx.analysis.completion_feat.postfix() {
72 return None;
73 }
74
75 let _ = node;
76
77 if !matches!(self.cursor.surrounding_syntax, SurroundingSyntax::Regular) {
78 return None;
79 }
80
81 let cursor_mode = self.cursor.leaf_mode();
82 let is_content = ty.is_content(&());
83 crate::log_debug_ct!("post snippet is_content: {is_content}");
84
85 let rng = node.range();
86 for snippet in self
87 .worker
88 .ctx
89 .analysis
90 .completion_feat
91 .postfix_snippets()
92 .clone()
93 {
94 if !snippet.mode.contains(&cursor_mode) {
95 continue;
96 }
97
98 let scope = match snippet.scope {
99 PostfixSnippetScope::Value => true,
100 PostfixSnippetScope::Content => is_content,
101 };
102 if !scope {
103 continue;
104 }
105 crate::log_debug_ct!("post snippet: {}", snippet.label);
106
107 static TYPST_SNIPPET_PLACEHOLDER_RE: LazyLock<Regex> =
108 LazyLock::new(|| Regex::new(r"\$\{(.*?)\}").unwrap());
109
110 let parsed_snippet = snippet.parsed_snippet.get_or_init(|| {
111 let split = TYPST_SNIPPET_PLACEHOLDER_RE
112 .find_iter(&snippet.snippet)
113 .map(|s| (&s.as_str()[2..s.as_str().len() - 1], s.start(), s.end()))
114 .collect::<Vec<_>>();
115 if split.len() > 2 {
116 return None;
117 }
118
119 let split0 = split[0];
120 let split1 = split.get(1);
121
122 if split0.0.contains("node") {
123 Some(ParsedSnippet {
124 node_before: snippet.snippet[..split0.1].into(),
125 node_before_before_cursor: None,
126 node_after: snippet.snippet[split0.2..].into(),
127 })
128 } else {
129 split1.map(|split1| ParsedSnippet {
130 node_before_before_cursor: Some(snippet.snippet[..split0.1].into()),
131 node_before: snippet.snippet[split0.1..split1.1].into(),
132 node_after: snippet.snippet[split1.2..].into(),
133 })
134 }
135 });
136 crate::log_debug_ct!("post snippet: {} on {:?}", snippet.label, parsed_snippet);
137 let Some(ParsedSnippet {
138 node_before,
139 node_before_before_cursor,
140 node_after,
141 }) = parsed_snippet
142 else {
143 continue;
144 };
145
146 let base = Completion {
147 kind: CompletionKind::Syntax,
148 apply: Some("".into()),
149 label: snippet.label.clone(),
150 label_details: snippet.label_detail.clone(),
151 detail: Some(snippet.description.clone()),
152 ..Default::default()
154 };
155 if let Some(node_before_before_cursor) = &node_before_before_cursor {
156 let node_content = SnippetEscape(node.get().clone().into_text());
157 let before = EcoTextEdit {
158 range: self.cursor.lsp_range_of(rng.start..self.cursor.from),
159 new_text: EcoString::new(),
160 };
161
162 self.push_completion(Completion {
163 apply: Some(eco_format!(
164 "{node_before_before_cursor}{node_before}{node_content}{node_after}"
165 )),
166 additional_text_edits: Some(vec![before]),
167 ..base
168 });
169 } else {
170 let before = EcoTextEdit {
171 range: self.cursor.lsp_range_of(rng.start..rng.start),
172 new_text: node_before.clone(),
173 };
174 let after = EcoTextEdit {
175 range: self.cursor.lsp_range_of(rng.end..self.cursor.from),
176 new_text: "".into(),
177 };
178 self.push_completion(Completion {
179 apply: Some(node_after.clone()),
180 additional_text_edits: Some(vec![before, after]),
181 ..base
182 });
183 }
184 }
185
186 Some(())
187 }
188
189 pub fn ufcs_completions(&mut self, node: &LinkedNode) {
192 if !self.worker.ctx.analysis.completion_feat.any_ufcs() {
193 return;
194 }
195
196 if !matches!(self.cursor.surrounding_syntax, SurroundingSyntax::Regular) {
197 return;
198 }
199
200 let Some(defines) = self.scope_defs() else {
201 return;
202 };
203
204 crate::log_debug_ct!("defines: {:?}", defines.defines.len());
205 let mut kind_checker = CompletionKindChecker {
206 symbols: HashSet::default(),
207 functions: HashSet::default(),
208 };
209
210 let rng = node.range();
211
212 let is_content_block = node.kind() == SyntaxKind::ContentBlock;
213
214 let lb = if is_content_block { "" } else { "(" };
215 let rb = if is_content_block { "" } else { ")" };
216
217 for (name, ty) in &defines.defines {
219 if name.is_empty() {
221 continue;
222 }
223
224 kind_checker.check(ty);
225
226 if !kind_checker.symbols.is_empty() {
227 continue;
228 }
229 if kind_checker.functions.is_empty() {
230 continue;
231 }
232
233 let label_details = ty.describe().or_else(|| Some("any".into()));
234 let base = Completion {
235 kind: CompletionKind::Func,
236 label_details,
237 apply: Some("".into()),
238 command: self
240 .worker
241 .ctx
242 .analysis
243 .trigger_on_snippet_with_param_hint(true)
244 .map(From::from),
245 ..Default::default()
246 };
247 let fn_feat = FnCompletionFeat::default().check(kind_checker.functions.iter().copied());
248
249 crate::log_debug_ct!("fn_feat: {name} {ty:?} -> {fn_feat:?}");
250
251 if fn_feat.min_pos() < 1 || !fn_feat.next_arg_is_content {
252 continue;
253 }
254 crate::log_debug_ct!("checked ufcs: {ty:?}");
255 if self.worker.ctx.analysis.completion_feat.ufcs() && fn_feat.min_pos() == 1 {
256 let before = EcoTextEdit {
257 range: self.cursor.lsp_range_of(rng.start..rng.start),
258 new_text: eco_format!("{name}{lb}"),
259 };
260 let after = EcoTextEdit {
261 range: self.cursor.lsp_range_of(rng.end..self.cursor.from),
262 new_text: rb.into(),
263 };
264
265 self.push_completion(Completion {
266 label: name.clone(),
267 additional_text_edits: Some(vec![before, after]),
268 ..base.clone()
269 });
270 }
271 let more_args = fn_feat.min_pos() > 1 || fn_feat.min_named() > 0;
272 if self.worker.ctx.analysis.completion_feat.ufcs_left() && more_args {
273 let node_content = SnippetEscape(node.get().clone().into_text());
274 let before = EcoTextEdit {
275 range: self.cursor.lsp_range_of(rng.start..self.cursor.from),
276 new_text: eco_format!("{name}{lb}"),
277 };
278 self.push_completion(Completion {
279 apply: if is_content_block {
280 Some(eco_format!("(${{}}){node_content}"))
281 } else {
282 Some(eco_format!("${{}}, {node_content})"))
283 },
284 label: eco_format!("{name}("),
285 additional_text_edits: Some(vec![before]),
286 ..base.clone()
287 });
288 }
289 if self.worker.ctx.analysis.completion_feat.ufcs_right() && more_args {
290 let before = EcoTextEdit {
291 range: self.cursor.lsp_range_of(rng.start..rng.start),
292 new_text: eco_format!("{name}("),
293 };
294 let after = EcoTextEdit {
295 range: self.cursor.lsp_range_of(rng.end..self.cursor.from),
296 new_text: "".into(),
297 };
298 self.push_completion(Completion {
299 apply: Some(eco_format!("${{}})")),
300 label: eco_format!("{name})"),
301 additional_text_edits: Some(vec![before, after]),
302 ..base
303 });
304 }
305 }
306 }
307}
308
309struct SnippetEscape(EcoString);
311
312impl fmt::Display for SnippetEscape {
313 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
314 let escaped = self
316 .0
317 .replace("\\", "\\\\")
318 .replace("$", "\\$")
319 .replace("}", "\\}");
320 write!(f, "{escaped}")
321 }
322}