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