tinymist_query/analysis/
completion.rs

1//! Provides completions for the document.
2
3use 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/// Tinymist's completion features.
68#[derive(Default, Debug, Clone, Serialize, Deserialize)]
69#[serde(rename_all = "camelCase")]
70pub struct CompletionFeat {
71    /// Whether to trigger completions on arguments (placeholders) of snippets.
72    #[serde(default, deserialize_with = "deserialize_null_default")]
73    pub trigger_on_snippet_placeholders: bool,
74    /// Whether supports trigger suggest completion, a.k.a. auto-completion.
75    #[serde(default, deserialize_with = "deserialize_null_default")]
76    pub trigger_suggest: bool,
77    /// Whether supports trigger parameter hint, a.k.a. signature help.
78    #[serde(default, deserialize_with = "deserialize_null_default")]
79    pub trigger_parameter_hints: bool,
80    /// Whether supports trigger the command combining suggest and parameter
81    /// hints.
82    #[serde(default, deserialize_with = "deserialize_null_default")]
83    pub trigger_suggest_and_parameter_hints: bool,
84
85    /// The Way to complete symbols.
86    pub symbol: Option<SymbolCompletionWay>,
87
88    /// Whether to enable postfix completion.
89    pub postfix: Option<bool>,
90    /// Whether to enable ufcs completion.
91    pub postfix_ufcs: Option<bool>,
92    /// Whether to enable ufcs completion (left variant).
93    pub postfix_ufcs_left: Option<bool>,
94    /// Whether to enable ufcs completion (right variant).
95    pub postfix_ufcs_right: Option<bool>,
96    /// Postfix snippets.
97    pub postfix_snippets: Option<EcoVec<PostfixSnippet>>,
98}
99
100impl CompletionFeat {
101    /// Whether to enable any postfix completion.
102    pub(crate) fn postfix(&self) -> bool {
103        self.postfix.unwrap_or(true)
104    }
105
106    /// Whether to enable any ufcs completion.
107    pub(crate) fn any_ufcs(&self) -> bool {
108        self.ufcs() || self.ufcs_left() || self.ufcs_right()
109    }
110
111    /// Whether to enable ufcs completion.
112    pub(crate) fn ufcs(&self) -> bool {
113        self.postfix() && self.postfix_ufcs.unwrap_or(true)
114    }
115
116    /// Whether to enable ufcs completion (left variant).
117    pub(crate) fn ufcs_left(&self) -> bool {
118        self.postfix() && self.postfix_ufcs_left.unwrap_or(true)
119    }
120
121    /// Whether to enable ufcs completion (right variant).
122    pub(crate) fn ufcs_right(&self) -> bool {
123        self.postfix() && self.postfix_ufcs_right.unwrap_or(true)
124    }
125
126    /// Gets the postfix snippets.
127    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/// Whether to make symbol completion stepless. For example, `$ar|$` will be
139/// completed to `$arrow.r$`. Hint: Restarting the editor is required to change
140/// this setting.
141#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
142#[serde(rename_all = "camelCase")]
143pub enum SymbolCompletionWay {
144    /// Complete symbols step by step
145    Step,
146    /// Complete symbols steplessly
147    Stepless,
148}
149
150/// The struct describing how a completion worker views the editor's cursor.
151pub struct CompletionCursor<'a> {
152    /// The shared context
153    ctx: Arc<SharedContext>,
154    /// The position from which the completions apply.
155    from: usize,
156    /// The cursor position.
157    cursor: usize,
158    /// The parsed source.
159    source: Source,
160    /// The source text.
161    text: &'a str,
162    /// The text before the cursor.
163    before: &'a str,
164    /// The text after the cursor.
165    after: &'a str,
166    /// The leaf node at the cursor.
167    leaf: LinkedNode<'a>,
168    /// The syntax class at the cursor.
169    syntax: Option<SyntaxClass<'a>>,
170    /// The syntax context at the cursor.
171    syntax_context: Option<SyntaxContext<'a>>,
172    /// The surrounding syntax at the cursor
173    surrounding_syntax: SurroundingSyntax,
174
175    /// Cache for the last lsp range conversion.
176    last_lsp_range_pair: Option<(Range<usize>, LspRange)>,
177    /// Cache for the ident cursor.
178    ident_cursor: OnceLock<Option<SelectedNode<'a>>>,
179    /// Cache for the arg cursor.
180    arg_cursor: OnceLock<Option<SyntaxNode>>,
181}
182
183impl<'a> CompletionCursor<'a> {
184    /// Creates a completion cursor.
185    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        // todo: cache
190        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    /// A small window of context before the cursor.
216    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    /// Whether the cursor is related to a callee item.
224    fn is_callee(&self) -> bool {
225        matches!(self.syntax, Some(SyntaxClass::Callee(..)))
226    }
227
228    /// Gets the interpret mode at the cursor.
229    pub fn leaf_mode(&self) -> InterpretMode {
230        interpret_mode_at(Some(&self.leaf))
231    }
232
233    /// Gets selected node under cursor.
234    fn selected_node(&self) -> &Option<SelectedNode<'a>> {
235        self.ident_cursor.get_or_init(|| {
236            // identifier
237            // ^ from
238            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            // <identifier
248            //  ^ from
249            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            // @identifier
256            //  ^ from
257            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            // @identifier
264            //  ^ from
265            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    /// Gets the argument cursor.
276    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    /// Gets the LSP range of a given range with caching.
309    fn lsp_range_of(&mut self, rng: Range<usize>) -> LspRange {
310        // self.ctx.to_lsp_range(rng, &self.source)
311        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    /// Makes a full completion item from a cursor-insensitive completion.
323    fn lsp_item_of(&mut self, item: &Completion) -> LspCompletion {
324        // Determine range to replace
325        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 modifying some arguments, we need to truncate and add a comma
331                if !self.is_callee() && self.cursor != rng.end && is_arg_like_context(from_ident) {
332                    // extend comma
333                    if !snippet.trim_end().ends_with(',') {
334                        snippet.push_str(", ");
335                    }
336
337                    // Truncate
338                    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
388/// Alias for a completion cursor, [`CompletionCursor`].
389type Cursor<'a> = CompletionCursor<'a>;
390
391/// A node selected by [`CompletionCursor`].
392enum SelectedNode<'a> {
393    /// Selects an identifier, e.g. `foobar|` or `foo|bar`.
394    Ident(LinkedNode<'a>),
395    /// Selects a label, e.g. `<foobar|>` or `<foo|bar>`.
396    Label(LinkedNode<'a>),
397    /// Selects a reference, e.g. `@foobar|` or `@foo|bar`.
398    Ref(LinkedNode<'a>),
399    /// Selects a `@` text, e.g. `@|`.
400    At(LinkedNode<'a>),
401}
402
403/// Autocomplete a cursor position in a source file.
404///
405/// Returns the position from which the completions apply and a list of
406/// completions.
407///
408/// When `explicit` is `true`, the user requested the completion by pressing
409/// control and space or something similar.
410///
411/// Passing a `document` (from a previous compilation) is optional, but
412/// enhances the autocompletions. Label completions, for instance, are
413/// only generated when the document is available.
414pub struct CompletionWorker<'a> {
415    /// The completions.
416    pub completions: Vec<LspCompletion>,
417    /// Whether the completion is incomplete.
418    pub incomplete: bool,
419
420    /// The analysis local context.
421    ctx: &'a mut LocalContext,
422    /// The compiled document.
423    document: Option<&'a TypstDocument>,
424    /// Whether the completion was explicitly requested.
425    explicit: bool,
426    /// The trigger character.
427    trigger_character: Option<char>,
428    /// The set of cast completions seen so far.
429    seen_casts: HashSet<u128>,
430    /// The set of type completions seen so far.
431    seen_types: HashSet<Ty>,
432    /// The set of field completions seen so far.
433    seen_fields: HashSet<Interned<str>>,
434}
435
436impl<'a> CompletionWorker<'a> {
437    /// Create a completion worker.
438    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    /// Gets the world.
458    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    /// Adds a prefix and suffix to all applications.
467    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    // if ctx.before.ends_with(':') {
479    //     ctx.enrich(" ", "");
480    // }
481
482    /// Starts the completion process.
483    pub(crate) fn work(&mut self, cursor: &mut Cursor) -> Option<()> {
484        // Skips if is the let binding item *directly*
485        if let Some(SyntaxClass::VarAccess(var)) = &cursor.syntax {
486            let node = var.node();
487            match node.parent_kind() {
488                // complete the init part of the let binding
489                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        // Skips if an error node starts with number (e.g. `1pt`)
506        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        // Excludes it self from auto completion
524        // e.g. `#let x = (1.);`
525        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        // Filters
541        // todo: reference filter
542        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    /// Starts the completion on a cursor.
583    pub(crate) fn complete_cursor(&mut self) -> Option<()> {
584        use SurroundingSyntax::*;
585
586        // Special completions, we should remove them finally
587        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        // Special completions 2, we should remove them finally
598        if matches!(surrounding_syntax, ImportList) {
599            return self.complete_imports().then_some(());
600        }
601
602        // Special completions 3, we should remove them finally
603        if matches!(surrounding_syntax, ParamList) {
604            return self.complete_params();
605        }
606
607        // Checks and completes `self.cursor.syntax_context`
608        match self.cursor.syntax_context.clone() {
609            Some(SyntaxContext::Element { container, .. }) => {
610                // The existing dictionary fields are not interesting
611                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                // The existing arguments are not interesting
621                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            // todo: complete field by types
629            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                    // todo: remove ctx.completions
655                    self.worker.completions.extend(paths.unwrap_or_default());
656                }
657
658                return Some(());
659            }
660            // todo: complete reference by type
661            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        // Triggers a complete type checking.
688        let ty = self
689            .worker
690            .ctx
691            .post_type_of_node(self.cursor.leaf.clone())
692            .filter(|ty| !matches!(ty, Ty::Any))
693            // Forbids argument completion list if the cursor is in a bad position. This will
694            // prevent the completion list from showing up.
695            .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        // Adjusts the completion position
703        // todo: syntax class seems not being considering `is_ident_like`
704        // todo: merge ident_content_offset and label_content_offset
705        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        // Completion by types.
717        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        // Completion by [`crate::syntax::InterpretMode`].
737        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        // Snippet completions associated by surrounding_syntax.
760        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        // todo: filter completions by type
797        // ctx.strict_scope_completions(false, |value| value.ty() == *ty);
798        // let length_ty = Type::of::<Length>();
799        // ctx.strict_scope_completions(false, |value| value.ty() == length_ty);
800        // let color_ty = Type::of::<Color>();
801        // ctx.strict_scope_completions(false, |value| value.ty() == color_ty);
802        // let ty = Type::of::<Dir>();
803        // ctx.strict_scope_completions(false, |value| value.ty() == ty);
804
805        crate::log_debug_ct!(
806            "sort completions: {type_completions:#?} {:#?}",
807            self.worker.completions
808        );
809
810        // Sorts completions
811        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    /// Pushes a cursor-insensitive completion item.
867    fn push_completion(&mut self, completion: Completion) {
868        self.worker
869            .completions
870            .push(self.cursor.lsp_item_of(&completion));
871    }
872}
873
874/// If is printable, return the symbol itself.
875/// Otherwise, return the symbol's unicode detailed description.
876pub 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
896/// If is printable, return the symbol itself.
897/// Otherwise, return the symbol's unicode description.
898pub 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        // replacer
925        '\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        // spaces
930        '\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
946/// Slices a smaller string at character boundaries safely.
947fn 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
965/// Adds numbering to placeholders in snippets
966fn 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        // todo: contextual
1005        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
1016// if param.attrs.named {
1017//     match param.ty {
1018//         Ty::Builtin(BuiltinTy::TextSize) => {
1019//             for size_template in &[
1020//                 "10.5pt", "12pt", "9pt", "14pt", "8pt", "16pt", "18pt",
1021// "20pt", "22pt",                 "24pt", "28pt",
1022//             ] {
1023//                 let compl = compl.clone();
1024//                 ctx.completions.push(Completion {
1025//                     label: eco_format!("{}: {}", param.name, size_template),
1026//                     apply: None,
1027//                     ..compl
1028//                 });
1029//             }
1030//         }
1031//         Ty::Builtin(BuiltinTy::Dir) => {
1032//             for dir_template in &["ltr", "rtl", "ttb", "btt"] {
1033//                 let compl = compl.clone();
1034//                 ctx.completions.push(Completion {
1035//                     label: eco_format!("{}: {}", param.name, dir_template),
1036//                     apply: None,
1037//                     ..compl
1038//                 });
1039//             }
1040//         }
1041//         _ => {}
1042//     }
1043//     ctx.completions.push(compl);
1044// }
1045
1046fn 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// todo: doesn't complete parameter now, which is not good.
1056
1057#[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}