1use serde::{Deserialize, Serialize};
60use tinymist_world::debug_loc::SourceSpanOffset;
61use typst::syntax::Span;
62
63use crate::prelude::*;
64
65pub 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
72pub fn first_ancestor_expr(node: LinkedNode) -> Option<LinkedNode> {
74 node_ancestors(&node)
75 .find(|n| n.is::<ast::Expr>())
76 .map(|mut node| {
77 while matches!(node.kind(), SyntaxKind::Ident | SyntaxKind::MathIdent) {
78 let Some(parent) = node.parent() else {
79 return node;
80 };
81
82 let Some(field_access) = parent.cast::<ast::FieldAccess>() else {
83 return node;
84 };
85
86 let dot = parent
87 .children()
88 .find(|n| matches!(n.kind(), SyntaxKind::Dot));
89
90 if dot.is_some_and(|dot| {
94 dot.offset() <= node.offset() && field_access.field().span() == node.span()
95 }) {
96 node = parent;
97 } else {
98 return node;
99 }
100 }
101
102 node
103 })
104 .cloned()
105}
106
107pub enum PreviousItem<'a> {
110 Parent(&'a LinkedNode<'a>, &'a LinkedNode<'a>),
112 Sibling(&'a LinkedNode<'a>),
114}
115
116impl<'a> PreviousItem<'a> {
117 pub fn node(&self) -> &'a LinkedNode<'a> {
119 match self {
120 PreviousItem::Sibling(node) => node,
121 PreviousItem::Parent(node, _) => node,
122 }
123 }
124}
125
126pub fn previous_items<T>(
129 node: LinkedNode,
130 mut recv: impl FnMut(PreviousItem) -> Option<T>,
131) -> Option<T> {
132 let mut ancestor = Some(node);
133 while let Some(node) = &ancestor {
134 let mut sibling = Some(node.clone());
135 while let Some(node) = &sibling {
136 if let Some(v) = recv(PreviousItem::Sibling(node)) {
137 return Some(v);
138 }
139
140 sibling = node.prev_sibling();
141 }
142
143 if let Some(parent) = node.parent() {
144 if let Some(v) = recv(PreviousItem::Parent(parent, node)) {
145 return Some(v);
146 }
147
148 ancestor = Some(parent.clone());
149 continue;
150 }
151
152 break;
153 }
154
155 None
156}
157
158pub enum PreviousDecl<'a> {
161 Ident(ast::Ident<'a>),
171 ImportSource(ast::Expr<'a>),
181 ImportAll(ast::ModuleImport<'a>),
191}
192
193pub fn previous_decls<T>(
196 node: LinkedNode,
197 mut recv: impl FnMut(PreviousDecl) -> Option<T>,
198) -> Option<T> {
199 previous_items(node, |item| {
200 match (&item, item.node().cast::<ast::Expr>()?) {
201 (PreviousItem::Sibling(..), ast::Expr::LetBinding(lb)) => {
202 for ident in lb.kind().bindings() {
203 if let Some(t) = recv(PreviousDecl::Ident(ident)) {
204 return Some(t);
205 }
206 }
207 }
208 (PreviousItem::Sibling(..), ast::Expr::ModuleImport(import)) => {
209 match import.imports() {
211 Some(ast::Imports::Wildcard) => {
212 if let Some(t) = recv(PreviousDecl::ImportAll(import)) {
213 return Some(t);
214 }
215 }
216 Some(ast::Imports::Items(items)) => {
217 for item in items.iter() {
218 if let Some(t) = recv(PreviousDecl::Ident(item.bound_name())) {
219 return Some(t);
220 }
221 }
222 }
223 _ => {}
224 }
225
226 if let Some(new_name) = import.new_name() {
228 if let Some(t) = recv(PreviousDecl::Ident(new_name)) {
229 return Some(t);
230 }
231 } else if import.imports().is_none()
232 && let Some(t) = recv(PreviousDecl::ImportSource(import.source()))
233 {
234 return Some(t);
235 }
236 }
237 (PreviousItem::Parent(parent, child), ast::Expr::ForLoop(for_expr)) => {
238 let body = parent.find(for_expr.body().span());
239 let in_body = body.is_some_and(|n| n.find(child.span()).is_some());
240 if !in_body {
241 return None;
242 }
243
244 for ident in for_expr.pattern().bindings() {
245 if let Some(t) = recv(PreviousDecl::Ident(ident)) {
246 return Some(t);
247 }
248 }
249 }
250 (PreviousItem::Parent(parent, child), ast::Expr::Closure(closure)) => {
251 let body = parent.find(closure.body().span());
252 let in_body = body.is_some_and(|n| n.find(child.span()).is_some());
253 if !in_body {
254 return None;
255 }
256
257 for param in closure.params().children() {
258 match param {
259 ast::Param::Pos(pos) => {
260 for ident in pos.bindings() {
261 if let Some(t) = recv(PreviousDecl::Ident(ident)) {
262 return Some(t);
263 }
264 }
265 }
266 ast::Param::Named(named) => {
267 if let Some(t) = recv(PreviousDecl::Ident(named.name())) {
268 return Some(t);
269 }
270 }
271 ast::Param::Spread(spread) => {
272 if let Some(sink_ident) = spread.sink_ident()
273 && let Some(t) = recv(PreviousDecl::Ident(sink_ident))
274 {
275 return Some(t);
276 }
277 }
278 }
279 }
280 }
281 _ => {}
282 };
283 None
284 })
285}
286
287pub fn is_mark(sk: SyntaxKind) -> bool {
289 use SyntaxKind::*;
290 #[allow(clippy::match_like_matches_macro)]
291 match sk {
292 MathAlignPoint | Plus | Minus | Dot | Dots | Arrow | Not | And | Or => true,
293 Eq | EqEq | ExclEq | Lt | LtEq | Gt | GtEq | PlusEq | HyphEq | StarEq | SlashEq => true,
294 LeftBrace | RightBrace | LeftBracket | RightBracket | LeftParen | RightParen => true,
295 Slash | Hat | Comma | Semicolon | Colon | Hash => true,
296 _ => false,
297 }
298}
299
300pub fn is_ident_like(node: &SyntaxNode) -> bool {
302 fn can_be_ident(node: &SyntaxNode) -> bool {
303 typst::syntax::is_ident(node.text())
304 }
305
306 use SyntaxKind::*;
307 let kind = node.kind();
308 matches!(kind, Ident | MathIdent | Underscore)
309 || (matches!(kind, Error) && can_be_ident(node))
310 || kind.is_keyword()
311}
312
313#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash, strum::EnumIter)]
315#[serde(rename_all = "camelCase")]
316pub enum InterpretMode {
317 Comment,
319 String,
321 Raw,
323 Markup,
325 Code,
327 Math,
329}
330
331pub fn interpret_mode_at(mut leaf: Option<&LinkedNode>) -> InterpretMode {
334 loop {
335 crate::log_debug_ct!("leaf for mode: {leaf:?}");
336 if let Some(t) = leaf {
337 if let Some(mode) = interpret_mode_at_kind(t.kind()) {
338 break mode;
339 }
340
341 if !t.kind().is_trivia() && {
342 t.prev_leaf().is_some_and(|n| n.kind() == SyntaxKind::Hash)
344 } {
345 return InterpretMode::Code;
346 }
347
348 leaf = t.parent();
349 } else {
350 break InterpretMode::Markup;
351 }
352 }
353}
354
355pub(crate) fn interpret_mode_at_kind(kind: SyntaxKind) -> Option<InterpretMode> {
357 use SyntaxKind::*;
358 Some(match kind {
359 LineComment | BlockComment | Shebang => InterpretMode::Comment,
360 Raw => InterpretMode::Raw,
361 Str => InterpretMode::String,
362 CodeBlock | Code => InterpretMode::Code,
363 ContentBlock | Markup => InterpretMode::Markup,
364 Equation | Math => InterpretMode::Math,
365 Hash => InterpretMode::Code,
366 Label | Text | Ident | Args | FuncCall | FieldAccess | Bool | Int | Float | Numeric
367 | Space | Linebreak | Parbreak | Escape | Shorthand | SmartQuote | RawLang | RawDelim
368 | RawTrimmed | LeftBrace | RightBrace | LeftBracket | RightBracket | LeftParen
369 | RightParen | Comma | Semicolon | Colon | Star | Underscore | Dollar | Plus | Minus
370 | Slash | Hat | Prime | Dot | Eq | EqEq | ExclEq | Lt | LtEq | Gt | GtEq | PlusEq
371 | HyphEq | StarEq | SlashEq | Dots | Arrow | Root | Not | And | Or | None | Auto | As
372 | Named | Keyed | Spread | Error | End => return Option::None,
373 Strong | Emph | Link | Ref | RefMarker | Heading | HeadingMarker | ListItem
374 | ListMarker | EnumItem | EnumMarker | TermItem | TermMarker => InterpretMode::Markup,
375 MathIdent | MathAlignPoint | MathDelimited | MathAttach | MathPrimes | MathFrac
376 | MathRoot | MathShorthand | MathText => InterpretMode::Math,
377 Let | Set | Show | Context | If | Else | For | In | While | Break | Continue | Return
378 | Import | Include | Closure | Params | LetBinding | SetRule | ShowRule | Contextual
379 | Conditional | WhileLoop | ForLoop | LoopBreak | ModuleImport | ImportItems
380 | ImportItemPath | RenamedImportItem | ModuleInclude | LoopContinue | FuncReturn
381 | Unary | Binary | Parenthesized | Dict | Array | Destructuring | DestructAssignment => {
382 InterpretMode::Code
383 }
384 })
385}
386
387#[derive(Debug, Clone)]
389pub enum DefClass<'a> {
390 Let(LinkedNode<'a>),
392 Import(LinkedNode<'a>),
394}
395
396impl DefClass<'_> {
397 pub fn node(&self) -> &LinkedNode<'_> {
399 match self {
400 DefClass::Let(node) => node,
401 DefClass::Import(node) => node,
402 }
403 }
404
405 pub fn name(&self) -> Option<LinkedNode<'_>> {
407 match self {
408 DefClass::Let(node) => {
409 let lb: ast::LetBinding<'_> = node.cast()?;
410 let names = match lb.kind() {
411 ast::LetBindingKind::Closure(name) => node.find(name.span())?,
412 ast::LetBindingKind::Normal(ast::Pattern::Normal(name)) => {
413 node.find(name.span())?
414 }
415 _ => return None,
416 };
417
418 Some(names)
419 }
420 DefClass::Import(_node) => {
421 None
425 }
426 }
427 }
428
429 pub fn name_range(&self) -> Option<Range<usize>> {
431 self.name().map(|node| node.range())
432 }
433}
434
435pub fn classify_def_loosely(node: LinkedNode<'_>) -> Option<DefClass<'_>> {
438 classify_def_(node, false)
439}
440
441pub fn classify_def(node: LinkedNode<'_>) -> Option<DefClass<'_>> {
443 classify_def_(node, true)
444}
445
446fn classify_def_(node: LinkedNode<'_>, strict: bool) -> Option<DefClass<'_>> {
448 let mut ancestor = node;
449 if ancestor.kind().is_trivia() || is_mark(ancestor.kind()) {
450 ancestor = ancestor.prev_sibling()?;
451 }
452
453 while !ancestor.is::<ast::Expr>() {
454 ancestor = ancestor.parent()?.clone();
455 }
456 crate::log_debug_ct!("ancestor: {ancestor:?}");
457 let adjusted = adjust_expr(ancestor)?;
458 crate::log_debug_ct!("adjust_expr: {adjusted:?}");
459
460 let may_ident = adjusted.cast::<ast::Expr>()?;
461 if strict && !may_ident.hash() && !matches!(may_ident, ast::Expr::MathIdent(_)) {
462 return None;
463 }
464
465 let expr = may_ident;
466 Some(match expr {
467 ast::Expr::FuncCall(..) => return None,
470 ast::Expr::SetRule(..) => return None,
471 ast::Expr::LetBinding(..) => DefClass::Let(adjusted),
472 ast::Expr::ModuleImport(..) => DefClass::Import(adjusted),
473 ast::Expr::Ident(..)
475 | ast::Expr::MathIdent(..)
476 | ast::Expr::FieldAccess(..)
477 | ast::Expr::Closure(..) => {
478 let mut ancestor = adjusted;
479 while !ancestor.is::<ast::LetBinding>() {
480 ancestor = ancestor.parent()?.clone();
481 }
482
483 DefClass::Let(ancestor)
484 }
485 ast::Expr::Str(..) => {
486 let parent = adjusted.parent()?;
487 if parent.kind() != SyntaxKind::ModuleImport {
488 return None;
489 }
490
491 DefClass::Import(parent.clone())
492 }
493 _ if expr.hash() => return None,
494 _ => {
495 crate::log_debug_ct!("unsupported kind {:?}", adjusted.kind());
496 return None;
497 }
498 })
499}
500
501pub fn adjust_expr(mut node: LinkedNode) -> Option<LinkedNode> {
506 while let Some(paren_expr) = node.cast::<ast::Parenthesized>() {
507 node = node.find(paren_expr.expr().span())?;
508 }
509 if let Some(parent) = node.parent()
510 && let Some(field_access) = parent.cast::<ast::FieldAccess>()
511 && node.span() == field_access.field().span()
512 {
513 return Some(parent.clone());
514 }
515 Some(node)
516}
517
518#[derive(Debug, Clone)]
520pub enum FieldClass<'a> {
521 Field(LinkedNode<'a>),
531
532 DotSuffix(SourceSpanOffset),
542}
543
544impl FieldClass<'_> {
545 pub fn offset(&self, source: &Source) -> Option<usize> {
547 Some(match self {
548 Self::Field(node) => node.offset(),
549 Self::DotSuffix(span_offset) => {
550 source.find(span_offset.span)?.offset() + span_offset.offset
551 }
552 })
553 }
554}
555
556#[derive(Debug, Clone)]
559pub enum VarClass<'a> {
560 Ident(LinkedNode<'a>),
562 FieldAccess(LinkedNode<'a>),
564 DotAccess(LinkedNode<'a>),
568}
569
570impl<'a> VarClass<'a> {
571 pub fn node(&self) -> &LinkedNode<'a> {
573 match self {
574 Self::Ident(node) | Self::FieldAccess(node) | Self::DotAccess(node) => node,
575 }
576 }
577
578 pub fn accessed_node(&self) -> Option<LinkedNode<'a>> {
580 Some(match self {
581 Self::Ident(node) => node.clone(),
582 Self::FieldAccess(node) => {
583 let field_access = node.cast::<ast::FieldAccess>()?;
584 node.find(field_access.target().span())?
585 }
586 Self::DotAccess(node) => node.clone(),
587 })
588 }
589
590 pub fn accessing_field(&self) -> Option<FieldClass<'a>> {
592 match self {
593 Self::FieldAccess(node) => {
594 let dot = node
595 .children()
596 .find(|n| matches!(n.kind(), SyntaxKind::Dot))?;
597 let mut iter_after_dot =
598 node.children().skip_while(|n| n.kind() != SyntaxKind::Dot);
599 let ident = iter_after_dot.find(|n| {
600 matches!(
601 n.kind(),
602 SyntaxKind::Ident | SyntaxKind::MathIdent | SyntaxKind::Error
603 )
604 });
605
606 let ident_case = ident.map(|ident| {
607 if ident.text().is_empty() {
608 FieldClass::DotSuffix(SourceSpanOffset {
609 span: ident.span(),
610 offset: 0,
611 })
612 } else {
613 FieldClass::Field(ident)
614 }
615 });
616
617 ident_case.or_else(|| {
618 Some(FieldClass::DotSuffix(SourceSpanOffset {
619 span: dot.span(),
620 offset: 1,
621 }))
622 })
623 }
624 Self::DotAccess(node) => Some(FieldClass::DotSuffix(SourceSpanOffset {
625 span: node.span(),
626 offset: node.range().len() + 1,
627 })),
628 Self::Ident(..) => None,
629 }
630 }
631}
632
633#[derive(Debug, Clone)]
635pub enum SyntaxClass<'a> {
636 VarAccess(VarClass<'a>),
640 Label {
642 node: LinkedNode<'a>,
644 is_error: bool,
646 },
647 Ref {
649 node: LinkedNode<'a>,
651 suffix_colon: bool,
654 },
655 At {
657 node: LinkedNode<'a>,
659 },
660 Callee(LinkedNode<'a>),
662 ImportPath(LinkedNode<'a>),
664 IncludePath(LinkedNode<'a>),
666 Normal(SyntaxKind, LinkedNode<'a>),
668}
669
670impl<'a> SyntaxClass<'a> {
671 pub fn label(node: LinkedNode<'a>) -> Self {
673 Self::Label {
674 node,
675 is_error: false,
676 }
677 }
678
679 pub fn error_as_label(node: LinkedNode<'a>) -> Self {
681 Self::Label {
682 node,
683 is_error: true,
684 }
685 }
686
687 pub fn node(&self) -> &LinkedNode<'a> {
689 match self {
690 SyntaxClass::VarAccess(cls) => cls.node(),
691 SyntaxClass::Label { node, .. }
692 | SyntaxClass::Ref { node, .. }
693 | SyntaxClass::At { node, .. }
694 | SyntaxClass::Callee(node)
695 | SyntaxClass::ImportPath(node)
696 | SyntaxClass::IncludePath(node)
697 | SyntaxClass::Normal(_, node) => node,
698 }
699 }
700
701 pub fn complete_offset(&self) -> Option<usize> {
703 match self {
704 SyntaxClass::Label { node, .. } => Some(node.offset() + 1),
707 _ => None,
708 }
709 }
710}
711
712pub fn classify_syntax(node: LinkedNode<'_>, cursor: usize) -> Option<SyntaxClass<'_>> {
715 if matches!(node.kind(), SyntaxKind::Error) && node.text().starts_with('<') {
716 return Some(SyntaxClass::error_as_label(node));
717 }
718
719 fn can_skip_trivia(node: &LinkedNode, cursor: usize) -> bool {
721 if !node.kind().is_trivia() || !node.parent_kind().is_some_and(possible_in_code_trivia) {
723 return false;
724 }
725
726 let previous_text = node.text().as_bytes();
728 let previous_text = if node.range().contains(&cursor) {
729 &previous_text[..cursor - node.offset()]
730 } else {
731 previous_text
732 };
733
734 !previous_text.contains(&b'\n')
739 }
740
741 let mut node = node;
743 if can_skip_trivia(&node, cursor) {
744 node = node.prev_sibling()?;
745 }
746
747 fn classify_dot_access<'a>(node: &LinkedNode<'a>) -> Option<SyntaxClass<'a>> {
753 let prev_leaf = node.prev_leaf();
754 let mode = interpret_mode_at(Some(node));
755
756 if matches!(mode, InterpretMode::Markup | InterpretMode::Math)
758 && prev_leaf
759 .as_ref()
760 .is_some_and(|leaf| leaf.range().end < node.offset())
761 {
762 return None;
763 }
764
765 if matches!(mode, InterpretMode::Math)
766 && prev_leaf.as_ref().is_some_and(|leaf| {
767 node_ancestors(leaf)
769 .find(|t| matches!(t.kind(), SyntaxKind::Equation))
770 .is_some_and(|parent| parent.offset() == leaf.offset())
771 })
772 {
773 return None;
774 }
775
776 let dot_target = prev_leaf.and_then(first_ancestor_expr)?;
777
778 if matches!(mode, InterpretMode::Math | InterpretMode::Code) || {
779 matches!(mode, InterpretMode::Markup)
780 && (matches!(
781 dot_target.kind(),
782 SyntaxKind::Ident
783 | SyntaxKind::MathIdent
784 | SyntaxKind::FieldAccess
785 | SyntaxKind::FuncCall
786 ) || (matches!(
787 dot_target.prev_leaf().as_deref().map(SyntaxNode::kind),
788 Some(SyntaxKind::Hash)
789 )))
790 } {
791 return Some(SyntaxClass::VarAccess(VarClass::DotAccess(dot_target)));
792 }
793
794 None
795 }
796
797 if node.offset() + 1 == cursor
798 && {
799 matches!(node.kind(), SyntaxKind::Dot)
801 || (matches!(
802 node.kind(),
803 SyntaxKind::Text | SyntaxKind::MathText | SyntaxKind::Error
804 ) && node.text().starts_with("."))
805 }
806 && let Some(dot_access) = classify_dot_access(&node)
807 {
808 return Some(dot_access);
809 }
810
811 if node.offset() + 1 == cursor
812 && matches!(node.kind(), SyntaxKind::Dots)
813 && matches!(node.parent_kind(), Some(SyntaxKind::Spread))
814 && let Some(dot_access) = classify_dot_access(&node)
815 {
816 return Some(dot_access);
817 }
818
819 fn classify_ref<'a>(node: &LinkedNode<'a>) -> Option<SyntaxClass<'a>> {
824 let prev_leaf = node.prev_leaf()?;
825
826 if matches!(prev_leaf.kind(), SyntaxKind::RefMarker)
827 && prev_leaf.range().end == node.offset()
828 {
829 return Some(SyntaxClass::Ref {
830 node: prev_leaf,
831 suffix_colon: true,
832 });
833 }
834
835 None
836 }
837
838 if node.offset() + 1 == cursor
839 && {
840 matches!(node.kind(), SyntaxKind::Colon)
842 || (matches!(
843 node.kind(),
844 SyntaxKind::Text | SyntaxKind::MathText | SyntaxKind::Error
845 ) && node.text().starts_with(":"))
846 }
847 && let Some(ref_syntax) = classify_ref(&node)
848 {
849 return Some(ref_syntax);
850 }
851
852 if node.kind() == SyntaxKind::Text
853 && node.offset() + 1 == cursor
854 && node.text().starts_with('@')
855 {
856 return Some(SyntaxClass::At { node });
857 }
858
859 if matches!(node.kind(), SyntaxKind::Text | SyntaxKind::MathText) {
861 let mode = interpret_mode_at(Some(&node));
862 if matches!(mode, InterpretMode::Math) && is_ident_like(&node) {
863 return Some(SyntaxClass::VarAccess(VarClass::Ident(node)));
864 }
865 }
866
867 let ancestor = first_ancestor_expr(node)?;
869 crate::log_debug_ct!("first_ancestor_expr: {ancestor:?}");
870
871 let adjusted = adjust_expr(ancestor)?;
873 crate::log_debug_ct!("adjust_expr: {adjusted:?}");
874
875 let expr = adjusted.cast::<ast::Expr>()?;
877 Some(match expr {
878 ast::Expr::Label(..) => SyntaxClass::label(adjusted),
879 ast::Expr::Ref(..) => SyntaxClass::Ref {
880 node: adjusted,
881 suffix_colon: false,
882 },
883 ast::Expr::FuncCall(call) => SyntaxClass::Callee(adjusted.find(call.callee().span())?),
884 ast::Expr::SetRule(set) => SyntaxClass::Callee(adjusted.find(set.target().span())?),
885 ast::Expr::Ident(..) | ast::Expr::MathIdent(..) => {
886 SyntaxClass::VarAccess(VarClass::Ident(adjusted))
887 }
888 ast::Expr::FieldAccess(..) => SyntaxClass::VarAccess(VarClass::FieldAccess(adjusted)),
889 ast::Expr::Str(..) => {
890 let parent = adjusted.parent()?;
891 if parent.kind() == SyntaxKind::ModuleImport {
892 SyntaxClass::ImportPath(adjusted)
893 } else if parent.kind() == SyntaxKind::ModuleInclude {
894 SyntaxClass::IncludePath(adjusted)
895 } else {
896 SyntaxClass::Normal(adjusted.kind(), adjusted)
897 }
898 }
899 _ if expr.hash()
900 || matches!(adjusted.kind(), SyntaxKind::MathIdent | SyntaxKind::Error) =>
901 {
902 SyntaxClass::Normal(adjusted.kind(), adjusted)
903 }
904 _ => return None,
905 })
906}
907
908fn possible_in_code_trivia(kind: SyntaxKind) -> bool {
911 !matches!(
912 interpret_mode_at_kind(kind),
913 Some(InterpretMode::Markup | InterpretMode::Math | InterpretMode::Comment)
914 )
915}
916
917#[derive(Debug, Clone)]
919pub enum ArgClass<'a> {
920 Positional {
922 spreads: EcoVec<LinkedNode<'a>>,
924 positional: usize,
926 is_spread: bool,
928 },
929 Named(LinkedNode<'a>),
931}
932
933impl ArgClass<'_> {
934 pub fn first_positional() -> Self {
936 ArgClass::Positional {
937 spreads: EcoVec::new(),
938 positional: 0,
939 is_spread: false,
940 }
941 }
942}
943
944#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash, strum::EnumIter)]
947pub enum SurroundingSyntax {
948 Regular,
950 StringContent,
952 Selector,
954 ShowTransform,
956 ImportList,
958 SetRule,
960 ParamList,
962}
963
964pub fn surrounding_syntax(node: &LinkedNode) -> SurroundingSyntax {
966 check_previous_syntax(node)
967 .or_else(|| check_surrounding_syntax(node))
968 .unwrap_or(SurroundingSyntax::Regular)
969}
970
971fn check_surrounding_syntax(mut leaf: &LinkedNode) -> Option<SurroundingSyntax> {
972 use SurroundingSyntax::*;
973 let mut met_args = false;
974
975 if matches!(leaf.kind(), SyntaxKind::Str) {
976 return Some(StringContent);
977 }
978
979 while let Some(parent) = leaf.parent() {
980 crate::log_debug_ct!(
981 "check_surrounding_syntax: {:?}::{:?}",
982 parent.kind(),
983 leaf.kind()
984 );
985 match parent.kind() {
986 SyntaxKind::CodeBlock
987 | SyntaxKind::ContentBlock
988 | SyntaxKind::Equation
989 | SyntaxKind::Closure => {
990 return Some(Regular);
991 }
992 SyntaxKind::ImportItemPath
993 | SyntaxKind::ImportItems
994 | SyntaxKind::RenamedImportItem => {
995 return Some(ImportList);
996 }
997 SyntaxKind::ModuleImport => {
998 let colon = parent.children().find(|s| s.kind() == SyntaxKind::Colon);
999 let Some(colon) = colon else {
1000 return Some(Regular);
1001 };
1002
1003 if leaf.offset() >= colon.offset() {
1004 return Some(ImportList);
1005 } else {
1006 return Some(Regular);
1007 }
1008 }
1009 SyntaxKind::Named => {
1010 let colon = parent.children().find(|s| s.kind() == SyntaxKind::Colon);
1011 let Some(colon) = colon else {
1012 return Some(Regular);
1013 };
1014
1015 return if leaf.offset() >= colon.offset() {
1016 Some(Regular)
1017 } else if node_ancestors(leaf).any(|n| n.kind() == SyntaxKind::Params) {
1018 Some(ParamList)
1019 } else {
1020 Some(Regular)
1021 };
1022 }
1023 SyntaxKind::Params => {
1024 return Some(ParamList);
1025 }
1026 SyntaxKind::Args => {
1027 met_args = true;
1028 }
1029 SyntaxKind::SetRule => {
1030 let rule = parent.get().cast::<ast::SetRule>()?;
1031 if met_args || enclosed_by(parent, rule.condition().map(|s| s.span()), leaf) {
1032 return Some(Regular);
1033 } else {
1034 return Some(SetRule);
1035 }
1036 }
1037 SyntaxKind::ShowRule => {
1038 if met_args {
1039 return Some(Regular);
1040 }
1041
1042 let rule = parent.get().cast::<ast::ShowRule>()?;
1043 let colon = rule
1044 .to_untyped()
1045 .children()
1046 .find(|s| s.kind() == SyntaxKind::Colon);
1047 let Some(colon) = colon.and_then(|colon| parent.find(colon.span())) else {
1048 return Some(Selector);
1050 };
1051
1052 if leaf.offset() >= colon.offset() {
1053 return Some(ShowTransform);
1054 } else {
1055 return Some(Selector); }
1057 }
1058 _ => {}
1059 }
1060
1061 leaf = parent;
1062 }
1063
1064 None
1065}
1066
1067fn check_previous_syntax(leaf: &LinkedNode) -> Option<SurroundingSyntax> {
1069 let mut leaf = leaf.clone();
1070 if leaf.kind().is_trivia() {
1071 leaf = leaf.prev_sibling()?;
1072 }
1073 if matches!(
1074 leaf.kind(),
1075 SyntaxKind::ShowRule
1076 | SyntaxKind::SetRule
1077 | SyntaxKind::ModuleImport
1078 | SyntaxKind::ModuleInclude
1079 ) {
1080 return check_surrounding_syntax(&leaf.rightmost_leaf()?);
1081 }
1082
1083 if matches!(leaf.kind(), SyntaxKind::Show) {
1084 return Some(SurroundingSyntax::Selector);
1085 }
1086 if matches!(leaf.kind(), SyntaxKind::Set) {
1087 return Some(SurroundingSyntax::SetRule);
1088 }
1089
1090 None
1091}
1092
1093fn enclosed_by(parent: &LinkedNode, s: Option<Span>, leaf: &LinkedNode) -> bool {
1095 s.and_then(|s| parent.find(s)?.find(leaf.span())).is_some()
1096}
1097
1098#[derive(Debug, Clone)]
1106pub enum SyntaxContext<'a> {
1107 Arg {
1109 callee: LinkedNode<'a>,
1111 args: LinkedNode<'a>,
1113 target: ArgClass<'a>,
1115 is_set: bool,
1117 },
1118 Element {
1120 container: LinkedNode<'a>,
1122 target: ArgClass<'a>,
1124 },
1125 Paren {
1127 container: LinkedNode<'a>,
1129 is_before: bool,
1132 },
1133 VarAccess(VarClass<'a>),
1137 ImportPath(LinkedNode<'a>),
1139 IncludePath(LinkedNode<'a>),
1141 Label {
1143 node: LinkedNode<'a>,
1145 is_error: bool,
1147 },
1148 Ref {
1150 node: LinkedNode<'a>,
1152 suffix_colon: bool,
1155 },
1156 At {
1158 node: LinkedNode<'a>,
1160 },
1161 Normal(LinkedNode<'a>),
1163}
1164
1165impl<'a> SyntaxContext<'a> {
1166 pub fn node(&self) -> Option<LinkedNode<'a>> {
1168 Some(match self {
1169 SyntaxContext::Arg { target, .. } | SyntaxContext::Element { target, .. } => {
1170 match target {
1171 ArgClass::Positional { .. } => return None,
1172 ArgClass::Named(node) => node.clone(),
1173 }
1174 }
1175 SyntaxContext::VarAccess(cls) => cls.node().clone(),
1176 SyntaxContext::Paren { container, .. } => container.clone(),
1177 SyntaxContext::Label { node, .. }
1178 | SyntaxContext::Ref { node, .. }
1179 | SyntaxContext::At { node, .. }
1180 | SyntaxContext::ImportPath(node)
1181 | SyntaxContext::IncludePath(node)
1182 | SyntaxContext::Normal(node) => node.clone(),
1183 })
1184 }
1185
1186 pub fn arg_container(&self) -> Option<&LinkedNode<'a>> {
1188 match self {
1189 Self::Arg { args, .. }
1190 | Self::Element {
1191 container: args, ..
1192 } => Some(args),
1193 Self::Paren { container, .. } => Some(container),
1194 _ => None,
1195 }
1196 }
1197}
1198
1199#[derive(Debug)]
1201enum ArgSourceKind {
1202 Call,
1204 Array,
1206 Dict,
1208}
1209
1210pub fn classify_context_outer<'a>(
1213 outer: LinkedNode<'a>,
1214 node: LinkedNode<'a>,
1215) -> Option<SyntaxContext<'a>> {
1216 use SyntaxClass::*;
1217 let context_syntax = classify_syntax(outer.clone(), node.offset())?;
1218 let node_syntax = classify_syntax(node.clone(), node.offset())?;
1219
1220 match context_syntax {
1221 Callee(callee)
1222 if matches!(node_syntax, Normal(..) | Label { .. } | Ref { .. })
1223 && !matches!(node_syntax, Callee(..)) =>
1224 {
1225 let parent = callee.parent()?;
1226 let args = match parent.cast::<ast::Expr>() {
1227 Some(ast::Expr::FuncCall(call)) => call.args(),
1228 Some(ast::Expr::SetRule(set)) => set.args(),
1229 _ => return None,
1230 };
1231 let args = parent.find(args.span())?;
1232
1233 let is_set = parent.kind() == SyntaxKind::SetRule;
1234 let arg_target = arg_context(args.clone(), node, ArgSourceKind::Call)?;
1235 Some(SyntaxContext::Arg {
1236 callee,
1237 args,
1238 target: arg_target,
1239 is_set,
1240 })
1241 }
1242 _ => None,
1243 }
1244}
1245
1246pub fn classify_context(node: LinkedNode<'_>, cursor: Option<usize>) -> Option<SyntaxContext<'_>> {
1249 let mut node = node;
1250 if node.kind().is_trivia() && node.parent_kind().is_some_and(possible_in_code_trivia) {
1251 loop {
1252 node = node.prev_sibling()?;
1253
1254 if !node.kind().is_trivia() {
1255 break;
1256 }
1257 }
1258 }
1259
1260 let cursor = cursor.unwrap_or_else(|| node.offset());
1261 let syntax = classify_syntax(node.clone(), cursor)?;
1262
1263 let normal_syntax = match syntax {
1264 SyntaxClass::Callee(callee) => {
1265 return callee_context(callee, node);
1266 }
1267 SyntaxClass::Label { node, is_error } => {
1268 return Some(SyntaxContext::Label { node, is_error });
1269 }
1270 SyntaxClass::Ref { node, suffix_colon } => {
1271 return Some(SyntaxContext::Ref { node, suffix_colon });
1272 }
1273 SyntaxClass::At { node } => {
1274 return Some(SyntaxContext::At { node });
1275 }
1276 SyntaxClass::ImportPath(node) => {
1277 return Some(SyntaxContext::ImportPath(node));
1278 }
1279 SyntaxClass::IncludePath(node) => {
1280 return Some(SyntaxContext::IncludePath(node));
1281 }
1282 syntax => syntax,
1283 };
1284
1285 let Some(mut node_parent) = node.parent().cloned() else {
1286 return Some(SyntaxContext::Normal(node));
1287 };
1288
1289 while let SyntaxKind::Named | SyntaxKind::Colon = node_parent.kind() {
1290 let Some(parent) = node_parent.parent() else {
1291 return Some(SyntaxContext::Normal(node));
1292 };
1293 node_parent = parent.clone();
1294 }
1295
1296 match node_parent.kind() {
1297 SyntaxKind::Args => {
1298 let callee = node_ancestors(&node_parent).find_map(|ancestor| {
1299 let span = match ancestor.cast::<ast::Expr>()? {
1300 ast::Expr::FuncCall(call) => call.callee().span(),
1301 ast::Expr::SetRule(set) => set.target().span(),
1302 _ => return None,
1303 };
1304 ancestor.find(span)
1305 })?;
1306
1307 let param_node = match node.kind() {
1308 SyntaxKind::Ident
1309 if matches!(
1310 node.parent_kind().zip(node.next_sibling_kind()),
1311 Some((SyntaxKind::Named, SyntaxKind::Colon))
1312 ) =>
1313 {
1314 node
1315 }
1316 _ if matches!(node.parent_kind(), Some(SyntaxKind::Named)) => {
1317 node.parent().cloned()?
1318 }
1319 _ => node,
1320 };
1321
1322 callee_context(callee, param_node)
1323 }
1324 SyntaxKind::Array | SyntaxKind::Dict => {
1325 let element_target = arg_context(
1326 node_parent.clone(),
1327 node.clone(),
1328 match node_parent.kind() {
1329 SyntaxKind::Array => ArgSourceKind::Array,
1330 SyntaxKind::Dict => ArgSourceKind::Dict,
1331 _ => unreachable!(),
1332 },
1333 )?;
1334 Some(SyntaxContext::Element {
1335 container: node_parent.clone(),
1336 target: element_target,
1337 })
1338 }
1339 SyntaxKind::Parenthesized => {
1340 let is_before = node.offset() <= node_parent.offset() + 1;
1341 Some(SyntaxContext::Paren {
1342 container: node_parent.clone(),
1343 is_before,
1344 })
1345 }
1346 _ => Some(match normal_syntax {
1347 SyntaxClass::VarAccess(v) => SyntaxContext::VarAccess(v),
1348 normal_syntax => SyntaxContext::Normal(normal_syntax.node().clone()),
1349 }),
1350 }
1351}
1352
1353fn callee_context<'a>(callee: LinkedNode<'a>, node: LinkedNode<'a>) -> Option<SyntaxContext<'a>> {
1355 let parent = callee.parent()?;
1356 let args = match parent.cast::<ast::Expr>() {
1357 Some(ast::Expr::FuncCall(call)) => call.args(),
1358 Some(ast::Expr::SetRule(set)) => set.args(),
1359 _ => return None,
1360 };
1361 let args = parent.find(args.span())?;
1362
1363 let mut parent = &node;
1364 loop {
1365 use SyntaxKind::*;
1366 match parent.kind() {
1367 ContentBlock | CodeBlock | Str | Raw | LineComment | BlockComment => {
1368 return Option::None;
1369 }
1370 Args if parent.range() == args.range() => {
1371 break;
1372 }
1373 _ => {}
1374 }
1375
1376 parent = parent.parent()?;
1377 }
1378
1379 let is_set = parent.kind() == SyntaxKind::SetRule;
1380 let target = arg_context(args.clone(), node, ArgSourceKind::Call)?;
1381 Some(SyntaxContext::Arg {
1382 callee,
1383 args,
1384 target,
1385 is_set,
1386 })
1387}
1388
1389fn arg_context<'a>(
1391 args_node: LinkedNode<'a>,
1392 mut node: LinkedNode<'a>,
1393 param_kind: ArgSourceKind,
1394) -> Option<ArgClass<'a>> {
1395 if node.kind() == SyntaxKind::RightParen {
1396 node = node.prev_sibling()?;
1397 }
1398 match node.kind() {
1399 SyntaxKind::Named => {
1400 let param_ident = node.cast::<ast::Named>()?.name();
1401 Some(ArgClass::Named(args_node.find(param_ident.span())?))
1402 }
1403 SyntaxKind::Colon => {
1404 let prev = node.prev_leaf()?;
1405 let param_ident = prev.cast::<ast::Ident>()?;
1406 Some(ArgClass::Named(args_node.find(param_ident.span())?))
1407 }
1408 _ => {
1409 let parent = node.parent();
1410 if let Some(parent) = parent
1411 && parent.kind() == SyntaxKind::Named
1412 {
1413 let param_ident = parent.cast::<ast::Named>()?;
1414 let name = param_ident.name();
1415 let init = param_ident.expr();
1416 let init = parent.find(init.span())?;
1417 if init.range().contains(&node.offset()) {
1418 let name = args_node.find(name.span())?;
1419 return Some(ArgClass::Named(name));
1420 }
1421 }
1422
1423 let mut spreads = EcoVec::new();
1424 let mut positional = 0;
1425 let is_spread = node.kind() == SyntaxKind::Spread;
1426
1427 let args_before = args_node
1428 .children()
1429 .take_while(|arg| arg.range().end <= node.offset());
1430 match param_kind {
1431 ArgSourceKind::Call => {
1432 for ch in args_before {
1433 match ch.cast::<ast::Arg>() {
1434 Some(ast::Arg::Pos(..)) => {
1435 positional += 1;
1436 }
1437 Some(ast::Arg::Spread(..)) => {
1438 spreads.push(ch);
1439 }
1440 Some(ast::Arg::Named(..)) | None => {}
1441 }
1442 }
1443 }
1444 ArgSourceKind::Array => {
1445 for ch in args_before {
1446 match ch.cast::<ast::ArrayItem>() {
1447 Some(ast::ArrayItem::Pos(..)) => {
1448 positional += 1;
1449 }
1450 Some(ast::ArrayItem::Spread(..)) => {
1451 spreads.push(ch);
1452 }
1453 _ => {}
1454 }
1455 }
1456 }
1457 ArgSourceKind::Dict => {
1458 for ch in args_before {
1459 if let Some(ast::DictItem::Spread(..)) = ch.cast::<ast::DictItem>() {
1460 spreads.push(ch);
1461 }
1462 }
1463 }
1464 }
1465
1466 Some(ArgClass::Positional {
1467 spreads,
1468 positional,
1469 is_spread,
1470 })
1471 }
1472 }
1473}
1474
1475pub enum BadCompletionCursor {
1477 ArgListPos,
1479}
1480
1481pub fn bad_completion_cursor(
1483 syntax: Option<&SyntaxClass>,
1484 syntax_context: Option<&SyntaxContext>,
1485 leaf: &LinkedNode,
1486) -> Option<BadCompletionCursor> {
1487 if (matches!(syntax, Some(SyntaxClass::Callee(..))) && {
1489 syntax_context
1490 .and_then(SyntaxContext::arg_container)
1491 .is_some_and(|container| {
1492 container.rightmost_leaf().map(|s| s.offset()) == Some(leaf.offset())
1493 })
1494 }) || (matches!(
1496 syntax,
1497 Some(SyntaxClass::Normal(SyntaxKind::ContentBlock, _))
1498 ) && matches!(leaf.kind(), SyntaxKind::RightBracket))
1499 {
1500 return Some(BadCompletionCursor::ArgListPos);
1501 }
1502
1503 None
1504}
1505
1506#[cfg(test)]
1507mod tests {
1508 use super::*;
1509 use insta::assert_snapshot;
1510 use typst::syntax::{Side, Source, is_newline};
1511
1512 fn map_node(source: &str, mapper: impl Fn(&LinkedNode, usize) -> char) -> String {
1513 let source = Source::detached(source.to_owned());
1514 let root = LinkedNode::new(source.root());
1515 let mut output_mapping = String::new();
1516
1517 let mut cursor = 0;
1518 for ch in source.text().chars() {
1519 cursor += ch.len_utf8();
1520 if is_newline(ch) {
1521 output_mapping.push(ch);
1522 continue;
1523 }
1524
1525 output_mapping.push(mapper(&root, cursor));
1526 }
1527
1528 source
1529 .text()
1530 .lines()
1531 .zip(output_mapping.lines())
1532 .flat_map(|(a, b)| [a, "\n", b, "\n"])
1533 .collect::<String>()
1534 }
1535
1536 fn map_syntax(source: &str) -> String {
1537 map_node(source, |root, cursor| {
1538 let node = root.leaf_at(cursor, Side::Before);
1539 let kind = node.and_then(|node| classify_syntax(node, cursor));
1540 match kind {
1541 Some(SyntaxClass::VarAccess(..)) => 'v',
1542 Some(SyntaxClass::Normal(..)) => 'n',
1543 Some(SyntaxClass::Label { .. }) => 'l',
1544 Some(SyntaxClass::Ref { .. }) => 'r',
1545 Some(SyntaxClass::At { .. }) => 'r',
1546 Some(SyntaxClass::Callee(..)) => 'c',
1547 Some(SyntaxClass::ImportPath(..)) => 'i',
1548 Some(SyntaxClass::IncludePath(..)) => 'I',
1549 None => ' ',
1550 }
1551 })
1552 }
1553
1554 fn map_context(source: &str) -> String {
1555 map_node(source, |root, cursor| {
1556 let node = root.leaf_at(cursor, Side::Before);
1557 let kind = node.and_then(|node| classify_context(node, Some(cursor)));
1558 match kind {
1559 Some(SyntaxContext::Arg { .. }) => 'p',
1560 Some(SyntaxContext::Element { .. }) => 'e',
1561 Some(SyntaxContext::Paren { .. }) => 'P',
1562 Some(SyntaxContext::VarAccess { .. }) => 'v',
1563 Some(SyntaxContext::ImportPath(..)) => 'i',
1564 Some(SyntaxContext::IncludePath(..)) => 'I',
1565 Some(SyntaxContext::Label { .. }) => 'l',
1566 Some(SyntaxContext::Ref { .. }) => 'r',
1567 Some(SyntaxContext::At { .. }) => 'r',
1568 Some(SyntaxContext::Normal(..)) => 'n',
1569 None => ' ',
1570 }
1571 })
1572 }
1573
1574 #[test]
1575 fn test_get_syntax() {
1576 assert_snapshot!(map_syntax(r#"#let x = 1
1577Text
1578= Heading #let y = 2;
1579== Heading"#).trim(), @r"
1580 #let x = 1
1581 nnnnvvnnn
1582 Text
1583
1584 = Heading #let y = 2;
1585 nnnnvvnnn
1586 == Heading
1587 ");
1588 assert_snapshot!(map_syntax(r#"#let f(x);"#).trim(), @r"
1589 #let f(x);
1590 nnnnv v
1591 ");
1592 assert_snapshot!(map_syntax(r#"#{
1593 calc.
1594}"#).trim(), @r"
1595 #{
1596 n
1597 calc.
1598 nnvvvvvnn
1599 }
1600 n
1601 ");
1602 }
1603
1604 #[test]
1605 fn test_get_context() {
1606 assert_snapshot!(map_context(r#"#let x = 1
1607Text
1608= Heading #let y = 2;
1609== Heading"#).trim(), @r"
1610 #let x = 1
1611 nnnnvvnnn
1612 Text
1613
1614 = Heading #let y = 2;
1615 nnnnvvnnn
1616 == Heading
1617 ");
1618 assert_snapshot!(map_context(r#"#let f(x);"#).trim(), @r"
1619 #let f(x);
1620 nnnnv v
1621 ");
1622 assert_snapshot!(map_context(r#"#f(1, 2) Test"#).trim(), @r"
1623 #f(1, 2) Test
1624 vpppppp
1625 ");
1626 assert_snapshot!(map_context(r#"#() Test"#).trim(), @r"
1627 #() Test
1628 ee
1629 ");
1630 assert_snapshot!(map_context(r#"#(1) Test"#).trim(), @r"
1631 #(1) Test
1632 PPP
1633 ");
1634 assert_snapshot!(map_context(r#"#(a: 1) Test"#).trim(), @r"
1635 #(a: 1) Test
1636 eeeeee
1637 ");
1638 assert_snapshot!(map_context(r#"#(1, 2) Test"#).trim(), @r"
1639 #(1, 2) Test
1640 eeeeee
1641 ");
1642 assert_snapshot!(map_context(r#"#(1, 2)
1643 Test"#).trim(), @r"
1644 #(1, 2)
1645 eeeeee
1646 Test
1647 ");
1648 }
1649
1650 #[test]
1651 fn ref_syntax() {
1652 assert_snapshot!(map_syntax("@ab:"), @r###"
1653 @ab:
1654 rrrr
1655 "###);
1656 assert_snapshot!(map_syntax("@"), @r"
1657 @
1658 r
1659 ");
1660 assert_snapshot!(map_syntax("@;"), @r"
1661 @;
1662 r
1663 ");
1664 assert_snapshot!(map_syntax("@ t"), @r"
1665 @ t
1666 r
1667 ");
1668 assert_snapshot!(map_syntax("@ab"), @r###"
1669 @ab
1670 rrr
1671 "###);
1672 assert_snapshot!(map_syntax("@ab:"), @r###"
1673 @ab:
1674 rrrr
1675 "###);
1676 assert_snapshot!(map_syntax("@ab:ab"), @r###"
1677 @ab:ab
1678 rrrrrr
1679 "###);
1680 assert_snapshot!(map_syntax("@ab:ab:"), @r###"
1681 @ab:ab:
1682 rrrrrrr
1683 "###);
1684 assert_snapshot!(map_syntax("@ab:ab:ab"), @r###"
1685 @ab:ab:ab
1686 rrrrrrrrr
1687 "###);
1688 assert_snapshot!(map_syntax("@ab[]:"), @r###"
1689 @ab[]:
1690 rrrnn
1691 "###);
1692 assert_snapshot!(map_syntax("@ab[ab]:"), @r###"
1693 @ab[ab]:
1694 rrrn n
1695 "###);
1696 assert_snapshot!(map_syntax("@ab :ab: ab"), @r###"
1697 @ab :ab: ab
1698 rrr
1699 "###);
1700 assert_snapshot!(map_syntax("@ab :ab:ab"), @r###"
1701 @ab :ab:ab
1702 rrr
1703 "###);
1704 }
1705
1706 fn access_node(s: &str, cursor: i32) -> String {
1707 access_node_(s, cursor).unwrap_or_default()
1708 }
1709
1710 fn access_node_(s: &str, cursor: i32) -> Option<String> {
1711 access_var(s, cursor, |_source, var| {
1712 Some(var.accessed_node()?.get().clone().into_text().into())
1713 })
1714 }
1715
1716 fn access_field(s: &str, cursor: i32) -> String {
1717 access_field_(s, cursor).unwrap_or_default()
1718 }
1719
1720 fn access_field_(s: &str, cursor: i32) -> Option<String> {
1721 access_var(s, cursor, |source, var| {
1722 let field = var.accessing_field()?;
1723 Some(match field {
1724 FieldClass::Field(ident) => format!("Field: {}", ident.text()),
1725 FieldClass::DotSuffix(span_offset) => {
1726 let offset = source.find(span_offset.span)?.offset() + span_offset.offset;
1727 format!("DotSuffix: {offset:?}")
1728 }
1729 })
1730 })
1731 }
1732
1733 fn access_var(
1734 s: &str,
1735 cursor: i32,
1736 f: impl FnOnce(&Source, VarClass) -> Option<String>,
1737 ) -> Option<String> {
1738 let cursor = if cursor < 0 {
1739 s.len() as i32 + cursor
1740 } else {
1741 cursor
1742 };
1743 let source = Source::detached(s.to_owned());
1744 let root = LinkedNode::new(source.root());
1745 let node = root.leaf_at(cursor as usize, Side::Before)?;
1746 let syntax = classify_syntax(node, cursor as usize)?;
1747 let SyntaxClass::VarAccess(var) = syntax else {
1748 return None;
1749 };
1750 f(&source, var)
1751 }
1752
1753 #[test]
1754 fn test_access_field() {
1755 assert_snapshot!(access_field("#(a.b)", 5), @r"Field: b");
1756 assert_snapshot!(access_field("#a.", 3), @"DotSuffix: 3");
1757 assert_snapshot!(access_field("$a.$", 3), @"DotSuffix: 3");
1758 assert_snapshot!(access_field("#(a.)", 4), @"DotSuffix: 4");
1759 assert_snapshot!(access_node("#(a..b)", 4), @"a");
1760 assert_snapshot!(access_field("#(a..b)", 4), @"DotSuffix: 4");
1761 assert_snapshot!(access_node("#(a..b())", 4), @"a");
1762 assert_snapshot!(access_field("#(a..b())", 4), @"DotSuffix: 4");
1763 }
1764
1765 #[test]
1766 fn test_code_access() {
1767 assert_snapshot!(access_node("#{`a`.}", 6), @"`a`");
1768 assert_snapshot!(access_field("#{`a`.}", 6), @"DotSuffix: 6");
1769 assert_snapshot!(access_node("#{$a$.}", 6), @"$a$");
1770 assert_snapshot!(access_field("#{$a$.}", 6), @"DotSuffix: 6");
1771 assert_snapshot!(access_node("#{\"a\".}", 6), @"\"a\"");
1772 assert_snapshot!(access_field("#{\"a\".}", 6), @"DotSuffix: 6");
1773 assert_snapshot!(access_node("#{<a>.}", 6), @"<a>");
1774 assert_snapshot!(access_field("#{<a>.}", 6), @"DotSuffix: 6");
1775 }
1776
1777 #[test]
1778 fn test_markup_access() {
1779 assert_snapshot!(access_field("_a_.", 4), @"");
1780 assert_snapshot!(access_field("*a*.", 4), @"");
1781 assert_snapshot!(access_field("`a`.", 4), @"");
1782 assert_snapshot!(access_field("$a$.", 4), @"");
1783 assert_snapshot!(access_field("\"a\".", 4), @"");
1784 assert_snapshot!(access_field("@a.", 3), @"");
1785 assert_snapshot!(access_field("<a>.", 4), @"");
1786 }
1787
1788 #[test]
1789 fn test_markup_chain_access() {
1790 assert_snapshot!(access_node("#a.b.", 5), @"a.b");
1791 assert_snapshot!(access_field("#a.b.", 5), @"DotSuffix: 5");
1792 assert_snapshot!(access_node("#a.b.c.", 7), @"a.b.c");
1793 assert_snapshot!(access_field("#a.b.c.", 7), @"DotSuffix: 7");
1794 assert_snapshot!(access_node("#context a.", 11), @"a");
1795 assert_snapshot!(access_field("#context a.", 11), @"DotSuffix: 11");
1796 assert_snapshot!(access_node("#context a.b.", 13), @"a.b");
1797 assert_snapshot!(access_field("#context a.b.", 13), @"DotSuffix: 13");
1798
1799 assert_snapshot!(access_node("#a.at(1).", 9), @"a.at(1)");
1800 assert_snapshot!(access_field("#a.at(1).", 9), @"DotSuffix: 9");
1801 assert_snapshot!(access_node("#context a.at(1).", 17), @"a.at(1)");
1802 assert_snapshot!(access_field("#context a.at(1).", 17), @"DotSuffix: 17");
1803
1804 assert_snapshot!(access_node("#a.at(1).c.", 11), @"a.at(1).c");
1805 assert_snapshot!(access_field("#a.at(1).c.", 11), @"DotSuffix: 11");
1806 assert_snapshot!(access_node("#context a.at(1).c.", 19), @"a.at(1).c");
1807 assert_snapshot!(access_field("#context a.at(1).c.", 19), @"DotSuffix: 19");
1808 }
1809
1810 #[test]
1811 fn test_hash_access() {
1812 assert_snapshot!(access_node("#a.", 3), @"a");
1813 assert_snapshot!(access_field("#a.", 3), @"DotSuffix: 3");
1814 assert_snapshot!(access_node("#(a).", 5), @"(a)");
1815 assert_snapshot!(access_field("#(a).", 5), @"DotSuffix: 5");
1816 assert_snapshot!(access_node("#`a`.", 5), @"`a`");
1817 assert_snapshot!(access_field("#`a`.", 5), @"DotSuffix: 5");
1818 assert_snapshot!(access_node("#$a$.", 5), @"$a$");
1819 assert_snapshot!(access_field("#$a$.", 5), @"DotSuffix: 5");
1820 assert_snapshot!(access_node("#(a,).", 6), @"(a,)");
1821 assert_snapshot!(access_field("#(a,).", 6), @"DotSuffix: 6");
1822 }
1823}