Skip to main content

syntect/parsing/
parser.rs

1// Suppression of a false positive clippy lint. Upstream issue:
2//
3//   mutable_key_type false positive for raw pointers
4//   https://github.com/rust-lang/rust-clippy/issues/6745
5//
6// We use `*const MatchPattern` as key in our `SearchCache` hash map.
7// Clippy thinks this is a problem since `MatchPattern` has interior mutability
8// via `MatchPattern::regex::regex` which is an `AtomicLazyCell`.
9// But raw pointers are hashed via the pointer itself, not what is pointed to.
10// See https://github.com/rust-lang/rust/blob/1.54.0/library/core/src/hash/mod.rs#L717-L725
11#![allow(clippy::mutable_key_type)]
12
13use super::regex::{Region, SearchScratch};
14use super::scope::*;
15use super::syntax_definition::*;
16use crate::parsing::syntax_definition::ContextId;
17use crate::parsing::syntax_set::{SyntaxReference, SyntaxSet};
18use fnv::FnvHasher;
19use std::collections::HashMap;
20use std::hash::BuildHasherDefault;
21
22struct LineSearch {
23    cache: SearchCache,
24    region: Region,
25    scratch: SearchScratch,
26    ascii_bytes: u128,
27}
28
29/// Errors that can occur while parsing.
30#[derive(Debug, thiserror::Error)]
31#[non_exhaustive]
32pub enum ParsingError {
33    #[error("Somehow main context was popped from the stack")]
34    MissingMainContext,
35    /// A context is missing. Usually caused by a syntax referencing a another
36    /// syntax that is not known to syntect. See e.g. <https://github.com/trishume/syntect/issues/421>
37    #[error("Missing context with ID '{0:?}'")]
38    MissingContext(ContextId),
39    #[error("Bad index to match_at: {0}")]
40    BadMatchIndex(usize),
41    #[error("Tried to use a ContextReference that has not bee resolved yet: {0:?}")]
42    UnresolvedContextReference(ContextReference),
43}
44
45/// Keeps the current parser state (the internal syntax interpreter stack) between lines of parsing.
46///
47/// If you are parsing an entire file you create one of these at the start and use it
48/// all the way to the end.
49///
50/// # Caching
51///
52/// One reason this is exposed is that since it implements `Clone` you can actually cache
53/// these (probably along with a [`HighlightState`]) and only re-start parsing from the point of a change.
54/// See the docs for [`HighlightState`] for more in-depth discussion of caching.
55///
56/// This state doesn't keep track of the current scope stack and parsing only returns changes to this stack
57/// so if you want to construct scope stacks you'll need to keep track of that as well.
58/// Note that [`HighlightState`] contains exactly this as a public field that you can use.
59///
60/// **Note:** Caching is for advanced users who have tons of time to maximize performance or want to do so eventually.
61/// It is not recommended that you try caching the first time you implement highlighting.
62///
63/// [`HighlightState`]: ../highlighting/struct.HighlightState.html
64#[derive(Debug, Clone, Eq, PartialEq)]
65pub struct ParseState {
66    stack: Vec<StateLevel>,
67    first_line: bool,
68    // See issue #101. Contains indices of frames pushed by `with_prototype`s.
69    // Doesn't look at `with_prototype`s below top of stack.
70    proto_starts: Vec<usize>,
71}
72
73#[derive(Debug, Clone, Eq, PartialEq)]
74struct StateLevel {
75    context: ContextId,
76    prototypes: Vec<ContextId>,
77    captures: Option<(Region, String)>,
78}
79
80#[derive(Debug)]
81struct RegexMatch<'a> {
82    regions: Region,
83    context: &'a Context,
84    pat_index: usize,
85    from_with_prototype: bool,
86    would_loop: bool,
87}
88
89/// Maps the pattern to the start index, which is -1 if not found.
90type SearchCache = HashMap<*const MatchPattern, Option<Region>, BuildHasherDefault<FnvHasher>>;
91
92// To understand the implementation of this, here's an introduction to how
93// Sublime Text syntax definitions work.
94//
95// Let's say we have the following made-up syntax definition:
96//
97//     contexts:
98//       main:
99//         - match: A
100//           scope: scope.a.first
101//           push: context-a
102//         - match: b
103//           scope: scope.b
104//         - match: \w+
105//           scope: scope.other
106//       context-a:
107//         - match: a+
108//           scope: scope.a.rest
109//         - match: (?=.)
110//           pop: true
111//
112// There are two contexts, `main` and `context-a`. Each context contains a list
113// of match rules with instructions for how to proceed.
114//
115// Let's say we have the input string " Aaaabxxx". We start at position 0 in
116// the string. We keep a stack of contexts, which at the beginning is just main.
117//
118// So we start by looking at the top of the context stack (main), and look at
119// the rules in order. The rule that wins is the first one that matches
120// "earliest" in the input string. In our example:
121//
122// 1. The first one matches "A". Note that matches are not anchored, so this
123//    matches at position 1.
124// 2. The second one matches "b", so position 5. The first rule is winning.
125// 3. The third one matches "\w+", so also position 1. But because the first
126//    rule comes first, it wins.
127//
128// So now we execute the winning rule. Whenever we matched some text, we assign
129// the scope (if there is one) to the matched text and advance our position to
130// after the matched text. The scope is "scope.a.first" and our new position is
131// after the "A", so 2. The "push" means that we should change our stack by
132// pushing `context-a` on top of it.
133//
134// In the next step, we repeat the above, but now with the rules in `context-a`.
135// The result is that we match "a+" and assign "scope.a.rest" to "aaa", and our
136// new position is now after the "aaa". Note that there was no instruction for
137// changing the stack, so we stay in that context.
138//
139// In the next step, the first rule doesn't match anymore, so we go to the next
140// rule where "(?=.)" matches. The instruction is to "pop", which means we
141// pop the top of our context stack, which means we're now back in main.
142//
143// This time in main, we match "b", and in the next step we match the rest with
144// "\w+", and we're done.
145//
146//
147// ## Preventing loops
148//
149// These are the basics of how matching works. Now, you saw that you can write
150// patterns that result in an empty match and don't change the position. These
151// are called non-consuming matches. The problem with them is that they could
152// result in infinite loops. Let's look at a syntax where that is the case:
153//
154//     contexts:
155//       main:
156//         - match: (?=.)
157//           push: test
158//       test:
159//         - match: \w+
160//           scope: word
161//         - match: (?=.)
162//           pop: true
163//
164// This is a bit silly, but it's a minimal example for explaining how matching
165// works in that case.
166//
167// Let's say we have the input string " hello". In `main`, our rule matches and
168// we go into `test` and stay at position 0. Now, the best match is the rule
169// with "pop". But if we used that rule, we'd pop back to `main` and would still
170// be at the same position we started at! So this would be an infinite loop,
171// which we don't want.
172//
173// So what Sublime Text does in case a looping rule "won":
174//
175// * If there's another rule that matches at the same position and does not
176//   result in a loop, use that instead.
177// * Otherwise, go to the next position and go through all the rules in the
178//   current context again. Note that it means that the "pop" could again be the
179//   winning rule, but that's ok as it wouldn't result in a loop anymore.
180//
181// So in our input string, we'd skip one character and try to match the rules
182// again. This time, the "\w+" wins because it comes first.
183
184impl ParseState {
185    /// Bytes in owned heap capacities, excluding this value, allocator overhead,
186    /// and shared syntax storage. Returns `None` on overflow or when capture
187    /// storage cannot be counted for the selected regex backend.
188    pub fn owned_heap_bytes(&self) -> Option<usize> {
189        let bytes = self
190            .stack
191            .capacity()
192            .checked_mul(std::mem::size_of::<StateLevel>())?
193            .checked_add(
194                self.proto_starts
195                    .capacity()
196                    .checked_mul(std::mem::size_of::<usize>())?,
197            )?;
198        self.stack.iter().try_fold(bytes, |bytes, level| {
199            let bytes = bytes.checked_add(
200                level
201                    .prototypes
202                    .capacity()
203                    .checked_mul(std::mem::size_of::<ContextId>())?,
204            )?;
205            match &level.captures {
206                Some((region, text)) => bytes
207                    .checked_add(text.capacity())?
208                    .checked_add(region.owned_heap_bytes()?),
209                None => Some(bytes),
210            }
211        })
212    }
213
214    /// Creates a state from a syntax definition, keeping its own reference-counted point to the
215    /// main context of the syntax
216    pub fn new(syntax: &SyntaxReference) -> ParseState {
217        let start_state = StateLevel {
218            context: syntax.context_ids()["__start"],
219            prototypes: Vec::new(),
220            captures: None,
221        };
222        ParseState {
223            stack: vec![start_state],
224            first_line: true,
225            proto_starts: Vec::new(),
226        }
227    }
228
229    /// Parses a single line of the file. Because of the way regex engines work you unfortunately
230    /// have to pass in a single line contiguous in memory. This can be bad for really long lines.
231    /// Sublime Text avoids this by just not highlighting lines that are too long (thousands of characters).
232    ///
233    /// For efficiency reasons this returns only the changes to the current scope at each point in the line.
234    /// You can use [`ScopeStack::apply`] on each operation in succession to get the stack for a given point.
235    /// Look at the code in `highlighter.rs` for an example of doing this for highlighting purposes.
236    ///
237    /// The returned vector is in order both by index to apply at (the `usize`) and also by order to apply them at a
238    /// given index (e.g popping old scopes before pushing new scopes).
239    ///
240    /// The [`SyntaxSet`] has to be the one that contained the syntax that was used to construct
241    /// this [`ParseState`], or an extended version of it. Otherwise the parsing would return the
242    /// wrong result or even panic. The reason for this is that contexts within the [`SyntaxSet`]
243    /// are referenced via indexes.
244    ///
245    /// [`ScopeStack::apply`]: struct.ScopeStack.html#method.apply
246    /// [`SyntaxSet`]: struct.SyntaxSet.html
247    /// [`ParseState`]: struct.ParseState.html
248    pub fn parse_line(
249        &mut self,
250        line: &str,
251        syntax_set: &SyntaxSet,
252    ) -> Result<Vec<(usize, ScopeStackOp)>, ParsingError> {
253        if self.stack.is_empty() {
254            return Err(ParsingError::MissingMainContext);
255        }
256        let mut match_start = 0;
257        let mut res = Vec::new();
258
259        if self.first_line {
260            let cur_level = &self.stack[self.stack.len() - 1];
261            let context = syntax_set.get_context(&cur_level.context)?;
262            if !context.meta_content_scope.is_empty() {
263                res.push((0, ScopeStackOp::Push(context.meta_content_scope[0])));
264            }
265            self.first_line = false;
266        }
267
268        let fnv = BuildHasherDefault::<FnvHasher>::default();
269        let mut search = LineSearch {
270            cache: HashMap::with_capacity_and_hasher(128, fnv),
271            region: Region::new(),
272            scratch: SearchScratch::default(),
273            ascii_bytes: line
274                .bytes()
275                .filter(u8::is_ascii)
276                .fold(0, |bytes, byte| bytes | (1u128 << byte)),
277        };
278        // Used for detecting loops with push/pop, see long comment above.
279        let mut non_consuming_push_at = (0, 0);
280
281        while self.parse_next_token(
282            line,
283            syntax_set,
284            &mut match_start,
285            &mut search,
286            &mut non_consuming_push_at,
287            &mut res,
288        )? {}
289
290        Ok(res)
291    }
292
293    #[allow(clippy::too_many_arguments)]
294    fn parse_next_token(
295        &mut self,
296        line: &str,
297        syntax_set: &SyntaxSet,
298        start: &mut usize,
299        search: &mut LineSearch,
300        non_consuming_push_at: &mut (usize, usize),
301        ops: &mut Vec<(usize, ScopeStackOp)>,
302    ) -> Result<bool, ParsingError> {
303        let check_pop_loop = {
304            let (pos, stack_depth) = *non_consuming_push_at;
305            pos == *start && stack_depth == self.stack.len()
306        };
307
308        // Trim proto_starts that are no longer valid
309        while self
310            .proto_starts
311            .last()
312            .map(|start| *start >= self.stack.len())
313            .unwrap_or(false)
314        {
315            self.proto_starts.pop();
316        }
317
318        let best_match = self.find_best_match(line, *start, syntax_set, search, check_pop_loop)?;
319
320        if let Some(reg_match) = best_match {
321            if reg_match.would_loop {
322                // A push that doesn't consume anything (a regex that resulted
323                // in an empty match at the current position) can not be
324                // followed by a non-consuming pop. Otherwise we're back where
325                // we started and would try the same sequence of matches again,
326                // resulting in an infinite loop. In this case, Sublime Text
327                // advances one character and tries again, thus preventing the
328                // loop.
329
330                // println!("pop_would_loop for match {:?}, start {}", reg_match, *start);
331
332                // nth(1) gets the next character if there is one. Need to do
333                // this instead of just += 1 because we have byte indices and
334                // unicode characters can be more than 1 byte.
335                if let Some((i, _)) = line[*start..].char_indices().nth(1) {
336                    *start += i;
337                    return Ok(true);
338                } else {
339                    // End of line, no character to advance and no point trying
340                    // any more patterns.
341                    return Ok(false);
342                }
343            }
344
345            let match_end = reg_match.regions.pos(0).unwrap().1;
346
347            let consuming = match_end > *start;
348            if !consuming {
349                // The match doesn't consume any characters. If this is a
350                // "push", remember the position and stack size so that we can
351                // check the next "pop" for loops. Otherwise leave the state,
352                // e.g. non-consuming "set" could also result in a loop.
353                let context = reg_match.context;
354                let match_pattern = context.match_at(reg_match.pat_index)?;
355                if let MatchOperation::Push(_) = match_pattern.operation {
356                    *non_consuming_push_at = (match_end, self.stack.len() + 1);
357                }
358            }
359
360            *start = match_end;
361
362            // ignore `with_prototype`s below this if a context is pushed
363            if reg_match.from_with_prototype {
364                // use current height, since we're before the actual push
365                self.proto_starts.push(self.stack.len());
366            }
367
368            let level_context = {
369                let id = &self.stack[self.stack.len() - 1].context;
370                syntax_set.get_context(id)?
371            };
372            self.exec_pattern(line, &reg_match, level_context, syntax_set, ops)?;
373
374            Ok(true)
375        } else {
376            Ok(false)
377        }
378    }
379
380    fn find_best_match<'a>(
381        &self,
382        line: &str,
383        start: usize,
384        syntax_set: &'a SyntaxSet,
385        search: &mut LineSearch,
386        check_pop_loop: bool,
387    ) -> Result<Option<RegexMatch<'a>>, ParsingError> {
388        let cur_level = &self.stack[self.stack.len() - 1];
389        let context = syntax_set.get_context(&cur_level.context)?;
390        let prototype = if let Some(ref p) = context.prototype {
391            Some(p)
392        } else {
393            None
394        };
395
396        // Build an iterator for the contexts we want to visit in order
397        let context_chain = {
398            let proto_start = self.proto_starts.last().cloned().unwrap_or(0);
399            // Sublime applies with_prototypes from bottom to top
400            let with_prototypes = self.stack[proto_start..].iter().flat_map(|lvl| {
401                lvl.prototypes
402                    .iter()
403                    .map(move |ctx| (true, ctx, lvl.captures.as_ref()))
404            });
405            let cur_prototype = prototype.into_iter().map(|ctx| (false, ctx, None));
406            let cur_context =
407                Some((false, &cur_level.context, cur_level.captures.as_ref())).into_iter();
408            with_prototypes.chain(cur_prototype).chain(cur_context)
409        };
410
411        // println!("{:#?}", cur_level);
412        // println!("token at {} on {}", start, line.trim_right());
413
414        let mut min_start = usize::MAX;
415        let mut best_match: Option<RegexMatch<'_>> = None;
416        let mut pop_would_loop = false;
417
418        for (from_with_proto, ctx, captures) in context_chain {
419            for (pat_context, pat_index) in context_iter(syntax_set, syntax_set.get_context(ctx)?) {
420                let match_pat = pat_context.match_at(pat_index)?;
421
422                if let Some(match_region) = self.search(line, start, match_pat, captures, search) {
423                    let (match_start, match_end) = match_region.pos(0).unwrap();
424
425                    // println!("matched pattern {:?} at start {} end {} (pop would loop: {}, min start: {}, initial start: {}, check_pop_loop: {}, stack_len: {})", match_pat, match_start, match_end, pop_would_loop, min_start, start, check_pop_loop, self.stack.len());
426
427                    if match_start < min_start || (match_start == min_start && pop_would_loop) {
428                        // New match is earlier in text than old match,
429                        // or old match was a looping pop at the same
430                        // position.
431
432                        // println!("setting as current match");
433
434                        min_start = match_start;
435
436                        let consuming = match_end > start;
437                        pop_would_loop = check_pop_loop
438                            && !consuming
439                            && matches!(match_pat.operation, MatchOperation::Pop);
440
441                        let push_too_deep = matches!(match_pat.operation, MatchOperation::Push(_))
442                            && self.stack.len() >= 100;
443
444                        if push_too_deep {
445                            return Ok(None);
446                        }
447
448                        best_match = Some(RegexMatch {
449                            regions: match_region,
450                            context: pat_context,
451                            pat_index,
452                            from_with_prototype: from_with_proto,
453                            would_loop: pop_would_loop,
454                        });
455
456                        if match_start == start && !pop_would_loop {
457                            // We're not gonna find a better match after this,
458                            // so as an optimization we can stop matching now.
459                            return Ok(best_match);
460                        }
461                    }
462                }
463            }
464        }
465        Ok(best_match)
466    }
467
468    fn search(
469        &self,
470        line: &str,
471        start: usize,
472        match_pat: &MatchPattern,
473        captures: Option<&(Region, String)>,
474        search: &mut LineSearch,
475    ) -> Option<Region> {
476        // println!("{} - {:?} - {:?}", match_pat.regex_str, match_pat.has_captures, cur_level.captures.is_some());
477        let match_ptr = match_pat as *const MatchPattern;
478
479        if let Some(maybe_region) = search.cache.get(&match_ptr) {
480            if let Some(ref region) = *maybe_region {
481                let match_start = region.pos(0).unwrap().0;
482                if match_start >= start {
483                    // Cached match is valid, return it. Otherwise do another
484                    // search below.
485                    return Some(region.clone());
486                }
487            } else {
488                // Didn't find a match earlier, so no point trying to match it again
489                return None;
490            }
491        }
492
493        let (regex, can_cache) = match (match_pat.has_captures, captures) {
494            (true, Some(captures)) => {
495                let (region, s) = captures;
496                (&match_pat.regex_with_refs(region, s), false)
497            }
498            _ => (match_pat.regex(), true),
499        };
500        // print!("  executing regex: {:?} at pos {} on line {}", regex.regex_str(), start, line);
501        let matched = regex.possible_with_ascii_bytes(search.ascii_bytes)
502            && regex.search_with_scratch(
503                line,
504                start,
505                line.len(),
506                &mut search.region,
507                &mut search.scratch,
508            );
509
510        if matched {
511            let (match_start, match_end) = search.region.pos(0).unwrap();
512            // this is necessary to avoid infinite looping on dumb patterns
513            let does_something = match match_pat.operation {
514                MatchOperation::None => match_start != match_end,
515                MatchOperation::Push(_) => self.stack.len() < 100,
516                _ => true,
517            };
518            if can_cache && does_something {
519                search.cache.insert(match_pat, Some(search.region.clone()));
520            }
521            if does_something {
522                // print!("catch {} at {} on {}", match_pat.regex_str, match_start, line);
523                return Some(search.region.clone());
524            }
525        } else if can_cache {
526            search.cache.insert(match_pat, None);
527        }
528        None
529    }
530
531    /// Returns true if the stack was changed
532    fn exec_pattern<'a>(
533        &mut self,
534        line: &str,
535        reg_match: &RegexMatch<'a>,
536        level_context: &'a Context,
537        syntax_set: &'a SyntaxSet,
538        ops: &mut Vec<(usize, ScopeStackOp)>,
539    ) -> Result<bool, ParsingError> {
540        let (match_start, match_end) = reg_match.regions.pos(0).unwrap();
541        let context = reg_match.context;
542        let pat = context.match_at(reg_match.pat_index)?;
543        // println!("running pattern {:?} on '{}' at {}, operation {:?}", pat.regex_str, line, match_start, pat.operation);
544
545        self.push_meta_ops(
546            true,
547            match_start,
548            level_context,
549            &pat.operation,
550            syntax_set,
551            ops,
552        )?;
553        for s in &pat.scope {
554            // println!("pushing {:?} at {}", s, match_start);
555            ops.push((match_start, ScopeStackOp::Push(*s)));
556        }
557        if let Some(ref capture_map) = pat.captures {
558            // captures could appear in an arbitrary order, have to produce ops in right order
559            // ex: ((bob)|(hi))* could match hibob in wrong order, and outer has to push first
560            // we don't have to handle a capture matching multiple times, Sublime doesn't
561            let mut map: Vec<((usize, i32), ScopeStackOp)> = Vec::new();
562            for &(cap_index, ref scopes) in capture_map.iter() {
563                if let Some((cap_start, cap_end)) = reg_match.regions.pos(cap_index) {
564                    // marking up empty captures causes pops to be sorted wrong
565                    if cap_start == cap_end {
566                        continue;
567                    }
568                    // println!("capture {:?} at {:?}-{:?}", scopes[0], cap_start, cap_end);
569                    for scope in scopes.iter() {
570                        map.push((
571                            (cap_start, -((cap_end - cap_start) as i32)),
572                            ScopeStackOp::Push(*scope),
573                        ));
574                    }
575                    map.push(((cap_end, i32::MIN), ScopeStackOp::Pop(scopes.len())));
576                }
577            }
578            map.sort_by(|a, b| a.0.cmp(&b.0));
579            for ((index, _), op) in map.into_iter() {
580                ops.push((index, op));
581            }
582        }
583        if !pat.scope.is_empty() {
584            // println!("popping at {}", match_end);
585            ops.push((match_end, ScopeStackOp::Pop(pat.scope.len())));
586        }
587        self.push_meta_ops(
588            false,
589            match_end,
590            level_context,
591            &pat.operation,
592            syntax_set,
593            ops,
594        )?;
595
596        self.perform_op(line, &reg_match.regions, pat, syntax_set)
597    }
598
599    fn push_meta_ops(
600        &self,
601        initial: bool,
602        index: usize,
603        cur_context: &Context,
604        match_op: &MatchOperation,
605        syntax_set: &SyntaxSet,
606        ops: &mut Vec<(usize, ScopeStackOp)>,
607    ) -> Result<(), ParsingError> {
608        // println!("metas ops for {:?}, initial: {}",
609        //          match_op,
610        //          initial);
611        // println!("{:?}", cur_context.meta_scope);
612        match *match_op {
613            MatchOperation::Pop => {
614                let v = if initial {
615                    &cur_context.meta_content_scope
616                } else {
617                    &cur_context.meta_scope
618                };
619                if !v.is_empty() {
620                    ops.push((index, ScopeStackOp::Pop(v.len())));
621                }
622
623                // cleared scopes are restored after the scopes from match pattern that invoked the pop are applied
624                if !initial && cur_context.clear_scopes.is_some() {
625                    ops.push((index, ScopeStackOp::Restore))
626                }
627            }
628            // for some reason the ST3 behaviour of set is convoluted and is inconsistent with the docs and other ops
629            // - the meta_content_scope of the current context is applied to the matched thing, unlike pop
630            // - the clear_scopes are applied after the matched token, unlike push
631            // - the interaction with meta scopes means that the token has the meta scopes of both the current scope and the new scope.
632            MatchOperation::Push(ref context_refs) | MatchOperation::Set(ref context_refs) => {
633                let is_set = matches!(*match_op, MatchOperation::Set(_));
634                // a match pattern that "set"s keeps the meta_content_scope and meta_scope from the previous context
635                if initial {
636                    if is_set && cur_context.clear_scopes.is_some() {
637                        // cleared scopes from the old context are restored immediately
638                        ops.push((index, ScopeStackOp::Restore));
639                    }
640                    // add each context's meta scope
641                    for r in context_refs.iter() {
642                        let ctx = r.resolve(syntax_set)?;
643
644                        if !is_set {
645                            if let Some(clear_amount) = ctx.clear_scopes {
646                                ops.push((index, ScopeStackOp::Clear(clear_amount)));
647                            }
648                        }
649
650                        for scope in ctx.meta_scope.iter() {
651                            ops.push((index, ScopeStackOp::Push(*scope)));
652                        }
653                    }
654                } else {
655                    let repush = (is_set
656                        && (!cur_context.meta_scope.is_empty()
657                            || !cur_context.meta_content_scope.is_empty()))
658                        || context_refs.iter().any(|r| {
659                            let ctx = r.resolve(syntax_set).unwrap();
660
661                            !ctx.meta_content_scope.is_empty()
662                                || (ctx.clear_scopes.is_some() && is_set)
663                        });
664                    if repush {
665                        // remove previously pushed meta scopes, so that meta content scopes will be applied in the correct order
666                        let mut num_to_pop: usize = context_refs
667                            .iter()
668                            .map(|r| {
669                                let ctx = r.resolve(syntax_set).unwrap();
670                                ctx.meta_scope.len()
671                            })
672                            .sum();
673
674                        // also pop off the original context's meta scopes
675                        if is_set {
676                            num_to_pop +=
677                                cur_context.meta_content_scope.len() + cur_context.meta_scope.len();
678                        }
679
680                        // do all the popping as one operation
681                        if num_to_pop > 0 {
682                            ops.push((index, ScopeStackOp::Pop(num_to_pop)));
683                        }
684
685                        // now we push meta scope and meta context scope for each context pushed
686                        for r in context_refs {
687                            let ctx = r.resolve(syntax_set)?;
688
689                            // for some reason, contrary to my reading of the docs, set does this after the token
690                            if is_set {
691                                if let Some(clear_amount) = ctx.clear_scopes {
692                                    ops.push((index, ScopeStackOp::Clear(clear_amount)));
693                                }
694                            }
695
696                            for scope in ctx.meta_scope.iter() {
697                                ops.push((index, ScopeStackOp::Push(*scope)));
698                            }
699                            for scope in ctx.meta_content_scope.iter() {
700                                ops.push((index, ScopeStackOp::Push(*scope)));
701                            }
702                        }
703                    }
704                }
705            }
706            MatchOperation::None => (),
707        }
708
709        Ok(())
710    }
711
712    /// Returns true if the stack was changed
713    fn perform_op(
714        &mut self,
715        line: &str,
716        regions: &Region,
717        pat: &MatchPattern,
718        syntax_set: &SyntaxSet,
719    ) -> Result<bool, ParsingError> {
720        let (ctx_refs, old_proto_ids) = match pat.operation {
721            MatchOperation::Push(ref ctx_refs) => (ctx_refs, None),
722            MatchOperation::Set(ref ctx_refs) => {
723                // a `with_prototype` stays active when the context is `set`
724                // until the context layer in the stack (where the `with_prototype`
725                // was initially applied) is popped off.
726                (ctx_refs, self.stack.pop().map(|s| s.prototypes))
727            }
728            MatchOperation::Pop => {
729                self.stack.pop();
730                return Ok(true);
731            }
732            MatchOperation::None => return Ok(false),
733        };
734        for (i, r) in ctx_refs.iter().enumerate() {
735            let mut proto_ids = if i == 0 {
736                // it is only necessary to preserve the old prototypes
737                // at the first stack frame pushed
738                old_proto_ids.clone().unwrap_or_else(Vec::new)
739            } else {
740                Vec::new()
741            };
742            if i == ctx_refs.len() - 1 {
743                // if a with_prototype was specified, and multiple contexts were pushed,
744                // then the with_prototype applies only to the last context pushed, i.e.
745                // top most on the stack after all the contexts are pushed - this is also
746                // referred to as the "target" of the push by sublimehq - see
747                // https://forum.sublimetext.com/t/dev-build-3111/19240/17 for more info
748                if let Some(ref p) = pat.with_prototype {
749                    proto_ids.push(p.id()?);
750                }
751            }
752            let context_id = r.id()?;
753            let context = syntax_set.get_context(&context_id)?;
754            let captures = {
755                let mut uses_backrefs = context.uses_backrefs;
756                if !proto_ids.is_empty() {
757                    uses_backrefs = uses_backrefs
758                        || proto_ids
759                            .iter()
760                            .any(|id| syntax_set.get_context(id).unwrap().uses_backrefs);
761                }
762                if uses_backrefs {
763                    Some((regions.clone(), line.to_owned()))
764                } else {
765                    None
766                }
767            };
768            self.stack.push(StateLevel {
769                context: context_id,
770                prototypes: proto_ids,
771                captures,
772            });
773        }
774        Ok(true)
775    }
776}
777
778#[cfg(feature = "yaml-load")]
779#[cfg(test)]
780mod tests {
781    use super::*;
782    use crate::parsing::ScopeStackOp::{Clear, Pop, Push, Restore};
783    use crate::parsing::{Scope, ScopeStack, SyntaxSet, SyntaxSetBuilder};
784    use crate::util::debug_print_ops;
785    use crate::utils::testdata;
786
787    const TEST_SYNTAX: &str = include_str!("../../testdata/parser_tests.sublime-syntax");
788    #[test]
789    fn can_parse_simple() {
790        let ss = &*testdata::PACKAGES_SYN_SET;
791        let mut state = {
792            let syntax = ss.find_syntax_by_name("Ruby on Rails").unwrap();
793            ParseState::new(syntax)
794        };
795
796        let ops1 = ops(&mut state, "module Bob::Wow::Troll::Five; 5; end", &ss);
797        let test_ops1 = vec![
798            (0, Push(Scope::new("source.ruby.rails").unwrap())),
799            (0, Push(Scope::new("meta.module.ruby").unwrap())),
800            (0, Push(Scope::new("keyword.control.module.ruby").unwrap())),
801            (6, Pop(2)),
802            (6, Push(Scope::new("meta.module.ruby").unwrap())),
803            (7, Pop(1)),
804            (7, Push(Scope::new("meta.module.ruby").unwrap())),
805            (7, Push(Scope::new("entity.name.module.ruby").unwrap())),
806            (7, Push(Scope::new("support.other.namespace.ruby").unwrap())),
807            (10, Pop(1)),
808            (10, Push(Scope::new("punctuation.accessor.ruby").unwrap())),
809        ];
810        assert_eq!(&ops1[0..test_ops1.len()], &test_ops1[..]);
811
812        let ops2 = ops(&mut state, "def lol(wow = 5)", &ss);
813        let test_ops2 = vec![
814            (0, Push(Scope::new("meta.function.ruby").unwrap())),
815            (0, Push(Scope::new("keyword.control.def.ruby").unwrap())),
816            (3, Pop(2)),
817            (3, Push(Scope::new("meta.function.ruby").unwrap())),
818            (4, Push(Scope::new("entity.name.function.ruby").unwrap())),
819            (7, Pop(1)),
820        ];
821        assert_eq!(&ops2[0..test_ops2.len()], &test_ops2[..]);
822    }
823
824    #[test]
825    fn can_parse_yaml() {
826        let ps = &*testdata::PACKAGES_SYN_SET;
827        let mut state = {
828            let syntax = ps.find_syntax_by_name("YAML").unwrap();
829            ParseState::new(syntax)
830        };
831
832        assert_eq!(
833            ops(&mut state, "key: value\n", &ps),
834            vec![
835                (0, Push(Scope::new("source.yaml").unwrap())),
836                (
837                    0,
838                    Push(Scope::new("string.unquoted.plain.out.yaml").unwrap())
839                ),
840                (0, Push(Scope::new("entity.name.tag.yaml").unwrap())),
841                (3, Pop(2)),
842                (
843                    3,
844                    Push(Scope::new("punctuation.separator.key-value.mapping.yaml").unwrap())
845                ),
846                (4, Pop(1)),
847                (
848                    5,
849                    Push(Scope::new("string.unquoted.plain.out.yaml").unwrap())
850                ),
851                (10, Pop(1)),
852            ]
853        );
854    }
855
856    #[test]
857    fn can_parse_includes() {
858        let ss = &*testdata::PACKAGES_SYN_SET;
859        let mut state = {
860            let syntax = ss.find_syntax_by_name("HTML (Rails)").unwrap();
861            ParseState::new(syntax)
862        };
863
864        let ops = ops(&mut state, "<script>var lol = '<% def wow(", &ss);
865
866        let mut test_stack = ScopeStack::new();
867        test_stack.push(Scope::new("text.html.ruby").unwrap());
868        test_stack.push(Scope::new("text.html.basic").unwrap());
869        test_stack.push(Scope::new("source.js.embedded.html").unwrap());
870        test_stack.push(Scope::new("source.js").unwrap());
871        test_stack.push(Scope::new("string.quoted.single.js").unwrap());
872        test_stack.push(Scope::new("source.ruby.rails.embedded.html").unwrap());
873        test_stack.push(Scope::new("meta.function.parameters.ruby").unwrap());
874
875        let mut stack = ScopeStack::new();
876        for (_, op) in ops.iter() {
877            stack.apply(op).expect("#[cfg(test)]");
878        }
879        assert_eq!(stack, test_stack);
880    }
881
882    #[test]
883    fn can_parse_backrefs() {
884        let ss = &*testdata::PACKAGES_SYN_SET;
885        let mut state = {
886            let syntax = ss.find_syntax_by_name("Ruby on Rails").unwrap();
887            ParseState::new(syntax)
888        };
889
890        // For parsing HEREDOC, the "SQL" is captured at the beginning and then used in another
891        // regex with a backref, to match the end of the HEREDOC. Note that there can be code
892        // after the marker (`.strip`) here.
893        assert_eq!(
894            ops(&mut state, "lol = <<-SQL.strip", &ss),
895            vec![
896                (0, Push(Scope::new("source.ruby.rails").unwrap())),
897                (
898                    4,
899                    Push(Scope::new("keyword.operator.assignment.ruby").unwrap())
900                ),
901                (5, Pop(1)),
902                (
903                    6,
904                    Push(Scope::new("string.unquoted.embedded.sql.ruby").unwrap())
905                ),
906                (
907                    6,
908                    Push(Scope::new("punctuation.definition.string.begin.ruby").unwrap())
909                ),
910                (12, Pop(1)),
911                (12, Pop(1)),
912                (
913                    12,
914                    Push(Scope::new("string.unquoted.embedded.sql.ruby").unwrap())
915                ),
916                (12, Push(Scope::new("text.sql.embedded.ruby").unwrap())),
917                (12, Clear(ClearAmount::TopN(2))),
918                (12, Push(Scope::new("punctuation.accessor.ruby").unwrap())),
919                (13, Pop(1)),
920                (18, Restore),
921            ]
922        );
923
924        assert_eq!(ops(&mut state, "wow", &ss), vec![]);
925
926        assert_eq!(
927            ops(&mut state, "SQL", &ss),
928            vec![
929                (0, Pop(1)),
930                (
931                    0,
932                    Push(Scope::new("punctuation.definition.string.end.ruby").unwrap())
933                ),
934                (3, Pop(1)),
935                (3, Pop(1)),
936            ]
937        );
938    }
939
940    #[test]
941    fn can_parse_preprocessor_rules() {
942        let ss = &*testdata::PACKAGES_SYN_SET;
943        let mut state = {
944            let syntax = ss.find_syntax_by_name("C").unwrap();
945            ParseState::new(syntax)
946        };
947
948        assert_eq!(
949            ops(&mut state, "#ifdef FOO", &ss),
950            vec![
951                (0, Push(Scope::new("source.c").unwrap())),
952                (0, Push(Scope::new("meta.preprocessor.c").unwrap())),
953                (0, Push(Scope::new("keyword.control.import.c").unwrap())),
954                (6, Pop(1)),
955                (10, Pop(1)),
956            ]
957        );
958        assert_eq!(
959            ops(&mut state, "{", &ss),
960            vec![
961                (0, Push(Scope::new("meta.block.c").unwrap())),
962                (
963                    0,
964                    Push(Scope::new("punctuation.section.block.begin.c").unwrap())
965                ),
966                (1, Pop(1)),
967            ]
968        );
969        assert_eq!(
970            ops(&mut state, "#else", &ss),
971            vec![
972                (0, Push(Scope::new("meta.preprocessor.c").unwrap())),
973                (0, Push(Scope::new("keyword.control.import.c").unwrap())),
974                (5, Pop(1)),
975                (5, Pop(1)),
976            ]
977        );
978        assert_eq!(
979            ops(&mut state, "{", &ss),
980            vec![
981                (0, Push(Scope::new("meta.block.c").unwrap())),
982                (
983                    0,
984                    Push(Scope::new("punctuation.section.block.begin.c").unwrap())
985                ),
986                (1, Pop(1)),
987            ]
988        );
989        assert_eq!(
990            ops(&mut state, "#endif", &ss),
991            vec![
992                (0, Pop(1)),
993                (0, Push(Scope::new("meta.block.c").unwrap())),
994                (0, Push(Scope::new("meta.preprocessor.c").unwrap())),
995                (0, Push(Scope::new("keyword.control.import.c").unwrap())),
996                (6, Pop(2)),
997                (6, Pop(2)),
998                (6, Push(Scope::new("meta.block.c").unwrap())),
999            ]
1000        );
1001        assert_eq!(
1002            ops(&mut state, "    foo;", &ss),
1003            vec![
1004                (7, Push(Scope::new("punctuation.terminator.c").unwrap())),
1005                (8, Pop(1)),
1006            ]
1007        );
1008        assert_eq!(
1009            ops(&mut state, "}", &ss),
1010            vec![
1011                (
1012                    0,
1013                    Push(Scope::new("punctuation.section.block.end.c").unwrap())
1014                ),
1015                (1, Pop(1)),
1016                (1, Pop(1)),
1017            ]
1018        );
1019    }
1020
1021    #[test]
1022    fn can_parse_issue25() {
1023        let ss = &*testdata::PACKAGES_SYN_SET;
1024        let mut state = {
1025            let syntax = ss.find_syntax_by_name("C").unwrap();
1026            ParseState::new(syntax)
1027        };
1028
1029        // test fix for issue #25
1030        assert_eq!(ops(&mut state, "struct{estruct", &ss).len(), 10);
1031    }
1032
1033    #[test]
1034    fn can_compare_parse_states() {
1035        let ss = &*testdata::PACKAGES_SYN_SET;
1036        let syntax = ss.find_syntax_by_name("Java").unwrap();
1037        let mut state1 = ParseState::new(syntax);
1038        let mut state2 = ParseState::new(syntax);
1039
1040        assert_eq!(ops(&mut state1, "class Foo {", &ss).len(), 11);
1041        assert_eq!(ops(&mut state2, "class Fooo {", &ss).len(), 11);
1042
1043        assert_eq!(state1, state2);
1044        ops(&mut state1, "}", &ss);
1045        assert_ne!(state1, state2);
1046    }
1047
1048    #[test]
1049    fn owned_heap_accounts_for_captured_delimiters_and_prototypes() {
1050        let definition = SyntaxDefinition::load_from_str(
1051            r#"name: Captured delimiters
1052scope: source.capture-test
1053contexts:
1054  main:
1055    - match: '^BEGIN ([A-Z]+)'
1056      push: body
1057      with_prototype:
1058        - match: 'SPECIAL'
1059          scope: keyword.control.capture-test
1060  body:
1061    - meta_scope: string.body.capture-test
1062    - match: '^END-\1'
1063      pop: true
1064    - match: '.+'
1065"#,
1066            true,
1067            None,
1068        )
1069        .unwrap();
1070        let mut builder = SyntaxSetBuilder::new();
1071        builder.add(definition);
1072        let syntaxes = builder.build();
1073        let mut state = ParseState::new(&syntaxes.syntaxes()[0]);
1074        let delimiter = "STOP".repeat(1024);
1075        let opening = format!("BEGIN {delimiter}\n");
1076        state.parse_line(&opening, &syntaxes).unwrap();
1077        let captured = state
1078            .stack
1079            .iter()
1080            .find(|level| level.captures.is_some())
1081            .expect("the delimiter must remain in parser state");
1082        assert!(!captured.prototypes.is_empty());
1083        let (region, text) = captured.captures.as_ref().unwrap();
1084        assert_eq!(text, &opening);
1085        if cfg!(feature = "regex-onig") {
1086            assert_eq!(state.owned_heap_bytes(), None);
1087        } else {
1088            let minimum = state.stack.capacity() * std::mem::size_of::<StateLevel>()
1089                + state.proto_starts.capacity() * std::mem::size_of::<usize>()
1090                + captured.prototypes.capacity() * std::mem::size_of::<ContextId>()
1091                + text.capacity()
1092                + region.owned_heap_bytes().unwrap();
1093            assert!(state.owned_heap_bytes().unwrap() >= minimum);
1094        }
1095        let mut restored = state.clone();
1096        assert_eq!(restored, state);
1097        for line in ["END-WRONG\n".to_owned(), format!("END-{delimiter}\n")] {
1098            assert_eq!(
1099                state.parse_line(&line, &syntaxes).unwrap(),
1100                restored.parse_line(&line, &syntaxes).unwrap()
1101            );
1102            assert_eq!(state, restored);
1103        }
1104        assert!(state.stack.iter().all(|level| level.captures.is_none()));
1105        assert!(state.owned_heap_bytes().is_some());
1106    }
1107
1108    #[test]
1109    fn can_parse_non_nested_clear_scopes() {
1110        let line = "'hello #simple_cleared_scopes_test world test \\n '";
1111        let expect = [
1112            "<source.test>, <example.meta-scope.after-clear-scopes.example>, <example.pushes-clear-scopes.example>",
1113            "<source.test>, <example.meta-scope.after-clear-scopes.example>, <example.pops-clear-scopes.example>",
1114            "<source.test>, <string.quoted.single.example>, <constant.character.escape.example>",
1115        ];
1116        expect_scope_stacks(line, &expect, TEST_SYNTAX);
1117    }
1118
1119    #[test]
1120    fn can_parse_non_nested_too_many_clear_scopes() {
1121        let line = "'hello #too_many_cleared_scopes_test world test \\n '";
1122        let expect = [
1123            "<example.meta-scope.after-clear-scopes.example>, <example.pushes-clear-scopes.example>",
1124            "<example.meta-scope.after-clear-scopes.example>, <example.pops-clear-scopes.example>",
1125            "<source.test>, <string.quoted.single.example>, <constant.character.escape.example>",
1126        ];
1127        expect_scope_stacks(line, &expect, TEST_SYNTAX);
1128    }
1129
1130    #[test]
1131    fn can_parse_nested_clear_scopes() {
1132        let line = "'hello #nested_clear_scopes_test world foo bar test \\n '";
1133        let expect = [
1134            "<source.test>, <example.meta-scope.after-clear-scopes.example>, <example.pushes-clear-scopes.example>",
1135            "<source.test>, <example.meta-scope.cleared-previous-meta-scope.example>, <foo>",
1136            "<source.test>, <example.meta-scope.after-clear-scopes.example>, <example.pops-clear-scopes.example>",
1137            "<source.test>, <string.quoted.single.example>, <constant.character.escape.example>",
1138        ];
1139        expect_scope_stacks(line, &expect, TEST_SYNTAX);
1140    }
1141
1142    #[test]
1143    fn can_parse_infinite_loop() {
1144        let line = "#infinite_loop_test 123";
1145        let expect = ["<source.test>, <constant.numeric.test>"];
1146        expect_scope_stacks(line, &expect, TEST_SYNTAX);
1147    }
1148
1149    #[test]
1150    fn can_parse_infinite_seeming_loop() {
1151        // See https://github.com/SublimeTextIssues/Core/issues/1190 for an
1152        // explanation.
1153        let line = "#infinite_seeming_loop_test hello";
1154        let expect = [
1155            "<source.test>, <keyword.test>",
1156            "<source.test>, <test>, <string.unquoted.test>",
1157            "<source.test>, <test>, <keyword.control.test>",
1158        ];
1159        expect_scope_stacks(line, &expect, TEST_SYNTAX);
1160    }
1161
1162    #[test]
1163    fn can_parse_prototype_that_pops_main() {
1164        let syntax = r#"
1165name: test
1166scope: source.test
1167contexts:
1168  prototype:
1169    # This causes us to pop out of the main context. Sublime Text handles that
1170    # by pushing main back automatically.
1171    - match: (?=!)
1172      pop: true
1173  main:
1174    - match: foo
1175      scope: test.good
1176"#;
1177
1178        let line = "foo!";
1179        let expect = ["<source.test>, <test.good>"];
1180        expect_scope_stacks(line, &expect, syntax);
1181    }
1182
1183    #[test]
1184    fn can_parse_syntax_with_newline_in_character_class() {
1185        let syntax = r#"
1186name: test
1187scope: source.test
1188contexts:
1189  main:
1190    - match: foo[\n]
1191      scope: foo.end
1192    - match: foo
1193      scope: foo.any
1194"#;
1195
1196        let line = "foo";
1197        let expect = ["<source.test>, <foo.end>"];
1198        expect_scope_stacks(line, &expect, syntax);
1199
1200        let line = "foofoofoo";
1201        let expect = [
1202            "<source.test>, <foo.any>",
1203            "<source.test>, <foo.any>",
1204            "<source.test>, <foo.end>",
1205        ];
1206        expect_scope_stacks(line, &expect, syntax);
1207    }
1208
1209    #[test]
1210    fn can_parse_issue120() {
1211        let syntax = SyntaxDefinition::load_from_str(
1212            include_str!("../../testdata/embed_escape_test.sublime-syntax"),
1213            false,
1214            None,
1215        )
1216        .unwrap();
1217
1218        let line1 = "\"abctest\" foobar";
1219        let expect1 = [
1220            "<meta.attribute-with-value.style.html>, <string.quoted.double>, <punctuation.definition.string.begin.html>",
1221            "<meta.attribute-with-value.style.html>, <source.css>",
1222            "<meta.attribute-with-value.style.html>, <string.quoted.double>, <punctuation.definition.string.end.html>",
1223            "<meta.attribute-with-value.style.html>, <source.css>, <test.embedded>",
1224            "<top-level.test>",
1225        ];
1226
1227        expect_scope_stacks_with_syntax(line1, &expect1, syntax.clone());
1228
1229        let line2 = ">abctest</style>foobar";
1230        let expect2 = [
1231            "<meta.tag.style.begin.html>, <punctuation.definition.tag.end.html>",
1232            "<source.css.embedded.html>, <test.embedded>",
1233            "<top-level.test>",
1234        ];
1235        expect_scope_stacks_with_syntax(line2, &expect2, syntax);
1236    }
1237
1238    #[test]
1239    fn can_parse_non_consuming_pop_that_would_loop() {
1240        // See https://github.com/trishume/syntect/issues/127
1241        let syntax = r#"
1242name: test
1243scope: source.test
1244contexts:
1245  main:
1246    # This makes us go into "test" without consuming any characters
1247    - match: (?=hello)
1248      push: test
1249  test:
1250    # If we used this match, we'd go back to "main" without consuming anything,
1251    # and then back into "test", infinitely looping. ST detects this at this
1252    # point and ignores this match until at least one character matched.
1253    - match: (?!world)
1254      pop: true
1255    - match: \w+
1256      scope: test.matched
1257"#;
1258
1259        let line = "hello";
1260        let expect = ["<source.test>, <test.matched>"];
1261        expect_scope_stacks(line, &expect, syntax);
1262    }
1263
1264    #[test]
1265    fn can_parse_non_consuming_set_and_pop_that_would_loop() {
1266        let syntax = r#"
1267name: test
1268scope: source.test
1269contexts:
1270  main:
1271    # This makes us go into "a" without advancing
1272    - match: (?=test)
1273      push: a
1274  a:
1275    # This makes us go into "b" without advancing
1276    - match: (?=t)
1277      set: b
1278  b:
1279    # If we used this match, we'd go back to "main" without having advanced,
1280    # which means we'd have an infinite loop like with the previous test.
1281    # So even for a "set", we have to check if we're advancing or not.
1282    - match: (?=t)
1283      pop: true
1284    - match: \w+
1285      scope: test.matched
1286"#;
1287
1288        let line = "test";
1289        let expect = ["<source.test>, <test.matched>"];
1290        expect_scope_stacks(line, &expect, syntax);
1291    }
1292
1293    #[test]
1294    fn can_parse_non_consuming_set_after_consuming_push_that_does_not_loop() {
1295        let syntax = r#"
1296name: test
1297scope: source.test
1298contexts:
1299  main:
1300    # This makes us go into "a", but we consumed a character
1301    - match: t
1302      push: a
1303    - match: \w+
1304      scope: test.matched
1305  a:
1306    # This makes us go into "b" without consuming
1307    - match: (?=e)
1308      set: b
1309  b:
1310    # This match does not result in an infinite loop because we already consumed
1311    # a character to get into "a", so it's ok to pop back into "main".
1312    - match: (?=e)
1313      pop: true
1314"#;
1315
1316        let line = "test";
1317        let expect = ["<source.test>, <test.matched>"];
1318        expect_scope_stacks(line, &expect, syntax);
1319    }
1320
1321    #[test]
1322    fn can_parse_non_consuming_set_after_consuming_set_that_does_not_loop() {
1323        let syntax = r#"
1324name: test
1325scope: source.test
1326contexts:
1327  main:
1328    - match: (?=hello)
1329      push: a
1330    - match: \w+
1331      scope: test.matched
1332  a:
1333    - match: h
1334      set: b
1335  b:
1336    - match: (?=e)
1337      set: c
1338  c:
1339    # This is not an infinite loop because "a" consumed a character, so we can
1340    # actually pop back into main and then match the rest of the input.
1341    - match: (?=e)
1342      pop: true
1343"#;
1344
1345        let line = "hello";
1346        let expect = ["<source.test>, <test.matched>"];
1347        expect_scope_stacks(line, &expect, syntax);
1348    }
1349
1350    #[test]
1351    fn can_parse_non_consuming_pop_that_would_loop_at_end_of_line() {
1352        let syntax = r#"
1353name: test
1354scope: source.test
1355contexts:
1356  main:
1357    # This makes us go into "test" without consuming, even at the end of line
1358    - match: ""
1359      push: test
1360  test:
1361    - match: ""
1362      pop: true
1363    - match: \w+
1364      scope: test.matched
1365"#;
1366
1367        let line = "hello";
1368        let expect = ["<source.test>, <test.matched>"];
1369        expect_scope_stacks(line, &expect, syntax);
1370    }
1371
1372    #[test]
1373    fn can_parse_empty_but_consuming_set_that_does_not_loop() {
1374        let syntax = r#"
1375name: test
1376scope: source.test
1377contexts:
1378  main:
1379    - match: (?=hello)
1380      push: a
1381    - match: ello
1382      scope: test.good
1383  a:
1384    # This is an empty match, but it consumed a character (the "h")
1385    - match: (?=e)
1386      set: b
1387  b:
1388    # .. so it's ok to pop back to main from here
1389    - match: ""
1390      pop: true
1391    - match: ello
1392      scope: test.bad
1393"#;
1394
1395        let line = "hello";
1396        let expect = ["<source.test>, <test.good>"];
1397        expect_scope_stacks(line, &expect, syntax);
1398    }
1399
1400    #[test]
1401    fn can_parse_non_consuming_pop_that_does_not_loop() {
1402        let syntax = r#"
1403name: test
1404scope: source.test
1405contexts:
1406  main:
1407    # This is a non-consuming push, so "b" will need to check for a
1408    # non-consuming pop
1409    - match: (?=hello)
1410      push: [b, a]
1411    - match: ello
1412      scope: test.good
1413  a:
1414    # This pop is ok, it consumed "h"
1415    - match: (?=e)
1416      pop: true
1417  b:
1418    # This is non-consuming, and we set to "c"
1419    - match: (?=e)
1420      set: c
1421  c:
1422    # It's ok to pop back to "main" here because we consumed a character in the
1423    # meantime.
1424    - match: ""
1425      pop: true
1426    - match: ello
1427      scope: test.bad
1428"#;
1429
1430        let line = "hello";
1431        let expect = ["<source.test>, <test.good>"];
1432        expect_scope_stacks(line, &expect, syntax);
1433    }
1434
1435    #[test]
1436    fn can_parse_non_consuming_pop_with_multi_push_that_does_not_loop() {
1437        let syntax = r#"
1438name: test
1439scope: source.test
1440contexts:
1441  main:
1442    - match: (?=hello)
1443      push: [b, a]
1444    - match: ello
1445      scope: test.good
1446  a:
1447    # This pop is ok, as we're not popping back to "main" yet (which would loop),
1448    # we're popping to "b"
1449    - match: ""
1450      pop: true
1451    - match: \w+
1452      scope: test.bad
1453  b:
1454    - match: \w+
1455      scope: test.good
1456"#;
1457
1458        let line = "hello";
1459        let expect = ["<source.test>, <test.good>"];
1460        expect_scope_stacks(line, &expect, syntax);
1461    }
1462
1463    #[test]
1464    fn can_parse_non_consuming_pop_of_recursive_context_that_does_not_loop() {
1465        let syntax = r#"
1466name: test
1467scope: source.test
1468contexts:
1469  main:
1470    - match: xxx
1471      scope: test.good
1472    - include: basic-identifiers
1473
1474  basic-identifiers:
1475    - match: '\w+::'
1476      scope: test.matched
1477      push: no-type-names
1478
1479  no-type-names:
1480      - include: basic-identifiers
1481      - match: \w+
1482        scope: test.matched.inside
1483      # This is a tricky one because when this is the best match,
1484      # we have two instances of "no-type-names" on the stack, so we're popping
1485      # back from "no-type-names" to another "no-type-names".
1486      - match: ''
1487        pop: true
1488"#;
1489
1490        let line = "foo::bar::* xxx";
1491        let expect = ["<source.test>, <test.good>"];
1492        expect_scope_stacks(line, &expect, syntax);
1493    }
1494
1495    #[test]
1496    fn can_parse_non_consuming_pop_order() {
1497        let syntax = r#"
1498name: test
1499scope: source.test
1500contexts:
1501  main:
1502    - match: (?=hello)
1503      push: test
1504  test:
1505    # This matches first
1506    - match: (?=e)
1507      push: good
1508    # But this (looping) match replaces it, because it's an earlier match
1509    - match: (?=h)
1510      pop: true
1511    # And this should not replace it, as it's a later match (only matches at
1512    # the same position can replace looping pops).
1513    - match: (?=o)
1514      push: bad
1515  good:
1516    - match: \w+
1517      scope: test.good
1518  bad:
1519    - match: \w+
1520      scope: test.bad
1521"#;
1522
1523        let line = "hello";
1524        let expect = ["<source.test>, <test.good>"];
1525        expect_scope_stacks(line, &expect, syntax);
1526    }
1527
1528    #[test]
1529    fn can_parse_prototype_with_embed() {
1530        let syntax = r#"
1531name: Javadoc
1532scope: text.html.javadoc
1533contexts:
1534  prototype:
1535    - match: \*
1536      scope: punctuation.definition.comment.javadoc
1537
1538  main:
1539    - meta_include_prototype: false
1540    - match: /\*\*
1541      scope: comment.block.documentation.javadoc punctuation.definition.comment.begin.javadoc
1542      embed: contents
1543      embed_scope: comment.block.documentation.javadoc text.html.javadoc
1544      escape: \*/
1545      escape_captures:
1546        0: comment.block.documentation.javadoc punctuation.definition.comment.end.javadoc
1547
1548  contents:
1549    - match: ''
1550"#;
1551
1552        let syntax = SyntaxDefinition::load_from_str(syntax, true, None).unwrap();
1553        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);
1554    }
1555
1556    #[test]
1557    fn can_parse_context_included_in_prototype_via_named_reference() {
1558        let syntax = r#"
1559scope: source.test
1560contexts:
1561  prototype:
1562    - match: a
1563      push: a
1564    - match: b
1565      scope: test.bad
1566  main:
1567    - match: unused
1568  # This context is included in the prototype (see `push: a`).
1569  # Because of that, ST doesn't apply the prototype to this context, so if
1570  # we're in here the "b" shouldn't match.
1571  a:
1572    - match: a
1573      scope: test.good
1574"#;
1575
1576        let stack_states = stack_states(parse("aa b", syntax));
1577        assert_eq!(
1578            stack_states,
1579            vec![
1580                "<source.test>",
1581                "<source.test>, <test.good>",
1582                "<source.test>",
1583            ],
1584            "Expected test.bad to not match"
1585        );
1586    }
1587
1588    #[test]
1589    fn can_parse_with_prototype_set() {
1590        let syntax = r#"%YAML 1.2
1591---
1592scope: source.test-set-with-proto
1593contexts:
1594  main:
1595    - match: a
1596      scope: a
1597      set: next1
1598      with_prototype:
1599        - match: '1'
1600          scope: '1'
1601        - match: '2'
1602          scope: '2'
1603        - match: '3'
1604          scope: '3'
1605        - match: '4'
1606          scope: '4'
1607    - match: '5'
1608      scope: '5'
1609      set: [next3, next2]
1610      with_prototype:
1611        - match: c
1612          scope: cwith
1613  next1:
1614    - match: b
1615      scope: b
1616      set: next2
1617  next2:
1618    - match: c
1619      scope: c
1620      push: next3
1621    - match: e
1622      scope: e
1623      pop: true
1624    - match: f
1625      scope: f
1626      set: [next1, next2]
1627  next3:
1628    - match: d
1629      scope: d
1630    - match: (?=e)
1631      pop: true
1632    - match: c
1633      scope: cwithout
1634"#;
1635
1636        expect_scope_stacks_with_syntax(
1637            "a1b2c3d4e5",
1638            &[
1639                "<a>", "<1>", "<b>", "<2>", "<c>", "<3>", "<d>", "<4>", "<e>", "<5>",
1640            ],
1641            SyntaxDefinition::load_from_str(syntax, true, None).unwrap(),
1642        );
1643        expect_scope_stacks_with_syntax(
1644            "5cfcecbedcdea",
1645            &[
1646                "<5>",
1647                "<cwith>",
1648                "<f>",
1649                "<e>",
1650                "<b>",
1651                "<d>",
1652                "<cwithout>",
1653                "<a>",
1654            ],
1655            SyntaxDefinition::load_from_str(syntax, true, None).unwrap(),
1656        );
1657    }
1658
1659    #[test]
1660    fn can_parse_issue176() {
1661        let syntax = r#"
1662scope: source.dummy
1663contexts:
1664  main:
1665    - match: (test)(?=(foo))(f)
1666      captures:
1667        1: test
1668        2: ignored
1669        3: f
1670      push:
1671        - match: (oo)
1672          captures:
1673            1: keyword
1674"#;
1675
1676        let syntax = SyntaxDefinition::load_from_str(syntax, true, None).unwrap();
1677        expect_scope_stacks_with_syntax(
1678            "testfoo",
1679            &["<test>", /*"<ignored>",*/ "<f>", "<keyword>"],
1680            syntax,
1681        );
1682    }
1683
1684    #[test]
1685    fn can_parse_two_with_prototypes_at_same_stack_level() {
1686        let syntax_yamlstr = r#"
1687%YAML 1.2
1688---
1689# See http://www.sublimetext.com/docs/3/syntax.html
1690scope: source.example-wp
1691contexts:
1692  main:
1693    - match: a
1694      scope: a
1695      push:
1696        - match: b
1697          scope: b
1698          set:
1699            - match: c
1700              scope: c
1701          with_prototype:
1702            - match: '2'
1703              scope: '2'
1704      with_prototype:
1705        - match: '1'
1706          scope: '1'
1707"#;
1708
1709        let syntax = SyntaxDefinition::load_from_str(syntax_yamlstr, true, None).unwrap();
1710        expect_scope_stacks_with_syntax("abc12", &["<1>", "<2>"], syntax);
1711    }
1712
1713    #[test]
1714    fn can_parse_two_with_prototypes_at_same_stack_level_set_multiple() {
1715        let syntax_yamlstr = r#"
1716%YAML 1.2
1717---
1718# See http://www.sublimetext.com/docs/3/syntax.html
1719scope: source.example-wp
1720contexts:
1721  main:
1722    - match: a
1723      scope: a
1724      push:
1725        - match: b
1726          scope: b
1727          set: [context1, context2, context3]
1728          with_prototype:
1729            - match: '2'
1730              scope: '2'
1731      with_prototype:
1732        - match: '1'
1733          scope: '1'
1734    - match: '1'
1735      scope: digit1
1736    - match: '2'
1737      scope: digit2
1738  context1:
1739    - match: e
1740      scope: e
1741      pop: true
1742    - match: '2'
1743      scope: digit2
1744  context2:
1745    - match: d
1746      scope: d
1747      pop: true
1748    - match: '2'
1749      scope: digit2
1750  context3:
1751    - match: c
1752      scope: c
1753      pop: true
1754"#;
1755
1756        let syntax = SyntaxDefinition::load_from_str(syntax_yamlstr, true, None).unwrap();
1757        expect_scope_stacks_with_syntax("ab12", &["<1>", "<2>"], syntax.clone());
1758        expect_scope_stacks_with_syntax("abc12", &["<1>", "<digit2>"], syntax.clone());
1759        expect_scope_stacks_with_syntax("abcd12", &["<1>", "<digit2>"], syntax.clone());
1760        expect_scope_stacks_with_syntax("abcde12", &["<digit1>", "<digit2>"], syntax);
1761    }
1762
1763    #[test]
1764    fn can_parse_two_with_prototypes_at_same_stack_level_updated_captures() {
1765        let syntax_yamlstr = r#"
1766%YAML 1.2
1767---
1768# See http://www.sublimetext.com/docs/3/syntax.html
1769scope: source.example-wp
1770contexts:
1771  main:
1772    - match: (a)
1773      scope: a
1774      push:
1775        - match: (b)
1776          scope: b
1777          set:
1778            - match: c
1779              scope: c
1780          with_prototype:
1781            - match: d
1782              scope: d
1783      with_prototype:
1784        - match: \1
1785          scope: '1'
1786          pop: true
1787"#;
1788
1789        let syntax = SyntaxDefinition::load_from_str(syntax_yamlstr, true, None).unwrap();
1790        expect_scope_stacks_with_syntax("aa", &["<a>", "<1>"], syntax.clone());
1791        expect_scope_stacks_with_syntax("abcdb", &["<a>", "<b>", "<c>", "<d>", "<1>"], syntax);
1792    }
1793
1794    #[test]
1795    fn can_parse_two_with_prototypes_at_same_stack_level_updated_captures_ignore_unexisting() {
1796        let syntax_yamlstr = r#"
1797%YAML 1.2
1798---
1799# See http://www.sublimetext.com/docs/3/syntax.html
1800scope: source.example-wp
1801contexts:
1802  main:
1803    - match: (a)(-)
1804      scope: a
1805      push:
1806        - match: (b)
1807          scope: b
1808          set:
1809            - match: c
1810              scope: c
1811          with_prototype:
1812            - match: d
1813              scope: d
1814      with_prototype:
1815        - match: \2
1816          scope: '2'
1817          pop: true
1818        - match: \1
1819          scope: '1'
1820          pop: true
1821"#;
1822
1823        let syntax = SyntaxDefinition::load_from_str(syntax_yamlstr, true, None).unwrap();
1824        expect_scope_stacks_with_syntax("a--", &["<a>", "<2>"], syntax.clone());
1825        // it seems that when ST encounters a non existing pop backreference, it just pops back to the with_prototype's original parent context - i.e. cdb is unscoped
1826        // TODO: it would be useful to have syntest functionality available here for easier testing and clarity
1827        expect_scope_stacks_with_syntax("a-bcdba-", &["<a>", "<b>"], syntax);
1828    }
1829
1830    #[test]
1831    fn can_parse_syntax_with_eol_and_newline() {
1832        let syntax = r#"
1833name: test
1834scope: source.test
1835contexts:
1836  main:
1837    - match: foo$\n
1838      scope: foo.newline
1839"#;
1840
1841        let line = "foo";
1842        let expect = ["<source.test>, <foo.newline>"];
1843        expect_scope_stacks(line, &expect, syntax);
1844    }
1845
1846    #[test]
1847    fn can_parse_syntax_with_eol_only() {
1848        let syntax = r#"
1849name: test
1850scope: source.test
1851contexts:
1852  main:
1853    - match: foo$
1854      scope: foo.newline
1855"#;
1856
1857        let line = "foo";
1858        let expect = ["<source.test>, <foo.newline>"];
1859        expect_scope_stacks(line, &expect, syntax);
1860    }
1861
1862    #[test]
1863    fn can_parse_syntax_with_beginning_of_line() {
1864        let syntax = r#"
1865name: test
1866scope: source.test
1867contexts:
1868  main:
1869    - match: \w+
1870      scope: word
1871      push:
1872        # this should not match at the end of the line
1873        - match: ^\s*$
1874          pop: true
1875        - match: =+
1876          scope: heading
1877          pop: true
1878    - match: .*
1879      scope: other
1880"#;
1881
1882        let syntax_newlines = SyntaxDefinition::load_from_str(syntax, true, None).unwrap();
1883        let syntax_set = link(syntax_newlines);
1884
1885        let mut state = ParseState::new(&syntax_set.syntaxes()[0]);
1886        assert_eq!(
1887            ops(&mut state, "foo\n", &syntax_set),
1888            vec![
1889                (0, Push(Scope::new("source.test").unwrap())),
1890                (0, Push(Scope::new("word").unwrap())),
1891                (3, Pop(1))
1892            ]
1893        );
1894        assert_eq!(
1895            ops(&mut state, "===\n", &syntax_set),
1896            vec![(0, Push(Scope::new("heading").unwrap())), (3, Pop(1))]
1897        );
1898
1899        assert_eq!(
1900            ops(&mut state, "bar\n", &syntax_set),
1901            vec![(0, Push(Scope::new("word").unwrap())), (3, Pop(1))]
1902        );
1903        // This should result in popping out of the context
1904        assert_eq!(ops(&mut state, "\n", &syntax_set), vec![]);
1905        // So now this matches other
1906        assert_eq!(
1907            ops(&mut state, "====\n", &syntax_set),
1908            vec![(0, Push(Scope::new("other").unwrap())), (4, Pop(1))]
1909        );
1910    }
1911
1912    #[test]
1913    fn can_parse_syntax_with_comment_and_eol() {
1914        let syntax = r#"
1915name: test
1916scope: source.test
1917contexts:
1918  main:
1919    - match: (//).*$
1920      scope: comment.line.double-slash
1921"#;
1922
1923        let syntax_newlines = SyntaxDefinition::load_from_str(syntax, true, None).unwrap();
1924        let syntax_set = link(syntax_newlines);
1925
1926        let mut state = ParseState::new(&syntax_set.syntaxes()[0]);
1927        assert_eq!(
1928            ops(&mut state, "// foo\n", &syntax_set),
1929            vec![
1930                (0, Push(Scope::new("source.test").unwrap())),
1931                (0, Push(Scope::new("comment.line.double-slash").unwrap())),
1932                // 6 is important here, should not be 7. The pattern should *not* consume the newline,
1933                // but instead match before it. This is important for whitespace-sensitive syntaxes
1934                // where newlines terminate statements such as Scala.
1935                (6, Pop(1))
1936            ]
1937        );
1938    }
1939
1940    #[test]
1941    fn can_parse_text_with_unicode_to_skip() {
1942        let syntax = r#"
1943name: test
1944scope: source.test
1945contexts:
1946  main:
1947    - match: (?=.)
1948      push: test
1949  test:
1950    - match: (?=.)
1951      pop: true
1952    - match: x
1953      scope: test.good
1954"#;
1955
1956        // U+03C0 GREEK SMALL LETTER PI, 2 bytes in UTF-8
1957        expect_scope_stacks("\u{03C0}x", &["<source.test>, <test.good>"], syntax);
1958        // U+0800 SAMARITAN LETTER ALAF, 3 bytes in UTF-8
1959        expect_scope_stacks("\u{0800}x", &["<source.test>, <test.good>"], syntax);
1960        // U+1F600 GRINNING FACE, 4 bytes in UTF-8
1961        expect_scope_stacks("\u{1F600}x", &["<source.test>, <test.good>"], syntax);
1962    }
1963
1964    #[test]
1965    fn can_include_backrefs() {
1966        let syntax = SyntaxDefinition::load_from_str(
1967            r#"
1968                name: Backref Include Test
1969                scope: source.backrefinc
1970                contexts:
1971                  main:
1972                    - match: (a)
1973                      scope: a
1974                      push: context1
1975                  context1:
1976                    - include: context2
1977                  context2:
1978                    - match: \1
1979                      scope: b
1980                      pop: true
1981                "#,
1982            true,
1983            None,
1984        )
1985        .unwrap();
1986
1987        expect_scope_stacks_with_syntax("aa", &["<a>", "<b>"], syntax);
1988    }
1989
1990    #[test]
1991    fn can_include_nested_backrefs() {
1992        let syntax = SyntaxDefinition::load_from_str(
1993            r#"
1994                name: Backref Include Test
1995                scope: source.backrefinc
1996                contexts:
1997                  main:
1998                    - match: (a)
1999                      scope: a
2000                      push: context1
2001                  context1:
2002                    - include: context3
2003                  context3:
2004                    - include: context2
2005                  context2:
2006                    - match: \1
2007                      scope: b
2008                      pop: true
2009                "#,
2010            true,
2011            None,
2012        )
2013        .unwrap();
2014
2015        expect_scope_stacks_with_syntax("aa", &["<a>", "<b>"], syntax);
2016    }
2017
2018    #[test]
2019    fn can_avoid_infinite_stack_depth() {
2020        let syntax = SyntaxDefinition::load_from_str(
2021            r#"
2022                name: Stack Depth Test
2023                scope: source.stack_depth
2024                contexts:
2025                  main:
2026                    - match: (a)
2027                      scope: a
2028                      push: context1
2029
2030                    
2031                  context1:
2032                    - match: b
2033                      scope: b
2034                    - match: ''
2035                      push: context1
2036                    - match: ''
2037                      pop: 1
2038                    - match: c
2039                      scope: c
2040                "#,
2041            true,
2042            None,
2043        )
2044        .unwrap();
2045
2046        let syntax_set = link(syntax);
2047        let mut state = ParseState::new(&syntax_set.syntaxes()[0]);
2048        expect_scope_stacks_for_ops(ops(&mut state, "a bc\n", &syntax_set), &["<a>"]);
2049        expect_scope_stacks_for_ops(ops(&mut state, "bc\n", &syntax_set), &["<b>"]);
2050    }
2051
2052    fn expect_scope_stacks(line_without_newline: &str, expect: &[&str], syntax: &str) {
2053        println!("Parsing with newlines");
2054        let line_with_newline = format!("{}\n", line_without_newline);
2055        let syntax_newlines = SyntaxDefinition::load_from_str(syntax, true, None).unwrap();
2056        expect_scope_stacks_with_syntax(&line_with_newline, expect, syntax_newlines);
2057
2058        println!("Parsing without newlines");
2059        let syntax_nonewlines = SyntaxDefinition::load_from_str(syntax, false, None).unwrap();
2060        expect_scope_stacks_with_syntax(line_without_newline, expect, syntax_nonewlines);
2061    }
2062
2063    fn expect_scope_stacks_with_syntax(line: &str, expect: &[&str], syntax: SyntaxDefinition) {
2064        // check that each expected scope stack appears at least once while parsing the given test line
2065
2066        let syntax_set = link(syntax);
2067        let mut state = ParseState::new(&syntax_set.syntaxes()[0]);
2068        let ops = ops(&mut state, line, &syntax_set);
2069        expect_scope_stacks_for_ops(ops, expect);
2070    }
2071
2072    fn expect_scope_stacks_for_ops(ops: Vec<(usize, ScopeStackOp)>, expect: &[&str]) {
2073        let mut criteria_met = Vec::new();
2074        for stack_str in stack_states(ops) {
2075            println!("{}", stack_str);
2076            for expectation in expect.iter() {
2077                if stack_str.contains(expectation) {
2078                    criteria_met.push(expectation);
2079                }
2080            }
2081        }
2082        if let Some(missing) = expect.iter().find(|e| !criteria_met.contains(e)) {
2083            panic!("expected scope stack '{}' missing", missing);
2084        }
2085    }
2086
2087    fn parse(line: &str, syntax: &str) -> Vec<(usize, ScopeStackOp)> {
2088        let syntax = SyntaxDefinition::load_from_str(syntax, true, None).unwrap();
2089        let syntax_set = link(syntax);
2090
2091        let mut state = ParseState::new(&syntax_set.syntaxes()[0]);
2092        ops(&mut state, line, &syntax_set)
2093    }
2094
2095    fn link(syntax: SyntaxDefinition) -> SyntaxSet {
2096        let mut builder = SyntaxSetBuilder::new();
2097        builder.add(syntax);
2098        builder.build()
2099    }
2100
2101    fn ops(
2102        state: &mut ParseState,
2103        line: &str,
2104        syntax_set: &SyntaxSet,
2105    ) -> Vec<(usize, ScopeStackOp)> {
2106        let ops = state.parse_line(line, syntax_set).expect("#[cfg(test)]");
2107        debug_print_ops(line, &ops);
2108        ops
2109    }
2110
2111    fn stack_states(ops: Vec<(usize, ScopeStackOp)>) -> Vec<String> {
2112        let mut states = Vec::new();
2113        let mut stack = ScopeStack::new();
2114        for (_, op) in ops.iter() {
2115            stack.apply(op).expect("#[cfg(test)]");
2116            let scopes: Vec<String> = stack
2117                .as_slice()
2118                .iter()
2119                .map(|s| format!("{:?}", s))
2120                .collect();
2121            let stack_str = scopes.join(", ");
2122            states.push(stack_str);
2123        }
2124        states
2125    }
2126}