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    if matches!(node.kind(), SyntaxKind::Text) {
750        let mode = interpret_mode_at(Some(&node));
751        if matches!(mode, InterpretMode::Math) && is_ident_like(&node) {
752            return Some(SyntaxClass::VarAccess(VarClass::Ident(node)));
753        }
754    }
755
756    // Move to the first ancestor that is an expression.
757    let ancestor = first_ancestor_expr(node)?;
758    crate::log_debug_ct!("first_ancestor_expr: {ancestor:?}");
759
760    // Unwrap all parentheses to get the actual expression.
761    let adjusted = adjust_expr(ancestor)?;
762    crate::log_debug_ct!("adjust_expr: {adjusted:?}");
763
764    // Identify convenient expression kinds.
765    let expr = adjusted.cast::<ast::Expr>()?;
766    Some(match expr {
767        ast::Expr::Label(..) => SyntaxClass::label(adjusted),
768        ast::Expr::Ref(..) => SyntaxClass::Ref(adjusted),
769        ast::Expr::FuncCall(call) => SyntaxClass::Callee(adjusted.find(call.callee().span())?),
770        ast::Expr::Set(set) => SyntaxClass::Callee(adjusted.find(set.target().span())?),
771        ast::Expr::Ident(..) | ast::Expr::MathIdent(..) => {
772            SyntaxClass::VarAccess(VarClass::Ident(adjusted))
773        }
774        ast::Expr::FieldAccess(..) => SyntaxClass::VarAccess(VarClass::FieldAccess(adjusted)),
775        ast::Expr::Str(..) => {
776            let parent = adjusted.parent()?;
777            if parent.kind() == SyntaxKind::ModuleImport {
778                SyntaxClass::ImportPath(adjusted)
779            } else if parent.kind() == SyntaxKind::ModuleInclude {
780                SyntaxClass::IncludePath(adjusted)
781            } else {
782                SyntaxClass::Normal(adjusted.kind(), adjusted)
783            }
784        }
785        _ if expr.hash()
786            || matches!(adjusted.kind(), SyntaxKind::MathIdent | SyntaxKind::Error) =>
787        {
788            SyntaxClass::Normal(adjusted.kind(), adjusted)
789        }
790        _ => return None,
791    })
792}
793
794/// Whether the node might be in code trivia. This is a bit internal so please
795/// check the caller to understand it.
796fn possible_in_code_trivia(kind: SyntaxKind) -> bool {
797    !matches!(
798        interpret_mode_at_kind(kind),
799        Some(InterpretMode::Markup | InterpretMode::Math | InterpretMode::Comment)
800    )
801}
802
803/// Classes of arguments that can be operated on by IDE functionality.
804#[derive(Debug, Clone)]
805pub enum ArgClass<'a> {
806    /// A positional argument.
807    Positional {
808        /// The spread arguments met before the positional argument.
809        spreads: EcoVec<LinkedNode<'a>>,
810        /// The index of the positional argument.
811        positional: usize,
812        /// Whether the positional argument is a spread argument.
813        is_spread: bool,
814    },
815    /// A named argument.
816    Named(LinkedNode<'a>),
817}
818
819impl ArgClass<'_> {
820    /// Creates the class refer to the first positional argument.
821    pub fn first_positional() -> Self {
822        ArgClass::Positional {
823            spreads: EcoVec::new(),
824            positional: 0,
825            is_spread: false,
826        }
827    }
828}
829
830// todo: whether we can merge `SurroundingSyntax` and `SyntaxContext`?
831/// Classes of syntax context (outer syntax) that can be operated on by IDE
832#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash, strum::EnumIter)]
833pub enum SurroundingSyntax {
834    /// Regular syntax.
835    Regular,
836    /// Content in a string.
837    StringContent,
838    /// The cursor is directly on the selector of a show rule.
839    Selector,
840    /// The cursor is directly on the transformation of a show rule.
841    ShowTransform,
842    /// The cursor is directly on the import list.
843    ImportList,
844    /// The cursor is directly on the set rule.
845    SetRule,
846    /// The cursor is directly on the parameter list.
847    ParamList,
848}
849
850/// Determines the surrounding syntax of the node at the position.
851pub fn surrounding_syntax(node: &LinkedNode) -> SurroundingSyntax {
852    check_previous_syntax(node)
853        .or_else(|| check_surrounding_syntax(node))
854        .unwrap_or(SurroundingSyntax::Regular)
855}
856
857fn check_surrounding_syntax(mut leaf: &LinkedNode) -> Option<SurroundingSyntax> {
858    use SurroundingSyntax::*;
859    let mut met_args = false;
860
861    if matches!(leaf.kind(), SyntaxKind::Str) {
862        return Some(StringContent);
863    }
864
865    while let Some(parent) = leaf.parent() {
866        crate::log_debug_ct!(
867            "check_surrounding_syntax: {:?}::{:?}",
868            parent.kind(),
869            leaf.kind()
870        );
871        match parent.kind() {
872            SyntaxKind::CodeBlock
873            | SyntaxKind::ContentBlock
874            | SyntaxKind::Equation
875            | SyntaxKind::Closure => {
876                return Some(Regular);
877            }
878            SyntaxKind::ImportItemPath
879            | SyntaxKind::ImportItems
880            | SyntaxKind::RenamedImportItem => {
881                return Some(ImportList);
882            }
883            SyntaxKind::ModuleImport => {
884                let colon = parent.children().find(|s| s.kind() == SyntaxKind::Colon);
885                let Some(colon) = colon else {
886                    return Some(Regular);
887                };
888
889                if leaf.offset() >= colon.offset() {
890                    return Some(ImportList);
891                } else {
892                    return Some(Regular);
893                }
894            }
895            SyntaxKind::Named => {
896                let colon = parent.children().find(|s| s.kind() == SyntaxKind::Colon);
897                let Some(colon) = colon else {
898                    return Some(Regular);
899                };
900
901                return if leaf.offset() >= colon.offset() {
902                    Some(Regular)
903                } else if node_ancestors(leaf).any(|n| n.kind() == SyntaxKind::Params) {
904                    Some(ParamList)
905                } else {
906                    Some(Regular)
907                };
908            }
909            SyntaxKind::Params => {
910                return Some(ParamList);
911            }
912            SyntaxKind::Args => {
913                met_args = true;
914            }
915            SyntaxKind::SetRule => {
916                let rule = parent.get().cast::<ast::SetRule>()?;
917                if met_args || enclosed_by(parent, rule.condition().map(|s| s.span()), leaf) {
918                    return Some(Regular);
919                } else {
920                    return Some(SetRule);
921                }
922            }
923            SyntaxKind::ShowRule => {
924                if met_args {
925                    return Some(Regular);
926                }
927
928                let rule = parent.get().cast::<ast::ShowRule>()?;
929                let colon = rule
930                    .to_untyped()
931                    .children()
932                    .find(|s| s.kind() == SyntaxKind::Colon);
933                let Some(colon) = colon.and_then(|colon| parent.find(colon.span())) else {
934                    // incomplete show rule
935                    return Some(Selector);
936                };
937
938                if leaf.offset() >= colon.offset() {
939                    return Some(ShowTransform);
940                } else {
941                    return Some(Selector); // query's first argument
942                }
943            }
944            _ => {}
945        }
946
947        leaf = parent;
948    }
949
950    None
951}
952
953fn check_previous_syntax(leaf: &LinkedNode) -> Option<SurroundingSyntax> {
954    let mut leaf = leaf.clone();
955    if leaf.kind().is_trivia() {
956        leaf = leaf.prev_sibling()?;
957    }
958    if matches!(
959        leaf.kind(),
960        SyntaxKind::ShowRule
961            | SyntaxKind::SetRule
962            | SyntaxKind::ModuleImport
963            | SyntaxKind::ModuleInclude
964    ) {
965        return check_surrounding_syntax(&leaf.rightmost_leaf()?);
966    }
967
968    if matches!(leaf.kind(), SyntaxKind::Show) {
969        return Some(SurroundingSyntax::Selector);
970    }
971    if matches!(leaf.kind(), SyntaxKind::Set) {
972        return Some(SurroundingSyntax::SetRule);
973    }
974
975    None
976}
977
978fn enclosed_by(parent: &LinkedNode, s: Option<Span>, leaf: &LinkedNode) -> bool {
979    s.and_then(|s| parent.find(s)?.find(leaf.span())).is_some()
980}
981
982/// Classes of syntax context (outer syntax) that can be operated on by IDE
983/// functionality.
984///
985/// A syntax context is either a [`SyntaxClass`] or other things.
986/// One thing is not necessary to refer to some exact node. For example, a
987/// cursor moving after some comma in a function call is identified as a
988/// [`SyntaxContext::Arg`].
989#[derive(Debug, Clone)]
990pub enum SyntaxContext<'a> {
991    /// A cursor on an argument.
992    Arg {
993        /// The callee node.
994        callee: LinkedNode<'a>,
995        /// The arguments node.
996        args: LinkedNode<'a>,
997        /// The argument target pointed by the cursor.
998        target: ArgClass<'a>,
999        /// Whether the callee is a set rule.
1000        is_set: bool,
1001    },
1002    /// A cursor on an element in an array or dictionary literal.
1003    Element {
1004        /// The container node.
1005        container: LinkedNode<'a>,
1006        /// The element target pointed by the cursor.
1007        target: ArgClass<'a>,
1008    },
1009    /// A cursor on a parenthesized expression.
1010    Paren {
1011        /// The parenthesized expression node.
1012        container: LinkedNode<'a>,
1013        /// Whether the cursor is on the left side of the parenthesized
1014        /// expression.
1015        is_before: bool,
1016    },
1017    /// A variable access expression.
1018    ///
1019    /// It can be either an identifier or a field access.
1020    VarAccess(VarClass<'a>),
1021    /// A cursor on an import path.
1022    ImportPath(LinkedNode<'a>),
1023    /// A cursor on an include path.
1024    IncludePath(LinkedNode<'a>),
1025    /// A cursor on a label.
1026    Label {
1027        /// The label node.
1028        node: LinkedNode<'a>,
1029        /// Whether the label is converted from an error node.
1030        is_error: bool,
1031    },
1032    /// A cursor on a normal [`SyntaxClass`].
1033    Normal(LinkedNode<'a>),
1034}
1035
1036impl<'a> SyntaxContext<'a> {
1037    /// Gets the node of the cursor class.
1038    pub fn node(&self) -> Option<LinkedNode<'a>> {
1039        Some(match self {
1040            SyntaxContext::Arg { target, .. } | SyntaxContext::Element { target, .. } => {
1041                match target {
1042                    ArgClass::Positional { .. } => return None,
1043                    ArgClass::Named(node) => node.clone(),
1044                }
1045            }
1046            SyntaxContext::VarAccess(cls) => cls.node().clone(),
1047            SyntaxContext::Paren { container, .. } => container.clone(),
1048            SyntaxContext::Label { node, .. }
1049            | SyntaxContext::ImportPath(node)
1050            | SyntaxContext::IncludePath(node)
1051            | SyntaxContext::Normal(node) => node.clone(),
1052        })
1053    }
1054}
1055
1056/// Kind of argument source.
1057#[derive(Debug)]
1058enum ArgSourceKind {
1059    /// An argument in a function call.
1060    Call,
1061    /// An argument (element) in an array literal.
1062    Array,
1063    /// An argument (element) in a dictionary literal.
1064    Dict,
1065}
1066
1067/// Classifies node's context (outer syntax) by outer node that can be operated
1068/// on by IDE functionality.
1069pub fn classify_context_outer<'a>(
1070    outer: LinkedNode<'a>,
1071    node: LinkedNode<'a>,
1072) -> Option<SyntaxContext<'a>> {
1073    use SyntaxClass::*;
1074    let context_syntax = classify_syntax(outer.clone(), node.offset())?;
1075    let node_syntax = classify_syntax(node.clone(), node.offset())?;
1076
1077    match context_syntax {
1078        Callee(callee)
1079            if matches!(node_syntax, Normal(..) | Label { .. } | Ref(..))
1080                && !matches!(node_syntax, Callee(..)) =>
1081        {
1082            let parent = callee.parent()?;
1083            let args = match parent.cast::<ast::Expr>() {
1084                Some(ast::Expr::FuncCall(call)) => call.args(),
1085                Some(ast::Expr::Set(set)) => set.args(),
1086                _ => return None,
1087            };
1088            let args = parent.find(args.span())?;
1089
1090            let is_set = parent.kind() == SyntaxKind::SetRule;
1091            let arg_target = arg_context(args.clone(), node, ArgSourceKind::Call)?;
1092            Some(SyntaxContext::Arg {
1093                callee,
1094                args,
1095                target: arg_target,
1096                is_set,
1097            })
1098        }
1099        _ => None,
1100    }
1101}
1102
1103/// Classifies node's context (outer syntax) that can be operated on by IDE
1104/// functionality.
1105pub fn classify_context(node: LinkedNode, cursor: Option<usize>) -> Option<SyntaxContext<'_>> {
1106    let mut node = node;
1107    if node.kind().is_trivia() && node.parent_kind().is_some_and(possible_in_code_trivia) {
1108        loop {
1109            node = node.prev_sibling()?;
1110
1111            if !node.kind().is_trivia() {
1112                break;
1113            }
1114        }
1115    }
1116
1117    let cursor = cursor.unwrap_or_else(|| node.offset());
1118    let syntax = classify_syntax(node.clone(), cursor)?;
1119
1120    let normal_syntax = match syntax {
1121        SyntaxClass::Callee(callee) => {
1122            return callee_context(callee, node);
1123        }
1124        SyntaxClass::Label { node, is_error } => {
1125            return Some(SyntaxContext::Label { node, is_error });
1126        }
1127        SyntaxClass::ImportPath(node) => {
1128            return Some(SyntaxContext::ImportPath(node));
1129        }
1130        SyntaxClass::IncludePath(node) => {
1131            return Some(SyntaxContext::IncludePath(node));
1132        }
1133        syntax => syntax,
1134    };
1135
1136    let Some(mut node_parent) = node.parent().cloned() else {
1137        return Some(SyntaxContext::Normal(node));
1138    };
1139
1140    while let SyntaxKind::Named | SyntaxKind::Colon = node_parent.kind() {
1141        let Some(parent) = node_parent.parent() else {
1142            return Some(SyntaxContext::Normal(node));
1143        };
1144        node_parent = parent.clone();
1145    }
1146
1147    match node_parent.kind() {
1148        SyntaxKind::Args => {
1149            let callee = node_ancestors(&node_parent).find_map(|ancestor| {
1150                let span = match ancestor.cast::<ast::Expr>()? {
1151                    ast::Expr::FuncCall(call) => call.callee().span(),
1152                    ast::Expr::Set(set) => set.target().span(),
1153                    _ => return None,
1154                };
1155                ancestor.find(span)
1156            })?;
1157
1158            let param_node = match node.kind() {
1159                SyntaxKind::Ident
1160                    if matches!(
1161                        node.parent_kind().zip(node.next_sibling_kind()),
1162                        Some((SyntaxKind::Named, SyntaxKind::Colon))
1163                    ) =>
1164                {
1165                    node
1166                }
1167                _ if matches!(node.parent_kind(), Some(SyntaxKind::Named)) => {
1168                    node.parent().cloned()?
1169                }
1170                _ => node,
1171            };
1172
1173            callee_context(callee, param_node)
1174        }
1175        SyntaxKind::Array | SyntaxKind::Dict => {
1176            let element_target = arg_context(
1177                node_parent.clone(),
1178                node.clone(),
1179                match node_parent.kind() {
1180                    SyntaxKind::Array => ArgSourceKind::Array,
1181                    SyntaxKind::Dict => ArgSourceKind::Dict,
1182                    _ => unreachable!(),
1183                },
1184            )?;
1185            Some(SyntaxContext::Element {
1186                container: node_parent.clone(),
1187                target: element_target,
1188            })
1189        }
1190        SyntaxKind::Parenthesized => {
1191            let is_before = node.offset() <= node_parent.offset() + 1;
1192            Some(SyntaxContext::Paren {
1193                container: node_parent.clone(),
1194                is_before,
1195            })
1196        }
1197        _ => Some(match normal_syntax {
1198            SyntaxClass::VarAccess(v) => SyntaxContext::VarAccess(v),
1199            normal_syntax => SyntaxContext::Normal(normal_syntax.node().clone()),
1200        }),
1201    }
1202}
1203
1204fn callee_context<'a>(callee: LinkedNode<'a>, node: LinkedNode<'a>) -> Option<SyntaxContext<'a>> {
1205    let parent = callee.parent()?;
1206    let args = match parent.cast::<ast::Expr>() {
1207        Some(ast::Expr::FuncCall(call)) => call.args(),
1208        Some(ast::Expr::Set(set)) => set.args(),
1209        _ => return None,
1210    };
1211    let args = parent.find(args.span())?;
1212
1213    let is_set = parent.kind() == SyntaxKind::SetRule;
1214    let target = arg_context(args.clone(), node, ArgSourceKind::Call)?;
1215    Some(SyntaxContext::Arg {
1216        callee,
1217        args,
1218        target,
1219        is_set,
1220    })
1221}
1222
1223fn arg_context<'a>(
1224    args_node: LinkedNode<'a>,
1225    mut node: LinkedNode<'a>,
1226    param_kind: ArgSourceKind,
1227) -> Option<ArgClass<'a>> {
1228    if node.kind() == SyntaxKind::RightParen {
1229        node = node.prev_sibling()?;
1230    }
1231    match node.kind() {
1232        SyntaxKind::Named => {
1233            let param_ident = node.cast::<ast::Named>()?.name();
1234            Some(ArgClass::Named(args_node.find(param_ident.span())?))
1235        }
1236        SyntaxKind::Colon => {
1237            let prev = node.prev_leaf()?;
1238            let param_ident = prev.cast::<ast::Ident>()?;
1239            Some(ArgClass::Named(args_node.find(param_ident.span())?))
1240        }
1241        _ => {
1242            let mut spreads = EcoVec::new();
1243            let mut positional = 0;
1244            let is_spread = node.kind() == SyntaxKind::Spread;
1245
1246            let args_before = args_node
1247                .children()
1248                .take_while(|arg| arg.range().end <= node.offset());
1249            match param_kind {
1250                ArgSourceKind::Call => {
1251                    for ch in args_before {
1252                        match ch.cast::<ast::Arg>() {
1253                            Some(ast::Arg::Pos(..)) => {
1254                                positional += 1;
1255                            }
1256                            Some(ast::Arg::Spread(..)) => {
1257                                spreads.push(ch);
1258                            }
1259                            Some(ast::Arg::Named(..)) | None => {}
1260                        }
1261                    }
1262                }
1263                ArgSourceKind::Array => {
1264                    for ch in args_before {
1265                        match ch.cast::<ast::ArrayItem>() {
1266                            Some(ast::ArrayItem::Pos(..)) => {
1267                                positional += 1;
1268                            }
1269                            Some(ast::ArrayItem::Spread(..)) => {
1270                                spreads.push(ch);
1271                            }
1272                            _ => {}
1273                        }
1274                    }
1275                }
1276                ArgSourceKind::Dict => {
1277                    for ch in args_before {
1278                        if let Some(ast::DictItem::Spread(..)) = ch.cast::<ast::DictItem>() {
1279                            spreads.push(ch);
1280                        }
1281                    }
1282                }
1283            }
1284
1285            Some(ArgClass::Positional {
1286                spreads,
1287                positional,
1288                is_spread,
1289            })
1290        }
1291    }
1292}
1293
1294#[cfg(test)]
1295mod tests {
1296    use super::*;
1297    use insta::assert_snapshot;
1298    use typst::syntax::{is_newline, Side, Source};
1299
1300    fn map_node(source: &str, mapper: impl Fn(&LinkedNode, usize) -> char) -> String {
1301        let source = Source::detached(source.to_owned());
1302        let root = LinkedNode::new(source.root());
1303        let mut output_mapping = String::new();
1304
1305        let mut cursor = 0;
1306        for ch in source.text().chars() {
1307            cursor += ch.len_utf8();
1308            if is_newline(ch) {
1309                output_mapping.push(ch);
1310                continue;
1311            }
1312
1313            output_mapping.push(mapper(&root, cursor));
1314        }
1315
1316        source
1317            .text()
1318            .lines()
1319            .zip(output_mapping.lines())
1320            .flat_map(|(a, b)| [a, "\n", b, "\n"])
1321            .collect::<String>()
1322    }
1323
1324    fn map_syntax(source: &str) -> String {
1325        map_node(source, |root, cursor| {
1326            let node = root.leaf_at(cursor, Side::Before);
1327            let kind = node.and_then(|node| classify_syntax(node, cursor));
1328            match kind {
1329                Some(SyntaxClass::VarAccess(..)) => 'v',
1330                Some(SyntaxClass::Normal(..)) => 'n',
1331                Some(SyntaxClass::Label { .. }) => 'l',
1332                Some(SyntaxClass::Ref(..)) => 'r',
1333                Some(SyntaxClass::Callee(..)) => 'c',
1334                Some(SyntaxClass::ImportPath(..)) => 'i',
1335                Some(SyntaxClass::IncludePath(..)) => 'I',
1336                None => ' ',
1337            }
1338        })
1339    }
1340
1341    fn map_context(source: &str) -> String {
1342        map_node(source, |root, cursor| {
1343            let node = root.leaf_at(cursor, Side::Before);
1344            let kind = node.and_then(|node| classify_context(node, Some(cursor)));
1345            match kind {
1346                Some(SyntaxContext::Arg { .. }) => 'p',
1347                Some(SyntaxContext::Element { .. }) => 'e',
1348                Some(SyntaxContext::Paren { .. }) => 'P',
1349                Some(SyntaxContext::VarAccess { .. }) => 'v',
1350                Some(SyntaxContext::ImportPath(..)) => 'i',
1351                Some(SyntaxContext::IncludePath(..)) => 'I',
1352                Some(SyntaxContext::Label { .. }) => 'l',
1353                Some(SyntaxContext::Normal(..)) => 'n',
1354                None => ' ',
1355            }
1356        })
1357    }
1358
1359    #[test]
1360    fn test_get_syntax() {
1361        assert_snapshot!(map_syntax(r#"#let x = 1  
1362Text
1363= Heading #let y = 2;  
1364== Heading"#).trim(), @r"
1365        #let x = 1  
1366         nnnnvvnnn  
1367        Text
1368            
1369        = Heading #let y = 2;  
1370                   nnnnvvnnn   
1371        == Heading
1372        ");
1373        assert_snapshot!(map_syntax(r#"#let f(x);"#).trim(), @r"
1374        #let f(x);
1375         nnnnv v
1376        ");
1377        assert_snapshot!(map_syntax(r#"#{
1378  calc.  
1379}"#).trim(), @r"
1380        #{
1381         n
1382          calc.  
1383        nnvvvvvnn
1384        }
1385        n
1386        ");
1387    }
1388
1389    #[test]
1390    fn test_get_context() {
1391        assert_snapshot!(map_context(r#"#let x = 1  
1392Text
1393= Heading #let y = 2;  
1394== Heading"#).trim(), @r"
1395        #let x = 1  
1396         nnnnvvnnn  
1397        Text
1398            
1399        = Heading #let y = 2;  
1400                   nnnnvvnnn   
1401        == Heading
1402        ");
1403        assert_snapshot!(map_context(r#"#let f(x);"#).trim(), @r"
1404        #let f(x);
1405         nnnnv v
1406        ");
1407        assert_snapshot!(map_context(r#"#f(1, 2)   Test"#).trim(), @r"
1408        #f(1, 2)   Test
1409         vpppppp
1410        ");
1411        assert_snapshot!(map_context(r#"#()   Test"#).trim(), @r"
1412        #()   Test
1413         ee
1414        ");
1415        assert_snapshot!(map_context(r#"#(1)   Test"#).trim(), @r"
1416        #(1)   Test
1417         PPP
1418        ");
1419        assert_snapshot!(map_context(r#"#(a: 1)   Test"#).trim(), @r"
1420        #(a: 1)   Test
1421         eeeeee
1422        ");
1423        assert_snapshot!(map_context(r#"#(1, 2)   Test"#).trim(), @r"
1424        #(1, 2)   Test
1425         eeeeee
1426        ");
1427        assert_snapshot!(map_context(r#"#(1, 2)  
1428  Test"#).trim(), @r"
1429        #(1, 2)  
1430         eeeeee  
1431          Test
1432        ");
1433    }
1434
1435    fn access_node(s: &str, cursor: i32) -> String {
1436        access_node_(s, cursor).unwrap_or_default()
1437    }
1438
1439    fn access_node_(s: &str, cursor: i32) -> Option<String> {
1440        access_var(s, cursor, |_source, var| {
1441            Some(var.accessed_node()?.get().clone().into_text().into())
1442        })
1443    }
1444
1445    fn access_field(s: &str, cursor: i32) -> String {
1446        access_field_(s, cursor).unwrap_or_default()
1447    }
1448
1449    fn access_field_(s: &str, cursor: i32) -> Option<String> {
1450        access_var(s, cursor, |source, var| {
1451            let field = var.accessing_field()?;
1452            Some(match field {
1453                FieldClass::Field(ident) => format!("Field: {}", ident.text()),
1454                FieldClass::DotSuffix(span_offset) => {
1455                    let offset = source.find(span_offset.span)?.offset() + span_offset.offset;
1456                    format!("DotSuffix: {offset:?}")
1457                }
1458            })
1459        })
1460    }
1461
1462    fn access_var(
1463        s: &str,
1464        cursor: i32,
1465        f: impl FnOnce(&Source, VarClass) -> Option<String>,
1466    ) -> Option<String> {
1467        let cursor = if cursor < 0 {
1468            s.len() as i32 + cursor
1469        } else {
1470            cursor
1471        };
1472        let source = Source::detached(s.to_owned());
1473        let root = LinkedNode::new(source.root());
1474        let node = root.leaf_at(cursor as usize, Side::Before)?;
1475        let syntax = classify_syntax(node, cursor as usize)?;
1476        let SyntaxClass::VarAccess(var) = syntax else {
1477            return None;
1478        };
1479        f(&source, var)
1480    }
1481
1482    #[test]
1483    fn test_access_field() {
1484        assert_snapshot!(access_field("#(a.b)", 5), @r"Field: b");
1485        assert_snapshot!(access_field("#a.", 3), @"DotSuffix: 3");
1486        assert_snapshot!(access_field("$a.$", 3), @"DotSuffix: 3");
1487        assert_snapshot!(access_field("#(a.)", 4), @"DotSuffix: 4");
1488        assert_snapshot!(access_node("#(a..b)", 4), @"a");
1489        assert_snapshot!(access_field("#(a..b)", 4), @"DotSuffix: 4");
1490        assert_snapshot!(access_node("#(a..b())", 4), @"a");
1491        assert_snapshot!(access_field("#(a..b())", 4), @"DotSuffix: 4");
1492    }
1493
1494    #[test]
1495    fn test_code_access() {
1496        assert_snapshot!(access_node("#{`a`.}", 6), @"`a`");
1497        assert_snapshot!(access_field("#{`a`.}", 6), @"DotSuffix: 6");
1498        assert_snapshot!(access_node("#{$a$.}", 6), @"$a$");
1499        assert_snapshot!(access_field("#{$a$.}", 6), @"DotSuffix: 6");
1500        assert_snapshot!(access_node("#{\"a\".}", 6), @"\"a\"");
1501        assert_snapshot!(access_field("#{\"a\".}", 6), @"DotSuffix: 6");
1502        assert_snapshot!(access_node("#{<a>.}", 6), @"<a>");
1503        assert_snapshot!(access_field("#{<a>.}", 6), @"DotSuffix: 6");
1504    }
1505
1506    #[test]
1507    fn test_markup_access() {
1508        assert_snapshot!(access_field("_a_.", 4), @"");
1509        assert_snapshot!(access_field("*a*.", 4), @"");
1510        assert_snapshot!(access_field("`a`.", 4), @"");
1511        assert_snapshot!(access_field("$a$.", 4), @"");
1512        assert_snapshot!(access_field("\"a\".", 4), @"");
1513        assert_snapshot!(access_field("@a.", 3), @"");
1514        assert_snapshot!(access_field("<a>.", 4), @"");
1515    }
1516
1517    #[test]
1518    fn test_hash_access() {
1519        assert_snapshot!(access_node("#a.", 3), @"a");
1520        assert_snapshot!(access_field("#a.", 3), @"DotSuffix: 3");
1521        assert_snapshot!(access_node("#(a).", 5), @"(a)");
1522        assert_snapshot!(access_field("#(a).", 5), @"DotSuffix: 5");
1523        assert_snapshot!(access_node("#`a`.", 5), @"`a`");
1524        assert_snapshot!(access_field("#`a`.", 5), @"DotSuffix: 5");
1525        assert_snapshot!(access_node("#$a$.", 5), @"$a$");
1526        assert_snapshot!(access_field("#$a$.", 5), @"DotSuffix: 5");
1527        assert_snapshot!(access_node("#(a,).", 6), @"(a,)");
1528        assert_snapshot!(access_field("#(a,).", 6), @"DotSuffix: 6");
1529    }
1530}