Skip to main content

syntect/parsing/
syntax_set.rs

1use super::scope::*;
2use super::syntax_definition::*;
3use super::ParsingError;
4
5#[cfg(feature = "metadata")]
6use super::metadata::{LoadMetadata, Metadata, RawMetadataEntry};
7
8#[cfg(feature = "yaml-load")]
9use super::super::LoadingError;
10
11use std::collections::{BTreeSet, HashMap, HashSet};
12use std::fs::File;
13use std::io::{self, BufRead, BufReader};
14use std::mem;
15use std::ops::DerefMut;
16use std::path::Path;
17
18use super::regex::Regex;
19use crate::parsing::syntax_definition::ContextId;
20use serde_derive::{Deserialize, Serialize};
21use std::sync::OnceLock;
22
23/// A syntax set holds multiple syntaxes that have been linked together.
24///
25/// Use a [`SyntaxSetBuilder`] to load syntax definitions and build a syntax set.
26///
27/// After building, the syntax set is immutable and can no longer be modified, but you can convert
28/// it back into a builder by using the [`into_builder`] method.
29///
30/// [`SyntaxSetBuilder`]: struct.SyntaxSetBuilder.html
31/// [`into_builder`]: #method.into_builder
32#[derive(Debug, Serialize, Deserialize)]
33pub struct SyntaxSet {
34    syntaxes: Vec<SyntaxReference>,
35    /// Stores the syntax index for every path that was loaded
36    path_syntaxes: Vec<(String, usize)>,
37
38    /// Warnings collected during syntax loading and linking.
39    #[serde(skip_serializing, skip_deserializing, default)]
40    warnings: Vec<String>,
41
42    #[serde(skip_serializing, skip_deserializing, default = "OnceLock::new")]
43    first_line_cache: OnceLock<FirstLineCache>,
44    /// Metadata, e.g. indent and commenting information.
45    ///
46    /// NOTE: if serializing, you should handle metadata manually; that is, you should serialize and
47    /// deserialize it separately. See `examples/gendata.rs` for an example.
48    #[cfg(feature = "metadata")]
49    #[serde(skip, default)]
50    pub(crate) metadata: Metadata,
51}
52
53/// A linked version of a [`SyntaxDefinition`] that is only useful as part of the
54/// [`SyntaxSet`] that contains it. See docs for [`SyntaxSetBuilder::build`] for
55/// more info.
56#[derive(Clone, Debug, Serialize, Deserialize)]
57pub struct SyntaxReference {
58    pub name: String,
59    pub file_extensions: Vec<String>,
60    pub scope: Scope,
61    pub first_line_match: Option<String>,
62    pub hidden: bool,
63    #[serde(serialize_with = "ordered_map")]
64    pub variables: HashMap<String, String>,
65    /// The version of the sublime-syntax format (1 or 2). Default is 1.
66    #[serde(default = "default_syntax_version")]
67    pub version: u32,
68    #[serde(skip)]
69    pub(crate) lazy_contexts: OnceLock<LazyContexts>,
70    pub(crate) serialized_lazy_contexts: Vec<u8>,
71}
72
73fn default_syntax_version() -> u32 {
74    1
75}
76
77/// The lazy-loaded parts of a [`SyntaxReference`].
78#[derive(Clone, Debug, Serialize, Deserialize)]
79pub(crate) struct LazyContexts {
80    #[serde(serialize_with = "ordered_map")]
81    pub(crate) context_ids: HashMap<String, ContextId>,
82    pub(crate) contexts: Vec<Context>,
83}
84
85/// A syntax set builder is used for loading syntax definitions from the file
86/// system or by adding [`SyntaxDefinition`] objects.
87///
88/// Once all the syntaxes have been added, call [`build`] to turn the builder into
89/// a [`SyntaxSet`] that can be used for parsing or highlighting.
90///
91/// [`SyntaxDefinition`]: syntax_definition/struct.SyntaxDefinition.html
92/// [`build`]: #method.build
93/// [`SyntaxSet`]: struct.SyntaxSet.html
94#[derive(Clone, Default)]
95pub struct SyntaxSetBuilder {
96    syntaxes: Vec<SyntaxDefinition>,
97    path_syntaxes: Vec<(String, usize)>,
98    warnings: Vec<String>,
99    /// Tracks the `lines_include_newline` flag from the most recent
100    /// `add_from_folder` call. Used by `resolve_extends` to re-resolve
101    /// regexes with the correct newline mode after merging parent variables.
102    lines_include_newline: bool,
103    #[cfg(feature = "metadata")]
104    raw_metadata: LoadMetadata,
105
106    /// If this `SyntaxSetBuilder` is created with `SyntaxSet::into_builder`
107    /// from a `SyntaxSet` that already had metadata, we keep that metadata,
108    /// merging it with newly loaded metadata.
109    #[cfg(feature = "metadata")]
110    existing_metadata: Option<Metadata>,
111}
112
113#[cfg(feature = "yaml-load")]
114fn load_syntax_file(
115    p: &Path,
116    lines_include_newline: bool,
117) -> Result<SyntaxDefinition, LoadingError> {
118    let s = std::fs::read_to_string(p)?;
119
120    SyntaxDefinition::load_from_str(
121        &s,
122        lines_include_newline,
123        p.file_stem().and_then(|x| x.to_str()),
124    )
125    .map_err(|e| LoadingError::ParseSyntax(e, format!("{}", p.display())))
126}
127
128impl Clone for SyntaxSet {
129    fn clone(&self) -> SyntaxSet {
130        SyntaxSet {
131            syntaxes: self.syntaxes.clone(),
132            path_syntaxes: self.path_syntaxes.clone(),
133            warnings: self.warnings.clone(),
134            // Will need to be re-initialized
135            first_line_cache: OnceLock::new(),
136            #[cfg(feature = "metadata")]
137            metadata: self.metadata.clone(),
138        }
139    }
140}
141
142impl Default for SyntaxSet {
143    fn default() -> Self {
144        SyntaxSet {
145            syntaxes: Vec::new(),
146            path_syntaxes: Vec::new(),
147            warnings: Vec::new(),
148            first_line_cache: OnceLock::new(),
149            #[cfg(feature = "metadata")]
150            metadata: Metadata::default(),
151        }
152    }
153}
154
155impl SyntaxSet {
156    pub fn new() -> SyntaxSet {
157        SyntaxSet::default()
158    }
159
160    /// Convenience constructor for creating a builder, then loading syntax
161    /// definitions from a folder and then building the syntax set.
162    ///
163    /// Note that this uses `lines_include_newline` set to `false`, see the
164    /// [`add_from_folder`] method docs on [`SyntaxSetBuilder`] for an explanation
165    /// as to why this might not be the best.
166    ///
167    /// [`add_from_folder`]: struct.SyntaxSetBuilder.html#method.add_from_folder
168    /// [`SyntaxSetBuilder`]: struct.SyntaxSetBuilder.html
169    #[cfg(feature = "yaml-load")]
170    pub fn load_from_folder<P: AsRef<Path>>(folder: P) -> Result<SyntaxSet, LoadingError> {
171        let mut builder = SyntaxSetBuilder::new();
172        builder.add_from_folder(folder, false)?;
173        Ok(builder.build())
174    }
175
176    /// Warnings collected during syntax loading and linking.
177    ///
178    /// These include issues like skipped files, version mismatches, and
179    /// unresolved `extends` references.
180    pub fn warnings(&self) -> &[String] {
181        &self.warnings
182    }
183
184    /// The list of syntaxes in the set
185    pub fn syntaxes(&self) -> &[SyntaxReference] {
186        &self.syntaxes[..]
187    }
188
189    #[cfg(feature = "metadata")]
190    pub fn set_metadata(&mut self, metadata: Metadata) {
191        self.metadata = metadata;
192    }
193
194    /// The loaded metadata for this set.
195    #[cfg(feature = "metadata")]
196    pub fn metadata(&self) -> &Metadata {
197        &self.metadata
198    }
199
200    /// Finds a syntax by its default scope, for example `source.regexp` finds the regex syntax.
201    ///
202    /// This and all similar methods below do a linear search of syntaxes, this should be fast
203    /// because there aren't many syntaxes, but don't think you can call it a bajillion times per
204    /// second.
205    pub fn find_syntax_by_scope(&self, scope: Scope) -> Option<&SyntaxReference> {
206        self.syntaxes.iter().rev().find(|&s| s.scope == scope)
207    }
208
209    pub fn find_syntax_by_name<'a>(&'a self, name: &str) -> Option<&'a SyntaxReference> {
210        self.syntaxes.iter().rev().find(|&s| name == s.name)
211    }
212
213    pub fn find_syntax_by_extension<'a>(&'a self, extension: &str) -> Option<&'a SyntaxReference> {
214        self.syntaxes.iter().rev().find(|&s| {
215            s.file_extensions
216                .iter()
217                .any(|e| e.eq_ignore_ascii_case(extension))
218        })
219    }
220
221    /// Searches for a syntax first by extension and then by case-insensitive name
222    ///
223    /// This is useful for things like Github-flavoured-markdown code block highlighting where all
224    /// you have to go on is a short token given by the user
225    pub fn find_syntax_by_token<'a>(&'a self, s: &str) -> Option<&'a SyntaxReference> {
226        {
227            let ext_res = self.find_syntax_by_extension(s);
228            if ext_res.is_some() {
229                return ext_res;
230            }
231        }
232        self.syntaxes
233            .iter()
234            .rev()
235            .find(|&syntax| syntax.name.eq_ignore_ascii_case(s))
236    }
237
238    /// Try to find the syntax for a file based on its first line
239    ///
240    /// This uses regexes that come with some sublime syntax grammars for matching things like
241    /// shebangs and mode lines like `-*- Mode: C -*-`
242    pub fn find_syntax_by_first_line<'a>(&'a self, s: &str) -> Option<&'a SyntaxReference> {
243        let s = s.strip_prefix("\u{feff}").unwrap_or(s); // Strip UTF-8 BOM
244        let cache = self.first_line_cache();
245        for &(ref reg, i) in cache.regexes.iter().rev() {
246            if reg.search(s, 0, s.len(), None, true) {
247                return Some(&self.syntaxes[i]);
248            }
249        }
250        None
251    }
252
253    /// Searches for a syntax by it's original file path when it was first loaded from disk
254    ///
255    /// This is primarily useful for syntax tests. Some may specify a
256    /// `Packages/PackageName/SyntaxName.sublime-syntax` path, and others may just have
257    /// `SyntaxName.sublime-syntax`. This caters for these by matching the end of the path of the
258    /// loaded syntax definition files
259    // however, if a syntax name is provided without a folder, make sure we don't accidentally match the end of a different syntax definition's name - by checking a / comes before it or it is the full path
260    pub fn find_syntax_by_path<'a>(&'a self, path: &str) -> Option<&'a SyntaxReference> {
261        let mut slash_path = "/".to_string();
262        slash_path.push_str(path);
263        self.path_syntaxes
264            .iter()
265            .rev()
266            .find(|t| t.0.ends_with(&slash_path) || t.0 == path)
267            .map(|&(_, i)| &self.syntaxes[i])
268    }
269
270    /// Convenience method that tries to find the syntax for a file path, first by extension/name
271    /// and then by first line of the file if that doesn't work.
272    ///
273    /// May IO Error because it sometimes tries to read the first line of the file.
274    ///
275    /// # Examples
276    ///
277    /// When determining how to highlight a file, use this in combination with a fallback to plain
278    /// text:
279    ///
280    /// ```
281    /// use syntect::parsing::SyntaxSet;
282    /// let ss = SyntaxSet::load_defaults_newlines();
283    /// let syntax = ss.find_syntax_for_file("testdata/highlight_test.erb")
284    ///     .unwrap() // for IO errors, you may want to use try!() or another plain text fallback
285    ///     .unwrap_or_else(|| ss.find_syntax_plain_text());
286    /// assert_eq!(syntax.name, "HTML (Rails)");
287    /// ```
288    pub fn find_syntax_for_file<P: AsRef<Path>>(
289        &self,
290        path_obj: P,
291    ) -> io::Result<Option<&SyntaxReference>> {
292        let path: &Path = path_obj.as_ref();
293        let file_name = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
294        let extension = path.extension().and_then(|x| x.to_str()).unwrap_or("");
295        let ext_syntax = self
296            .find_syntax_by_extension(file_name)
297            .or_else(|| self.find_syntax_by_extension(extension));
298        let line_syntax = if ext_syntax.is_none() {
299            let mut line = String::new();
300            let f = File::open(path)?;
301            let mut line_reader = BufReader::new(&f);
302            line_reader.read_line(&mut line)?;
303            self.find_syntax_by_first_line(&line)
304        } else {
305            None
306        };
307        let syntax = ext_syntax.or(line_syntax);
308        Ok(syntax)
309    }
310
311    /// Finds a syntax for plain text, which usually has no highlighting rules.
312    ///
313    /// This is good as a fallback when you can't find another syntax but you still want to use the
314    /// same highlighting pipeline code.
315    ///
316    /// This syntax should always be present, if not this method will panic. If the way you load
317    /// syntaxes doesn't create one, use [`add_plain_text_syntax`].
318    ///
319    /// # Examples
320    /// ```
321    /// use syntect::parsing::SyntaxSetBuilder;
322    /// let mut builder = SyntaxSetBuilder::new();
323    /// builder.add_plain_text_syntax();
324    /// let ss = builder.build();
325    /// let syntax = ss.find_syntax_by_token("rs").unwrap_or_else(|| ss.find_syntax_plain_text());
326    /// assert_eq!(syntax.name, "Plain Text");
327    /// ```
328    ///
329    /// [`add_plain_text_syntax`]: struct.SyntaxSetBuilder.html#method.add_plain_text_syntax
330    pub fn find_syntax_plain_text(&self) -> &SyntaxReference {
331        self.find_syntax_by_name("Plain Text")
332            .expect("All syntax sets ought to have a plain text syntax")
333    }
334
335    /// Converts this syntax set into a builder so that more syntaxes can be
336    /// added to it.
337    ///
338    /// Note that newly added syntaxes can have references to existing syntaxes
339    /// in the set, but not the other way around.
340    pub fn into_builder(self) -> SyntaxSetBuilder {
341        #[cfg(feature = "metadata")]
342        let SyntaxSet {
343            syntaxes,
344            path_syntaxes,
345            metadata,
346            ..
347        } = self;
348        #[cfg(not(feature = "metadata"))]
349        let SyntaxSet {
350            syntaxes,
351            path_syntaxes,
352            ..
353        } = self;
354
355        let mut context_map = HashMap::new();
356        for (syntax_index, syntax) in syntaxes.iter().enumerate() {
357            for (context_index, context) in syntax.contexts().iter().enumerate() {
358                context_map.insert(
359                    ContextId {
360                        syntax_index,
361                        context_index,
362                    },
363                    context.clone(),
364                );
365            }
366        }
367
368        let mut builder_syntaxes = Vec::with_capacity(syntaxes.len());
369
370        for syntax in syntaxes {
371            let SyntaxReference {
372                name,
373                file_extensions,
374                scope,
375                first_line_match,
376                hidden,
377                variables,
378                version,
379                serialized_lazy_contexts,
380                ..
381            } = syntax;
382
383            let lazy_contexts = LazyContexts::deserialize(&serialized_lazy_contexts[..]);
384            let mut builder_contexts = HashMap::with_capacity(lazy_contexts.context_ids.len());
385            for (name, context_id) in lazy_contexts.context_ids {
386                if let Some(context) = context_map.remove(&context_id) {
387                    builder_contexts.insert(name, context);
388                }
389            }
390
391            let syntax_definition = SyntaxDefinition {
392                name,
393                file_extensions,
394                scope,
395                first_line_match,
396                hidden,
397                variables,
398                contexts: builder_contexts,
399                extends: vec![],
400                version,
401            };
402            builder_syntaxes.push(syntax_definition);
403        }
404
405        SyntaxSetBuilder {
406            syntaxes: builder_syntaxes,
407            path_syntaxes,
408            warnings: Vec::new(),
409            lines_include_newline: false,
410            #[cfg(feature = "metadata")]
411            existing_metadata: Some(metadata),
412            #[cfg(feature = "metadata")]
413            raw_metadata: LoadMetadata::default(),
414        }
415    }
416
417    #[inline(always)]
418    pub(crate) fn get_context(&self, context_id: &ContextId) -> Result<&Context, ParsingError> {
419        let syntax = &self
420            .syntaxes
421            .get(context_id.syntax_index)
422            .ok_or(ParsingError::MissingContext(*context_id))?;
423        syntax
424            .contexts()
425            .get(context_id.context_index)
426            .ok_or(ParsingError::MissingContext(*context_id))
427    }
428
429    fn first_line_cache(&self) -> &FirstLineCache {
430        self.first_line_cache
431            .get_or_init(|| FirstLineCache::new(self.syntaxes()))
432    }
433
434    pub fn find_unlinked_contexts(&self) -> BTreeSet<String> {
435        let SyntaxSet { syntaxes, .. } = self;
436
437        let mut unlinked_contexts = BTreeSet::new();
438
439        for syntax in syntaxes {
440            let SyntaxReference { name, scope, .. } = syntax;
441
442            for context in syntax.contexts() {
443                Self::find_unlinked_contexts_in_context(
444                    name,
445                    scope,
446                    context,
447                    &mut unlinked_contexts,
448                );
449            }
450        }
451        unlinked_contexts
452    }
453
454    fn find_unlinked_contexts_in_context(
455        name: &str,
456        scope: &Scope,
457        context: &Context,
458        unlinked_contexts: &mut BTreeSet<String>,
459    ) {
460        for pattern in context.patterns.iter() {
461            let maybe_refs_to_check = match pattern {
462                Pattern::Match(match_pat) => match &match_pat.operation {
463                    MatchOperation::Push(context_refs)
464                    | MatchOperation::Set {
465                        ctx_refs: context_refs,
466                        ..
467                    }
468                    | MatchOperation::Branch {
469                        alternatives: context_refs,
470                        ..
471                    } => Some(context_refs),
472                    MatchOperation::Embed { ref contexts, .. } => Some(contexts),
473                    MatchOperation::Pop(_) | MatchOperation::None | MatchOperation::Fail(_) => None,
474                },
475                _ => None,
476            };
477            for context_ref in maybe_refs_to_check.into_iter().flatten() {
478                match context_ref {
479                    ContextReference::Direct(_) => {}
480                    _ => {
481                        unlinked_contexts.insert(format!(
482                            "Syntax '{}' with scope '{}' has unresolved context reference {:?}",
483                            name, scope, &context_ref
484                        ));
485                    }
486                }
487            }
488        }
489    }
490}
491
492impl SyntaxReference {
493    pub(crate) fn context_ids(&self) -> &HashMap<String, ContextId> {
494        &self.lazy_contexts().context_ids
495    }
496
497    fn contexts(&self) -> &[Context] {
498        &self.lazy_contexts().contexts
499    }
500
501    fn lazy_contexts(&self) -> &LazyContexts {
502        self.lazy_contexts
503            .get_or_init(|| LazyContexts::deserialize(&self.serialized_lazy_contexts[..]))
504    }
505}
506
507impl LazyContexts {
508    fn deserialize(data: &[u8]) -> LazyContexts {
509        crate::dumps::from_reader(data).expect("data is not corrupt or out of sync with the code")
510    }
511}
512
513impl SyntaxSetBuilder {
514    pub fn new() -> SyntaxSetBuilder {
515        SyntaxSetBuilder::default()
516    }
517
518    /// Add a syntax to the set.
519    pub fn add(&mut self, syntax: SyntaxDefinition) {
520        self.syntaxes.push(syntax);
521    }
522
523    /// The list of syntaxes added so far.
524    pub fn syntaxes(&self) -> &[SyntaxDefinition] {
525        &self.syntaxes[..]
526    }
527
528    /// Warnings collected during syntax loading and linking.
529    ///
530    /// These include issues like skipped files, version mismatches, and
531    /// unresolved `extends` references that were previously printed to stderr.
532    pub fn warnings(&self) -> &[String] {
533        &self.warnings
534    }
535
536    /// A rarely useful method that loads in a syntax with no highlighting rules for plain text
537    ///
538    /// Exists mainly for adding the plain text syntax to syntax set dumps, because for some reason
539    /// the default Sublime plain text syntax is still in `.tmLanguage` format.
540    #[cfg(feature = "yaml-load")]
541    pub fn add_plain_text_syntax(&mut self) {
542        let s = "---\nname: Plain Text\nfile_extensions: [txt]\nscope: text.plain\ncontexts: \
543                 {main: []}";
544        let syn = SyntaxDefinition::load_from_str(s, false, None).unwrap();
545        self.syntaxes.push(syn);
546    }
547
548    /// Loads all the `.sublime-syntax` files in a folder into this builder.
549    ///
550    /// The `lines_include_newline` parameter is used to work around the fact that Sublime Text
551    /// normally passes line strings including newline characters (`\n`) to its regex engine. This
552    /// results in many syntaxes having regexes matching `\n`, which doesn't work if you don't pass
553    /// in newlines. It is recommended that if you can you pass in lines with newlines if you can
554    /// and pass `true` for this parameter. If that is inconvenient pass `false` and the loader
555    /// will do some hacky find and replaces on the match regexes that seem to work for the default
556    /// syntax set, but may not work for any other syntaxes.
557    ///
558    /// In the future I might include a "slow mode" that copies the lines passed in and appends a
559    /// newline if there isn't one, but in the interest of performance currently this hacky fix will
560    /// have to do.
561    #[cfg(feature = "yaml-load")]
562    pub fn add_from_folder<P: AsRef<Path>>(
563        &mut self,
564        folder: P,
565        lines_include_newline: bool,
566    ) -> Result<(), LoadingError> {
567        self.lines_include_newline = lines_include_newline;
568        for entry in crate::utils::walk_dir(folder).sort_by(|a, b| a.file_name().cmp(b.file_name()))
569        {
570            let entry = entry.map_err(|e| LoadingError::WalkDir(Box::new(e)))?;
571            if entry
572                .path()
573                .extension()
574                .is_some_and(|e| e == "sublime-syntax")
575            {
576                match load_syntax_file(entry.path(), lines_include_newline) {
577                    Ok(syntax) => {
578                        if let Some(path_str) = entry.path().to_str() {
579                            // Split the path up and rejoin with slashes so that syntaxes loaded on Windows
580                            // can still be loaded the same way.
581                            let path = Path::new(path_str);
582                            let path_parts: Vec<_> =
583                                path.iter().map(|c| c.to_str().unwrap()).collect();
584                            self.path_syntaxes
585                                .push((path_parts.join("/").to_string(), self.syntaxes.len()));
586                        }
587                        self.syntaxes.push(syntax);
588                    }
589                    Err(err) => {
590                        self.warnings
591                            .push(format!("skipping {:?}: {}", entry.path(), err));
592                    }
593                }
594            }
595
596            #[cfg(feature = "metadata")]
597            {
598                if entry.path().extension() == Some("tmPreferences".as_ref()) {
599                    match RawMetadataEntry::load(entry.path()) {
600                        Ok(meta) => self.raw_metadata.add_raw(meta),
601                        Err(_err) => (),
602                    }
603                }
604            }
605        }
606
607        Ok(())
608    }
609
610    /// Build a [`SyntaxSet`] from the syntaxes that have been added to this
611    /// builder.
612    ///
613    /// ### Linking
614    ///
615    /// The contexts in syntaxes can reference other contexts in the same syntax
616    /// or even other syntaxes. For example, a HTML syntax can reference a CSS
617    /// syntax so that CSS blocks in HTML work as expected.
618    ///
619    /// Those references work in various ways and involve one or two lookups.
620    /// To avoid having to do these lookups during parsing/highlighting, the
621    /// references are changed to directly reference contexts via index. That's
622    /// called linking.
623    ///
624    /// Linking is done in this build step. So in order to get the best
625    /// performance, you should try to avoid calling this too much. Ideally,
626    /// create a [`SyntaxSet`] once and then use it many times. If you can,
627    /// serialize a [`SyntaxSet`] for your program and when you run the program,
628    /// directly load the [`SyntaxSet`].
629    ///
630    /// [`SyntaxSet`]: struct.SyntaxSet.html
631    pub fn build(self) -> SyntaxSet {
632        #[cfg(not(feature = "metadata"))]
633        let SyntaxSetBuilder {
634            syntaxes: syntax_definitions,
635            path_syntaxes,
636            mut warnings,
637            lines_include_newline,
638        } = self;
639        #[cfg(feature = "metadata")]
640        let SyntaxSetBuilder {
641            syntaxes: syntax_definitions,
642            path_syntaxes,
643            mut warnings,
644            lines_include_newline,
645            raw_metadata,
646            existing_metadata,
647        } = self;
648
649        // Extends resolution phase: merge parent contexts/variables into children
650        let (syntax_definitions, extends_warnings) =
651            Self::resolve_extends(syntax_definitions, &path_syntaxes, lines_include_newline);
652        warnings.extend(extends_warnings);
653
654        let mut syntaxes = Vec::with_capacity(syntax_definitions.len());
655        let mut all_context_ids = Vec::new();
656        let mut all_contexts = vec![Vec::new(); syntax_definitions.len()];
657
658        for (syntax_index, syntax_definition) in syntax_definitions.into_iter().enumerate() {
659            let SyntaxDefinition {
660                name,
661                file_extensions,
662                scope,
663                first_line_match,
664                hidden,
665                variables,
666                contexts,
667                extends: _,
668                version,
669            } = syntax_definition;
670
671            let mut context_ids = HashMap::new();
672
673            let mut contexts: Vec<(String, Context)> = contexts.into_iter().collect();
674            // Sort the values of the HashMap so that the contexts in the
675            // resulting SyntaxSet have a deterministic order for serializing.
676            // Because we're sorting by the keys which are unique, we can use
677            // an unstable sort.
678            contexts.sort_unstable_by(|(name_a, _), (name_b, _)| name_a.cmp(name_b));
679            for (name, context) in contexts {
680                let context_index = all_contexts[syntax_index].len();
681                context_ids.insert(
682                    name,
683                    ContextId {
684                        syntax_index,
685                        context_index,
686                    },
687                );
688                all_contexts[syntax_index].push(context);
689            }
690
691            let syntax = SyntaxReference {
692                name,
693                file_extensions,
694                scope,
695                first_line_match,
696                hidden,
697                variables,
698                version,
699                lazy_contexts: OnceLock::new(),
700                serialized_lazy_contexts: Vec::new(), // initialized in the last step
701            };
702            syntaxes.push(syntax);
703            all_context_ids.push(context_ids);
704        }
705
706        let mut found_more_backref_includes = true;
707        for (syntax_index, _syntax) in syntaxes.iter().enumerate() {
708            let mut no_prototype = HashSet::new();
709            let prototype = all_context_ids[syntax_index].get("prototype");
710            if let Some(prototype_id) = prototype {
711                // TODO: We could do this after parsing YAML, instead of here?
712                Self::recursively_mark_no_prototype(
713                    prototype_id,
714                    &all_context_ids[syntax_index],
715                    &all_contexts,
716                    &mut no_prototype,
717                );
718            }
719
720            for context_id in all_context_ids[syntax_index].values() {
721                let context = &mut all_contexts[context_id.syntax_index][context_id.context_index];
722                if let Some(prototype_id) = prototype {
723                    if context.meta_include_prototype.unwrap_or(true)
724                        && !no_prototype.contains(context_id)
725                    {
726                        context.prototype = Some(*prototype_id);
727                    }
728                }
729                Self::link_context(context, syntax_index, &all_context_ids, &syntaxes);
730
731                if context.uses_backrefs {
732                    found_more_backref_includes = true;
733                }
734            }
735        }
736
737        // We need to recursively mark contexts that include contexts which
738        // use backreferences as using backreferences. In theory we could use
739        // a more efficient method here like doing a toposort or constructing
740        // a representation with reversed edges and then tracing in the
741        // opposite direction, but I benchmarked this and it adds <2% to link
742        // time on the default syntax set, and linking doesn't even happen
743        // when loading from a binary dump.
744        while found_more_backref_includes {
745            found_more_backref_includes = false;
746            // find any contexts which include a context which uses backrefs
747            // and mark those as using backrefs - to support nested includes
748            for syntax_index in 0..syntaxes.len() {
749                for context_index in 0..all_contexts[syntax_index].len() {
750                    let context = &all_contexts[syntax_index][context_index];
751                    if !context.uses_backrefs && context.patterns.iter().any(|pattern| {
752                        matches!(pattern, Pattern::Include(ContextReference::Direct(id)) | Pattern::IncludeWithPrototype(ContextReference::Direct(id)) if all_contexts[id.syntax_index][id.context_index].uses_backrefs)
753                    }) {
754                        let context = &mut all_contexts[syntax_index][context_index];
755                        context.uses_backrefs = true;
756                        // look for contexts including this context
757                        found_more_backref_includes = true;
758                    }
759                }
760            }
761        }
762
763        #[cfg(feature = "metadata")]
764        let metadata = match existing_metadata {
765            Some(existing) => existing.merged_with_raw(raw_metadata),
766            None => raw_metadata.into(),
767        };
768
769        // The combination of
770        //  * the algorithms above
771        //  * the borrow checker
772        // makes it necessary to set these up as the last step.
773        for syntax in &mut syntaxes {
774            let lazy_contexts = LazyContexts {
775                context_ids: all_context_ids.remove(0),
776                contexts: all_contexts.remove(0),
777            };
778
779            syntax.serialized_lazy_contexts = crate::dumps::dump_binary(&lazy_contexts);
780        }
781
782        SyntaxSet {
783            syntaxes,
784            path_syntaxes,
785            warnings,
786            first_line_cache: OnceLock::new(),
787            #[cfg(feature = "metadata")]
788            metadata,
789        }
790    }
791
792    /// No-op extends resolution when yaml-load feature is not available.
793    #[cfg(not(feature = "yaml-load"))]
794    fn resolve_extends(
795        syntax_definitions: Vec<SyntaxDefinition>,
796        _path_syntaxes: &[(String, usize)],
797        _lines_include_newline: bool,
798    ) -> (Vec<SyntaxDefinition>, Vec<String>) {
799        (syntax_definitions, Vec::new())
800    }
801
802    /// Resolve `extends` relationships between syntax definitions.
803    ///
804    /// For each child syntax that extends a parent, merge the parent's contexts and variables
805    /// into the child. Respects `meta_prepend`/`meta_append` merge modes.
806    #[cfg(feature = "yaml-load")]
807    fn resolve_extends(
808        mut syntax_definitions: Vec<SyntaxDefinition>,
809        path_syntaxes: &[(String, usize)],
810        lines_include_newline: bool,
811    ) -> (Vec<SyntaxDefinition>, Vec<String>) {
812        let mut warnings = Vec::new();
813
814        // Build lookup maps: name -> index and path-suffix -> index
815        let mut name_to_index: HashMap<String, usize> = HashMap::new();
816        for (i, sd) in syntax_definitions.iter().enumerate() {
817            name_to_index.insert(sd.name.clone(), i);
818        }
819
820        // Track which syntaxes need extends resolution
821        let mut unresolved: HashSet<usize> = HashSet::new();
822        for (i, sd) in syntax_definitions.iter().enumerate() {
823            if !sd.extends.is_empty() {
824                unresolved.insert(i);
825            }
826        }
827
828        if unresolved.is_empty() {
829            return (syntax_definitions, warnings);
830        }
831
832        // Track root ancestor for each syntax (syntaxes with no extends are their own root)
833        let mut syntax_roots: HashMap<usize, usize> = HashMap::new();
834        for (i, sd) in syntax_definitions.iter().enumerate() {
835            if sd.extends.is_empty() {
836                syntax_roots.insert(i, i);
837            }
838        }
839
840        // Fixed-point loop: resolve extends iteratively (to handle chains and multiple parents)
841        let mut made_progress = true;
842        while made_progress && !unresolved.is_empty() {
843            made_progress = false;
844
845            let still_unresolved: Vec<usize> = unresolved.iter().copied().collect();
846            for child_idx in still_unresolved {
847                let extends_paths = syntax_definitions[child_idx].extends.clone();
848                if extends_paths.is_empty() {
849                    continue;
850                }
851
852                // Find all parent indices; skip if any parent is not found or unresolved
853                let mut parent_indices = Vec::with_capacity(extends_paths.len());
854                let mut all_parents_ready = true;
855                for extends_path in &extends_paths {
856                    let parent_idx = Self::find_parent_index(
857                        extends_path,
858                        path_syntaxes,
859                        &syntax_definitions,
860                        &name_to_index,
861                    );
862                    match parent_idx {
863                        Some(idx) if !unresolved.contains(&idx) => {
864                            parent_indices.push(idx);
865                        }
866                        _ => {
867                            all_parents_ready = false;
868                            break;
869                        }
870                    }
871                }
872
873                if !all_parents_ready {
874                    continue;
875                }
876
877                // H & I: all parents must share the same version as the child
878                let child_version = syntax_definitions[child_idx].version;
879                let version_ok = parent_indices
880                    .iter()
881                    .all(|&pi| syntax_definitions[pi].version == child_version);
882                if !version_ok {
883                    warnings.push(format!(
884                        "syntax '{}' has a version mismatch with one or more parents; \
885                         extends will not be applied",
886                        syntax_definitions[child_idx].name
887                    ));
888                    unresolved.remove(&child_idx);
889                    syntax_roots.insert(child_idx, child_idx);
890                    made_progress = true;
891                    continue;
892                }
893
894                // G: for multiple parents, all must share the same root ancestor
895                let parent_roots: Vec<usize> = parent_indices
896                    .iter()
897                    .map(|&pi| *syntax_roots.get(&pi).unwrap_or(&pi))
898                    .collect();
899                let common_root = parent_roots[0];
900                if !parent_roots.iter().all(|&r| r == common_root) {
901                    warnings.push(format!(
902                        "syntax '{}' extends parents that derive from different base syntaxes; \
903                         extends will not be applied",
904                        syntax_definitions[child_idx].name
905                    ));
906                    unresolved.remove(&child_idx);
907                    syntax_roots.insert(child_idx, child_idx);
908                    made_progress = true;
909                    continue;
910                }
911
912                // Merge all parents left-to-right: later parent overrides earlier
913                let mut merged_variables: HashMap<String, String> = HashMap::new();
914                let mut merged_contexts: HashMap<String, Context> = HashMap::new();
915
916                for &parent_idx in &parent_indices {
917                    let parent_variables = syntax_definitions[parent_idx].variables.clone();
918                    let parent_contexts = syntax_definitions[parent_idx].contexts.clone();
919
920                    // Merge variables: later parent overrides earlier
921                    for (k, v) in parent_variables {
922                        merged_variables.insert(k, v);
923                    }
924
925                    // Merge contexts: later parent overrides earlier
926                    for (ctx_name, parent_ctx) in parent_contexts {
927                        merged_contexts.insert(ctx_name, parent_ctx);
928                    }
929                }
930
931                let child = &mut syntax_definitions[child_idx];
932
933                // Child variables override merged parent variables
934                let child_variables: HashMap<String, String> = child.variables.drain().collect();
935                for (k, v) in child_variables {
936                    merged_variables.insert(k, v);
937                }
938                child.variables = merged_variables;
939
940                // Merge contexts: child applies merge_mode against merged parent result
941                for (ctx_name, parent_ctx) in merged_contexts {
942                    if let Some(child_ctx) = child.contexts.get_mut(&ctx_name) {
943                        match child_ctx.merge_mode {
944                            ContextMergeMode::Replace => {
945                                // Child's version wins, keep as-is
946                            }
947                            ContextMergeMode::Prepend => {
948                                // child patterns + parent patterns
949                                let mut merged_patterns = child_ctx.patterns.clone();
950                                merged_patterns.extend(parent_ctx.patterns);
951                                child_ctx.patterns = merged_patterns;
952                                if child_ctx.meta_scope.is_empty() {
953                                    child_ctx.meta_scope = parent_ctx.meta_scope;
954                                }
955                                if child_ctx.meta_content_scope.is_empty() {
956                                    child_ctx.meta_content_scope = parent_ctx.meta_content_scope;
957                                }
958                                if child_ctx.clear_scopes.is_none() {
959                                    child_ctx.clear_scopes = parent_ctx.clear_scopes;
960                                }
961                                if child_ctx.meta_include_prototype.is_none() {
962                                    child_ctx.meta_include_prototype =
963                                        parent_ctx.meta_include_prototype;
964                                }
965                            }
966                            ContextMergeMode::Append => {
967                                // parent patterns + child patterns
968                                let child_patterns = child_ctx.patterns.clone();
969                                child_ctx.patterns = parent_ctx.patterns;
970                                child_ctx.patterns.extend(child_patterns);
971                                if child_ctx.meta_scope.is_empty() {
972                                    child_ctx.meta_scope = parent_ctx.meta_scope;
973                                }
974                                if child_ctx.meta_content_scope.is_empty() {
975                                    child_ctx.meta_content_scope = parent_ctx.meta_content_scope;
976                                }
977                                if child_ctx.clear_scopes.is_none() {
978                                    child_ctx.clear_scopes = parent_ctx.clear_scopes;
979                                }
980                                if child_ctx.meta_include_prototype.is_none() {
981                                    child_ctx.meta_include_prototype =
982                                        parent_ctx.meta_include_prototype;
983                                }
984                            }
985                        }
986                    } else {
987                        // Parent context not in child: inherit it
988                        child.contexts.insert(ctx_name, parent_ctx);
989                    }
990                }
991
992                // Regenerate __start/__main for the child's own scope.
993                // The parent's __start carries the parent's meta_scope (e.g.
994                // text.html.plain), which is wrong for the child (e.g.
995                // text.html.rails). Always regenerate so the child's scope
996                // is used.
997                if child.contexts.contains_key("main") {
998                    let mut scope_repo = crate::parsing::scope::lock_global_scope_repo();
999                    let top_level_scope = child.scope;
1000                    SyntaxDefinition::add_initial_contexts(
1001                        &mut child.contexts,
1002                        &mut crate::parsing::yaml_load::ParserState {
1003                            scope_repo: scope_repo.deref_mut(),
1004                            variables: child.variables.clone(),
1005                            variable_regex: Regex::new(r"\{\{([A-Za-z0-9_]+)\}\}".into()),
1006                            backref_regex: Regex::new(r"\\\d".into()),
1007                            lines_include_newline,
1008                            version: child.version,
1009                            defer_regex_validation: false,
1010                        },
1011                        top_level_scope,
1012                    );
1013                }
1014
1015                if let Err(e) =
1016                    crate::parsing::yaml_load::re_resolve_all_regexes(child, lines_include_newline)
1017                {
1018                    warnings.push(format!(
1019                        "failed to re-resolve regexes for '{}' after extends: {}",
1020                        child.name, e
1021                    ));
1022                }
1023
1024                syntax_roots.insert(child_idx, common_root);
1025                unresolved.remove(&child_idx);
1026                made_progress = true;
1027            }
1028        }
1029
1030        if !unresolved.is_empty() {
1031            for idx in &unresolved {
1032                let syntax = &mut syntax_definitions[*idx];
1033                let e = &syntax.extends;
1034                let extends_str = if e.is_empty() {
1035                    "?".to_string()
1036                } else {
1037                    e.join(", ")
1038                };
1039                warnings.push(format!(
1040                    "syntax '{}' extends '{}' but parent was not found or has circular dependency",
1041                    syntax.name, extends_str,
1042                ));
1043                // Mark broken syntaxes as hidden so they won't be found by
1044                // name/extension lookups and won't cause panics when used.
1045                syntax.hidden = true;
1046
1047                // Ensure the syntax has __start and __main contexts so that
1048                // ParseState::new won't panic if this syntax is still accessed.
1049                if !syntax.contexts.contains_key("main") {
1050                    syntax
1051                        .contexts
1052                        .insert("main".to_string(), Context::new(None));
1053                }
1054                if !syntax.contexts.contains_key("__start") {
1055                    let mut scope_repo = crate::parsing::scope::lock_global_scope_repo();
1056                    let top_level_scope = syntax.scope;
1057                    SyntaxDefinition::add_initial_contexts(
1058                        &mut syntax.contexts,
1059                        &mut crate::parsing::yaml_load::ParserState {
1060                            scope_repo: scope_repo.deref_mut(),
1061                            variables: syntax.variables.clone(),
1062                            variable_regex: Regex::new(r"\{\{([A-Za-z0-9_]+)\}\}".into()),
1063                            backref_regex: Regex::new(r"\\\d".into()),
1064                            lines_include_newline,
1065                            version: syntax.version,
1066                            defer_regex_validation: false,
1067                        },
1068                        top_level_scope,
1069                    );
1070                }
1071            }
1072        }
1073
1074        (syntax_definitions, warnings)
1075    }
1076
1077    /// Find the index of a parent syntax by matching the extends path.
1078    #[cfg(feature = "yaml-load")]
1079    fn find_parent_index(
1080        extends_path: &str,
1081        path_syntaxes: &[(String, usize)],
1082        syntax_definitions: &[SyntaxDefinition],
1083        name_to_index: &HashMap<String, usize>,
1084    ) -> Option<usize> {
1085        // Normalize separators for matching
1086        let normalized = extends_path.replace('\\', "/");
1087
1088        // First try matching against path_syntaxes (path ends with extends value)
1089        let slash_normalized = format!("/{}", normalized);
1090        for (path, idx) in path_syntaxes {
1091            let path_normalized = path.replace('\\', "/");
1092            if path_normalized.ends_with(&slash_normalized) || path_normalized == normalized {
1093                return Some(*idx);
1094            }
1095        }
1096
1097        // Try matching by file stem (e.g., "Packages/C/C.sublime-syntax" -> look for syntax named "C")
1098        if let Some(file_name) = std::path::Path::new(&normalized).file_stem() {
1099            if let Some(name_str) = file_name.to_str() {
1100                if let Some(&idx) = name_to_index.get(name_str) {
1101                    return Some(idx);
1102                }
1103                // Try case-insensitive match
1104                for (i, sd) in syntax_definitions.iter().enumerate() {
1105                    if sd.name.eq_ignore_ascii_case(name_str) {
1106                        return Some(i);
1107                    }
1108                }
1109            }
1110        }
1111
1112        None
1113    }
1114
1115    /// Anything recursively included by the prototype shouldn't include the prototype.
1116    /// This marks them as such.
1117    fn recursively_mark_no_prototype(
1118        context_id: &ContextId,
1119        syntax_context_ids: &HashMap<String, ContextId>,
1120        all_contexts: &[Vec<Context>],
1121        no_prototype: &mut HashSet<ContextId>,
1122    ) {
1123        let first_time = no_prototype.insert(*context_id);
1124        if !first_time {
1125            return;
1126        }
1127
1128        for pattern in &all_contexts[context_id.syntax_index][context_id.context_index].patterns {
1129            match *pattern {
1130                // Apparently inline blocks also don't include the prototype when within the prototype.
1131                // This is really weird, but necessary to run the YAML syntax.
1132                Pattern::Match(ref match_pat) => {
1133                    let maybe_context_refs = match match_pat.operation {
1134                        MatchOperation::Push(ref context_refs)
1135                        | MatchOperation::Set {
1136                            ctx_refs: ref context_refs,
1137                            ..
1138                        }
1139                        | MatchOperation::Branch {
1140                            alternatives: ref context_refs,
1141                            ..
1142                        }
1143                        | MatchOperation::Embed {
1144                            contexts: ref context_refs,
1145                            ..
1146                        } => Some(context_refs),
1147                        MatchOperation::Pop(_) | MatchOperation::None | MatchOperation::Fail(_) => {
1148                            None
1149                        }
1150                    };
1151                    if let Some(context_refs) = maybe_context_refs {
1152                        for context_ref in context_refs.iter() {
1153                            match context_ref {
1154                                ContextReference::Inline(ref s)
1155                                | ContextReference::Named(ref s) => {
1156                                    if let Some(i) = syntax_context_ids.get(s) {
1157                                        Self::recursively_mark_no_prototype(
1158                                            i,
1159                                            syntax_context_ids,
1160                                            all_contexts,
1161                                            no_prototype,
1162                                        );
1163                                    }
1164                                }
1165                                ContextReference::Direct(ref id) => {
1166                                    Self::recursively_mark_no_prototype(
1167                                        id,
1168                                        syntax_context_ids,
1169                                        all_contexts,
1170                                        no_prototype,
1171                                    );
1172                                }
1173                                _ => (),
1174                            }
1175                        }
1176                    }
1177                }
1178                Pattern::Include(ref reference) | Pattern::IncludeWithPrototype(ref reference) => {
1179                    match reference {
1180                        ContextReference::Named(ref s) => {
1181                            if let Some(id) = syntax_context_ids.get(s) {
1182                                Self::recursively_mark_no_prototype(
1183                                    id,
1184                                    syntax_context_ids,
1185                                    all_contexts,
1186                                    no_prototype,
1187                                );
1188                            }
1189                        }
1190                        ContextReference::Direct(ref id) => {
1191                            Self::recursively_mark_no_prototype(
1192                                id,
1193                                syntax_context_ids,
1194                                all_contexts,
1195                                no_prototype,
1196                            );
1197                        }
1198                        _ => (),
1199                    }
1200                }
1201            }
1202        }
1203    }
1204
1205    fn link_context(
1206        context: &mut Context,
1207        syntax_index: usize,
1208        all_context_ids: &[HashMap<String, ContextId>],
1209        syntaxes: &[SyntaxReference],
1210    ) {
1211        for pattern in &mut context.patterns {
1212            match *pattern {
1213                Pattern::Match(ref mut match_pat) => {
1214                    Self::link_match_pat(match_pat, syntax_index, all_context_ids, syntaxes)
1215                }
1216                Pattern::Include(ref mut context_ref)
1217                | Pattern::IncludeWithPrototype(ref mut context_ref) => {
1218                    Self::link_ref(context_ref, syntax_index, all_context_ids, syntaxes)
1219                }
1220            }
1221        }
1222    }
1223
1224    fn link_ref(
1225        context_ref: &mut ContextReference,
1226        syntax_index: usize,
1227        all_context_ids: &[HashMap<String, ContextId>],
1228        syntaxes: &[SyntaxReference],
1229    ) {
1230        // println!("{:?}", context_ref);
1231        use super::syntax_definition::ContextReference::*;
1232        let linked_context_id = match *context_ref {
1233            Named(ref s) | Inline(ref s) => {
1234                // This isn't actually correct, but it is better than nothing/crashing.
1235                // This is being phased out anyhow, see https://github.com/sublimehq/Packages/issues/73
1236                // Fixes issue #30
1237                if s == "$top_level_main" {
1238                    all_context_ids[syntax_index].get("main")
1239                } else {
1240                    all_context_ids[syntax_index].get(s)
1241                }
1242            }
1243            ByScope {
1244                scope,
1245                ref sub_context,
1246                with_escape,
1247            } => Self::with_plain_text_fallback(
1248                all_context_ids,
1249                syntaxes,
1250                with_escape,
1251                Self::find_id(sub_context, all_context_ids, syntaxes, |index_and_syntax| {
1252                    index_and_syntax.1.scope == scope
1253                }),
1254            ),
1255            File {
1256                ref name,
1257                ref sub_context,
1258                with_escape,
1259            } => Self::with_plain_text_fallback(
1260                all_context_ids,
1261                syntaxes,
1262                with_escape,
1263                Self::find_id(sub_context, all_context_ids, syntaxes, |index_and_syntax| {
1264                    &index_and_syntax.1.name == name
1265                }),
1266            ),
1267            Direct(_) => None,
1268        };
1269        if let Some(context_id) = linked_context_id {
1270            let mut new_ref = Direct(*context_id);
1271            mem::swap(context_ref, &mut new_ref);
1272        }
1273    }
1274
1275    fn with_plain_text_fallback<'a>(
1276        all_context_ids: &'a [HashMap<String, ContextId>],
1277        syntaxes: &'a [SyntaxReference],
1278        with_escape: bool,
1279        context_id: Option<&'a ContextId>,
1280    ) -> Option<&'a ContextId> {
1281        context_id.or_else(|| {
1282            if with_escape {
1283                // If we keep this reference unresolved, syntect will crash
1284                // when it encounters the reference. Rather than crashing,
1285                // we instead fall back to "Plain Text". This seems to be
1286                // how Sublime Text behaves. It should be a safe thing to do
1287                // since `embed`s always includes an `escape` to get out of
1288                // the `embed`.
1289                Self::find_id(&None, all_context_ids, syntaxes, |index_and_syntax| {
1290                    index_and_syntax.1.name == "Plain Text"
1291                })
1292            } else {
1293                None
1294            }
1295        })
1296    }
1297
1298    fn find_id<'a>(
1299        sub_context: &Option<String>,
1300        all_context_ids: &'a [HashMap<String, ContextId>],
1301        syntaxes: &'a [SyntaxReference],
1302        predicate: impl FnMut(&(usize, &SyntaxReference)) -> bool,
1303    ) -> Option<&'a ContextId> {
1304        let context_name = sub_context.as_ref().map_or("main", |x| &**x);
1305        syntaxes
1306            .iter()
1307            .enumerate()
1308            .rev()
1309            .find(predicate)
1310            .and_then(|index_and_syntax| all_context_ids[index_and_syntax.0].get(context_name))
1311    }
1312
1313    fn link_match_pat(
1314        match_pat: &mut MatchPattern,
1315        syntax_index: usize,
1316        all_context_ids: &[HashMap<String, ContextId>],
1317        syntaxes: &[SyntaxReference],
1318    ) {
1319        let maybe_context_refs = match match_pat.operation {
1320            MatchOperation::Push(ref mut context_refs)
1321            | MatchOperation::Set {
1322                ctx_refs: ref mut context_refs,
1323                ..
1324            }
1325            | MatchOperation::Branch {
1326                alternatives: ref mut context_refs,
1327                ..
1328            }
1329            | MatchOperation::Embed {
1330                contexts: ref mut context_refs,
1331                ..
1332            } => Some(context_refs),
1333            MatchOperation::Pop(_) | MatchOperation::None | MatchOperation::Fail(_) => None,
1334        };
1335        if let Some(context_refs) = maybe_context_refs {
1336            for context_ref in context_refs.iter_mut() {
1337                Self::link_ref(context_ref, syntax_index, all_context_ids, syntaxes);
1338            }
1339        }
1340        if let Some(ref mut context_ref) = match_pat.with_prototype {
1341            Self::link_ref(context_ref, syntax_index, all_context_ids, syntaxes);
1342        }
1343    }
1344}
1345
1346#[derive(Debug)]
1347struct FirstLineCache {
1348    /// (first line regex, syntax index) pairs for all syntaxes with a first line regex
1349    regexes: Vec<(Regex, usize)>,
1350}
1351
1352impl FirstLineCache {
1353    fn new(syntaxes: &[SyntaxReference]) -> FirstLineCache {
1354        let mut regexes = Vec::new();
1355        for (i, syntax) in syntaxes.iter().enumerate() {
1356            if let Some(ref reg_str) = syntax.first_line_match {
1357                let reg = Regex::new(reg_str.into());
1358                regexes.push((reg, i));
1359            }
1360        }
1361        FirstLineCache { regexes }
1362    }
1363}
1364
1365#[cfg(feature = "yaml-load")]
1366#[cfg(test)]
1367mod tests {
1368    use super::*;
1369    use crate::{
1370        parsing::{syntax_definition, ParseState, Scope},
1371        utils::testdata,
1372    };
1373    use std::collections::HashMap;
1374
1375    #[test]
1376    fn can_load() {
1377        let mut builder = testdata::PACKAGES_SYN_SET.to_owned().into_builder();
1378
1379        let cmake_dummy_syntax = SyntaxDefinition {
1380            name: "CMake".to_string(),
1381            file_extensions: vec!["CMakeLists.txt".to_string(), "cmake".to_string()],
1382            scope: Scope::new("source.cmake").unwrap(),
1383            first_line_match: None,
1384            hidden: false,
1385            variables: HashMap::new(),
1386            contexts: HashMap::new(),
1387            extends: vec![],
1388            version: 1,
1389        };
1390
1391        builder.add(cmake_dummy_syntax);
1392        builder.add_plain_text_syntax();
1393
1394        let ps = builder.build();
1395
1396        assert_eq!(
1397            &ps.find_syntax_by_first_line("#!/usr/bin/env node")
1398                .unwrap()
1399                .name,
1400            "JavaScript"
1401        );
1402        let rails_scope = Scope::new("source.ruby.rails").unwrap();
1403        let syntax = ps.find_syntax_by_name("Ruby (Rails)").unwrap();
1404        ps.find_syntax_plain_text();
1405        assert_eq!(&ps.find_syntax_by_extension("rake").unwrap().name, "Ruby");
1406        assert_eq!(&ps.find_syntax_by_extension("RAKE").unwrap().name, "Ruby");
1407        assert_eq!(&ps.find_syntax_by_token("ruby").unwrap().name, "Ruby");
1408        assert_eq!(
1409            &ps.find_syntax_by_first_line("// -*- Mode: C -*- such line")
1410                .unwrap()
1411                .name,
1412            "C"
1413        );
1414        assert_eq!(
1415            &ps.find_syntax_for_file("testdata/parser.rs")
1416                .unwrap()
1417                .unwrap()
1418                .name,
1419            "Rust"
1420        );
1421        assert_eq!(
1422            &ps.find_syntax_for_file("testdata/test_first_line.test")
1423                .expect("Error finding syntax for file")
1424                .expect("No syntax found for file")
1425                .name,
1426            "Ruby"
1427        );
1428        assert_eq!(
1429            &ps.find_syntax_for_file(".bashrc").unwrap().unwrap().name,
1430            "Bash"
1431        );
1432        assert_eq!(
1433            &ps.find_syntax_for_file("CMakeLists.txt")
1434                .unwrap()
1435                .unwrap()
1436                .name,
1437            "CMake"
1438        );
1439        assert_eq!(
1440            &ps.find_syntax_for_file("test.cmake").unwrap().unwrap().name,
1441            "CMake"
1442        );
1443        assert_eq!(
1444            &ps.find_syntax_for_file("Rakefile").unwrap().unwrap().name,
1445            "Ruby"
1446        );
1447        assert!(&ps.find_syntax_by_first_line("derp derp hi lol").is_none());
1448        assert_eq!(
1449            &ps.find_syntax_by_path("Packages/Rust/Rust.sublime-syntax")
1450                .unwrap()
1451                .name,
1452            "Rust"
1453        );
1454        // println!("{:#?}", syntax);
1455        assert_eq!(syntax.scope, rails_scope);
1456        // unreachable!();
1457        let main_context = ps
1458            .get_context(&syntax.context_ids()["main"])
1459            .expect("#[cfg(test)]");
1460        let count = syntax_definition::context_iter(&ps, main_context).count();
1461        assert_eq!(count, 185);
1462    }
1463
1464    #[test]
1465    fn can_clone() {
1466        let cloned_syntax_set = {
1467            let mut builder = SyntaxSetBuilder::new();
1468            builder.add(syntax_a());
1469            builder.add(syntax_b());
1470
1471            let syntax_set_original = builder.build();
1472            #[allow(clippy::redundant_clone)] // We want to test .clone()
1473            syntax_set_original.clone()
1474            // Note: The original syntax set is dropped
1475        };
1476
1477        let syntax = cloned_syntax_set.find_syntax_by_extension("a").unwrap();
1478        let mut parse_state = ParseState::new(syntax);
1479        let ops = parse_state
1480            .parse_line("a go_b b", &cloned_syntax_set)
1481            .expect("#[cfg(test)]")
1482            .ops;
1483        let expected = (7, ScopeStackOp::Push(Scope::new("b").unwrap()));
1484        assert_ops_contain(&ops, &expected);
1485    }
1486
1487    #[test]
1488    fn can_list_added_syntaxes() {
1489        let mut builder = SyntaxSetBuilder::new();
1490        builder.add(syntax_a());
1491        builder.add(syntax_b());
1492        let syntaxes = builder.syntaxes();
1493
1494        assert_eq!(syntaxes.len(), 2);
1495        assert_eq!(syntaxes[0].name, "A");
1496        assert_eq!(syntaxes[1].name, "B");
1497    }
1498
1499    #[test]
1500    fn can_add_more_syntaxes_with_builder() {
1501        let syntax_set_original = {
1502            let mut builder = SyntaxSetBuilder::new();
1503            builder.add(syntax_a());
1504            builder.add(syntax_b());
1505            builder.build()
1506        };
1507
1508        let mut builder = syntax_set_original.into_builder();
1509
1510        let syntax_c = SyntaxDefinition::load_from_str(
1511            r#"
1512        name: C
1513        scope: source.c
1514        file_extensions: [c]
1515        contexts:
1516          main:
1517            - match: 'c'
1518              scope: c
1519            - match: 'go_a'
1520              push: scope:source.a#main
1521        "#,
1522            true,
1523            None,
1524        )
1525        .unwrap();
1526
1527        builder.add(syntax_c);
1528
1529        let syntax_set = builder.build();
1530
1531        let syntax = syntax_set.find_syntax_by_extension("c").unwrap();
1532        let mut parse_state = ParseState::new(syntax);
1533        let ops = parse_state
1534            .parse_line("c go_a a go_b b", &syntax_set)
1535            .expect("#[cfg(test)]")
1536            .ops;
1537        let expected = (14, ScopeStackOp::Push(Scope::new("b").unwrap()));
1538        assert_ops_contain(&ops, &expected);
1539    }
1540
1541    #[test]
1542    fn falls_back_to_plain_text_when_embedded_scope_is_missing() {
1543        test_plain_text_fallback(
1544            r#"
1545        name: Z
1546        scope: source.z
1547        file_extensions: [z]
1548        contexts:
1549          main:
1550            - match: 'z'
1551              scope: z
1552            - match: 'go_x'
1553              embed: scope:does.not.exist
1554              escape: 'leave_x'
1555        "#,
1556        );
1557    }
1558
1559    #[test]
1560    fn falls_back_to_plain_text_when_embedded_file_is_missing() {
1561        test_plain_text_fallback(
1562            r#"
1563        name: Z
1564        scope: source.z
1565        file_extensions: [z]
1566        contexts:
1567          main:
1568            - match: 'z'
1569              scope: z
1570            - match: 'go_x'
1571              embed: DoesNotExist.sublime-syntax
1572              escape: 'leave_x'
1573        "#,
1574        );
1575    }
1576
1577    fn test_plain_text_fallback(syntax_definition: &str) {
1578        let syntax = SyntaxDefinition::load_from_str(syntax_definition, true, None).unwrap();
1579
1580        let mut builder = SyntaxSetBuilder::new();
1581        builder.add_plain_text_syntax();
1582        builder.add(syntax);
1583        let syntax_set = builder.build();
1584
1585        let syntax = syntax_set.find_syntax_by_extension("z").unwrap();
1586        let mut parse_state = ParseState::new(syntax);
1587        let ops = parse_state
1588            .parse_line("z go_x x leave_x z", &syntax_set)
1589            .unwrap()
1590            .ops;
1591        let expected_ops = vec![
1592            (0, ScopeStackOp::Push(Scope::new("source.z").unwrap())),
1593            (0, ScopeStackOp::Push(Scope::new("z").unwrap())),
1594            (1, ScopeStackOp::Pop(1)),
1595            (6, ScopeStackOp::Push(Scope::new("text.plain").unwrap())),
1596            (9, ScopeStackOp::Pop(1)),
1597            (17, ScopeStackOp::Push(Scope::new("z").unwrap())),
1598            (18, ScopeStackOp::Pop(1)),
1599        ];
1600        assert_eq!(ops, expected_ops);
1601    }
1602
1603    #[test]
1604    fn can_find_unlinked_contexts() {
1605        let syntax_set = {
1606            let mut builder = SyntaxSetBuilder::new();
1607            builder.add(syntax_a());
1608            builder.add(syntax_b());
1609            builder.build()
1610        };
1611
1612        let unlinked_contexts = syntax_set.find_unlinked_contexts();
1613        assert_eq!(unlinked_contexts.len(), 0);
1614
1615        let syntax_set = {
1616            let mut builder = SyntaxSetBuilder::new();
1617            builder.add(syntax_a());
1618            builder.build()
1619        };
1620
1621        let unlinked_contexts: Vec<String> =
1622            syntax_set.find_unlinked_contexts().into_iter().collect();
1623        assert_eq!(unlinked_contexts.len(), 1);
1624        assert_eq!(unlinked_contexts[0], "Syntax 'A' with scope 'source.a' has unresolved context reference ByScope { scope: <source.b>, sub_context: Some(\"main\"), with_escape: false }");
1625    }
1626
1627    #[test]
1628    fn can_use_in_multiple_threads() {
1629        use rayon::prelude::*;
1630
1631        let syntax_set = {
1632            let mut builder = SyntaxSetBuilder::new();
1633            builder.add(syntax_a());
1634            builder.add(syntax_b());
1635            builder.build()
1636        };
1637
1638        let lines = vec!["a a a", "a go_b b", "go_b b", "go_b b  b"];
1639
1640        let results: Vec<Vec<(usize, ScopeStackOp)>> = lines
1641            .par_iter()
1642            .map(|line| {
1643                let syntax = syntax_set.find_syntax_by_extension("a").unwrap();
1644                let mut parse_state = ParseState::new(syntax);
1645                parse_state
1646                    .parse_line(line, &syntax_set)
1647                    .expect("#[cfg(test)]")
1648                    .ops
1649            })
1650            .collect();
1651
1652        assert_ops_contain(
1653            &results[0],
1654            &(4, ScopeStackOp::Push(Scope::new("a").unwrap())),
1655        );
1656        assert_ops_contain(
1657            &results[1],
1658            &(7, ScopeStackOp::Push(Scope::new("b").unwrap())),
1659        );
1660        assert_ops_contain(
1661            &results[2],
1662            &(5, ScopeStackOp::Push(Scope::new("b").unwrap())),
1663        );
1664        assert_ops_contain(
1665            &results[3],
1666            &(8, ScopeStackOp::Push(Scope::new("b").unwrap())),
1667        );
1668    }
1669
1670    #[test]
1671    fn is_sync() {
1672        check_sync::<SyntaxSet>();
1673    }
1674
1675    #[test]
1676    fn is_send() {
1677        check_send::<SyntaxSet>();
1678    }
1679
1680    #[test]
1681    fn can_override_syntaxes() {
1682        let syntax_set = {
1683            let mut builder = SyntaxSetBuilder::new();
1684            builder.add(syntax_a());
1685            builder.add(syntax_b());
1686
1687            let syntax_a2 = SyntaxDefinition::load_from_str(
1688                r#"
1689                name: A improved
1690                scope: source.a
1691                file_extensions: [a]
1692                first_line_match: syntax\s+a
1693                contexts:
1694                  main:
1695                    - match: a
1696                      scope: a2
1697                    - match: go_b
1698                      push: scope:source.b#main
1699                "#,
1700                true,
1701                None,
1702            )
1703            .unwrap();
1704
1705            builder.add(syntax_a2);
1706
1707            let syntax_c = SyntaxDefinition::load_from_str(
1708                r#"
1709                name: C
1710                scope: source.c
1711                file_extensions: [c]
1712                first_line_match: syntax\s+.*
1713                contexts:
1714                  main:
1715                    - match: c
1716                      scope: c
1717                    - match: go_a
1718                      push: scope:source.a#main
1719                "#,
1720                true,
1721                None,
1722            )
1723            .unwrap();
1724
1725            builder.add(syntax_c);
1726
1727            builder.build()
1728        };
1729
1730        let mut syntax = syntax_set.find_syntax_by_extension("a").unwrap();
1731        assert_eq!(syntax.name, "A improved");
1732        syntax = syntax_set
1733            .find_syntax_by_scope(Scope::new("source.a").unwrap())
1734            .unwrap();
1735        assert_eq!(syntax.name, "A improved");
1736        syntax = syntax_set.find_syntax_by_first_line("syntax a").unwrap();
1737        assert_eq!(syntax.name, "C");
1738
1739        let mut parse_state = ParseState::new(syntax);
1740        let ops = parse_state
1741            .parse_line("c go_a a", &syntax_set)
1742            .expect("msg")
1743            .ops;
1744        let expected = (7, ScopeStackOp::Push(Scope::new("a2").unwrap()));
1745        assert_ops_contain(&ops, &expected);
1746    }
1747
1748    #[test]
1749    fn can_parse_issue219() {
1750        // Go to builder and back after loading so that build() gets Direct references instead of
1751        // Named ones. The bug was that Direct references were not handled when marking as
1752        // "no prototype", so prototype contexts accidentally had the prototype set, which made
1753        // the parser loop forever.
1754        let syntax_set = SyntaxSet::load_defaults_newlines().into_builder().build();
1755        let syntax = syntax_set.find_syntax_by_extension("yaml").unwrap();
1756
1757        let mut parse_state = ParseState::new(syntax);
1758        let ops = parse_state
1759            .parse_line("# test\n", &syntax_set)
1760            .expect("#[cfg(test)]")
1761            .ops;
1762        let expected = (
1763            0,
1764            ScopeStackOp::Push(Scope::new("comment.line.number-sign.yaml").unwrap()),
1765        );
1766        assert_ops_contain(&ops, &expected);
1767    }
1768
1769    #[test]
1770    fn no_prototype_for_contexts_included_from_prototype() {
1771        let mut builder = SyntaxSetBuilder::new();
1772        let syntax = SyntaxDefinition::load_from_str(
1773            r#"
1774                name: Test Prototype
1775                scope: source.test
1776                file_extensions: [test]
1777                contexts:
1778                  prototype:
1779                    - include: included_from_prototype
1780                  main:
1781                    - match: main
1782                    - match: other
1783                      push: other
1784                  other:
1785                    - match: o
1786                  included_from_prototype:
1787                    - match: p
1788                      scope: p
1789                "#,
1790            true,
1791            None,
1792        )
1793        .unwrap();
1794        builder.add(syntax);
1795        let ss = builder.build();
1796
1797        // "main" and "other" should have context set, "prototype" and "included_from_prototype"
1798        // must not have a prototype set.
1799        assert_prototype_only_on(&["main", "other"], &ss, &ss.syntaxes()[0]);
1800
1801        // Building again should have the same result. The difference is that after the first
1802        // build(), the references have been replaced with Direct references, so the code needs to
1803        // handle that correctly.
1804        let rebuilt = ss.into_builder().build();
1805        assert_prototype_only_on(&["main", "other"], &rebuilt, &rebuilt.syntaxes()[0]);
1806    }
1807
1808    #[test]
1809    fn no_prototype_for_contexts_inline_in_prototype() {
1810        let mut builder = SyntaxSetBuilder::new();
1811        let syntax = SyntaxDefinition::load_from_str(
1812            r#"
1813                name: Test Prototype
1814                scope: source.test
1815                file_extensions: [test]
1816                contexts:
1817                  prototype:
1818                    - match: p
1819                      push:
1820                        - match: p2
1821                  main:
1822                    - match: main
1823                "#,
1824            true,
1825            None,
1826        )
1827        .unwrap();
1828        builder.add(syntax);
1829        let ss = builder.build();
1830
1831        assert_prototype_only_on(&["main"], &ss, &ss.syntaxes()[0]);
1832
1833        let rebuilt = ss.into_builder().build();
1834        assert_prototype_only_on(&["main"], &rebuilt, &rebuilt.syntaxes()[0]);
1835    }
1836
1837    #[test]
1838    fn find_syntax_set_from_line_with_bom() {
1839        // Regression test for #529
1840        let syntax_set = SyntaxSet::load_defaults_newlines();
1841        let syntax_ref = syntax_set
1842            .find_syntax_by_first_line("\u{feff}<?xml version=\"1.0\"?>")
1843            .unwrap();
1844        assert_eq!(syntax_ref.name, "XML");
1845    }
1846
1847    fn assert_ops_contain(ops: &[(usize, ScopeStackOp)], expected: &(usize, ScopeStackOp)) {
1848        assert!(
1849            ops.contains(expected),
1850            "expected operations to contain {:?}: {:?}",
1851            expected,
1852            ops
1853        );
1854    }
1855
1856    fn assert_prototype_only_on(
1857        expected: &[&str],
1858        syntax_set: &SyntaxSet,
1859        syntax: &SyntaxReference,
1860    ) {
1861        for (name, id) in syntax.context_ids() {
1862            if name == "__main" || name == "__start" {
1863                // Skip special contexts
1864                continue;
1865            }
1866            let context = syntax_set.get_context(id).expect("#[cfg(test)]");
1867            if expected.contains(&name.as_str()) {
1868                assert!(
1869                    context.prototype.is_some(),
1870                    "Expected context {} to have prototype",
1871                    name
1872                );
1873            } else {
1874                assert!(
1875                    context.prototype.is_none(),
1876                    "Expected context {} to not have prototype",
1877                    name
1878                );
1879            }
1880        }
1881    }
1882
1883    fn check_send<T: Send>() {}
1884
1885    fn check_sync<T: Sync>() {}
1886
1887    fn syntax_a() -> SyntaxDefinition {
1888        SyntaxDefinition::load_from_str(
1889            r#"
1890            name: A
1891            scope: source.a
1892            file_extensions: [a]
1893            contexts:
1894              main:
1895                - match: 'a'
1896                  scope: a
1897                - match: 'go_b'
1898                  push: scope:source.b#main
1899            "#,
1900            true,
1901            None,
1902        )
1903        .unwrap()
1904    }
1905
1906    fn syntax_b() -> SyntaxDefinition {
1907        SyntaxDefinition::load_from_str(
1908            r#"
1909            name: B
1910            scope: source.b
1911            file_extensions: [b]
1912            contexts:
1913              main:
1914                - match: 'b'
1915                  scope: b
1916            "#,
1917            true,
1918            None,
1919        )
1920        .unwrap()
1921    }
1922
1923    // =====================================================
1924    // Tests for extends (syntax inheritance)
1925    // =====================================================
1926
1927    fn base_syntax() -> SyntaxDefinition {
1928        SyntaxDefinition::load_from_str(
1929            r#"
1930            name: Base
1931            scope: source.base
1932            file_extensions: [base]
1933            variables:
1934              ident: '[a-z]+'
1935            contexts:
1936              main:
1937                - match: '{{ident}}'
1938                  scope: variable.base
1939              string:
1940                - meta_scope: string.base
1941                - match: '"'
1942                  pop: true
1943            "#,
1944            true,
1945            None,
1946        )
1947        .unwrap()
1948    }
1949
1950    #[test]
1951    fn extends_inherits_contexts() {
1952        // Child extends base and inherits the 'string' context
1953        let base = base_syntax();
1954        let child = SyntaxDefinition::load_from_str(
1955            r#"
1956            name: Child
1957            scope: source.child
1958            file_extensions: [child]
1959            extends: Base.sublime-syntax
1960            contexts:
1961              main:
1962                - match: 'child'
1963                  scope: keyword.child
1964            "#,
1965            true,
1966            None,
1967        )
1968        .unwrap();
1969
1970        let mut builder = SyntaxSetBuilder::new();
1971        builder.add(base);
1972        builder.add(child);
1973        let ss = builder.build();
1974
1975        let syntax = ss.find_syntax_by_name("Child").unwrap();
1976        // Child should have the 'string' context inherited from Base
1977        assert!(syntax.context_ids().contains_key("string"));
1978    }
1979
1980    #[test]
1981    fn extends_overrides_context() {
1982        // Child overrides the 'main' context
1983        let base = base_syntax();
1984        let child = SyntaxDefinition::load_from_str(
1985            r#"
1986            name: Child
1987            scope: source.child
1988            file_extensions: [child]
1989            extends: Base.sublime-syntax
1990            contexts:
1991              main:
1992                - match: 'override'
1993                  scope: keyword.override
1994            "#,
1995            true,
1996            None,
1997        )
1998        .unwrap();
1999
2000        let mut builder = SyntaxSetBuilder::new();
2001        builder.add(base);
2002        builder.add(child);
2003        let ss = builder.build();
2004
2005        let syntax = ss.find_syntax_by_name("Child").unwrap();
2006        let mut parse_state = ParseState::new(syntax);
2007        let ops = parse_state
2008            .parse_line("override\n", &ss)
2009            .expect("parse failed")
2010            .ops;
2011        // Should match child's 'override' keyword, not base's ident
2012        let expected = (
2013            0,
2014            ScopeStackOp::Push(Scope::new("keyword.override").unwrap()),
2015        );
2016        assert_ops_contain(&ops, &expected);
2017    }
2018
2019    #[test]
2020    fn extends_meta_prepend() {
2021        // Child prepends patterns to main
2022        let base = base_syntax();
2023        let child = SyntaxDefinition::load_from_str(
2024            r#"
2025            name: Child
2026            scope: source.child
2027            file_extensions: [child]
2028            extends: Base.sublime-syntax
2029            contexts:
2030              main:
2031                - meta_prepend: true
2032                - match: 'keyword'
2033                  scope: keyword.child
2034            "#,
2035            true,
2036            None,
2037        )
2038        .unwrap();
2039
2040        let mut builder = SyntaxSetBuilder::new();
2041        builder.add(base);
2042        builder.add(child);
2043        let ss = builder.build();
2044
2045        let syntax = ss.find_syntax_by_name("Child").unwrap();
2046        let mut parse_state = ParseState::new(syntax);
2047        // 'keyword' should match the child's pattern (prepended, so first in list)
2048        let ops = parse_state
2049            .parse_line("keyword\n", &ss)
2050            .expect("parse failed")
2051            .ops;
2052        let expected = (0, ScopeStackOp::Push(Scope::new("keyword.child").unwrap()));
2053        assert_ops_contain(&ops, &expected);
2054    }
2055
2056    #[test]
2057    fn extends_meta_append() {
2058        // Child appends patterns to main
2059        let base = base_syntax();
2060        let child = SyntaxDefinition::load_from_str(
2061            r#"
2062            name: Child
2063            scope: source.child
2064            file_extensions: [child]
2065            extends: Base.sublime-syntax
2066            contexts:
2067              main:
2068                - meta_append: true
2069                - match: 'extra'
2070                  scope: keyword.extra
2071            "#,
2072            true,
2073            None,
2074        )
2075        .unwrap();
2076
2077        let mut builder = SyntaxSetBuilder::new();
2078        builder.add(base);
2079        builder.add(child);
2080        let ss = builder.build();
2081
2082        let syntax = ss.find_syntax_by_name("Child").unwrap();
2083        let mut parse_state = ParseState::new(syntax);
2084        // 'abc' should still match base's ident pattern (it comes first since child is appended)
2085        let ops = parse_state
2086            .parse_line("abc\n", &ss)
2087            .expect("parse failed")
2088            .ops;
2089        let expected = (0, ScopeStackOp::Push(Scope::new("variable.base").unwrap()));
2090        assert_ops_contain(&ops, &expected);
2091    }
2092
2093    #[test]
2094    fn extends_variable_override() {
2095        // Child overrides parent's 'ident' variable
2096        let base = SyntaxDefinition::load_from_str(
2097            r#"
2098            name: Base
2099            scope: source.base
2100            file_extensions: [base]
2101            variables:
2102              ident: '[a-z]+'
2103            contexts:
2104              main:
2105                - match: '{{ident}}'
2106                  scope: variable.base
2107            "#,
2108            true,
2109            None,
2110        )
2111        .unwrap();
2112        let child = SyntaxDefinition::load_from_str(
2113            r#"
2114            name: Child
2115            scope: source.child
2116            file_extensions: [child]
2117            extends: Base.sublime-syntax
2118            variables:
2119              ident: '[A-Z]+'
2120            contexts: {}
2121            "#,
2122            true,
2123            None,
2124        )
2125        .unwrap();
2126
2127        let mut builder = SyntaxSetBuilder::new();
2128        builder.add(base);
2129        builder.add(child);
2130        let ss = builder.build();
2131
2132        let syntax = ss.find_syntax_by_name("Child").unwrap();
2133        let mut parse_state = ParseState::new(syntax);
2134        // Lowercase should NOT match (child overrides ident to uppercase only)
2135        let ops = parse_state
2136            .parse_line("ABC\n", &ss)
2137            .expect("parse failed")
2138            .ops;
2139        let expected = (0, ScopeStackOp::Push(Scope::new("variable.base").unwrap()));
2140        assert_ops_contain(&ops, &expected);
2141    }
2142
2143    #[test]
2144    fn extends_variable_inherited_without_override() {
2145        // A child whose own regex references a variable defined only in
2146        // the parent must still compile that regex correctly. Without
2147        // deferred regex validation, the child's load-time pass would
2148        // resolve {{ident}} to "" (Base hasn't been merged yet) and
2149        // compile a broken pattern, causing the syntax to be skipped
2150        // entirely. `re_resolve_all_regexes` reruns compilation after
2151        // `resolve_extends` merges Base's variables into Child.
2152        let base = SyntaxDefinition::load_from_str(
2153            r#"
2154            name: Base
2155            scope: source.base
2156            file_extensions: [base]
2157            variables:
2158              ident: '[a-z]+'
2159            contexts:
2160              main:
2161                - match: '{{ident}}'
2162                  scope: variable.base
2163            "#,
2164            true,
2165            None,
2166        )
2167        .unwrap();
2168        // No `variables:` section — ident comes solely from Base.
2169        let child = SyntaxDefinition::load_from_str(
2170            r#"
2171            name: Child
2172            scope: source.child
2173            file_extensions: [child]
2174            extends: Base.sublime-syntax
2175            contexts:
2176              main:
2177                - match: '\${{ident}}'
2178                  scope: variable.child
2179            "#,
2180            true,
2181            None,
2182        )
2183        .unwrap();
2184
2185        let mut builder = SyntaxSetBuilder::new();
2186        builder.add(base);
2187        builder.add(child);
2188        let ss = builder.build();
2189
2190        let syntax = ss.find_syntax_by_name("Child").unwrap();
2191        let mut parse_state = ParseState::new(syntax);
2192        let ops = parse_state
2193            .parse_line("$abc\n", &ss)
2194            .expect("parse failed")
2195            .ops;
2196        let expected = (0, ScopeStackOp::Push(Scope::new("variable.child").unwrap()));
2197        assert_ops_contain(&ops, &expected);
2198    }
2199
2200    #[test]
2201    fn extends_only_syntax_with_no_contexts_key() {
2202        // Syntaxes that consist solely of `extends:` and have no
2203        // `contexts:` key at all (e.g. "Batch File (Compound)") must
2204        // load successfully — all contexts come from the parent.
2205        let base = SyntaxDefinition::load_from_str(
2206            r#"
2207            name: Base
2208            scope: source.base
2209            file_extensions: [base]
2210            variables:
2211              ident: '[a-z]+'
2212            contexts:
2213              main:
2214                - match: '{{ident}}'
2215                  scope: variable.base
2216            "#,
2217            true,
2218            None,
2219        )
2220        .unwrap();
2221        // No `contexts:` key at all — only extends + variables.
2222        let child = SyntaxDefinition::load_from_str(
2223            r#"
2224            name: ChildNoCtx
2225            scope: source.childnoctx
2226            file_extensions: [childnoctx]
2227            extends: Base.sublime-syntax
2228            variables:
2229              ident: '[A-Z]+'
2230            "#,
2231            true,
2232            None,
2233        )
2234        .unwrap();
2235
2236        let mut builder = SyntaxSetBuilder::new();
2237        builder.add(base);
2238        builder.add(child);
2239        let ss = builder.build();
2240
2241        let syntax = ss.find_syntax_by_name("ChildNoCtx").unwrap();
2242        let mut parse_state = ParseState::new(syntax);
2243        // Uppercase should match (child overrides ident to uppercase)
2244        let ops = parse_state
2245            .parse_line("ABC\n", &ss)
2246            .expect("parse failed")
2247            .ops;
2248        let expected = (0, ScopeStackOp::Push(Scope::new("variable.base").unwrap()));
2249        assert_ops_contain(&ops, &expected);
2250    }
2251
2252    #[test]
2253    fn extends_missing_parent_warns_but_no_panic() {
2254        // A syntax extends a non-existent parent. Should not panic.
2255        let child = SyntaxDefinition::load_from_str(
2256            r#"
2257            name: Orphan
2258            scope: source.orphan
2259            file_extensions: [orphan]
2260            extends: NonExistent.sublime-syntax
2261            contexts:
2262              main:
2263                - match: 'x'
2264                  scope: x
2265            "#,
2266            true,
2267            None,
2268        )
2269        .unwrap();
2270
2271        let mut builder = SyntaxSetBuilder::new();
2272        builder.add(child);
2273        // Should not panic
2274        let ss = builder.build();
2275        assert!(ss.find_syntax_by_name("Orphan").is_some());
2276    }
2277
2278    #[test]
2279    fn extends_multiple_parents_must_share_common_base() {
2280        // Per Sublime docs: all parents in `extends` list must derive from the same base syntax.
2281        // If they don't, the child should be rejected.
2282        let base1 = SyntaxDefinition::load_from_str(
2283            r#"
2284            name: Base1
2285            scope: source.base1
2286            file_extensions: [base1]
2287            contexts:
2288              main:
2289                - match: 'x'
2290                  scope: keyword.base1
2291              base1_only_ctx:
2292                - match: 'y'
2293                  scope: keyword.base1.y
2294            "#,
2295            true,
2296            None,
2297        )
2298        .unwrap();
2299
2300        let base2 = SyntaxDefinition::load_from_str(
2301            r#"
2302            name: Base2
2303            scope: source.base2
2304            file_extensions: [base2]
2305            contexts:
2306              main:
2307                - match: 'x'
2308                  scope: keyword.base2
2309              base2_only_ctx:
2310                - match: 'z'
2311                  scope: keyword.base2.z
2312            "#,
2313            true,
2314            None,
2315        )
2316        .unwrap();
2317
2318        let parent_a = SyntaxDefinition::load_from_str(
2319            r#"
2320            name: ParentA
2321            scope: source.parenta
2322            file_extensions: [parenta]
2323            extends: Base1.sublime-syntax
2324            contexts:
2325              parent_a_ctx:
2326                - match: 'a'
2327                  scope: keyword.a
2328            "#,
2329            true,
2330            None,
2331        )
2332        .unwrap();
2333
2334        let parent_b = SyntaxDefinition::load_from_str(
2335            r#"
2336            name: ParentB
2337            scope: source.parentb
2338            file_extensions: [parentb]
2339            extends: Base2.sublime-syntax
2340            contexts:
2341              parent_b_ctx:
2342                - match: 'b'
2343                  scope: keyword.b
2344            "#,
2345            true,
2346            None,
2347        )
2348        .unwrap();
2349
2350        let child = SyntaxDefinition::load_from_str(
2351            r#"
2352            name: Child
2353            scope: source.child_diffbase
2354            file_extensions: [child_diffbase]
2355            extends:
2356              - ParentA.sublime-syntax
2357              - ParentB.sublime-syntax
2358            contexts:
2359              child_ctx:
2360                - match: 'c'
2361                  scope: keyword.c
2362            "#,
2363            true,
2364            None,
2365        )
2366        .unwrap();
2367
2368        let mut builder = SyntaxSetBuilder::new();
2369        builder.add(base1);
2370        builder.add(base2);
2371        builder.add(parent_a);
2372        builder.add(parent_b);
2373        builder.add(child);
2374        let ss = builder.build();
2375
2376        let child_ref = ss.find_syntax_by_name("Child").unwrap();
2377        let context_ids = child_ref.context_ids();
2378
2379        // Per Sublime spec: a child whose parents derive from different bases should be rejected.
2380        // Syntect currently silently merges both, so it will have contexts from both unrelated bases.
2381        // This assertion reflects the CORRECT behavior and is EXPECTED TO FAIL until validation
2382        // is implemented.
2383        assert!(
2384            !(context_ids.contains_key("base1_only_ctx")
2385                && context_ids.contains_key("base2_only_ctx")),
2386            "Child with parents from different bases should be rejected; \
2387             found contexts from both unrelated bases: {:?}",
2388            context_ids.keys().collect::<Vec<_>>()
2389        );
2390    }
2391
2392    #[test]
2393    fn extends_parents_and_base_must_have_same_version() {
2394        // Per Sublime docs: all syntaxes in the inheritance chain must have the same version.
2395        // Here: Base is v1, Parent is v2 extending Base — version mismatch, should be invalid.
2396        let base = SyntaxDefinition::load_from_str(
2397            r#"
2398            name: BaseV1
2399            scope: source.basev1
2400            file_extensions: [basev1]
2401            contexts:
2402              main:
2403                - match: 'x'
2404                  scope: keyword.base
2405              base_only_ctx:
2406                - match: 'y'
2407                  scope: keyword.base.y
2408            "#,
2409            true,
2410            None,
2411        )
2412        .unwrap();
2413
2414        let parent = SyntaxDefinition::load_from_str(
2415            r#"
2416            name: ParentV2
2417            scope: source.parentv2
2418            file_extensions: [parentv2]
2419            version: 2
2420            extends: BaseV1.sublime-syntax
2421            contexts:
2422              parent_ctx:
2423                - match: 'p'
2424                  scope: keyword.parent
2425            "#,
2426            true,
2427            None,
2428        )
2429        .unwrap();
2430
2431        let mut builder = SyntaxSetBuilder::new();
2432        builder.add(base);
2433        builder.add(parent);
2434        let ss = builder.build();
2435
2436        let parent_ref = ss.find_syntax_by_name("ParentV2").unwrap();
2437        let context_ids = parent_ref.context_ids();
2438
2439        // Per Sublime spec: a v2 syntax extending a v1 base is invalid (version mismatch).
2440        // The extends should not be applied. Syntect currently silently merges regardless,
2441        // so it will contain base_only_ctx from the v1 base.
2442        // This assertion reflects the CORRECT behavior and is EXPECTED TO FAIL until validation
2443        // is implemented.
2444        assert!(
2445            !context_ids.contains_key("base_only_ctx"),
2446            "ParentV2 (v2) should not inherit from BaseV1 (v1) due to version mismatch; \
2447             found base_only_ctx in parent's contexts: {:?}",
2448            context_ids.keys().collect::<Vec<_>>()
2449        );
2450    }
2451
2452    #[test]
2453    fn extends_source_and_parent_must_have_same_version() {
2454        // Per Sublime docs: the source syntax must share the same version as its parents.
2455        // Here: Base (v1), Parent (v1 extends Base), Child (v2 extends Parent) — mismatch.
2456        let base = SyntaxDefinition::load_from_str(
2457            r#"
2458            name: SharedBase
2459            scope: source.sharedbase
2460            file_extensions: [sharedbase]
2461            contexts:
2462              main:
2463                - match: 'x'
2464                  scope: keyword.base
2465              shared_ctx:
2466                - match: 'y'
2467                  scope: keyword.base.shared
2468            "#,
2469            true,
2470            None,
2471        )
2472        .unwrap();
2473
2474        let parent = SyntaxDefinition::load_from_str(
2475            r#"
2476            name: ParentV1
2477            scope: source.parentv1
2478            file_extensions: [parentv1]
2479            extends: SharedBase.sublime-syntax
2480            contexts:
2481              parent_ctx:
2482                - match: 'p'
2483                  scope: keyword.parent
2484            "#,
2485            true,
2486            None,
2487        )
2488        .unwrap();
2489
2490        let child = SyntaxDefinition::load_from_str(
2491            r#"
2492            name: ChildV2
2493            scope: source.childv2
2494            file_extensions: [childv2]
2495            version: 2
2496            extends: ParentV1.sublime-syntax
2497            contexts:
2498              child_ctx:
2499                - match: 'c'
2500                  scope: keyword.child
2501            "#,
2502            true,
2503            None,
2504        )
2505        .unwrap();
2506
2507        let mut builder = SyntaxSetBuilder::new();
2508        builder.add(base);
2509        builder.add(parent);
2510        builder.add(child);
2511        let ss = builder.build();
2512
2513        let child_ref = ss.find_syntax_by_name("ChildV2").unwrap();
2514        let context_ids = child_ref.context_ids();
2515
2516        // Per Sublime spec: a v2 syntax extending a v1 parent is invalid (version mismatch).
2517        // The extends should not be applied. Syntect currently silently merges regardless,
2518        // so it will contain parent_ctx and shared_ctx from the v1 hierarchy.
2519        // This assertion reflects the CORRECT behavior and is EXPECTED TO FAIL until validation
2520        // is implemented.
2521        assert!(
2522            !context_ids.contains_key("parent_ctx"),
2523            "ChildV2 (v2) should not inherit from ParentV1 (v1) due to version mismatch; \
2524             found parent_ctx in child's contexts: {:?}",
2525            context_ids.keys().collect::<Vec<_>>()
2526        );
2527    }
2528
2529    // =====================================================
2530    // Tests for apply_prototype
2531    // =====================================================
2532
2533    #[test]
2534    fn apply_prototype_includes_external_prototype() {
2535        let syntax_with_proto = SyntaxDefinition::load_from_str(
2536            r#"
2537            name: WithProto
2538            scope: source.withproto
2539            file_extensions: [wp]
2540            contexts:
2541              prototype:
2542                - match: '#'
2543                  scope: comment.proto
2544                  push:
2545                    - meta_scope: comment.line
2546                    - match: '$'
2547                      pop: true
2548              main:
2549                - match: 'x'
2550                  scope: x
2551            "#,
2552            true,
2553            None,
2554        )
2555        .unwrap();
2556
2557        let syntax_using_proto = SyntaxDefinition::load_from_str(
2558            r#"
2559            name: UsingProto
2560            scope: source.usingproto
2561            file_extensions: [up]
2562            contexts:
2563              main:
2564                - match: 'y'
2565                  scope: y
2566                - include: scope:source.withproto
2567                  apply_prototype: true
2568            "#,
2569            true,
2570            None,
2571        )
2572        .unwrap();
2573
2574        let mut builder = SyntaxSetBuilder::new();
2575        builder.add(syntax_with_proto);
2576        builder.add(syntax_using_proto);
2577        let ss = builder.build();
2578
2579        // Just verify it builds without errors and the syntax exists
2580        assert!(ss.find_syntax_by_name("UsingProto").is_some());
2581    }
2582
2583    // =====================================================
2584    // Tests for version 2 behavioral fixes
2585    // =====================================================
2586
2587    #[test]
2588    fn v2_set_excludes_parent_meta_content_scope() {
2589        let syntax = SyntaxDefinition::load_from_str(
2590            r#"
2591            name: V2Test
2592            scope: source.v2test
2593            file_extensions: [v2]
2594            version: 2
2595            contexts:
2596              main:
2597                - meta_content_scope: meta.content.main
2598                - match: 'go'
2599                  set: other
2600              other:
2601                - match: 'x'
2602                  scope: x
2603                  pop: true
2604            "#,
2605            true,
2606            None,
2607        )
2608        .unwrap();
2609
2610        let mut builder = SyntaxSetBuilder::new();
2611        builder.add(syntax);
2612        let ss = builder.build();
2613
2614        let syntax = ss.find_syntax_by_name("V2Test").unwrap();
2615        assert_eq!(syntax.version, 2);
2616    }
2617
2618    #[test]
2619    fn version_preserved_in_syntax_reference() {
2620        let syntax = SyntaxDefinition::load_from_str(
2621            r#"
2622            name: V2
2623            scope: source.v2
2624            version: 2
2625            contexts:
2626              main:
2627                - match: 'x'
2628                  scope: x
2629            "#,
2630            true,
2631            None,
2632        )
2633        .unwrap();
2634
2635        let mut builder = SyntaxSetBuilder::new();
2636        builder.add(syntax);
2637        let ss = builder.build();
2638
2639        let syntax_ref = ss.find_syntax_by_name("V2").unwrap();
2640        assert_eq!(syntax_ref.version, 2);
2641    }
2642
2643    #[test]
2644    fn v2_set_applies_clear_scopes() {
2645        use crate::parsing::ParseState;
2646
2647        let syntax = SyntaxDefinition::load_from_str(
2648            r#"
2649            name: V2ClearScopes
2650            scope: source.v2clearscopes
2651            file_extensions: [v2cs]
2652            version: 2
2653            contexts:
2654              main:
2655                - match: 'go'
2656                  set: cleared
2657              cleared:
2658                - clear_scopes: true
2659                - match: 'x'
2660                  scope: x
2661                  pop: true
2662            "#,
2663            true,
2664            None,
2665        )
2666        .unwrap();
2667
2668        let mut builder = SyntaxSetBuilder::new();
2669        builder.add(syntax);
2670        let ss = builder.build();
2671
2672        let syntax = ss.find_syntax_by_name("V2ClearScopes").unwrap();
2673        let mut state = ParseState::new(syntax);
2674        let ops = state.parse_line("gox\n", &ss).unwrap().ops;
2675
2676        // After "go" sets to "cleared" which has clear_scopes: true,
2677        // the source.v2clearscopes scope should be cleared before "x" is matched
2678        let has_clear = ops
2679            .iter()
2680            .any(|(_, op)| matches!(op, ScopeStackOp::Clear(_)));
2681        assert!(
2682            has_clear,
2683            "v2 set should apply clear_scopes; ops: {:?}",
2684            ops
2685        );
2686    }
2687
2688    #[test]
2689    fn pop_n_set_pops_n_then_pushes_target() {
2690        use crate::parsing::{ParseState, ScopeStack};
2691
2692        // `pop: 2 + set: target` on a 3-deep stack should leave a 2-deep stack
2693        // where the top frame is `target`, not silently discard the set.
2694        let syntax = SyntaxDefinition::load_from_str(
2695            r#"
2696            name: PopSet
2697            scope: source.popset
2698            file_extensions: [pset]
2699            version: 2
2700            contexts:
2701              main:
2702                - match: 'a'
2703                  scope: a
2704                  push: level1
2705              level1:
2706                - meta_scope: meta.level1
2707                - match: 'b'
2708                  scope: b
2709                  push: level2
2710              level2:
2711                - meta_scope: meta.level2
2712                - match: 'c'
2713                  scope: c
2714                  pop: 2
2715                  set: target
2716              target:
2717                - meta_scope: meta.target
2718                - match: 'd'
2719                  scope: d
2720                  pop: true
2721            "#,
2722            true,
2723            None,
2724        )
2725        .unwrap();
2726
2727        let mut builder = SyntaxSetBuilder::new();
2728        builder.add(syntax);
2729        let ss = builder.build();
2730
2731        let syntax = ss.find_syntax_by_name("PopSet").unwrap();
2732        let mut state = ParseState::new(syntax);
2733        let mut stack = ScopeStack::new();
2734        for (_, op) in state.parse_line("abc", &ss).unwrap().ops {
2735            stack.apply(&op).unwrap();
2736        }
2737
2738        // After 'a' (push level1), 'b' (push level2), 'c' (pop 2 + set target):
2739        // the stack should hold [source.popset, meta.target]. If pop: 2 + set:
2740        // were silently dropped (old behavior), it'd be [source.popset] with no
2741        // meta.target.
2742        let scopes: Vec<String> = stack.as_slice().iter().map(|s| format!("{}", s)).collect();
2743        let has_target = scopes.iter().any(|s| s == "meta.target");
2744        let no_level1 = !scopes.iter().any(|s| s == "meta.level1");
2745        let no_level2 = !scopes.iter().any(|s| s == "meta.level2");
2746        assert!(
2747            has_target && no_level1 && no_level2,
2748            "expected meta.target on stack with no intermediate meta scopes, got: {:?}",
2749            scopes
2750        );
2751
2752        // `d` should pop 'target' and emit its scope.
2753        let ops2 = state.parse_line("d", &ss).unwrap().ops;
2754        let d_pushed = ops2.iter().any(|(_, op)| match op {
2755            ScopeStackOp::Push(s) => format!("{}", s) == "d",
2756            _ => false,
2757        });
2758        assert!(
2759            d_pushed,
2760            "'d' should fire 'target's d rule, ops: {:?}",
2761            ops2
2762        );
2763    }
2764
2765    #[test]
2766    fn v2_embed_scope_replaces_embedded_scope() {
2767        use crate::parsing::ParseState;
2768        use crate::parsing::ScopeStack;
2769
2770        let host = SyntaxDefinition::load_from_str(
2771            r#"
2772            name: V2Host
2773            scope: source.v2host
2774            file_extensions: [v2host]
2775            version: 2
2776            contexts:
2777              main:
2778                - match: '<<'
2779                  embed: scope:source.v2embedded
2780                  embed_scope: meta.embedded.custom
2781                  escape: '>>'
2782            "#,
2783            true,
2784            None,
2785        )
2786        .unwrap();
2787
2788        let embedded = SyntaxDefinition::load_from_str(
2789            r#"
2790            name: V2Embedded
2791            scope: source.v2embedded
2792            file_extensions: [v2emb]
2793            version: 2
2794            contexts:
2795              main:
2796                - match: 'x'
2797                  scope: keyword.x
2798            "#,
2799            true,
2800            None,
2801        )
2802        .unwrap();
2803
2804        let mut builder = SyntaxSetBuilder::new();
2805        builder.add(host);
2806        builder.add(embedded);
2807        let ss = builder.build();
2808
2809        let syntax = ss.find_syntax_by_name("V2Host").unwrap();
2810        let mut state = ParseState::new(syntax);
2811        let ops = state.parse_line("<<x>>\n", &ss).unwrap().ops;
2812
2813        // Build scope stack to check what scopes are active when "x" is matched
2814        let mut scope_stack = ScopeStack::new();
2815        let mut x_scopes = None;
2816        for (idx, op) in &ops {
2817            if *idx <= 2 {
2818                scope_stack.apply(op).unwrap();
2819            }
2820            // After applying ops at index 2 (the "x"), capture scopes
2821            if *idx > 2 && x_scopes.is_none() {
2822                x_scopes = Some(scope_stack.clone());
2823            }
2824        }
2825        let x_scopes = x_scopes.unwrap_or(scope_stack);
2826        let scopes: Vec<_> = x_scopes.as_slice().to_vec();
2827
2828        // embed_scope should replace, not stack with, embedded syntax's scope
2829        // So we should have: source.v2host, meta.embedded.custom, keyword.x
2830        // but NOT source.v2embedded
2831        let has_custom = scopes
2832            .iter()
2833            .any(|s| s.build_string() == "meta.embedded.custom");
2834        let has_embedded_scope = scopes
2835            .iter()
2836            .any(|s| s.build_string() == "source.v2embedded");
2837        assert!(
2838            has_custom,
2839            "should have embed_scope; scopes: {:?}",
2840            scopes.iter().map(|s| s.build_string()).collect::<Vec<_>>()
2841        );
2842        assert!(
2843            !has_embedded_scope,
2844            "should NOT have embedded syntax scope; scopes: {:?}",
2845            scopes.iter().map(|s| s.build_string()).collect::<Vec<_>>()
2846        );
2847    }
2848
2849    #[test]
2850    fn v2_set_does_not_apply_parent_meta_content_scope_to_matched_text() {
2851        // Per Sublime docs (v2): set action does NOT apply the parent context's
2852        // meta_content_scope to the matched text. In v1 it does.
2853        use crate::parsing::{ParseState, ScopeStack};
2854
2855        fn scopes_at_pos(ops: &[(usize, ScopeStackOp)], pos: usize) -> Vec<String> {
2856            let mut stack = ScopeStack::new();
2857            for (idx, op) in ops {
2858                if *idx <= pos {
2859                    stack.apply(op).unwrap();
2860                }
2861            }
2862            stack.as_slice().iter().map(|s| s.build_string()).collect()
2863        }
2864
2865        // v2 syntax: main has meta_content_scope, 'go' triggers set: other
2866        let v2_syntax = SyntaxDefinition::load_from_str(
2867            r#"
2868            name: V2SetMCS
2869            scope: source.v2setmcs
2870            file_extensions: [v2setmcs]
2871            version: 2
2872            contexts:
2873              main:
2874                - meta_content_scope: meta.content.main
2875                - match: 'go'
2876                  set: other
2877              other:
2878                - match: 'x'
2879                  scope: x
2880            "#,
2881            true,
2882            None,
2883        )
2884        .unwrap();
2885
2886        let mut builder = SyntaxSetBuilder::new();
2887        builder.add(v2_syntax);
2888        let ss = builder.build();
2889
2890        let syntax = ss.find_syntax_by_name("V2SetMCS").unwrap();
2891        let mut state = ParseState::new(syntax);
2892        // "go" is at positions [0, 2); check at position 0 (inside the matched text)
2893        let ops = state.parse_line("go\n", &ss).unwrap().ops;
2894        let v2_scopes = scopes_at_pos(&ops, 0);
2895
2896        // v2: the matched text 'go' should NOT have meta.content.main
2897        // NOTE: This test is expected to FAIL if the v2 behavior is not yet correctly implemented.
2898        assert!(
2899            !v2_scopes.iter().any(|s| s == "meta.content.main"),
2900            "v2: matched text 'go' should NOT have meta.content.main; scopes: {:?}",
2901            v2_scopes
2902        );
2903
2904        // v1 syntax: same structure, version 1
2905        let v1_syntax = SyntaxDefinition::load_from_str(
2906            r#"
2907            name: V1SetMCS
2908            scope: source.v1setmcs
2909            file_extensions: [v1setmcs]
2910            contexts:
2911              main:
2912                - meta_content_scope: meta.content.main
2913                - match: 'go'
2914                  set: other
2915              other:
2916                - match: 'x'
2917                  scope: x
2918            "#,
2919            true,
2920            None,
2921        )
2922        .unwrap();
2923
2924        let mut builder2 = SyntaxSetBuilder::new();
2925        builder2.add(v1_syntax);
2926        let ss2 = builder2.build();
2927
2928        let syntax2 = ss2.find_syntax_by_name("V1SetMCS").unwrap();
2929        let mut state2 = ParseState::new(syntax2);
2930        let ops2 = state2.parse_line("go\n", &ss2).unwrap().ops;
2931        let v1_scopes = scopes_at_pos(&ops2, 0);
2932
2933        // v1: the matched text 'go' SHOULD have meta.content.main
2934        assert!(
2935            v1_scopes.iter().any(|s| s == "meta.content.main"),
2936            "v1: matched text 'go' SHOULD have meta.content.main; scopes: {:?}",
2937            v1_scopes
2938        );
2939    }
2940
2941    #[test]
2942    fn v2_embed_escape_does_not_get_embed_scope() {
2943        // Per Sublime docs: embed_scope applies to text "after the match and before the escape",
2944        // so the escape text should NOT have the embed_scope (meta_content_scope).
2945        // This is the same in both v1 and v2.
2946        use crate::parsing::{ParseState, ScopeStack};
2947
2948        let host = SyntaxDefinition::load_from_str(
2949            r#"
2950            name: V2EmbedMeta
2951            scope: source.v2embedmeta
2952            file_extensions: [v2em]
2953            version: 2
2954            contexts:
2955              main:
2956                - match: '<<'
2957                  embed: scope:source.v2em_embedded
2958                  embed_scope: meta.embedded.block
2959                  escape: '>>'
2960            "#,
2961            true,
2962            None,
2963        )
2964        .unwrap();
2965
2966        let embedded = SyntaxDefinition::load_from_str(
2967            r#"
2968            name: V2EmbedMetaEmbedded
2969            scope: source.v2em_embedded
2970            file_extensions: [v2eme]
2971            version: 2
2972            contexts:
2973              main:
2974                - match: 'x'
2975                  scope: keyword.x
2976            "#,
2977            true,
2978            None,
2979        )
2980        .unwrap();
2981
2982        let mut builder = SyntaxSetBuilder::new();
2983        builder.add(host);
2984        builder.add(embedded);
2985        let ss = builder.build();
2986
2987        // "<<x>>" — '<<' at [0,2], 'x' at [2,3], '>>' at [3,5]
2988        let syntax = ss.find_syntax_by_name("V2EmbedMeta").unwrap();
2989        let mut state = ParseState::new(syntax);
2990        let ops = state.parse_line("<<x>>\n", &ss).unwrap().ops;
2991
2992        // Build scope stack at position 3 (start of '>>' escape text)
2993        let mut stack = ScopeStack::new();
2994        for (idx, op) in &ops {
2995            if *idx <= 3 {
2996                stack.apply(op).unwrap();
2997            }
2998        }
2999        let scopes: Vec<String> = stack.as_slice().iter().map(|s| s.build_string()).collect();
3000
3001        // Escape text '>>' should NOT have the embed_scope (meta.embedded.block)
3002        assert!(
3003            !scopes.iter().any(|s| s == "meta.embedded.block"),
3004            "escape text '>>' should not have meta.embedded.block; scopes: {:?}",
3005            scopes
3006        );
3007    }
3008
3009    #[test]
3010    fn v2_push_multiple_clear_scopes_each_applies() {
3011        // v2 push emits `Clear` for every pushed context that has
3012        // `clear_scopes`, at that context's index position in the push
3013        // order — same as v1. Python's f/t-string interpolation relies on
3014        // this: `clear_scopes: 1` sits on `f-string-replacement-meta` at
3015        // index 0 of a 3-context push, not on the topmost entry.
3016        use crate::parsing::ParseState;
3017
3018        let v2_syntax = SyntaxDefinition::load_from_str(
3019            r#"
3020            name: V2MultiClear
3021            scope: source.v2multiclear
3022            file_extensions: [v2mc]
3023            version: 2
3024            contexts:
3025              main:
3026                - meta_scope: source.v2multiclear
3027                - match: 'go'
3028                  push:
3029                    - ctx_a
3030                    - ctx_b
3031              ctx_a:
3032                - clear_scopes: 1
3033                - meta_scope: ctx.a
3034                - match: 'x'
3035                  pop: 2
3036              ctx_b:
3037                - clear_scopes: 2
3038                - meta_scope: ctx.b
3039                - match: 'x'
3040                  pop: 1
3041            "#,
3042            true,
3043            None,
3044        )
3045        .unwrap();
3046
3047        let mut builder = SyntaxSetBuilder::new();
3048        builder.add(v2_syntax);
3049        let ss = builder.build();
3050
3051        let syntax = ss.find_syntax_by_name("V2MultiClear").unwrap();
3052        let mut state = ParseState::new(syntax);
3053        let ops = state.parse_line("go\n", &ss).unwrap().ops;
3054
3055        let clear_ops: Vec<_> = ops
3056            .iter()
3057            .filter_map(|(_, op)| match op {
3058                ScopeStackOp::Clear(a) => Some(*a),
3059                _ => None,
3060            })
3061            .collect();
3062
3063        // Both ctx_a (TopN(1)) and ctx_b (TopN(2)) apply, in push order.
3064        assert_eq!(
3065            clear_ops,
3066            vec![ClearAmount::TopN(1), ClearAmount::TopN(2)],
3067            "v2: push [ctx_a(1), ctx_b(2)] should emit Clear(1) then Clear(2); got: {:?}",
3068            clear_ops
3069        );
3070
3071        // v1: both ctx_a and ctx_b apply their clear_scopes — two Clear ops
3072        let v1_syntax = SyntaxDefinition::load_from_str(
3073            r#"
3074            name: V1MultiClear
3075            scope: source.v1multiclear
3076            file_extensions: [v1mc]
3077            contexts:
3078              main:
3079                - meta_scope: source.v1multiclear
3080                - match: 'go'
3081                  push:
3082                    - ctx_a
3083                    - ctx_b
3084              ctx_a:
3085                - clear_scopes: 1
3086                - meta_scope: ctx.a
3087                - match: 'x'
3088                  pop: 2
3089              ctx_b:
3090                - clear_scopes: 2
3091                - meta_scope: ctx.b
3092                - match: 'x'
3093                  pop: 1
3094            "#,
3095            true,
3096            None,
3097        )
3098        .unwrap();
3099
3100        let mut builder2 = SyntaxSetBuilder::new();
3101        builder2.add(v1_syntax);
3102        let ss2 = builder2.build();
3103
3104        let syntax2 = ss2.find_syntax_by_name("V1MultiClear").unwrap();
3105        let mut state2 = ParseState::new(syntax2);
3106        let ops2 = state2.parse_line("go\n", &ss2).unwrap().ops;
3107
3108        let v1_clear_ops: Vec<_> = ops2
3109            .iter()
3110            .filter(|(_, op)| matches!(op, ScopeStackOp::Clear(_)))
3111            .collect();
3112
3113        // v1: both ctx_a's clear_scopes and ctx_b's clear_scopes should produce TWO Clear ops
3114        assert_eq!(
3115            v1_clear_ops.len(),
3116            2,
3117            "v1: push [ctx_a, ctx_b] should produce TWO Clear ops (one per context); \
3118             got: {:?}",
3119            v1_clear_ops
3120        );
3121    }
3122
3123    #[test]
3124    fn v2_push_deeper_clear_scopes_applies_and_restores() {
3125        // Pins the invariant Python f/t-string interpolation relies on:
3126        // a multi-context push where a non-topmost entry carries
3127        // `clear_scopes`. The Clear must fire when that entry is pushed,
3128        // and a paired Restore must fire when it is popped — even when the
3129        // pop reaches it via `pop: N` from a shallower context.
3130        //
3131        // Shape:
3132        //   main mcs = "m.outer string.outer"
3133        //   push  = [wrap(clear_scopes: 1, meta_scope: m.wrap), body]
3134        //   body  = matches 'x' with `pop: 2` (unwinds body + wrap together,
3135        //           exercising the deeper-context Restore path).
3136        use crate::parsing::{ParseState, ScopeStack};
3137
3138        let syntax = SyntaxDefinition::load_from_str(
3139            r#"
3140            name: V2DeeperClear
3141            scope: source.deeperclear
3142            file_extensions: [v2dc]
3143            version: 2
3144            contexts:
3145              main:
3146                - meta_content_scope: m.outer string.outer
3147                - match: 'go'
3148                  push:
3149                    - wrap
3150                    - body
3151              wrap:
3152                - clear_scopes: 1
3153                - meta_scope: m.wrap
3154                - include: immediately-pop
3155              immediately-pop:
3156                - match: ''
3157                  pop: 1
3158              body:
3159                - match: 'x'
3160                  scope: word.x
3161                  pop: 2
3162            "#,
3163            true,
3164            None,
3165        )
3166        .unwrap();
3167
3168        let mut builder = SyntaxSetBuilder::new();
3169        builder.add(syntax);
3170        let ss = builder.build();
3171        let sref = ss.find_syntax_by_name("V2DeeperClear").unwrap();
3172
3173        // Scope stack at each position through "go x y\n".
3174        let mut state = ParseState::new(sref);
3175        let mut stack = ScopeStack::new();
3176        let mut scopes_at = |line: &str, cols: &[usize]| -> Vec<Vec<String>> {
3177            let ops = state.parse_line(line, &ss).unwrap().ops;
3178            let mut out = vec![Vec::new(); cols.len()];
3179            let mut next_op = 0;
3180            for (i, _) in line.char_indices() {
3181                while next_op < ops.len() && ops[next_op].0 <= i {
3182                    stack.apply(&ops[next_op].1).unwrap();
3183                    next_op += 1;
3184                }
3185                if let Some(pos) = cols.iter().position(|c| *c == i) {
3186                    out[pos] = stack.as_slice().iter().map(|s| s.build_string()).collect();
3187                }
3188            }
3189            while next_op < ops.len() {
3190                stack.apply(&ops[next_op].1).unwrap();
3191                next_op += 1;
3192            }
3193            out
3194        };
3195
3196        // col 3 = 'x' inside the push — must see m.outer, m.wrap, word.x,
3197        // but NOT string.outer (cleared by wrap's clear_scopes: 1).
3198        // col 5 = 'y' (any trailing byte) after the pop — string.outer
3199        // must be back (Restore restored the cleared atom).
3200        let line = "go x y\n";
3201        let scopes = scopes_at(line, &[3, 5]);
3202
3203        let at_x = &scopes[0];
3204        assert!(
3205            at_x.iter().any(|s| s == "m.outer"),
3206            "'x' should carry m.outer; got {:?}",
3207            at_x
3208        );
3209        assert!(
3210            at_x.iter().any(|s| s == "m.wrap"),
3211            "'x' should carry m.wrap from the pushed wrap meta_scope; got {:?}",
3212            at_x
3213        );
3214        assert!(
3215            !at_x.iter().any(|s| s == "string.outer"),
3216            "'x' should NOT carry string.outer (cleared by wrap.clear_scopes: 1); got {:?}",
3217            at_x
3218        );
3219
3220        let at_y = &scopes[1];
3221        assert!(
3222            at_y.iter().any(|s| s == "m.outer"),
3223            "'y' should carry m.outer after pop; got {:?}",
3224            at_y
3225        );
3226        assert!(
3227            at_y.iter().any(|s| s == "string.outer"),
3228            "'y' should carry string.outer after pop (Restore must fire); got {:?}",
3229            at_y
3230        );
3231        assert!(
3232            !at_y.iter().any(|s| s == "m.wrap"),
3233            "'y' should NOT carry m.wrap after pop; got {:?}",
3234            at_y
3235        );
3236    }
3237
3238    #[test]
3239    fn v2_capture_group_ordering_applies_scopes_in_text_order() {
3240        // Capture group scopes should be applied in text position order, not capture-number order.
3241        // The code sorts captures by (start_pos, -length) so outer/earlier captures come first.
3242        use crate::parsing::ParseState;
3243
3244        let syntax = SyntaxDefinition::load_from_str(
3245            r#"
3246            name: CaptureOrder
3247            scope: source.captureorder
3248            file_extensions: [co]
3249            version: 2
3250            contexts:
3251              main:
3252                - match: '(a(b))'
3253                  captures:
3254                    1: outer.scope
3255                    2: inner.scope
3256            "#,
3257            true,
3258            None,
3259        )
3260        .unwrap();
3261
3262        let mut builder = SyntaxSetBuilder::new();
3263        builder.add(syntax);
3264        let ss = builder.build();
3265
3266        let syntax = ss.find_syntax_by_name("CaptureOrder").unwrap();
3267        let mut state = ParseState::new(syntax);
3268        // "ab" — capture 1 (outer) matches [0,2], capture 2 (inner) matches [1,2]
3269        let ops = state.parse_line("ab\n", &ss).unwrap().ops;
3270
3271        let outer_scope = Scope::new("outer.scope").unwrap();
3272        let inner_scope = Scope::new("inner.scope").unwrap();
3273
3274        let outer_push_idx = ops
3275            .iter()
3276            .position(|(_, op)| matches!(op, ScopeStackOp::Push(s) if *s == outer_scope));
3277        let inner_push_idx = ops
3278            .iter()
3279            .position(|(_, op)| matches!(op, ScopeStackOp::Push(s) if *s == inner_scope));
3280
3281        assert!(
3282            outer_push_idx.is_some(),
3283            "outer.scope should be pushed; ops: {:?}",
3284            ops
3285        );
3286        assert!(
3287            inner_push_idx.is_some(),
3288            "inner.scope should be pushed; ops: {:?}",
3289            ops
3290        );
3291
3292        // outer.scope (starts at pos 0) must be pushed before inner.scope (starts at pos 1)
3293        assert!(
3294            outer_push_idx.unwrap() < inner_push_idx.unwrap(),
3295            "outer.scope (byte pos 0) should be pushed before inner.scope (byte pos 1) in ops; \
3296             outer at ops[{}], inner at ops[{}]",
3297            outer_push_idx.unwrap(),
3298            inner_push_idx.unwrap()
3299        );
3300
3301        // Also verify the byte positions are in text order
3302        let (outer_byte_pos, _) = ops[outer_push_idx.unwrap()];
3303        let (inner_byte_pos, _) = ops[inner_push_idx.unwrap()];
3304        assert!(
3305            outer_byte_pos <= inner_byte_pos,
3306            "outer.scope byte pos ({}) should be <= inner.scope byte pos ({})",
3307            outer_byte_pos,
3308            inner_byte_pos
3309        );
3310    }
3311
3312    #[test]
3313    fn multiple_inheritance_extends_array() {
3314        // Base syntax with a variable and main context
3315        let base = SyntaxDefinition::load_from_str(
3316            r#"
3317            name: Base
3318            scope: source.base
3319            file_extensions: [base]
3320            variables:
3321              IDENT: '[a-z]+'
3322            contexts:
3323              main:
3324                - match: '{{IDENT}}'
3325                  scope: variable.base
3326              helpers:
3327                - match: 'help'
3328                  scope: keyword.help
3329            "#,
3330            false,
3331            None,
3332        )
3333        .unwrap();
3334
3335        // ExtA overrides IDENT and adds a context
3336        let ext_a = SyntaxDefinition::load_from_str(
3337            r#"
3338            name: ExtA
3339            scope: source.ext_a
3340            extends: Base
3341            variables:
3342              IDENT: '[a-zA-Z]+'
3343            contexts:
3344              ext_a_ctx:
3345                - match: 'aaa'
3346                  scope: keyword.a
3347            "#,
3348            false,
3349            None,
3350        )
3351        .unwrap();
3352
3353        // ExtB adds a different variable and context
3354        let ext_b = SyntaxDefinition::load_from_str(
3355            r#"
3356            name: ExtB
3357            scope: source.ext_b
3358            extends: Base
3359            variables:
3360              NUM: '[0-9]+'
3361            contexts:
3362              ext_b_ctx:
3363                - match: 'bbb'
3364                  scope: keyword.b
3365            "#,
3366            false,
3367            None,
3368        )
3369        .unwrap();
3370
3371        // Child extends both ExtA and ExtB
3372        let child = SyntaxDefinition::load_from_str(
3373            r#"
3374            name: Child
3375            scope: source.child
3376            file_extensions: [child]
3377            extends:
3378              - ExtA
3379              - ExtB
3380            contexts:
3381              child_ctx:
3382                - match: 'ccc'
3383                  scope: keyword.c
3384            "#,
3385            false,
3386            None,
3387        )
3388        .unwrap();
3389
3390        assert_eq!(child.extends, vec!["ExtA".to_owned(), "ExtB".to_owned()]);
3391
3392        let mut builder = SyntaxSetBuilder::new();
3393        builder.add(base);
3394        builder.add(ext_a);
3395        builder.add(ext_b);
3396        builder.add(child);
3397        let ss = builder.build();
3398
3399        let child_ref = ss.find_syntax_by_name("Child").unwrap();
3400
3401        // Child should have contexts from both parents and itself
3402        let context_ids = child_ref.context_ids();
3403        assert!(
3404            context_ids.contains_key("main"),
3405            "should inherit main from Base"
3406        );
3407        assert!(
3408            context_ids.contains_key("helpers"),
3409            "should inherit helpers from Base"
3410        );
3411        assert!(
3412            context_ids.contains_key("ext_a_ctx"),
3413            "should inherit ext_a_ctx from ExtA"
3414        );
3415        assert!(
3416            context_ids.contains_key("ext_b_ctx"),
3417            "should inherit ext_b_ctx from ExtB"
3418        );
3419        assert!(
3420            context_ids.contains_key("child_ctx"),
3421            "should have own child_ctx"
3422        );
3423
3424        // Variables: ExtB's NUM should be present, ExtA's IDENT override should be present
3425        // (ExtA overrides Base's IDENT, then ExtB doesn't override it, so ExtA's wins)
3426        // Actually, since ExtB extends Base too, it inherits IDENT from Base.
3427        // Merge order: ExtA first, then ExtB. ExtB's IDENT is Base's '[a-z]+'.
3428        // So the final IDENT depends on merge order: ExtB overrides ExtA's IDENT.
3429        // But ExtB doesn't define IDENT itself, it inherits from Base.
3430        // After resolving ExtB, its variables include Base's IDENT='[a-z]+' and NUM='[0-9]+'.
3431        // After resolving ExtA, its variables include Base+ExtA IDENT='[a-zA-Z]+'.
3432        // Child merges: ExtA first (IDENT='[a-zA-Z]+'), then ExtB (IDENT='[a-z]+', NUM='[0-9]+').
3433        // So final IDENT = '[a-z]+' (from ExtB, which overrides ExtA).
3434
3435        // Verify the child syntax can be used without panicking
3436        let syntax = ss.find_syntax_by_name("Child").unwrap();
3437        let mut state = crate::parsing::ParseState::new(syntax);
3438        let _ops = state.parse_line("hello\n", &ss).unwrap();
3439    }
3440
3441    #[cfg(feature = "yaml-load")]
3442    #[test]
3443    fn find_parent_index_resolves_relative_paths() {
3444        // Simulates a syntax loaded from "Packages/Test/syntaxes/Child.sublime-syntax"
3445        // being extended via the relative path "syntaxes/Child.sublime-syntax".
3446        let syntax = SyntaxDefinition {
3447            name: "Child".to_string(),
3448            file_extensions: vec![],
3449            scope: Scope::new("source.child").unwrap(),
3450            first_line_match: None,
3451            hidden: false,
3452            variables: HashMap::new(),
3453            contexts: HashMap::new(),
3454            extends: vec![],
3455            version: 1,
3456        };
3457
3458        let syntax_definitions = vec![syntax];
3459        let path_syntaxes = vec![(
3460            "Packages/Test/syntaxes/Child.sublime-syntax".to_string(),
3461            0usize,
3462        )];
3463        let mut name_to_index = HashMap::new();
3464        name_to_index.insert("Child".to_string(), 0);
3465
3466        // Relative path should match via suffix matching
3467        let result = SyntaxSetBuilder::find_parent_index(
3468            "syntaxes/Child.sublime-syntax",
3469            &path_syntaxes,
3470            &syntax_definitions,
3471            &name_to_index,
3472        );
3473        assert_eq!(result, Some(0), "relative path should match via suffix");
3474
3475        // Absolute path should also match
3476        let result = SyntaxSetBuilder::find_parent_index(
3477            "Packages/Test/syntaxes/Child.sublime-syntax",
3478            &path_syntaxes,
3479            &syntax_definitions,
3480            &name_to_index,
3481        );
3482        assert_eq!(result, Some(0), "absolute path should match via suffix");
3483
3484        // Just the filename (without extension) should match via name lookup
3485        let result = SyntaxSetBuilder::find_parent_index(
3486            "Child.sublime-syntax",
3487            &path_syntaxes,
3488            &syntax_definitions,
3489            &name_to_index,
3490        );
3491        assert_eq!(
3492            result,
3493            Some(0),
3494            "bare filename should match via name lookup"
3495        );
3496
3497        // A non-matching relative path should return None
3498        let result = SyntaxSetBuilder::find_parent_index(
3499            "other/NonExistent.sublime-syntax",
3500            &path_syntaxes,
3501            &syntax_definitions,
3502            &name_to_index,
3503        );
3504        assert_eq!(result, None, "non-matching path should return None");
3505    }
3506}