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#[derive(Debug, Clone, Serialize, Deserialize)]
15pub enum PostfixSnippetScope {
16 Value,
18 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 pub mode: ContextSelector<InterpretMode>,
97 pub syntax: ContextSelector<SurroundingSyntax>,
99}
100
101#[derive(Debug, Clone, PartialEq, Eq, Hash)]
102pub struct CompletionContextKeyRepr {
103 pub mode: Option<InterpretMode>,
105 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 pub fn new(mode: Option<InterpretMode>, syntax: Option<SurroundingSyntax>) -> Self {
117 CompletionContextKey(Interned::new(CompletionContextKeyRepr { mode, syntax }))
118 }
119}
120
121#[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#[derive(Debug, Clone, Serialize, Deserialize)]
131pub struct PostfixSnippet {
132 pub mode: EcoVec<InterpretMode>,
134 pub scope: PostfixSnippetScope,
136 pub label: EcoString,
138 pub label_detail: Option<EcoString>,
140 pub snippet: EcoString,
142 pub description: EcoString,
144 #[serde(skip)]
146 pub parsed_snippet: OnceLock<Option<ParsedSnippet>>,
147}
148
149#[derive(Debug, Clone, Default, Serialize, Deserialize)]
151pub struct PrefixSnippet {
152 pub context: EcoVec<CompletionContext>,
154 pub label: EcoString,
156 pub label_detail: Option<EcoString>,
158 pub snippet: EcoString,
160 pub description: EcoString,
162 pub command: Option<CompletionCommand>,
164 #[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});