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, Label, NoneValue, Repr, Scope, StyleChain, Type, Value, fields_on, format_str,
20 repr,
21};
22use typst::syntax::ast::{self, AstNode, Param};
23use typst::syntax::{is_id_continue, is_id_start, is_ident};
24use typst::text::RawElem;
25use typst::visualize::Color;
26use typst_shim::{syntax::LinkedNodeExt, utils::hash128};
27use unscanny::Scanner;
28
29use crate::adt::interner::Interned;
30use crate::analysis::{BuiltinTy, LocalContext, PathKind, Ty};
31use crate::completion::{
32 Completion, CompletionCommand, CompletionContextKey, CompletionItem, CompletionKind,
33 DEFAULT_POSTFIX_SNIPPET, DEFAULT_PREFIX_SNIPPET, EcoTextEdit, ParsedSnippet, PostfixSnippet,
34 PostfixSnippetScope, PrefixSnippet,
35};
36use crate::prelude::*;
37use crate::syntax::{
38 InterpretMode, PreviousDecl, SurroundingSyntax, SyntaxClass, SyntaxContext, VarClass,
39 classify_context, interpret_mode_at, is_ident_like, node_ancestors, previous_decls,
40 surrounding_syntax,
41};
42use crate::ty::{
43 DynTypeBounds, Iface, IfaceChecker, InsTy, SigTy, TyCtx, TypeInfo, TypeInterface, TypeVar,
44};
45use crate::upstream::{plain_docs_sentence, summarize_font_family};
46
47use super::SharedContext;
48
49mod field_access;
50mod func;
51mod import;
52mod kind;
53mod mode;
54mod param;
55mod path;
56mod scope;
57mod snippet;
58#[path = "completion/type.rs"]
59mod type_;
60mod typst_specific;
61use kind::*;
62use scope::*;
63use type_::*;
64
65type LspCompletion = CompletionItem;
66
67#[derive(Default, Debug, Clone, Serialize, Deserialize)]
69#[serde(rename_all = "camelCase")]
70pub struct CompletionFeat {
71 #[serde(default, deserialize_with = "deserialize_null_default")]
73 pub trigger_on_snippet_placeholders: bool,
74 #[serde(default, deserialize_with = "deserialize_null_default")]
76 pub trigger_suggest: bool,
77 #[serde(default, deserialize_with = "deserialize_null_default")]
79 pub trigger_parameter_hints: bool,
80 #[serde(default, deserialize_with = "deserialize_null_default")]
83 pub trigger_suggest_and_parameter_hints: bool,
84
85 pub symbol: Option<SymbolCompletionWay>,
87
88 pub postfix: Option<bool>,
90 pub postfix_ufcs: Option<bool>,
92 pub postfix_ufcs_left: Option<bool>,
94 pub postfix_ufcs_right: Option<bool>,
96 pub postfix_snippets: Option<EcoVec<PostfixSnippet>>,
98}
99
100impl CompletionFeat {
101 pub(crate) fn postfix(&self) -> bool {
103 self.postfix.unwrap_or(true)
104 }
105
106 pub(crate) fn any_ufcs(&self) -> bool {
108 self.ufcs() || self.ufcs_left() || self.ufcs_right()
109 }
110
111 pub(crate) fn ufcs(&self) -> bool {
113 self.postfix() && self.postfix_ufcs.unwrap_or(true)
114 }
115
116 pub(crate) fn ufcs_left(&self) -> bool {
118 self.postfix() && self.postfix_ufcs_left.unwrap_or(true)
119 }
120
121 pub(crate) fn ufcs_right(&self) -> bool {
123 self.postfix() && self.postfix_ufcs_right.unwrap_or(true)
124 }
125
126 pub(crate) fn postfix_snippets(&self) -> &EcoVec<PostfixSnippet> {
128 self.postfix_snippets
129 .as_ref()
130 .unwrap_or(&DEFAULT_POSTFIX_SNIPPET)
131 }
132
133 pub(crate) fn is_stepless(&self) -> bool {
134 matches!(self.symbol, Some(SymbolCompletionWay::Stepless))
135 }
136}
137
138#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
142#[serde(rename_all = "camelCase")]
143pub enum SymbolCompletionWay {
144 Step,
146 Stepless,
148}
149
150pub struct CompletionCursor<'a> {
152 ctx: Arc<SharedContext>,
154 from: usize,
156 cursor: usize,
158 source: Source,
160 text: &'a str,
162 before: &'a str,
164 after: &'a str,
166 leaf: LinkedNode<'a>,
168 syntax: Option<SyntaxClass<'a>>,
170 syntax_context: Option<SyntaxContext<'a>>,
172 surrounding_syntax: SurroundingSyntax,
174
175 last_lsp_range_pair: Option<(Range<usize>, LspRange)>,
177 ident_cursor: OnceLock<Option<SelectedNode<'a>>>,
179 arg_cursor: OnceLock<Option<SyntaxNode>>,
181}
182
183impl<'a> CompletionCursor<'a> {
184 pub fn new(ctx: Arc<SharedContext>, source: &'a Source, cursor: usize) -> Option<Self> {
186 let text = source.text();
187 let root = LinkedNode::new(source.root());
188 let leaf = root.leaf_at_compat(cursor)?;
189 let syntax = classify_syntax(leaf.clone(), cursor);
191 let syntax_context = classify_context(leaf.clone(), Some(cursor));
192 let surrounding_syntax = surrounding_syntax(&leaf);
193
194 crate::log_debug_ct!("CompletionCursor: syntax {leaf:?} -> {syntax:#?}");
195 crate::log_debug_ct!("CompletionCursor: context {leaf:?} -> {syntax_context:#?}");
196 crate::log_debug_ct!("CompletionCursor: surrounding {leaf:?} -> {surrounding_syntax:#?}");
197 Some(Self {
198 ctx,
199 text,
200 source: source.clone(),
201 before: &text[..cursor],
202 after: &text[cursor..],
203 leaf,
204 syntax,
205 syntax_context,
206 surrounding_syntax,
207 cursor,
208 from: cursor,
209 last_lsp_range_pair: None,
210 ident_cursor: OnceLock::new(),
211 arg_cursor: OnceLock::new(),
212 })
213 }
214
215 fn before_window(&self, size: usize) -> &str {
217 slice_at(
218 self.before,
219 self.cursor.saturating_sub(size)..self.before.len(),
220 )
221 }
222
223 fn is_callee(&self) -> bool {
225 matches!(self.syntax, Some(SyntaxClass::Callee(..)))
226 }
227
228 pub fn leaf_mode(&self) -> InterpretMode {
230 interpret_mode_at(Some(&self.leaf))
231 }
232
233 fn selected_node(&self) -> &Option<SelectedNode<'a>> {
235 self.ident_cursor.get_or_init(|| {
236 let is_from_ident = matches!(
239 self.syntax,
240 Some(SyntaxClass::Callee(..) | SyntaxClass::VarAccess(..))
241 ) && is_ident_like(&self.leaf)
242 && self.leaf.offset() == self.from;
243 if is_from_ident {
244 return Some(SelectedNode::Ident(self.leaf.clone()));
245 }
246
247 let is_from_label = matches!(self.syntax, Some(SyntaxClass::Label { .. }))
250 && self.leaf.offset() + 1 == self.from;
251 if is_from_label {
252 return Some(SelectedNode::Label(self.leaf.clone()));
253 }
254
255 let is_from_ref = matches!(self.syntax, Some(SyntaxClass::Ref { .. }))
258 && self.leaf.offset() + 1 == self.from;
259 if is_from_ref {
260 return Some(SelectedNode::Ref(self.leaf.clone()));
261 }
262
263 let is_from_ref = matches!(self.syntax, Some(SyntaxClass::At { .. }))
266 && self.leaf.offset() + 1 == self.from;
267 if is_from_ref {
268 return Some(SelectedNode::At(self.leaf.clone()));
269 }
270
271 None
272 })
273 }
274
275 fn arg_cursor(&self) -> &Option<SyntaxNode> {
277 self.arg_cursor.get_or_init(|| {
278 let mut args_node = None;
279
280 match self.syntax_context.clone() {
281 Some(SyntaxContext::Arg { args, .. }) => {
282 args_node = Some(args.cast::<ast::Args>()?.to_untyped().clone());
283 }
284 Some(SyntaxContext::Normal(node))
285 if (matches!(node.kind(), SyntaxKind::ContentBlock)
286 && matches!(self.leaf.kind(), SyntaxKind::LeftBracket)) =>
287 {
288 args_node = node.parent().map(|s| s.get().clone());
289 }
290 Some(
291 SyntaxContext::Element { .. }
292 | SyntaxContext::ImportPath(..)
293 | SyntaxContext::IncludePath(..)
294 | SyntaxContext::VarAccess(..)
295 | SyntaxContext::Paren { .. }
296 | SyntaxContext::Label { .. }
297 | SyntaxContext::Ref { .. }
298 | SyntaxContext::At { .. }
299 | SyntaxContext::Normal(..),
300 )
301 | None => {}
302 }
303
304 args_node
305 })
306 }
307
308 fn lsp_range_of(&mut self, rng: Range<usize>) -> LspRange {
310 if let Some((last_rng, last_lsp_rng)) = &self.last_lsp_range_pair
312 && *last_rng == rng
313 {
314 return *last_lsp_rng;
315 }
316
317 let lsp_rng = self.ctx.to_lsp_range(rng.clone(), &self.source);
318 self.last_lsp_range_pair = Some((rng, lsp_rng));
319 lsp_rng
320 }
321
322 fn lsp_item_of(&mut self, item: &Completion) -> LspCompletion {
324 let mut snippet = item.apply.as_ref().unwrap_or(&item.label).clone();
326 let replace_range = match self.selected_node() {
327 Some(SelectedNode::Ident(from_ident)) => {
328 let mut rng = from_ident.range();
329
330 if !self.is_callee() && self.cursor != rng.end && is_arg_like_context(from_ident) {
332 if !snippet.trim_end().ends_with(',') {
334 snippet.push_str(", ");
335 }
336
337 rng.end = self.cursor;
339 }
340
341 self.lsp_range_of(rng)
342 }
343 Some(SelectedNode::Label(from_label)) => {
344 let mut rng = from_label.range();
345 if from_label.text().starts_with('<') && !snippet.starts_with('<') {
346 rng.start += 1;
347 }
348 if from_label.text().ends_with('>') && !snippet.ends_with('>') {
349 rng.end -= 1;
350 }
351
352 self.lsp_range_of(rng)
353 }
354 Some(node @ (SelectedNode::At(from_ref) | SelectedNode::Ref(from_ref))) => {
355 let mut rng = if matches!(node, SelectedNode::At(..)) {
356 let offset = from_ref.offset();
357 offset..offset + 1
358 } else {
359 from_ref.range()
360 };
361 if from_ref.text().starts_with('@') && !snippet.starts_with('@') {
362 rng.start += 1;
363 }
364
365 self.lsp_range_of(rng)
366 }
367 None => self.lsp_range_of(self.from..self.cursor),
368 };
369
370 let text_edit = EcoTextEdit::new(replace_range, snippet);
371
372 LspCompletion {
373 label: item.label.clone(),
374 kind: item.kind.clone(),
375 detail: item.detail.clone(),
376 sort_text: item.sort_text.clone(),
377 filter_text: item.filter_text.clone(),
378 label_details: item.label_details.clone().map(From::from),
379 text_edit: Some(text_edit),
380 additional_text_edits: item.additional_text_edits.clone(),
381 insert_text_format: Some(InsertTextFormat::SNIPPET),
382 command: item.command.clone(),
383 ..Default::default()
384 }
385 }
386}
387
388type Cursor<'a> = CompletionCursor<'a>;
390
391enum SelectedNode<'a> {
393 Ident(LinkedNode<'a>),
395 Label(LinkedNode<'a>),
397 Ref(LinkedNode<'a>),
399 At(LinkedNode<'a>),
401}
402
403pub struct CompletionWorker<'a> {
415 pub completions: Vec<LspCompletion>,
417 pub incomplete: bool,
419
420 ctx: &'a mut LocalContext,
422 document: Option<&'a TypstDocument>,
424 explicit: bool,
426 trigger_character: Option<char>,
428 seen_casts: HashSet<u128>,
430 seen_types: HashSet<Ty>,
432 seen_fields: HashSet<Interned<str>>,
434}
435
436impl<'a> CompletionWorker<'a> {
437 pub fn new(
439 ctx: &'a mut LocalContext,
440 document: Option<&'a TypstDocument>,
441 explicit: bool,
442 trigger_character: Option<char>,
443 ) -> Option<Self> {
444 Some(Self {
445 ctx,
446 document,
447 trigger_character,
448 explicit,
449 incomplete: true,
450 completions: vec![],
451 seen_casts: HashSet::new(),
452 seen_types: HashSet::new(),
453 seen_fields: HashSet::new(),
454 })
455 }
456
457 pub fn world(&self) -> &LspWorld {
459 self.ctx.world()
460 }
461
462 fn seen_field(&mut self, field: Interned<str>) -> bool {
463 !self.seen_fields.insert(field)
464 }
465
466 fn enrich(&mut self, prefix: &str, suffix: &str) {
468 for LspCompletion { text_edit, .. } in &mut self.completions {
469 let apply = match text_edit {
470 Some(EcoTextEdit { new_text, .. }) => new_text,
471 _ => continue,
472 };
473
474 *apply = eco_format!("{prefix}{apply}{suffix}");
475 }
476 }
477
478 pub(crate) fn work(&mut self, cursor: &mut Cursor) -> Option<()> {
484 if let Some(SyntaxClass::VarAccess(var)) = &cursor.syntax {
486 let node = var.node();
487 match node.parent_kind() {
488 Some(SyntaxKind::LetBinding) => {
490 let parent = node.parent()?;
491 let parent_init = parent.cast::<ast::LetBinding>()?.init()?;
492 let parent_init = parent.find(parent_init.span())?;
493 parent_init.find(node.span())?;
494 }
495 Some(SyntaxKind::Closure) => {
496 let parent = node.parent()?;
497 let parent_body = parent.cast::<ast::Closure>()?.body();
498 let parent_body = parent.find(parent_body.span())?;
499 parent_body.find(node.span())?;
500 }
501 _ => {}
502 }
503 }
504
505 if matches!(
507 cursor.syntax,
508 Some(SyntaxClass::Callee(..) | SyntaxClass::VarAccess(..) | SyntaxClass::Normal(..))
509 ) && cursor.leaf.erroneous()
510 {
511 let mut chars = cursor.leaf.text().chars();
512 match chars.next() {
513 Some(ch) if ch.is_numeric() => return None,
514 Some('.') => {
515 if matches!(chars.next(), Some(ch) if ch.is_numeric()) {
516 return None;
517 }
518 }
519 _ => {}
520 }
521 }
522
523 let self_ty = cursor.leaf.cast::<ast::Expr>().and_then(|leaf| {
526 let v = self.ctx.mini_eval(leaf)?;
527 Some(Ty::Value(InsTy::new(v)))
528 });
529
530 if let Some(self_ty) = self_ty {
531 self.seen_types.insert(self_ty);
532 };
533
534 let mut pair = Pair {
535 worker: self,
536 cursor,
537 };
538 let _ = pair.complete_cursor();
539
540 if let Some(SelectedNode::Ident(from_ident)) = cursor.selected_node() {
543 let ident_prefix = cursor.text[from_ident.offset()..cursor.cursor].to_string();
544
545 self.completions.retain(|item| {
546 let mut prefix_matcher = item.label.chars();
547 'ident_matching: for ch in ident_prefix.chars() {
548 for item in prefix_matcher.by_ref() {
549 if item == ch {
550 continue 'ident_matching;
551 }
552 }
553
554 return false;
555 }
556
557 true
558 });
559 }
560
561 for item in &mut self.completions {
562 if let Some(EcoTextEdit {
563 ref mut new_text, ..
564 }) = item.text_edit
565 {
566 *new_text = to_lsp_snippet(new_text);
567 }
568 }
569
570 Some(())
571 }
572}
573
574struct CompletionPair<'a, 'b, 'c> {
575 worker: &'c mut CompletionWorker<'a>,
576 cursor: &'c mut Cursor<'b>,
577}
578
579type Pair<'a, 'b, 'c> = CompletionPair<'a, 'b, 'c>;
580
581impl CompletionPair<'_, '_, '_> {
582 pub(crate) fn complete_cursor(&mut self) -> Option<()> {
584 use SurroundingSyntax::*;
585
586 if matches!(
588 self.cursor.leaf.kind(),
589 SyntaxKind::LineComment | SyntaxKind::BlockComment
590 ) {
591 return self.complete_comments().then_some(());
592 }
593
594 let surrounding_syntax = self.cursor.surrounding_syntax;
595 let mode = self.cursor.leaf_mode();
596
597 if matches!(surrounding_syntax, ImportList) {
599 return self.complete_imports().then_some(());
600 }
601
602 if matches!(surrounding_syntax, ParamList) {
604 return self.complete_params();
605 }
606
607 match self.cursor.syntax_context.clone() {
609 Some(SyntaxContext::Element { container, .. }) => {
610 if let Some(container) = container.cast::<ast::Dict>() {
612 for named in container.items() {
613 if let ast::DictItem::Named(named) = named {
614 self.worker.seen_field(named.name().into());
615 }
616 }
617 };
618 }
619 Some(SyntaxContext::Arg { args, .. }) => {
620 let args = args.cast::<ast::Args>()?;
622 for arg in args.items() {
623 if let ast::Arg::Named(named) = arg {
624 self.worker.seen_field(named.name().into());
625 }
626 }
627 }
628 Some(SyntaxContext::VarAccess(
630 var @ (VarClass::FieldAccess { .. } | VarClass::DotAccess { .. }),
631 )) => {
632 let target = var.accessed_node()?;
633 let field = var.accessing_field()?;
634
635 self.cursor.from = field.offset(&self.cursor.source)?;
636
637 self.doc_access_completions(&target);
638 return Some(());
639 }
640 Some(SyntaxContext::ImportPath(path) | SyntaxContext::IncludePath(path)) => {
641 let Some(ast::Expr::Str(str)) = path.cast() else {
642 return None;
643 };
644 self.cursor.from = path.offset();
645 let value = str.get();
646 if value.starts_with('@') {
647 let all_versions = value.contains(':');
648 self.package_completions(all_versions);
649 return Some(());
650 } else {
651 let paths = self.complete_path(&crate::analysis::PathKind::Source {
652 allow_package: true,
653 });
654 self.worker.completions.extend(paths.unwrap_or_default());
656 }
657
658 return Some(());
659 }
660 Some(
662 SyntaxContext::Ref {
663 node,
664 suffix_colon: _,
665 }
666 | SyntaxContext::At { node },
667 ) => {
668 self.cursor.from = node.offset() + 1;
669 self.ref_completions();
670 return Some(());
671 }
672 Some(
673 SyntaxContext::VarAccess(VarClass::Ident { .. })
674 | SyntaxContext::Paren { .. }
675 | SyntaxContext::Label { .. }
676 | SyntaxContext::Normal(..),
677 )
678 | None => {}
679 }
680
681 let cursor_pos = bad_completion_cursor(
682 self.cursor.syntax.as_ref(),
683 self.cursor.syntax_context.as_ref(),
684 &self.cursor.leaf,
685 );
686
687 let ty = self
689 .worker
690 .ctx
691 .post_type_of_node(self.cursor.leaf.clone())
692 .filter(|ty| !matches!(ty, Ty::Any))
693 .filter(|_| !matches!(cursor_pos, Some(BadCompletionCursor::ArgListPos)));
696
697 crate::log_debug_ct!(
698 "complete_type: {:?} -> ({surrounding_syntax:?}, {ty:#?})",
699 self.cursor.leaf
700 );
701
702 if is_ident_like(&self.cursor.leaf) {
706 self.cursor.from = self.cursor.leaf.offset();
707 } else if let Some(offset) = self
708 .cursor
709 .syntax
710 .as_ref()
711 .and_then(SyntaxClass::complete_offset)
712 {
713 self.cursor.from = offset;
714 }
715
716 if let Some(ty) = ty {
718 let filter = |ty: &Ty| match surrounding_syntax {
719 SurroundingSyntax::StringContent => match ty {
720 Ty::Builtin(
721 BuiltinTy::Path(..) | BuiltinTy::TextFont | BuiltinTy::TextFeature,
722 ) => true,
723 Ty::Value(val) => matches!(val.val, Value::Str(..)),
724 _ => false,
725 },
726 _ => true,
727 };
728 let mut ctx = TypeCompletionWorker {
729 base: self,
730 filter: &filter,
731 };
732 ctx.type_completion(&ty, None);
733 }
734 let mut type_completions = std::mem::take(&mut self.worker.completions);
735
736 match mode {
738 InterpretMode::Code => {
739 self.complete_code();
740 }
741 InterpretMode::Math => {
742 self.complete_math();
743 }
744 InterpretMode::Raw => {
745 self.complete_markup();
746 }
747 InterpretMode::Markup => match surrounding_syntax {
748 Regular => {
749 self.complete_markup();
750 }
751 Selector | ShowTransform | SetRule => {
752 self.complete_code();
753 }
754 StringContent | ImportList | ParamList => {}
755 },
756 InterpretMode::Comment | InterpretMode::String => {}
757 };
758
759 match surrounding_syntax {
761 Regular | StringContent | ImportList | ParamList | SetRule => {}
762 Selector => {
763 self.snippet_completion(
764 "text selector",
765 "\"${text}\"",
766 "Replace occurrences of specific text.",
767 );
768
769 self.snippet_completion(
770 "regex selector",
771 "regex(\"${regex}\")",
772 "Replace matches of a regular expression.",
773 );
774 }
775 ShowTransform => {
776 self.snippet_completion(
777 "replacement",
778 "[${content}]",
779 "Replace the selected element with content.",
780 );
781
782 self.snippet_completion(
783 "replacement (string)",
784 "\"${text}\"",
785 "Replace the selected element with a string of text.",
786 );
787
788 self.snippet_completion(
789 "transformation",
790 "element => [${content}]",
791 "Transform the element with a function.",
792 );
793 }
794 }
795
796 crate::log_debug_ct!(
806 "sort completions: {type_completions:#?} {:#?}",
807 self.worker.completions
808 );
809
810 type_completions.sort_by(|a, b| {
812 a.sort_text
813 .as_ref()
814 .cmp(&b.sort_text.as_ref())
815 .then_with(|| a.label.cmp(&b.label))
816 });
817 self.worker.completions.sort_by(|a, b| {
818 a.sort_text
819 .as_ref()
820 .cmp(&b.sort_text.as_ref())
821 .then_with(|| a.label.cmp(&b.label))
822 });
823
824 for (idx, compl) in type_completions
825 .iter_mut()
826 .chain(self.worker.completions.iter_mut())
827 .enumerate()
828 {
829 compl.sort_text = Some(eco_format!("{idx:03}"));
830 }
831
832 self.worker.completions.append(&mut type_completions);
833
834 crate::log_debug_ct!("sort completions after: {:#?}", self.worker.completions);
835
836 if let Some(node) = self.cursor.arg_cursor() {
837 crate::log_debug_ct!("content block compl: args {node:?}");
838 let is_unclosed = matches!(node.kind(), SyntaxKind::Args)
839 && node.children().fold(0i32, |acc, node| match node.kind() {
840 SyntaxKind::LeftParen => acc + 1,
841 SyntaxKind::RightParen => acc - 1,
842 SyntaxKind::Error if node.text() == "(" => acc + 1,
843 SyntaxKind::Error if node.text() == ")" => acc - 1,
844 _ => acc,
845 }) > 0;
846 if is_unclosed {
847 self.worker.enrich("", ")");
848 }
849 }
850
851 if self.cursor.before.ends_with(',') || self.cursor.before.ends_with(':') {
852 self.worker.enrich(" ", "");
853 }
854 match surrounding_syntax {
855 Regular | ImportList | ParamList | ShowTransform | SetRule | StringContent => {}
856 Selector => {
857 self.worker.enrich("", ": ${}");
858 }
859 }
860
861 crate::log_debug_ct!("enrich completions: {:?}", self.worker.completions);
862
863 Some(())
864 }
865
866 fn push_completion(&mut self, completion: Completion) {
868 self.worker
869 .completions
870 .push(self.cursor.lsp_item_of(&completion));
871 }
872}
873
874pub fn symbol_detail(s: &str) -> EcoString {
877 let ld = symbol_label_detail(s);
878 if ld.starts_with("\\u") {
879 return ld;
880 }
881
882 let mut chars = s.chars();
883 let unicode_repr = if let (Some(ch), None) = (chars.next(), chars.next()) {
884 format!("\\u{{{:04x}}}", ch as u32)
885 } else {
886 let codes: Vec<String> = s
887 .chars()
888 .map(|ch| format!("\\u{{{:04x}}}", ch as u32))
889 .collect();
890 codes.join(" + ")
891 };
892
893 format!("{ld}, unicode: `{unicode_repr}`").into()
894}
895
896pub fn symbol_label_detail(s: &str) -> EcoString {
899 let mut chars = s.chars();
900 if let (Some(ch), None) = (chars.next(), chars.next()) {
901 return symbol_label_detail_single_char(ch);
902 }
903
904 if s.chars().all(|ch| !ch.is_whitespace() && !ch.is_control()) {
905 return s.into();
906 }
907
908 let codes: Vec<String> = s
909 .chars()
910 .map(|ch| format!("\\u{{{:04x}}}", ch as u32))
911 .collect();
912 codes.join(" + ").into()
913}
914
915fn symbol_label_detail_single_char(ch: char) -> EcoString {
916 if !ch.is_whitespace() && !ch.is_control() {
917 return ch.into();
918 }
919 match ch {
920 ' ' => "space".into(),
921 '\t' => "tab".into(),
922 '\n' => "newline".into(),
923 '\r' => "carriage return".into(),
924 '\u{200D}' => "zero width joiner".into(),
926 '\u{200C}' => "zero width non-joiner".into(),
927 '\u{200B}' => "zero width space".into(),
928 '\u{2060}' => "word joiner".into(),
929 '\u{00A0}' => "non-breaking space".into(),
931 '\u{202F}' => "narrow no-break space".into(),
932 '\u{2002}' => "en space".into(),
933 '\u{2003}' => "em space".into(),
934 '\u{2004}' => "three-per-em space".into(),
935 '\u{2005}' => "four-per-em space".into(),
936 '\u{2006}' => "six-per-em space".into(),
937 '\u{2007}' => "figure space".into(),
938 '\u{205f}' => "medium mathematical space".into(),
939 '\u{2008}' => "punctuation space".into(),
940 '\u{2009}' => "thin space".into(),
941 '\u{200A}' => "hair space".into(),
942 _ => format!("\\u{{{:04x}}}", ch as u32).into(),
943 }
944}
945
946fn slice_at(s: &str, mut rng: Range<usize>) -> &str {
948 while !rng.is_empty() && !s.is_char_boundary(rng.start) {
949 rng.start += 1;
950 }
951 while !rng.is_empty() && !s.is_char_boundary(rng.end) {
952 rng.end -= 1;
953 }
954
955 if rng.is_empty() {
956 return "";
957 }
958
959 &s[rng]
960}
961
962static TYPST_SNIPPET_PLACEHOLDER_RE: LazyLock<Regex> =
963 LazyLock::new(|| Regex::new(r"\$\{(.*?)\}").unwrap());
964
965fn to_lsp_snippet(typst_snippet: &str) -> EcoString {
967 let mut counter = 1;
968 let result = TYPST_SNIPPET_PLACEHOLDER_RE.replace_all(typst_snippet, |cap: &Captures| {
969 let substitution = format!("${{{}:{}}}", counter, &cap[1]);
970 counter += 1;
971 substitution
972 });
973
974 result.into()
975}
976
977fn is_hash_expr(leaf: &LinkedNode<'_>) -> bool {
978 is_hash_expr_(leaf).is_some()
979}
980
981fn is_hash_expr_(leaf: &LinkedNode<'_>) -> Option<()> {
982 match leaf.kind() {
983 SyntaxKind::Hash => Some(()),
984 SyntaxKind::Ident => {
985 let prev_leaf = leaf.prev_leaf()?;
986 if prev_leaf.kind() == SyntaxKind::Hash {
987 Some(())
988 } else {
989 None
990 }
991 }
992 _ => None,
993 }
994}
995
996fn is_triggered_by_punc(trigger_character: Option<char>) -> bool {
997 trigger_character.is_some_and(|ch| ch.is_ascii_punctuation())
998}
999
1000fn is_arg_like_context(mut matching: &LinkedNode) -> bool {
1001 while let Some(parent) = matching.parent() {
1002 use SyntaxKind::*;
1003
1004 match parent.kind() {
1006 ContentBlock | Equation | CodeBlock | Markup | Math | Code => return false,
1007 Args | Params | Destructuring | Array | Dict => return true,
1008 _ => {}
1009 }
1010
1011 matching = parent;
1012 }
1013 false
1014}
1015
1016fn deserialize_null_default<'de, D, T>(deserializer: D) -> Result<T, D::Error>
1047where
1048 T: Default + Deserialize<'de>,
1049 D: serde::Deserializer<'de>,
1050{
1051 let opt = Option::deserialize(deserializer)?;
1052 Ok(opt.unwrap_or_default())
1053}
1054
1055#[cfg(test)]
1058mod tests {
1059 use super::slice_at;
1060
1061 #[test]
1062 fn test_before() {
1063 const TEST_UTF8_STR: &str = "我们";
1064 for i in 0..=TEST_UTF8_STR.len() {
1065 for j in 0..=TEST_UTF8_STR.len() {
1066 let _s = std::hint::black_box(slice_at(TEST_UTF8_STR, i..j));
1067 }
1068 }
1069 }
1070}