tinymist_query/completion/
snippet.rs

1use std::collections::HashSet;
2use std::sync::OnceLock;
3
4use ecow::{EcoString, eco_format};
5use serde::de::DeserializeOwned;
6use serde::{Deserialize, Deserializer, Serialize};
7use strum::IntoEnumIterator;
8
9use crate::adt::interner::Interned;
10use crate::prelude::*;
11use crate::syntax::{InterpretMode, SurroundingSyntax};
12
13/// This is the poorman's type filter, which is less powerful but more steady.
14#[derive(Debug, Clone, Serialize, Deserialize)]
15pub enum PostfixSnippetScope {
16    /// Any "dottable" value, i.e. having type `Ty::Any`.
17    Value,
18    /// Any value having content type, i.e. having type `Ty::Content`.
19    Content,
20}
21
22#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
23#[serde(rename_all = "camelCase")]
24pub enum CompletionCommand {
25    TriggerSuggest,
26}
27
28#[derive(Debug, Clone, PartialEq, Eq, Hash)]
29pub enum ContextSelector<T> {
30    Positive(Option<T>),
31    Negative(Vec<T>),
32}
33
34impl<T> Default for ContextSelector<T> {
35    fn default() -> Self {
36        ContextSelector::Positive(None)
37    }
38}
39
40impl<'de, T> Deserialize<'de> for ContextSelector<T>
41where
42    T: DeserializeOwned,
43{
44    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
45    where
46        D: Deserializer<'de>,
47    {
48        let value = serde_json::Value::deserialize(deserializer)?;
49        match value {
50            serde_json::Value::Null => Ok(ContextSelector::Positive(None)),
51            serde_json::Value::Object(map) => {
52                let negative = map
53                    .get("negative")
54                    .ok_or_else(|| serde::de::Error::custom("missing field `negative`"))?;
55                let negative = serde_json::from_value(negative.clone())
56                    .map_err(|err| serde::de::Error::custom(err.to_string()))?;
57                Ok(ContextSelector::Negative(negative))
58            }
59            _ => {
60                let value = serde_json::from_value(value)
61                    .map_err(|err| serde::de::Error::custom(err.to_string()))?;
62                Ok(ContextSelector::Positive(Some(value)))
63            }
64        }
65    }
66}
67
68impl<T> Serialize for ContextSelector<T>
69where
70    T: Serialize,
71{
72    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
73    where
74        S: serde::ser::Serializer,
75    {
76        match self {
77            ContextSelector::Positive(value) => {
78                if let Some(value) = value {
79                    value.serialize(serializer)
80                } else {
81                    serde_json::Value::Null.serialize(serializer)
82                }
83            }
84            ContextSelector::Negative(value) => {
85                let mut map = serde_json::Map::new();
86                map.insert("negative".into(), serde_json::to_value(value).unwrap());
87                serde_json::Value::Object(map).serialize(serializer)
88            }
89        }
90    }
91}
92
93#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq, Hash)]
94pub struct CompletionContext {
95    /// The mode in which the snippet is applicable.
96    pub mode: ContextSelector<InterpretMode>,
97    /// The syntax in which the snippet is applicable.
98    pub syntax: ContextSelector<SurroundingSyntax>,
99}
100
101#[derive(Debug, Clone, PartialEq, Eq, Hash)]
102pub struct CompletionContextKeyRepr {
103    /// The mode in which the snippet is applicable.
104    pub mode: Option<InterpretMode>,
105    /// The syntax in which the snippet is applicable.
106    pub syntax: Option<SurroundingSyntax>,
107}
108
109crate::adt::interner::impl_internable!(CompletionContextKeyRepr,);
110
111#[derive(Debug, Clone, PartialEq, Eq, Hash)]
112pub struct CompletionContextKey(Interned<CompletionContextKeyRepr>);
113
114impl CompletionContextKey {
115    /// Creates a new completion context key.
116    pub fn new(mode: Option<InterpretMode>, syntax: Option<SurroundingSyntax>) -> Self {
117        CompletionContextKey(Interned::new(CompletionContextKeyRepr { mode, syntax }))
118    }
119}
120
121/// A parsed snippet
122#[derive(Debug, Clone, Serialize, Deserialize)]
123pub struct ParsedSnippet {
124    pub node_before: EcoString,
125    pub node_before_before_cursor: Option<EcoString>,
126    pub node_after: EcoString,
127}
128
129/// A postfix completion snippet.
130#[derive(Debug, Clone, Serialize, Deserialize)]
131pub struct PostfixSnippet {
132    /// The mode in which the snippet is applicable.
133    pub mode: EcoVec<InterpretMode>,
134    /// The scope in which the snippet is applicable.
135    pub scope: PostfixSnippetScope,
136    /// The snippet name.
137    pub label: EcoString,
138    /// The detailed snippet name shown in UI (might be truncated).
139    pub label_detail: Option<EcoString>,
140    /// The snippet content.
141    pub snippet: EcoString,
142    /// The snippet description.
143    pub description: EcoString,
144    /// Lazily parsed snippet.
145    #[serde(skip)]
146    pub parsed_snippet: OnceLock<Option<ParsedSnippet>>,
147}
148
149/// A prefix completion snippet.
150#[derive(Debug, Clone, Default, Serialize, Deserialize)]
151pub struct PrefixSnippet {
152    /// The mode in which the snippet is applicable.
153    pub context: EcoVec<CompletionContext>,
154    /// The snippet name.
155    pub label: EcoString,
156    /// The detailed snippet name shown in UI (might be truncated).
157    pub label_detail: Option<EcoString>,
158    /// The snippet content.
159    pub snippet: EcoString,
160    /// The snippet description.
161    pub description: EcoString,
162    /// The command to execute.
163    pub command: Option<CompletionCommand>,
164    /// Lazily expanded context.
165    #[serde(skip)]
166    pub expanded_context: OnceLock<HashSet<CompletionContextKey>>,
167}
168crate::adt::interner::impl_internable!(PrefixSnippet,);
169
170impl std::hash::Hash for PrefixSnippet {
171    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
172        self.context.hash(state);
173        self.label.hash(state);
174    }
175}
176
177impl std::cmp::Eq for PrefixSnippet {}
178impl std::cmp::PartialEq for PrefixSnippet {
179    fn eq(&self, other: &Self) -> bool {
180        self.context == other.context && self.label == other.label
181    }
182}
183
184impl PrefixSnippet {
185    pub(crate) fn applies_to(&self, context_key: &CompletionContextKey) -> bool {
186        self.expanded_context
187            .get_or_init(|| {
188                let mut set = HashSet::new();
189                for context in &self.context {
190                    let modes = match &context.mode {
191                        ContextSelector::Positive(mode) => vec![*mode],
192                        ContextSelector::Negative(modes) => {
193                            let all_modes = InterpretMode::iter()
194                                .filter(|mode| !modes.iter().any(|m| m == mode));
195                            all_modes.map(Some).collect()
196                        }
197                    };
198                    let syntaxes = match &context.syntax {
199                        ContextSelector::Positive(syntax) => vec![*syntax],
200                        ContextSelector::Negative(syntaxes) => {
201                            let all_syntaxes = SurroundingSyntax::iter()
202                                .filter(|syntax| !syntaxes.iter().any(|s| s == syntax));
203                            all_syntaxes.map(Some).collect()
204                        }
205                    };
206                    for mode in &modes {
207                        for syntax in &syntaxes {
208                            set.insert(CompletionContextKey::new(*mode, *syntax));
209                        }
210                    }
211                }
212                set
213            })
214            .contains(context_key)
215    }
216}
217
218struct ConstPrefixSnippet {
219    context: InterpretMode,
220    label: &'static str,
221    snippet: &'static str,
222    description: &'static str,
223}
224
225impl From<&ConstPrefixSnippet> for Interned<PrefixSnippet> {
226    fn from(snippet: &ConstPrefixSnippet) -> Self {
227        Interned::new(PrefixSnippet {
228            context: eco_vec![CompletionContext {
229                mode: ContextSelector::Positive(Some(snippet.context)),
230                syntax: ContextSelector::Positive(None),
231            }],
232            label: snippet.label.into(),
233            label_detail: None,
234            snippet: snippet.snippet.into(),
235            description: snippet.description.into(),
236            command: None,
237            expanded_context: OnceLock::new(),
238        })
239    }
240}
241
242struct ConstPrefixSnippetWithSuggest {
243    context: InterpretMode,
244    label: &'static str,
245    snippet: &'static str,
246    description: &'static str,
247}
248
249impl From<&ConstPrefixSnippetWithSuggest> for Interned<PrefixSnippet> {
250    fn from(snippet: &ConstPrefixSnippetWithSuggest) -> Self {
251        Interned::new(PrefixSnippet {
252            context: eco_vec![CompletionContext {
253                mode: ContextSelector::Positive(Some(snippet.context)),
254                syntax: ContextSelector::Positive(None),
255            }],
256            label: snippet.label.into(),
257            label_detail: None,
258            snippet: snippet.snippet.into(),
259            description: snippet.description.into(),
260            command: Some(CompletionCommand::TriggerSuggest),
261            expanded_context: OnceLock::new(),
262        })
263    }
264}
265
266pub static DEFAULT_PREFIX_SNIPPET: LazyLock<Vec<Interned<PrefixSnippet>>> = LazyLock::new(|| {
267    const SNIPPETS: &[ConstPrefixSnippet] = &[
268        ConstPrefixSnippet {
269            context: InterpretMode::Code,
270            label: "function call",
271            snippet: "${function}(${arguments})[${body}]",
272            description: "Evaluates a function.",
273        },
274        ConstPrefixSnippet {
275            context: InterpretMode::Code,
276            label: "code block",
277            snippet: "{ ${} }",
278            description: "Inserts a nested code block.",
279        },
280        ConstPrefixSnippet {
281            context: InterpretMode::Code,
282            label: "content block",
283            snippet: "[${content}]",
284            description: "Switches into markup mode.",
285        },
286        ConstPrefixSnippet {
287            context: InterpretMode::Code,
288            label: "set rule",
289            snippet: "set ${}",
290            description: "Sets style properties on an element.",
291        },
292        ConstPrefixSnippet {
293            context: InterpretMode::Code,
294            label: "show rule",
295            snippet: "show ${}",
296            description: "Redefines the look of an element.",
297        },
298        ConstPrefixSnippet {
299            context: InterpretMode::Code,
300            label: "show rule (everything)",
301            snippet: "show: ${}",
302            description: "Transforms everything that follows.",
303        },
304        ConstPrefixSnippet {
305            context: InterpretMode::Code,
306            label: "context expression",
307            snippet: "context ${}",
308            description: "Provides contextual data.",
309        },
310        ConstPrefixSnippet {
311            context: InterpretMode::Code,
312            label: "let binding",
313            snippet: "let ${name} = ${value}",
314            description: "Saves a value in a variable.",
315        },
316        ConstPrefixSnippet {
317            context: InterpretMode::Code,
318            label: "let binding (function)",
319            snippet: "let ${name}(${params}) = ${output}",
320            description: "Defines a function.",
321        },
322        ConstPrefixSnippet {
323            context: InterpretMode::Code,
324            label: "if conditional",
325            snippet: "if ${1 < 2} {\n\t${}\n}",
326            description: "Computes or inserts something conditionally.",
327        },
328        ConstPrefixSnippet {
329            context: InterpretMode::Code,
330            label: "if-else conditional",
331            snippet: "if ${1 < 2} {\n\t${}\n} else {\n\t${}\n}",
332            description: "Computes or inserts different things based on a condition.",
333        },
334        ConstPrefixSnippet {
335            context: InterpretMode::Code,
336            label: "while loop",
337            snippet: "while ${1 < 2} {\n\t${}\n}",
338            description: "Computes or inserts something while a condition is met.",
339        },
340        ConstPrefixSnippet {
341            context: InterpretMode::Code,
342            label: "for loop",
343            snippet: "for ${value} in ${(1, 2, 3)} {\n\t${}\n}",
344            description: "Computes or inserts something for each value in a collection.",
345        },
346        ConstPrefixSnippet {
347            context: InterpretMode::Code,
348            label: "for loop (with key)",
349            snippet: "for (${key}, ${value}) in ${(a: 1, b: 2)} {\n\t${}\n}",
350            description: "Computes or inserts something for each key and value in a collection.",
351        },
352        ConstPrefixSnippet {
353            context: InterpretMode::Code,
354            label: "break",
355            snippet: "break",
356            description: "Exits early from a loop.",
357        },
358        ConstPrefixSnippet {
359            context: InterpretMode::Code,
360            label: "continue",
361            snippet: "continue",
362            description: "Continues with the next iteration of a loop.",
363        },
364        ConstPrefixSnippet {
365            context: InterpretMode::Code,
366            label: "return",
367            snippet: "return ${output}",
368            description: "Returns early from a function.",
369        },
370        ConstPrefixSnippet {
371            context: InterpretMode::Code,
372            label: "array literal",
373            snippet: "(${1, 2, 3})",
374            description: "Creates a sequence of values.",
375        },
376        ConstPrefixSnippet {
377            context: InterpretMode::Code,
378            label: "dictionary literal",
379            snippet: "(${a: 1, b: 2})",
380            description: "Creates a mapping from names to value.",
381        },
382        ConstPrefixSnippet {
383            context: InterpretMode::Math,
384            label: "subscript",
385            snippet: "${x}_${2:2}",
386            description: "Sets something in subscript.",
387        },
388        ConstPrefixSnippet {
389            context: InterpretMode::Math,
390            label: "superscript",
391            snippet: "${x}^${2:2}",
392            description: "Sets something in superscript.",
393        },
394        ConstPrefixSnippet {
395            context: InterpretMode::Math,
396            label: "fraction",
397            snippet: "${x}/${y}",
398            description: "Inserts a fraction.",
399        },
400        ConstPrefixSnippet {
401            context: InterpretMode::Markup,
402            label: "expression",
403            snippet: "#${}",
404            description: "Variables, function calls, blocks, and more.",
405        },
406        ConstPrefixSnippet {
407            context: InterpretMode::Markup,
408            label: "linebreak",
409            snippet: "\\\n${}",
410            description: "Inserts a forced linebreak.",
411        },
412        ConstPrefixSnippet {
413            context: InterpretMode::Markup,
414            label: "strong text",
415            snippet: "*${strong}*",
416            description: "Strongly emphasizes content by increasing the font weight.",
417        },
418        ConstPrefixSnippet {
419            context: InterpretMode::Markup,
420            label: "emphasized text",
421            snippet: "_${emphasized}_",
422            description: "Emphasizes content by setting it in italic font style.",
423        },
424        ConstPrefixSnippet {
425            context: InterpretMode::Markup,
426            label: "raw text",
427            snippet: "`${text}`",
428            description: "Displays text verbatim, in monospace.",
429        },
430        ConstPrefixSnippet {
431            context: InterpretMode::Markup,
432            label: "code listing",
433            snippet: "```${lang}\n${code}\n```",
434            description: "Inserts computer code with syntax highlighting.",
435        },
436        ConstPrefixSnippet {
437            context: InterpretMode::Markup,
438            label: "hyperlink",
439            snippet: "https://${example.com}",
440            description: "Links to a URL.",
441        },
442        ConstPrefixSnippet {
443            context: InterpretMode::Markup,
444            label: "label",
445            snippet: "<${name}>",
446            description: "Makes the preceding element referenceable.",
447        },
448        ConstPrefixSnippet {
449            context: InterpretMode::Markup,
450            label: "reference",
451            snippet: "@${name}",
452            description: "Inserts a reference to a label.",
453        },
454        ConstPrefixSnippet {
455            context: InterpretMode::Markup,
456            label: "heading",
457            snippet: "= ${title}",
458            description: "Inserts a section heading.",
459        },
460        ConstPrefixSnippet {
461            context: InterpretMode::Markup,
462            label: "list item",
463            snippet: "- ${item}",
464            description: "Inserts an item of a bullet list.",
465        },
466        ConstPrefixSnippet {
467            context: InterpretMode::Markup,
468            label: "enumeration item",
469            snippet: "+ ${item}",
470            description: "Inserts an item of a numbered list.",
471        },
472        ConstPrefixSnippet {
473            context: InterpretMode::Markup,
474            label: "enumeration item (numbered)",
475            snippet: "${number}. ${item}",
476            description: "Inserts an explicitly numbered list item.",
477        },
478        ConstPrefixSnippet {
479            context: InterpretMode::Markup,
480            label: "term list item",
481            snippet: "/ ${term}: ${description}",
482            description: "Inserts an item of a term list.",
483        },
484        ConstPrefixSnippet {
485            context: InterpretMode::Markup,
486            label: "math (inline)",
487            snippet: "$${x}$",
488            description: "Inserts an inline-level mathematical equation.",
489        },
490        ConstPrefixSnippet {
491            context: InterpretMode::Markup,
492            label: "math (block)",
493            snippet: "$ ${sum_x^2} $",
494            description: "Inserts a block-level mathematical equation.",
495        },
496    ];
497
498    const SNIPPET_SUGGEST: &[ConstPrefixSnippetWithSuggest] = &[
499        ConstPrefixSnippetWithSuggest {
500            context: InterpretMode::Code,
501            label: "import module",
502            snippet: "import \"${}\"",
503            description: "Imports module from another file.",
504        },
505        ConstPrefixSnippetWithSuggest {
506            context: InterpretMode::Code,
507            label: "import module by expression",
508            snippet: "import ${}",
509            description: "Imports items by expression.",
510        },
511        ConstPrefixSnippetWithSuggest {
512            context: InterpretMode::Code,
513            label: "import (package)",
514            snippet: "import \"@${}\"",
515            description: "Imports variables from another file.",
516        },
517        ConstPrefixSnippetWithSuggest {
518            context: InterpretMode::Code,
519            label: "include (file)",
520            snippet: "include \"${}\"",
521            description: "Includes content from another file.",
522        },
523        ConstPrefixSnippetWithSuggest {
524            context: InterpretMode::Code,
525            label: "include (package)",
526            snippet: "include \"@${}\"",
527            description: "Includes content from another file.",
528        },
529    ];
530
531    let snippets = SNIPPETS.iter().map(From::from);
532    let snippets2 = SNIPPET_SUGGEST.iter().map(From::from);
533    snippets.chain(snippets2).collect()
534});
535
536pub static DEFAULT_POSTFIX_SNIPPET: LazyLock<EcoVec<PostfixSnippet>> = LazyLock::new(|| {
537    eco_vec![
538        PostfixSnippet {
539            scope: PostfixSnippetScope::Content,
540            mode: eco_vec![InterpretMode::Code, InterpretMode::Markup],
541            label: eco_format!("text fill"),
542            label_detail: Some(eco_format!(".text fill")),
543            snippet: "text(fill: ${}, ${node})".into(),
544            description: eco_format!("wrap with text fill"),
545            parsed_snippet: OnceLock::new(),
546        },
547        PostfixSnippet {
548            scope: PostfixSnippetScope::Content,
549            mode: eco_vec![InterpretMode::Code, InterpretMode::Markup],
550            label: eco_format!("text size"),
551            label_detail: Some(eco_format!(".text size")),
552            snippet: "text(size: ${}, ${node})".into(),
553            description: eco_format!("wrap with text size"),
554            parsed_snippet: OnceLock::new(),
555        },
556        PostfixSnippet {
557            scope: PostfixSnippetScope::Content,
558            mode: eco_vec![InterpretMode::Code, InterpretMode::Markup],
559            label: eco_format!("align"),
560            label_detail: Some(eco_format!(".align")),
561            snippet: "align(${}, ${node})".into(),
562            description: eco_format!("wrap with alignment"),
563            parsed_snippet: OnceLock::new(),
564        },
565        PostfixSnippet {
566            scope: PostfixSnippetScope::Value,
567            mode: eco_vec![InterpretMode::Code, InterpretMode::Markup],
568            label: "if".into(),
569            label_detail: Some(".if".into()),
570            snippet: "if ${node} { ${} }".into(),
571            description: "wrap as if expression".into(),
572            parsed_snippet: OnceLock::new(),
573        },
574        PostfixSnippet {
575            scope: PostfixSnippetScope::Value,
576            mode: eco_vec![InterpretMode::Code, InterpretMode::Markup],
577            label: "else".into(),
578            label_detail: Some(".else".into()),
579            snippet: "if not ${node} { ${} }".into(),
580            description: "wrap as if not expression".into(),
581            parsed_snippet: OnceLock::new(),
582        },
583        PostfixSnippet {
584            scope: PostfixSnippetScope::Value,
585            mode: eco_vec![InterpretMode::Code, InterpretMode::Markup],
586            label: "none".into(),
587            label_detail: Some(".if none".into()),
588            snippet: "if ${node} == none { ${} }".into(),
589            description: "wrap as if expression to check none-ish".into(),
590            parsed_snippet: OnceLock::new(),
591        },
592        PostfixSnippet {
593            scope: PostfixSnippetScope::Value,
594            mode: eco_vec![InterpretMode::Code, InterpretMode::Markup],
595            label: "notnone".into(),
596            label_detail: Some(".if not none".into()),
597            snippet: "if ${node} != none { ${} }".into(),
598            description: "wrap as if expression to check none-ish".into(),
599            parsed_snippet: OnceLock::new(),
600        },
601        PostfixSnippet {
602            scope: PostfixSnippetScope::Value,
603            mode: eco_vec![InterpretMode::Code, InterpretMode::Markup],
604            label: "return".into(),
605            label_detail: Some(".return".into()),
606            snippet: "return ${node}".into(),
607            description: "wrap as return expression".into(),
608            parsed_snippet: OnceLock::new(),
609        },
610        PostfixSnippet {
611            scope: PostfixSnippetScope::Value,
612            mode: eco_vec![InterpretMode::Code, InterpretMode::Markup],
613            label: "tup".into(),
614            label_detail: Some(".tup".into()),
615            snippet: "(${node}, ${})".into(),
616            description: "wrap as tuple (array) expression".into(),
617            parsed_snippet: OnceLock::new(),
618        },
619        PostfixSnippet {
620            scope: PostfixSnippetScope::Value,
621            mode: eco_vec![InterpretMode::Code, InterpretMode::Markup],
622            label: "let".into(),
623            label_detail: Some(".let".into()),
624            snippet: "let ${_} = ${node}".into(),
625            description: "wrap as let expression".into(),
626            parsed_snippet: OnceLock::new(),
627        },
628        PostfixSnippet {
629            scope: PostfixSnippetScope::Value,
630            mode: eco_vec![InterpretMode::Code, InterpretMode::Markup],
631            label: "in".into(),
632            label_detail: Some(".in".into()),
633            snippet: "${_} in ${node}".into(),
634            description: "wrap with in expression".into(),
635            parsed_snippet: OnceLock::new(),
636        },
637    ]
638});