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.clone(),
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(s: &str) -> EcoString {
857    let ld = symbol_label_detail(s);
858    if ld.starts_with("\\u") {
859        return ld;
860    }
861
862    let mut chars = s.chars();
863    let unicode_repr = if let (Some(ch), None) = (chars.next(), chars.next()) {
864        format!("\\u{{{:04x}}}", ch as u32)
865    } else {
866        let codes: Vec<String> = s
867            .chars()
868            .map(|ch| format!("\\u{{{:04x}}}", ch as u32))
869            .collect();
870        codes.join(" + ")
871    };
872
873    format!("{ld}, unicode: `{unicode_repr}`").into()
874}
875
876pub fn symbol_label_detail(s: &str) -> EcoString {
879    let mut chars = s.chars();
880    if let (Some(ch), None) = (chars.next(), chars.next()) {
881        return symbol_label_detail_single_char(ch);
882    }
883
884    if s.chars().all(|ch| !ch.is_whitespace() && !ch.is_control()) {
885        return s.into();
886    }
887
888    let codes: Vec<String> = s
889        .chars()
890        .map(|ch| format!("\\u{{{:04x}}}", ch as u32))
891        .collect();
892    codes.join(" + ").into()
893}
894
895fn symbol_label_detail_single_char(ch: char) -> EcoString {
896    if !ch.is_whitespace() && !ch.is_control() {
897        return ch.into();
898    }
899    match ch {
900        ' ' => "space".into(),
901        '\t' => "tab".into(),
902        '\n' => "newline".into(),
903        '\r' => "carriage return".into(),
904        '\u{200D}' => "zero width joiner".into(),
906        '\u{200C}' => "zero width non-joiner".into(),
907        '\u{200B}' => "zero width space".into(),
908        '\u{2060}' => "word joiner".into(),
909        '\u{00A0}' => "non-breaking space".into(),
911        '\u{202F}' => "narrow no-break space".into(),
912        '\u{2002}' => "en space".into(),
913        '\u{2003}' => "em space".into(),
914        '\u{2004}' => "three-per-em space".into(),
915        '\u{2005}' => "four-per-em space".into(),
916        '\u{2006}' => "six-per-em space".into(),
917        '\u{2007}' => "figure space".into(),
918        '\u{205f}' => "medium mathematical space".into(),
919        '\u{2008}' => "punctuation space".into(),
920        '\u{2009}' => "thin space".into(),
921        '\u{200A}' => "hair space".into(),
922        _ => format!("\\u{{{:04x}}}", ch as u32).into(),
923    }
924}
925
926fn slice_at(s: &str, mut rng: Range<usize>) -> &str {
928    while !rng.is_empty() && !s.is_char_boundary(rng.start) {
929        rng.start += 1;
930    }
931    while !rng.is_empty() && !s.is_char_boundary(rng.end) {
932        rng.end -= 1;
933    }
934
935    if rng.is_empty() {
936        return "";
937    }
938
939    &s[rng]
940}
941
942static TYPST_SNIPPET_PLACEHOLDER_RE: LazyLock<Regex> =
943    LazyLock::new(|| Regex::new(r"\$\{(.*?)\}").unwrap());
944
945fn to_lsp_snippet(typst_snippet: &str) -> EcoString {
947    let mut counter = 1;
948    let result = TYPST_SNIPPET_PLACEHOLDER_RE.replace_all(typst_snippet, |cap: &Captures| {
949        let substitution = format!("${{{}:{}}}", counter, &cap[1]);
950        counter += 1;
951        substitution
952    });
953
954    result.into()
955}
956
957fn is_hash_expr(leaf: &LinkedNode<'_>) -> bool {
958    is_hash_expr_(leaf).is_some()
959}
960
961fn is_hash_expr_(leaf: &LinkedNode<'_>) -> Option<()> {
962    match leaf.kind() {
963        SyntaxKind::Hash => Some(()),
964        SyntaxKind::Ident => {
965            let prev_leaf = leaf.prev_leaf()?;
966            if prev_leaf.kind() == SyntaxKind::Hash {
967                Some(())
968            } else {
969                None
970            }
971        }
972        _ => None,
973    }
974}
975
976fn is_triggered_by_punc(trigger_character: Option<char>) -> bool {
977    trigger_character.is_some_and(|ch| ch.is_ascii_punctuation())
978}
979
980fn is_arg_like_context(mut matching: &LinkedNode) -> bool {
981    while let Some(parent) = matching.parent() {
982        use SyntaxKind::*;
983
984        match parent.kind() {
986            ContentBlock | Equation | CodeBlock | Markup | Math | Code => return false,
987            Args | Params | Destructuring | Array | Dict => return true,
988            _ => {}
989        }
990
991        matching = parent;
992    }
993    false
994}
995
996fn deserialize_null_default<'de, D, T>(deserializer: D) -> Result<T, D::Error>
1027where
1028    T: Default + Deserialize<'de>,
1029    D: serde::Deserializer<'de>,
1030{
1031    let opt = Option::deserialize(deserializer)?;
1032    Ok(opt.unwrap_or_default())
1033}
1034
1035#[cfg(test)]
1038mod tests {
1039    use super::slice_at;
1040
1041    #[test]
1042    fn test_before() {
1043        const TEST_UTF8_STR: &str = "我们";
1044        for i in 0..=TEST_UTF8_STR.len() {
1045            for j in 0..=TEST_UTF8_STR.len() {
1046                let _s = std::hint::black_box(slice_at(TEST_UTF8_STR, i..j));
1047            }
1048        }
1049    }
1050}