tinymist_query/analysis/completion/
snippet.rs

1//! Snippet completions.
2//!
3//! A prefix snippet is a snippet that completes non-existing items. For example
4//! `RR` is completed as `ℝ`.
5//!
6//! A postfix snippet is a snippet that modifies existing items by the dot
7//! accessor syntax. For example `$ RR.abs| $` is completed as `$ abs(RR) $`.
8
9use core::fmt;
10
11use super::*;
12
13impl CompletionPair<'_, '_, '_> {
14    /// Add a (prefix) snippet completion.
15    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                // range: Some(range),
153                ..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    /// Make ufcs-style completions. Note: you must check that node is a content
190    /// before calling this. Todo: ufcs completions for other types.
191    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        // we don't check literal type here for faster completion
218        for (name, ty) in &defines.defines {
219            // todo: filter ty
220            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                // range: Some(range),
239                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
309// https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#snippet_syntax
310struct SnippetEscape(EcoString);
311
312impl fmt::Display for SnippetEscape {
313    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
314        // todo: poor performance
315        let escaped = self
316            .0
317            .replace("\\", "\\\\")
318            .replace("$", "\\$")
319            .replace("}", "\\}");
320        write!(f, "{escaped}")
321    }
322}