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, NoneValue, Repr, Scope, StyleChain, Type, Value, fields_on, format_str, repr,
20};
21use typst::syntax::ast::{self, AstNode, Param};
22use typst::syntax::{is_id_continue, is_id_start, is_ident};
23use typst::text::RawElem;
24use typst_shim::{syntax::LinkedNodeExt, utils::hash128};
25use unscanny::Scanner;
26
27use crate::adt::interner::Interned;
28use crate::analysis::{BuiltinTy, LocalContext, PathKind, Ty};
29use crate::completion::{
30    Completion, CompletionCommand, CompletionContextKey, CompletionItem, CompletionKind,
31    DEFAULT_POSTFIX_SNIPPET, DEFAULT_PREFIX_SNIPPET, EcoCompletionTextEdit, EcoInsertReplaceEdit,
32    EcoTextEdit, ParsedSnippet, PostfixSnippet, PostfixSnippetScope, PrefixSnippet,
33};
34use crate::prelude::*;
35use crate::syntax::{
36    InterpretMode, PreviousDecl, SurroundingSyntax, SyntaxClass, SyntaxContext, VarClass,
37    classify_context, interpret_mode_at, is_ident_like, node_ancestors, previous_decls,
38    surrounding_syntax,
39};
40use crate::ty::{
41    DynTypeBounds, Iface, IfaceChecker, InsTy, SigTy, TyCtx, TypeInfo, TypeInterface, TypeVar,
42};
43use crate::upstream::{plain_docs_sentence, summarize_font_family};
44
45use super::SharedContext;
46
47mod field_access;
48mod func;
49mod import;
50mod kind;
51mod mode;
52mod param;
53mod path;
54mod scope;
55mod snippet;
56#[path = "completion/type.rs"]
57mod type_;
58mod typst_specific;
59use kind::*;
60use scope::*;
61use type_::*;
62
63type LspCompletion = CompletionItem;
64
65/// Tinymist's completion features.
66#[derive(Default, Debug, Clone, Serialize, Deserialize)]
67#[serde(rename_all = "camelCase")]
68pub struct CompletionFeat {
69    /// Whether to trigger completions on arguments (placeholders) of snippets.
70    #[serde(default, deserialize_with = "deserialize_null_default")]
71    pub trigger_on_snippet_placeholders: bool,
72    /// Whether supports trigger suggest completion, a.k.a. auto-completion.
73    #[serde(default, deserialize_with = "deserialize_null_default")]
74    pub trigger_suggest: bool,
75    /// Whether supports trigger parameter hint, a.k.a. signature help.
76    #[serde(default, deserialize_with = "deserialize_null_default")]
77    pub trigger_parameter_hints: bool,
78    /// Whether supports trigger the command combining suggest and parameter
79    /// hints.
80    #[serde(default, deserialize_with = "deserialize_null_default")]
81    pub trigger_suggest_and_parameter_hints: bool,
82    /// Whether the client supports LSP insert/replace completion text edits.
83    #[serde(skip)]
84    pub insert_replace_edit: bool,
85    /// Whether path completion may read directories from the host file system.
86    #[serde(skip)]
87    pub path_completion_by_filesystem: bool,
88
89    /// The Way to complete symbols.
90    pub symbol: Option<SymbolCompletionWay>,
91
92    /// Whether to enable postfix completion.
93    pub postfix: Option<bool>,
94    /// Whether to enable ufcs completion.
95    pub postfix_ufcs: Option<bool>,
96    /// Whether to enable ufcs completion (left variant).
97    pub postfix_ufcs_left: Option<bool>,
98    /// Whether to enable ufcs completion (right variant).
99    pub postfix_ufcs_right: Option<bool>,
100    /// Postfix snippets.
101    pub postfix_snippets: Option<EcoVec<PostfixSnippet>>,
102}
103
104impl CompletionFeat {
105    /// Whether to enable any postfix completion.
106    pub(crate) fn postfix(&self) -> bool {
107        self.postfix.unwrap_or(true)
108    }
109
110    /// Whether to enable any ufcs completion.
111    pub(crate) fn any_ufcs(&self) -> bool {
112        self.ufcs() || self.ufcs_left() || self.ufcs_right()
113    }
114
115    /// Whether to enable ufcs completion.
116    pub(crate) fn ufcs(&self) -> bool {
117        self.postfix() && self.postfix_ufcs.unwrap_or(true)
118    }
119
120    /// Whether to enable ufcs completion (left variant).
121    pub(crate) fn ufcs_left(&self) -> bool {
122        self.postfix() && self.postfix_ufcs_left.unwrap_or(true)
123    }
124
125    /// Whether to enable ufcs completion (right variant).
126    pub(crate) fn ufcs_right(&self) -> bool {
127        self.postfix() && self.postfix_ufcs_right.unwrap_or(true)
128    }
129
130    /// Gets the postfix snippets.
131    pub(crate) fn postfix_snippets(&self) -> &EcoVec<PostfixSnippet> {
132        self.postfix_snippets
133            .as_ref()
134            .unwrap_or(&DEFAULT_POSTFIX_SNIPPET)
135    }
136
137    pub(crate) fn is_stepless(&self) -> bool {
138        matches!(self.symbol, Some(SymbolCompletionWay::Stepless))
139    }
140}
141
142/// Whether to make symbol completion stepless. For example, `$ar|$` will be
143/// completed to `$arrow.r$`. Hint: Restarting the editor is required to change
144/// this setting.
145#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
146#[serde(rename_all = "camelCase")]
147pub enum SymbolCompletionWay {
148    /// Complete symbols step by step
149    Step,
150    /// Complete symbols steplessly
151    Stepless,
152}
153
154/// The struct describing how a completion worker views the editor's cursor.
155pub struct CompletionCursor<'a> {
156    /// The shared context
157    ctx: Arc<SharedContext>,
158    /// The position from which the completions apply.
159    from: usize,
160    /// The cursor position.
161    cursor: usize,
162    /// The parsed source.
163    source: Source,
164    /// The source text.
165    text: &'a str,
166    /// The text before the cursor.
167    before: &'a str,
168    /// The text after the cursor.
169    after: &'a str,
170    /// The leaf node at the cursor.
171    leaf: LinkedNode<'a>,
172    /// The syntax class at the cursor.
173    syntax: Option<SyntaxClass<'a>>,
174    /// The syntax context at the cursor.
175    syntax_context: Option<SyntaxContext<'a>>,
176    /// The surrounding syntax at the cursor
177    surrounding_syntax: SurroundingSyntax,
178
179    /// Cache for the last lsp range conversion.
180    last_lsp_range_pair: Option<(Range<usize>, LspRange)>,
181    /// Cache for the ident cursor.
182    ident_cursor: OnceLock<Option<SelectedNode<'a>>>,
183    /// Cache for the arg cursor.
184    arg_cursor: OnceLock<Option<SyntaxNode>>,
185}
186
187impl<'a> CompletionCursor<'a> {
188    /// Creates a completion cursor.
189    pub fn new(ctx: Arc<SharedContext>, source: &'a Source, cursor: usize) -> Option<Self> {
190        let text = source.text();
191        let root = LinkedNode::new(source.root());
192        let leaf = root.leaf_at_compat(cursor)?;
193        // todo: cache
194        let syntax = classify_syntax(leaf.clone(), cursor);
195        let syntax_context = classify_context(leaf.clone(), Some(cursor));
196        let surrounding_syntax = surrounding_syntax(&leaf);
197
198        crate::log_debug_ct!("CompletionCursor: syntax {leaf:?} -> {syntax:#?}");
199        crate::log_debug_ct!("CompletionCursor: context {leaf:?} -> {syntax_context:#?}");
200        crate::log_debug_ct!("CompletionCursor: surrounding {leaf:?} -> {surrounding_syntax:#?}");
201        Some(Self {
202            ctx,
203            text,
204            source: source.clone(),
205            before: &text[..cursor],
206            after: &text[cursor..],
207            leaf,
208            syntax,
209            syntax_context,
210            surrounding_syntax,
211            cursor,
212            from: cursor,
213            last_lsp_range_pair: None,
214            ident_cursor: OnceLock::new(),
215            arg_cursor: OnceLock::new(),
216        })
217    }
218
219    /// A small window of context before the cursor.
220    fn before_window(&self, size: usize) -> &str {
221        slice_at(
222            self.before,
223            self.cursor.saturating_sub(size)..self.before.len(),
224        )
225    }
226
227    /// Whether the cursor is related to a callee item.
228    fn is_callee(&self) -> bool {
229        matches!(self.syntax, Some(SyntaxClass::Callee(..)))
230    }
231
232    /// Gets the interpret mode at the cursor.
233    pub fn leaf_mode(&self) -> InterpretMode {
234        interpret_mode_at(Some(&self.leaf))
235    }
236
237    /// Gets selected node under cursor.
238    fn selected_node(&self) -> &Option<SelectedNode<'a>> {
239        self.ident_cursor.get_or_init(|| {
240            // identifier
241            // ^ from
242            let is_from_ident = matches!(
243                self.syntax,
244                Some(SyntaxClass::Callee(..) | SyntaxClass::VarAccess(..))
245            ) && is_ident_like(&self.leaf)
246                && self.leaf.offset() == self.from;
247            if is_from_ident {
248                return Some(SelectedNode::Ident(self.leaf.clone()));
249            }
250
251            // <identifier
252            //  ^ from
253            let is_from_label = matches!(self.syntax, Some(SyntaxClass::Label { .. }))
254                && self.leaf.offset() + 1 == self.from;
255            if is_from_label {
256                return Some(SelectedNode::Label(self.leaf.clone()));
257            }
258
259            // @identifier
260            //  ^ from
261            let is_from_ref = matches!(self.syntax, Some(SyntaxClass::Ref { .. }))
262                && self.leaf.offset() + 1 == self.from;
263            if is_from_ref {
264                return Some(SelectedNode::Ref(self.leaf.clone()));
265            }
266
267            // @identifier
268            //  ^ from
269            let is_from_ref = matches!(self.syntax, Some(SyntaxClass::At { .. }))
270                && self.leaf.offset() + 1 == self.from;
271            if is_from_ref {
272                return Some(SelectedNode::At(self.leaf.clone()));
273            }
274
275            None
276        })
277    }
278
279    /// Gets the argument cursor.
280    fn arg_cursor(&self) -> &Option<SyntaxNode> {
281        self.arg_cursor.get_or_init(|| {
282            let mut args_node = None;
283
284            match self.syntax_context.clone() {
285                Some(SyntaxContext::Arg { args, .. }) => {
286                    args_node = Some(args.get().clone());
287                }
288                Some(SyntaxContext::Normal(node))
289                    if (matches!(node.kind(), SyntaxKind::ContentBlock)
290                        && matches!(self.leaf.kind(), SyntaxKind::LeftBracket)) =>
291                {
292                    args_node = node.parent().map(|s| s.get().clone());
293                }
294                Some(
295                    SyntaxContext::Element { .. }
296                    | SyntaxContext::ImportPath(..)
297                    | SyntaxContext::IncludePath(..)
298                    | SyntaxContext::VarAccess(..)
299                    | SyntaxContext::Paren { .. }
300                    | SyntaxContext::Label { .. }
301                    | SyntaxContext::Ref { .. }
302                    | SyntaxContext::At { .. }
303                    | SyntaxContext::Normal(..),
304                )
305                | None => {}
306            }
307
308            args_node
309        })
310    }
311
312    /// Gets the LSP range of a given range with caching.
313    fn lsp_range_of(&mut self, rng: Range<usize>) -> LspRange {
314        // self.ctx.to_lsp_range(rng, &self.source)
315        if let Some((last_rng, last_lsp_rng)) = &self.last_lsp_range_pair
316            && *last_rng == rng
317        {
318            return *last_lsp_rng;
319        }
320
321        let lsp_rng = self.ctx.to_lsp_range(rng.clone(), &self.source);
322        self.last_lsp_range_pair = Some((rng, lsp_rng));
323        lsp_rng
324    }
325
326    fn insert_range_of(&self, replace: Range<usize>) -> Range<usize> {
327        let end = self.cursor.clamp(replace.start, replace.end);
328        replace.start..end
329    }
330
331    fn completion_text_edit(
332        &mut self,
333        insert: Range<usize>,
334        replace: Range<usize>,
335        new_text: EcoString,
336    ) -> EcoCompletionTextEdit {
337        let insert = self.lsp_range_of(insert);
338        let replace = self.lsp_range_of(replace);
339
340        if self.ctx.analysis.completion_feat.insert_replace_edit && insert != replace {
341            EcoInsertReplaceEdit::new(insert, replace, new_text).into()
342        } else {
343            EcoTextEdit::new(replace, new_text).into()
344        }
345    }
346
347    fn capture_suffix_as_first_arg(&self, snippet: &mut EcoString, replace: Range<usize>) -> bool {
348        if self.cursor >= replace.end {
349            return false;
350        }
351
352        let suffix = &self.text[self.cursor..replace.end];
353        if suffix.is_empty() || !suffix.chars().all(is_id_continue) {
354            return false;
355        }
356
357        fill_first_empty_placeholder(snippet, suffix)
358    }
359
360    fn string_content_range(&self) -> Option<Range<usize>> {
361        if !self.leaf.is::<ast::Str>() {
362            return None;
363        }
364
365        let str_range = self.leaf.range();
366        if str_range.end <= str_range.start + 1 {
367            return None;
368        }
369
370        let content_range = str_range.start + 1..str_range.end - 1;
371        if self.cursor == content_range.end || content_range.contains(&self.cursor) {
372            Some(content_range)
373        } else {
374            None
375        }
376    }
377
378    /// Makes a full completion item from a cursor-insensitive completion.
379    fn lsp_item_of(&mut self, item: &Completion) -> LspCompletion {
380        // Determine range to replace
381        let mut snippet = item.apply.as_ref().unwrap_or(&item.label).clone();
382        let mut replace_range = match self.selected_node() {
383            Some(SelectedNode::Ident(from_ident)) => {
384                let mut rng = from_ident.range();
385
386                // if modifying some arguments, we need to truncate and add a comma
387                if !self.is_callee() && self.cursor != rng.end && is_arg_like_context(from_ident) {
388                    // extend comma
389                    if !snippet.trim_end().ends_with(',') {
390                        snippet.push_str(", ");
391                    }
392
393                    // Truncate
394                    rng.end = self.cursor;
395                }
396
397                rng
398            }
399            Some(SelectedNode::Label(from_label)) => {
400                let mut rng = from_label.range();
401                if from_label.leaf_text().starts_with('<') && !snippet.starts_with('<') {
402                    rng.start += 1;
403                }
404                if from_label.leaf_text().ends_with('>') && !snippet.ends_with('>') {
405                    rng.end -= 1;
406                }
407
408                rng
409            }
410            Some(node @ (SelectedNode::At(from_ref) | SelectedNode::Ref(from_ref))) => {
411                let mut rng = if matches!(node, SelectedNode::At(..)) {
412                    let offset = from_ref.offset();
413                    offset..offset + 1
414                } else {
415                    from_ref.range()
416                };
417                if from_ref.leaf_text().starts_with('@') && !snippet.starts_with('@') {
418                    rng.start += 1;
419                }
420
421                rng
422            }
423            None => self.from..self.cursor,
424        };
425        if let Some(range) = self.string_content_range() {
426            if let Some(trimmed) = snippet.strip_prefix('"') {
427                snippet = trimmed.into();
428            }
429            if let Some(trimmed) = snippet.strip_suffix('"') {
430                snippet = trimmed.into();
431            }
432            replace_range = range;
433        }
434
435        let text_edit = if item.capture_suffix
436            && self.capture_suffix_as_first_arg(&mut snippet, replace_range.clone())
437        {
438            let replace_range = self.lsp_range_of(replace_range);
439            EcoTextEdit::new(replace_range, snippet).into()
440        } else {
441            let insert_range = self.insert_range_of(replace_range.clone());
442            self.completion_text_edit(insert_range, replace_range, snippet)
443        };
444
445        LspCompletion {
446            label: item.label.clone(),
447            kind: item.kind.clone(),
448            detail: item.detail.clone(),
449            sort_text: item.sort_text.clone(),
450            filter_text: item.filter_text.clone(),
451            label_details: item.label_details.clone().map(From::from),
452            text_edit: Some(text_edit),
453            additional_text_edits: item.additional_text_edits.clone(),
454            insert_text_format: Some(InsertTextFormat::SNIPPET),
455            command: item.command.clone(),
456            ..Default::default()
457        }
458    }
459}
460
461/// Alias for a completion cursor, [`CompletionCursor`].
462type Cursor<'a> = CompletionCursor<'a>;
463
464/// A node selected by [`CompletionCursor`].
465enum SelectedNode<'a> {
466    /// Selects an identifier, e.g. `foobar|` or `foo|bar`.
467    Ident(LinkedNode<'a>),
468    /// Selects a label, e.g. `<foobar|>` or `<foo|bar>`.
469    Label(LinkedNode<'a>),
470    /// Selects a reference, e.g. `@foobar|` or `@foo|bar`.
471    Ref(LinkedNode<'a>),
472    /// Selects a `@` text, e.g. `@|`.
473    At(LinkedNode<'a>),
474}
475
476/// Autocomplete a cursor position in a source file.
477///
478/// Returns the position from which the completions apply and a list of
479/// completions.
480///
481/// When `explicit` is `true`, the user requested the completion by pressing
482/// control and space or something similar.
483///
484/// Passing a `document` (from a previous compilation) is optional, but
485/// enhances the autocompletions. Label completions, for instance, are
486/// only generated when the document is available.
487pub struct CompletionWorker<'a> {
488    /// The completions.
489    pub completions: Vec<LspCompletion>,
490    /// Whether the completion is incomplete.
491    pub incomplete: bool,
492
493    /// The analysis local context.
494    ctx: &'a mut LocalContext,
495    /// The compiled document.
496    document: Option<&'a TypstDocument>,
497    /// Whether the completion was explicitly requested.
498    explicit: bool,
499    /// The trigger character.
500    trigger_character: Option<char>,
501    /// The set of cast completions seen so far.
502    seen_casts: HashSet<u128>,
503    /// The set of type completions seen so far.
504    seen_types: HashSet<Ty>,
505    /// The set of field completions seen so far.
506    seen_fields: HashSet<Interned<str>>,
507}
508
509impl<'a> CompletionWorker<'a> {
510    /// Create a completion worker.
511    pub fn new(
512        ctx: &'a mut LocalContext,
513        document: Option<&'a TypstDocument>,
514        explicit: bool,
515        trigger_character: Option<char>,
516    ) -> Option<Self> {
517        Some(Self {
518            ctx,
519            document,
520            trigger_character,
521            explicit,
522            incomplete: true,
523            completions: vec![],
524            seen_casts: HashSet::new(),
525            seen_types: HashSet::new(),
526            seen_fields: HashSet::new(),
527        })
528    }
529
530    /// Gets the world.
531    pub fn world(&self) -> &LspWorld {
532        self.ctx.world()
533    }
534
535    fn seen_field(&mut self, field: Interned<str>) -> bool {
536        !self.seen_fields.insert(field)
537    }
538
539    /// Adds a prefix and suffix to all applications.
540    fn enrich(&mut self, prefix: &str, suffix: &str) {
541        for LspCompletion { text_edit, .. } in &mut self.completions {
542            let apply = match text_edit {
543                Some(text_edit) => text_edit.new_text_mut(),
544                _ => continue,
545            };
546
547            *apply = eco_format!("{prefix}{apply}{suffix}");
548        }
549    }
550
551    // if ctx.before.ends_with(':') {
552    //     ctx.enrich(" ", "");
553    // }
554
555    /// Starts the completion process.
556    pub(crate) fn work(&mut self, cursor: &mut Cursor) -> Option<()> {
557        // Skips if is the let binding item *directly*
558        if let Some(SyntaxClass::VarAccess(var)) = &cursor.syntax {
559            let node = var.node();
560            match node.parent_kind() {
561                // complete the init part of the let binding
562                Some(SyntaxKind::LetBinding) => {
563                    let parent = node.parent()?;
564                    let parent_init = parent.cast::<ast::LetBinding>()?.init()?;
565                    let parent_init = parent.find(parent_init.span())?;
566                    parent_init.find(node.span())?;
567                }
568                Some(SyntaxKind::Closure) => {
569                    let parent = node.parent()?;
570                    let parent_body = parent.cast::<ast::Closure>()?.body();
571                    let parent_body = parent.find(parent_body.span())?;
572                    parent_body.find(node.span())?;
573                }
574                _ => {}
575            }
576        }
577
578        // Skips if an error node starts with number (e.g. `1pt`)
579        if matches!(
580            cursor.syntax,
581            Some(SyntaxClass::Callee(..) | SyntaxClass::VarAccess(..) | SyntaxClass::Normal(..))
582        ) && cursor.leaf.diagnosis().errors
583        {
584            let mut chars = cursor.leaf.leaf_text().chars();
585            match chars.next() {
586                Some(ch) if ch.is_numeric() => return None,
587                Some('.') => {
588                    if matches!(chars.next(), Some(ch) if ch.is_numeric()) {
589                        return None;
590                    }
591                }
592                _ => {}
593            }
594        }
595
596        // Excludes it self from auto completion
597        // e.g. `#let x = (1.);`
598        let self_ty = cursor.leaf.cast::<ast::Expr>().and_then(|leaf| {
599            let v = self.ctx.mini_eval(leaf)?;
600            Some(Ty::Value(InsTy::new(v)))
601        });
602
603        if let Some(self_ty) = self_ty {
604            self.seen_types.insert(self_ty);
605        };
606
607        let mut pair = Pair {
608            worker: self,
609            cursor,
610        };
611        let _ = pair.complete_cursor();
612
613        // Filters
614        // todo: reference filter
615        if let Some(SelectedNode::Ident(from_ident)) = cursor.selected_node() {
616            let ident_prefix = cursor.text[from_ident.offset()..cursor.cursor].to_string();
617
618            self.completions.retain(|item| {
619                let mut prefix_matcher = item.label.chars();
620                'ident_matching: for ch in ident_prefix.chars() {
621                    for item in prefix_matcher.by_ref() {
622                        if item == ch {
623                            continue 'ident_matching;
624                        }
625                    }
626
627                    return false;
628                }
629
630                true
631            });
632        }
633
634        for item in &mut self.completions {
635            if let Some(text_edit) = &mut item.text_edit {
636                let new_text = text_edit.new_text_mut();
637                *new_text = to_lsp_snippet(new_text);
638            }
639        }
640
641        Some(())
642    }
643}
644
645struct CompletionPair<'a, 'b, 'c> {
646    worker: &'c mut CompletionWorker<'a>,
647    cursor: &'c mut Cursor<'b>,
648}
649
650type Pair<'a, 'b, 'c> = CompletionPair<'a, 'b, 'c>;
651
652impl CompletionPair<'_, '_, '_> {
653    /// Starts the completion on a cursor.
654    pub(crate) fn complete_cursor(&mut self) -> Option<()> {
655        use SurroundingSyntax::*;
656
657        // Special completions, we should remove them finally
658        if matches!(
659            self.cursor.leaf.kind(),
660            SyntaxKind::LineComment | SyntaxKind::BlockComment
661        ) {
662            return self.complete_comments().then_some(());
663        }
664
665        let surrounding_syntax = self.cursor.surrounding_syntax;
666        let mode = self.cursor.leaf_mode();
667
668        // Special completions 2, we should remove them finally
669        if matches!(surrounding_syntax, ImportList) {
670            return self.complete_imports().then_some(());
671        }
672
673        // Special completions 3, we should remove them finally
674        if matches!(surrounding_syntax, ParamList) {
675            return self.complete_params();
676        }
677
678        // Checks and completes `self.cursor.syntax_context`
679        match self.cursor.syntax_context.clone() {
680            Some(SyntaxContext::Element { container, .. }) => {
681                // The existing dictionary fields are not interesting
682                if let Some(container) = container.cast::<ast::Dict>() {
683                    for named in container.items() {
684                        if let ast::DictItem::Named(named) = named {
685                            self.worker.seen_field(named.name().into());
686                        }
687                    }
688                };
689            }
690            Some(SyntaxContext::Arg { args, .. }) => {
691                for arg in args.children() {
692                    let Some(ast::Arg::Named(named)) = arg.cast::<ast::Arg>() else {
693                        continue;
694                    };
695                    self.worker.seen_field(named.name().into());
696                }
697            }
698            // todo: complete field by types
699            Some(SyntaxContext::VarAccess(
700                var @ (VarClass::FieldAccess { .. } | VarClass::DotAccess { .. }),
701            )) => {
702                let target = var.accessed_node()?;
703                let field = var.accessing_field()?;
704
705                self.cursor.from = field.offset(&self.cursor.source)?;
706
707                self.doc_access_completions(&target);
708                return Some(());
709            }
710            Some(SyntaxContext::ImportPath(path) | SyntaxContext::IncludePath(path)) => {
711                let Some(ast::Expr::Str(str)) = path.cast() else {
712                    return None;
713                };
714                self.cursor.from = path.offset();
715                let value = str.get();
716                if value.starts_with('@') {
717                    let all_versions = value.contains(':');
718                    self.package_completions(all_versions);
719                    return Some(());
720                } else {
721                    let paths = self.complete_path(&crate::analysis::PathKind::Source {
722                        allow_package: true,
723                    });
724                    // todo: remove ctx.completions
725                    self.worker.completions.extend(paths.unwrap_or_default());
726                }
727
728                return Some(());
729            }
730            // todo: complete reference by type
731            Some(
732                SyntaxContext::Ref {
733                    node,
734                    suffix_colon: _,
735                }
736                | SyntaxContext::At { node },
737            ) => {
738                self.cursor.from = node.offset() + 1;
739                self.ref_completions();
740                return Some(());
741            }
742            Some(
743                SyntaxContext::VarAccess(VarClass::Ident { .. })
744                | SyntaxContext::Paren { .. }
745                | SyntaxContext::Label { .. }
746                | SyntaxContext::Normal(..),
747            )
748            | None => {}
749        }
750
751        let cursor_pos = bad_completion_cursor(
752            self.cursor.syntax.as_ref(),
753            self.cursor.syntax_context.as_ref(),
754            &self.cursor.leaf,
755        );
756
757        // Triggers a complete type checking.
758        let ty = self
759            .worker
760            .ctx
761            .post_type_of_node(self.cursor.leaf.clone())
762            .filter(|ty| !matches!(ty, Ty::Any))
763            // Forbids argument completion list if the cursor is in a bad position. This will
764            // prevent the completion list from showing up.
765            .filter(|_| !matches!(cursor_pos, Some(BadCompletionCursor::ArgListPos)));
766
767        crate::log_debug_ct!(
768            "complete_type: {:?} -> ({surrounding_syntax:?}, {ty:#?})",
769            self.cursor.leaf
770        );
771
772        // Adjusts the completion position
773        // todo: syntax class seems not being considering `is_ident_like`
774        // todo: merge ident_content_offset and label_content_offset
775        if is_ident_like(&self.cursor.leaf) {
776            self.cursor.from = self.cursor.leaf.offset();
777        } else if let Some(offset) = self
778            .cursor
779            .syntax
780            .as_ref()
781            .and_then(SyntaxClass::complete_offset)
782        {
783            self.cursor.from = offset;
784        }
785
786        // Completion by types.
787        if let Some(ty) = ty {
788            let filter = |ty: &Ty| match surrounding_syntax {
789                SurroundingSyntax::StringContent => match ty {
790                    Ty::Builtin(
791                        BuiltinTy::Path(..) | BuiltinTy::TextFont | BuiltinTy::TextFeature,
792                    ) => true,
793                    Ty::Value(val) => matches!(val.val, Value::Str(..)),
794                    _ => false,
795                },
796                _ => true,
797            };
798            let mut ctx = TypeCompletionWorker {
799                base: self,
800                filter: &filter,
801            };
802            ctx.type_completion(&ty, None);
803        }
804        let mut type_completions = std::mem::take(&mut self.worker.completions);
805
806        // Completion by [`crate::syntax::InterpretMode`].
807        match mode {
808            InterpretMode::Code => {
809                self.complete_code();
810            }
811            InterpretMode::Math => {
812                self.complete_math();
813            }
814            InterpretMode::Raw => {
815                self.complete_markup();
816            }
817            InterpretMode::Markup => match surrounding_syntax {
818                Regular => {
819                    self.complete_markup();
820                }
821                Selector | ShowTransform | SetRule => {
822                    self.complete_code();
823                }
824                StringContent | ImportList | ParamList => {}
825            },
826            InterpretMode::Comment | InterpretMode::String => {}
827        };
828
829        // Snippet completions associated by surrounding_syntax.
830        match surrounding_syntax {
831            Regular | StringContent | ImportList | ParamList | SetRule => {}
832            Selector => {
833                self.snippet_completion(
834                    "text selector",
835                    "\"${text}\"",
836                    "Replace occurrences of specific text.",
837                );
838
839                self.snippet_completion(
840                    "regex selector",
841                    "regex(\"${regex}\")",
842                    "Replace matches of a regular expression.",
843                );
844            }
845            ShowTransform => {
846                self.snippet_completion(
847                    "replacement",
848                    "[${content}]",
849                    "Replace the selected element with content.",
850                );
851
852                self.snippet_completion(
853                    "replacement (string)",
854                    "\"${text}\"",
855                    "Replace the selected element with a string of text.",
856                );
857
858                self.snippet_completion(
859                    "transformation",
860                    "element => [${content}]",
861                    "Transform the element with a function.",
862                );
863            }
864        }
865
866        // todo: filter completions by type
867        // ctx.strict_scope_completions(false, |value| value.ty() == *ty);
868        // let length_ty = Type::of::<Length>();
869        // ctx.strict_scope_completions(false, |value| value.ty() == length_ty);
870        // let color_ty = Type::of::<Color>();
871        // ctx.strict_scope_completions(false, |value| value.ty() == color_ty);
872        // let ty = Type::of::<Dir>();
873        // ctx.strict_scope_completions(false, |value| value.ty() == ty);
874
875        crate::log_debug_ct!(
876            "sort completions: {type_completions:#?} {:#?}",
877            self.worker.completions
878        );
879
880        // Sorts completions
881        type_completions.sort_by(|a, b| {
882            a.sort_text
883                .as_ref()
884                .cmp(&b.sort_text.as_ref())
885                .then_with(|| a.label.cmp(&b.label))
886        });
887        self.worker.completions.sort_by(|a, b| {
888            a.sort_text
889                .as_ref()
890                .cmp(&b.sort_text.as_ref())
891                .then_with(|| a.label.cmp(&b.label))
892        });
893
894        for (idx, compl) in type_completions
895            .iter_mut()
896            .chain(self.worker.completions.iter_mut())
897            .enumerate()
898        {
899            compl.sort_text = Some(eco_format!("{idx:03}"));
900        }
901
902        self.worker.completions.append(&mut type_completions);
903
904        crate::log_debug_ct!("sort completions after: {:#?}", self.worker.completions);
905
906        if let Some(node) = self.cursor.arg_cursor() {
907            crate::log_debug_ct!("content block compl: args {node:?}");
908            let is_unclosed = matches!(node.kind(), SyntaxKind::Args)
909                && node.children().fold(0i32, |acc, node| match node.kind() {
910                    SyntaxKind::LeftParen => acc + 1,
911                    SyntaxKind::RightParen => acc - 1,
912                    SyntaxKind::Error if node.leaf_text() == "(" => acc + 1,
913                    SyntaxKind::Error if node.leaf_text() == ")" => acc - 1,
914                    _ => acc,
915                }) > 0;
916            if is_unclosed {
917                self.worker.enrich("", ")");
918            }
919        }
920
921        if self.cursor.before.ends_with(',') || self.cursor.before.ends_with(':') {
922            self.worker.enrich(" ", "");
923        }
924        match surrounding_syntax {
925            Regular | ImportList | ParamList | ShowTransform | SetRule | StringContent => {}
926            Selector => {
927                self.worker.enrich("", ": ${}");
928            }
929        }
930
931        crate::log_debug_ct!("enrich completions: {:?}", self.worker.completions);
932
933        Some(())
934    }
935
936    /// Pushes a cursor-insensitive completion item.
937    fn push_completion(&mut self, completion: Completion) {
938        self.worker
939            .completions
940            .push(self.cursor.lsp_item_of(&completion));
941    }
942}
943
944/// If is printable, return the symbol itself.
945/// Otherwise, return the symbol's unicode detailed description.
946pub fn symbol_detail(s: &str) -> EcoString {
947    let ld = symbol_label_detail(s);
948    if ld.starts_with("\\u") {
949        return ld;
950    }
951
952    let mut chars = s.chars();
953    let unicode_repr = if let (Some(ch), None) = (chars.next(), chars.next()) {
954        format!("\\u{{{:04x}}}", ch as u32)
955    } else {
956        let codes: Vec<String> = s
957            .chars()
958            .map(|ch| format!("\\u{{{:04x}}}", ch as u32))
959            .collect();
960        codes.join(" + ")
961    };
962
963    format!("{ld}, unicode: `{unicode_repr}`").into()
964}
965
966/// If is printable, return the symbol itself.
967/// Otherwise, return the symbol's unicode description.
968pub fn symbol_label_detail(s: &str) -> EcoString {
969    let mut chars = s.chars();
970    if let (Some(ch), None) = (chars.next(), chars.next()) {
971        return symbol_label_detail_single_char(ch);
972    }
973
974    if s.chars().all(|ch| !ch.is_whitespace() && !ch.is_control()) {
975        return s.into();
976    }
977
978    let codes: Vec<String> = s
979        .chars()
980        .map(|ch| format!("\\u{{{:04x}}}", ch as u32))
981        .collect();
982    codes.join(" + ").into()
983}
984
985fn symbol_label_detail_single_char(ch: char) -> EcoString {
986    if !ch.is_whitespace() && !ch.is_control() {
987        return ch.into();
988    }
989    match ch {
990        ' ' => "space".into(),
991        '\t' => "tab".into(),
992        '\n' => "newline".into(),
993        '\r' => "carriage return".into(),
994        // replacer
995        '\u{200D}' => "zero width joiner".into(),
996        '\u{200C}' => "zero width non-joiner".into(),
997        '\u{200B}' => "zero width space".into(),
998        '\u{2060}' => "word joiner".into(),
999        // spaces
1000        '\u{00A0}' => "non-breaking space".into(),
1001        '\u{202F}' => "narrow no-break space".into(),
1002        '\u{2002}' => "en space".into(),
1003        '\u{2003}' => "em space".into(),
1004        '\u{2004}' => "three-per-em space".into(),
1005        '\u{2005}' => "four-per-em space".into(),
1006        '\u{2006}' => "six-per-em space".into(),
1007        '\u{2007}' => "figure space".into(),
1008        '\u{205f}' => "medium mathematical space".into(),
1009        '\u{2008}' => "punctuation space".into(),
1010        '\u{2009}' => "thin space".into(),
1011        '\u{200A}' => "hair space".into(),
1012        _ => format!("\\u{{{:04x}}}", ch as u32).into(),
1013    }
1014}
1015
1016/// Slices a smaller string at character boundaries safely.
1017fn slice_at(s: &str, mut rng: Range<usize>) -> &str {
1018    while !rng.is_empty() && !s.is_char_boundary(rng.start) {
1019        rng.start += 1;
1020    }
1021    while !rng.is_empty() && !s.is_char_boundary(rng.end) {
1022        rng.end -= 1;
1023    }
1024
1025    if rng.is_empty() {
1026        return "";
1027    }
1028
1029    &s[rng]
1030}
1031
1032fn fill_first_empty_placeholder(snippet: &mut EcoString, text: &str) -> bool {
1033    let Some(pos) = snippet.find("${}") else {
1034        return false;
1035    };
1036
1037    let escaped = text
1038        .replace('\\', "\\\\")
1039        .replace('$', "\\$")
1040        .replace('}', "\\}");
1041    let mut filled = EcoString::new();
1042    filled.push_str(&snippet[..pos]);
1043    filled.push_str("${");
1044    filled.push_str(&escaped);
1045    filled.push('}');
1046    filled.push_str(&snippet[pos + "${}".len()..]);
1047    *snippet = filled;
1048    true
1049}
1050
1051static TYPST_SNIPPET_PLACEHOLDER_RE: LazyLock<Regex> =
1052    LazyLock::new(|| Regex::new(r"\$\{(.*?)\}").unwrap());
1053
1054/// Adds numbering to placeholders in snippets
1055fn to_lsp_snippet(typst_snippet: &str) -> EcoString {
1056    let mut counter = 1;
1057    let result = TYPST_SNIPPET_PLACEHOLDER_RE.replace_all(typst_snippet, |cap: &Captures| {
1058        let substitution = format!("${{{}:{}}}", counter, &cap[1]);
1059        counter += 1;
1060        substitution
1061    });
1062
1063    result.into()
1064}
1065
1066fn is_hash_expr(leaf: &LinkedNode<'_>) -> bool {
1067    is_hash_expr_(leaf).is_some()
1068}
1069
1070fn is_hash_expr_(leaf: &LinkedNode<'_>) -> Option<()> {
1071    match leaf.kind() {
1072        SyntaxKind::Hash => Some(()),
1073        SyntaxKind::Ident => {
1074            let prev_leaf = leaf.prev_leaf()?;
1075            if prev_leaf.kind() == SyntaxKind::Hash {
1076                Some(())
1077            } else {
1078                None
1079            }
1080        }
1081        _ => None,
1082    }
1083}
1084
1085fn is_triggered_by_punc(trigger_character: Option<char>) -> bool {
1086    trigger_character.is_some_and(|ch| ch.is_ascii_punctuation())
1087}
1088
1089fn is_arg_like_context(mut matching: &LinkedNode) -> bool {
1090    while let Some(parent) = matching.parent() {
1091        use SyntaxKind::*;
1092
1093        // todo: contextual
1094        match parent.kind() {
1095            ContentBlock | Equation | CodeBlock | Markup | Math | Code => return false,
1096            Args | Params | Destructuring | Array | Dict => return true,
1097            _ => {}
1098        }
1099
1100        matching = parent;
1101    }
1102    false
1103}
1104
1105// if param.attrs.named {
1106//     match param.ty {
1107//         Ty::Builtin(BuiltinTy::TextSize) => {
1108//             for size_template in &[
1109//                 "10.5pt", "12pt", "9pt", "14pt", "8pt", "16pt", "18pt",
1110// "20pt", "22pt",                 "24pt", "28pt",
1111//             ] {
1112//                 let compl = compl.clone();
1113//                 ctx.completions.push(Completion {
1114//                     label: eco_format!("{}: {}", param.name, size_template),
1115//                     apply: None,
1116//                     ..compl
1117//                 });
1118//             }
1119//         }
1120//         Ty::Builtin(BuiltinTy::Dir) => {
1121//             for dir_template in &["ltr", "rtl", "ttb", "btt"] {
1122//                 let compl = compl.clone();
1123//                 ctx.completions.push(Completion {
1124//                     label: eco_format!("{}: {}", param.name, dir_template),
1125//                     apply: None,
1126//                     ..compl
1127//                 });
1128//             }
1129//         }
1130//         _ => {}
1131//     }
1132//     ctx.completions.push(compl);
1133// }
1134
1135fn deserialize_null_default<'de, D, T>(deserializer: D) -> Result<T, D::Error>
1136where
1137    T: Default + Deserialize<'de>,
1138    D: serde::Deserializer<'de>,
1139{
1140    let opt = Option::deserialize(deserializer)?;
1141    Ok(opt.unwrap_or_default())
1142}
1143
1144// todo: doesn't complete parameter now, which is not good.
1145
1146#[cfg(test)]
1147mod tests {
1148    use super::slice_at;
1149
1150    #[test]
1151    fn test_before() {
1152        const TEST_UTF8_STR: &str = "我们";
1153        for i in 0..=TEST_UTF8_STR.len() {
1154            for j in 0..=TEST_UTF8_STR.len() {
1155                let _s = std::hint::black_box(slice_at(TEST_UTF8_STR, i..j));
1156            }
1157        }
1158    }
1159}