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 None
264 })
265 }
266
267 fn arg_cursor(&self) -> &Option<SyntaxNode> {
269 self.arg_cursor.get_or_init(|| {
270 let mut args_node = None;
271
272 match self.syntax_context.clone() {
273 Some(SyntaxContext::Arg { args, .. }) => {
274 args_node = Some(args.cast::<ast::Args>()?.to_untyped().clone());
275 }
276 Some(SyntaxContext::Normal(node))
277 if (matches!(node.kind(), SyntaxKind::ContentBlock)
278 && matches!(self.leaf.kind(), SyntaxKind::LeftBracket)) =>
279 {
280 args_node = node.parent().map(|s| s.get().clone());
281 }
282 Some(
283 SyntaxContext::Element { .. }
284 | SyntaxContext::ImportPath(..)
285 | SyntaxContext::IncludePath(..)
286 | SyntaxContext::VarAccess(..)
287 | SyntaxContext::Paren { .. }
288 | SyntaxContext::Label { .. }
289 | SyntaxContext::Ref { .. }
290 | SyntaxContext::Normal(..),
291 )
292 | None => {}
293 }
294
295 args_node
296 })
297 }
298
299 fn lsp_range_of(&mut self, rng: Range<usize>) -> LspRange {
301 if let Some((last_rng, last_lsp_rng)) = &self.last_lsp_range_pair
303 && *last_rng == rng
304 {
305 return *last_lsp_rng;
306 }
307
308 let lsp_rng = self.ctx.to_lsp_range(rng.clone(), &self.source);
309 self.last_lsp_range_pair = Some((rng, lsp_rng));
310 lsp_rng
311 }
312
313 fn lsp_item_of(&mut self, item: &Completion) -> LspCompletion {
315 let mut snippet = item.apply.as_ref().unwrap_or(&item.label).clone();
317 let replace_range = match self.selected_node() {
318 Some(SelectedNode::Ident(from_ident)) => {
319 let mut rng = from_ident.range();
320
321 if !self.is_callee() && self.cursor != rng.end && is_arg_like_context(from_ident) {
323 if !snippet.trim_end().ends_with(',') {
325 snippet.push_str(", ");
326 }
327
328 rng.end = self.cursor;
330 }
331
332 self.lsp_range_of(rng)
333 }
334 Some(SelectedNode::Label(from_label)) => {
335 let mut rng = from_label.range();
336 if from_label.text().starts_with('<') && !snippet.starts_with('<') {
337 rng.start += 1;
338 }
339 if from_label.text().ends_with('>') && !snippet.ends_with('>') {
340 rng.end -= 1;
341 }
342
343 self.lsp_range_of(rng)
344 }
345 Some(SelectedNode::Ref(from_ref)) => {
346 let mut rng = from_ref.range();
347 if from_ref.text().starts_with('@') && !snippet.starts_with('@') {
348 rng.start += 1;
349 }
350
351 self.lsp_range_of(rng)
352 }
353 None => self.lsp_range_of(self.from..self.cursor),
354 };
355
356 let text_edit = EcoTextEdit::new(replace_range, snippet);
357
358 LspCompletion {
359 label: item.label.clone(),
360 kind: item.kind,
361 detail: item.detail.clone(),
362 sort_text: item.sort_text.clone(),
363 filter_text: item.filter_text.clone(),
364 label_details: item.label_details.clone().map(From::from),
365 text_edit: Some(text_edit),
366 additional_text_edits: item.additional_text_edits.clone(),
367 insert_text_format: Some(InsertTextFormat::SNIPPET),
368 command: item.command.clone(),
369 ..Default::default()
370 }
371 }
372}
373
374type Cursor<'a> = CompletionCursor<'a>;
376
377enum SelectedNode<'a> {
379 Ident(LinkedNode<'a>),
381 Label(LinkedNode<'a>),
383 Ref(LinkedNode<'a>),
385}
386
387pub struct CompletionWorker<'a> {
399 pub completions: Vec<LspCompletion>,
401 pub incomplete: bool,
403
404 ctx: &'a mut LocalContext,
406 document: Option<&'a TypstDocument>,
408 explicit: bool,
410 trigger_character: Option<char>,
412 seen_casts: HashSet<u128>,
414 seen_types: HashSet<Ty>,
416 seen_fields: HashSet<Interned<str>>,
418}
419
420impl<'a> CompletionWorker<'a> {
421 pub fn new(
423 ctx: &'a mut LocalContext,
424 document: Option<&'a TypstDocument>,
425 explicit: bool,
426 trigger_character: Option<char>,
427 ) -> Option<Self> {
428 Some(Self {
429 ctx,
430 document,
431 trigger_character,
432 explicit,
433 incomplete: true,
434 completions: vec![],
435 seen_casts: HashSet::new(),
436 seen_types: HashSet::new(),
437 seen_fields: HashSet::new(),
438 })
439 }
440
441 pub fn world(&self) -> &LspWorld {
443 self.ctx.world()
444 }
445
446 fn seen_field(&mut self, field: Interned<str>) -> bool {
447 !self.seen_fields.insert(field)
448 }
449
450 fn enrich(&mut self, prefix: &str, suffix: &str) {
452 for LspCompletion { text_edit, .. } in &mut self.completions {
453 let apply = match text_edit {
454 Some(EcoTextEdit { new_text, .. }) => new_text,
455 _ => continue,
456 };
457
458 *apply = eco_format!("{prefix}{apply}{suffix}");
459 }
460 }
461
462 pub(crate) fn work(&mut self, cursor: &mut Cursor) -> Option<()> {
468 if let Some(SyntaxClass::VarAccess(var)) = &cursor.syntax {
470 let node = var.node();
471 match node.parent_kind() {
472 Some(SyntaxKind::LetBinding) => {
474 let parent = node.parent()?;
475 let parent_init = parent.cast::<ast::LetBinding>()?.init()?;
476 let parent_init = parent.find(parent_init.span())?;
477 parent_init.find(node.span())?;
478 }
479 Some(SyntaxKind::Closure) => {
480 let parent = node.parent()?;
481 let parent_body = parent.cast::<ast::Closure>()?.body();
482 let parent_body = parent.find(parent_body.span())?;
483 parent_body.find(node.span())?;
484 }
485 _ => {}
486 }
487 }
488
489 if matches!(
491 cursor.syntax,
492 Some(SyntaxClass::Callee(..) | SyntaxClass::VarAccess(..) | SyntaxClass::Normal(..))
493 ) && cursor.leaf.erroneous()
494 {
495 let mut chars = cursor.leaf.text().chars();
496 match chars.next() {
497 Some(ch) if ch.is_numeric() => return None,
498 Some('.') => {
499 if matches!(chars.next(), Some(ch) if ch.is_numeric()) {
500 return None;
501 }
502 }
503 _ => {}
504 }
505 }
506
507 let self_ty = cursor.leaf.cast::<ast::Expr>().and_then(|leaf| {
510 let v = self.ctx.mini_eval(leaf)?;
511 Some(Ty::Value(InsTy::new(v)))
512 });
513
514 if let Some(self_ty) = self_ty {
515 self.seen_types.insert(self_ty);
516 };
517
518 let mut pair = Pair {
519 worker: self,
520 cursor,
521 };
522 let _ = pair.complete_cursor();
523
524 if let Some(SelectedNode::Ident(from_ident)) = cursor.selected_node() {
526 let ident_prefix = cursor.text[from_ident.offset()..cursor.cursor].to_string();
527
528 self.completions.retain(|item| {
529 let mut prefix_matcher = item.label.chars();
530 'ident_matching: for ch in ident_prefix.chars() {
531 for item in prefix_matcher.by_ref() {
532 if item == ch {
533 continue 'ident_matching;
534 }
535 }
536
537 return false;
538 }
539
540 true
541 });
542 }
543
544 for item in &mut self.completions {
545 if let Some(EcoTextEdit {
546 ref mut new_text, ..
547 }) = item.text_edit
548 {
549 *new_text = to_lsp_snippet(new_text);
550 }
551 }
552
553 Some(())
554 }
555}
556
557struct CompletionPair<'a, 'b, 'c> {
558 worker: &'c mut CompletionWorker<'a>,
559 cursor: &'c mut Cursor<'b>,
560}
561
562type Pair<'a, 'b, 'c> = CompletionPair<'a, 'b, 'c>;
563
564impl CompletionPair<'_, '_, '_> {
565 pub(crate) fn complete_cursor(&mut self) -> Option<()> {
567 use SurroundingSyntax::*;
568
569 if matches!(
571 self.cursor.leaf.kind(),
572 SyntaxKind::LineComment | SyntaxKind::BlockComment
573 ) {
574 return self.complete_comments().then_some(());
575 }
576
577 let surrounding_syntax = self.cursor.surrounding_syntax;
578 let mode = self.cursor.leaf_mode();
579
580 if matches!(surrounding_syntax, ImportList) {
582 return self.complete_imports().then_some(());
583 }
584
585 if matches!(surrounding_syntax, ParamList) {
587 return self.complete_params();
588 }
589
590 match self.cursor.syntax_context.clone() {
592 Some(SyntaxContext::Element { container, .. }) => {
593 if let Some(container) = container.cast::<ast::Dict>() {
595 for named in container.items() {
596 if let ast::DictItem::Named(named) = named {
597 self.worker.seen_field(named.name().into());
598 }
599 }
600 };
601 }
602 Some(SyntaxContext::Arg { args, .. }) => {
603 let args = args.cast::<ast::Args>()?;
605 for arg in args.items() {
606 if let ast::Arg::Named(named) = arg {
607 self.worker.seen_field(named.name().into());
608 }
609 }
610 }
611 Some(SyntaxContext::VarAccess(
613 var @ (VarClass::FieldAccess { .. } | VarClass::DotAccess { .. }),
614 )) => {
615 let target = var.accessed_node()?;
616 let field = var.accessing_field()?;
617
618 self.cursor.from = field.offset(&self.cursor.source)?;
619
620 self.doc_access_completions(&target);
621 return Some(());
622 }
623 Some(SyntaxContext::ImportPath(path) | SyntaxContext::IncludePath(path)) => {
624 let Some(ast::Expr::Str(str)) = path.cast() else {
625 return None;
626 };
627 self.cursor.from = path.offset();
628 let value = str.get();
629 if value.starts_with('@') {
630 let all_versions = value.contains(':');
631 self.package_completions(all_versions);
632 return Some(());
633 } else {
634 let paths = self.complete_path(&crate::analysis::PathKind::Source {
635 allow_package: true,
636 });
637 self.worker.completions.extend(paths.unwrap_or_default());
639 }
640
641 return Some(());
642 }
643 Some(SyntaxContext::Ref {
645 node,
646 suffix_colon: _,
647 }) => {
648 self.cursor.from = node.offset() + 1;
649 self.ref_completions();
650 return Some(());
651 }
652 Some(
653 SyntaxContext::VarAccess(VarClass::Ident { .. })
654 | SyntaxContext::Paren { .. }
655 | SyntaxContext::Label { .. }
656 | SyntaxContext::Normal(..),
657 )
658 | None => {}
659 }
660
661 let cursor_pos = bad_completion_cursor(
662 self.cursor.syntax.as_ref(),
663 self.cursor.syntax_context.as_ref(),
664 &self.cursor.leaf,
665 );
666
667 let ty = self
669 .worker
670 .ctx
671 .post_type_of_node(self.cursor.leaf.clone())
672 .filter(|ty| !matches!(ty, Ty::Any))
673 .filter(|_| !matches!(cursor_pos, Some(BadCompletionCursor::ArgListPos)));
676
677 crate::log_debug_ct!(
678 "complete_type: {:?} -> ({surrounding_syntax:?}, {ty:#?})",
679 self.cursor.leaf
680 );
681
682 if is_ident_like(&self.cursor.leaf) {
686 self.cursor.from = self.cursor.leaf.offset();
687 } else if let Some(offset) = self
688 .cursor
689 .syntax
690 .as_ref()
691 .and_then(SyntaxClass::complete_offset)
692 {
693 self.cursor.from = offset;
694 }
695
696 if let Some(ty) = ty {
698 let filter = |ty: &Ty| match surrounding_syntax {
699 SurroundingSyntax::StringContent => match ty {
700 Ty::Builtin(
701 BuiltinTy::Path(..) | BuiltinTy::TextFont | BuiltinTy::TextFeature,
702 ) => true,
703 Ty::Value(val) => matches!(val.val, Value::Str(..)),
704 _ => false,
705 },
706 _ => true,
707 };
708 let mut ctx = TypeCompletionWorker {
709 base: self,
710 filter: &filter,
711 };
712 ctx.type_completion(&ty, None);
713 }
714 let mut type_completions = std::mem::take(&mut self.worker.completions);
715
716 match mode {
718 InterpretMode::Code => {
719 self.complete_code();
720 }
721 InterpretMode::Math => {
722 self.complete_math();
723 }
724 InterpretMode::Raw => {
725 self.complete_markup();
726 }
727 InterpretMode::Markup => match surrounding_syntax {
728 Regular => {
729 self.complete_markup();
730 }
731 Selector | ShowTransform | SetRule => {
732 self.complete_code();
733 }
734 StringContent | ImportList | ParamList => {}
735 },
736 InterpretMode::Comment | InterpretMode::String => {}
737 };
738
739 match surrounding_syntax {
741 Regular | StringContent | ImportList | ParamList | SetRule => {}
742 Selector => {
743 self.snippet_completion(
744 "text selector",
745 "\"${text}\"",
746 "Replace occurrences of specific text.",
747 );
748
749 self.snippet_completion(
750 "regex selector",
751 "regex(\"${regex}\")",
752 "Replace matches of a regular expression.",
753 );
754 }
755 ShowTransform => {
756 self.snippet_completion(
757 "replacement",
758 "[${content}]",
759 "Replace the selected element with content.",
760 );
761
762 self.snippet_completion(
763 "replacement (string)",
764 "\"${text}\"",
765 "Replace the selected element with a string of text.",
766 );
767
768 self.snippet_completion(
769 "transformation",
770 "element => [${content}]",
771 "Transform the element with a function.",
772 );
773 }
774 }
775
776 crate::log_debug_ct!(
786 "sort completions: {type_completions:#?} {:#?}",
787 self.worker.completions
788 );
789
790 type_completions.sort_by(|a, b| {
792 a.sort_text
793 .as_ref()
794 .cmp(&b.sort_text.as_ref())
795 .then_with(|| a.label.cmp(&b.label))
796 });
797 self.worker.completions.sort_by(|a, b| {
798 a.sort_text
799 .as_ref()
800 .cmp(&b.sort_text.as_ref())
801 .then_with(|| a.label.cmp(&b.label))
802 });
803
804 for (idx, compl) in type_completions
805 .iter_mut()
806 .chain(self.worker.completions.iter_mut())
807 .enumerate()
808 {
809 compl.sort_text = Some(eco_format!("{idx:03}"));
810 }
811
812 self.worker.completions.append(&mut type_completions);
813
814 crate::log_debug_ct!("sort completions after: {:#?}", self.worker.completions);
815
816 if let Some(node) = self.cursor.arg_cursor() {
817 crate::log_debug_ct!("content block compl: args {node:?}");
818 let is_unclosed = matches!(node.kind(), SyntaxKind::Args)
819 && node.children().fold(0i32, |acc, node| match node.kind() {
820 SyntaxKind::LeftParen => acc + 1,
821 SyntaxKind::RightParen => acc - 1,
822 SyntaxKind::Error if node.text() == "(" => acc + 1,
823 SyntaxKind::Error if node.text() == ")" => acc - 1,
824 _ => acc,
825 }) > 0;
826 if is_unclosed {
827 self.worker.enrich("", ")");
828 }
829 }
830
831 if self.cursor.before.ends_with(',') || self.cursor.before.ends_with(':') {
832 self.worker.enrich(" ", "");
833 }
834 match surrounding_syntax {
835 Regular | ImportList | ParamList | ShowTransform | SetRule | StringContent => {}
836 Selector => {
837 self.worker.enrich("", ": ${}");
838 }
839 }
840
841 crate::log_debug_ct!("enrich completions: {:?}", self.worker.completions);
842
843 Some(())
844 }
845
846 fn push_completion(&mut self, completion: Completion) {
848 self.worker
849 .completions
850 .push(self.cursor.lsp_item_of(&completion));
851 }
852}
853
854pub fn symbol_detail(ch: char) -> EcoString {
857 let ld = symbol_label_detail(ch);
858 if ld.starts_with("\\u") {
859 return ld;
860 }
861 format!("{}, unicode: `\\u{{{:04x}}}`", ld, ch as u32).into()
862}
863
864pub fn symbol_label_detail(ch: char) -> EcoString {
867 if !ch.is_whitespace() && !ch.is_control() {
868 return ch.into();
869 }
870 match ch {
871 ' ' => "space".into(),
872 '\t' => "tab".into(),
873 '\n' => "newline".into(),
874 '\r' => "carriage return".into(),
875 '\u{200D}' => "zero width joiner".into(),
877 '\u{200C}' => "zero width non-joiner".into(),
878 '\u{200B}' => "zero width space".into(),
879 '\u{2060}' => "word joiner".into(),
880 '\u{00A0}' => "non-breaking space".into(),
882 '\u{202F}' => "narrow no-break space".into(),
883 '\u{2002}' => "en space".into(),
884 '\u{2003}' => "em space".into(),
885 '\u{2004}' => "three-per-em space".into(),
886 '\u{2005}' => "four-per-em space".into(),
887 '\u{2006}' => "six-per-em space".into(),
888 '\u{2007}' => "figure space".into(),
889 '\u{205f}' => "medium mathematical space".into(),
890 '\u{2008}' => "punctuation space".into(),
891 '\u{2009}' => "thin space".into(),
892 '\u{200A}' => "hair space".into(),
893 _ => format!("\\u{{{:04x}}}", ch as u32).into(),
894 }
895}
896
897fn slice_at(s: &str, mut rng: Range<usize>) -> &str {
899 while !rng.is_empty() && !s.is_char_boundary(rng.start) {
900 rng.start += 1;
901 }
902 while !rng.is_empty() && !s.is_char_boundary(rng.end) {
903 rng.end -= 1;
904 }
905
906 if rng.is_empty() {
907 return "";
908 }
909
910 &s[rng]
911}
912
913static TYPST_SNIPPET_PLACEHOLDER_RE: LazyLock<Regex> =
914 LazyLock::new(|| Regex::new(r"\$\{(.*?)\}").unwrap());
915
916fn to_lsp_snippet(typst_snippet: &str) -> EcoString {
918 let mut counter = 1;
919 let result = TYPST_SNIPPET_PLACEHOLDER_RE.replace_all(typst_snippet, |cap: &Captures| {
920 let substitution = format!("${{{}:{}}}", counter, &cap[1]);
921 counter += 1;
922 substitution
923 });
924
925 result.into()
926}
927
928fn is_hash_expr(leaf: &LinkedNode<'_>) -> bool {
929 is_hash_expr_(leaf).is_some()
930}
931
932fn is_hash_expr_(leaf: &LinkedNode<'_>) -> Option<()> {
933 match leaf.kind() {
934 SyntaxKind::Hash => Some(()),
935 SyntaxKind::Ident => {
936 let prev_leaf = leaf.prev_leaf()?;
937 if prev_leaf.kind() == SyntaxKind::Hash {
938 Some(())
939 } else {
940 None
941 }
942 }
943 _ => None,
944 }
945}
946
947fn is_triggered_by_punc(trigger_character: Option<char>) -> bool {
948 trigger_character.is_some_and(|ch| ch.is_ascii_punctuation())
949}
950
951fn is_arg_like_context(mut matching: &LinkedNode) -> bool {
952 while let Some(parent) = matching.parent() {
953 use SyntaxKind::*;
954
955 match parent.kind() {
957 ContentBlock | Equation | CodeBlock | Markup | Math | Code => return false,
958 Args | Params | Destructuring | Array | Dict => return true,
959 _ => {}
960 }
961
962 matching = parent;
963 }
964 false
965}
966
967fn deserialize_null_default<'de, D, T>(deserializer: D) -> Result<T, D::Error>
998where
999 T: Default + Deserialize<'de>,
1000 D: serde::Deserializer<'de>,
1001{
1002 let opt = Option::deserialize(deserializer)?;
1003 Ok(opt.unwrap_or_default())
1004}
1005
1006#[cfg(test)]
1009mod tests {
1010 use super::slice_at;
1011
1012 #[test]
1013 fn test_before() {
1014 const TEST_UTF8_STR: &str = "我们";
1015 for i in 0..=TEST_UTF8_STR.len() {
1016 for j in 0..=TEST_UTF8_STR.len() {
1017 let _s = std::hint::black_box(slice_at(TEST_UTF8_STR, i..j));
1018 }
1019 }
1020 }
1021}