1#![allow(clippy::mutable_key_type)]
12
13use super::regex::{Regex, Region};
14use super::scope::*;
15use super::syntax_definition::*;
16use crate::parsing::syntax_definition::ContextId;
17use crate::parsing::syntax_set::{SyntaxReference, SyntaxSet};
18use fnv::FnvHasher;
19use regex_syntax::escape;
20use std::collections::HashMap;
21use std::hash::BuildHasherDefault;
22
23#[derive(Debug, thiserror::Error)]
25#[non_exhaustive]
26pub enum ParsingError {
27 #[error("Somehow main context was popped from the stack")]
28 MissingMainContext,
29 #[error("Missing context with ID '{0:?}'")]
32 MissingContext(ContextId),
33 #[error("Bad index to match_at: {0}")]
34 BadMatchIndex(usize),
35 #[error("Tried to use a ContextReference that has not bee resolved yet: {0:?}")]
36 UnresolvedContextReference(ContextReference),
37}
38
39#[derive(Debug, Clone, Default)]
68pub struct ParseLineOutput {
69 pub ops: Vec<(usize, ScopeStackOp)>,
71 pub replayed: Vec<Vec<(usize, ScopeStackOp)>>,
74 pub warnings: Vec<String>,
76}
77
78#[derive(Debug, Clone, Eq, PartialEq)]
79pub struct ParseState {
80 stack: Vec<StateLevel>,
81 first_line: bool,
82 proto_starts: Vec<usize>,
85 branch_points: Vec<BranchPoint>,
87 line_number: usize,
89 pending_lines: Vec<String>,
93 flushed_ops: Vec<Vec<(usize, ScopeStackOp)>>,
96 warnings: Vec<String>,
98 escape_stack: Vec<EscapeEntry>,
102}
103
104#[derive(Debug, Clone, Eq, PartialEq)]
106struct EscapeEntry {
107 regex: Regex,
109 captures: Option<CaptureMapping>,
111 stack_depth: usize,
114}
115
116#[derive(Debug, Clone, Eq, PartialEq)]
118struct BranchPoint {
119 name: String,
120 next_alternative: usize,
122 alternatives: Vec<ContextReference>,
123 stack_snapshot: Vec<StateLevel>,
124 proto_starts_snapshot: Vec<usize>,
125 match_start: usize,
128 trigger_match_start: usize,
135 pat_scope: Vec<Scope>,
138 line_number: usize,
140 ops_snapshot_len: usize,
142 stack_depth: usize,
144 non_consuming_push_at_snapshot: (usize, usize),
145 first_line_snapshot: bool,
146 with_prototype: Option<ContextReference>,
147 pending_lines_snapshot_len: usize,
149 escape_stack_snapshot: Vec<EscapeEntry>,
150 pop_count: usize,
152 prefix_ops: Vec<(usize, ScopeStackOp)>,
160 capture_ops: Vec<(usize, ScopeStackOp)>,
168}
169
170#[derive(Debug, Clone, Eq, PartialEq)]
171struct StateLevel {
172 context: ContextId,
173 prototypes: Vec<ContextId>,
174 captures: Option<(Region, String)>,
175}
176
177#[derive(Debug)]
178struct RegexMatch<'a> {
179 regions: Region,
180 context: &'a Context,
181 pat_index: usize,
182 from_with_prototype: bool,
183 would_loop: bool,
184 escape_index: usize,
186}
187
188type SearchCache = HashMap<*const MatchPattern, Option<Region>, BuildHasherDefault<FnvHasher>>;
190
191fn build_capture_ops(capture_map: &CaptureMapping, regions: &Region) -> Vec<(usize, ScopeStackOp)> {
198 let mut map: Vec<((usize, i32), ScopeStackOp)> = Vec::new();
199 for &(cap_index, ref scopes) in capture_map.iter() {
200 if let Some((cap_start, cap_end)) = regions.pos(cap_index) {
201 if cap_start == cap_end {
202 continue;
203 }
204 for scope in scopes.iter() {
205 map.push((
206 (cap_start, -((cap_end - cap_start) as i32)),
207 ScopeStackOp::Push(*scope),
208 ));
209 }
210 map.push(((cap_end, i32::MIN), ScopeStackOp::Pop(scopes.len())));
211 }
212 }
213 map.sort_by(|a, b| a.0.cmp(&b.0));
214 map.into_iter().map(|((i, _), op)| (i, op)).collect()
215}
216
217impl ParseState {
310 pub fn new(syntax: &SyntaxReference) -> ParseState {
313 let start_state = StateLevel {
314 context: syntax.context_ids()["__start"],
315 prototypes: Vec::new(),
316 captures: None,
317 };
318 ParseState {
319 stack: vec![start_state],
320 first_line: true,
321 proto_starts: Vec::new(),
322 branch_points: Vec::new(),
323 line_number: 0,
324 pending_lines: Vec::new(),
325 flushed_ops: Vec::new(),
326 warnings: Vec::new(),
327 escape_stack: Vec::new(),
328 }
329 }
330
331 pub fn parse_line(
351 &mut self,
352 line: &str,
353 syntax_set: &SyntaxSet,
354 ) -> Result<ParseLineOutput, ParsingError> {
355 if self.stack.is_empty() {
356 return Err(ParsingError::MissingMainContext);
357 }
358
359 let cur_line = self.line_number;
361 let warnings = &mut self.warnings;
362 self.branch_points.retain(|bp| {
363 let alive = cur_line.saturating_sub(bp.line_number) <= 128;
364 if !alive {
365 warnings.push(format!(
366 "branch point '{}' expired (exceeded 128-line rewind limit)",
367 bp.name
368 ));
369 }
370 alive
371 });
372 self.line_number += 1;
373
374 let ops = self.parse_line_inner(line, syntax_set)?;
375
376 let replayed = std::mem::take(&mut self.flushed_ops);
379
380 if !self.branch_points.is_empty() {
382 self.pending_lines.push(line.to_string());
383 } else {
384 self.pending_lines.clear();
386 }
387
388 let warnings = std::mem::take(&mut self.warnings);
389
390 Ok(ParseLineOutput {
391 ops,
392 replayed,
393 warnings,
394 })
395 }
396
397 pub fn is_speculative(&self) -> bool {
402 !self.branch_points.is_empty()
403 }
404
405 fn parse_line_inner(
410 &mut self,
411 line: &str,
412 syntax_set: &SyntaxSet,
413 ) -> Result<Vec<(usize, ScopeStackOp)>, ParsingError> {
414 self.parse_line_inner_from(line, syntax_set, 0)
415 }
416
417 fn parse_line_inner_from(
425 &mut self,
426 line: &str,
427 syntax_set: &SyntaxSet,
428 start_at: usize,
429 ) -> Result<Vec<(usize, ScopeStackOp)>, ParsingError> {
430 let mut match_start = start_at;
431 let mut res = Vec::new();
432
433 if start_at == 0 && self.first_line {
434 let cur_level = &self.stack[self.stack.len() - 1];
435 let context = syntax_set.get_context(&cur_level.context)?;
436 if !context.meta_content_scope.is_empty() {
437 res.push((0, ScopeStackOp::Push(context.meta_content_scope[0])));
438 }
439 self.first_line = false;
440 }
441
442 let mut regions = Region::new();
443 let fnv = BuildHasherDefault::<FnvHasher>::default();
444 let mut search_cache: SearchCache = HashMap::with_capacity_and_hasher(128, fnv);
445 let mut non_consuming_push_at = (0, 0);
447
448 while self.parse_next_token(
449 line,
450 syntax_set,
451 &mut match_start,
452 &mut search_cache,
453 &mut regions,
454 &mut non_consuming_push_at,
455 &mut res,
456 )? {}
457
458 Ok(res)
459 }
460
461 #[allow(clippy::too_many_arguments)]
462 fn parse_next_token(
463 &mut self,
464 line: &str,
465 syntax_set: &SyntaxSet,
466 start: &mut usize,
467 search_cache: &mut SearchCache,
468 regions: &mut Region,
469 non_consuming_push_at: &mut (usize, usize),
470 ops: &mut Vec<(usize, ScopeStackOp)>,
471 ) -> Result<bool, ParsingError> {
472 let check_pop_loop = {
473 let (pos, stack_depth) = *non_consuming_push_at;
474 pos == *start && stack_depth == self.stack.len()
475 };
476
477 while self
479 .proto_starts
480 .last()
481 .map(|start| *start >= self.stack.len())
482 .unwrap_or(false)
483 {
484 self.proto_starts.pop();
485 }
486
487 let best_match = self.find_best_match(
488 line,
489 *start,
490 syntax_set,
491 search_cache,
492 regions,
493 check_pop_loop,
494 )?;
495
496 if let Some(reg_match) = best_match {
497 if reg_match.pat_index == usize::MAX {
499 let (match_start, match_end) = reg_match.regions.pos(0).unwrap();
500 *start = match_end;
501 self.exec_escape(
502 reg_match.escape_index,
503 match_start,
504 match_end,
505 ®_match.regions,
506 syntax_set,
507 ops,
508 )?;
509 search_cache.clear();
510 return Ok(true);
511 }
512
513 if reg_match.would_loop {
514 if let Some((i, _)) = line[*start..].char_indices().nth(1) {
528 *start += i;
529 return Ok(true);
530 } else {
531 return Ok(false);
534 }
535 }
536
537 let match_end = reg_match.regions.pos(0).unwrap().1;
538
539 let context = reg_match.context;
541 let match_pattern = context.match_at(reg_match.pat_index)?;
542 if let MatchOperation::Fail(_) = match_pattern.operation {
543 let level_context = {
544 let id = &self.stack[self.stack.len() - 1].context;
545 syntax_set.get_context(id)?
546 };
547 return self.exec_pattern(
548 line,
549 ®_match,
550 level_context,
551 syntax_set,
552 start,
553 non_consuming_push_at,
554 ops,
555 search_cache,
556 );
557 }
558
559 let consuming = match_end > *start;
560 if !consuming {
561 if matches!(
566 match_pattern.operation,
567 MatchOperation::Push(_)
568 | MatchOperation::Branch { .. }
569 | MatchOperation::Embed { .. }
570 ) {
571 *non_consuming_push_at = (match_end, self.stack.len() + 1);
572 }
573 }
574
575 *start = match_end;
576
577 if reg_match.from_with_prototype {
579 self.proto_starts.push(self.stack.len());
581 }
582
583 let level_context = {
584 let id = &self.stack[self.stack.len() - 1].context;
585 syntax_set.get_context(id)?
586 };
587 self.exec_pattern(
588 line,
589 ®_match,
590 level_context,
591 syntax_set,
592 start,
593 non_consuming_push_at,
594 ops,
595 search_cache,
596 )?;
597
598 Ok(true)
599 } else {
600 Ok(false)
601 }
602 }
603
604 fn find_best_match<'a>(
605 &self,
606 line: &str,
607 start: usize,
608 syntax_set: &'a SyntaxSet,
609 search_cache: &mut SearchCache,
610 regions: &mut Region,
611 check_pop_loop: bool,
612 ) -> Result<Option<RegexMatch<'a>>, ParsingError> {
613 let cur_level = &self.stack[self.stack.len() - 1];
614 let context = syntax_set.get_context(&cur_level.context)?;
615 let prototype = if let Some(ref p) = context.prototype {
616 Some(p)
617 } else {
618 None
619 };
620
621 let context_chain = {
623 let proto_start = self.proto_starts.last().cloned().unwrap_or(0);
624 let with_prototypes = self.stack[proto_start..].iter().flat_map(|lvl| {
626 lvl.prototypes
627 .iter()
628 .map(move |ctx| (true, ctx, lvl.captures.as_ref()))
629 });
630 let cur_prototype = prototype.into_iter().map(|ctx| (false, ctx, None));
631 let cur_context =
632 Some((false, &cur_level.context, cur_level.captures.as_ref())).into_iter();
633 with_prototypes.chain(cur_prototype).chain(cur_context)
634 };
635
636 let mut search_end = line.len();
643 let mut escape_match: Option<(usize, Region)> = None; for (ei, entry) in self.escape_stack.iter().enumerate() {
646 let mut esc_regions = Region::new();
647 if entry
648 .regex
649 .search(line, start, line.len(), Some(&mut esc_regions), true)
650 {
651 let (esc_start, _esc_end) = esc_regions.pos(0).unwrap();
652 if esc_start < search_end {
653 search_end = esc_start;
654 escape_match = Some((ei, esc_regions));
655 }
656 }
657 }
658
659 if let Some((ei, ref esc_region)) = escape_match {
662 let esc_start = esc_region.pos(0).unwrap().0;
663 if esc_start == start {
664 return Ok(Some(RegexMatch {
665 regions: esc_region.clone(),
666 context: syntax_set.get_context(&cur_level.context)?,
667 pat_index: usize::MAX, from_with_prototype: false,
669 would_loop: false,
670 escape_index: ei,
671 }));
672 }
673 }
674
675 let mut min_start = usize::MAX;
676 let mut best_match: Option<RegexMatch<'_>> = None;
677 let mut pop_would_loop = false;
678
679 for (from_with_proto, ctx, captures) in context_chain {
680 for (pat_context, pat_index) in context_iter(syntax_set, syntax_set.get_context(ctx)?) {
681 let match_pat = pat_context.match_at(pat_index)?;
682
683 if let Some(match_region) = self.search_with_end(
684 line,
685 start,
686 search_end,
687 match_pat,
688 captures,
689 search_cache,
690 regions,
691 ) {
692 let (match_start, match_end) = match_region.pos(0).unwrap();
693
694 if match_start < min_start || (match_start == min_start && pop_would_loop) {
697 min_start = match_start;
704
705 let consuming = match_end > start;
706 pop_would_loop = check_pop_loop
715 && !consuming
716 && matches!(match_pat.operation, MatchOperation::Pop(1));
717
718 let push_too_deep = matches!(
719 match_pat.operation,
720 MatchOperation::Push(_)
721 | MatchOperation::Branch { .. }
722 | MatchOperation::Embed { .. }
723 ) && self.stack.len() >= 100;
724
725 if push_too_deep {
726 return Ok(None);
727 }
728
729 best_match = Some(RegexMatch {
730 regions: match_region,
731 context: pat_context,
732 pat_index,
733 from_with_prototype: from_with_proto,
734 would_loop: pop_would_loop,
735 escape_index: 0, });
737
738 if match_start == start && !pop_would_loop {
739 return Ok(best_match);
742 }
743 }
744 }
745 }
746 }
747
748 if let Some((ei, esc_region)) = escape_match {
751 let esc_start = esc_region.pos(0).unwrap().0;
752 if esc_start < min_start || (esc_start == min_start && pop_would_loop) {
753 return Ok(Some(RegexMatch {
754 regions: esc_region,
755 context: syntax_set.get_context(&cur_level.context)?,
756 pat_index: usize::MAX, from_with_prototype: false,
758 would_loop: false,
759 escape_index: ei,
760 }));
761 }
762 }
763
764 Ok(best_match)
765 }
766
767 fn search_with_end(
768 &self,
769 line: &str,
770 start: usize,
771 search_end: usize,
772 match_pat: &MatchPattern,
773 captures: Option<&(Region, String)>,
774 search_cache: &mut SearchCache,
775 regions: &mut Region,
776 ) -> Option<Region> {
777 let match_ptr = match_pat as *const MatchPattern;
779
780 if let Some(maybe_region) = search_cache.get(&match_ptr) {
781 if let Some(ref region) = *maybe_region {
782 let (cached_start, cached_end) = region.pos(0).unwrap();
783 if cached_start >= start && cached_end <= search_end {
784 return Some(region.clone());
786 } else if cached_start >= start && cached_start < search_end {
787 } else if cached_start >= search_end {
790 return None;
792 }
793 } else {
795 return None;
797 }
798 }
799
800 let (regex, can_cache) = match (match_pat.has_captures, captures) {
801 (true, Some(captures)) => {
802 let (region, s) = captures;
803 (&match_pat.regex_with_refs(region, s), false)
804 }
805 _ => (match_pat.regex(), true),
806 };
807 let allow_empty = !matches!(match_pat.operation, MatchOperation::None);
811 let matched = regex.search(line, start, search_end, Some(regions), allow_empty);
813
814 if matched {
815 let (match_start, match_end) = regions.pos(0).unwrap();
816 let does_something = match match_pat.operation {
818 MatchOperation::None => match_start != match_end,
819 MatchOperation::Push(_)
820 | MatchOperation::Branch { .. }
821 | MatchOperation::Embed { .. } => self.stack.len() < 100,
822 _ => true,
823 };
824 if can_cache && does_something && search_end == line.len() {
825 search_cache.insert(match_pat, Some(regions.clone()));
828 }
829 if does_something {
830 return Some(regions.clone());
832 }
833 } else if can_cache && search_end == line.len() {
834 search_cache.insert(match_pat, None);
835 }
836 None
837 }
838
839 fn exec_pattern<'a>(
843 &mut self,
844 line: &str,
845 reg_match: &RegexMatch<'a>,
846 level_context: &'a Context,
847 syntax_set: &'a SyntaxSet,
848 start: &mut usize,
849 non_consuming_push_at: &mut (usize, usize),
850 ops: &mut Vec<(usize, ScopeStackOp)>,
851 search_cache: &mut SearchCache,
852 ) -> Result<bool, ParsingError> {
853 let (match_start, match_end) = reg_match.regions.pos(0).unwrap();
854 let context = reg_match.context;
855 let pat = context.match_at(reg_match.pat_index)?;
856
857 if let MatchOperation::Fail(ref name) = pat.operation {
859 return self.handle_fail(
860 name,
861 line,
862 start,
863 non_consuming_push_at,
864 ops,
865 search_cache,
866 syntax_set,
867 );
868 }
869
870 let is_branch = matches!(pat.operation, MatchOperation::Branch { .. });
872 let synthetic_op;
873
874 if is_branch {
875 if let MatchOperation::Branch {
876 ref name,
877 ref alternatives,
878 pop_count,
879 } = pat.operation
880 {
881 let bp = BranchPoint {
892 name: name.clone(),
893 next_alternative: 1, alternatives: alternatives.clone(),
895 stack_snapshot: self.stack.clone(),
896 proto_starts_snapshot: self.proto_starts.clone(),
897 match_start: *start, trigger_match_start: match_start,
899 pat_scope: pat.scope.clone(),
900 line_number: self.line_number.saturating_sub(1), ops_snapshot_len: ops.len(),
902 stack_depth: self.stack.len(),
903 non_consuming_push_at_snapshot: *non_consuming_push_at,
904 first_line_snapshot: self.first_line,
905 with_prototype: pat.with_prototype.clone(),
906 pending_lines_snapshot_len: self.pending_lines.len(),
907 escape_stack_snapshot: self.escape_stack.clone(),
908 pop_count,
909 prefix_ops: ops.clone(),
910 capture_ops: pat
911 .captures
912 .as_ref()
913 .map(|m| build_capture_ops(m, ®_match.regions))
914 .unwrap_or_default(),
915 };
916 self.branch_points.push(bp);
917 synthetic_op = if pop_count > 0 {
920 MatchOperation::Set {
921 ctx_refs: vec![alternatives[0].clone()],
922 pop_count,
923 }
924 } else {
925 MatchOperation::Push(vec![alternatives[0].clone()])
926 };
927 } else {
928 unreachable!()
929 }
930 } else {
931 synthetic_op = pat.operation.clone();
932 }
933
934 let op_to_use = if is_branch {
935 &synthetic_op
936 } else {
937 &pat.operation
938 };
939
940 self.push_meta_ops(true, match_start, level_context, op_to_use, syntax_set, ops)?;
941 for s in &pat.scope {
942 ops.push((match_start, ScopeStackOp::Push(*s)));
943 }
944 let capture_ops = pat
945 .captures
946 .as_ref()
947 .map(|m| build_capture_ops(m, ®_match.regions))
948 .unwrap_or_default();
949 ops.extend(capture_ops.iter().cloned());
950 if !pat.scope.is_empty() {
951 ops.push((match_end, ScopeStackOp::Pop(pat.scope.len())));
952 }
953 self.push_meta_ops(false, match_end, level_context, op_to_use, syntax_set, ops)?;
954
955 if is_branch {
956 let synthetic_pat = MatchPattern::new(
958 pat.has_captures,
959 pat.regex.regex_str().to_string(),
960 pat.scope.clone(),
961 pat.captures.clone(),
962 synthetic_op,
963 pat.with_prototype.clone(),
964 );
965 self.perform_op(line, ®_match.regions, &synthetic_pat, syntax_set)
966 } else {
967 self.perform_op(line, ®_match.regions, pat, syntax_set)
968 }
969 }
970
971 fn handle_fail(
975 &mut self,
976 name: &str,
977 line: &str,
978 start: &mut usize,
979 non_consuming_push_at: &mut (usize, usize),
980 ops: &mut Vec<(usize, ScopeStackOp)>,
981 search_cache: &mut SearchCache,
982 syntax_set: &SyntaxSet,
983 ) -> Result<bool, ParsingError> {
984 let stack_len = self.stack.len();
995 let bp_index = self.branch_points.iter().rposition(|bp| {
996 bp.name == name && stack_len > bp.stack_depth.saturating_sub(bp.pop_count)
997 });
998 let bp_index = match bp_index {
999 Some(i) => i,
1000 None => return Ok(false), };
1002
1003 let cur_line = self.line_number.saturating_sub(1);
1004 let bp = &self.branch_points[bp_index];
1005
1006 if cur_line.saturating_sub(bp.line_number) > 128 {
1008 let bp = self.branch_points.remove(bp_index);
1009 self.warnings.push(format!(
1010 "branch point '{}' expired (exceeded 128-line rewind limit)",
1011 bp.name
1012 ));
1013 return Ok(false);
1014 }
1015
1016 if self.stack.len() < bp.stack_depth {
1018 self.branch_points.remove(bp_index);
1019 return Ok(false);
1020 }
1021
1022 if bp.next_alternative >= bp.alternatives.len() {
1024 let is_cross_line = bp.line_number < cur_line;
1049 let stack_snapshot = bp.stack_snapshot.clone();
1050 let proto_starts_snapshot = bp.proto_starts_snapshot.clone();
1051 let escape_stack_snapshot = bp.escape_stack_snapshot.clone();
1052 let first_line_snapshot = bp.first_line_snapshot;
1053 let non_consuming_push_at_snapshot = bp.non_consuming_push_at_snapshot;
1054 let ops_snapshot_len = bp.ops_snapshot_len;
1055 let match_start_pos = bp.match_start;
1056 let pending_lines_snapshot_len = bp.pending_lines_snapshot_len;
1057 let prefix_ops = bp.prefix_ops.clone();
1058 self.branch_points.remove(bp_index);
1059
1060 self.stack = stack_snapshot;
1061 self.proto_starts = proto_starts_snapshot;
1062 self.escape_stack = escape_stack_snapshot;
1063 self.first_line = first_line_snapshot;
1064 *non_consuming_push_at = non_consuming_push_at_snapshot;
1065 ops.truncate(ops_snapshot_len.min(ops.len()));
1066
1067 if is_cross_line {
1068 let truncated_lines: Vec<String> =
1082 self.pending_lines[pending_lines_snapshot_len..].to_vec();
1083 let mut replayed_ops: Vec<Vec<(usize, ScopeStackOp)>> =
1084 Vec::with_capacity(truncated_lines.len());
1085 for (i, replay_line) in truncated_lines.iter().enumerate() {
1086 let line_ops = if i == 0 {
1087 let mut first_line_ops = prefix_ops.clone();
1088 let resume_at = if let Some((j, _)) =
1089 replay_line[match_start_pos..].char_indices().nth(1)
1090 {
1091 match_start_pos + j
1092 } else {
1093 replay_line.len()
1094 };
1095 let tail_ops =
1096 self.parse_line_inner_from(replay_line, syntax_set, resume_at)?;
1097 first_line_ops.extend(tail_ops);
1098 first_line_ops
1099 } else {
1100 self.parse_line_inner(replay_line, syntax_set)?
1101 };
1102 replayed_ops.push(line_ops);
1103 }
1104 self.flushed_ops.extend(replayed_ops);
1105
1106 ops.clear();
1109 *start = 0;
1110 *non_consuming_push_at = (0, 0);
1111 search_cache.clear();
1112 return Ok(true);
1113 }
1114
1115 if let Some((i, _)) = line[match_start_pos..].char_indices().nth(1) {
1118 *start = match_start_pos + i;
1119 } else {
1120 *start = line.len();
1122 }
1123 search_cache.clear();
1124 return Ok(true);
1125 }
1126
1127 let is_cross_line = bp.line_number < cur_line;
1129
1130 let next_alt_index = bp.next_alternative;
1132 let next_alt = bp.alternatives[next_alt_index].clone();
1133 let match_start_pos = bp.match_start;
1134 let trigger_match_start = bp.trigger_match_start;
1135 let trigger_pat_scope = bp.pat_scope.clone();
1136 let trigger_capture_ops = bp.capture_ops.clone();
1137 let stack_snapshot = bp.stack_snapshot.clone();
1138 let proto_starts_snapshot = bp.proto_starts_snapshot.clone();
1139 let first_line_snapshot = bp.first_line_snapshot;
1140 let non_consuming_push_at_snapshot = bp.non_consuming_push_at_snapshot;
1141 let ops_snapshot_len = bp.ops_snapshot_len;
1142 let pending_lines_snapshot_len = bp.pending_lines_snapshot_len;
1143 let escape_stack_snapshot = bp.escape_stack_snapshot.clone();
1144 let prefix_ops = bp.prefix_ops.clone();
1145 let pop_count = self.branch_points[bp_index].pop_count;
1148
1149 self.stack = stack_snapshot;
1151 self.proto_starts = proto_starts_snapshot;
1152 self.escape_stack = escape_stack_snapshot;
1153 self.first_line = first_line_snapshot;
1154 *non_consuming_push_at = non_consuming_push_at_snapshot;
1155
1156 self.branch_points[bp_index].next_alternative = next_alt_index + 1;
1159
1160 if pop_count > 0 {
1162 for _ in 0..pop_count {
1163 self.stack.pop();
1164 }
1165 }
1166
1167 let with_prototype = self.branch_points[bp_index].with_prototype.clone();
1169 let context_id = next_alt.id()?;
1170 let context = syntax_set.get_context(&context_id)?;
1171 let captures = None; let proto_ids = match with_prototype {
1174 Some(ref p) => vec![p.id()?],
1175 None => Vec::new(),
1176 };
1177
1178 self.stack.push(StateLevel {
1179 context: context_id,
1180 prototypes: proto_ids,
1181 captures,
1182 });
1183
1184 if is_cross_line {
1185 let truncated_lines: Vec<String> =
1207 self.pending_lines[pending_lines_snapshot_len..].to_vec();
1208
1209 let mut replayed_ops: Vec<Vec<(usize, ScopeStackOp)>> =
1210 Vec::with_capacity(truncated_lines.len());
1211 for (i, replay_line) in truncated_lines.iter().enumerate() {
1212 let line_ops = if i == 0 {
1213 let mut first_line_ops = prefix_ops.clone();
1215 if let Some(clear_amount) = context.clear_scopes {
1224 first_line_ops
1225 .push((trigger_match_start, ScopeStackOp::Clear(clear_amount)));
1226 }
1227 for scope in context.meta_scope.iter() {
1228 first_line_ops.push((trigger_match_start, ScopeStackOp::Push(*scope)));
1229 }
1230 for scope in &trigger_pat_scope {
1231 first_line_ops.push((trigger_match_start, ScopeStackOp::Push(*scope)));
1232 }
1233 first_line_ops.extend(trigger_capture_ops.iter().cloned());
1237 if !trigger_pat_scope.is_empty() {
1238 first_line_ops
1239 .push((match_start_pos, ScopeStackOp::Pop(trigger_pat_scope.len())));
1240 }
1241 for scope in context.meta_content_scope.iter() {
1242 first_line_ops.push((match_start_pos, ScopeStackOp::Push(*scope)));
1243 }
1244 let tail_ops =
1246 self.parse_line_inner_from(replay_line, syntax_set, match_start_pos)?;
1247 first_line_ops.extend(tail_ops);
1248 first_line_ops
1249 } else {
1250 self.parse_line_inner(replay_line, syntax_set)?
1251 };
1252 replayed_ops.push(line_ops);
1253 }
1254 self.flushed_ops.extend(replayed_ops);
1257
1258 ops.clear();
1260 *start = 0;
1261 *non_consuming_push_at = (0, 0);
1262
1263 if bp_index < self.branch_points.len() {
1274 self.branch_points[bp_index].ops_snapshot_len = 0;
1275 }
1276 } else {
1277 ops.truncate(ops_snapshot_len.min(ops.len()));
1279 *start = match_start_pos;
1280
1281 self.branch_points[bp_index].ops_snapshot_len = ops.len();
1291
1292 if let Some(clear_amount) = context.clear_scopes {
1312 ops.push((trigger_match_start, ScopeStackOp::Clear(clear_amount)));
1313 }
1314 for scope in context.meta_scope.iter() {
1315 ops.push((trigger_match_start, ScopeStackOp::Push(*scope)));
1316 }
1317 for scope in &trigger_pat_scope {
1318 ops.push((trigger_match_start, ScopeStackOp::Push(*scope)));
1319 }
1320 ops.extend(trigger_capture_ops.iter().cloned());
1326 if !trigger_pat_scope.is_empty() {
1327 ops.push((match_start_pos, ScopeStackOp::Pop(trigger_pat_scope.len())));
1328 }
1329 for scope in context.meta_content_scope.iter() {
1330 ops.push((match_start_pos, ScopeStackOp::Push(*scope)));
1331 }
1332 }
1333
1334 search_cache.clear();
1336
1337 Ok(true)
1338 }
1339
1340 fn current_syntax_version(&self, syntax_set: &SyntaxSet) -> u32 {
1342 if let Some(level) = self.stack.last() {
1343 let syntax_index = level.context.syntax_index;
1344 syntax_set
1345 .syntaxes()
1346 .get(syntax_index)
1347 .map_or(1, |s| s.version)
1348 } else {
1349 1
1350 }
1351 }
1352
1353 fn push_meta_ops(
1354 &self,
1355 initial: bool,
1356 index: usize,
1357 cur_context: &Context,
1358 match_op: &MatchOperation,
1359 syntax_set: &SyntaxSet,
1360 ops: &mut Vec<(usize, ScopeStackOp)>,
1361 ) -> Result<(), ParsingError> {
1362 let version = self.current_syntax_version(syntax_set);
1363 match *match_op {
1368 MatchOperation::Pop(n) => {
1369 let stack_len = self.stack.len();
1386 let pop_count = n.min(stack_len);
1387 if initial {
1388 let skip = version >= 2
1392 && stack_len >= 2
1393 && syntax_set
1394 .get_context(&self.stack[stack_len - 2].context)
1395 .map(|c| c.embed_scope_replaces)
1396 .unwrap_or(false);
1397 if !skip && !cur_context.meta_content_scope.is_empty() {
1398 ops.push((
1399 index,
1400 ScopeStackOp::Pop(cur_context.meta_content_scope.len()),
1401 ));
1402 }
1403 } else {
1404 if !cur_context.meta_scope.is_empty() {
1408 ops.push((index, ScopeStackOp::Pop(cur_context.meta_scope.len())));
1409 }
1410 for depth in 1..pop_count {
1424 let level_idx = stack_len - 1 - depth;
1425 let ctx = syntax_set.get_context(&self.stack[level_idx].context)?;
1426 let skip_content = version >= 2
1427 && level_idx >= 1
1428 && syntax_set
1429 .get_context(&self.stack[level_idx - 1].context)
1430 .map(|c| c.embed_scope_replaces)
1431 .unwrap_or(false);
1432 if !skip_content && !ctx.meta_content_scope.is_empty() {
1433 ops.push((index, ScopeStackOp::Pop(ctx.meta_content_scope.len())));
1434 }
1435 if !ctx.meta_scope.is_empty() {
1436 ops.push((index, ScopeStackOp::Pop(ctx.meta_scope.len())));
1437 }
1438 if ctx.clear_scopes.is_some() {
1439 ops.push((index, ScopeStackOp::Restore));
1440 }
1441 }
1442 }
1443
1444 if !initial && cur_context.clear_scopes.is_some() {
1446 ops.push((index, ScopeStackOp::Restore))
1447 }
1448 }
1449 MatchOperation::Push(ref context_refs)
1454 | MatchOperation::Set {
1455 ctx_refs: ref context_refs,
1456 ..
1457 } => {
1458 let is_set = matches!(*match_op, MatchOperation::Set { .. });
1459 let set_pop_count = match *match_op {
1460 MatchOperation::Set { pop_count, .. } => pop_count.max(1),
1461 _ => 1,
1462 };
1463 if initial {
1465 if is_set && version >= 2 && !cur_context.meta_content_scope.is_empty() {
1467 ops.push((
1468 index,
1469 ScopeStackOp::Pop(cur_context.meta_content_scope.len()),
1470 ));
1471 }
1472 if version >= 2 {
1482 let single_context_set_clear = is_set && context_refs.len() == 1;
1515 for r in context_refs.iter() {
1516 let ctx = r.resolve(syntax_set)?;
1517
1518 let emit_clear_here = !is_set || single_context_set_clear;
1519 if emit_clear_here {
1520 if let Some(clear_amount) = ctx.clear_scopes {
1521 ops.push((index, ScopeStackOp::Clear(clear_amount)));
1522 }
1523 }
1524
1525 for scope in ctx.meta_scope.iter() {
1526 ops.push((index, ScopeStackOp::Push(*scope)));
1527 }
1528 }
1529 } else {
1530 for r in context_refs.iter() {
1531 let ctx = r.resolve(syntax_set)?;
1532
1533 if !is_set {
1534 if let Some(clear_amount) = ctx.clear_scopes {
1535 ops.push((index, ScopeStackOp::Clear(clear_amount)));
1536 }
1537 }
1538
1539 for scope in ctx.meta_scope.iter() {
1540 ops.push((index, ScopeStackOp::Push(*scope)));
1541 }
1542 }
1543 }
1544 } else {
1545 let repush = (is_set
1551 && (set_pop_count > 1
1552 || !cur_context.meta_scope.is_empty()
1553 || !cur_context.meta_content_scope.is_empty()
1554 || cur_context.clear_scopes.is_some()))
1559 || context_refs.iter().any(|r| {
1560 let ctx = r.resolve(syntax_set).unwrap();
1561
1562 !ctx.meta_content_scope.is_empty()
1563 || (ctx.clear_scopes.is_some() && is_set)
1564 });
1565 if repush {
1566 let mut num_to_pop: usize = context_refs
1568 .iter()
1569 .map(|r| {
1570 let ctx = r.resolve(syntax_set).unwrap();
1571 ctx.meta_scope.len()
1572 })
1573 .sum();
1574
1575 if is_set {
1577 if version >= 2 {
1578 num_to_pop += cur_context.meta_scope.len();
1580 } else {
1581 num_to_pop += cur_context.meta_content_scope.len()
1582 + cur_context.meta_scope.len();
1583 }
1584 if set_pop_count > 1 {
1590 let stack_len = self.stack.len();
1591 for depth in 1..set_pop_count.min(stack_len) {
1592 let level_idx = stack_len - 1 - depth;
1593 let ctx =
1594 syntax_set.get_context(&self.stack[level_idx].context)?;
1595 num_to_pop +=
1596 ctx.meta_content_scope.len() + ctx.meta_scope.len();
1597 }
1598 }
1599 }
1600
1601 if num_to_pop > 0 {
1603 ops.push((index, ScopeStackOp::Pop(num_to_pop)));
1604 }
1605
1606 if is_set && cur_context.clear_scopes.is_some() {
1611 ops.push((index, ScopeStackOp::Restore));
1612 }
1613
1614 if version >= 2 {
1616 let single_context_set_clear = is_set && context_refs.len() == 1;
1635 let mut prev_embed_scope_replaces = false;
1636 for r in context_refs.iter() {
1637 let ctx = r.resolve(syntax_set)?;
1638
1639 if is_set && !single_context_set_clear {
1640 if let Some(clear_amount) = ctx.clear_scopes {
1641 ops.push((index, ScopeStackOp::Clear(clear_amount)));
1642 }
1643 }
1644
1645 for scope in ctx.meta_scope.iter() {
1646 ops.push((index, ScopeStackOp::Push(*scope)));
1647 }
1648 if !prev_embed_scope_replaces {
1652 for scope in ctx.meta_content_scope.iter() {
1653 ops.push((index, ScopeStackOp::Push(*scope)));
1654 }
1655 }
1656 prev_embed_scope_replaces = ctx.embed_scope_replaces;
1657 }
1658 } else {
1659 for r in context_refs {
1660 let ctx = r.resolve(syntax_set)?;
1661
1662 if is_set {
1664 if let Some(clear_amount) = ctx.clear_scopes {
1665 ops.push((index, ScopeStackOp::Clear(clear_amount)));
1666 }
1667 }
1668
1669 for scope in ctx.meta_scope.iter() {
1670 ops.push((index, ScopeStackOp::Push(*scope)));
1671 }
1672 for scope in ctx.meta_content_scope.iter() {
1673 ops.push((index, ScopeStackOp::Push(*scope)));
1674 }
1675 }
1676 }
1677 }
1678 }
1679 }
1680 MatchOperation::Embed {
1681 ref contexts,
1682 pop_count,
1683 ..
1684 } => {
1685 let synthetic = if pop_count > 0 {
1689 MatchOperation::Set {
1690 ctx_refs: contexts.clone(),
1691 pop_count,
1692 }
1693 } else {
1694 MatchOperation::Push(contexts.clone())
1695 };
1696 return self.push_meta_ops(
1697 initial,
1698 index,
1699 cur_context,
1700 &synthetic,
1701 syntax_set,
1702 ops,
1703 );
1704 }
1705 MatchOperation::None | MatchOperation::Fail(_) => (),
1706 MatchOperation::Branch {
1707 ref alternatives,
1708 pop_count,
1709 ..
1710 } => {
1711 let synthetic = if pop_count > 0 {
1715 MatchOperation::Set {
1716 ctx_refs: alternatives.clone(),
1717 pop_count,
1718 }
1719 } else {
1720 MatchOperation::Push(alternatives.clone())
1721 };
1722 return self.push_meta_ops(
1723 initial,
1724 index,
1725 cur_context,
1726 &synthetic,
1727 syntax_set,
1728 ops,
1729 );
1730 }
1731 }
1732
1733 Ok(())
1734 }
1735
1736 fn perform_op(
1738 &mut self,
1739 line: &str,
1740 regions: &Region,
1741 pat: &MatchPattern,
1742 syntax_set: &SyntaxSet,
1743 ) -> Result<bool, ParsingError> {
1744 let (ctx_refs, old_proto_ids, is_embed) = match pat.operation {
1745 MatchOperation::Push(ref ctx_refs) => (ctx_refs, None, false),
1746 MatchOperation::Embed {
1747 ref contexts,
1748 pop_count,
1749 ..
1750 } => {
1751 if pop_count > 0 {
1752 for _ in 0..pop_count {
1753 self.stack.pop();
1754 }
1755 self.branch_points
1756 .retain(|bp| bp.stack_depth <= self.stack.len());
1757 self.escape_stack
1758 .retain(|e| e.stack_depth < self.stack.len());
1759 }
1760 (contexts, None, true)
1761 }
1762 MatchOperation::Set {
1763 ref ctx_refs,
1764 pop_count,
1765 } => {
1766 let pops = pop_count.max(1);
1772 let old_proto_ids = self.stack.pop().map(|s| s.prototypes);
1773 for _ in 1..pops {
1774 self.stack.pop();
1775 }
1776 let final_len = self.stack.len() + ctx_refs.len();
1781 self.branch_points.retain(|bp| bp.stack_depth <= final_len);
1782 self.escape_stack.retain(|e| e.stack_depth < final_len);
1783 (ctx_refs, old_proto_ids, false)
1784 }
1785 MatchOperation::Pop(n) => {
1786 for _ in 0..n {
1787 self.stack.pop();
1788 }
1789 self.branch_points
1791 .retain(|bp| bp.stack_depth <= self.stack.len());
1792 self.escape_stack
1794 .retain(|e| e.stack_depth < self.stack.len());
1795 return Ok(true);
1796 }
1797 MatchOperation::None => return Ok(false),
1798 MatchOperation::Branch { .. } | MatchOperation::Fail(_) => {
1799 return Ok(false);
1801 }
1802 };
1803
1804 let stack_depth_before = self.stack.len();
1806
1807 for (i, r) in ctx_refs.iter().enumerate() {
1808 let mut proto_ids = if i == 0 {
1809 old_proto_ids.clone().unwrap_or_else(Vec::new)
1812 } else {
1813 Vec::new()
1814 };
1815 if i == ctx_refs.len() - 1 {
1816 if let Some(ref p) = pat.with_prototype {
1822 proto_ids.push(p.id()?);
1823 }
1824 }
1825 let context_id = r.id()?;
1826 let context = syntax_set.get_context(&context_id)?;
1827 let captures = {
1828 let mut uses_backrefs = context.uses_backrefs;
1829 if !proto_ids.is_empty() {
1830 uses_backrefs = uses_backrefs
1831 || proto_ids
1832 .iter()
1833 .any(|id| syntax_set.get_context(id).unwrap().uses_backrefs);
1834 }
1835 if uses_backrefs {
1836 Some((regions.clone(), line.to_owned()))
1837 } else {
1838 None
1839 }
1840 };
1841 self.stack.push(StateLevel {
1842 context: context_id,
1843 prototypes: proto_ids,
1844 captures,
1845 });
1846 }
1847
1848 if is_embed {
1850 if let MatchOperation::Embed { ref escape, .. } = pat.operation {
1851 let resolved_regex = if escape.has_captures {
1852 let new_regex_str =
1854 substitute_backrefs_in_regex(escape.escape_regex.regex_str(), |i| {
1855 regions.pos(i).map(|(s, e)| escape_str(&line[s..e]))
1856 });
1857 Regex::new(new_regex_str)
1858 } else {
1859 escape.escape_regex.clone()
1860 };
1861 self.escape_stack.push(EscapeEntry {
1862 regex: resolved_regex,
1863 captures: escape.escape_captures.clone(),
1864 stack_depth: stack_depth_before,
1865 });
1866 }
1867 }
1868
1869 Ok(true)
1870 }
1871
1872 fn exec_escape(
1875 &mut self,
1876 escape_idx: usize,
1877 match_start: usize,
1878 _match_end: usize,
1879 regions: &Region,
1880 syntax_set: &SyntaxSet,
1881 ops: &mut Vec<(usize, ScopeStackOp)>,
1882 ) -> Result<(), ParsingError> {
1883 let entry = &self.escape_stack[escape_idx];
1884 let target_depth = entry.stack_depth;
1885 let escape_captures = entry.captures.clone();
1886
1887 while self.stack.len() > target_depth {
1889 let level = &self.stack[self.stack.len() - 1];
1890 let ctx = syntax_set.get_context(&level.context)?;
1891
1892 if !ctx.meta_content_scope.is_empty() {
1903 let skip = self.stack.len() >= 2
1904 && syntax_set
1905 .get_context(&self.stack[self.stack.len() - 2].context)
1906 .map(|c| c.embed_scope_replaces)
1907 .unwrap_or(false);
1908 if !skip {
1909 ops.push((match_start, ScopeStackOp::Pop(ctx.meta_content_scope.len())));
1910 }
1911 }
1912
1913 if !ctx.meta_scope.is_empty() {
1915 ops.push((match_start, ScopeStackOp::Pop(ctx.meta_scope.len())));
1916 }
1917
1918 if ctx.clear_scopes.is_some() {
1920 ops.push((match_start, ScopeStackOp::Restore));
1921 }
1922
1923 self.stack.pop();
1924 }
1925
1926 if let Some(ref capture_map) = escape_captures {
1928 let mut map: Vec<((usize, i32), ScopeStackOp)> = Vec::new();
1929 for &(cap_index, ref scopes) in capture_map.iter() {
1930 if let Some((cap_start, cap_end)) = regions.pos(cap_index) {
1931 if cap_start == cap_end {
1932 continue;
1933 }
1934 for scope in scopes.iter() {
1935 map.push((
1936 (cap_start, -((cap_end - cap_start) as i32)),
1937 ScopeStackOp::Push(*scope),
1938 ));
1939 }
1940 map.push(((cap_end, i32::MIN), ScopeStackOp::Pop(scopes.len())));
1941 }
1942 }
1943 map.sort_by(|a, b| a.0.cmp(&b.0));
1944 for ((index, _), op) in map.into_iter() {
1945 ops.push((index, op));
1946 }
1947 }
1948
1949 self.escape_stack.truncate(escape_idx);
1951
1952 self.branch_points
1954 .retain(|bp| bp.stack_depth <= self.stack.len());
1955
1956 Ok(())
1957 }
1958}
1959
1960fn escape_str(s: &str) -> String {
1962 escape(s)
1963}
1964
1965#[cfg(feature = "yaml-load")]
1966#[cfg(test)]
1967mod tests {
1968 use super::*;
1969 use crate::parsing::ScopeStackOp::{Pop, Push};
1970 use crate::parsing::{Scope, ScopeStack, SyntaxSet, SyntaxSetBuilder};
1971 use crate::util::debug_print_ops;
1972 use crate::utils::testdata;
1973
1974 const TEST_SYNTAX: &str = include_str!("../../testdata/parser_tests.sublime-syntax");
1975 #[test]
1976 fn can_parse_simple() {
1977 let ss = &*testdata::PACKAGES_SYN_SET;
1978 let mut state = {
1979 let syntax = ss.find_syntax_by_name("Ruby (Rails)").unwrap();
1980 ParseState::new(syntax)
1981 };
1982
1983 let ops1 = ops(&mut state, "module Bob::Wow::Troll::Five; 5; end", ss);
1984 let test_ops1 = vec![
1992 (0, Push(Scope::new("source.ruby.rails").unwrap())),
1993 (0, Push(Scope::new("meta.namespace.ruby").unwrap())),
1994 (
1995 0,
1996 Push(Scope::new("keyword.declaration.namespace.ruby").unwrap()),
1997 ),
1998 (6, Pop(1)),
1999 (7, Pop(1)),
2000 (7, Push(Scope::new("meta.namespace.ruby").unwrap())),
2001 (7, Push(Scope::new("entity.name.namespace.ruby").unwrap())),
2002 (7, Push(Scope::new("support.other.namespace.ruby").unwrap())),
2003 ];
2004 assert_eq!(&ops1[0..test_ops1.len()], &test_ops1[..]);
2005
2006 let ops2 = ops(&mut state, "def lol(wow = 5)", ss);
2007 let test_ops2 = [
2008 (0, Push(Scope::new("meta.function.ruby").unwrap())),
2009 (
2010 0,
2011 Push(Scope::new("keyword.declaration.function.ruby").unwrap()),
2012 ),
2013 (3, Pop(2)),
2014 (3, Push(Scope::new("meta.function.ruby").unwrap())),
2015 (4, Push(Scope::new("entity.name.function.ruby").unwrap())),
2016 (7, Pop(1)),
2017 ];
2018 assert_eq!(&ops2[0..test_ops2.len()], &test_ops2[..]);
2019 }
2020
2021 #[test]
2022 fn can_parse_yaml() {
2023 let ps = &*testdata::PACKAGES_SYN_SET;
2024 let mut state = {
2025 let syntax = ps.find_syntax_by_name("YAML").unwrap();
2026 ParseState::new(syntax)
2027 };
2028
2029 assert_eq!(
2030 ops(&mut state, "key: value\n", ps),
2031 vec![
2032 (0, Push(Scope::new("source.yaml").unwrap())),
2033 (0, Push(Scope::new("meta.mapping.key.yaml").unwrap())),
2034 (0, Push(Scope::new("meta.string.yaml").unwrap())),
2035 (
2036 0,
2037 Push(Scope::new("string.unquoted.plain.out.yaml").unwrap())
2038 ),
2039 (3, Pop(2)),
2040 (3, Pop(1)),
2041 (3, Push(Scope::new("meta.mapping.yaml").unwrap())),
2042 (
2043 3,
2044 Push(Scope::new("punctuation.separator.key-value.mapping.yaml").unwrap())
2045 ),
2046 (4, Pop(2)),
2047 (5, Push(Scope::new("meta.string.yaml").unwrap())),
2048 (
2049 5,
2050 Push(Scope::new("string.unquoted.plain.out.yaml").unwrap())
2051 ),
2052 (10, Pop(2)),
2053 ]
2054 );
2055 }
2056
2057 #[test]
2058 fn can_parse_includes() {
2059 let ss = &*testdata::PACKAGES_SYN_SET;
2060 let mut state = {
2061 let syntax = ss.find_syntax_by_name("HTML (Rails)").unwrap();
2062 ParseState::new(syntax)
2063 };
2064
2065 let ops = ops(&mut state, "<script>var lol = '<% def wow(", ss);
2066
2067 assert!(
2068 !ops.is_empty(),
2069 "expected non-empty ops for line with includes"
2070 );
2071 let mut stack = ScopeStack::new();
2072 for (_, op) in ops.iter() {
2073 stack.apply(op).expect("#[cfg(test)]");
2074 }
2075 let stack_str = format!("{:?}", stack.as_slice());
2076 assert!(
2077 stack_str.contains("text.html.rails"),
2078 "expected text.html.rails in scope stack, got: {:?}",
2079 stack.as_slice()
2080 );
2081 }
2082
2083 #[test]
2084 fn can_parse_backrefs() {
2085 let ss = &*testdata::PACKAGES_SYN_SET;
2086 let mut state = {
2087 let syntax = ss.find_syntax_by_name("Ruby (Rails)").unwrap();
2088 ParseState::new(syntax)
2089 };
2090
2091 assert_eq!(
2095 ops(&mut state, "lol = <<-SQL.strip", ss),
2096 vec![
2097 (0, Push(Scope::new("source.ruby.rails").unwrap())),
2098 (
2099 4,
2100 Push(Scope::new("keyword.operator.assignment.ruby").unwrap())
2101 ),
2102 (5, Pop(1)),
2103 (6, Push(Scope::new("meta.string.heredoc.ruby").unwrap())),
2104 (
2105 6,
2106 Push(Scope::new("punctuation.definition.heredoc.ruby").unwrap())
2107 ),
2108 (9, Pop(1)),
2109 (9, Push(Scope::new("meta.tag.heredoc.ruby").unwrap())),
2110 (9, Push(Scope::new("entity.name.tag.ruby").unwrap())),
2111 (12, Pop(1)),
2112 (12, Pop(2)),
2113 (
2114 12,
2115 Push(Scope::new("punctuation.accessor.dot.ruby").unwrap())
2116 ),
2117 (13, Pop(1)),
2118 ]
2119 );
2120
2121 assert_eq!(
2122 ops(&mut state, "wow", ss),
2123 vec![
2124 (0, Push(Scope::new("meta.string.heredoc.ruby").unwrap())),
2125 (0, Push(Scope::new("source.sql.embedded.ruby").unwrap()),),
2126 (0, Push(Scope::new("source.sql").unwrap())),
2127 (0, Push(Scope::new("source.sql.mysql").unwrap())),
2128 (0, Push(Scope::new("source.sql.basic").unwrap())),
2129 (0, Push(Scope::new("meta.column-name.sql").unwrap())),
2130 (3, Pop(1)),
2131 ]
2132 );
2133
2134 assert_eq!(
2135 ops(&mut state, "SQL", ss),
2136 vec![
2137 (0, Pop(4)),
2138 (0, Pop(1)),
2139 (0, Push(Scope::new("meta.string.heredoc.ruby").unwrap())),
2140 (0, Push(Scope::new("meta.tag.heredoc.ruby").unwrap())),
2141 (0, Push(Scope::new("entity.name.tag.ruby").unwrap())),
2142 (3, Pop(2)),
2143 (3, Pop(1)),
2144 ]
2145 );
2146 }
2147
2148 #[test]
2149 fn can_parse_preprocessor_rules() {
2150 let ss = &*testdata::PACKAGES_SYN_SET;
2151 let mut state = {
2152 let syntax = ss.find_syntax_by_name("C").unwrap();
2153 ParseState::new(syntax)
2154 };
2155
2156 assert_eq!(
2157 ops(&mut state, "#ifdef FOO", ss),
2158 vec![
2159 (0, Push(Scope::new("source.c").unwrap())),
2160 (0, Push(Scope::new("meta.preprocessor.c").unwrap())),
2161 (0, Push(Scope::new("keyword.control.import.c").unwrap())),
2162 (6, Pop(1)),
2163 (10, Pop(1)),
2164 ]
2165 );
2166 assert_eq!(
2167 ops(&mut state, "{", ss),
2168 vec![
2169 (0, Push(Scope::new("meta.block.c").unwrap())),
2170 (
2171 0,
2172 Push(Scope::new("punctuation.section.block.begin.c").unwrap())
2173 ),
2174 (1, Pop(1)),
2175 ]
2176 );
2177 assert_eq!(
2178 ops(&mut state, "#else", ss),
2179 vec![
2180 (0, Push(Scope::new("meta.preprocessor.c").unwrap())),
2181 (0, Push(Scope::new("keyword.control.import.c").unwrap())),
2182 (5, Pop(1)),
2183 (5, Pop(1)),
2184 ]
2185 );
2186 assert_eq!(
2187 ops(&mut state, "{", ss),
2188 vec![
2189 (0, Push(Scope::new("meta.block.c").unwrap())),
2190 (
2191 0,
2192 Push(Scope::new("punctuation.section.block.begin.c").unwrap())
2193 ),
2194 (1, Pop(1)),
2195 ]
2196 );
2197 assert_eq!(
2198 ops(&mut state, "#endif", ss),
2199 vec![
2200 (0, Pop(1)),
2201 (0, Push(Scope::new("meta.block.c").unwrap())),
2202 (0, Push(Scope::new("meta.preprocessor.c").unwrap())),
2203 (0, Push(Scope::new("keyword.control.import.c").unwrap())),
2204 (6, Pop(2)),
2205 (6, Pop(2)),
2206 (6, Push(Scope::new("meta.block.c").unwrap())),
2207 ]
2208 );
2209 assert_eq!(
2210 ops(&mut state, " foo;", ss),
2211 vec![
2212 (7, Push(Scope::new("punctuation.terminator.c").unwrap())),
2213 (8, Pop(1)),
2214 ]
2215 );
2216 assert_eq!(
2217 ops(&mut state, "}", ss),
2218 vec![
2219 (
2220 0,
2221 Push(Scope::new("punctuation.section.block.end.c").unwrap())
2222 ),
2223 (1, Pop(1)),
2224 (1, Pop(1)),
2225 ]
2226 );
2227 }
2228
2229 #[test]
2230 fn can_parse_issue25() {
2231 let ss = &*testdata::PACKAGES_SYN_SET;
2232 let mut state = {
2233 let syntax = ss.find_syntax_by_name("C").unwrap();
2234 ParseState::new(syntax)
2235 };
2236
2237 assert_eq!(ops(&mut state, "struct{estruct", ss).len(), 10);
2239 }
2240
2241 #[test]
2242 fn can_compare_parse_states() {
2243 let ss = &*testdata::PACKAGES_SYN_SET;
2253 let syntax = ss.find_syntax_by_name("Java").unwrap();
2254 let mut state1 = ParseState::new(syntax);
2255 let mut state2 = ParseState::new(syntax);
2256
2257 assert_eq!(ops(&mut state1, "class Foo {", ss).len(), 13);
2258 assert_eq!(ops(&mut state2, "class Foo {", ss).len(), 13);
2259
2260 assert_eq!(state1, state2);
2261 ops(&mut state1, "}", ss);
2262 assert_ne!(state1, state2);
2263 }
2264
2265 #[test]
2266 fn can_parse_non_nested_clear_scopes() {
2267 let line = "'hello #simple_cleared_scopes_test world test \\n '";
2268 let expect = [
2269 "<source.test>, <example.meta-scope.after-clear-scopes.example>, <example.pushes-clear-scopes.example>",
2270 "<source.test>, <example.meta-scope.after-clear-scopes.example>, <example.pops-clear-scopes.example>",
2271 "<source.test>, <string.quoted.single.example>, <constant.character.escape.example>",
2272 ];
2273 expect_scope_stacks(line, &expect, TEST_SYNTAX);
2274 }
2275
2276 #[test]
2277 fn can_parse_non_nested_too_many_clear_scopes() {
2278 let line = "'hello #too_many_cleared_scopes_test world test \\n '";
2279 let expect = [
2280 "<example.meta-scope.after-clear-scopes.example>, <example.pushes-clear-scopes.example>",
2281 "<example.meta-scope.after-clear-scopes.example>, <example.pops-clear-scopes.example>",
2282 "<source.test>, <string.quoted.single.example>, <constant.character.escape.example>",
2283 ];
2284 expect_scope_stacks(line, &expect, TEST_SYNTAX);
2285 }
2286
2287 #[test]
2288 fn can_parse_nested_clear_scopes() {
2289 let line = "'hello #nested_clear_scopes_test world foo bar test \\n '";
2290 let expect = [
2291 "<source.test>, <example.meta-scope.after-clear-scopes.example>, <example.pushes-clear-scopes.example>",
2292 "<source.test>, <example.meta-scope.cleared-previous-meta-scope.example>, <foo>",
2293 "<source.test>, <example.meta-scope.after-clear-scopes.example>, <example.pops-clear-scopes.example>",
2294 "<source.test>, <string.quoted.single.example>, <constant.character.escape.example>",
2295 ];
2296 expect_scope_stacks(line, &expect, TEST_SYNTAX);
2297 }
2298
2299 #[test]
2300 fn can_parse_infinite_loop() {
2301 let line = "#infinite_loop_test 123";
2302 let expect = ["<source.test>, <constant.numeric.test>"];
2303 expect_scope_stacks(line, &expect, TEST_SYNTAX);
2304 }
2305
2306 #[test]
2307 fn can_parse_infinite_seeming_loop() {
2308 let line = "#infinite_seeming_loop_test hello";
2311 let expect = [
2312 "<source.test>, <keyword.test>",
2313 "<source.test>, <test>, <string.unquoted.test>",
2314 "<source.test>, <test>, <keyword.control.test>",
2315 ];
2316 expect_scope_stacks(line, &expect, TEST_SYNTAX);
2317 }
2318
2319 #[test]
2320 fn can_parse_prototype_that_pops_main() {
2321 let syntax = r#"
2322name: test
2323scope: source.test
2324contexts:
2325 prototype:
2326 # This causes us to pop out of the main context. Sublime Text handles that
2327 # by pushing main back automatically.
2328 - match: (?=!)
2329 pop: true
2330 main:
2331 - match: foo
2332 scope: test.good
2333"#;
2334
2335 let line = "foo!";
2336 let expect = ["<source.test>, <test.good>"];
2337 expect_scope_stacks(line, &expect, syntax);
2338 }
2339
2340 #[test]
2341 fn can_parse_prototype_that_pops_multiple_context() {
2342 let syntax = r#"
2343name: test
2344scope: source.test
2345contexts:
2346 prototype:
2347 - match: "!"
2348 pop: 2
2349 bar:
2350 - match: \bbaz\b
2351 push: baz
2352 scope: main.baz
2353 foo:
2354 - match: \bbar\b
2355 push: bar
2356 scope: test.bar
2357 - match: \bgood\b
2358 push: baz
2359 scope: test.good
2360 baz: []
2361
2362 main:
2363 - match: \bfoo\b
2364 push: foo
2365 scope: test.foo
2366"#;
2367
2368 let line = "foo bar baz ! good";
2369 let expect = ["<source.test>, <test.good>"];
2370 expect_scope_stacks(line, &expect, syntax);
2371 }
2372
2373 #[test]
2374 fn can_parse_syntax_with_newline_in_character_class() {
2375 let syntax = r#"
2376name: test
2377scope: source.test
2378contexts:
2379 main:
2380 - match: foo[\n]
2381 scope: foo.end
2382 - match: foo
2383 scope: foo.any
2384"#;
2385
2386 let line = "foo";
2387 let expect = ["<source.test>, <foo.end>"];
2388 expect_scope_stacks(line, &expect, syntax);
2389
2390 let line = "foofoofoo";
2391 let expect = [
2392 "<source.test>, <foo.any>",
2393 "<source.test>, <foo.any>",
2394 "<source.test>, <foo.end>",
2395 ];
2396 expect_scope_stacks(line, &expect, syntax);
2397 }
2398
2399 #[test]
2400 fn can_parse_issue120() {
2401 let syntax = SyntaxDefinition::load_from_str(
2402 include_str!("../../testdata/embed_escape_test.sublime-syntax"),
2403 false,
2404 None,
2405 )
2406 .unwrap();
2407
2408 let line1 = "\"abctest\" foobar";
2409 let expect1 = [
2410 "<meta.attribute-with-value.style.html>, <string.quoted.double>, <punctuation.definition.string.begin.html>",
2411 "<meta.attribute-with-value.style.html>, <source.css>",
2412 "<meta.attribute-with-value.style.html>, <string.quoted.double>, <punctuation.definition.string.end.html>",
2413 "<meta.attribute-with-value.style.html>, <source.css>, <test.embedded>",
2414 "<top-level.test>",
2415 ];
2416
2417 expect_scope_stacks_with_syntax(line1, &expect1, syntax.clone());
2418
2419 let line2 = ">abctest</style>foobar";
2420 let expect2 = [
2421 "<meta.tag.style.begin.html>, <punctuation.definition.tag.end.html>",
2422 "<source.css.embedded.html>, <test.embedded>",
2423 "<top-level.test>",
2424 ];
2425 expect_scope_stacks_with_syntax(line2, &expect2, syntax);
2426 }
2427
2428 #[test]
2429 fn can_parse_non_consuming_pop_that_would_loop() {
2430 let syntax = r#"
2432name: test
2433scope: source.test
2434contexts:
2435 main:
2436 # This makes us go into "test" without consuming any characters
2437 - match: (?=hello)
2438 push: test
2439 test:
2440 # If we used this match, we'd go back to "main" without consuming anything,
2441 # and then back into "test", infinitely looping. ST detects this at this
2442 # point and ignores this match until at least one character matched.
2443 - match: (?!world)
2444 pop: true
2445 - match: \w+
2446 scope: test.matched
2447"#;
2448
2449 let line = "hello";
2450 let expect = ["<source.test>, <test.matched>"];
2451 expect_scope_stacks(line, &expect, syntax);
2452 }
2453
2454 #[test]
2455 fn can_parse_non_consuming_set_and_pop_that_would_loop() {
2456 let syntax = r#"
2457name: test
2458scope: source.test
2459contexts:
2460 main:
2461 # This makes us go into "a" without advancing
2462 - match: (?=test)
2463 push: a
2464 a:
2465 # This makes us go into "b" without advancing
2466 - match: (?=t)
2467 set: b
2468 b:
2469 # If we used this match, we'd go back to "main" without having advanced,
2470 # which means we'd have an infinite loop like with the previous test.
2471 # So even for a "set", we have to check if we're advancing or not.
2472 - match: (?=t)
2473 pop: true
2474 - match: \w+
2475 scope: test.matched
2476"#;
2477
2478 let line = "test";
2479 let expect = ["<source.test>, <test.matched>"];
2480 expect_scope_stacks(line, &expect, syntax);
2481 }
2482
2483 #[test]
2484 fn can_parse_non_consuming_set_after_consuming_push_that_does_not_loop() {
2485 let syntax = r#"
2486name: test
2487scope: source.test
2488contexts:
2489 main:
2490 # This makes us go into "a", but we consumed a character
2491 - match: t
2492 push: a
2493 - match: \w+
2494 scope: test.matched
2495 a:
2496 # This makes us go into "b" without consuming
2497 - match: (?=e)
2498 set: b
2499 b:
2500 # This match does not result in an infinite loop because we already consumed
2501 # a character to get into "a", so it's ok to pop back into "main".
2502 - match: (?=e)
2503 pop: true
2504"#;
2505
2506 let line = "test";
2507 let expect = ["<source.test>, <test.matched>"];
2508 expect_scope_stacks(line, &expect, syntax);
2509 }
2510
2511 #[test]
2512 fn can_parse_non_consuming_set_after_consuming_set_that_does_not_loop() {
2513 let syntax = r#"
2514name: test
2515scope: source.test
2516contexts:
2517 main:
2518 - match: (?=hello)
2519 push: a
2520 - match: \w+
2521 scope: test.matched
2522 a:
2523 - match: h
2524 set: b
2525 b:
2526 - match: (?=e)
2527 set: c
2528 c:
2529 # This is not an infinite loop because "a" consumed a character, so we can
2530 # actually pop back into main and then match the rest of the input.
2531 - match: (?=e)
2532 pop: true
2533"#;
2534
2535 let line = "hello";
2536 let expect = ["<source.test>, <test.matched>"];
2537 expect_scope_stacks(line, &expect, syntax);
2538 }
2539
2540 #[test]
2541 fn can_parse_non_consuming_pop_that_would_loop_at_end_of_line() {
2542 let syntax = r#"
2543name: test
2544scope: source.test
2545contexts:
2546 main:
2547 # This makes us go into "test" without consuming, even at the end of line
2548 - match: ""
2549 push: test
2550 test:
2551 - match: ""
2552 pop: true
2553 - match: \w+
2554 scope: test.matched
2555"#;
2556
2557 let line = "hello";
2558 let expect = ["<source.test>, <test.matched>"];
2559 expect_scope_stacks(line, &expect, syntax);
2560 }
2561
2562 #[test]
2563 fn non_consuming_pop_n_below_pre_push_depth_is_not_a_loop() {
2564 let syntax = r#"
2573name: test
2574scope: source.test
2575contexts:
2576 main:
2577 - match: open
2578 scope: test.open
2579 push: wrapper
2580 - match: y
2581 scope: test.main.y
2582 - match: z
2583 scope: test.main.z
2584 wrapper:
2585 - meta_scope: test.wrapper
2586 - match: ""
2587 branch_point: fallback
2588 branch:
2589 - try
2590 - give-up
2591 try:
2592 - match: x
2593 scope: test.try.match
2594 - match: (?=y)
2595 fail: fallback
2596 give-up:
2597 - match: ""
2598 pop: 2
2599"#;
2600 expect_scope_stacks("openyz", &["<source.test>, <test.main.y>"], syntax);
2607 }
2608
2609 #[test]
2610 fn can_parse_empty_but_consuming_set_that_does_not_loop() {
2611 let syntax = r#"
2612name: test
2613scope: source.test
2614contexts:
2615 main:
2616 - match: (?=hello)
2617 push: a
2618 - match: ello
2619 scope: test.good
2620 a:
2621 # This is an empty match, but it consumed a character (the "h")
2622 - match: (?=e)
2623 set: b
2624 b:
2625 # .. so it's ok to pop back to main from here
2626 - match: ""
2627 pop: true
2628 - match: ello
2629 scope: test.bad
2630"#;
2631
2632 let line = "hello";
2633 let expect = ["<source.test>, <test.good>"];
2634 expect_scope_stacks(line, &expect, syntax);
2635 }
2636
2637 #[test]
2638 fn can_parse_non_consuming_pop_that_does_not_loop() {
2639 let syntax = r#"
2640name: test
2641scope: source.test
2642contexts:
2643 main:
2644 # This is a non-consuming push, so "b" will need to check for a
2645 # non-consuming pop
2646 - match: (?=hello)
2647 push: [b, a]
2648 - match: ello
2649 scope: test.good
2650 a:
2651 # This pop is ok, it consumed "h"
2652 - match: (?=e)
2653 pop: true
2654 b:
2655 # This is non-consuming, and we set to "c"
2656 - match: (?=e)
2657 set: c
2658 c:
2659 # It's ok to pop back to "main" here because we consumed a character in the
2660 # meantime.
2661 - match: ""
2662 pop: true
2663 - match: ello
2664 scope: test.bad
2665"#;
2666
2667 let line = "hello";
2668 let expect = ["<source.test>, <test.good>"];
2669 expect_scope_stacks(line, &expect, syntax);
2670 }
2671
2672 #[test]
2673 fn can_parse_non_consuming_pop_with_multi_push_that_does_not_loop() {
2674 let syntax = r#"
2675name: test
2676scope: source.test
2677contexts:
2678 main:
2679 - match: (?=hello)
2680 push: [b, a]
2681 - match: ello
2682 scope: test.good
2683 a:
2684 # This pop is ok, as we're not popping back to "main" yet (which would loop),
2685 # we're popping to "b"
2686 - match: ""
2687 pop: true
2688 - match: \w+
2689 scope: test.bad
2690 b:
2691 - match: \w+
2692 scope: test.good
2693"#;
2694
2695 let line = "hello";
2696 let expect = ["<source.test>, <test.good>"];
2697 expect_scope_stacks(line, &expect, syntax);
2698 }
2699
2700 #[test]
2701 fn can_parse_non_consuming_pop_of_recursive_context_that_does_not_loop() {
2702 let syntax = r#"
2703name: test
2704scope: source.test
2705contexts:
2706 main:
2707 - match: xxx
2708 scope: test.good
2709 - include: basic-identifiers
2710
2711 basic-identifiers:
2712 - match: '\w+::'
2713 scope: test.matched
2714 push: no-type-names
2715
2716 no-type-names:
2717 - include: basic-identifiers
2718 - match: \w+
2719 scope: test.matched.inside
2720 # This is a tricky one because when this is the best match,
2721 # we have two instances of "no-type-names" on the stack, so we're popping
2722 # back from "no-type-names" to another "no-type-names".
2723 - match: ''
2724 pop: true
2725"#;
2726
2727 let line = "foo::bar::* xxx";
2728 let expect = ["<source.test>, <test.good>"];
2729 expect_scope_stacks(line, &expect, syntax);
2730 }
2731
2732 #[test]
2739 fn scope_only_pattern_that_matches_zero_width_finds_later_non_empty() {
2740 let syntax = r#"
2741name: test
2742scope: source.test
2743contexts:
2744 main:
2745 - match: \h{0,6}
2746 scope: number.hex
2747 - match: \S
2748 scope: invalid.illegal
2749"#;
2750
2751 let line = "012ACF 0gxs";
2752 let expect = [
2753 "<source.test>, <number.hex>", "<source.test>, <invalid.illegal>", ];
2756 expect_scope_stacks(line, &expect, syntax);
2757 }
2758
2759 #[test]
2765 fn scope_only_pattern_with_middle_empty_alt_matches_bang() {
2766 let syntax = r#"
2767name: test
2768scope: source.test
2769contexts:
2770 main:
2771 - match: \|\||&&||!
2772 scope: keyword.operator
2773"#;
2774 let line = "!";
2775 let expect = ["<source.test>, <keyword.operator>"];
2776 expect_scope_stacks(line, &expect, syntax);
2777 }
2778
2779 #[test]
2786 fn scope_only_pattern_with_leading_empty_alt_in_group_matches_name() {
2787 let syntax = r#"
2788name: test
2789scope: source.test
2790contexts:
2791 main:
2792 - match: \b(?x:|Box|Vec)\b
2793 scope: support.type
2794"#;
2795 let line = "Vec";
2796 let expect = ["<source.test>, <support.type>"];
2797 expect_scope_stacks(line, &expect, syntax);
2798 }
2799
2800 #[test]
2801 fn can_parse_non_consuming_pop_order() {
2802 let syntax = r#"
2803name: test
2804scope: source.test
2805contexts:
2806 main:
2807 - match: (?=hello)
2808 push: test
2809 test:
2810 # This matches first
2811 - match: (?=e)
2812 push: good
2813 # But this (looping) match replaces it, because it's an earlier match
2814 - match: (?=h)
2815 pop: true
2816 # And this should not replace it, as it's a later match (only matches at
2817 # the same position can replace looping pops).
2818 - match: (?=o)
2819 push: bad
2820 good:
2821 - match: \w+
2822 scope: test.good
2823 bad:
2824 - match: \w+
2825 scope: test.bad
2826"#;
2827
2828 let line = "hello";
2829 let expect = ["<source.test>, <test.good>"];
2830 expect_scope_stacks(line, &expect, syntax);
2831 }
2832
2833 #[test]
2834 fn can_parse_prototype_with_embed() {
2835 let syntax = r#"
2836name: Javadoc
2837scope: text.html.javadoc
2838contexts:
2839 prototype:
2840 - match: \*
2841 scope: punctuation.definition.comment.javadoc
2842
2843 main:
2844 - meta_include_prototype: false
2845 - match: /\*\*
2846 scope: comment.block.documentation.javadoc punctuation.definition.comment.begin.javadoc
2847 embed: contents
2848 embed_scope: comment.block.documentation.javadoc text.html.javadoc
2849 escape: \*/
2850 escape_captures:
2851 0: comment.block.documentation.javadoc punctuation.definition.comment.end.javadoc
2852
2853 contents:
2854 - match: ''
2855"#;
2856
2857 let syntax = SyntaxDefinition::load_from_str(syntax, true, None).unwrap();
2858 expect_scope_stacks_with_syntax("/** * */", &["<comment.block.documentation.javadoc>, <punctuation.definition.comment.begin.javadoc>", "<comment.block.documentation.javadoc>, <text.html.javadoc>, <punctuation.definition.comment.javadoc>", "<comment.block.documentation.javadoc>, <punctuation.definition.comment.end.javadoc>"], syntax);
2859 }
2860
2861 #[test]
2862 fn can_parse_context_included_in_prototype_via_named_reference() {
2863 let syntax = r#"
2864scope: source.test
2865contexts:
2866 prototype:
2867 - match: a
2868 push: a
2869 - match: b
2870 scope: test.bad
2871 main:
2872 - match: unused
2873 # This context is included in the prototype (see `push: a`).
2874 # Because of that, ST doesn't apply the prototype to this context, so if
2875 # we're in here the "b" shouldn't match.
2876 a:
2877 - match: a
2878 scope: test.good
2879"#;
2880
2881 let stack_states = stack_states(parse("aa b", syntax));
2882 assert_eq!(
2883 stack_states,
2884 vec![
2885 "<source.test>",
2886 "<source.test>, <test.good>",
2887 "<source.test>",
2888 ],
2889 "Expected test.bad to not match"
2890 );
2891 }
2892
2893 #[test]
2894 fn can_parse_with_prototype_set() {
2895 let syntax = r#"%YAML 1.2
2896---
2897scope: source.test-set-with-proto
2898contexts:
2899 main:
2900 - match: a
2901 scope: a
2902 set: next1
2903 with_prototype:
2904 - match: '1'
2905 scope: '1'
2906 - match: '2'
2907 scope: '2'
2908 - match: '3'
2909 scope: '3'
2910 - match: '4'
2911 scope: '4'
2912 - match: '5'
2913 scope: '5'
2914 set: [next3, next2]
2915 with_prototype:
2916 - match: c
2917 scope: cwith
2918 next1:
2919 - match: b
2920 scope: b
2921 set: next2
2922 next2:
2923 - match: c
2924 scope: c
2925 push: next3
2926 - match: e
2927 scope: e
2928 pop: true
2929 - match: f
2930 scope: f
2931 set: [next1, next2]
2932 next3:
2933 - match: d
2934 scope: d
2935 - match: (?=e)
2936 pop: true
2937 - match: c
2938 scope: cwithout
2939"#;
2940
2941 expect_scope_stacks_with_syntax(
2942 "a1b2c3d4e5",
2943 &[
2944 "<a>", "<1>", "<b>", "<2>", "<c>", "<3>", "<d>", "<4>", "<e>", "<5>",
2945 ],
2946 SyntaxDefinition::load_from_str(syntax, true, None).unwrap(),
2947 );
2948 expect_scope_stacks_with_syntax(
2949 "5cfcecbedcdea",
2950 &[
2951 "<5>",
2952 "<cwith>",
2953 "<f>",
2954 "<e>",
2955 "<b>",
2956 "<d>",
2957 "<cwithout>",
2958 "<a>",
2959 ],
2960 SyntaxDefinition::load_from_str(syntax, true, None).unwrap(),
2961 );
2962 }
2963
2964 #[test]
2965 fn can_parse_issue176() {
2966 let syntax = r#"
2967scope: source.dummy
2968contexts:
2969 main:
2970 - match: (test)(?=(foo))(f)
2971 captures:
2972 1: test
2973 2: ignored
2974 3: f
2975 push:
2976 - match: (oo)
2977 captures:
2978 1: keyword
2979"#;
2980
2981 let syntax = SyntaxDefinition::load_from_str(syntax, true, None).unwrap();
2982 expect_scope_stacks_with_syntax(
2983 "testfoo",
2984 &["<test>", "<f>", "<keyword>"],
2985 syntax,
2986 );
2987 }
2988
2989 #[test]
2990 fn can_parse_two_with_prototypes_at_same_stack_level() {
2991 let syntax_yamlstr = r#"
2992%YAML 1.2
2993---
2994# See http://www.sublimetext.com/docs/3/syntax.html
2995scope: source.example-wp
2996contexts:
2997 main:
2998 - match: a
2999 scope: a
3000 push:
3001 - match: b
3002 scope: b
3003 set:
3004 - match: c
3005 scope: c
3006 with_prototype:
3007 - match: '2'
3008 scope: '2'
3009 with_prototype:
3010 - match: '1'
3011 scope: '1'
3012"#;
3013
3014 let syntax = SyntaxDefinition::load_from_str(syntax_yamlstr, true, None).unwrap();
3015 expect_scope_stacks_with_syntax("abc12", &["<1>", "<2>"], syntax);
3016 }
3017
3018 #[test]
3019 fn can_parse_two_with_prototypes_at_same_stack_level_set_multiple() {
3020 let syntax_yamlstr = r#"
3021%YAML 1.2
3022---
3023# See http://www.sublimetext.com/docs/3/syntax.html
3024scope: source.example-wp
3025contexts:
3026 main:
3027 - match: a
3028 scope: a
3029 push:
3030 - match: b
3031 scope: b
3032 set: [context1, context2, context3]
3033 with_prototype:
3034 - match: '2'
3035 scope: '2'
3036 with_prototype:
3037 - match: '1'
3038 scope: '1'
3039 - match: '1'
3040 scope: digit1
3041 - match: '2'
3042 scope: digit2
3043 context1:
3044 - match: e
3045 scope: e
3046 pop: true
3047 - match: '2'
3048 scope: digit2
3049 context2:
3050 - match: d
3051 scope: d
3052 pop: true
3053 - match: '2'
3054 scope: digit2
3055 context3:
3056 - match: c
3057 scope: c
3058 pop: true
3059"#;
3060
3061 let syntax = SyntaxDefinition::load_from_str(syntax_yamlstr, true, None).unwrap();
3062 expect_scope_stacks_with_syntax("ab12", &["<1>", "<2>"], syntax.clone());
3063 expect_scope_stacks_with_syntax("abc12", &["<1>", "<digit2>"], syntax.clone());
3064 expect_scope_stacks_with_syntax("abcd12", &["<1>", "<digit2>"], syntax.clone());
3065 expect_scope_stacks_with_syntax("abcde12", &["<digit1>", "<digit2>"], syntax);
3066 }
3067
3068 #[test]
3069 fn can_parse_two_with_prototypes_at_same_stack_level_updated_captures() {
3070 let syntax_yamlstr = r#"
3071%YAML 1.2
3072---
3073# See http://www.sublimetext.com/docs/3/syntax.html
3074scope: source.example-wp
3075contexts:
3076 main:
3077 - match: (a)
3078 scope: a
3079 push:
3080 - match: (b)
3081 scope: b
3082 set:
3083 - match: c
3084 scope: c
3085 with_prototype:
3086 - match: d
3087 scope: d
3088 with_prototype:
3089 - match: \1
3090 scope: '1'
3091 pop: true
3092"#;
3093
3094 let syntax = SyntaxDefinition::load_from_str(syntax_yamlstr, true, None).unwrap();
3095 expect_scope_stacks_with_syntax("aa", &["<a>", "<1>"], syntax.clone());
3096 expect_scope_stacks_with_syntax("abcdb", &["<a>", "<b>", "<c>", "<d>", "<1>"], syntax);
3097 }
3098
3099 #[test]
3100 fn can_parse_two_with_prototypes_at_same_stack_level_updated_captures_ignore_unexisting() {
3101 let syntax_yamlstr = r#"
3102%YAML 1.2
3103---
3104# See http://www.sublimetext.com/docs/3/syntax.html
3105scope: source.example-wp
3106contexts:
3107 main:
3108 - match: (a)(-)
3109 scope: a
3110 push:
3111 - match: (b)
3112 scope: b
3113 set:
3114 - match: c
3115 scope: c
3116 with_prototype:
3117 - match: d
3118 scope: d
3119 with_prototype:
3120 - match: \2
3121 scope: '2'
3122 pop: true
3123 - match: \1
3124 scope: '1'
3125 pop: true
3126"#;
3127
3128 let syntax = SyntaxDefinition::load_from_str(syntax_yamlstr, true, None).unwrap();
3129 expect_scope_stacks_with_syntax("a--", &["<a>", "<2>"], syntax.clone());
3130 expect_scope_stacks_with_syntax("a-bcdba-", &["<a>", "<b>"], syntax);
3133 }
3134
3135 #[test]
3136 fn can_parse_syntax_with_eol_and_newline() {
3137 let syntax = r#"
3138name: test
3139scope: source.test
3140contexts:
3141 main:
3142 - match: foo$\n
3143 scope: foo.newline
3144"#;
3145
3146 let line = "foo";
3147 let expect = ["<source.test>, <foo.newline>"];
3148 expect_scope_stacks(line, &expect, syntax);
3149 }
3150
3151 #[test]
3152 fn can_parse_syntax_with_eol_only() {
3153 let syntax = r#"
3154name: test
3155scope: source.test
3156contexts:
3157 main:
3158 - match: foo$
3159 scope: foo.newline
3160"#;
3161
3162 let line = "foo";
3163 let expect = ["<source.test>, <foo.newline>"];
3164 expect_scope_stacks(line, &expect, syntax);
3165 }
3166
3167 #[test]
3168 fn can_parse_syntax_with_beginning_of_line() {
3169 let syntax = r#"
3170name: test
3171scope: source.test
3172contexts:
3173 main:
3174 - match: \w+
3175 scope: word
3176 push:
3177 # this should not match at the end of the line
3178 - match: ^\s*$
3179 pop: true
3180 - match: =+
3181 scope: heading
3182 pop: true
3183 - match: .*
3184 scope: other
3185"#;
3186
3187 let syntax_newlines = SyntaxDefinition::load_from_str(syntax, true, None).unwrap();
3188 let syntax_set = link(syntax_newlines);
3189
3190 let mut state = ParseState::new(&syntax_set.syntaxes()[0]);
3191 assert_eq!(
3192 ops(&mut state, "foo\n", &syntax_set),
3193 vec![
3194 (0, Push(Scope::new("source.test").unwrap())),
3195 (0, Push(Scope::new("word").unwrap())),
3196 (3, Pop(1))
3197 ]
3198 );
3199 assert_eq!(
3200 ops(&mut state, "===\n", &syntax_set),
3201 vec![(0, Push(Scope::new("heading").unwrap())), (3, Pop(1))]
3202 );
3203
3204 assert_eq!(
3205 ops(&mut state, "bar\n", &syntax_set),
3206 vec![(0, Push(Scope::new("word").unwrap())), (3, Pop(1))]
3207 );
3208 assert_eq!(ops(&mut state, "\n", &syntax_set), vec![]);
3210 assert_eq!(
3212 ops(&mut state, "====\n", &syntax_set),
3213 vec![(0, Push(Scope::new("other").unwrap())), (4, Pop(1))]
3214 );
3215 }
3216
3217 #[test]
3218 fn can_parse_syntax_with_comment_and_eol() {
3219 let syntax = r#"
3220name: test
3221scope: source.test
3222contexts:
3223 main:
3224 - match: (//).*$
3225 scope: comment.line.double-slash
3226"#;
3227
3228 let syntax_newlines = SyntaxDefinition::load_from_str(syntax, true, None).unwrap();
3229 let syntax_set = link(syntax_newlines);
3230
3231 let mut state = ParseState::new(&syntax_set.syntaxes()[0]);
3232 assert_eq!(
3233 ops(&mut state, "// foo\n", &syntax_set),
3234 vec![
3235 (0, Push(Scope::new("source.test").unwrap())),
3236 (0, Push(Scope::new("comment.line.double-slash").unwrap())),
3237 (6, Pop(1))
3241 ]
3242 );
3243 }
3244
3245 #[test]
3246 fn can_parse_text_with_unicode_to_skip() {
3247 let syntax = r#"
3248name: test
3249scope: source.test
3250contexts:
3251 main:
3252 - match: (?=.)
3253 push: test
3254 test:
3255 - match: (?=.)
3256 pop: true
3257 - match: x
3258 scope: test.good
3259"#;
3260
3261 expect_scope_stacks("\u{03C0}x", &["<source.test>, <test.good>"], syntax);
3263 expect_scope_stacks("\u{0800}x", &["<source.test>, <test.good>"], syntax);
3265 expect_scope_stacks("\u{1F600}x", &["<source.test>, <test.good>"], syntax);
3267 }
3268
3269 #[test]
3270 fn can_include_backrefs() {
3271 let syntax = SyntaxDefinition::load_from_str(
3272 r#"
3273 name: Backref Include Test
3274 scope: source.backrefinc
3275 contexts:
3276 main:
3277 - match: (a)
3278 scope: a
3279 push: context1
3280 context1:
3281 - include: context2
3282 context2:
3283 - match: \1
3284 scope: b
3285 pop: true
3286 "#,
3287 true,
3288 None,
3289 )
3290 .unwrap();
3291
3292 expect_scope_stacks_with_syntax("aa", &["<a>", "<b>"], syntax);
3293 }
3294
3295 #[test]
3296 fn can_include_nested_backrefs() {
3297 let syntax = SyntaxDefinition::load_from_str(
3298 r#"
3299 name: Backref Include Test
3300 scope: source.backrefinc
3301 contexts:
3302 main:
3303 - match: (a)
3304 scope: a
3305 push: context1
3306 context1:
3307 - include: context3
3308 context3:
3309 - include: context2
3310 context2:
3311 - match: \1
3312 scope: b
3313 pop: true
3314 "#,
3315 true,
3316 None,
3317 )
3318 .unwrap();
3319
3320 expect_scope_stacks_with_syntax("aa", &["<a>", "<b>"], syntax);
3321 }
3322
3323 #[test]
3324 fn can_avoid_infinite_stack_depth() {
3325 let syntax = SyntaxDefinition::load_from_str(
3326 r#"
3327 name: Stack Depth Test
3328 scope: source.stack_depth
3329 contexts:
3330 main:
3331 - match: (a)
3332 scope: a
3333 push: context1
3334
3335
3336 context1:
3337 - match: b
3338 scope: b
3339 - match: ''
3340 push: context1
3341 - match: ''
3342 pop: 1
3343 - match: c
3344 scope: c
3345 "#,
3346 true,
3347 None,
3348 )
3349 .unwrap();
3350
3351 let syntax_set = link(syntax);
3352 let mut state = ParseState::new(&syntax_set.syntaxes()[0]);
3353 expect_scope_stacks_for_ops(ops(&mut state, "a bc\n", &syntax_set), &["<a>"]);
3354 expect_scope_stacks_for_ops(ops(&mut state, "bc\n", &syntax_set), &["<b>"]);
3355 }
3356
3357 #[test]
3388 fn meta_append_inherits_meta_include_prototype_from_parent() {
3389 use crate::parsing::syntax_set::SyntaxSetBuilder;
3390
3391 let dir =
3392 std::env::temp_dir().join(format!("syntect-meta-append-test-{}", std::process::id()));
3393 let _ = std::fs::remove_dir_all(&dir);
3394 std::fs::create_dir_all(&dir).unwrap();
3395 std::fs::write(
3396 dir.join("parent.sublime-syntax"),
3397 r#"
3398name: Parent
3399scope: source.parent
3400file_extensions: [parent]
3401contexts:
3402 prototype:
3403 - match: '--'
3404 scope: punctuation.definition.comment
3405 push: comment-body
3406 comment-body:
3407 - meta_scope: comment.line
3408 - match: $
3409 pop: 1
3410 main:
3411 - match: \bopen\b
3412 push: inside
3413 inside:
3414 - meta_include_prototype: false
3415 - meta_scope: meta.inside
3416 - match: \bclose\b
3417 pop: 1
3418"#,
3419 )
3420 .unwrap();
3421 std::fs::write(
3422 dir.join("child.sublime-syntax"),
3423 r#"
3424name: Child
3425scope: source.child
3426file_extensions: [child]
3427extends: parent.sublime-syntax
3428contexts:
3429 inside:
3430 - meta_append: true
3431 - match: '!'
3432 scope: punctuation.bang.child
3433"#,
3434 )
3435 .unwrap();
3436 let mut builder = SyntaxSetBuilder::new();
3437 builder.add_from_folder(&dir, true).unwrap();
3438 let ss = builder.build();
3439 let syntax = ss
3440 .find_syntax_by_scope(Scope::new("source.child").unwrap())
3441 .unwrap();
3442 let mut state = ParseState::new(syntax);
3443 let o = ops(&mut state, "open -- close\n", &ss);
3444 let _ = std::fs::remove_dir_all(&dir);
3445 let comment_pushes = o
3446 .iter()
3447 .filter(|(_, op)| {
3448 matches!(op, ScopeStackOp::Push(s) if format!("{:?}", s).contains("comment"))
3449 })
3450 .count();
3451 assert_eq!(
3452 comment_pushes, 0,
3453 "`--` inside the inside context (meta_include_prototype: false in parent) \
3454 must not match the parent's prototype comment rule after meta_append merge; \
3455 ops were: {:?}",
3456 o
3457 );
3458 }
3459
3460 #[test]
3462 fn extending_syntax_does_not_double_push_top_level_scope() {
3463 use crate::parsing::SyntaxSet;
3464 let ss = SyntaxSet::load_from_folder("testdata/Packages").unwrap();
3465 let syntax = ss.find_syntax_by_name("Git Diff").unwrap();
3467 let mut state = ParseState::new(syntax);
3468 let o = ops(
3469 &mut state,
3470 "From 1234567890 Mon Sep 17 00:00:00 2001\n",
3471 &ss,
3472 );
3473 let source_pushes = o
3474 .iter()
3475 .filter(|(_, op)| matches!(op, ScopeStackOp::Push(s) if format!("{:?}", s) == "<source.diff.git>"))
3476 .count();
3477 assert_eq!(
3478 source_pushes, 1,
3479 "source.diff.git should be pushed exactly once for the file's top-level scope; ops were: {:?}",
3480 o
3481 );
3482 }
3483
3484 #[test]
3499 fn branch_point_match_scope_survives_fail_retry() {
3500 expect_scope_stacks(
3503 "trigger yes",
3504 &["<keyword.operator.test>", "<ok.test>"],
3505 r#"
3506 name: Branch Pat Scope Test
3507 scope: source.test
3508 contexts:
3509 main:
3510 - match: \btrigger\b
3511 scope: keyword.operator.test
3512 branch_point: t
3513 branch:
3514 - alt-fails
3515 - alt-succeeds
3516 - match: \S+
3517 scope: text.test
3518 alt-fails:
3519 - match: (?=\S)
3520 fail: t
3521 alt-succeeds:
3522 - match: \S+
3523 scope: ok.test
3524 pop: 1
3525 "#,
3526 );
3527 }
3528
3529 #[test]
3542 fn branch_point_capture_scopes_survive_fail_retry() {
3543 expect_scope_stacks(
3548 "word !",
3549 &["<outer.match>, <inner.capture>"],
3550 r#"
3551 name: Branch Capture Re-emit Test
3552 scope: source.test
3553 contexts:
3554 main:
3555 - match: (word)\s
3556 scope: outer.match
3557 captures:
3558 1: inner.capture
3559 branch_point: bp
3560 branch:
3561 - alt-fails
3562 - alt-succeeds
3563 alt-fails:
3564 - match: (?=!)
3565 fail: bp
3566 alt-succeeds:
3567 - match: \S+
3568 scope: ok.test
3569 pop: 1
3570 "#,
3571 );
3572 }
3573
3574 #[test]
3586 fn branch_point_fail_retry_applies_meta_scope_to_trigger() {
3587 expect_scope_stacks(
3591 "(x",
3592 &["<meta.group.test>, <punctuation.test>"],
3593 r#"
3594 name: Branch Meta Scope Test
3595 scope: source.test
3596 contexts:
3597 main:
3598 - match: ''
3599 push: trigger
3600 trigger:
3601 - match: \(
3602 scope: punctuation.test
3603 branch_point: g
3604 branch:
3605 - alt-fails
3606 - alt-succeeds
3607 pop: 1
3608 alt-fails:
3609 - meta_scope: meta.group.test
3610 - match: (?=\S)
3611 fail: g
3612 alt-succeeds:
3613 - meta_scope: meta.group.test
3614 - match: \S+
3615 scope: ok.test
3616 pop: 1
3617 "#,
3618 );
3619 }
3620
3621 #[test]
3631 fn branch_point_with_all_alternatives_failing_unwinds_state() {
3632 let syntax = SyntaxDefinition::load_from_str(
3633 r#"
3634 name: All Alternatives Fail Test
3635 scope: source.test
3636 contexts:
3637 main:
3638 - match: (?=\{)
3639 branch_point: brace
3640 branch:
3641 - brace-strict
3642 - brace-numeric
3643 - match: \w+
3644 scope: plain.test
3645 brace-strict:
3646 - meta_scope: meta.interpolation.brace.test
3647 - match: \{
3648 scope: punctuation.begin.test
3649 push: brace-strict-body
3650 brace-strict-body:
3651 - meta_content_scope: inside-strict.test
3652 - match: foo
3653 scope: keyword.test
3654 - match: \}
3655 scope: punctuation.end.test
3656 pop: 2
3657 - match: (?=\S)
3658 fail: brace
3659 brace-numeric:
3660 - meta_scope: meta.interpolation.brace.test
3661 - match: \{
3662 scope: punctuation.begin.test
3663 push: brace-numeric-body
3664 brace-numeric-body:
3665 - meta_content_scope: inside-numeric.test
3666 - match: \d+
3667 scope: constant.numeric.test
3668 - match: \}
3669 scope: punctuation.end.test
3670 pop: 2
3671 - match: (?=\S)
3672 fail: brace
3673 "#,
3674 true,
3675 None,
3676 )
3677 .unwrap();
3678
3679 let syntax_set = link(syntax);
3680 let mut state = ParseState::new(&syntax_set.syntaxes()[0]);
3681 let o = ops(&mut state, "{no}\n", &syntax_set);
3685 let mut stack = ScopeStack::new();
3686 for (_, op) in &o {
3687 stack.apply(op).unwrap();
3688 }
3689 let final_scopes: Vec<String> = stack
3690 .as_slice()
3691 .iter()
3692 .map(|s| format!("{:?}", s))
3693 .collect();
3694 assert!(
3695 !final_scopes
3696 .iter()
3697 .any(|s| s.contains("meta.interpolation.brace")),
3698 "meta.interpolation.brace leaked past end of line; stack: {:?}",
3699 final_scopes
3700 );
3701 assert!(
3702 !final_scopes
3703 .iter()
3704 .any(|s| s.contains("inside-strict") || s.contains("inside-numeric")),
3705 "inside-* meta_content_scope leaked past end of line; stack: {:?}",
3706 final_scopes
3707 );
3708 }
3709
3710 #[test]
3736 fn cross_line_branch_exhaustion_unwinds_state() {
3737 let syntax_str = r#"
3738name: CrossLineExhaustion
3739scope: source.cle
3740contexts:
3741 main:
3742 - match: 'TRY'
3743 scope: trigger.cle
3744 branch_point: bp
3745 branch: [alpha, beta]
3746 - match: '\w+'
3747 scope: main.word.cle
3748 alpha:
3749 - meta_scope: meta.alpha.cle
3750 - match: '\n'
3751 - match: 'FAIL'
3752 fail: bp
3753 - match: '\w+'
3754 scope: alpha.word.cle
3755 beta:
3756 - meta_scope: meta.beta.cle
3757 - match: '\n'
3758 - match: 'FAIL'
3759 fail: bp
3760 - match: '\w+'
3761 scope: beta.word.cle
3762"#;
3763 let syntax = SyntaxDefinition::load_from_str(syntax_str, true, None).unwrap();
3764 let ss = link(syntax);
3765 let mut state = ParseState::new(&ss.syntaxes()[0]);
3766
3767 let _out1 = state.parse_line("TRY\n", &ss).expect("parse line 1");
3770
3771 let out2 = state.parse_line("FAIL\n", &ss).expect("parse line 2");
3777
3778 assert!(
3780 !state.is_speculative(),
3781 "cross-line exhaustion must drop all branch_point records"
3782 );
3783
3784 assert!(
3787 !out2.replayed.is_empty(),
3788 "cross-line exhaustion must emit replayed ops for the pre-branch state"
3789 );
3790
3791 let out3 = state.parse_line("benign\n", &ss).expect("parse line 3");
3796 let pushed: Vec<String> = out3
3797 .ops
3798 .iter()
3799 .filter_map(|(_, op)| match op {
3800 ScopeStackOp::Push(s) => Some(format!("{:?}", s)),
3801 _ => None,
3802 })
3803 .collect();
3804
3805 assert!(
3806 pushed.iter().any(|s| s.contains("main.word.cle")),
3807 "post-exhaustion line must be scoped under main; got pushes: {:?}",
3808 pushed
3809 );
3810 for leaked in [
3811 "meta.alpha.cle",
3812 "meta.beta.cle",
3813 "alpha.word.cle",
3814 "beta.word.cle",
3815 ] {
3816 assert!(
3817 !pushed.iter().any(|s| s.contains(leaked)),
3818 "{} leaked into post-exhaustion line; got pushes: {:?}",
3819 leaked,
3820 pushed
3821 );
3822 }
3823 }
3824
3825 #[test]
3838 #[ignore = "requires testdata/Packages submodule"]
3839 fn cross_line_fail_with_nested_branch_does_not_panic() {
3840 use crate::parsing::SyntaxSet;
3841 use std::panic::AssertUnwindSafe;
3842 let ss = SyntaxSet::load_from_folder("testdata/Packages").unwrap();
3843 let syntax = ss
3844 .find_syntax_by_path("Packages/JavaScript/JavaScript.sublime-syntax")
3845 .unwrap();
3846 let path = "testdata/Packages/JavaScript/tests/syntax_test_js.js";
3847 let content = std::fs::read_to_string(path).unwrap();
3848 let mut state = ParseState::new(syntax);
3849 let result = std::panic::catch_unwind(AssertUnwindSafe(|| {
3853 for line in content.lines() {
3854 let mut s = line.to_string();
3855 s.push('\n');
3856 let _ = state.parse_line(&s, &ss);
3857 }
3858 }));
3859 if let Err(payload) = result {
3860 let msg = if let Some(s) = payload.downcast_ref::<&'static str>() {
3863 (*s).to_string()
3864 } else if let Some(s) = payload.downcast_ref::<String>() {
3865 s.clone()
3866 } else {
3867 String::from("<non-string panic payload>")
3868 };
3869 assert!(
3870 !msg.contains("index out of bounds"),
3871 "parser panicked with bounds violation (Category E \
3872 regression): {msg}"
3873 );
3874 }
3877 }
3878
3879 #[test]
3888 fn pop_n_unwinds_all_n_contexts_meta_scopes() {
3889 let syntax = SyntaxDefinition::load_from_str(
3890 r#"
3891 name: Pop N Test
3892 scope: source.test
3893 contexts:
3894 main:
3895 - match: \(
3896 scope: open
3897 push: [outer, inner]
3898 outer:
3899 - meta_scope: outer.test
3900 inner:
3901 - meta_content_scope: inner.test
3902 - match: \)
3903 scope: close
3904 pop: 2
3905 "#,
3906 true,
3907 None,
3908 )
3909 .unwrap();
3910
3911 let syntax_set = link(syntax);
3912 let mut state = ParseState::new(&syntax_set.syntaxes()[0]);
3913 let o = ops(&mut state, "(x)\n", &syntax_set);
3914 let mut stack = ScopeStack::new();
3915 for (_, op) in &o {
3916 stack.apply(op).unwrap();
3917 }
3918 let final_scopes: Vec<String> = stack
3919 .as_slice()
3920 .iter()
3921 .map(|s| format!("{:?}", s))
3922 .collect();
3923 assert!(
3924 !final_scopes.iter().any(|s| s.contains("outer.test")),
3925 "outer.test meta_scope leaked past pop: 2; final stack: {:?}",
3926 final_scopes
3927 );
3928 assert!(
3929 !final_scopes.iter().any(|s| s.contains("inner.test")),
3930 "inner.test meta_content_scope leaked past pop: 2; final stack: {:?}",
3931 final_scopes
3932 );
3933 }
3934
3935 #[test]
3944 #[ignore = "requires testdata/Packages submodule"]
3945 fn makefile_meta_string_does_not_leak_past_eol() {
3946 use crate::parsing::SyntaxSet;
3947 let ss = SyntaxSet::load_from_folder("testdata/Packages").unwrap();
3948 let syntax = ss
3949 .find_syntax_by_path("Packages/Makefile/Makefile.sublime-syntax")
3950 .unwrap();
3951 let mut state = ParseState::new(syntax);
3952 let mut stack = ScopeStack::new();
3953 for (_, op) in ops(&mut state, "bar := $(foo)\n", &ss) {
3954 stack.apply(&op).unwrap();
3955 }
3956 let after_assignment: Vec<String> = stack
3957 .as_slice()
3958 .iter()
3959 .map(|s| format!("{:?}", s))
3960 .collect();
3961 assert!(
3962 !after_assignment
3963 .iter()
3964 .any(|s| s.contains("meta.string.makefile")),
3965 "meta.string.makefile leaks past EOL of `bar := $(foo)\\n`; stack: {:?}",
3966 after_assignment
3967 );
3968 }
3969
3970 #[test]
3982 fn chained_set_with_included_eol_popper_pops_at_line_boundary() {
3983 let syntax = SyntaxDefinition::load_from_str(
3984 r#"
3985 name: EOL Pop Chained Test
3986 scope: source.test
3987 contexts:
3988 main:
3989 - match: (?=\S)
3990 push: outer
3991 outer:
3992 - match: ''
3993 set: [value-body, eat-whitespace-then-pop]
3994 eat-whitespace-then-pop:
3995 - match: \s*
3996 pop: 1
3997 value-body:
3998 - match: ''
3999 set: value-content
4000 value-content:
4001 - meta_content_scope: meta.string.test
4002 - include: pop-on-eol
4003 pop-on-eol:
4004 - match: $
4005 pop: 1
4006 "#,
4007 true,
4008 None,
4009 )
4010 .unwrap();
4011
4012 let syntax_set = link(syntax);
4013 let mut state = ParseState::new(&syntax_set.syntaxes()[0]);
4014 let o = ops(&mut state, "bar\n", &syntax_set);
4015
4016 let mut stack = ScopeStack::new();
4020 for (_, op) in &o {
4021 stack.apply(op).unwrap();
4022 }
4023 let final_scopes: Vec<String> = stack
4024 .as_slice()
4025 .iter()
4026 .map(|s| format!("{:?}", s))
4027 .collect();
4028 assert!(
4029 !final_scopes.iter().any(|s| s.contains("meta.string.test")),
4030 "meta.string.test leaked past EOL; final scope stack: {:?}",
4031 final_scopes
4032 );
4033 }
4034
4035 fn expect_scope_stacks(line_without_newline: &str, expect: &[&str], syntax: &str) {
4036 println!("Parsing with newlines");
4037 let line_with_newline = format!("{}\n", line_without_newline);
4038 let syntax_newlines = SyntaxDefinition::load_from_str(syntax, true, None).unwrap();
4039 expect_scope_stacks_with_syntax(&line_with_newline, expect, syntax_newlines);
4040
4041 println!("Parsing without newlines");
4042 let syntax_nonewlines = SyntaxDefinition::load_from_str(syntax, false, None).unwrap();
4043 expect_scope_stacks_with_syntax(line_without_newline, expect, syntax_nonewlines);
4044 }
4045
4046 fn expect_scope_stacks_with_syntax(line: &str, expect: &[&str], syntax: SyntaxDefinition) {
4047 let syntax_set = link(syntax);
4050 let mut state = ParseState::new(&syntax_set.syntaxes()[0]);
4051 let ops = ops(&mut state, line, &syntax_set);
4052 expect_scope_stacks_for_ops(ops, expect);
4053 }
4054
4055 fn expect_scope_stacks_for_ops(ops: Vec<(usize, ScopeStackOp)>, expect: &[&str]) {
4056 let mut criteria_met = Vec::new();
4057 for stack_str in stack_states(ops) {
4058 println!("{}", stack_str);
4059 for expectation in expect.iter() {
4060 if stack_str.contains(expectation) {
4061 criteria_met.push(expectation);
4062 }
4063 }
4064 }
4065 if let Some(missing) = expect.iter().find(|e| !criteria_met.contains(e)) {
4066 panic!("expected scope stack '{}' missing", missing);
4067 }
4068 }
4069
4070 fn parse(line: &str, syntax: &str) -> Vec<(usize, ScopeStackOp)> {
4071 let syntax = SyntaxDefinition::load_from_str(syntax, true, None).unwrap();
4072 let syntax_set = link(syntax);
4073
4074 let mut state = ParseState::new(&syntax_set.syntaxes()[0]);
4075 ops(&mut state, line, &syntax_set)
4076 }
4077
4078 fn link(syntax: SyntaxDefinition) -> SyntaxSet {
4079 let mut builder = SyntaxSetBuilder::new();
4080 builder.add(syntax);
4081 builder.build()
4082 }
4083
4084 fn ops(
4085 state: &mut ParseState,
4086 line: &str,
4087 syntax_set: &SyntaxSet,
4088 ) -> Vec<(usize, ScopeStackOp)> {
4089 let output = state.parse_line(line, syntax_set).expect("#[cfg(test)]");
4090 debug_print_ops(line, &output.ops);
4091 output.ops
4092 }
4093
4094 fn stack_states(ops: Vec<(usize, ScopeStackOp)>) -> Vec<String> {
4095 let mut states = Vec::new();
4096 let mut stack = ScopeStack::new();
4097 for (_, op) in ops.iter() {
4098 stack.apply(op).expect("#[cfg(test)]");
4099 let scopes: Vec<String> = stack
4100 .as_slice()
4101 .iter()
4102 .map(|s| format!("{:?}", s))
4103 .collect();
4104 let stack_str = scopes.join(", ");
4105 states.push(stack_str);
4106 }
4107 states
4108 }
4109
4110 const BRANCH_SYNTAX: &str = r#"
4111scope: source.branch-test
4112contexts:
4113 main:
4114 - match: '(?=\S)'
4115 branch_point: stmt
4116 branch: [let-stmt, generic-stmt]
4117
4118 let-stmt:
4119 - match: 'let'
4120 scope: keyword.declaration.branch-test
4121 set: let-assign
4122 - match: '(?=\S)'
4123 fail: stmt
4124
4125 let-assign:
4126 - match: '='
4127 scope: keyword.operator.assignment.branch-test
4128 set: let-value
4129 - match: '(?=\S)'
4130 fail: stmt
4131
4132 let-value:
4133 - match: '\w+'
4134 scope: constant.other.branch-test
4135 - match: ';'
4136 scope: punctuation.terminator.branch-test
4137 pop: true
4138
4139 generic-stmt:
4140 - match: '[^;]+'
4141 scope: string.unquoted.branch-test
4142 - match: ';'
4143 scope: punctuation.terminator.branch-test
4144 pop: true
4145"#;
4146
4147 #[test]
4148 fn branch_first_alternative_succeeds() {
4149 let syntax = SyntaxDefinition::load_from_str(BRANCH_SYNTAX, true, None).unwrap();
4151 let ss = link(syntax);
4152 let mut state = ParseState::new(&ss.syntaxes()[0]);
4153 let ops = ops(&mut state, "let = foo;", &ss);
4154 let states = stack_states(ops);
4155 assert!(
4157 states.iter().any(|s| s.contains("keyword.declaration")),
4158 "Expected keyword.declaration scope, got: {:?}",
4159 states
4160 );
4161 assert!(
4162 states
4163 .iter()
4164 .any(|s| s.contains("keyword.operator.assignment")),
4165 "Expected keyword.operator.assignment scope, got: {:?}",
4166 states
4167 );
4168 assert!(
4169 states.iter().any(|s| s.contains("constant.other")),
4170 "Expected constant.other scope, got: {:?}",
4171 states
4172 );
4173 }
4174
4175 #[test]
4176 fn branch_fail_backtracks_to_second_alternative() {
4177 let syntax = SyntaxDefinition::load_from_str(BRANCH_SYNTAX, true, None).unwrap();
4179 let ss = link(syntax);
4180 let mut state = ParseState::new(&ss.syntaxes()[0]);
4181 let ops = ops(&mut state, "hello;", &ss);
4182 let states = stack_states(ops);
4183 assert!(
4185 states.iter().any(|s| s.contains("string.unquoted")),
4186 "Expected string.unquoted scope, got: {:?}",
4187 states
4188 );
4189 assert!(
4190 !states.iter().any(|s| s.contains("keyword.declaration")),
4191 "Should NOT contain keyword.declaration scope, got: {:?}",
4192 states
4193 );
4194 }
4195
4196 #[test]
4197 fn branch_fail_after_partial_match() {
4198 let syntax = SyntaxDefinition::load_from_str(BRANCH_SYNTAX, true, None).unwrap();
4200 let ss = link(syntax);
4201 let mut state = ParseState::new(&ss.syntaxes()[0]);
4202 let raw_ops = ops(&mut state, "let hello;", &ss);
4203
4204 let states = stack_states(raw_ops.clone());
4206 assert!(
4207 !states.iter().any(|s| s.contains("keyword.declaration")),
4208 "keyword.declaration should be absent after backtrack, got: {:?}",
4209 states
4210 );
4211
4212 assert!(
4214 states.iter().any(|s| s.contains("string.unquoted")),
4215 "Expected string.unquoted scope after backtrack, got: {:?}",
4216 states
4217 );
4218
4219 let unquoted_pos = raw_ops.iter().find_map(|(pos, op)| match op {
4221 ScopeStackOp::Push(s) if format!("{:?}", s).contains("string.unquoted") => Some(*pos),
4222 _ => None,
4223 });
4224 assert_eq!(
4225 unquoted_pos,
4226 Some(0),
4227 "string.unquoted should start at position 0 after rewind, got: {:?}",
4228 unquoted_pos
4229 );
4230 }
4231
4232 #[test]
4233 fn branch_all_alternatives_exhausted() {
4234 let syntax_str = r#"
4236scope: source.exhaust-test
4237contexts:
4238 main:
4239 - match: '(?=\S)'
4240 branch_point: bp
4241 branch: [alt-a, alt-b]
4242 - match: '\S+'
4243 scope: fallback.exhaust-test
4244
4245 alt-a:
4246 - match: 'AAA'
4247 scope: alt-a.exhaust-test
4248 pop: true
4249 - match: '(?=\S)'
4250 fail: bp
4251
4252 alt-b:
4253 - match: 'BBB'
4254 scope: alt-b.exhaust-test
4255 pop: true
4256 - match: '(?=\S)'
4257 fail: bp
4258"#;
4259 let syntax = SyntaxDefinition::load_from_str(syntax_str, true, None).unwrap();
4260 let ss = link(syntax);
4261 let mut state = ParseState::new(&ss.syntaxes()[0]);
4262 let ops = ops(&mut state, "xyz", &ss);
4264 assert!(!ops.is_empty(), "Expected some ops, got empty");
4266 }
4267
4268 #[test]
4269 fn branch_fail_emits_meta_content_scope() {
4270 let syntax_str = r#"
4273scope: source.meta-test
4274contexts:
4275 main:
4276 - match: '(?=\S)'
4277 branch_point: bp
4278 branch: [try-special, fallback-ctx]
4279
4280 try-special:
4281 - match: 'SPECIAL'
4282 scope: keyword.meta-test
4283 pop: true
4284 - match: '(?=\S)'
4285 fail: bp
4286
4287 fallback-ctx:
4288 - meta_content_scope: meta.fallback.meta-test
4289 - match: '\w+'
4290 scope: variable.meta-test
4291 pop: true
4292"#;
4293 let syntax = SyntaxDefinition::load_from_str(syntax_str, true, None).unwrap();
4294 let ss = link(syntax);
4295 let mut state = ParseState::new(&ss.syntaxes()[0]);
4296 let ops = ops(&mut state, "hello", &ss);
4297 let states = stack_states(ops);
4298 assert!(
4300 states.iter().any(|s| s.contains("meta.fallback")),
4301 "Expected meta.fallback.meta-test scope after backtrack, got: {:?}",
4302 states
4303 );
4304 assert!(
4305 states.iter().any(|s| s.contains("variable.meta-test")),
4306 "Expected variable.meta-test scope, got: {:?}",
4307 states
4308 );
4309 }
4310
4311 #[test]
4312 fn branch_fail_applies_with_prototype() {
4313 let syntax_str = r#"
4316scope: source.proto-test
4317contexts:
4318 main:
4319 - match: '(?=\S)'
4320 branch_point: bp
4321 branch: [try-num, fallback-word]
4322 with_prototype:
4323 - match: '#'
4324 scope: comment.proto-test
4325 pop: true
4326
4327 try-num:
4328 - match: '\d+'
4329 scope: constant.numeric.proto-test
4330 pop: true
4331 - match: '(?=\S)'
4332 fail: bp
4333
4334 fallback-word:
4335 - match: '\w+'
4336 scope: variable.proto-test
4337 - match: ';'
4338 pop: true
4339"#;
4340 let syntax = SyntaxDefinition::load_from_str(syntax_str, true, None).unwrap();
4341 let ss = link(syntax);
4342 let mut state = ParseState::new(&ss.syntaxes()[0]);
4343 let ops = ops(&mut state, "abc#", &ss);
4345 let states = stack_states(ops);
4346 assert!(
4347 states.iter().any(|s| s.contains("variable.proto-test")),
4348 "Expected variable.proto-test scope, got: {:?}",
4349 states
4350 );
4351 assert!(
4352 states.iter().any(|s| s.contains("comment.proto-test")),
4353 "Expected comment.proto-test from with_prototype after backtrack, got: {:?}",
4354 states
4355 );
4356 }
4357
4358 #[test]
4359 fn branch_cross_line_backtrack() {
4360 let syntax_str = r#"
4367name: CrossLineTest
4368scope: source.clt
4369contexts:
4370 main:
4371 - match: 'TRY'
4372 branch_point: bp
4373 branch: [try-ctx, fallback-ctx]
4374 - match: '.*'
4375 scope: main.other
4376 try-ctx:
4377 - match: '\n'
4378 # consume newline, stay in context for the next line
4379 - match: 'FAIL'
4380 fail: bp
4381 - match: '\w+'
4382 scope: try.word
4383 pop: true
4384 fallback-ctx:
4385 - match: '.*'
4386 scope: fallback.content
4387 pop: true
4388"#;
4389 let syntax = SyntaxDefinition::load_from_str(syntax_str, true, None).unwrap();
4390 let ss = link(syntax);
4391 let mut state = ParseState::new(&ss.syntaxes()[0]);
4392
4393 let out1 = state.parse_line("TRY\n", &ss).expect("parse line 1 failed");
4396 assert!(
4398 out1.replayed.is_empty(),
4399 "line 1: expected no replayed ops, got {:?}",
4400 out1.replayed
4401 );
4402
4403 let out2 = state
4406 .parse_line("FAIL\n", &ss)
4407 .expect("parse line 2 failed");
4408 assert_eq!(
4409 out2.replayed.len(),
4410 1,
4411 "expected exactly one replayed line, got {:?}",
4412 out2.replayed
4413 );
4414 let has_fallback = out2.replayed[0].iter().any(|(_, op)| {
4415 matches!(op, ScopeStackOp::Push(s) if format!("{:?}", s).contains("fallback.content"))
4416 });
4417 assert!(
4418 has_fallback,
4419 "expected fallback.content scope in replayed line 1 ops, got: {:?}",
4420 out2.replayed[0]
4421 );
4422 let has_try_word = out2.replayed[0].iter().any(|(_, op)| {
4424 matches!(op, ScopeStackOp::Push(s) if format!("{:?}", s).contains("try.word"))
4425 });
4426 assert!(
4427 !has_try_word,
4428 "try.word must not appear in replayed ops after backtrack, got: {:?}",
4429 out2.replayed[0]
4430 );
4431 let current_has_try = out2.ops.iter().any(
4433 |(_, op)| matches!(op, ScopeStackOp::Push(s) if format!("{:?}", s).contains("try")),
4434 );
4435 assert!(
4436 !current_has_try,
4437 "current-line ops should not contain try.* scopes after cross-line fail, got: {:?}",
4438 out2.ops
4439 );
4440 }
4441
4442 #[test]
4443 fn cross_line_fail_preserves_pre_branch_prefix_ops() {
4444 let syntax_str = r#"
4457name: CrossLinePrefix
4458scope: source.clp
4459contexts:
4460 main:
4461 - match: 'prefix'
4462 scope: prefix.word.clp
4463 - match: 'TRY'
4464 branch_point: bp
4465 branch: [try-ctx, fallback-ctx]
4466 - match: '\s+'
4467 try-ctx:
4468 - match: 'END'
4469 pop: true
4470 - match: 'FAIL'
4471 fail: bp
4472 - match: '\w+'
4473 scope: try.word.clp
4474 - match: '\s+'
4475 fallback-ctx:
4476 - match: 'END'
4477 pop: true
4478 - match: '\w+'
4479 scope: fallback.content.clp
4480 - match: '\s+'
4481"#;
4482 let syntax = SyntaxDefinition::load_from_str(syntax_str, true, None).unwrap();
4483 let ss = link(syntax);
4484 let mut state = ParseState::new(&ss.syntaxes()[0]);
4485
4486 let _out1 = state
4489 .parse_line("prefix TRY post\n", &ss)
4490 .expect("parse line 1 failed");
4491
4492 let out2 = state
4494 .parse_line("FAIL\n", &ss)
4495 .expect("parse line 2 failed");
4496 assert_eq!(
4497 out2.replayed.len(),
4498 1,
4499 "expected one replayed line, got {:?}",
4500 out2.replayed
4501 );
4502 let replayed_has_prefix = out2.replayed[0].iter().any(|(_, op)| {
4506 matches!(op, ScopeStackOp::Push(s) if format!("{:?}", s).contains("prefix.word"))
4507 });
4508 assert!(
4509 replayed_has_prefix,
4510 "replayed line must preserve prefix.word from pre-branch parse, got: {:?}",
4511 out2.replayed[0]
4512 );
4513 let replayed_has_fallback = out2.replayed[0].iter().any(|(_, op)| {
4515 matches!(op, ScopeStackOp::Push(s) if format!("{:?}", s).contains("fallback.content"))
4516 });
4517 assert!(
4518 replayed_has_fallback,
4519 "replayed line must apply fallback.content for post-branch remainder, got: {:?}",
4520 out2.replayed[0]
4521 );
4522 }
4523
4524 #[test]
4525 fn branch_point_expiry_after_128_lines() {
4526 let syntax_str = r#"
4529name: ExpiryTest
4530scope: source.expiry-test
4531contexts:
4532 main:
4533 - match: 'START'
4534 branch_point: bp
4535 branch: [try-ctx, fallback-ctx]
4536 - match: '.*'
4537 scope: filler.expiry-test
4538 try-ctx:
4539 - match: '\n'
4540 # consume newlines, staying in context
4541 - match: 'FAIL'
4542 fail: bp
4543 - match: '\w+'
4544 scope: try.matched
4545 pop: true
4546 fallback-ctx:
4547 - match: '.*'
4548 scope: fallback.content
4549 pop: true
4550"#;
4551 let syntax = SyntaxDefinition::load_from_str(syntax_str, true, None).unwrap();
4552 let ss = link(syntax);
4553 let mut state = ParseState::new(&ss.syntaxes()[0]);
4554
4555 let out0 = state.parse_line("START\n", &ss).expect("parse START");
4556 assert!(out0.replayed.is_empty());
4557
4558 let mut all_warnings: Vec<String> = Vec::new();
4561 for _ in 0..129 {
4562 let out = state.parse_line("\n", &ss).expect("parse filler");
4563 all_warnings.extend(out.warnings);
4564 }
4565
4566 let out_fail = state.parse_line("FAIL\n", &ss).expect("parse FAIL");
4568 all_warnings.extend(out_fail.warnings);
4569 assert!(
4570 out_fail.replayed.is_empty(),
4571 "branch point should have expired, but got replayed ops: {:?}",
4572 out_fail.replayed
4573 );
4574 assert!(
4575 all_warnings
4576 .iter()
4577 .any(|w| w.contains("expired") && w.contains("bp")),
4578 "expected a warning about branch point expiry, got: {:?}",
4579 all_warnings
4580 );
4581 }
4582
4583 #[test]
4584 fn branch_point_still_valid_at_128_lines() {
4585 let syntax_str = r#"
4588name: ExpiryTest
4589scope: source.expiry-test
4590contexts:
4591 main:
4592 - match: 'START'
4593 branch_point: bp
4594 branch: [try-ctx, fallback-ctx]
4595 - match: '.*'
4596 scope: filler.expiry-test
4597 try-ctx:
4598 - match: '\n'
4599 # consume newlines, staying in context
4600 - match: 'FAIL'
4601 fail: bp
4602 - match: '\w+'
4603 scope: try.matched
4604 pop: true
4605 fallback-ctx:
4606 - match: '.*'
4607 scope: fallback.content
4608 pop: true
4609"#;
4610 let syntax = SyntaxDefinition::load_from_str(syntax_str, true, None).unwrap();
4611 let ss = link(syntax);
4612 let mut state = ParseState::new(&ss.syntaxes()[0]);
4613
4614 let out0 = state.parse_line("START\n", &ss).expect("parse START");
4615 assert!(out0.replayed.is_empty());
4616
4617 let mut all_warnings: Vec<String> = Vec::new();
4620 for _ in 0..127 {
4621 let out = state.parse_line("\n", &ss).expect("parse filler");
4622 all_warnings.extend(out.warnings);
4623 }
4624
4625 let out_fail = state.parse_line("FAIL\n", &ss).expect("parse FAIL");
4627 all_warnings.extend(out_fail.warnings);
4628 assert!(
4629 !out_fail.replayed.is_empty(),
4630 "branch point should still be valid at exactly 128 lines, but got no replayed ops"
4631 );
4632 assert!(
4633 all_warnings.is_empty(),
4634 "expected no warnings at the 128-line boundary, got: {:?}",
4635 all_warnings
4636 );
4637 let has_fallback = out_fail.replayed[0].iter().any(|(_, op)| {
4638 matches!(op, ScopeStackOp::Push(s) if format!("{:?}", s).contains("fallback.content"))
4639 });
4640 assert!(
4641 has_fallback,
4642 "expected fallback.content in replayed ops, got: {:?}",
4643 out_fail.replayed[0]
4644 );
4645 }
4646
4647 #[test]
4648 fn branch_stack_depth_invalidation() {
4649 let syntax_str = r#"
4653name: DepthTest
4654scope: source.depth-test
4655contexts:
4656 main:
4657 - match: '(?=\S)'
4658 branch_point: bp
4659 branch: [try-ctx, fallback-ctx]
4660 try-ctx:
4661 - match: 'OK'
4662 scope: try.ok
4663 set: post-try
4664 - match: '(?=\S)'
4665 fail: bp
4666 post-try:
4667 - match: 'FAIL'
4668 fail: bp
4669 - match: '\w+'
4670 scope: post.word
4671 pop: true
4672 fallback-ctx:
4673 - match: '.*'
4674 scope: fallback.content
4675 pop: true
4676"#;
4677 let syntax = SyntaxDefinition::load_from_str(syntax_str, true, None).unwrap();
4678 let ss = link(syntax);
4679
4680 let mut state = ParseState::new(&ss.syntaxes()[0]);
4684 let line_ops = ops(&mut state, "OK FAIL\n", &ss);
4685 let states = stack_states(line_ops);
4686 assert!(
4687 states.iter().any(|s| s.contains("fallback.content")),
4688 "fail at equal depth should trigger backtrack to fallback, got: {:?}",
4689 states
4690 );
4691 assert!(
4692 !states.iter().any(|s| s.contains("try.ok")),
4693 "try.ok should be absent after backtrack, got: {:?}",
4694 states
4695 );
4696
4697 let syntax_str2 = r#"
4700name: DepthTest2
4701scope: source.depth-test2
4702contexts:
4703 main:
4704 - match: 'GO'
4705 branch_point: bp
4706 branch: [try-ctx2, fallback-ctx2]
4707 - match: 'FAIL'
4708 fail: bp
4709 - match: '.*'
4710 scope: main.other
4711 try-ctx2:
4712 - match: 'OK'
4713 scope: try.ok2
4714 pop: true
4715 - match: '(?=\S)'
4716 fail: bp
4717 fallback-ctx2:
4718 - match: '.*'
4719 scope: fallback.content2
4720 pop: true
4721"#;
4722 let syntax2 = SyntaxDefinition::load_from_str(syntax_str2, true, None).unwrap();
4723 let ss2 = link(syntax2);
4724 let mut state2 = ParseState::new(&ss2.syntaxes()[0]);
4725 let line_ops2 = ops(&mut state2, "GO OK FAIL\n", &ss2);
4728 let states2 = stack_states(line_ops2);
4729 assert!(
4730 !states2.iter().any(|s| s.contains("fallback.content2")),
4731 "fail should be a no-op when stack is shallower than branch point, got: {:?}",
4732 states2
4733 );
4734 assert!(
4735 states2.iter().any(|s| s.contains("try.ok2")),
4736 "expected try.ok2 from first alternative, got: {:?}",
4737 states2
4738 );
4739 }
4740
4741 #[test]
4742 fn branch_nested_overlapping_branch_points() {
4743 let syntax_str = r#"
4746name: NestedTest
4747scope: source.nested-test
4748contexts:
4749 main:
4750 - match: '(?=\S)'
4751 branch_point: outer
4752 branch: [outer-try, outer-fallback]
4753 outer-try:
4754 - match: 'A'
4755 scope: outer.a
4756 set: inner-branch
4757 - match: '(?=\S)'
4758 fail: outer
4759 inner-branch:
4760 - match: '(?=\S)'
4761 branch_point: inner
4762 branch: [inner-try, inner-fallback]
4763 inner-try:
4764 - match: 'X'
4765 scope: inner.x
4766 pop: true
4767 - match: '(?=\S)'
4768 fail: inner
4769 inner-fallback:
4770 - match: '\w+'
4771 scope: inner.fallback
4772 pop: true
4773 outer-fallback:
4774 - match: '.*'
4775 scope: outer.fallback
4776 pop: true
4777"#;
4778 let syntax = SyntaxDefinition::load_from_str(syntax_str, true, None).unwrap();
4779 let ss = link(syntax);
4780 let mut state = ParseState::new(&ss.syntaxes()[0]);
4781
4782 let line_ops = ops(&mut state, "A B\n", &ss);
4784 let states = stack_states(line_ops);
4785 assert!(
4786 states.iter().any(|s| s.contains("inner.fallback")),
4787 "expected inner.fallback after inner branch fail, got: {:?}",
4788 states
4789 );
4790 assert!(
4791 !states.iter().any(|s| s.contains("outer.fallback")),
4792 "outer branch should not have failed, got: {:?}",
4793 states
4794 );
4795 }
4796
4797 #[test]
4824 fn nested_same_name_branch_point_outer_fail_replays_outer() {
4825 let syntax_str = r#"
4826name: NestedSameNameBranch
4827scope: source.nested-same-name-branch
4828contexts:
4829 main:
4830 - include: brackets
4831
4832 brackets:
4833 - match: '(?=\[)'
4834 branch_point: bp
4835 branch: [list, quasi]
4836
4837 list:
4838 - match: '\['
4839 scope: list.open
4840 set: list-body
4841
4842 list-body:
4843 - meta_scope: list.body
4844 - match: '\|\]'
4845 fail: bp
4846 - match: '\]'
4847 scope: list.close
4848 pop: true
4849 - include: brackets
4850 - match: '\w+'
4851 scope: list.word
4852
4853 quasi:
4854 - match: '\['
4855 scope: quasi.open
4856 set: quasi-body
4857
4858 quasi-body:
4859 - meta_scope: quasi.body
4860 - match: '\|\]'
4861 scope: quasi.close
4862 pop: true
4863 - match: '.'
4864 scope: quasi.char
4865"#;
4866 let syntax = SyntaxDefinition::load_from_str(syntax_str, true, None).unwrap();
4867 let ss = link(syntax);
4868 let mut state = ParseState::new(&ss.syntaxes()[0]);
4869
4870 let line_ops = ops(&mut state, "[x[y]|]\n", &ss);
4878 let states = stack_states(line_ops);
4879 assert!(
4880 states.iter().any(|s| s.contains("quasi.body")),
4881 "expected quasi.body after outer fail replay, got: {:?}",
4882 states
4883 );
4884 assert!(
4885 !states.iter().any(|s| s.contains("list.body")),
4886 "list.body meta_scope leaked past outer fail replay, got: {:?}",
4887 states
4888 );
4889 }
4890
4891 #[test]
4892 fn branch_fail_nonexistent_name() {
4893 let syntax_str = r#"
4895name: NoNameTest
4896scope: source.noname-test
4897contexts:
4898 main:
4899 - match: '\w+'
4900 scope: word.noname-test
4901 - match: '(?=;)'
4902 fail: nonexistent
4903"#;
4904 let syntax = SyntaxDefinition::load_from_str(syntax_str, true, None).unwrap();
4905 let ss = link(syntax);
4906 let mut state = ParseState::new(&ss.syntaxes()[0]);
4907
4908 let line_ops = ops(&mut state, "hello;\n", &ss);
4910 let states = stack_states(line_ops);
4911 assert!(
4912 states.iter().any(|s| s.contains("word.noname-test")),
4913 "expected word.noname-test, got: {:?}",
4914 states
4915 );
4916 }
4917
4918 #[test]
4919 fn branch_cross_line_multi_replay() {
4920 let syntax_str = r#"
4923name: MultiReplayTest
4924scope: source.multi-replay
4925contexts:
4926 main:
4927 - match: 'TRY'
4928 branch_point: bp
4929 branch: [try-ctx, fallback-ctx]
4930 - match: '.*'
4931 scope: main.other
4932 try-ctx:
4933 - match: '\n'
4934 # stay in context
4935 - match: 'FAIL'
4936 fail: bp
4937 - match: '\w+'
4938 scope: try.word
4939 fallback-ctx:
4940 - match: '.*'
4941 scope: fallback.content
4942 pop: true
4943"#;
4944 let syntax = SyntaxDefinition::load_from_str(syntax_str, true, None).unwrap();
4945 let ss = link(syntax);
4946 let mut state = ParseState::new(&ss.syntaxes()[0]);
4947
4948 let out1 = state.parse_line("TRY\n", &ss).expect("line 1");
4949 assert!(out1.replayed.is_empty());
4950
4951 let out2 = state.parse_line("aaa\n", &ss).expect("line 2");
4952 assert!(out2.replayed.is_empty());
4953
4954 let out3 = state.parse_line("bbb\n", &ss).expect("line 3");
4955 assert!(out3.replayed.is_empty());
4956
4957 let out4 = state.parse_line("FAIL\n", &ss).expect("line 4");
4959 assert_eq!(
4960 out4.replayed.len(),
4961 3,
4962 "expected 3 replayed lines (lines 1-3), got {:?}",
4963 out4.replayed
4964 );
4965
4966 let has_fallback = out4.replayed[0].iter().any(|(_, op)| {
4970 matches!(op, ScopeStackOp::Push(s) if format!("{:?}", s).contains("fallback.content"))
4971 });
4972 assert!(
4973 has_fallback,
4974 "replayed line 0 missing fallback.content, got: {:?}",
4975 out4.replayed[0]
4976 );
4977
4978 for (i, line_ops) in out4.replayed.iter().enumerate() {
4980 let has_try_word = line_ops.iter().any(|(_, op)| {
4981 matches!(op, ScopeStackOp::Push(s) if format!("{:?}", s).contains("try.word"))
4982 });
4983 assert!(
4984 !has_try_word,
4985 "replayed line {} should not have try.word, got: {:?}",
4986 i, line_ops
4987 );
4988 }
4989 let current_has_try = out4.ops.iter().any(
4991 |(_, op)| matches!(op, ScopeStackOp::Push(s) if format!("{:?}", s).contains("try")),
4992 );
4993 assert!(
4994 !current_has_try,
4995 "current-line ops should not contain try.* scopes after cross-line fail, got: {:?}",
4996 out4.ops
4997 );
4998 }
4999
5000 #[test]
5001 fn branch_cross_line_fail_with_preceding_ops() {
5002 let syntax_str = r#"
5006name: PrecedingOpsTest
5007scope: source.preceding-ops
5008contexts:
5009 main:
5010 - match: 'TRY'
5011 branch_point: bp
5012 branch: [try-ctx, fallback-ctx]
5013 - match: '.*'
5014 scope: main.other
5015 try-ctx:
5016 - match: '\n'
5017 # stay in context across lines
5018 - match: 'FAIL'
5019 fail: bp
5020 - match: '\w+'
5021 scope: try.word
5022 fallback-ctx:
5023 - match: '.*'
5024 scope: fallback.content
5025 pop: true
5026"#;
5027 let syntax = SyntaxDefinition::load_from_str(syntax_str, true, None).unwrap();
5028 let ss = link(syntax);
5029 let mut state = ParseState::new(&ss.syntaxes()[0]);
5030
5031 let out1 = state.parse_line("TRY\n", &ss).expect("line 1");
5033 assert!(out1.replayed.is_empty());
5034
5035 let out2 = state.parse_line("stuff FAIL\n", &ss).expect("line 2");
5038
5039 assert_eq!(
5041 out2.replayed.len(),
5042 1,
5043 "expected 1 replayed line, got {}",
5044 out2.replayed.len()
5045 );
5046
5047 let replay_has_fallback = out2.replayed[0].iter().any(|(_, op)| {
5049 matches!(op, ScopeStackOp::Push(s) if format!("{:?}", s).contains("fallback.content"))
5050 });
5051 assert!(
5052 replay_has_fallback,
5053 "replayed line should have fallback.content, got: {:?}",
5054 out2.replayed[0]
5055 );
5056
5057 let current_has_try = out2.ops.iter().any(|(_, op)| {
5059 matches!(op, ScopeStackOp::Push(s) if format!("{:?}", s).contains("try.word"))
5060 });
5061 assert!(
5062 !current_has_try,
5063 "current-line ops should not contain try.word after cross-line fail, got: {:?}",
5064 out2.ops
5065 );
5066
5067 let current_has_main = out2.ops.iter().any(|(_, op)| {
5069 matches!(op, ScopeStackOp::Push(s) if format!("{:?}", s).contains("main.other"))
5070 });
5071 assert!(
5072 current_has_main,
5073 "current-line should be re-parsed as main.other from position 0, got: {:?}",
5074 out2.ops
5075 );
5076 }
5077
5078 #[test]
5081 fn is_speculative_reflects_branch_state() {
5082 let syntax_str = r#"
5087name: SpeculativeTest
5088scope: source.spec-test
5089contexts:
5090 main:
5091 - match: '(?=\S)'
5092 branch_point: bp
5093 branch: [alt-a, alt-b]
5094 - match: '\S+'
5095 scope: fallback.spec-test
5096
5097 alt-a:
5098 - match: 'AAA'
5099 scope: alt-a.spec-test
5100 pop: true
5101 - match: '(?=\S)'
5102 fail: bp
5103
5104 alt-b:
5105 - match: 'BBB'
5106 scope: alt-b.spec-test
5107 pop: true
5108 - match: '(?=\S)'
5109 fail: bp
5110"#;
5111 let syntax = SyntaxDefinition::load_from_str(syntax_str, true, None).unwrap();
5112 let ss = link(syntax);
5113 let mut state = ParseState::new(&ss.syntaxes()[0]);
5114
5115 assert!(
5117 !state.is_speculative(),
5118 "should not be speculative before branch_point is created"
5119 );
5120
5121 let _ = state.parse_line("AAA\n", &ss).unwrap();
5123 let mut state2 = ParseState::new(&ss.syntaxes()[0]);
5127 assert!(!state2.is_speculative());
5128
5129 let _ = state2.parse_line("xyz\n", &ss).unwrap();
5131 assert!(
5132 !state2.is_speculative(),
5133 "should not be speculative after all alternatives exhausted"
5134 );
5135
5136 let syntax_str2 = r#"
5138name: SpecCross
5139scope: source.spec-cross
5140contexts:
5141 main:
5142 - match: 'TRY'
5143 branch_point: bp
5144 branch: [try-ctx, fallback-ctx]
5145 - match: '.*'
5146 scope: main.other
5147 try-ctx:
5148 - match: '\n'
5149 - match: 'FAIL'
5150 fail: bp
5151 - match: '\w+'
5152 scope: try.word
5153 pop: true
5154 fallback-ctx:
5155 - match: '.*'
5156 scope: fallback.content
5157 pop: true
5158"#;
5159 let syntax2 = SyntaxDefinition::load_from_str(syntax_str2, true, None).unwrap();
5160 let ss2 = link(syntax2);
5161 let mut state3 = ParseState::new(&ss2.syntaxes()[0]);
5162 assert!(!state3.is_speculative());
5163
5164 let _ = state3.parse_line("TRY\n", &ss2).unwrap();
5165 assert!(
5166 state3.is_speculative(),
5167 "should be speculative after branch_point creation"
5168 );
5169 }
5170
5171 #[test]
5172 fn consuming_match_not_treated_as_loop() {
5173 let syntax_str = r#"
5178name: ConsumingTest
5179scope: source.consuming
5180contexts:
5181 main:
5182 - match: '(?=\S)'
5183 push: inner
5184 - match: '\n'
5185 inner:
5186 - match: '\w+'
5187 scope: word.consuming
5188 pop: true
5189"#;
5190 let syntax = SyntaxDefinition::load_from_str(syntax_str, true, None).unwrap();
5191 let ss = link(syntax);
5192 let mut state = ParseState::new(&ss.syntaxes()[0]);
5193
5194 let raw_ops = ops(&mut state, "hello world\n", &ss);
5200 let first_word_pos = raw_ops
5201 .iter()
5202 .find_map(|(pos, op)| match op {
5203 ScopeStackOp::Push(s) if format!("{:?}", s).contains("word.consuming") => {
5204 Some(*pos)
5205 }
5206 _ => None,
5207 })
5208 .expect("expected at least one word.consuming push");
5209 assert_eq!(
5210 first_word_pos, 0,
5211 "word.consuming must start at position 0 (consuming pop should not be treated as loop)"
5212 );
5213 }
5214
5215 #[test]
5216 fn capture_sort_by_span_length() {
5217 let syntax_str = r#"
5222name: CaptureSort
5223scope: source.capsort
5224contexts:
5225 main:
5226 - match: '((a)(b))'
5227 captures:
5228 1: outer.capsort
5229 2: inner-a.capsort
5230 3: inner-b.capsort
5231"#;
5232 let syntax = SyntaxDefinition::load_from_str(syntax_str, true, None).unwrap();
5233 let ss = link(syntax);
5234 let mut state = ParseState::new(&ss.syntaxes()[0]);
5235 let raw_ops = ops(&mut state, "ab\n", &ss);
5236
5237 let push_order: Vec<&str> = raw_ops
5241 .iter()
5242 .filter_map(|(_, op)| match op {
5243 ScopeStackOp::Push(s) => {
5244 let name = format!("{:?}", s);
5245 if name.contains("outer.capsort") {
5246 Some("outer")
5247 } else if name.contains("inner-a.capsort") {
5248 Some("inner-a")
5249 } else if name.contains("inner-b.capsort") {
5250 Some("inner-b")
5251 } else {
5252 None
5253 }
5254 }
5255 _ => None,
5256 })
5257 .collect();
5258 assert_eq!(
5259 push_order,
5260 vec!["outer", "inner-a", "inner-b"],
5261 "captures must push in longest-span-first order, got: {:?}",
5262 push_order
5263 );
5264 }
5265
5266 #[test]
5267 fn v2_set_pops_meta_content_scope_from_matched_text() {
5268 let syntax_str = r#"
5272name: V2SetMeta
5273scope: source.v2setmeta
5274version: 2
5275contexts:
5276 main:
5277 - match: '(?=\S)'
5278 push: ctx-a
5279 ctx-a:
5280 - meta_scope: meta.a.v2setmeta
5281 - match: 'GO'
5282 scope: keyword.go.v2setmeta
5283 set: ctx-b
5284 - match: '\w+'
5285 scope: word.a.v2setmeta
5286 ctx-b:
5287 - meta_scope: meta.b.v2setmeta
5288 - match: '\w+'
5289 scope: word.b.v2setmeta
5290 pop: true
5291"#;
5292 let syntax = SyntaxDefinition::load_from_str(syntax_str, true, None).unwrap();
5293 let ss = link(syntax);
5294 let mut state = ParseState::new(&ss.syntaxes()[0]);
5295 let raw_ops = ops(&mut state, "GO hello\n", &ss);
5296 let states = stack_states(raw_ops);
5297
5298 let last_state = states.last().expect("expected some states");
5302 assert!(
5303 !last_state.contains("meta.a.v2setmeta"),
5304 "meta.a should have been popped after set, got: {:?}",
5305 last_state
5306 );
5307 }
5308
5309 #[test]
5310 fn v2_set_from_context_with_clear_scopes_restores_cleared_atoms() {
5311 let syntax_str = r#"
5330name: V2SetRestore
5331scope: source.v2setrestore
5332version: 2
5333contexts:
5334 main:
5335 - meta_scope: meta.outer.v2setrestore
5336 - match: '\{'
5337 push: value-body
5338
5339 value-body:
5340 - clear_scopes: 1
5341 - meta_scope: meta.value.v2setrestore
5342 - match: 'x'
5343 scope: keyword.x.v2setrestore
5344 set: follow-up
5345
5346 follow-up:
5347 - match: '\w+'
5348 scope: word.follow.v2setrestore
5349"#;
5350 let syntax = SyntaxDefinition::load_from_str(syntax_str, true, None).unwrap();
5351 let ss = link(syntax);
5352 let mut state = ParseState::new(&ss.syntaxes()[0]);
5353 let raw_ops = ops(&mut state, "{xhello\n", &ss);
5354
5355 let states = stack_states(raw_ops);
5356 let follow_states: Vec<_> = states
5358 .iter()
5359 .filter(|s| s.contains("word.follow.v2setrestore"))
5360 .collect();
5361 assert!(
5362 !follow_states.is_empty(),
5363 "expected to enter follow-up context, got states: {:?}",
5364 states
5365 );
5366 assert!(
5370 follow_states
5371 .iter()
5372 .any(|s| s.contains("meta.outer.v2setrestore")),
5373 "meta.outer must be restored after leaving value-body (which cleared it): {:?}",
5374 follow_states
5375 );
5376 assert!(
5378 !follow_states
5379 .iter()
5380 .any(|s| s.contains("meta.value.v2setrestore")),
5381 "meta.value (from the exited context) must not leak into follow-up: {:?}",
5382 follow_states
5383 );
5384 }
5385
5386 #[test]
5387 fn v2_set_to_target_with_clear_scopes_clears_parent_meta_content_scope() {
5388 let syntax_str = r#"
5402name: V2SetTargetClear
5403scope: source.v2settargetclear
5404version: 2
5405contexts:
5406 main:
5407 - match: '\('
5408 scope: punctuation.section.parens.begin.v2settargetclear
5409 push: [body, params-open]
5410
5411 body:
5412 - meta_content_scope: meta.function.v2settargetclear
5413 - match: '\)'
5414 scope: punctuation.section.parens.end.v2settargetclear
5415 pop: 1
5416
5417 params-open:
5418 - match: '\('
5419 scope: punctuation.section.parameters.begin.v2settargetclear
5420 set: params-body
5421 - include: else-pop
5422
5423 params-body:
5424 - clear_scopes: 1
5425 - meta_scope: meta.function.parameters.v2settargetclear
5426 - match: '\)'
5427 scope: punctuation.section.parameters.end.v2settargetclear
5428 pop: 1
5429 - match: '\w+'
5430 scope: variable.parameter.v2settargetclear
5431"#;
5432 let syntax = SyntaxDefinition::load_from_str(syntax_str, true, None).unwrap();
5433 let ss = link(syntax);
5434 let mut state = ParseState::new(&ss.syntaxes()[0]);
5435 let raw_ops = ops(&mut state, "( (n1 n2))\n", &ss);
5438
5439 let states = stack_states(raw_ops);
5440
5441 let param_states: Vec<_> = states
5445 .iter()
5446 .filter(|s| s.contains("meta.function.parameters.v2settargetclear"))
5447 .collect();
5448 assert!(
5449 !param_states.is_empty(),
5450 "expected to enter params-body context, got states: {:?}",
5451 states
5452 );
5453 let outer = "<meta.function.v2settargetclear>";
5458 for s in ¶m_states {
5459 assert!(
5460 !s.contains(outer),
5461 "outer meta.function must be cleared under params-body, \
5462 but found it alongside meta.function.parameters: {:?}",
5463 s
5464 );
5465 }
5466
5467 let after_inner_close: Vec<_> = states
5470 .iter()
5471 .rev()
5472 .take_while(|s| !s.contains("meta.function.parameters.v2settargetclear"))
5473 .collect();
5474 assert!(
5475 after_inner_close
5476 .iter()
5477 .any(|s| s.contains("meta.function.v2settargetclear")),
5478 "meta.function must be restored after params-body pops, got trailing states: {:?}",
5479 after_inner_close
5480 );
5481 }
5482
5483 #[test]
5484 fn v2_set_clear_scopes_applies_from_every_context() {
5485 let syntax_str = r#"
5497name: V2ClearMid
5498scope: source.v2clear
5499version: 2
5500contexts:
5501 main:
5502 - meta_scope: meta.main.v2clear
5503 - match: 'GO'
5504 set: [ctx-bottom, ctx-middle, ctx-top]
5505 ctx-bottom:
5506 - meta_content_scope: mcs.bottom.v2clear
5507 ctx-middle:
5508 - clear_scopes: 1
5509 - meta_content_scope: mcs.middle.v2clear
5510 ctx-top:
5511 - meta_content_scope: mcs.top.v2clear
5512 - match: '\w+'
5513 scope: word.top.v2clear
5514 pop: true
5515"#;
5516 let syntax = SyntaxDefinition::load_from_str(syntax_str, true, None).unwrap();
5517 let ss = link(syntax);
5518 let mut state = ParseState::new(&ss.syntaxes()[0]);
5519 let raw_ops = ops(&mut state, "GO hello\n", &ss);
5520 let states = stack_states(raw_ops);
5521
5522 let hello_states: Vec<_> = states
5526 .iter()
5527 .filter(|s| s.contains("word.top.v2clear"))
5528 .collect();
5529 assert!(
5530 !hello_states.is_empty(),
5531 "expected word.top.v2clear, got states: {:?}",
5532 states
5533 );
5534 for s in &hello_states {
5535 assert!(
5536 !s.contains("mcs.bottom.v2clear"),
5537 "ctx-bottom's mcs should have been cleared by ctx-middle's \
5538 clear_scopes: 1 before ctx-top's match, got: {:?}",
5539 s
5540 );
5541 assert!(
5542 s.contains("mcs.middle.v2clear"),
5543 "ctx-middle's mcs should be on the stack during ctx-top's match, \
5544 got: {:?}",
5545 s
5546 );
5547 }
5548
5549 }
5553
5554 #[test]
5555 fn v2_embed_scope_replaces_skips_meta_content_pop_on_exit() {
5556 use crate::parsing::ScopeStack;
5564
5565 let host = SyntaxDefinition::load_from_str(
5571 r#"
5572name: V2SkipHost
5573scope: source.v2skip
5574file_extensions: [v2skip]
5575version: 2
5576contexts:
5577 main:
5578 - match: '(?=<)'
5579 push: wrapper-a
5580 - match: '\w+'
5581 scope: word.v2skip
5582 wrapper-a:
5583 - match: '(?=<)'
5584 push: wrapper-b
5585 wrapper-b:
5586 - match: '<<'
5587 embed: scope:source.v2skipemb
5588 embed_scope: meta.embedded.v2skip
5589 escape: '>>'
5590 escape_captures:
5591 0: punctuation.end.v2skip
5592"#,
5593 true,
5594 None,
5595 )
5596 .unwrap();
5597
5598 let embedded = SyntaxDefinition::load_from_str(
5599 r#"
5600name: V2SkipEmb
5601scope: source.v2skipemb
5602file_extensions: [v2skipemb]
5603version: 2
5604contexts:
5605 main:
5606 - meta_content_scope: content.v2skipemb
5607 - match: '\w+'
5608 scope: keyword.v2skipemb
5609"#,
5610 true,
5611 None,
5612 )
5613 .unwrap();
5614
5615 let mut builder = SyntaxSetBuilder::new();
5616 builder.add(host);
5617 builder.add(embedded);
5618 let ss = builder.build();
5619
5620 let syntax = ss.find_syntax_by_name("V2SkipHost").unwrap();
5621 let mut state = ParseState::new(syntax);
5622 let raw_ops = state.parse_line("<<x>> hello\n", &ss).unwrap().ops;
5623
5624 let mut scope_stack = ScopeStack::new();
5629 for (_, op) in &raw_ops {
5630 scope_stack
5631 .apply(op)
5632 .expect("applying op should not fail — extra Pop means the skip logic is broken");
5633 }
5634 let final_scopes: Vec<String> = scope_stack
5638 .as_slice()
5639 .iter()
5640 .map(|s| format!("{:?}", s))
5641 .collect();
5642 assert!(
5643 final_scopes.iter().any(|s| s.contains("source.v2skip")),
5644 "source.v2skip should remain on stack after all ops, got: {:?}",
5645 final_scopes
5646 );
5647 }
5648
5649 #[test]
5650 fn v2_host_embedding_v1_guest_skips_meta_content_pop_on_escape() {
5651 use crate::parsing::ScopeStack;
5664
5665 let host = SyntaxDefinition::load_from_str(
5666 r#"
5667name: V2HostV1Guest
5668scope: source.v2host
5669file_extensions: [v2host]
5670version: 2
5671contexts:
5672 main:
5673 - match: '<<'
5674 embed: scope:source.v1guest
5675 embed_scope: meta.embedded.v2host
5676 escape: '>>'
5677 escape_captures:
5678 0: punctuation.end.v2host
5679 - match: '\w+'
5680 scope: word.v2host
5681"#,
5682 true,
5683 None,
5684 )
5685 .unwrap();
5686
5687 let guest = SyntaxDefinition::load_from_str(
5692 r#"
5693name: V1Guest
5694scope: source.v1guest
5695file_extensions: [v1guest]
5696contexts:
5697 main:
5698 - match: '\w+'
5699 scope: keyword.v1guest
5700"#,
5701 true,
5702 None,
5703 )
5704 .unwrap();
5705
5706 let mut builder = SyntaxSetBuilder::new();
5707 builder.add(host);
5708 builder.add(guest);
5709 let ss = builder.build();
5710
5711 let syntax = ss.find_syntax_by_name("V2HostV1Guest").unwrap();
5712 let mut state = ParseState::new(syntax);
5713 let raw_ops = state.parse_line("<<x>> hello\n", &ss).unwrap().ops;
5714
5715 let mut scope_stack = ScopeStack::new();
5720 for (_, op) in &raw_ops {
5721 scope_stack.apply(op).expect(
5722 "applying op stream must succeed — a spurious Pop indicates the skip was gated \
5723 on the guest's syntax version instead of the embed_scope_replaces flag",
5724 );
5725 }
5726 let final_scopes: Vec<String> = scope_stack
5727 .as_slice()
5728 .iter()
5729 .map(|s| format!("{:?}", s))
5730 .collect();
5731 assert!(
5732 final_scopes.iter().any(|s| s.contains("source.v2host")),
5733 "source.v2host should remain after escape; got: {:?}",
5734 final_scopes
5735 );
5736 }
5737
5738 #[test]
5739 fn nested_embed_outer_escape_wins() {
5740 let syntax = r#"
5744name: NestedEmbed
5745scope: source.nested-embed
5746contexts:
5747 main:
5748 - match: 'OUTER'
5749 embed: mid
5750 escape: 'END'
5751 escape_captures:
5752 0: keyword.escape.outer
5753 - match: '.'
5754 scope: main.char
5755
5756 mid:
5757 - match: 'INNER'
5758 embed: deep
5759 escape: 'zzz'
5760 escape_captures:
5761 0: keyword.escape.inner
5762 - match: '.'
5763 scope: mid.char
5764
5765 deep:
5766 - match: '.'
5767 scope: deep.char
5768"#;
5769 let syntax = SyntaxDefinition::load_from_str(syntax, true, None).unwrap();
5770 let ss = link(syntax);
5771 let mut state = ParseState::new(&ss.syntaxes()[0]);
5772
5773 let out1 = state.parse_line("OUTERINNER\n", &ss).expect("line 1");
5775 debug_print_ops("OUTERINNER\n", &out1.ops);
5776
5777 let out2 = state.parse_line("xxENDzzzAFTER\n", &ss).expect("line 2");
5780 let states = stack_states(out2.ops);
5781 println!("states: {:?}", states);
5782
5783 assert!(
5785 states.iter().any(|s| s.contains("keyword.escape.outer")),
5786 "outer escape must fire, got: {:?}",
5787 states
5788 );
5789 assert!(
5791 !states.iter().any(|s| s.contains("keyword.escape.inner")),
5792 "inner escape must not fire when outer escape is earlier, got: {:?}",
5793 states
5794 );
5795 assert!(
5797 states.iter().any(|s| s.contains("main.char")),
5798 "after outer escape we should be in main, got: {:?}",
5799 states
5800 );
5801 }
5802
5803 #[test]
5804 fn embed_escape_with_backref_at_parse_time() {
5805 let syntax = r#"
5808name: BackrefEscape
5809scope: source.backref-escape
5810contexts:
5811 main:
5812 - match: '(<<|>>)'
5813 scope: punctuation.open
5814 embed: inner
5815 escape: '\1'
5816 escape_captures:
5817 0: punctuation.close
5818 - match: '.'
5819 scope: main.char
5820
5821 inner:
5822 - match: '.'
5823 scope: inner.char
5824"#;
5825 let syntax = SyntaxDefinition::load_from_str(syntax, true, None).unwrap();
5826 let ss = link(syntax);
5827
5828 let mut state = ParseState::new(&ss.syntaxes()[0]);
5830 let out1 = state.parse_line("<<>>stuff<<after\n", &ss).expect("line 1");
5831 let states = stack_states(out1.ops);
5832 println!("backref states: {:?}", states);
5833
5834 assert!(
5836 states.iter().any(|s| s.contains("punctuation.open")),
5837 "expected punctuation.open, got: {:?}",
5838 states
5839 );
5840 assert!(
5842 states.iter().any(|s| s.contains("inner.char")),
5843 ">> should be inner.char since escape is <<, got: {:?}",
5844 states
5845 );
5846 assert!(
5848 states.iter().any(|s| s.contains("punctuation.close")),
5849 "matching << should trigger escape, got: {:?}",
5850 states
5851 );
5852 assert!(
5854 states.iter().any(|s| s.contains("main.char")),
5855 "after escape we should be in main, got: {:?}",
5856 states
5857 );
5858 }
5859
5860 #[test]
5861 fn embed_escape_cross_line() {
5862 let syntax = r#"
5865name: CrossLineEscape
5866scope: source.cross-line-escape
5867contexts:
5868 main:
5869 - match: 'BEGIN'
5870 scope: keyword.begin
5871 embed: body
5872 escape: 'STOP'
5873 escape_captures:
5874 0: keyword.stop
5875 - match: '.'
5876 scope: main.char
5877
5878 body:
5879 - match: '.'
5880 scope: body.char
5881"#;
5882 let syntax = SyntaxDefinition::load_from_str(syntax, true, None).unwrap();
5883 let ss = link(syntax);
5884 let mut state = ParseState::new(&ss.syntaxes()[0]);
5885
5886 let out1 = state.parse_line("BEGIN\n", &ss).expect("line 1");
5888 let states1 = stack_states(out1.ops);
5889 assert!(
5890 states1.iter().any(|s| s.contains("keyword.begin")),
5891 "line 1 should have keyword.begin, got: {:?}",
5892 states1
5893 );
5894
5895 let out2 = state.parse_line("hello\n", &ss).expect("line 2");
5897 let states2 = stack_states(out2.ops);
5898 assert!(
5899 states2.iter().any(|s| s.contains("body.char")),
5900 "line 2 should be body content, got: {:?}",
5901 states2
5902 );
5903
5904 let out3 = state.parse_line("STOPafter\n", &ss).expect("line 3");
5906 let states3 = stack_states(out3.ops);
5907 assert!(
5908 states3.iter().any(|s| s.contains("keyword.stop")),
5909 "line 3 should have escape keyword.stop, got: {:?}",
5910 states3
5911 );
5912 assert!(
5913 states3.iter().any(|s| s.contains("main.char")),
5914 "after escape on line 3, should be in main, got: {:?}",
5915 states3
5916 );
5917 }
5918
5919 #[test]
5920 fn embed_inside_branch_then_fail_restores_escape_stack() {
5921 let syntax = r#"
5927name: EmbedBranchFail
5928scope: source.embed-branch
5929contexts:
5930 main:
5931 - match: 'START'
5932 branch_point: bp
5933 branch: [try-embed, fallback]
5934 - match: '.'
5935 scope: main.char
5936
5937 try-embed:
5938 - match: 'EMB'
5939 embed: embedded
5940 escape: 'ESC'
5941 escape_captures:
5942 0: keyword.escape
5943
5944 fallback:
5945 # No pop — stays on the stack so a stale escape entry would persist
5946 - match: '\w+'
5947 scope: fallback.matched
5948 - match: '\n'
5949
5950 embedded:
5951 - match: 'FAIL'
5952 fail: bp
5953 - match: '.'
5954 scope: embedded.char
5955"#;
5956 let syntax = SyntaxDefinition::load_from_str(syntax, true, None).unwrap();
5957 let ss = link(syntax);
5958 let mut state = ParseState::new(&ss.syntaxes()[0]);
5959
5960 let out = state
5965 .parse_line("STARTEMBFAIL\n", &ss)
5966 .expect("parse failed");
5967 let states = stack_states(out.ops);
5968 println!("embed+branch+fail states: {:?}", states);
5969
5970 assert!(
5972 states.iter().any(|s| s.contains("fallback.matched")),
5973 "fallback should match after fail, got: {:?}",
5974 states
5975 );
5976
5977 let out2 = state.parse_line("xESCy\n", &ss).expect("line 2");
5981 let states2 = stack_states(out2.ops);
5982 assert!(
5983 !states2.iter().any(|s| s.contains("keyword.escape")),
5984 "stale escape must not fire after branch revert, got: {:?}",
5985 states2
5986 );
5987 }
5988
5989 #[test]
5990 fn erb_escape_captures() {
5991 let ss = SyntaxSet::load_defaults_newlines();
5992 let syntax = ss.find_syntax_by_extension("erb").unwrap();
5993 let mut state = ParseState::new(syntax);
5994 let mut scope_stack = ScopeStack::new();
5995 let ops = state.parse_line("<%= puts \"hi\" %>\n", &ss).unwrap();
5996 eprintln!("ERB line ops:");
5997 for (pos, op) in &ops.ops {
5998 scope_stack.apply(op).ok();
5999 eprintln!(
6000 " pos={} op={:?} stack={:?}",
6001 pos,
6002 op,
6003 scope_stack.as_slice()
6004 );
6005 }
6006 let stack_str = format!("{:?}", scope_stack.as_slice());
6007 assert!(
6010 !stack_str.contains("source.ruby"),
6011 "Expected Ruby embed to have ended, got: {}",
6012 stack_str
6013 );
6014 }
6015
6016 #[test]
6017 fn embed_js_in_html() {
6018 let ss = SyntaxSet::load_defaults_newlines();
6019
6020 for ext in &["html", "erb"] {
6021 let syntax = ss.find_syntax_by_extension(ext).unwrap();
6022 let mut state = ParseState::new(syntax);
6023 let mut scope_stack = ScopeStack::new();
6024 state
6025 .parse_line("<script type=\"text/javascript\">\n", &ss)
6026 .unwrap()
6027 .ops
6028 .iter()
6029 .for_each(|(_, op)| {
6030 scope_stack.apply(op).ok();
6031 });
6032 let ops = state.parse_line("var x = 5;\n", &ss).unwrap();
6033 for (_, op) in &ops.ops {
6034 scope_stack.apply(op).ok();
6035 }
6036 let stack_str = format!("{:?}", scope_stack.as_slice());
6037 assert!(
6038 stack_str.contains("source.js"),
6039 "Extension {}: expected source.js in scope stack, got: {}",
6040 ext,
6041 stack_str
6042 );
6043 }
6044 }
6045}