tinymist_analysis/syntax/
matcher.rs

1//! Convenient utilities to match syntax structures of code.
2//! - Iterators/Finders to traverse nodes.
3//! - Predicates to check nodes' properties.
4//! - Classifiers to check nodes' syntax.
5//!
6//! ## Classifiers of syntax structures
7//!
8//! A node can have a quadruple to describe its syntax:
9//!
10//! ```text
11//! (InterpretMode, SurroundingSyntax/SyntaxContext, DefClass/SyntaxClass, SyntaxNode)
12//! ```
13//!
14//! Among them, [`InterpretMode`], [`SurroundingSyntax`], and [`SyntaxContext`]
15//! describes outer syntax. [`DefClass`], [`SyntaxClass`] and
16//! [`typst::syntax::SyntaxNode`] describes inner syntax.
17//!
18//! - [`typst::syntax::SyntaxNode`]: Its contextual version is
19//!   [`typst::syntax::LinkedNode`], containing AST information, like inner text
20//!   and [`SyntaxKind`], on the position.
21//! - [`SyntaxClass`]: Provided by [`classify_syntax`], it describes the
22//!   context-free syntax of the node that are more suitable for IDE operations.
23//!   For example, it identifies users' half-typed syntax like half-completed
24//!   labels and dot accesses.
25//! - [`DefClass`]: Provided by [`classify_def`], it describes the definition
26//!   class of the node at the position. The difference between `SyntaxClass`
27//!   and `DefClass` is that the latter matcher will skip the nodes that do not
28//!   define a definition.
29//! - [`SyntaxContext`]: Provided by [`classify_context`], it describes the
30//!   outer syntax of the node that are more suitable for IDE operations. For
31//!   example, it identifies the context of a cursor on the comma in a function
32//!   call.
33//! - [`SurroundingSyntax`]: Provided by [`surrounding_syntax`], it describes
34//!   the surrounding syntax of the node that are more suitable for IDE
35//!   operations. The difference between `SyntaxContext` and `SurroundingSyntax`
36//!   is that the former is more specific and the latter is more general can be
37//!   used for filtering customized snippets.
38//! - [`InterpretMode`]: Provided by [`interpret_mode_at`], it describes the how
39//!   an interpreter should interpret the code at the position.
40//!
41//! Some examples of the quadruple (the cursor is marked by `|`):
42//!
43//! ```text
44//! #(x|);
45//!    ^ SyntaxContext::Paren, SyntaxClass::Normal(SyntaxKind::Ident)
46//! #(x,|);
47//!     ^ SyntaxContext::Element, SyntaxClass::Normal(SyntaxKind::Array)
48//! #f(x,|);
49//!      ^ SyntaxContext::Arg, SyntaxClass::Normal(SyntaxKind::FuncCall)
50//! ```
51//!
52//! ```text
53//! #show raw|: |it => it|
54//!          ^ SurroundingSyntax::Selector
55//!             ^ SurroundingSyntax::ShowTransform
56//!                      ^ SurroundingSyntax::Regular
57//! ```
58
59use crate::debug_loc::SourceSpanOffset;
60use serde::{Deserialize, Serialize};
61use typst::syntax::Span;
62
63use crate::prelude::*;
64
65/// Returns the ancestor iterator of the given node.
66pub fn node_ancestors<'a, 'b>(
67    node: &'b LinkedNode<'a>,
68) -> impl Iterator<Item = &'b LinkedNode<'a>> {
69    std::iter::successors(Some(node), |node| node.parent())
70}
71
72/// Finds the first ancestor node that is an expression.
73pub fn first_ancestor_expr(node: LinkedNode) -> Option<LinkedNode> {
74    node_ancestors(&node).find(|n| n.is::<ast::Expr>()).cloned()
75}
76
77/// A node that is an ancestor of the given node or the previous sibling
78/// of some ancestor.
79pub enum PreviousItem<'a> {
80    /// When the iterator is crossing an ancesstor node.
81    Parent(&'a LinkedNode<'a>, &'a LinkedNode<'a>),
82    /// When the iterator is on a sibling node of some ancestor.
83    Sibling(&'a LinkedNode<'a>),
84}
85
86impl<'a> PreviousItem<'a> {
87    /// Gets the underlying [`LinkedNode`] of the item.
88    pub fn node(&self) -> &'a LinkedNode<'a> {
89        match self {
90            PreviousItem::Sibling(node) => node,
91            PreviousItem::Parent(node, _) => node,
92        }
93    }
94}
95
96/// Finds the previous items (in the scope) starting from the given position
97/// inclusively. See [`PreviousItem`] for the possible items.
98pub fn previous_items<T>(
99    node: LinkedNode,
100    mut recv: impl FnMut(PreviousItem) -> Option<T>,
101) -> Option<T> {
102    let mut ancestor = Some(node);
103    while let Some(node) = &ancestor {
104        let mut sibling = Some(node.clone());
105        while let Some(node) = &sibling {
106            if let Some(v) = recv(PreviousItem::Sibling(node)) {
107                return Some(v);
108            }
109
110            sibling = node.prev_sibling();
111        }
112
113        if let Some(parent) = node.parent() {
114            if let Some(v) = recv(PreviousItem::Parent(parent, node)) {
115                return Some(v);
116            }
117
118            ancestor = Some(parent.clone());
119            continue;
120        }
121
122        break;
123    }
124
125    None
126}
127
128/// A declaration that is an ancestor of the given node or the previous sibling
129/// of some ancestor.
130pub enum PreviousDecl<'a> {
131    /// An declaration having an identifier.
132    ///
133    /// ## Example
134    ///
135    /// The `x` in the following code:
136    ///
137    /// ```typst
138    /// #let x = 1;
139    /// ```
140    Ident(ast::Ident<'a>),
141    /// An declaration yielding from an import source.
142    ///
143    /// ## Example
144    ///
145    /// The `x` in the following code:
146    ///
147    /// ```typst
148    /// #import "path.typ": x;
149    /// ```
150    ImportSource(ast::Expr<'a>),
151    /// A wildcard import that possibly containing visible declarations.
152    ///
153    /// ## Example
154    ///
155    /// The following import is matched:
156    ///
157    /// ```typst
158    /// #import "path.typ": *;
159    /// ```
160    ImportAll(ast::ModuleImport<'a>),
161}
162
163/// Finds the previous declarations starting from the given position. It checks
164/// [`PreviousItem`] and returns the found declarations.
165pub fn previous_decls<T>(
166    node: LinkedNode,
167    mut recv: impl FnMut(PreviousDecl) -> Option<T>,
168) -> Option<T> {
169    previous_items(node, |item| {
170        match (&item, item.node().cast::<ast::Expr>()?) {
171            (PreviousItem::Sibling(..), ast::Expr::Let(lb)) => {
172                for ident in lb.kind().bindings() {
173                    if let Some(t) = recv(PreviousDecl::Ident(ident)) {
174                        return Some(t);
175                    }
176                }
177            }
178            (PreviousItem::Sibling(..), ast::Expr::Import(import)) => {
179                // import items
180                match import.imports() {
181                    Some(ast::Imports::Wildcard) => {
182                        if let Some(t) = recv(PreviousDecl::ImportAll(import)) {
183                            return Some(t);
184                        }
185                    }
186                    Some(ast::Imports::Items(items)) => {
187                        for item in items.iter() {
188                            if let Some(t) = recv(PreviousDecl::Ident(item.bound_name())) {
189                                return Some(t);
190                            }
191                        }
192                    }
193                    _ => {}
194                }
195
196                // import it self
197                if let Some(new_name) = import.new_name() {
198                    if let Some(t) = recv(PreviousDecl::Ident(new_name)) {
199                        return Some(t);
200                    }
201                } else if import.imports().is_none() {
202                    if let Some(t) = recv(PreviousDecl::ImportSource(import.source())) {
203                        return Some(t);
204                    }
205                }
206            }
207            (PreviousItem::Parent(parent, child), ast::Expr::For(for_expr)) => {
208                let body = parent.find(for_expr.body().span());
209                let in_body = body.is_some_and(|n| n.find(child.span()).is_some());
210                if !in_body {
211                    return None;
212                }
213
214                for ident in for_expr.pattern().bindings() {
215                    if let Some(t) = recv(PreviousDecl::Ident(ident)) {
216                        return Some(t);
217                    }
218                }
219            }
220            (PreviousItem::Parent(parent, child), ast::Expr::Closure(closure)) => {
221                let body = parent.find(closure.body().span());
222                let in_body = body.is_some_and(|n| n.find(child.span()).is_some());
223                if !in_body {
224                    return None;
225                }
226
227                for param in closure.params().children() {
228                    match param {
229                        ast::Param::Pos(pos) => {
230                            for ident in pos.bindings() {
231                                if let Some(t) = recv(PreviousDecl::Ident(ident)) {
232                                    return Some(t);
233                                }
234                            }
235                        }
236                        ast::Param::Named(named) => {
237                            if let Some(t) = recv(PreviousDecl::Ident(named.name())) {
238                                return Some(t);
239                            }
240                        }
241                        ast::Param::Spread(spread) => {
242                            if let Some(sink_ident) = spread.sink_ident() {
243                                if let Some(t) = recv(PreviousDecl::Ident(sink_ident)) {
244                                    return Some(t);
245                                }
246                            }
247                        }
248                    }
249                }
250            }
251            _ => {}
252        };
253        None
254    })
255}
256
257/// Whether the node can be recognized as a mark.
258pub fn is_mark(sk: SyntaxKind) -> bool {
259    use SyntaxKind::*;
260    #[allow(clippy::match_like_matches_macro)]
261    match sk {
262        MathAlignPoint | Plus | Minus | Dot | Dots | Arrow | Not | And | Or => true,
263        Eq | EqEq | ExclEq | Lt | LtEq | Gt | GtEq | PlusEq | HyphEq | StarEq | SlashEq => true,
264        LeftBrace | RightBrace | LeftBracket | RightBracket | LeftParen | RightParen => true,
265        Slash | Hat | Comma | Semicolon | Colon | Hash => true,
266        _ => false,
267    }
268}
269
270/// Whether the node can be recognized as an identifier.
271pub fn is_ident_like(node: &SyntaxNode) -> bool {
272    fn can_be_ident(node: &SyntaxNode) -> bool {
273        typst::syntax::is_ident(node.text())
274    }
275
276    use SyntaxKind::*;
277    let kind = node.kind();
278    matches!(kind, Ident | MathIdent | Underscore)
279        || (matches!(kind, Error) && can_be_ident(node))
280        || kind.is_keyword()
281}
282
283/// A mode in which a text document is interpreted.
284#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash, strum::EnumIter)]
285#[serde(rename_all = "camelCase")]
286pub enum InterpretMode {
287    /// The position is in a comment.
288    Comment,
289    /// The position is in a string.
290    String,
291    /// The position is in a raw.
292    Raw,
293    /// The position is in a markup block.
294    Markup,
295    /// The position is in a code block.
296    Code,
297    /// The position is in a math equation.
298    Math,
299}
300
301/// Determine the interpretation mode at the given position (context-sensitive).
302pub fn interpret_mode_at(mut leaf: Option<&LinkedNode>) -> InterpretMode {
303    loop {
304        crate::log_debug_ct!("leaf for mode: {leaf:?}");
305        if let Some(t) = leaf {
306            if let Some(mode) = interpret_mode_at_kind(t.kind()) {
307                break mode;
308            }
309
310            if !t.kind().is_trivia() && {
311                // Previous leaf is hash
312                t.prev_leaf().is_some_and(|n| n.kind() == SyntaxKind::Hash)
313            } {
314                return InterpretMode::Code;
315            }
316
317            leaf = t.parent();
318        } else {
319            break InterpretMode::Markup;
320        }
321    }
322}
323
324/// Determine the interpretation mode at the given kind (context-free).
325pub(crate) fn interpret_mode_at_kind(kind: SyntaxKind) -> Option<InterpretMode> {
326    use SyntaxKind::*;
327    Some(match kind {
328        LineComment | BlockComment | Shebang => InterpretMode::Comment,
329        Raw => InterpretMode::Raw,
330        Str => InterpretMode::String,
331        CodeBlock | Code => InterpretMode::Code,
332        ContentBlock | Markup => InterpretMode::Markup,
333        Equation | Math => InterpretMode::Math,
334        Hash => InterpretMode::Code,
335        Label | Text | Ident | Args | FuncCall | FieldAccess | Bool | Int | Float | Numeric
336        | Space | Linebreak | Parbreak | Escape | Shorthand | SmartQuote | RawLang | RawDelim
337        | RawTrimmed | LeftBrace | RightBrace | LeftBracket | RightBracket | LeftParen
338        | RightParen | Comma | Semicolon | Colon | Star | Underscore | Dollar | Plus | Minus
339        | Slash | Hat | Prime | Dot | Eq | EqEq | ExclEq | Lt | LtEq | Gt | GtEq | PlusEq
340        | HyphEq | StarEq | SlashEq | Dots | Arrow | Root | Not | And | Or | None | Auto | As
341        | Named | Keyed | Spread | Error | End => return Option::None,
342        Strong | Emph | Link | Ref | RefMarker | Heading | HeadingMarker | ListItem
343        | ListMarker | EnumItem | EnumMarker | TermItem | TermMarker => InterpretMode::Markup,
344        MathIdent | MathAlignPoint | MathDelimited | MathAttach | MathPrimes | MathFrac
345        | MathRoot | MathShorthand | MathText => InterpretMode::Math,
346        Let | Set | Show | Context | If | Else | For | In | While | Break | Continue | Return
347        | Import | Include | Closure | Params | LetBinding | SetRule | ShowRule | Contextual
348        | Conditional | WhileLoop | ForLoop | LoopBreak | ModuleImport | ImportItems
349        | ImportItemPath | RenamedImportItem | ModuleInclude | LoopContinue | FuncReturn
350        | Unary | Binary | Parenthesized | Dict | Array | Destructuring | DestructAssignment => {
351            InterpretMode::Code
352        }
353    })
354}
355
356/// Classes of def items that can be operated on by IDE functionality.
357#[derive(Debug, Clone)]
358pub enum DefClass<'a> {
359    /// A let binding item.
360    Let(LinkedNode<'a>),
361    /// A module import item.
362    Import(LinkedNode<'a>),
363}
364
365impl DefClass<'_> {
366    /// Gets the node of the def class.
367    pub fn node(&self) -> &LinkedNode {
368        match self {
369            DefClass::Let(node) => node,
370            DefClass::Import(node) => node,
371        }
372    }
373
374    /// Gets the name node of the def class.
375    pub fn name(&self) -> Option<LinkedNode> {
376        match self {
377            DefClass::Let(node) => {
378                let lb: ast::LetBinding<'_> = node.cast()?;
379                let names = match lb.kind() {
380                    ast::LetBindingKind::Closure(name) => node.find(name.span())?,
381                    ast::LetBindingKind::Normal(ast::Pattern::Normal(name)) => {
382                        node.find(name.span())?
383                    }
384                    _ => return None,
385                };
386
387                Some(names)
388            }
389            DefClass::Import(_node) => {
390                // let ident = node.cast::<ast::ImportItem>()?;
391                // Some(ident.span().into())
392                // todo: implement this
393                None
394            }
395        }
396    }
397
398    /// Gets the name's range in code of the def class.
399    pub fn name_range(&self) -> Option<Range<usize>> {
400        self.name().map(|node| node.range())
401    }
402}
403
404// todo: whether we should distinguish between strict and loose def classes
405/// Classifies a definition loosely.
406pub fn classify_def_loosely(node: LinkedNode) -> Option<DefClass<'_>> {
407    classify_def_(node, false)
408}
409
410/// Classifies a definition strictly.
411pub fn classify_def(node: LinkedNode) -> Option<DefClass<'_>> {
412    classify_def_(node, true)
413}
414
415/// The internal implementation of classifying a definition.
416fn classify_def_(node: LinkedNode, strict: bool) -> Option<DefClass<'_>> {
417    let mut ancestor = node;
418    if ancestor.kind().is_trivia() || is_mark(ancestor.kind()) {
419        ancestor = ancestor.prev_sibling()?;
420    }
421
422    while !ancestor.is::<ast::Expr>() {
423        ancestor = ancestor.parent()?.clone();
424    }
425    crate::log_debug_ct!("ancestor: {ancestor:?}");
426    let adjusted = adjust_expr(ancestor)?;
427    crate::log_debug_ct!("adjust_expr: {adjusted:?}");
428
429    let may_ident = adjusted.cast::<ast::Expr>()?;
430    if strict && !may_ident.hash() && !matches!(may_ident, ast::Expr::MathIdent(_)) {
431        return None;
432    }
433
434    let expr = may_ident;
435    Some(match expr {
436        // todo: label, reference
437        // todo: include
438        ast::Expr::FuncCall(..) => return None,
439        ast::Expr::Set(..) => return None,
440        ast::Expr::Let(..) => DefClass::Let(adjusted),
441        ast::Expr::Import(..) => DefClass::Import(adjusted),
442        // todo: parameter
443        ast::Expr::Ident(..)
444        | ast::Expr::MathIdent(..)
445        | ast::Expr::FieldAccess(..)
446        | ast::Expr::Closure(..) => {
447            let mut ancestor = adjusted;
448            while !ancestor.is::<ast::LetBinding>() {
449                ancestor = ancestor.parent()?.clone();
450            }
451
452            DefClass::Let(ancestor)
453        }
454        ast::Expr::Str(..) => {
455            let parent = adjusted.parent()?;
456            if parent.kind() != SyntaxKind::ModuleImport {
457                return None;
458            }
459
460            DefClass::Import(parent.clone())
461        }
462        _ if expr.hash() => return None,
463        _ => {
464            crate::log_debug_ct!("unsupported kind {:?}", adjusted.kind());
465            return None;
466        }
467    })
468}
469
470/// Adjusts an expression node to a more suitable one for classification.
471/// It is not formal, but the following cases are forbidden:
472/// - Parenthesized expression.
473/// - Identifier on the right side of a dot operator (field access).
474fn adjust_expr(mut node: LinkedNode) -> Option<LinkedNode> {
475    while let Some(paren_expr) = node.cast::<ast::Parenthesized>() {
476        node = node.find(paren_expr.expr().span())?;
477    }
478    if let Some(parent) = node.parent() {
479        if let Some(field_access) = parent.cast::<ast::FieldAccess>() {
480            if node.span() == field_access.field().span() {
481                return Some(parent.clone());
482            }
483        }
484    }
485    Some(node)
486}
487
488/// Classes of field syntax that can be operated on by IDE functionality.
489#[derive(Debug, Clone)]
490pub enum FieldClass<'a> {
491    /// A field node.
492    ///
493    /// ## Example
494    ///
495    /// The `x` in the following code:
496    ///
497    /// ```typst
498    /// #a.x
499    /// ```
500    Field(LinkedNode<'a>),
501
502    /// A dot suffix missing a field.
503    ///
504    /// ## Example
505    ///
506    /// The `.` in the following code:
507    ///
508    /// ```typst
509    /// #a.
510    /// ```
511    DotSuffix(SourceSpanOffset),
512}
513
514impl FieldClass<'_> {
515    /// Gets the node of the field class.
516    pub fn offset(&self, source: &Source) -> Option<usize> {
517        Some(match self {
518            Self::Field(node) => node.offset(),
519            Self::DotSuffix(span_offset) => {
520                source.find(span_offset.span)?.offset() + span_offset.offset
521            }
522        })
523    }
524}
525
526/// Classes of variable (access) syntax that can be operated on by IDE
527/// functionality.
528#[derive(Debug, Clone)]
529pub enum VarClass<'a> {
530    /// An identifier expression.
531    Ident(LinkedNode<'a>),
532    /// A field access expression.
533    FieldAccess(LinkedNode<'a>),
534    /// A dot access expression, for example, `#a.|`, `$a.|$`, or `x.|.y`.
535    /// Note the cursor of the last example is on the middle of the spread
536    /// operator.
537    DotAccess(LinkedNode<'a>),
538}
539
540impl<'a> VarClass<'a> {
541    /// Gets the node of the var (access) class.
542    pub fn node(&self) -> &LinkedNode<'a> {
543        match self {
544            Self::Ident(node) | Self::FieldAccess(node) | Self::DotAccess(node) => node,
545        }
546    }
547
548    /// Gets the accessed node of the var (access) class.
549    pub fn accessed_node(&self) -> Option<LinkedNode<'a>> {
550        Some(match self {
551            Self::Ident(node) => node.clone(),
552            Self::FieldAccess(node) => {
553                let field_access = node.cast::<ast::FieldAccess>()?;
554                node.find(field_access.target().span())?
555            }
556            Self::DotAccess(node) => node.clone(),
557        })
558    }
559
560    /// Gets the accessing field of the var (access) class.
561    pub fn accessing_field(&self) -> Option<FieldClass<'a>> {
562        match self {
563            Self::FieldAccess(node) => {
564                let dot = node
565                    .children()
566                    .find(|n| matches!(n.kind(), SyntaxKind::Dot))?;
567                let mut iter_after_dot =
568                    node.children().skip_while(|n| n.kind() != SyntaxKind::Dot);
569                let ident = iter_after_dot.find(|n| {
570                    matches!(
571                        n.kind(),
572                        SyntaxKind::Ident | SyntaxKind::MathIdent | SyntaxKind::Error
573                    )
574                });
575
576                let ident_case = ident.map(|ident| {
577                    if ident.text().is_empty() {
578                        FieldClass::DotSuffix(SourceSpanOffset {
579                            span: ident.span(),
580                            offset: 0,
581                        })
582                    } else {
583                        FieldClass::Field(ident)
584                    }
585                });
586
587                ident_case.or_else(|| {
588                    Some(FieldClass::DotSuffix(SourceSpanOffset {
589                        span: dot.span(),
590                        offset: 1,
591                    }))
592                })
593            }
594            Self::DotAccess(node) => Some(FieldClass::DotSuffix(SourceSpanOffset {
595                span: node.span(),
596                offset: node.range().len() + 1,
597            })),
598            Self::Ident(_) => None,
599        }
600    }
601}
602
603/// Classes of syntax that can be operated on by IDE functionality.
604#[derive(Debug, Clone)]
605pub enum SyntaxClass<'a> {
606    /// A variable access expression.
607    ///
608    /// It can be either an identifier or a field access.
609    VarAccess(VarClass<'a>),
610    /// A (content) label expression.
611    Label {
612        /// The node of the label.
613        node: LinkedNode<'a>,
614        /// Whether the label is converted from an error node.
615        is_error: bool,
616    },
617    /// A (content) reference expression.
618    Ref(LinkedNode<'a>),
619    /// A callee expression.
620    Callee(LinkedNode<'a>),
621    /// An import path expression.
622    ImportPath(LinkedNode<'a>),
623    /// An include path expression.
624    IncludePath(LinkedNode<'a>),
625    /// Rest kind of **expressions**.
626    Normal(SyntaxKind, LinkedNode<'a>),
627}
628
629impl<'a> SyntaxClass<'a> {
630    /// Creates a label syntax class.
631    pub fn label(node: LinkedNode<'a>) -> Self {
632        Self::Label {
633            node,
634            is_error: false,
635        }
636    }
637
638    /// Creates an error label syntax class.
639    pub fn error_as_label(node: LinkedNode<'a>) -> Self {
640        Self::Label {
641            node,
642            is_error: true,
643        }
644    }
645
646    /// Gets the node of the syntax class.
647    pub fn node(&self) -> &LinkedNode<'a> {
648        match self {
649            SyntaxClass::VarAccess(cls) => cls.node(),
650            SyntaxClass::Label { node, .. }
651            | SyntaxClass::Ref(node)
652            | SyntaxClass::Callee(node)
653            | SyntaxClass::ImportPath(node)
654            | SyntaxClass::IncludePath(node)
655            | SyntaxClass::Normal(_, node) => node,
656        }
657    }
658
659    /// Gets the content offset at which the completion should be triggered.
660    pub fn complete_offset(&self) -> Option<usize> {
661        match self {
662            // `<label`
663            //   ^ node.offset() + 1
664            SyntaxClass::Label { node, .. } => Some(node.offset() + 1),
665            _ => None,
666        }
667    }
668}
669
670/// Classifies node's syntax (inner syntax) that can be operated on by IDE
671/// functionality.
672pub fn classify_syntax(node: LinkedNode, cursor: usize) -> Option<SyntaxClass<'_>> {
673    if matches!(node.kind(), SyntaxKind::Error) && node.text().starts_with('<') {
674        return Some(SyntaxClass::error_as_label(node));
675    }
676
677    /// Skips trivia nodes that are on the same line as the cursor.
678    fn can_skip_trivia(node: &LinkedNode, cursor: usize) -> bool {
679        // A non-trivia node is our target so we stop at it.
680        if !node.kind().is_trivia() || !node.parent_kind().is_some_and(possible_in_code_trivia) {
681            return false;
682        }
683
684        // Gets the trivia text before the cursor.
685        let previous_text = node.text().as_bytes();
686        let previous_text = if node.range().contains(&cursor) {
687            &previous_text[..cursor - node.offset()]
688        } else {
689            previous_text
690        };
691
692        // The deref target should be on the same line as the cursor.
693        // Assuming the underlying text is utf-8 encoded, we can check for newlines by
694        // looking for b'\n'.
695        // todo: if we are in markup mode, we should check if we are at start of node
696        !previous_text.contains(&b'\n')
697    }
698
699    // Moves to the first non-trivia node before the cursor.
700    let mut node = node;
701    if can_skip_trivia(&node, cursor) {
702        node = node.prev_sibling()?;
703    }
704
705    /// Matches complete or incomplete dot accesses in code, math, and markup
706    /// mode.
707    ///
708    /// When in markup mode, the dot access is valid if the dot is after a hash
709    /// expression.
710    fn classify_dot_access<'a>(node: &LinkedNode<'a>) -> Option<SyntaxClass<'a>> {
711        let dot_target = node.prev_leaf().and_then(first_ancestor_expr)?;
712        let mode = interpret_mode_at(Some(node));
713
714        if matches!(mode, InterpretMode::Math | InterpretMode::Code) || {
715            matches!(mode, InterpretMode::Markup)
716                && matches!(
717                    dot_target.prev_leaf().as_deref().map(SyntaxNode::kind),
718                    Some(SyntaxKind::Hash)
719                )
720        } {
721            return Some(SyntaxClass::VarAccess(VarClass::DotAccess(dot_target)));
722        }
723
724        None
725    }
726
727    if node.offset() + 1 == cursor && {
728        // Check if the cursor is exactly after single dot.
729        matches!(node.kind(), SyntaxKind::Dot)
730            || (matches!(
731                node.kind(),
732                SyntaxKind::Text | SyntaxKind::MathText | SyntaxKind::Error
733            ) && node.text().starts_with("."))
734    } {
735        if let Some(dot_access) = classify_dot_access(&node) {
736            return Some(dot_access);
737        }
738    }
739
740    if node.offset() + 1 == cursor
741        && matches!(node.kind(), SyntaxKind::Dots)
742        && matches!(node.parent_kind(), Some(SyntaxKind::Spread))
743    {
744        if let Some(dot_access) = classify_dot_access(&node) {
745            return Some(dot_access);
746        }
747    }
748
749    // todo: check if we can remove Text here
750    if matches!(node.kind(), SyntaxKind::Text | SyntaxKind::MathText) {
751        let mode = interpret_mode_at(Some(&node));
752        if matches!(mode, InterpretMode::Math) && is_ident_like(&node) {
753            return Some(SyntaxClass::VarAccess(VarClass::Ident(node)));
754        }
755    }
756
757    // Move to the first ancestor that is an expression.
758    let ancestor = first_ancestor_expr(node)?;
759    crate::log_debug_ct!("first_ancestor_expr: {ancestor:?}");
760
761    // Unwrap all parentheses to get the actual expression.
762    let adjusted = adjust_expr(ancestor)?;
763    crate::log_debug_ct!("adjust_expr: {adjusted:?}");
764
765    // Identify convenient expression kinds.
766    let expr = adjusted.cast::<ast::Expr>()?;
767    Some(match expr {
768        ast::Expr::Label(..) => SyntaxClass::label(adjusted),
769        ast::Expr::Ref(..) => SyntaxClass::Ref(adjusted),
770        ast::Expr::FuncCall(call) => SyntaxClass::Callee(adjusted.find(call.callee().span())?),
771        ast::Expr::Set(set) => SyntaxClass::Callee(adjusted.find(set.target().span())?),
772        ast::Expr::Ident(..) | ast::Expr::MathIdent(..) => {
773            SyntaxClass::VarAccess(VarClass::Ident(adjusted))
774        }
775        ast::Expr::FieldAccess(..) => SyntaxClass::VarAccess(VarClass::FieldAccess(adjusted)),
776        ast::Expr::Str(..) => {
777            let parent = adjusted.parent()?;
778            if parent.kind() == SyntaxKind::ModuleImport {
779                SyntaxClass::ImportPath(adjusted)
780            } else if parent.kind() == SyntaxKind::ModuleInclude {
781                SyntaxClass::IncludePath(adjusted)
782            } else {
783                SyntaxClass::Normal(adjusted.kind(), adjusted)
784            }
785        }
786        _ if expr.hash()
787            || matches!(adjusted.kind(), SyntaxKind::MathIdent | SyntaxKind::Error) =>
788        {
789            SyntaxClass::Normal(adjusted.kind(), adjusted)
790        }
791        _ => return None,
792    })
793}
794
795/// Whether the node might be in code trivia. This is a bit internal so please
796/// check the caller to understand it.
797fn possible_in_code_trivia(kind: SyntaxKind) -> bool {
798    !matches!(
799        interpret_mode_at_kind(kind),
800        Some(InterpretMode::Markup | InterpretMode::Math | InterpretMode::Comment)
801    )
802}
803
804/// Classes of arguments that can be operated on by IDE functionality.
805#[derive(Debug, Clone)]
806pub enum ArgClass<'a> {
807    /// A positional argument.
808    Positional {
809        /// The spread arguments met before the positional argument.
810        spreads: EcoVec<LinkedNode<'a>>,
811        /// The index of the positional argument.
812        positional: usize,
813        /// Whether the positional argument is a spread argument.
814        is_spread: bool,
815    },
816    /// A named argument.
817    Named(LinkedNode<'a>),
818}
819
820impl ArgClass<'_> {
821    /// Creates the class refer to the first positional argument.
822    pub fn first_positional() -> Self {
823        ArgClass::Positional {
824            spreads: EcoVec::new(),
825            positional: 0,
826            is_spread: false,
827        }
828    }
829}
830
831// todo: whether we can merge `SurroundingSyntax` and `SyntaxContext`?
832/// Classes of syntax context (outer syntax) that can be operated on by IDE
833#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash, strum::EnumIter)]
834pub enum SurroundingSyntax {
835    /// Regular syntax.
836    Regular,
837    /// Content in a string.
838    StringContent,
839    /// The cursor is directly on the selector of a show rule.
840    Selector,
841    /// The cursor is directly on the transformation of a show rule.
842    ShowTransform,
843    /// The cursor is directly on the import list.
844    ImportList,
845    /// The cursor is directly on the set rule.
846    SetRule,
847    /// The cursor is directly on the parameter list.
848    ParamList,
849}
850
851/// Determines the surrounding syntax of the node at the position.
852pub fn surrounding_syntax(node: &LinkedNode) -> SurroundingSyntax {
853    check_previous_syntax(node)
854        .or_else(|| check_surrounding_syntax(node))
855        .unwrap_or(SurroundingSyntax::Regular)
856}
857
858fn check_surrounding_syntax(mut leaf: &LinkedNode) -> Option<SurroundingSyntax> {
859    use SurroundingSyntax::*;
860    let mut met_args = false;
861
862    if matches!(leaf.kind(), SyntaxKind::Str) {
863        return Some(StringContent);
864    }
865
866    while let Some(parent) = leaf.parent() {
867        crate::log_debug_ct!(
868            "check_surrounding_syntax: {:?}::{:?}",
869            parent.kind(),
870            leaf.kind()
871        );
872        match parent.kind() {
873            SyntaxKind::CodeBlock
874            | SyntaxKind::ContentBlock
875            | SyntaxKind::Equation
876            | SyntaxKind::Closure => {
877                return Some(Regular);
878            }
879            SyntaxKind::ImportItemPath
880            | SyntaxKind::ImportItems
881            | SyntaxKind::RenamedImportItem => {
882                return Some(ImportList);
883            }
884            SyntaxKind::ModuleImport => {
885                let colon = parent.children().find(|s| s.kind() == SyntaxKind::Colon);
886                let Some(colon) = colon else {
887                    return Some(Regular);
888                };
889
890                if leaf.offset() >= colon.offset() {
891                    return Some(ImportList);
892                } else {
893                    return Some(Regular);
894                }
895            }
896            SyntaxKind::Named => {
897                let colon = parent.children().find(|s| s.kind() == SyntaxKind::Colon);
898                let Some(colon) = colon else {
899                    return Some(Regular);
900                };
901
902                return if leaf.offset() >= colon.offset() {
903                    Some(Regular)
904                } else if node_ancestors(leaf).any(|n| n.kind() == SyntaxKind::Params) {
905                    Some(ParamList)
906                } else {
907                    Some(Regular)
908                };
909            }
910            SyntaxKind::Params => {
911                return Some(ParamList);
912            }
913            SyntaxKind::Args => {
914                met_args = true;
915            }
916            SyntaxKind::SetRule => {
917                let rule = parent.get().cast::<ast::SetRule>()?;
918                if met_args || enclosed_by(parent, rule.condition().map(|s| s.span()), leaf) {
919                    return Some(Regular);
920                } else {
921                    return Some(SetRule);
922                }
923            }
924            SyntaxKind::ShowRule => {
925                if met_args {
926                    return Some(Regular);
927                }
928
929                let rule = parent.get().cast::<ast::ShowRule>()?;
930                let colon = rule
931                    .to_untyped()
932                    .children()
933                    .find(|s| s.kind() == SyntaxKind::Colon);
934                let Some(colon) = colon.and_then(|colon| parent.find(colon.span())) else {
935                    // incomplete show rule
936                    return Some(Selector);
937                };
938
939                if leaf.offset() >= colon.offset() {
940                    return Some(ShowTransform);
941                } else {
942                    return Some(Selector); // query's first argument
943                }
944            }
945            _ => {}
946        }
947
948        leaf = parent;
949    }
950
951    None
952}
953
954fn check_previous_syntax(leaf: &LinkedNode) -> Option<SurroundingSyntax> {
955    let mut leaf = leaf.clone();
956    if leaf.kind().is_trivia() {
957        leaf = leaf.prev_sibling()?;
958    }
959    if matches!(
960        leaf.kind(),
961        SyntaxKind::ShowRule
962            | SyntaxKind::SetRule
963            | SyntaxKind::ModuleImport
964            | SyntaxKind::ModuleInclude
965    ) {
966        return check_surrounding_syntax(&leaf.rightmost_leaf()?);
967    }
968
969    if matches!(leaf.kind(), SyntaxKind::Show) {
970        return Some(SurroundingSyntax::Selector);
971    }
972    if matches!(leaf.kind(), SyntaxKind::Set) {
973        return Some(SurroundingSyntax::SetRule);
974    }
975
976    None
977}
978
979fn enclosed_by(parent: &LinkedNode, s: Option<Span>, leaf: &LinkedNode) -> bool {
980    s.and_then(|s| parent.find(s)?.find(leaf.span())).is_some()
981}
982
983/// Classes of syntax context (outer syntax) that can be operated on by IDE
984/// functionality.
985///
986/// A syntax context is either a [`SyntaxClass`] or other things.
987/// One thing is not necessary to refer to some exact node. For example, a
988/// cursor moving after some comma in a function call is identified as a
989/// [`SyntaxContext::Arg`].
990#[derive(Debug, Clone)]
991pub enum SyntaxContext<'a> {
992    /// A cursor on an argument.
993    Arg {
994        /// The callee node.
995        callee: LinkedNode<'a>,
996        /// The arguments node.
997        args: LinkedNode<'a>,
998        /// The argument target pointed by the cursor.
999        target: ArgClass<'a>,
1000        /// Whether the callee is a set rule.
1001        is_set: bool,
1002    },
1003    /// A cursor on an element in an array or dictionary literal.
1004    Element {
1005        /// The container node.
1006        container: LinkedNode<'a>,
1007        /// The element target pointed by the cursor.
1008        target: ArgClass<'a>,
1009    },
1010    /// A cursor on a parenthesized expression.
1011    Paren {
1012        /// The parenthesized expression node.
1013        container: LinkedNode<'a>,
1014        /// Whether the cursor is on the left side of the parenthesized
1015        /// expression.
1016        is_before: bool,
1017    },
1018    /// A variable access expression.
1019    ///
1020    /// It can be either an identifier or a field access.
1021    VarAccess(VarClass<'a>),
1022    /// A cursor on an import path.
1023    ImportPath(LinkedNode<'a>),
1024    /// A cursor on an include path.
1025    IncludePath(LinkedNode<'a>),
1026    /// A cursor on a label.
1027    Label {
1028        /// The label node.
1029        node: LinkedNode<'a>,
1030        /// Whether the label is converted from an error node.
1031        is_error: bool,
1032    },
1033    /// A cursor on a normal [`SyntaxClass`].
1034    Normal(LinkedNode<'a>),
1035}
1036
1037impl<'a> SyntaxContext<'a> {
1038    /// Gets the node of the cursor class.
1039    pub fn node(&self) -> Option<LinkedNode<'a>> {
1040        Some(match self {
1041            SyntaxContext::Arg { target, .. } | SyntaxContext::Element { target, .. } => {
1042                match target {
1043                    ArgClass::Positional { .. } => return None,
1044                    ArgClass::Named(node) => node.clone(),
1045                }
1046            }
1047            SyntaxContext::VarAccess(cls) => cls.node().clone(),
1048            SyntaxContext::Paren { container, .. } => container.clone(),
1049            SyntaxContext::Label { node, .. }
1050            | SyntaxContext::ImportPath(node)
1051            | SyntaxContext::IncludePath(node)
1052            | SyntaxContext::Normal(node) => node.clone(),
1053        })
1054    }
1055}
1056
1057/// Kind of argument source.
1058#[derive(Debug)]
1059enum ArgSourceKind {
1060    /// An argument in a function call.
1061    Call,
1062    /// An argument (element) in an array literal.
1063    Array,
1064    /// An argument (element) in a dictionary literal.
1065    Dict,
1066}
1067
1068/// Classifies node's context (outer syntax) by outer node that can be operated
1069/// on by IDE functionality.
1070pub fn classify_context_outer<'a>(
1071    outer: LinkedNode<'a>,
1072    node: LinkedNode<'a>,
1073) -> Option<SyntaxContext<'a>> {
1074    use SyntaxClass::*;
1075    let context_syntax = classify_syntax(outer.clone(), node.offset())?;
1076    let node_syntax = classify_syntax(node.clone(), node.offset())?;
1077
1078    match context_syntax {
1079        Callee(callee)
1080            if matches!(node_syntax, Normal(..) | Label { .. } | Ref(..))
1081                && !matches!(node_syntax, Callee(..)) =>
1082        {
1083            let parent = callee.parent()?;
1084            let args = match parent.cast::<ast::Expr>() {
1085                Some(ast::Expr::FuncCall(call)) => call.args(),
1086                Some(ast::Expr::Set(set)) => set.args(),
1087                _ => return None,
1088            };
1089            let args = parent.find(args.span())?;
1090
1091            let is_set = parent.kind() == SyntaxKind::SetRule;
1092            let arg_target = arg_context(args.clone(), node, ArgSourceKind::Call)?;
1093            Some(SyntaxContext::Arg {
1094                callee,
1095                args,
1096                target: arg_target,
1097                is_set,
1098            })
1099        }
1100        _ => None,
1101    }
1102}
1103
1104/// Classifies node's context (outer syntax) that can be operated on by IDE
1105/// functionality.
1106pub fn classify_context(node: LinkedNode, cursor: Option<usize>) -> Option<SyntaxContext<'_>> {
1107    let mut node = node;
1108    if node.kind().is_trivia() && node.parent_kind().is_some_and(possible_in_code_trivia) {
1109        loop {
1110            node = node.prev_sibling()?;
1111
1112            if !node.kind().is_trivia() {
1113                break;
1114            }
1115        }
1116    }
1117
1118    let cursor = cursor.unwrap_or_else(|| node.offset());
1119    let syntax = classify_syntax(node.clone(), cursor)?;
1120
1121    let normal_syntax = match syntax {
1122        SyntaxClass::Callee(callee) => {
1123            return callee_context(callee, node);
1124        }
1125        SyntaxClass::Label { node, is_error } => {
1126            return Some(SyntaxContext::Label { node, is_error });
1127        }
1128        SyntaxClass::ImportPath(node) => {
1129            return Some(SyntaxContext::ImportPath(node));
1130        }
1131        SyntaxClass::IncludePath(node) => {
1132            return Some(SyntaxContext::IncludePath(node));
1133        }
1134        syntax => syntax,
1135    };
1136
1137    let Some(mut node_parent) = node.parent().cloned() else {
1138        return Some(SyntaxContext::Normal(node));
1139    };
1140
1141    while let SyntaxKind::Named | SyntaxKind::Colon = node_parent.kind() {
1142        let Some(parent) = node_parent.parent() else {
1143            return Some(SyntaxContext::Normal(node));
1144        };
1145        node_parent = parent.clone();
1146    }
1147
1148    match node_parent.kind() {
1149        SyntaxKind::Args => {
1150            let callee = node_ancestors(&node_parent).find_map(|ancestor| {
1151                let span = match ancestor.cast::<ast::Expr>()? {
1152                    ast::Expr::FuncCall(call) => call.callee().span(),
1153                    ast::Expr::Set(set) => set.target().span(),
1154                    _ => return None,
1155                };
1156                ancestor.find(span)
1157            })?;
1158
1159            let param_node = match node.kind() {
1160                SyntaxKind::Ident
1161                    if matches!(
1162                        node.parent_kind().zip(node.next_sibling_kind()),
1163                        Some((SyntaxKind::Named, SyntaxKind::Colon))
1164                    ) =>
1165                {
1166                    node
1167                }
1168                _ if matches!(node.parent_kind(), Some(SyntaxKind::Named)) => {
1169                    node.parent().cloned()?
1170                }
1171                _ => node,
1172            };
1173
1174            callee_context(callee, param_node)
1175        }
1176        SyntaxKind::Array | SyntaxKind::Dict => {
1177            let element_target = arg_context(
1178                node_parent.clone(),
1179                node.clone(),
1180                match node_parent.kind() {
1181                    SyntaxKind::Array => ArgSourceKind::Array,
1182                    SyntaxKind::Dict => ArgSourceKind::Dict,
1183                    _ => unreachable!(),
1184                },
1185            )?;
1186            Some(SyntaxContext::Element {
1187                container: node_parent.clone(),
1188                target: element_target,
1189            })
1190        }
1191        SyntaxKind::Parenthesized => {
1192            let is_before = node.offset() <= node_parent.offset() + 1;
1193            Some(SyntaxContext::Paren {
1194                container: node_parent.clone(),
1195                is_before,
1196            })
1197        }
1198        _ => Some(match normal_syntax {
1199            SyntaxClass::VarAccess(v) => SyntaxContext::VarAccess(v),
1200            normal_syntax => SyntaxContext::Normal(normal_syntax.node().clone()),
1201        }),
1202    }
1203}
1204
1205fn callee_context<'a>(callee: LinkedNode<'a>, node: LinkedNode<'a>) -> Option<SyntaxContext<'a>> {
1206    let parent = callee.parent()?;
1207    let args = match parent.cast::<ast::Expr>() {
1208        Some(ast::Expr::FuncCall(call)) => call.args(),
1209        Some(ast::Expr::Set(set)) => set.args(),
1210        _ => return None,
1211    };
1212    let args = parent.find(args.span())?;
1213
1214    let is_set = parent.kind() == SyntaxKind::SetRule;
1215    let target = arg_context(args.clone(), node, ArgSourceKind::Call)?;
1216    Some(SyntaxContext::Arg {
1217        callee,
1218        args,
1219        target,
1220        is_set,
1221    })
1222}
1223
1224fn arg_context<'a>(
1225    args_node: LinkedNode<'a>,
1226    mut node: LinkedNode<'a>,
1227    param_kind: ArgSourceKind,
1228) -> Option<ArgClass<'a>> {
1229    if node.kind() == SyntaxKind::RightParen {
1230        node = node.prev_sibling()?;
1231    }
1232    match node.kind() {
1233        SyntaxKind::Named => {
1234            let param_ident = node.cast::<ast::Named>()?.name();
1235            Some(ArgClass::Named(args_node.find(param_ident.span())?))
1236        }
1237        SyntaxKind::Colon => {
1238            let prev = node.prev_leaf()?;
1239            let param_ident = prev.cast::<ast::Ident>()?;
1240            Some(ArgClass::Named(args_node.find(param_ident.span())?))
1241        }
1242        _ => {
1243            let parent = node.parent();
1244            if let Some(parent) = parent {
1245                if parent.kind() == SyntaxKind::Named {
1246                    let param_ident = parent.cast::<ast::Named>()?;
1247                    let name = param_ident.name();
1248                    let init = param_ident.expr();
1249                    let init = parent.find(init.span())?;
1250                    if init.range().contains(&node.offset()) {
1251                        let name = args_node.find(name.span())?;
1252                        return Some(ArgClass::Named(name));
1253                    }
1254                }
1255            }
1256
1257            let mut spreads = EcoVec::new();
1258            let mut positional = 0;
1259            let is_spread = node.kind() == SyntaxKind::Spread;
1260
1261            let args_before = args_node
1262                .children()
1263                .take_while(|arg| arg.range().end <= node.offset());
1264            match param_kind {
1265                ArgSourceKind::Call => {
1266                    for ch in args_before {
1267                        match ch.cast::<ast::Arg>() {
1268                            Some(ast::Arg::Pos(..)) => {
1269                                positional += 1;
1270                            }
1271                            Some(ast::Arg::Spread(..)) => {
1272                                spreads.push(ch);
1273                            }
1274                            Some(ast::Arg::Named(..)) | None => {}
1275                        }
1276                    }
1277                }
1278                ArgSourceKind::Array => {
1279                    for ch in args_before {
1280                        match ch.cast::<ast::ArrayItem>() {
1281                            Some(ast::ArrayItem::Pos(..)) => {
1282                                positional += 1;
1283                            }
1284                            Some(ast::ArrayItem::Spread(..)) => {
1285                                spreads.push(ch);
1286                            }
1287                            _ => {}
1288                        }
1289                    }
1290                }
1291                ArgSourceKind::Dict => {
1292                    for ch in args_before {
1293                        if let Some(ast::DictItem::Spread(..)) = ch.cast::<ast::DictItem>() {
1294                            spreads.push(ch);
1295                        }
1296                    }
1297                }
1298            }
1299
1300            Some(ArgClass::Positional {
1301                spreads,
1302                positional,
1303                is_spread,
1304            })
1305        }
1306    }
1307}
1308
1309#[cfg(test)]
1310mod tests {
1311    use super::*;
1312    use insta::assert_snapshot;
1313    use typst::syntax::{is_newline, Side, Source};
1314
1315    fn map_node(source: &str, mapper: impl Fn(&LinkedNode, usize) -> char) -> String {
1316        let source = Source::detached(source.to_owned());
1317        let root = LinkedNode::new(source.root());
1318        let mut output_mapping = String::new();
1319
1320        let mut cursor = 0;
1321        for ch in source.text().chars() {
1322            cursor += ch.len_utf8();
1323            if is_newline(ch) {
1324                output_mapping.push(ch);
1325                continue;
1326            }
1327
1328            output_mapping.push(mapper(&root, cursor));
1329        }
1330
1331        source
1332            .text()
1333            .lines()
1334            .zip(output_mapping.lines())
1335            .flat_map(|(a, b)| [a, "\n", b, "\n"])
1336            .collect::<String>()
1337    }
1338
1339    fn map_syntax(source: &str) -> String {
1340        map_node(source, |root, cursor| {
1341            let node = root.leaf_at(cursor, Side::Before);
1342            let kind = node.and_then(|node| classify_syntax(node, cursor));
1343            match kind {
1344                Some(SyntaxClass::VarAccess(..)) => 'v',
1345                Some(SyntaxClass::Normal(..)) => 'n',
1346                Some(SyntaxClass::Label { .. }) => 'l',
1347                Some(SyntaxClass::Ref(..)) => 'r',
1348                Some(SyntaxClass::Callee(..)) => 'c',
1349                Some(SyntaxClass::ImportPath(..)) => 'i',
1350                Some(SyntaxClass::IncludePath(..)) => 'I',
1351                None => ' ',
1352            }
1353        })
1354    }
1355
1356    fn map_context(source: &str) -> String {
1357        map_node(source, |root, cursor| {
1358            let node = root.leaf_at(cursor, Side::Before);
1359            let kind = node.and_then(|node| classify_context(node, Some(cursor)));
1360            match kind {
1361                Some(SyntaxContext::Arg { .. }) => 'p',
1362                Some(SyntaxContext::Element { .. }) => 'e',
1363                Some(SyntaxContext::Paren { .. }) => 'P',
1364                Some(SyntaxContext::VarAccess { .. }) => 'v',
1365                Some(SyntaxContext::ImportPath(..)) => 'i',
1366                Some(SyntaxContext::IncludePath(..)) => 'I',
1367                Some(SyntaxContext::Label { .. }) => 'l',
1368                Some(SyntaxContext::Normal(..)) => 'n',
1369                None => ' ',
1370            }
1371        })
1372    }
1373
1374    #[test]
1375    fn test_get_syntax() {
1376        assert_snapshot!(map_syntax(r#"#let x = 1  
1377Text
1378= Heading #let y = 2;  
1379== Heading"#).trim(), @r"
1380        #let x = 1  
1381         nnnnvvnnn  
1382        Text
1383            
1384        = Heading #let y = 2;  
1385                   nnnnvvnnn   
1386        == Heading
1387        ");
1388        assert_snapshot!(map_syntax(r#"#let f(x);"#).trim(), @r"
1389        #let f(x);
1390         nnnnv v
1391        ");
1392        assert_snapshot!(map_syntax(r#"#{
1393  calc.  
1394}"#).trim(), @r"
1395        #{
1396         n
1397          calc.  
1398        nnvvvvvnn
1399        }
1400        n
1401        ");
1402    }
1403
1404    #[test]
1405    fn test_get_context() {
1406        assert_snapshot!(map_context(r#"#let x = 1  
1407Text
1408= Heading #let y = 2;  
1409== Heading"#).trim(), @r"
1410        #let x = 1  
1411         nnnnvvnnn  
1412        Text
1413            
1414        = Heading #let y = 2;  
1415                   nnnnvvnnn   
1416        == Heading
1417        ");
1418        assert_snapshot!(map_context(r#"#let f(x);"#).trim(), @r"
1419        #let f(x);
1420         nnnnv v
1421        ");
1422        assert_snapshot!(map_context(r#"#f(1, 2)   Test"#).trim(), @r"
1423        #f(1, 2)   Test
1424         vpppppp
1425        ");
1426        assert_snapshot!(map_context(r#"#()   Test"#).trim(), @r"
1427        #()   Test
1428         ee
1429        ");
1430        assert_snapshot!(map_context(r#"#(1)   Test"#).trim(), @r"
1431        #(1)   Test
1432         PPP
1433        ");
1434        assert_snapshot!(map_context(r#"#(a: 1)   Test"#).trim(), @r"
1435        #(a: 1)   Test
1436         eeeeee
1437        ");
1438        assert_snapshot!(map_context(r#"#(1, 2)   Test"#).trim(), @r"
1439        #(1, 2)   Test
1440         eeeeee
1441        ");
1442        assert_snapshot!(map_context(r#"#(1, 2)  
1443  Test"#).trim(), @r"
1444        #(1, 2)  
1445         eeeeee  
1446          Test
1447        ");
1448    }
1449
1450    fn access_node(s: &str, cursor: i32) -> String {
1451        access_node_(s, cursor).unwrap_or_default()
1452    }
1453
1454    fn access_node_(s: &str, cursor: i32) -> Option<String> {
1455        access_var(s, cursor, |_source, var| {
1456            Some(var.accessed_node()?.get().clone().into_text().into())
1457        })
1458    }
1459
1460    fn access_field(s: &str, cursor: i32) -> String {
1461        access_field_(s, cursor).unwrap_or_default()
1462    }
1463
1464    fn access_field_(s: &str, cursor: i32) -> Option<String> {
1465        access_var(s, cursor, |source, var| {
1466            let field = var.accessing_field()?;
1467            Some(match field {
1468                FieldClass::Field(ident) => format!("Field: {}", ident.text()),
1469                FieldClass::DotSuffix(span_offset) => {
1470                    let offset = source.find(span_offset.span)?.offset() + span_offset.offset;
1471                    format!("DotSuffix: {offset:?}")
1472                }
1473            })
1474        })
1475    }
1476
1477    fn access_var(
1478        s: &str,
1479        cursor: i32,
1480        f: impl FnOnce(&Source, VarClass) -> Option<String>,
1481    ) -> Option<String> {
1482        let cursor = if cursor < 0 {
1483            s.len() as i32 + cursor
1484        } else {
1485            cursor
1486        };
1487        let source = Source::detached(s.to_owned());
1488        let root = LinkedNode::new(source.root());
1489        let node = root.leaf_at(cursor as usize, Side::Before)?;
1490        let syntax = classify_syntax(node, cursor as usize)?;
1491        let SyntaxClass::VarAccess(var) = syntax else {
1492            return None;
1493        };
1494        f(&source, var)
1495    }
1496
1497    #[test]
1498    fn test_access_field() {
1499        assert_snapshot!(access_field("#(a.b)", 5), @r"Field: b");
1500        assert_snapshot!(access_field("#a.", 3), @"DotSuffix: 3");
1501        assert_snapshot!(access_field("$a.$", 3), @"DotSuffix: 3");
1502        assert_snapshot!(access_field("#(a.)", 4), @"DotSuffix: 4");
1503        assert_snapshot!(access_node("#(a..b)", 4), @"a");
1504        assert_snapshot!(access_field("#(a..b)", 4), @"DotSuffix: 4");
1505        assert_snapshot!(access_node("#(a..b())", 4), @"a");
1506        assert_snapshot!(access_field("#(a..b())", 4), @"DotSuffix: 4");
1507    }
1508
1509    #[test]
1510    fn test_code_access() {
1511        assert_snapshot!(access_node("#{`a`.}", 6), @"`a`");
1512        assert_snapshot!(access_field("#{`a`.}", 6), @"DotSuffix: 6");
1513        assert_snapshot!(access_node("#{$a$.}", 6), @"$a$");
1514        assert_snapshot!(access_field("#{$a$.}", 6), @"DotSuffix: 6");
1515        assert_snapshot!(access_node("#{\"a\".}", 6), @"\"a\"");
1516        assert_snapshot!(access_field("#{\"a\".}", 6), @"DotSuffix: 6");
1517        assert_snapshot!(access_node("#{<a>.}", 6), @"<a>");
1518        assert_snapshot!(access_field("#{<a>.}", 6), @"DotSuffix: 6");
1519    }
1520
1521    #[test]
1522    fn test_markup_access() {
1523        assert_snapshot!(access_field("_a_.", 4), @"");
1524        assert_snapshot!(access_field("*a*.", 4), @"");
1525        assert_snapshot!(access_field("`a`.", 4), @"");
1526        assert_snapshot!(access_field("$a$.", 4), @"");
1527        assert_snapshot!(access_field("\"a\".", 4), @"");
1528        assert_snapshot!(access_field("@a.", 3), @"");
1529        assert_snapshot!(access_field("<a>.", 4), @"");
1530    }
1531
1532    #[test]
1533    fn test_hash_access() {
1534        assert_snapshot!(access_node("#a.", 3), @"a");
1535        assert_snapshot!(access_field("#a.", 3), @"DotSuffix: 3");
1536        assert_snapshot!(access_node("#(a).", 5), @"(a)");
1537        assert_snapshot!(access_field("#(a).", 5), @"DotSuffix: 5");
1538        assert_snapshot!(access_node("#`a`.", 5), @"`a`");
1539        assert_snapshot!(access_field("#`a`.", 5), @"DotSuffix: 5");
1540        assert_snapshot!(access_node("#$a$.", 5), @"$a$");
1541        assert_snapshot!(access_field("#$a$.", 5), @"DotSuffix: 5");
1542        assert_snapshot!(access_node("#(a,).", 6), @"(a,)");
1543        assert_snapshot!(access_field("#(a,).", 6), @"DotSuffix: 6");
1544    }
1545}