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            None
264        })
265    }
266
267    /// Gets the argument cursor.
268    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    /// Gets the LSP range of a given range with caching.
300    fn lsp_range_of(&mut self, rng: Range<usize>) -> LspRange {
301        // self.ctx.to_lsp_range(rng, &self.source)
302        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    /// Makes a full completion item from a cursor-insensitive completion.
314    fn lsp_item_of(&mut self, item: &Completion) -> LspCompletion {
315        // Determine range to replace
316        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 modifying some arguments, we need to truncate and add a comma
322                if !self.is_callee() && self.cursor != rng.end && is_arg_like_context(from_ident) {
323                    // extend comma
324                    if !snippet.trim_end().ends_with(',') {
325                        snippet.push_str(", ");
326                    }
327
328                    // Truncate
329                    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
374/// Alias for a completion cursor, [`CompletionCursor`].
375type Cursor<'a> = CompletionCursor<'a>;
376
377/// A node selected by [`CompletionCursor`].
378enum SelectedNode<'a> {
379    /// Selects an identifier, e.g. `foobar|` or `foo|bar`.
380    Ident(LinkedNode<'a>),
381    /// Selects a label, e.g. `<foobar|>` or `<foo|bar>`.
382    Label(LinkedNode<'a>),
383    /// Selects a reference, e.g. `@foobar|` or `@foo|bar`.
384    Ref(LinkedNode<'a>),
385}
386
387/// Autocomplete a cursor position in a source file.
388///
389/// Returns the position from which the completions apply and a list of
390/// completions.
391///
392/// When `explicit` is `true`, the user requested the completion by pressing
393/// control and space or something similar.
394///
395/// Passing a `document` (from a previous compilation) is optional, but
396/// enhances the autocompletions. Label completions, for instance, are
397/// only generated when the document is available.
398pub struct CompletionWorker<'a> {
399    /// The completions.
400    pub completions: Vec<LspCompletion>,
401    /// Whether the completion is incomplete.
402    pub incomplete: bool,
403
404    /// The analysis local context.
405    ctx: &'a mut LocalContext,
406    /// The compiled document.
407    document: Option<&'a TypstDocument>,
408    /// Whether the completion was explicitly requested.
409    explicit: bool,
410    /// The trigger character.
411    trigger_character: Option<char>,
412    /// The set of cast completions seen so far.
413    seen_casts: HashSet<u128>,
414    /// The set of type completions seen so far.
415    seen_types: HashSet<Ty>,
416    /// The set of field completions seen so far.
417    seen_fields: HashSet<Interned<str>>,
418}
419
420impl<'a> CompletionWorker<'a> {
421    /// Create a completion worker.
422    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    /// Gets the world.
442    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    /// Adds a prefix and suffix to all applications.
451    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    // if ctx.before.ends_with(':') {
463    //     ctx.enrich(" ", "");
464    // }
465
466    /// Starts the completion process.
467    pub(crate) fn work(&mut self, cursor: &mut Cursor) -> Option<()> {
468        // Skips if is the let binding item *directly*
469        if let Some(SyntaxClass::VarAccess(var)) = &cursor.syntax {
470            let node = var.node();
471            match node.parent_kind() {
472                // complete the init part of the let binding
473                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        // Skips if an error node starts with number (e.g. `1pt`)
490        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        // Excludes it self from auto completion
508        // e.g. `#let x = (1.);`
509        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        // Filters
525        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    /// Starts the completion on a cursor.
566    pub(crate) fn complete_cursor(&mut self) -> Option<()> {
567        use SurroundingSyntax::*;
568
569        // Special completions, we should remove them finally
570        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        // Special completions 2, we should remove them finally
581        if matches!(surrounding_syntax, ImportList) {
582            return self.complete_imports().then_some(());
583        }
584
585        // Special completions 3, we should remove them finally
586        if matches!(surrounding_syntax, ParamList) {
587            return self.complete_params();
588        }
589
590        // Checks and completes `self.cursor.syntax_context`
591        match self.cursor.syntax_context.clone() {
592            Some(SyntaxContext::Element { container, .. }) => {
593                // The existing dictionary fields are not interesting
594                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                // The existing arguments are not interesting
604                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            // todo: complete field by types
612            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                    // todo: remove ctx.completions
638                    self.worker.completions.extend(paths.unwrap_or_default());
639                }
640
641                return Some(());
642            }
643            // todo: complete reference by type
644            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        // Triggers a complete type checking.
668        let ty = self
669            .worker
670            .ctx
671            .post_type_of_node(self.cursor.leaf.clone())
672            .filter(|ty| !matches!(ty, Ty::Any))
673            // Forbids argument completion list if the cursor is in a bad position. This will
674            // prevent the completion list from showing up.
675            .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        // Adjusts the completion position
683        // todo: syntax class seems not being considering `is_ident_like`
684        // todo: merge ident_content_offset and label_content_offset
685        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        // Completion by types.
697        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        // Completion by [`crate::syntax::InterpretMode`].
717        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        // Snippet completions associated by surrounding_syntax.
740        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        // todo: filter completions by type
777        // ctx.strict_scope_completions(false, |value| value.ty() == *ty);
778        // let length_ty = Type::of::<Length>();
779        // ctx.strict_scope_completions(false, |value| value.ty() == length_ty);
780        // let color_ty = Type::of::<Color>();
781        // ctx.strict_scope_completions(false, |value| value.ty() == color_ty);
782        // let ty = Type::of::<Dir>();
783        // ctx.strict_scope_completions(false, |value| value.ty() == ty);
784
785        crate::log_debug_ct!(
786            "sort completions: {type_completions:#?} {:#?}",
787            self.worker.completions
788        );
789
790        // Sorts completions
791        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    /// Pushes a cursor-insensitive completion item.
847    fn push_completion(&mut self, completion: Completion) {
848        self.worker
849            .completions
850            .push(self.cursor.lsp_item_of(&completion));
851    }
852}
853
854/// If is printable, return the symbol itself.
855/// Otherwise, return the symbol's unicode detailed description.
856pub 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
864/// If is printable, return the symbol itself.
865/// Otherwise, return the symbol's unicode description.
866pub 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        // replacer
876        '\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        // spaces
881        '\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
897/// Slices a smaller string at character boundaries safely.
898fn 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
916/// Adds numbering to placeholders in snippets
917fn 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        // todo: contextual
956        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
967// if param.attrs.named {
968//     match param.ty {
969//         Ty::Builtin(BuiltinTy::TextSize) => {
970//             for size_template in &[
971//                 "10.5pt", "12pt", "9pt", "14pt", "8pt", "16pt", "18pt",
972// "20pt", "22pt",                 "24pt", "28pt",
973//             ] {
974//                 let compl = compl.clone();
975//                 ctx.completions.push(Completion {
976//                     label: eco_format!("{}: {}", param.name, size_template),
977//                     apply: None,
978//                     ..compl
979//                 });
980//             }
981//         }
982//         Ty::Builtin(BuiltinTy::Dir) => {
983//             for dir_template in &["ltr", "rtl", "ttb", "btt"] {
984//                 let compl = compl.clone();
985//                 ctx.completions.push(Completion {
986//                     label: eco_format!("{}: {}", param.name, dir_template),
987//                     apply: None,
988//                     ..compl
989//                 });
990//             }
991//         }
992//         _ => {}
993//     }
994//     ctx.completions.push(compl);
995// }
996
997fn 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// todo: doesn't complete parameter now, which is not good.
1007
1008#[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}