1use std::cmp::Reverse;
4use std::collections::{BTreeMap, HashSet};
5use std::ops::Range;
6
7use ecow::{EcoString, eco_format};
8use lsp_types::InsertTextFormat;
9use regex::{Captures, Regex};
10use serde::{Deserialize, Serialize};
11use tinymist_analysis::syntax::{BadCompletionCursor, bad_completion_cursor};
12use tinymist_analysis::{DynLabel, analyze_labels, func_signature};
13use tinymist_derive::BindTyCtx;
14use tinymist_project::LspWorld;
15use tinymist_std::path::unix_slash;
16use tinymist_std::typst::TypstDocument;
17use typst::World;
18use typst::foundations::{
19 AutoValue, Func, NoneValue, Repr, Scope, StyleChain, Type, Value, fields_on, format_str, repr,
20};
21use typst::syntax::ast::{self, AstNode, Param};
22use typst::syntax::{is_id_continue, is_id_start, is_ident};
23use typst::text::RawElem;
24use typst_shim::{syntax::LinkedNodeExt, utils::hash128};
25use unscanny::Scanner;
26
27use crate::adt::interner::Interned;
28use crate::analysis::{BuiltinTy, LocalContext, PathKind, Ty};
29use crate::completion::{
30 Completion, CompletionCommand, CompletionContextKey, CompletionItem, CompletionKind,
31 DEFAULT_POSTFIX_SNIPPET, DEFAULT_PREFIX_SNIPPET, EcoCompletionTextEdit, EcoInsertReplaceEdit,
32 EcoTextEdit, ParsedSnippet, PostfixSnippet, PostfixSnippetScope, PrefixSnippet,
33};
34use crate::prelude::*;
35use crate::syntax::{
36 InterpretMode, PreviousDecl, SurroundingSyntax, SyntaxClass, SyntaxContext, VarClass,
37 classify_context, interpret_mode_at, is_ident_like, node_ancestors, previous_decls,
38 surrounding_syntax,
39};
40use crate::ty::{
41 DynTypeBounds, Iface, IfaceChecker, InsTy, SigTy, TyCtx, TypeInfo, TypeInterface, TypeVar,
42};
43use crate::upstream::{plain_docs_sentence, summarize_font_family};
44
45use super::SharedContext;
46
47mod field_access;
48mod func;
49mod import;
50mod kind;
51mod mode;
52mod param;
53mod path;
54mod scope;
55mod snippet;
56#[path = "completion/type.rs"]
57mod type_;
58mod typst_specific;
59use kind::*;
60use scope::*;
61use type_::*;
62
63type LspCompletion = CompletionItem;
64
65#[derive(Default, Debug, Clone, Serialize, Deserialize)]
67#[serde(rename_all = "camelCase")]
68pub struct CompletionFeat {
69 #[serde(default, deserialize_with = "deserialize_null_default")]
71 pub trigger_on_snippet_placeholders: bool,
72 #[serde(default, deserialize_with = "deserialize_null_default")]
74 pub trigger_suggest: bool,
75 #[serde(default, deserialize_with = "deserialize_null_default")]
77 pub trigger_parameter_hints: bool,
78 #[serde(default, deserialize_with = "deserialize_null_default")]
81 pub trigger_suggest_and_parameter_hints: bool,
82 #[serde(skip)]
84 pub insert_replace_edit: bool,
85 #[serde(skip)]
87 pub path_completion_by_filesystem: bool,
88
89 pub symbol: Option<SymbolCompletionWay>,
91
92 pub postfix: Option<bool>,
94 pub postfix_ufcs: Option<bool>,
96 pub postfix_ufcs_left: Option<bool>,
98 pub postfix_ufcs_right: Option<bool>,
100 pub postfix_snippets: Option<EcoVec<PostfixSnippet>>,
102}
103
104impl CompletionFeat {
105 pub(crate) fn postfix(&self) -> bool {
107 self.postfix.unwrap_or(true)
108 }
109
110 pub(crate) fn any_ufcs(&self) -> bool {
112 self.ufcs() || self.ufcs_left() || self.ufcs_right()
113 }
114
115 pub(crate) fn ufcs(&self) -> bool {
117 self.postfix() && self.postfix_ufcs.unwrap_or(true)
118 }
119
120 pub(crate) fn ufcs_left(&self) -> bool {
122 self.postfix() && self.postfix_ufcs_left.unwrap_or(true)
123 }
124
125 pub(crate) fn ufcs_right(&self) -> bool {
127 self.postfix() && self.postfix_ufcs_right.unwrap_or(true)
128 }
129
130 pub(crate) fn postfix_snippets(&self) -> &EcoVec<PostfixSnippet> {
132 self.postfix_snippets
133 .as_ref()
134 .unwrap_or(&DEFAULT_POSTFIX_SNIPPET)
135 }
136
137 pub(crate) fn is_stepless(&self) -> bool {
138 matches!(self.symbol, Some(SymbolCompletionWay::Stepless))
139 }
140}
141
142#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
146#[serde(rename_all = "camelCase")]
147pub enum SymbolCompletionWay {
148 Step,
150 Stepless,
152}
153
154pub struct CompletionCursor<'a> {
156 ctx: Arc<SharedContext>,
158 from: usize,
160 cursor: usize,
162 source: Source,
164 text: &'a str,
166 before: &'a str,
168 after: &'a str,
170 leaf: LinkedNode<'a>,
172 syntax: Option<SyntaxClass<'a>>,
174 syntax_context: Option<SyntaxContext<'a>>,
176 surrounding_syntax: SurroundingSyntax,
178
179 last_lsp_range_pair: Option<(Range<usize>, LspRange)>,
181 ident_cursor: OnceLock<Option<SelectedNode<'a>>>,
183 arg_cursor: OnceLock<Option<SyntaxNode>>,
185}
186
187impl<'a> CompletionCursor<'a> {
188 pub fn new(ctx: Arc<SharedContext>, source: &'a Source, cursor: usize) -> Option<Self> {
190 let text = source.text();
191 let root = LinkedNode::new(source.root());
192 let leaf = root.leaf_at_compat(cursor)?;
193 let syntax = classify_syntax(leaf.clone(), cursor);
195 let syntax_context = classify_context(leaf.clone(), Some(cursor));
196 let surrounding_syntax = surrounding_syntax(&leaf);
197
198 crate::log_debug_ct!("CompletionCursor: syntax {leaf:?} -> {syntax:#?}");
199 crate::log_debug_ct!("CompletionCursor: context {leaf:?} -> {syntax_context:#?}");
200 crate::log_debug_ct!("CompletionCursor: surrounding {leaf:?} -> {surrounding_syntax:#?}");
201 Some(Self {
202 ctx,
203 text,
204 source: source.clone(),
205 before: &text[..cursor],
206 after: &text[cursor..],
207 leaf,
208 syntax,
209 syntax_context,
210 surrounding_syntax,
211 cursor,
212 from: cursor,
213 last_lsp_range_pair: None,
214 ident_cursor: OnceLock::new(),
215 arg_cursor: OnceLock::new(),
216 })
217 }
218
219 fn before_window(&self, size: usize) -> &str {
221 slice_at(
222 self.before,
223 self.cursor.saturating_sub(size)..self.before.len(),
224 )
225 }
226
227 fn is_callee(&self) -> bool {
229 matches!(self.syntax, Some(SyntaxClass::Callee(..)))
230 }
231
232 pub fn leaf_mode(&self) -> InterpretMode {
234 interpret_mode_at(Some(&self.leaf))
235 }
236
237 fn selected_node(&self) -> &Option<SelectedNode<'a>> {
239 self.ident_cursor.get_or_init(|| {
240 let is_from_ident = matches!(
243 self.syntax,
244 Some(SyntaxClass::Callee(..) | SyntaxClass::VarAccess(..))
245 ) && is_ident_like(&self.leaf)
246 && self.leaf.offset() == self.from;
247 if is_from_ident {
248 return Some(SelectedNode::Ident(self.leaf.clone()));
249 }
250
251 let is_from_label = matches!(self.syntax, Some(SyntaxClass::Label { .. }))
254 && self.leaf.offset() + 1 == self.from;
255 if is_from_label {
256 return Some(SelectedNode::Label(self.leaf.clone()));
257 }
258
259 let is_from_ref = matches!(self.syntax, Some(SyntaxClass::Ref { .. }))
262 && self.leaf.offset() + 1 == self.from;
263 if is_from_ref {
264 return Some(SelectedNode::Ref(self.leaf.clone()));
265 }
266
267 let is_from_ref = matches!(self.syntax, Some(SyntaxClass::At { .. }))
270 && self.leaf.offset() + 1 == self.from;
271 if is_from_ref {
272 return Some(SelectedNode::At(self.leaf.clone()));
273 }
274
275 None
276 })
277 }
278
279 fn arg_cursor(&self) -> &Option<SyntaxNode> {
281 self.arg_cursor.get_or_init(|| {
282 let mut args_node = None;
283
284 match self.syntax_context.clone() {
285 Some(SyntaxContext::Arg { args, .. }) => {
286 args_node = Some(args.get().clone());
287 }
288 Some(SyntaxContext::Normal(node))
289 if (matches!(node.kind(), SyntaxKind::ContentBlock)
290 && matches!(self.leaf.kind(), SyntaxKind::LeftBracket)) =>
291 {
292 args_node = node.parent().map(|s| s.get().clone());
293 }
294 Some(
295 SyntaxContext::Element { .. }
296 | SyntaxContext::ImportPath(..)
297 | SyntaxContext::IncludePath(..)
298 | SyntaxContext::VarAccess(..)
299 | SyntaxContext::Paren { .. }
300 | SyntaxContext::Label { .. }
301 | SyntaxContext::Ref { .. }
302 | SyntaxContext::At { .. }
303 | SyntaxContext::Normal(..),
304 )
305 | None => {}
306 }
307
308 args_node
309 })
310 }
311
312 fn lsp_range_of(&mut self, rng: Range<usize>) -> LspRange {
314 if let Some((last_rng, last_lsp_rng)) = &self.last_lsp_range_pair
316 && *last_rng == rng
317 {
318 return *last_lsp_rng;
319 }
320
321 let lsp_rng = self.ctx.to_lsp_range(rng.clone(), &self.source);
322 self.last_lsp_range_pair = Some((rng, lsp_rng));
323 lsp_rng
324 }
325
326 fn insert_range_of(&self, replace: Range<usize>) -> Range<usize> {
327 let end = self.cursor.clamp(replace.start, replace.end);
328 replace.start..end
329 }
330
331 fn completion_text_edit(
332 &mut self,
333 insert: Range<usize>,
334 replace: Range<usize>,
335 new_text: EcoString,
336 ) -> EcoCompletionTextEdit {
337 let insert = self.lsp_range_of(insert);
338 let replace = self.lsp_range_of(replace);
339
340 if self.ctx.analysis.completion_feat.insert_replace_edit && insert != replace {
341 EcoInsertReplaceEdit::new(insert, replace, new_text).into()
342 } else {
343 EcoTextEdit::new(replace, new_text).into()
344 }
345 }
346
347 fn capture_suffix_as_first_arg(&self, snippet: &mut EcoString, replace: Range<usize>) -> bool {
348 if self.cursor >= replace.end {
349 return false;
350 }
351
352 let suffix = &self.text[self.cursor..replace.end];
353 if suffix.is_empty() || !suffix.chars().all(is_id_continue) {
354 return false;
355 }
356
357 fill_first_empty_placeholder(snippet, suffix)
358 }
359
360 fn string_content_range(&self) -> Option<Range<usize>> {
361 if !self.leaf.is::<ast::Str>() {
362 return None;
363 }
364
365 let str_range = self.leaf.range();
366 if str_range.end <= str_range.start + 1 {
367 return None;
368 }
369
370 let content_range = str_range.start + 1..str_range.end - 1;
371 if self.cursor == content_range.end || content_range.contains(&self.cursor) {
372 Some(content_range)
373 } else {
374 None
375 }
376 }
377
378 fn lsp_item_of(&mut self, item: &Completion) -> LspCompletion {
380 let mut snippet = item.apply.as_ref().unwrap_or(&item.label).clone();
382 let mut replace_range = match self.selected_node() {
383 Some(SelectedNode::Ident(from_ident)) => {
384 let mut rng = from_ident.range();
385
386 if !self.is_callee() && self.cursor != rng.end && is_arg_like_context(from_ident) {
388 if !snippet.trim_end().ends_with(',') {
390 snippet.push_str(", ");
391 }
392
393 rng.end = self.cursor;
395 }
396
397 rng
398 }
399 Some(SelectedNode::Label(from_label)) => {
400 let mut rng = from_label.range();
401 if from_label.leaf_text().starts_with('<') && !snippet.starts_with('<') {
402 rng.start += 1;
403 }
404 if from_label.leaf_text().ends_with('>') && !snippet.ends_with('>') {
405 rng.end -= 1;
406 }
407
408 rng
409 }
410 Some(node @ (SelectedNode::At(from_ref) | SelectedNode::Ref(from_ref))) => {
411 let mut rng = if matches!(node, SelectedNode::At(..)) {
412 let offset = from_ref.offset();
413 offset..offset + 1
414 } else {
415 from_ref.range()
416 };
417 if from_ref.leaf_text().starts_with('@') && !snippet.starts_with('@') {
418 rng.start += 1;
419 }
420
421 rng
422 }
423 None => self.from..self.cursor,
424 };
425 if let Some(range) = self.string_content_range() {
426 if let Some(trimmed) = snippet.strip_prefix('"') {
427 snippet = trimmed.into();
428 }
429 if let Some(trimmed) = snippet.strip_suffix('"') {
430 snippet = trimmed.into();
431 }
432 replace_range = range;
433 }
434
435 let text_edit = if item.capture_suffix
436 && self.capture_suffix_as_first_arg(&mut snippet, replace_range.clone())
437 {
438 let replace_range = self.lsp_range_of(replace_range);
439 EcoTextEdit::new(replace_range, snippet).into()
440 } else {
441 let insert_range = self.insert_range_of(replace_range.clone());
442 self.completion_text_edit(insert_range, replace_range, snippet)
443 };
444
445 LspCompletion {
446 label: item.label.clone(),
447 kind: item.kind.clone(),
448 detail: item.detail.clone(),
449 sort_text: item.sort_text.clone(),
450 filter_text: item.filter_text.clone(),
451 label_details: item.label_details.clone().map(From::from),
452 text_edit: Some(text_edit),
453 additional_text_edits: item.additional_text_edits.clone(),
454 insert_text_format: Some(InsertTextFormat::SNIPPET),
455 command: item.command.clone(),
456 ..Default::default()
457 }
458 }
459}
460
461type Cursor<'a> = CompletionCursor<'a>;
463
464enum SelectedNode<'a> {
466 Ident(LinkedNode<'a>),
468 Label(LinkedNode<'a>),
470 Ref(LinkedNode<'a>),
472 At(LinkedNode<'a>),
474}
475
476pub struct CompletionWorker<'a> {
488 pub completions: Vec<LspCompletion>,
490 pub incomplete: bool,
492
493 ctx: &'a mut LocalContext,
495 document: Option<&'a TypstDocument>,
497 explicit: bool,
499 trigger_character: Option<char>,
501 seen_casts: HashSet<u128>,
503 seen_types: HashSet<Ty>,
505 seen_fields: HashSet<Interned<str>>,
507}
508
509impl<'a> CompletionWorker<'a> {
510 pub fn new(
512 ctx: &'a mut LocalContext,
513 document: Option<&'a TypstDocument>,
514 explicit: bool,
515 trigger_character: Option<char>,
516 ) -> Option<Self> {
517 Some(Self {
518 ctx,
519 document,
520 trigger_character,
521 explicit,
522 incomplete: true,
523 completions: vec![],
524 seen_casts: HashSet::new(),
525 seen_types: HashSet::new(),
526 seen_fields: HashSet::new(),
527 })
528 }
529
530 pub fn world(&self) -> &LspWorld {
532 self.ctx.world()
533 }
534
535 fn seen_field(&mut self, field: Interned<str>) -> bool {
536 !self.seen_fields.insert(field)
537 }
538
539 fn enrich(&mut self, prefix: &str, suffix: &str) {
541 for LspCompletion { text_edit, .. } in &mut self.completions {
542 let apply = match text_edit {
543 Some(text_edit) => text_edit.new_text_mut(),
544 _ => continue,
545 };
546
547 *apply = eco_format!("{prefix}{apply}{suffix}");
548 }
549 }
550
551 pub(crate) fn work(&mut self, cursor: &mut Cursor) -> Option<()> {
557 if let Some(SyntaxClass::VarAccess(var)) = &cursor.syntax {
559 let node = var.node();
560 match node.parent_kind() {
561 Some(SyntaxKind::LetBinding) => {
563 let parent = node.parent()?;
564 let parent_init = parent.cast::<ast::LetBinding>()?.init()?;
565 let parent_init = parent.find(parent_init.span())?;
566 parent_init.find(node.span())?;
567 }
568 Some(SyntaxKind::Closure) => {
569 let parent = node.parent()?;
570 let parent_body = parent.cast::<ast::Closure>()?.body();
571 let parent_body = parent.find(parent_body.span())?;
572 parent_body.find(node.span())?;
573 }
574 _ => {}
575 }
576 }
577
578 if matches!(
580 cursor.syntax,
581 Some(SyntaxClass::Callee(..) | SyntaxClass::VarAccess(..) | SyntaxClass::Normal(..))
582 ) && cursor.leaf.diagnosis().errors
583 {
584 let mut chars = cursor.leaf.leaf_text().chars();
585 match chars.next() {
586 Some(ch) if ch.is_numeric() => return None,
587 Some('.') => {
588 if matches!(chars.next(), Some(ch) if ch.is_numeric()) {
589 return None;
590 }
591 }
592 _ => {}
593 }
594 }
595
596 let self_ty = cursor.leaf.cast::<ast::Expr>().and_then(|leaf| {
599 let v = self.ctx.mini_eval(leaf)?;
600 Some(Ty::Value(InsTy::new(v)))
601 });
602
603 if let Some(self_ty) = self_ty {
604 self.seen_types.insert(self_ty);
605 };
606
607 let mut pair = Pair {
608 worker: self,
609 cursor,
610 };
611 let _ = pair.complete_cursor();
612
613 if let Some(SelectedNode::Ident(from_ident)) = cursor.selected_node() {
616 let ident_prefix = cursor.text[from_ident.offset()..cursor.cursor].to_string();
617
618 self.completions.retain(|item| {
619 let mut prefix_matcher = item.label.chars();
620 'ident_matching: for ch in ident_prefix.chars() {
621 for item in prefix_matcher.by_ref() {
622 if item == ch {
623 continue 'ident_matching;
624 }
625 }
626
627 return false;
628 }
629
630 true
631 });
632 }
633
634 for item in &mut self.completions {
635 if let Some(text_edit) = &mut item.text_edit {
636 let new_text = text_edit.new_text_mut();
637 *new_text = to_lsp_snippet(new_text);
638 }
639 }
640
641 Some(())
642 }
643}
644
645struct CompletionPair<'a, 'b, 'c> {
646 worker: &'c mut CompletionWorker<'a>,
647 cursor: &'c mut Cursor<'b>,
648}
649
650type Pair<'a, 'b, 'c> = CompletionPair<'a, 'b, 'c>;
651
652impl CompletionPair<'_, '_, '_> {
653 pub(crate) fn complete_cursor(&mut self) -> Option<()> {
655 use SurroundingSyntax::*;
656
657 if matches!(
659 self.cursor.leaf.kind(),
660 SyntaxKind::LineComment | SyntaxKind::BlockComment
661 ) {
662 return self.complete_comments().then_some(());
663 }
664
665 let surrounding_syntax = self.cursor.surrounding_syntax;
666 let mode = self.cursor.leaf_mode();
667
668 if matches!(surrounding_syntax, ImportList) {
670 return self.complete_imports().then_some(());
671 }
672
673 if matches!(surrounding_syntax, ParamList) {
675 return self.complete_params();
676 }
677
678 match self.cursor.syntax_context.clone() {
680 Some(SyntaxContext::Element { container, .. }) => {
681 if let Some(container) = container.cast::<ast::Dict>() {
683 for named in container.items() {
684 if let ast::DictItem::Named(named) = named {
685 self.worker.seen_field(named.name().into());
686 }
687 }
688 };
689 }
690 Some(SyntaxContext::Arg { args, .. }) => {
691 for arg in args.children() {
692 let Some(ast::Arg::Named(named)) = arg.cast::<ast::Arg>() else {
693 continue;
694 };
695 self.worker.seen_field(named.name().into());
696 }
697 }
698 Some(SyntaxContext::VarAccess(
700 var @ (VarClass::FieldAccess { .. } | VarClass::DotAccess { .. }),
701 )) => {
702 let target = var.accessed_node()?;
703 let field = var.accessing_field()?;
704
705 self.cursor.from = field.offset(&self.cursor.source)?;
706
707 self.doc_access_completions(&target);
708 return Some(());
709 }
710 Some(SyntaxContext::ImportPath(path) | SyntaxContext::IncludePath(path)) => {
711 let Some(ast::Expr::Str(str)) = path.cast() else {
712 return None;
713 };
714 self.cursor.from = path.offset();
715 let value = str.get();
716 if value.starts_with('@') {
717 let all_versions = value.contains(':');
718 self.package_completions(all_versions);
719 return Some(());
720 } else {
721 let paths = self.complete_path(&crate::analysis::PathKind::Source {
722 allow_package: true,
723 });
724 self.worker.completions.extend(paths.unwrap_or_default());
726 }
727
728 return Some(());
729 }
730 Some(
732 SyntaxContext::Ref {
733 node,
734 suffix_colon: _,
735 }
736 | SyntaxContext::At { node },
737 ) => {
738 self.cursor.from = node.offset() + 1;
739 self.ref_completions();
740 return Some(());
741 }
742 Some(
743 SyntaxContext::VarAccess(VarClass::Ident { .. })
744 | SyntaxContext::Paren { .. }
745 | SyntaxContext::Label { .. }
746 | SyntaxContext::Normal(..),
747 )
748 | None => {}
749 }
750
751 let cursor_pos = bad_completion_cursor(
752 self.cursor.syntax.as_ref(),
753 self.cursor.syntax_context.as_ref(),
754 &self.cursor.leaf,
755 );
756
757 let ty = self
759 .worker
760 .ctx
761 .post_type_of_node(self.cursor.leaf.clone())
762 .filter(|ty| !matches!(ty, Ty::Any))
763 .filter(|_| !matches!(cursor_pos, Some(BadCompletionCursor::ArgListPos)));
766
767 crate::log_debug_ct!(
768 "complete_type: {:?} -> ({surrounding_syntax:?}, {ty:#?})",
769 self.cursor.leaf
770 );
771
772 if is_ident_like(&self.cursor.leaf) {
776 self.cursor.from = self.cursor.leaf.offset();
777 } else if let Some(offset) = self
778 .cursor
779 .syntax
780 .as_ref()
781 .and_then(SyntaxClass::complete_offset)
782 {
783 self.cursor.from = offset;
784 }
785
786 if let Some(ty) = ty {
788 let filter = |ty: &Ty| match surrounding_syntax {
789 SurroundingSyntax::StringContent => match ty {
790 Ty::Builtin(
791 BuiltinTy::Path(..) | BuiltinTy::TextFont | BuiltinTy::TextFeature,
792 ) => true,
793 Ty::Value(val) => matches!(val.val, Value::Str(..)),
794 _ => false,
795 },
796 _ => true,
797 };
798 let mut ctx = TypeCompletionWorker {
799 base: self,
800 filter: &filter,
801 };
802 ctx.type_completion(&ty, None);
803 }
804 let mut type_completions = std::mem::take(&mut self.worker.completions);
805
806 match mode {
808 InterpretMode::Code => {
809 self.complete_code();
810 }
811 InterpretMode::Math => {
812 self.complete_math();
813 }
814 InterpretMode::Raw => {
815 self.complete_markup();
816 }
817 InterpretMode::Markup => match surrounding_syntax {
818 Regular => {
819 self.complete_markup();
820 }
821 Selector | ShowTransform | SetRule => {
822 self.complete_code();
823 }
824 StringContent | ImportList | ParamList => {}
825 },
826 InterpretMode::Comment | InterpretMode::String => {}
827 };
828
829 match surrounding_syntax {
831 Regular | StringContent | ImportList | ParamList | SetRule => {}
832 Selector => {
833 self.snippet_completion(
834 "text selector",
835 "\"${text}\"",
836 "Replace occurrences of specific text.",
837 );
838
839 self.snippet_completion(
840 "regex selector",
841 "regex(\"${regex}\")",
842 "Replace matches of a regular expression.",
843 );
844 }
845 ShowTransform => {
846 self.snippet_completion(
847 "replacement",
848 "[${content}]",
849 "Replace the selected element with content.",
850 );
851
852 self.snippet_completion(
853 "replacement (string)",
854 "\"${text}\"",
855 "Replace the selected element with a string of text.",
856 );
857
858 self.snippet_completion(
859 "transformation",
860 "element => [${content}]",
861 "Transform the element with a function.",
862 );
863 }
864 }
865
866 crate::log_debug_ct!(
876 "sort completions: {type_completions:#?} {:#?}",
877 self.worker.completions
878 );
879
880 type_completions.sort_by(|a, b| {
882 a.sort_text
883 .as_ref()
884 .cmp(&b.sort_text.as_ref())
885 .then_with(|| a.label.cmp(&b.label))
886 });
887 self.worker.completions.sort_by(|a, b| {
888 a.sort_text
889 .as_ref()
890 .cmp(&b.sort_text.as_ref())
891 .then_with(|| a.label.cmp(&b.label))
892 });
893
894 for (idx, compl) in type_completions
895 .iter_mut()
896 .chain(self.worker.completions.iter_mut())
897 .enumerate()
898 {
899 compl.sort_text = Some(eco_format!("{idx:03}"));
900 }
901
902 self.worker.completions.append(&mut type_completions);
903
904 crate::log_debug_ct!("sort completions after: {:#?}", self.worker.completions);
905
906 if let Some(node) = self.cursor.arg_cursor() {
907 crate::log_debug_ct!("content block compl: args {node:?}");
908 let is_unclosed = matches!(node.kind(), SyntaxKind::Args)
909 && node.children().fold(0i32, |acc, node| match node.kind() {
910 SyntaxKind::LeftParen => acc + 1,
911 SyntaxKind::RightParen => acc - 1,
912 SyntaxKind::Error if node.leaf_text() == "(" => acc + 1,
913 SyntaxKind::Error if node.leaf_text() == ")" => acc - 1,
914 _ => acc,
915 }) > 0;
916 if is_unclosed {
917 self.worker.enrich("", ")");
918 }
919 }
920
921 if self.cursor.before.ends_with(',') || self.cursor.before.ends_with(':') {
922 self.worker.enrich(" ", "");
923 }
924 match surrounding_syntax {
925 Regular | ImportList | ParamList | ShowTransform | SetRule | StringContent => {}
926 Selector => {
927 self.worker.enrich("", ": ${}");
928 }
929 }
930
931 crate::log_debug_ct!("enrich completions: {:?}", self.worker.completions);
932
933 Some(())
934 }
935
936 fn push_completion(&mut self, completion: Completion) {
938 self.worker
939 .completions
940 .push(self.cursor.lsp_item_of(&completion));
941 }
942}
943
944pub fn symbol_detail(s: &str) -> EcoString {
947 let ld = symbol_label_detail(s);
948 if ld.starts_with("\\u") {
949 return ld;
950 }
951
952 let mut chars = s.chars();
953 let unicode_repr = if let (Some(ch), None) = (chars.next(), chars.next()) {
954 format!("\\u{{{:04x}}}", ch as u32)
955 } else {
956 let codes: Vec<String> = s
957 .chars()
958 .map(|ch| format!("\\u{{{:04x}}}", ch as u32))
959 .collect();
960 codes.join(" + ")
961 };
962
963 format!("{ld}, unicode: `{unicode_repr}`").into()
964}
965
966pub fn symbol_label_detail(s: &str) -> EcoString {
969 let mut chars = s.chars();
970 if let (Some(ch), None) = (chars.next(), chars.next()) {
971 return symbol_label_detail_single_char(ch);
972 }
973
974 if s.chars().all(|ch| !ch.is_whitespace() && !ch.is_control()) {
975 return s.into();
976 }
977
978 let codes: Vec<String> = s
979 .chars()
980 .map(|ch| format!("\\u{{{:04x}}}", ch as u32))
981 .collect();
982 codes.join(" + ").into()
983}
984
985fn symbol_label_detail_single_char(ch: char) -> EcoString {
986 if !ch.is_whitespace() && !ch.is_control() {
987 return ch.into();
988 }
989 match ch {
990 ' ' => "space".into(),
991 '\t' => "tab".into(),
992 '\n' => "newline".into(),
993 '\r' => "carriage return".into(),
994 '\u{200D}' => "zero width joiner".into(),
996 '\u{200C}' => "zero width non-joiner".into(),
997 '\u{200B}' => "zero width space".into(),
998 '\u{2060}' => "word joiner".into(),
999 '\u{00A0}' => "non-breaking space".into(),
1001 '\u{202F}' => "narrow no-break space".into(),
1002 '\u{2002}' => "en space".into(),
1003 '\u{2003}' => "em space".into(),
1004 '\u{2004}' => "three-per-em space".into(),
1005 '\u{2005}' => "four-per-em space".into(),
1006 '\u{2006}' => "six-per-em space".into(),
1007 '\u{2007}' => "figure space".into(),
1008 '\u{205f}' => "medium mathematical space".into(),
1009 '\u{2008}' => "punctuation space".into(),
1010 '\u{2009}' => "thin space".into(),
1011 '\u{200A}' => "hair space".into(),
1012 _ => format!("\\u{{{:04x}}}", ch as u32).into(),
1013 }
1014}
1015
1016fn slice_at(s: &str, mut rng: Range<usize>) -> &str {
1018 while !rng.is_empty() && !s.is_char_boundary(rng.start) {
1019 rng.start += 1;
1020 }
1021 while !rng.is_empty() && !s.is_char_boundary(rng.end) {
1022 rng.end -= 1;
1023 }
1024
1025 if rng.is_empty() {
1026 return "";
1027 }
1028
1029 &s[rng]
1030}
1031
1032fn fill_first_empty_placeholder(snippet: &mut EcoString, text: &str) -> bool {
1033 let Some(pos) = snippet.find("${}") else {
1034 return false;
1035 };
1036
1037 let escaped = text
1038 .replace('\\', "\\\\")
1039 .replace('$', "\\$")
1040 .replace('}', "\\}");
1041 let mut filled = EcoString::new();
1042 filled.push_str(&snippet[..pos]);
1043 filled.push_str("${");
1044 filled.push_str(&escaped);
1045 filled.push('}');
1046 filled.push_str(&snippet[pos + "${}".len()..]);
1047 *snippet = filled;
1048 true
1049}
1050
1051static TYPST_SNIPPET_PLACEHOLDER_RE: LazyLock<Regex> =
1052 LazyLock::new(|| Regex::new(r"\$\{(.*?)\}").unwrap());
1053
1054fn to_lsp_snippet(typst_snippet: &str) -> EcoString {
1056 let mut counter = 1;
1057 let result = TYPST_SNIPPET_PLACEHOLDER_RE.replace_all(typst_snippet, |cap: &Captures| {
1058 let substitution = format!("${{{}:{}}}", counter, &cap[1]);
1059 counter += 1;
1060 substitution
1061 });
1062
1063 result.into()
1064}
1065
1066fn is_hash_expr(leaf: &LinkedNode<'_>) -> bool {
1067 is_hash_expr_(leaf).is_some()
1068}
1069
1070fn is_hash_expr_(leaf: &LinkedNode<'_>) -> Option<()> {
1071 match leaf.kind() {
1072 SyntaxKind::Hash => Some(()),
1073 SyntaxKind::Ident => {
1074 let prev_leaf = leaf.prev_leaf()?;
1075 if prev_leaf.kind() == SyntaxKind::Hash {
1076 Some(())
1077 } else {
1078 None
1079 }
1080 }
1081 _ => None,
1082 }
1083}
1084
1085fn is_triggered_by_punc(trigger_character: Option<char>) -> bool {
1086 trigger_character.is_some_and(|ch| ch.is_ascii_punctuation())
1087}
1088
1089fn is_arg_like_context(mut matching: &LinkedNode) -> bool {
1090 while let Some(parent) = matching.parent() {
1091 use SyntaxKind::*;
1092
1093 match parent.kind() {
1095 ContentBlock | Equation | CodeBlock | Markup | Math | Code => return false,
1096 Args | Params | Destructuring | Array | Dict => return true,
1097 _ => {}
1098 }
1099
1100 matching = parent;
1101 }
1102 false
1103}
1104
1105fn deserialize_null_default<'de, D, T>(deserializer: D) -> Result<T, D::Error>
1136where
1137 T: Default + Deserialize<'de>,
1138 D: serde::Deserializer<'de>,
1139{
1140 let opt = Option::deserialize(deserializer)?;
1141 Ok(opt.unwrap_or_default())
1142}
1143
1144#[cfg(test)]
1147mod tests {
1148 use super::slice_at;
1149
1150 #[test]
1151 fn test_before() {
1152 const TEST_UTF8_STR: &str = "我们";
1153 for i in 0..=TEST_UTF8_STR.len() {
1154 for j in 0..=TEST_UTF8_STR.len() {
1155 let _s = std::hint::black_box(slice_at(TEST_UTF8_STR, i..j));
1156 }
1157 }
1158 }
1159}