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::path::Path;
16use std::sync::{Arc, Mutex};
17
18use super::regex::{Regex, RegexCache};
19use crate::parsing::syntax_definition::ContextId;
20use once_cell::sync::OnceCell;
21use serde_derive::{Deserialize, Serialize};
22
23#[cfg(feature = "regex-census")]
24#[path = "regex_census.rs"]
25mod regex_census;
26#[cfg(feature = "regex-census")]
27pub use regex_census::*;
28
29/// A syntax set holds multiple syntaxes that have been linked together.
30///
31/// Use a [`SyntaxSetBuilder`] to load syntax definitions and build a syntax set.
32///
33/// After building, the syntax set is immutable and can no longer be modified, but you can convert
34/// it back into a builder by using the [`into_builder`] method.
35///
36/// [`SyntaxSetBuilder`]: struct.SyntaxSetBuilder.html
37/// [`into_builder`]: #method.into_builder
38#[derive(Debug, Serialize, Deserialize)]
39pub struct SyntaxSet {
40    #[serde(deserialize_with = "deserialize_syntaxes")]
41    syntaxes: Vec<SyntaxReference>,
42    /// Stores the syntax index for every path that was loaded
43    path_syntaxes: Vec<(String, usize)>,
44
45    #[serde(skip_serializing, skip_deserializing, default = "OnceCell::new")]
46    first_line_cache: OnceCell<FirstLineCache>,
47    /// Metadata, e.g. indent and commenting information.
48    ///
49    /// NOTE: if serializing, you should handle metadata manually; that is, you should serialize and
50    /// deserialize it separately. See `examples/gendata.rs` for an example.
51    #[cfg(feature = "metadata")]
52    #[serde(skip, default)]
53    pub(crate) metadata: Metadata,
54}
55
56/// A linked version of a [`SyntaxDefinition`] that is only useful as part of the
57/// [`SyntaxSet`] that contains it. See docs for [`SyntaxSetBuilder::build`] for
58/// more info.
59#[derive(Clone, Debug, Serialize, Deserialize)]
60pub struct SyntaxReference {
61    pub name: String,
62    pub file_extensions: Vec<String>,
63    pub scope: Scope,
64    pub first_line_match: Option<String>,
65    pub hidden: bool,
66    #[serde(serialize_with = "ordered_map")]
67    pub variables: HashMap<String, String>,
68    #[serde(skip)]
69    pub(crate) lazy_contexts: OnceCell<LazyContexts>,
70    #[serde(skip)]
71    regex_cache: Option<Arc<Mutex<RegexCache>>>,
72    pub(crate) serialized_lazy_contexts: Vec<u8>,
73}
74
75fn deserialize_syntaxes<'de, D>(deserializer: D) -> Result<Vec<SyntaxReference>, D::Error>
76where
77    D: serde::Deserializer<'de>,
78{
79    let mut syntaxes = <Vec<SyntaxReference> as serde::Deserialize>::deserialize(deserializer)?;
80    let cache = Arc::new(Mutex::new(RegexCache::default()));
81    for syntax in &mut syntaxes {
82        syntax.regex_cache = Some(Arc::clone(&cache));
83    }
84    Ok(syntaxes)
85}
86
87/// The lazy-loaded parts of a [`SyntaxReference`].
88#[derive(Clone, Debug, Serialize, Deserialize)]
89pub(crate) struct LazyContexts {
90    #[serde(serialize_with = "ordered_map")]
91    pub(crate) context_ids: HashMap<String, ContextId>,
92    pub(crate) contexts: Vec<Context>,
93}
94
95/// A syntax set builder is used for loading syntax definitions from the file
96/// system or by adding [`SyntaxDefinition`] objects.
97///
98/// Once all the syntaxes have been added, call [`build`] to turn the builder into
99/// a [`SyntaxSet`] that can be used for parsing or highlighting.
100///
101/// [`SyntaxDefinition`]: syntax_definition/struct.SyntaxDefinition.html
102/// [`build`]: #method.build
103/// [`SyntaxSet`]: struct.SyntaxSet.html
104#[derive(Clone, Default)]
105pub struct SyntaxSetBuilder {
106    syntaxes: Vec<SyntaxDefinition>,
107    path_syntaxes: Vec<(String, usize)>,
108    #[cfg(feature = "metadata")]
109    raw_metadata: LoadMetadata,
110
111    /// If this `SyntaxSetBuilder` is created with `SyntaxSet::into_builder`
112    /// from a `SyntaxSet` that already had metadata, we keep that metadata,
113    /// merging it with newly loaded metadata.
114    #[cfg(feature = "metadata")]
115    existing_metadata: Option<Metadata>,
116}
117
118#[cfg(feature = "yaml-load")]
119fn load_syntax_file(
120    p: &Path,
121    lines_include_newline: bool,
122) -> Result<SyntaxDefinition, LoadingError> {
123    let s = std::fs::read_to_string(p)?;
124
125    SyntaxDefinition::load_from_str(
126        &s,
127        lines_include_newline,
128        p.file_stem().and_then(|x| x.to_str()),
129    )
130    .map_err(|e| LoadingError::ParseSyntax(e, format!("{}", p.display())))
131}
132
133impl Clone for SyntaxSet {
134    fn clone(&self) -> SyntaxSet {
135        SyntaxSet {
136            syntaxes: self.syntaxes.clone(),
137            path_syntaxes: self.path_syntaxes.clone(),
138            // Will need to be re-initialized
139            first_line_cache: OnceCell::new(),
140            #[cfg(feature = "metadata")]
141            metadata: self.metadata.clone(),
142        }
143    }
144}
145
146impl Default for SyntaxSet {
147    fn default() -> Self {
148        SyntaxSet {
149            syntaxes: Vec::new(),
150            path_syntaxes: Vec::new(),
151            first_line_cache: OnceCell::new(),
152            #[cfg(feature = "metadata")]
153            metadata: Metadata::default(),
154        }
155    }
156}
157
158impl SyntaxSet {
159    pub fn new() -> SyntaxSet {
160        SyntaxSet::default()
161    }
162
163    /// Convenience constructor for creating a builder, then loading syntax
164    /// definitions from a folder and then building the syntax set.
165    ///
166    /// Note that this uses `lines_include_newline` set to `false`, see the
167    /// [`add_from_folder`] method docs on [`SyntaxSetBuilder`] for an explanation
168    /// as to why this might not be the best.
169    ///
170    /// [`add_from_folder`]: struct.SyntaxSetBuilder.html#method.add_from_folder
171    /// [`SyntaxSetBuilder`]: struct.SyntaxSetBuilder.html
172    #[cfg(feature = "yaml-load")]
173    pub fn load_from_folder<P: AsRef<Path>>(folder: P) -> Result<SyntaxSet, LoadingError> {
174        let mut builder = SyntaxSetBuilder::new();
175        builder.add_from_folder(folder, false)?;
176        Ok(builder.build())
177    }
178
179    /// The list of syntaxes in the set
180    pub fn syntaxes(&self) -> &[SyntaxReference] {
181        &self.syntaxes[..]
182    }
183
184    #[cfg(feature = "metadata")]
185    pub fn set_metadata(&mut self, metadata: Metadata) {
186        self.metadata = metadata;
187    }
188
189    /// The loaded metadata for this set.
190    #[cfg(feature = "metadata")]
191    pub fn metadata(&self) -> &Metadata {
192        &self.metadata
193    }
194
195    /// Finds a syntax by its default scope, for example `source.regexp` finds the regex syntax.
196    ///
197    /// This and all similar methods below do a linear search of syntaxes, this should be fast
198    /// because there aren't many syntaxes, but don't think you can call it a bajillion times per
199    /// second.
200    pub fn find_syntax_by_scope(&self, scope: Scope) -> Option<&SyntaxReference> {
201        self.syntaxes.iter().rev().find(|&s| s.scope == scope)
202    }
203
204    pub fn find_syntax_by_name<'a>(&'a self, name: &str) -> Option<&'a SyntaxReference> {
205        self.syntaxes.iter().rev().find(|&s| name == s.name)
206    }
207
208    pub fn find_syntax_by_extension<'a>(&'a self, extension: &str) -> Option<&'a SyntaxReference> {
209        self.syntaxes.iter().rev().find(|&s| {
210            s.file_extensions
211                .iter()
212                .any(|e| e.eq_ignore_ascii_case(extension))
213        })
214    }
215
216    /// Searches for a syntax first by extension and then by case-insensitive name
217    ///
218    /// This is useful for things like Github-flavoured-markdown code block highlighting where all
219    /// you have to go on is a short token given by the user
220    pub fn find_syntax_by_token<'a>(&'a self, s: &str) -> Option<&'a SyntaxReference> {
221        {
222            let ext_res = self.find_syntax_by_extension(s);
223            if ext_res.is_some() {
224                return ext_res;
225            }
226        }
227        self.syntaxes
228            .iter()
229            .rev()
230            .find(|&syntax| syntax.name.eq_ignore_ascii_case(s))
231    }
232
233    /// Try to find the syntax for a file based on its first line
234    ///
235    /// This uses regexes that come with some sublime syntax grammars for matching things like
236    /// shebangs and mode lines like `-*- Mode: C -*-`
237    pub fn find_syntax_by_first_line<'a>(&'a self, s: &str) -> Option<&'a SyntaxReference> {
238        let s = s.strip_prefix("\u{feff}").unwrap_or(s); // Strip UTF-8 BOM
239        let cache = self.first_line_cache();
240        for &(ref reg, i) in cache.regexes.iter().rev() {
241            if reg.search(s, 0, s.len(), None) {
242                return Some(&self.syntaxes[i]);
243            }
244        }
245        None
246    }
247
248    /// Searches for a syntax by it's original file path when it was first loaded from disk
249    ///
250    /// This is primarily useful for syntax tests. Some may specify a
251    /// `Packages/PackageName/SyntaxName.sublime-syntax` path, and others may just have
252    /// `SyntaxName.sublime-syntax`. This caters for these by matching the end of the path of the
253    /// loaded syntax definition files
254    // 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
255    pub fn find_syntax_by_path<'a>(&'a self, path: &str) -> Option<&'a SyntaxReference> {
256        let mut slash_path = "/".to_string();
257        slash_path.push_str(path);
258        self.path_syntaxes
259            .iter()
260            .rev()
261            .find(|t| t.0.ends_with(&slash_path) || t.0 == path)
262            .map(|&(_, i)| &self.syntaxes[i])
263    }
264
265    /// Convenience method that tries to find the syntax for a file path, first by extension/name
266    /// and then by first line of the file if that doesn't work.
267    ///
268    /// May IO Error because it sometimes tries to read the first line of the file.
269    ///
270    /// # Examples
271    ///
272    /// When determining how to highlight a file, use this in combination with a fallback to plain
273    /// text:
274    ///
275    /// ```
276    /// use syntect::parsing::SyntaxSet;
277    /// let ss = SyntaxSet::load_defaults_newlines();
278    /// let syntax = ss.find_syntax_for_file("testdata/highlight_test.erb")
279    ///     .unwrap() // for IO errors, you may want to use try!() or another plain text fallback
280    ///     .unwrap_or_else(|| ss.find_syntax_plain_text());
281    /// assert_eq!(syntax.name, "HTML (Rails)");
282    /// ```
283    pub fn find_syntax_for_file<P: AsRef<Path>>(
284        &self,
285        path_obj: P,
286    ) -> io::Result<Option<&SyntaxReference>> {
287        let path: &Path = path_obj.as_ref();
288        let file_name = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
289        let extension = path.extension().and_then(|x| x.to_str()).unwrap_or("");
290        let ext_syntax = self
291            .find_syntax_by_extension(file_name)
292            .or_else(|| self.find_syntax_by_extension(extension));
293        let line_syntax = if ext_syntax.is_none() {
294            let mut line = String::new();
295            let f = File::open(path)?;
296            let mut line_reader = BufReader::new(&f);
297            line_reader.read_line(&mut line)?;
298            self.find_syntax_by_first_line(&line)
299        } else {
300            None
301        };
302        let syntax = ext_syntax.or(line_syntax);
303        Ok(syntax)
304    }
305
306    /// Finds a syntax for plain text, which usually has no highlighting rules.
307    ///
308    /// This is good as a fallback when you can't find another syntax but you still want to use the
309    /// same highlighting pipeline code.
310    ///
311    /// This syntax should always be present, if not this method will panic. If the way you load
312    /// syntaxes doesn't create one, use [`add_plain_text_syntax`].
313    ///
314    /// # Examples
315    /// ```
316    /// use syntect::parsing::SyntaxSetBuilder;
317    /// let mut builder = SyntaxSetBuilder::new();
318    /// builder.add_plain_text_syntax();
319    /// let ss = builder.build();
320    /// let syntax = ss.find_syntax_by_token("rs").unwrap_or_else(|| ss.find_syntax_plain_text());
321    /// assert_eq!(syntax.name, "Plain Text");
322    /// ```
323    ///
324    /// [`add_plain_text_syntax`]: struct.SyntaxSetBuilder.html#method.add_plain_text_syntax
325    pub fn find_syntax_plain_text(&self) -> &SyntaxReference {
326        self.find_syntax_by_name("Plain Text")
327            .expect("All syntax sets ought to have a plain text syntax")
328    }
329
330    /// Converts this syntax set into a builder so that more syntaxes can be
331    /// added to it.
332    ///
333    /// Note that newly added syntaxes can have references to existing syntaxes
334    /// in the set, but not the other way around.
335    pub fn into_builder(self) -> SyntaxSetBuilder {
336        #[cfg(feature = "metadata")]
337        let SyntaxSet {
338            syntaxes,
339            path_syntaxes,
340            metadata,
341            ..
342        } = self;
343        #[cfg(not(feature = "metadata"))]
344        let SyntaxSet {
345            syntaxes,
346            path_syntaxes,
347            ..
348        } = self;
349
350        let mut context_map = HashMap::new();
351        for (syntax_index, syntax) in syntaxes.iter().enumerate() {
352            for (context_index, context) in syntax.contexts().iter().enumerate() {
353                context_map.insert(
354                    ContextId {
355                        syntax_index,
356                        context_index,
357                    },
358                    context.clone(),
359                );
360            }
361        }
362
363        let mut builder_syntaxes = Vec::with_capacity(syntaxes.len());
364
365        for syntax in syntaxes {
366            let SyntaxReference {
367                name,
368                file_extensions,
369                scope,
370                first_line_match,
371                hidden,
372                variables,
373                serialized_lazy_contexts,
374                ..
375            } = syntax;
376
377            let lazy_contexts = LazyContexts::deserialize(&serialized_lazy_contexts[..]);
378            let mut builder_contexts = HashMap::with_capacity(lazy_contexts.context_ids.len());
379            for (name, context_id) in lazy_contexts.context_ids {
380                if let Some(context) = context_map.remove(&context_id) {
381                    builder_contexts.insert(name, context);
382                }
383            }
384
385            let syntax_definition = SyntaxDefinition {
386                name,
387                file_extensions,
388                scope,
389                first_line_match,
390                hidden,
391                variables,
392                contexts: builder_contexts,
393            };
394            builder_syntaxes.push(syntax_definition);
395        }
396
397        SyntaxSetBuilder {
398            syntaxes: builder_syntaxes,
399            path_syntaxes,
400            #[cfg(feature = "metadata")]
401            existing_metadata: Some(metadata),
402            #[cfg(feature = "metadata")]
403            raw_metadata: LoadMetadata::default(),
404        }
405    }
406
407    #[inline(always)]
408    pub(crate) fn get_context(&self, context_id: &ContextId) -> Result<&Context, ParsingError> {
409        let syntax = &self
410            .syntaxes
411            .get(context_id.syntax_index)
412            .ok_or(ParsingError::MissingContext(*context_id))?;
413        syntax
414            .contexts()
415            .get(context_id.context_index)
416            .ok_or(ParsingError::MissingContext(*context_id))
417    }
418
419    fn first_line_cache(&self) -> &FirstLineCache {
420        self.first_line_cache
421            .get_or_init(|| FirstLineCache::new(self.syntaxes()))
422    }
423
424    pub fn find_unlinked_contexts(&self) -> BTreeSet<String> {
425        let SyntaxSet { syntaxes, .. } = self;
426
427        let mut unlinked_contexts = BTreeSet::new();
428
429        for syntax in syntaxes {
430            let SyntaxReference { name, scope, .. } = syntax;
431
432            for context in syntax.contexts() {
433                Self::find_unlinked_contexts_in_context(
434                    name,
435                    scope,
436                    context,
437                    &mut unlinked_contexts,
438                );
439            }
440        }
441        unlinked_contexts
442    }
443
444    fn find_unlinked_contexts_in_context(
445        name: &str,
446        scope: &Scope,
447        context: &Context,
448        unlinked_contexts: &mut BTreeSet<String>,
449    ) {
450        for pattern in context.patterns.iter() {
451            let maybe_refs_to_check = match pattern {
452                Pattern::Match(match_pat) => match &match_pat.operation {
453                    MatchOperation::Push(context_refs) => Some(context_refs),
454                    MatchOperation::Set(context_refs) => Some(context_refs),
455                    _ => None,
456                },
457                _ => None,
458            };
459            for context_ref in maybe_refs_to_check.into_iter().flatten() {
460                match context_ref {
461                    ContextReference::Direct(_) => {}
462                    _ => {
463                        unlinked_contexts.insert(format!(
464                            "Syntax '{}' with scope '{}' has unresolved context reference {:?}",
465                            name, scope, &context_ref
466                        ));
467                    }
468                }
469            }
470        }
471    }
472}
473
474impl SyntaxReference {
475    pub(crate) fn context_ids(&self) -> &HashMap<String, ContextId> {
476        &self.lazy_contexts().context_ids
477    }
478
479    fn contexts(&self) -> &[Context] {
480        &self.lazy_contexts().contexts
481    }
482
483    fn lazy_contexts(&self) -> &LazyContexts {
484        self.lazy_contexts.get_or_init(|| {
485            #[cfg(feature = "syntax-timing")]
486            let _timing =
487                super::syntax_timing::Initializer::new(super::syntax_timing::Phase::Grammar);
488            let mut contexts = LazyContexts::deserialize(&self.serialized_lazy_contexts[..]);
489            if let Some(cache) = &self.regex_cache {
490                contexts.share_regexes(&mut cache.lock().unwrap());
491            } else {
492                contexts.share_regexes(&mut RegexCache::default());
493            }
494            contexts
495        })
496    }
497}
498
499impl LazyContexts {
500    fn share_regexes(&mut self, cache: &mut RegexCache) {
501        for context in &mut self.contexts {
502            for pattern in &mut context.patterns {
503                if let Pattern::Match(pattern) = pattern {
504                    cache.share(&mut pattern.regex);
505                }
506            }
507        }
508    }
509
510    fn deserialize(data: &[u8]) -> LazyContexts {
511        crate::dumps::from_reader(data).expect("data is not corrupt or out of sync with the code")
512    }
513}
514
515impl SyntaxSetBuilder {
516    pub fn new() -> SyntaxSetBuilder {
517        SyntaxSetBuilder::default()
518    }
519
520    /// Add a syntax to the set.
521    pub fn add(&mut self, syntax: SyntaxDefinition) {
522        self.syntaxes.push(syntax);
523    }
524
525    /// The list of syntaxes added so far.
526    pub fn syntaxes(&self) -> &[SyntaxDefinition] {
527        &self.syntaxes[..]
528    }
529
530    /// A rarely useful method that loads in a syntax with no highlighting rules for plain text
531    ///
532    /// Exists mainly for adding the plain text syntax to syntax set dumps, because for some reason
533    /// the default Sublime plain text syntax is still in `.tmLanguage` format.
534    #[cfg(feature = "yaml-load")]
535    pub fn add_plain_text_syntax(&mut self) {
536        let s = "---\nname: Plain Text\nfile_extensions: [txt]\nscope: text.plain\ncontexts: \
537                 {main: []}";
538        let syn = SyntaxDefinition::load_from_str(s, false, None).unwrap();
539        self.syntaxes.push(syn);
540    }
541
542    /// Loads all the `.sublime-syntax` files in a folder into this builder.
543    ///
544    /// The `lines_include_newline` parameter is used to work around the fact that Sublime Text
545    /// normally passes line strings including newline characters (`\n`) to its regex engine. This
546    /// results in many syntaxes having regexes matching `\n`, which doesn't work if you don't pass
547    /// in newlines. It is recommended that if you can you pass in lines with newlines if you can
548    /// and pass `true` for this parameter. If that is inconvenient pass `false` and the loader
549    /// will do some hacky find and replaces on the match regexes that seem to work for the default
550    /// syntax set, but may not work for any other syntaxes.
551    ///
552    /// In the future I might include a "slow mode" that copies the lines passed in and appends a
553    /// newline if there isn't one, but in the interest of performance currently this hacky fix will
554    /// have to do.
555    #[cfg(feature = "yaml-load")]
556    pub fn add_from_folder<P: AsRef<Path>>(
557        &mut self,
558        folder: P,
559        lines_include_newline: bool,
560    ) -> Result<(), LoadingError> {
561        for entry in crate::utils::walk_dir(folder).sort_by(|a, b| a.file_name().cmp(b.file_name()))
562        {
563            let entry = entry.map_err(LoadingError::WalkDir)?;
564            if entry
565                .path()
566                .extension()
567                .is_some_and(|e| e == "sublime-syntax")
568            {
569                let syntax = load_syntax_file(entry.path(), lines_include_newline)?;
570                if let Some(path_str) = entry.path().to_str() {
571                    // Split the path up and rejoin with slashes so that syntaxes loaded on Windows
572                    // can still be loaded the same way.
573                    let path = Path::new(path_str);
574                    let path_parts: Vec<_> = path.iter().map(|c| c.to_str().unwrap()).collect();
575                    self.path_syntaxes
576                        .push((path_parts.join("/").to_string(), self.syntaxes.len()));
577                }
578                self.syntaxes.push(syntax);
579            }
580
581            #[cfg(feature = "metadata")]
582            {
583                if entry.path().extension() == Some("tmPreferences".as_ref()) {
584                    match RawMetadataEntry::load(entry.path()) {
585                        Ok(meta) => self.raw_metadata.add_raw(meta),
586                        Err(_err) => (),
587                    }
588                }
589            }
590        }
591
592        Ok(())
593    }
594
595    /// Build a [`SyntaxSet`] from the syntaxes that have been added to this
596    /// builder.
597    ///
598    /// ### Linking
599    ///
600    /// The contexts in syntaxes can reference other contexts in the same syntax
601    /// or even other syntaxes. For example, a HTML syntax can reference a CSS
602    /// syntax so that CSS blocks in HTML work as expected.
603    ///
604    /// Those references work in various ways and involve one or two lookups.
605    /// To avoid having to do these lookups during parsing/highlighting, the
606    /// references are changed to directly reference contexts via index. That's
607    /// called linking.
608    ///
609    /// Linking is done in this build step. So in order to get the best
610    /// performance, you should try to avoid calling this too much. Ideally,
611    /// create a [`SyntaxSet`] once and then use it many times. If you can,
612    /// serialize a [`SyntaxSet`] for your program and when you run the program,
613    /// directly load the [`SyntaxSet`].
614    ///
615    /// [`SyntaxSet`]: struct.SyntaxSet.html
616    pub fn build(self) -> SyntaxSet {
617        #[cfg(not(feature = "metadata"))]
618        let SyntaxSetBuilder {
619            syntaxes: syntax_definitions,
620            path_syntaxes,
621        } = self;
622        #[cfg(feature = "metadata")]
623        let SyntaxSetBuilder {
624            syntaxes: syntax_definitions,
625            path_syntaxes,
626            raw_metadata,
627            existing_metadata,
628        } = self;
629
630        let mut syntaxes = Vec::with_capacity(syntax_definitions.len());
631        let regex_cache = Arc::new(Mutex::new(RegexCache::default()));
632        let mut all_context_ids = Vec::new();
633        let mut all_contexts = vec![Vec::new(); syntax_definitions.len()];
634
635        for (syntax_index, syntax_definition) in syntax_definitions.into_iter().enumerate() {
636            let SyntaxDefinition {
637                name,
638                file_extensions,
639                scope,
640                first_line_match,
641                hidden,
642                variables,
643                contexts,
644            } = syntax_definition;
645
646            let mut context_ids = HashMap::new();
647
648            let mut contexts: Vec<(String, Context)> = contexts.into_iter().collect();
649            // Sort the values of the HashMap so that the contexts in the
650            // resulting SyntaxSet have a deterministic order for serializing.
651            // Because we're sorting by the keys which are unique, we can use
652            // an unstable sort.
653            contexts.sort_unstable_by(|(name_a, _), (name_b, _)| name_a.cmp(name_b));
654            for (name, context) in contexts {
655                let context_index = all_contexts[syntax_index].len();
656                context_ids.insert(
657                    name,
658                    ContextId {
659                        syntax_index,
660                        context_index,
661                    },
662                );
663                all_contexts[syntax_index].push(context);
664            }
665
666            let syntax = SyntaxReference {
667                name,
668                file_extensions,
669                scope,
670                first_line_match,
671                hidden,
672                variables,
673                lazy_contexts: OnceCell::new(),
674                regex_cache: Some(Arc::clone(&regex_cache)),
675                serialized_lazy_contexts: Vec::new(), // initialized in the last step
676            };
677            syntaxes.push(syntax);
678            all_context_ids.push(context_ids);
679        }
680
681        let mut found_more_backref_includes = true;
682        for (syntax_index, _syntax) in syntaxes.iter().enumerate() {
683            let mut no_prototype = HashSet::new();
684            let prototype = all_context_ids[syntax_index].get("prototype");
685            if let Some(prototype_id) = prototype {
686                // TODO: We could do this after parsing YAML, instead of here?
687                Self::recursively_mark_no_prototype(
688                    prototype_id,
689                    &all_context_ids[syntax_index],
690                    &all_contexts,
691                    &mut no_prototype,
692                );
693            }
694
695            for context_id in all_context_ids[syntax_index].values() {
696                let context = &mut all_contexts[context_id.syntax_index][context_id.context_index];
697                if let Some(prototype_id) = prototype {
698                    if context.meta_include_prototype && !no_prototype.contains(context_id) {
699                        context.prototype = Some(*prototype_id);
700                    }
701                }
702                Self::link_context(context, syntax_index, &all_context_ids, &syntaxes);
703
704                if context.uses_backrefs {
705                    found_more_backref_includes = true;
706                }
707            }
708        }
709
710        // We need to recursively mark contexts that include contexts which
711        // use backreferences as using backreferences. In theory we could use
712        // a more efficient method here like doing a toposort or constructing
713        // a representation with reversed edges and then tracing in the
714        // opposite direction, but I benchmarked this and it adds <2% to link
715        // time on the default syntax set, and linking doesn't even happen
716        // when loading from a binary dump.
717        while found_more_backref_includes {
718            found_more_backref_includes = false;
719            // find any contexts which include a context which uses backrefs
720            // and mark those as using backrefs - to support nested includes
721            for syntax_index in 0..syntaxes.len() {
722                for context_index in 0..all_contexts[syntax_index].len() {
723                    let context = &all_contexts[syntax_index][context_index];
724                    if !context.uses_backrefs && context.patterns.iter().any(|pattern| {
725                        matches!(pattern, Pattern::Include(ContextReference::Direct(id)) if all_contexts[id.syntax_index][id.context_index].uses_backrefs)
726                    }) {
727                        let context = &mut all_contexts[syntax_index][context_index];
728                        context.uses_backrefs = true;
729                        // look for contexts including this context
730                        found_more_backref_includes = true;
731                    }
732                }
733            }
734        }
735
736        #[cfg(feature = "metadata")]
737        let metadata = match existing_metadata {
738            Some(existing) => existing.merged_with_raw(raw_metadata),
739            None => raw_metadata.into(),
740        };
741
742        // The combination of
743        //  * the algorithms above
744        //  * the borrow checker
745        // makes it necessary to set these up as the last step.
746        for syntax in &mut syntaxes {
747            let lazy_contexts = LazyContexts {
748                context_ids: all_context_ids.remove(0),
749                contexts: all_contexts.remove(0),
750            };
751
752            syntax.serialized_lazy_contexts = crate::dumps::dump_binary(&lazy_contexts);
753        }
754
755        SyntaxSet {
756            syntaxes,
757            path_syntaxes,
758            first_line_cache: OnceCell::new(),
759            #[cfg(feature = "metadata")]
760            metadata,
761        }
762    }
763
764    /// Anything recursively included by the prototype shouldn't include the prototype.
765    /// This marks them as such.
766    fn recursively_mark_no_prototype(
767        context_id: &ContextId,
768        syntax_context_ids: &HashMap<String, ContextId>,
769        all_contexts: &[Vec<Context>],
770        no_prototype: &mut HashSet<ContextId>,
771    ) {
772        let first_time = no_prototype.insert(*context_id);
773        if !first_time {
774            return;
775        }
776
777        for pattern in &all_contexts[context_id.syntax_index][context_id.context_index].patterns {
778            match *pattern {
779                // Apparently inline blocks also don't include the prototype when within the prototype.
780                // This is really weird, but necessary to run the YAML syntax.
781                Pattern::Match(ref match_pat) => {
782                    let maybe_context_refs = match match_pat.operation {
783                        MatchOperation::Push(ref context_refs)
784                        | MatchOperation::Set(ref context_refs) => Some(context_refs),
785                        MatchOperation::Pop | MatchOperation::None => None,
786                    };
787                    if let Some(context_refs) = maybe_context_refs {
788                        for context_ref in context_refs.iter() {
789                            match context_ref {
790                                ContextReference::Inline(ref s)
791                                | ContextReference::Named(ref s) => {
792                                    if let Some(i) = syntax_context_ids.get(s) {
793                                        Self::recursively_mark_no_prototype(
794                                            i,
795                                            syntax_context_ids,
796                                            all_contexts,
797                                            no_prototype,
798                                        );
799                                    }
800                                }
801                                ContextReference::Direct(ref id) => {
802                                    Self::recursively_mark_no_prototype(
803                                        id,
804                                        syntax_context_ids,
805                                        all_contexts,
806                                        no_prototype,
807                                    );
808                                }
809                                _ => (),
810                            }
811                        }
812                    }
813                }
814                Pattern::Include(ref reference) => match reference {
815                    ContextReference::Named(ref s) => {
816                        if let Some(id) = syntax_context_ids.get(s) {
817                            Self::recursively_mark_no_prototype(
818                                id,
819                                syntax_context_ids,
820                                all_contexts,
821                                no_prototype,
822                            );
823                        }
824                    }
825                    ContextReference::Direct(ref id) => {
826                        Self::recursively_mark_no_prototype(
827                            id,
828                            syntax_context_ids,
829                            all_contexts,
830                            no_prototype,
831                        );
832                    }
833                    _ => (),
834                },
835            }
836        }
837    }
838
839    fn link_context(
840        context: &mut Context,
841        syntax_index: usize,
842        all_context_ids: &[HashMap<String, ContextId>],
843        syntaxes: &[SyntaxReference],
844    ) {
845        for pattern in &mut context.patterns {
846            match *pattern {
847                Pattern::Match(ref mut match_pat) => {
848                    Self::link_match_pat(match_pat, syntax_index, all_context_ids, syntaxes)
849                }
850                Pattern::Include(ref mut context_ref) => {
851                    Self::link_ref(context_ref, syntax_index, all_context_ids, syntaxes)
852                }
853            }
854        }
855    }
856
857    fn link_ref(
858        context_ref: &mut ContextReference,
859        syntax_index: usize,
860        all_context_ids: &[HashMap<String, ContextId>],
861        syntaxes: &[SyntaxReference],
862    ) {
863        // println!("{:?}", context_ref);
864        use super::syntax_definition::ContextReference::*;
865        let linked_context_id = match *context_ref {
866            Named(ref s) | Inline(ref s) => {
867                // This isn't actually correct, but it is better than nothing/crashing.
868                // This is being phased out anyhow, see https://github.com/sublimehq/Packages/issues/73
869                // Fixes issue #30
870                if s == "$top_level_main" {
871                    all_context_ids[syntax_index].get("main")
872                } else {
873                    all_context_ids[syntax_index].get(s)
874                }
875            }
876            ByScope {
877                scope,
878                ref sub_context,
879                with_escape,
880            } => Self::with_plain_text_fallback(
881                all_context_ids,
882                syntaxes,
883                with_escape,
884                Self::find_id(sub_context, all_context_ids, syntaxes, |index_and_syntax| {
885                    index_and_syntax.1.scope == scope
886                }),
887            ),
888            File {
889                ref name,
890                ref sub_context,
891                with_escape,
892            } => Self::with_plain_text_fallback(
893                all_context_ids,
894                syntaxes,
895                with_escape,
896                Self::find_id(sub_context, all_context_ids, syntaxes, |index_and_syntax| {
897                    &index_and_syntax.1.name == name
898                }),
899            ),
900            Direct(_) => None,
901        };
902        if let Some(context_id) = linked_context_id {
903            let mut new_ref = Direct(*context_id);
904            mem::swap(context_ref, &mut new_ref);
905        }
906    }
907
908    fn with_plain_text_fallback<'a>(
909        all_context_ids: &'a [HashMap<String, ContextId>],
910        syntaxes: &'a [SyntaxReference],
911        with_escape: bool,
912        context_id: Option<&'a ContextId>,
913    ) -> Option<&'a ContextId> {
914        context_id.or_else(|| {
915            if with_escape {
916                // If we keep this reference unresolved, syntect will crash
917                // when it encounters the reference. Rather than crashing,
918                // we instead fall back to "Plain Text". This seems to be
919                // how Sublime Text behaves. It should be a safe thing to do
920                // since `embed`s always includes an `escape` to get out of
921                // the `embed`.
922                Self::find_id(&None, all_context_ids, syntaxes, |index_and_syntax| {
923                    index_and_syntax.1.name == "Plain Text"
924                })
925            } else {
926                None
927            }
928        })
929    }
930
931    fn find_id<'a>(
932        sub_context: &Option<String>,
933        all_context_ids: &'a [HashMap<String, ContextId>],
934        syntaxes: &'a [SyntaxReference],
935        predicate: impl FnMut(&(usize, &SyntaxReference)) -> bool,
936    ) -> Option<&'a ContextId> {
937        let context_name = sub_context.as_ref().map_or("main", |x| &**x);
938        syntaxes
939            .iter()
940            .enumerate()
941            .rev()
942            .find(predicate)
943            .and_then(|index_and_syntax| all_context_ids[index_and_syntax.0].get(context_name))
944    }
945
946    fn link_match_pat(
947        match_pat: &mut MatchPattern,
948        syntax_index: usize,
949        all_context_ids: &[HashMap<String, ContextId>],
950        syntaxes: &[SyntaxReference],
951    ) {
952        let maybe_context_refs = match match_pat.operation {
953            MatchOperation::Push(ref mut context_refs)
954            | MatchOperation::Set(ref mut context_refs) => Some(context_refs),
955            MatchOperation::Pop | MatchOperation::None => None,
956        };
957        if let Some(context_refs) = maybe_context_refs {
958            for context_ref in context_refs.iter_mut() {
959                Self::link_ref(context_ref, syntax_index, all_context_ids, syntaxes);
960            }
961        }
962        if let Some(ref mut context_ref) = match_pat.with_prototype {
963            Self::link_ref(context_ref, syntax_index, all_context_ids, syntaxes);
964        }
965    }
966}
967
968#[derive(Debug)]
969struct FirstLineCache {
970    /// (first line regex, syntax index) pairs for all syntaxes with a first line regex
971    regexes: Vec<(Regex, usize)>,
972}
973
974impl FirstLineCache {
975    fn new(syntaxes: &[SyntaxReference]) -> FirstLineCache {
976        let mut regexes = Vec::new();
977        for (i, syntax) in syntaxes.iter().enumerate() {
978            if let Some(ref reg_str) = syntax.first_line_match {
979                let reg = Regex::new(reg_str.into());
980                regexes.push((reg, i));
981            }
982        }
983        FirstLineCache { regexes }
984    }
985}
986
987#[cfg(feature = "yaml-load")]
988#[cfg(test)]
989mod tests {
990    use super::*;
991    use crate::{
992        parsing::{syntax_definition, ParseState, Scope},
993        utils::testdata,
994    };
995    use std::collections::HashMap;
996
997    #[test]
998    fn can_load() {
999        let mut builder = testdata::PACKAGES_SYN_SET.to_owned().into_builder();
1000
1001        let cmake_dummy_syntax = SyntaxDefinition {
1002            name: "CMake".to_string(),
1003            file_extensions: vec!["CMakeLists.txt".to_string(), "cmake".to_string()],
1004            scope: Scope::new("source.cmake").unwrap(),
1005            first_line_match: None,
1006            hidden: false,
1007            variables: HashMap::new(),
1008            contexts: HashMap::new(),
1009        };
1010
1011        builder.add(cmake_dummy_syntax);
1012        builder.add_plain_text_syntax();
1013
1014        let ps = builder.build();
1015
1016        assert_eq!(
1017            &ps.find_syntax_by_first_line("#!/usr/bin/env node")
1018                .unwrap()
1019                .name,
1020            "JavaScript"
1021        );
1022        let rails_scope = Scope::new("source.ruby.rails").unwrap();
1023        let syntax = ps.find_syntax_by_name("Ruby on Rails").unwrap();
1024        ps.find_syntax_plain_text();
1025        assert_eq!(&ps.find_syntax_by_extension("rake").unwrap().name, "Ruby");
1026        assert_eq!(&ps.find_syntax_by_extension("RAKE").unwrap().name, "Ruby");
1027        assert_eq!(&ps.find_syntax_by_token("ruby").unwrap().name, "Ruby");
1028        assert_eq!(
1029            &ps.find_syntax_by_first_line("lol -*- Mode: C -*- such line")
1030                .unwrap()
1031                .name,
1032            "C"
1033        );
1034        assert_eq!(
1035            &ps.find_syntax_for_file("testdata/parser.rs")
1036                .unwrap()
1037                .unwrap()
1038                .name,
1039            "Rust"
1040        );
1041        assert_eq!(
1042            &ps.find_syntax_for_file("testdata/test_first_line.test")
1043                .expect("Error finding syntax for file")
1044                .expect("No syntax found for file")
1045                .name,
1046            "Ruby"
1047        );
1048        assert_eq!(
1049            &ps.find_syntax_for_file(".bashrc").unwrap().unwrap().name,
1050            "Bourne Again Shell (bash)"
1051        );
1052        assert_eq!(
1053            &ps.find_syntax_for_file("CMakeLists.txt")
1054                .unwrap()
1055                .unwrap()
1056                .name,
1057            "CMake"
1058        );
1059        assert_eq!(
1060            &ps.find_syntax_for_file("test.cmake").unwrap().unwrap().name,
1061            "CMake"
1062        );
1063        assert_eq!(
1064            &ps.find_syntax_for_file("Rakefile").unwrap().unwrap().name,
1065            "Ruby"
1066        );
1067        assert!(&ps.find_syntax_by_first_line("derp derp hi lol").is_none());
1068        assert_eq!(
1069            &ps.find_syntax_by_path("Packages/Rust/Rust.sublime-syntax")
1070                .unwrap()
1071                .name,
1072            "Rust"
1073        );
1074        // println!("{:#?}", syntax);
1075        assert_eq!(syntax.scope, rails_scope);
1076        // unreachable!();
1077        let main_context = ps
1078            .get_context(&syntax.context_ids()["main"])
1079            .expect("#[cfg(test)]");
1080        let count = syntax_definition::context_iter(&ps, main_context).count();
1081        assert_eq!(count, 109);
1082    }
1083
1084    #[test]
1085    fn can_clone() {
1086        let cloned_syntax_set = {
1087            let mut builder = SyntaxSetBuilder::new();
1088            builder.add(syntax_a());
1089            builder.add(syntax_b());
1090
1091            let syntax_set_original = builder.build();
1092            #[allow(clippy::redundant_clone)] // We want to test .clone()
1093            syntax_set_original.clone()
1094            // Note: The original syntax set is dropped
1095        };
1096
1097        let syntax = cloned_syntax_set.find_syntax_by_extension("a").unwrap();
1098        let mut parse_state = ParseState::new(syntax);
1099        let ops = parse_state
1100            .parse_line("a go_b b", &cloned_syntax_set)
1101            .expect("#[cfg(test)]");
1102        let expected = (7, ScopeStackOp::Push(Scope::new("b").unwrap()));
1103        assert_ops_contain(&ops, &expected);
1104    }
1105
1106    #[test]
1107    fn can_list_added_syntaxes() {
1108        let mut builder = SyntaxSetBuilder::new();
1109        builder.add(syntax_a());
1110        builder.add(syntax_b());
1111        let syntaxes = builder.syntaxes();
1112
1113        assert_eq!(syntaxes.len(), 2);
1114        assert_eq!(syntaxes[0].name, "A");
1115        assert_eq!(syntaxes[1].name, "B");
1116    }
1117
1118    #[test]
1119    fn can_add_more_syntaxes_with_builder() {
1120        let syntax_set_original = {
1121            let mut builder = SyntaxSetBuilder::new();
1122            builder.add(syntax_a());
1123            builder.add(syntax_b());
1124            builder.build()
1125        };
1126
1127        let mut builder = syntax_set_original.into_builder();
1128
1129        let syntax_c = SyntaxDefinition::load_from_str(
1130            r#"
1131        name: C
1132        scope: source.c
1133        file_extensions: [c]
1134        contexts:
1135          main:
1136            - match: 'c'
1137              scope: c
1138            - match: 'go_a'
1139              push: scope:source.a#main
1140        "#,
1141            true,
1142            None,
1143        )
1144        .unwrap();
1145
1146        builder.add(syntax_c);
1147
1148        let syntax_set = builder.build();
1149
1150        let syntax = syntax_set.find_syntax_by_extension("c").unwrap();
1151        let mut parse_state = ParseState::new(syntax);
1152        let ops = parse_state
1153            .parse_line("c go_a a go_b b", &syntax_set)
1154            .expect("#[cfg(test)]");
1155        let expected = (14, ScopeStackOp::Push(Scope::new("b").unwrap()));
1156        assert_ops_contain(&ops, &expected);
1157    }
1158
1159    #[test]
1160    fn falls_back_to_plain_text_when_embedded_scope_is_missing() {
1161        test_plain_text_fallback(
1162            r#"
1163        name: Z
1164        scope: source.z
1165        file_extensions: [z]
1166        contexts:
1167          main:
1168            - match: 'z'
1169              scope: z
1170            - match: 'go_x'
1171              embed: scope:does.not.exist
1172              escape: 'leave_x'
1173        "#,
1174        );
1175    }
1176
1177    #[test]
1178    fn falls_back_to_plain_text_when_embedded_file_is_missing() {
1179        test_plain_text_fallback(
1180            r#"
1181        name: Z
1182        scope: source.z
1183        file_extensions: [z]
1184        contexts:
1185          main:
1186            - match: 'z'
1187              scope: z
1188            - match: 'go_x'
1189              embed: DoesNotExist.sublime-syntax
1190              escape: 'leave_x'
1191        "#,
1192        );
1193    }
1194
1195    fn test_plain_text_fallback(syntax_definition: &str) {
1196        let syntax = SyntaxDefinition::load_from_str(syntax_definition, true, None).unwrap();
1197
1198        let mut builder = SyntaxSetBuilder::new();
1199        builder.add_plain_text_syntax();
1200        builder.add(syntax);
1201        let syntax_set = builder.build();
1202
1203        let syntax = syntax_set.find_syntax_by_extension("z").unwrap();
1204        let mut parse_state = ParseState::new(syntax);
1205        let ops = parse_state
1206            .parse_line("z go_x x leave_x z", &syntax_set)
1207            .unwrap();
1208        let expected_ops = vec![
1209            (0, ScopeStackOp::Push(Scope::new("source.z").unwrap())),
1210            (0, ScopeStackOp::Push(Scope::new("z").unwrap())),
1211            (1, ScopeStackOp::Pop(1)),
1212            (6, ScopeStackOp::Push(Scope::new("text.plain").unwrap())),
1213            (9, ScopeStackOp::Pop(1)),
1214            (17, ScopeStackOp::Push(Scope::new("z").unwrap())),
1215            (18, ScopeStackOp::Pop(1)),
1216        ];
1217        assert_eq!(ops, expected_ops);
1218    }
1219
1220    #[test]
1221    fn can_find_unlinked_contexts() {
1222        let syntax_set = {
1223            let mut builder = SyntaxSetBuilder::new();
1224            builder.add(syntax_a());
1225            builder.add(syntax_b());
1226            builder.build()
1227        };
1228
1229        let unlinked_contexts = syntax_set.find_unlinked_contexts();
1230        assert_eq!(unlinked_contexts.len(), 0);
1231
1232        let syntax_set = {
1233            let mut builder = SyntaxSetBuilder::new();
1234            builder.add(syntax_a());
1235            builder.build()
1236        };
1237
1238        let unlinked_contexts: Vec<String> =
1239            syntax_set.find_unlinked_contexts().into_iter().collect();
1240        assert_eq!(unlinked_contexts.len(), 1);
1241        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 }");
1242    }
1243
1244    #[test]
1245    fn can_use_in_multiple_threads() {
1246        use rayon::prelude::*;
1247
1248        let syntax_set = {
1249            let mut builder = SyntaxSetBuilder::new();
1250            builder.add(syntax_a());
1251            builder.add(syntax_b());
1252            builder.build()
1253        };
1254
1255        let lines = vec!["a a a", "a go_b b", "go_b b", "go_b b  b"];
1256
1257        let results: Vec<Vec<(usize, ScopeStackOp)>> = lines
1258            .par_iter()
1259            .map(|line| {
1260                let syntax = syntax_set.find_syntax_by_extension("a").unwrap();
1261                let mut parse_state = ParseState::new(syntax);
1262                parse_state
1263                    .parse_line(line, &syntax_set)
1264                    .expect("#[cfg(test)]")
1265            })
1266            .collect();
1267
1268        assert_ops_contain(
1269            &results[0],
1270            &(4, ScopeStackOp::Push(Scope::new("a").unwrap())),
1271        );
1272        assert_ops_contain(
1273            &results[1],
1274            &(7, ScopeStackOp::Push(Scope::new("b").unwrap())),
1275        );
1276        assert_ops_contain(
1277            &results[2],
1278            &(5, ScopeStackOp::Push(Scope::new("b").unwrap())),
1279        );
1280        assert_ops_contain(
1281            &results[3],
1282            &(8, ScopeStackOp::Push(Scope::new("b").unwrap())),
1283        );
1284    }
1285
1286    #[test]
1287    fn is_sync() {
1288        check_sync::<SyntaxSet>();
1289    }
1290
1291    #[test]
1292    fn is_send() {
1293        check_send::<SyntaxSet>();
1294    }
1295
1296    #[test]
1297    fn can_override_syntaxes() {
1298        let syntax_set = {
1299            let mut builder = SyntaxSetBuilder::new();
1300            builder.add(syntax_a());
1301            builder.add(syntax_b());
1302
1303            let syntax_a2 = SyntaxDefinition::load_from_str(
1304                r#"
1305                name: A improved
1306                scope: source.a
1307                file_extensions: [a]
1308                first_line_match: syntax\s+a
1309                contexts:
1310                  main:
1311                    - match: a
1312                      scope: a2
1313                    - match: go_b
1314                      push: scope:source.b#main
1315                "#,
1316                true,
1317                None,
1318            )
1319            .unwrap();
1320
1321            builder.add(syntax_a2);
1322
1323            let syntax_c = SyntaxDefinition::load_from_str(
1324                r#"
1325                name: C
1326                scope: source.c
1327                file_extensions: [c]
1328                first_line_match: syntax\s+.*
1329                contexts:
1330                  main:
1331                    - match: c
1332                      scope: c
1333                    - match: go_a
1334                      push: scope:source.a#main
1335                "#,
1336                true,
1337                None,
1338            )
1339            .unwrap();
1340
1341            builder.add(syntax_c);
1342
1343            builder.build()
1344        };
1345
1346        let mut syntax = syntax_set.find_syntax_by_extension("a").unwrap();
1347        assert_eq!(syntax.name, "A improved");
1348        syntax = syntax_set
1349            .find_syntax_by_scope(Scope::new("source.a").unwrap())
1350            .unwrap();
1351        assert_eq!(syntax.name, "A improved");
1352        syntax = syntax_set.find_syntax_by_first_line("syntax a").unwrap();
1353        assert_eq!(syntax.name, "C");
1354
1355        let mut parse_state = ParseState::new(syntax);
1356        let ops = parse_state
1357            .parse_line("c go_a a", &syntax_set)
1358            .expect("msg");
1359        let expected = (7, ScopeStackOp::Push(Scope::new("a2").unwrap()));
1360        assert_ops_contain(&ops, &expected);
1361    }
1362
1363    #[test]
1364    fn can_parse_issue219() {
1365        // Go to builder and back after loading so that build() gets Direct references instead of
1366        // Named ones. The bug was that Direct references were not handled when marking as
1367        // "no prototype", so prototype contexts accidentally had the prototype set, which made
1368        // the parser loop forever.
1369        let syntax_set = SyntaxSet::load_defaults_newlines().into_builder().build();
1370        let syntax = syntax_set.find_syntax_by_extension("yaml").unwrap();
1371
1372        let mut parse_state = ParseState::new(syntax);
1373        let ops = parse_state
1374            .parse_line("# test\n", &syntax_set)
1375            .expect("#[cfg(test)]");
1376        let expected = (
1377            0,
1378            ScopeStackOp::Push(Scope::new("comment.line.number-sign.yaml").unwrap()),
1379        );
1380        assert_ops_contain(&ops, &expected);
1381    }
1382
1383    #[test]
1384    fn no_prototype_for_contexts_included_from_prototype() {
1385        let mut builder = SyntaxSetBuilder::new();
1386        let syntax = SyntaxDefinition::load_from_str(
1387            r#"
1388                name: Test Prototype
1389                scope: source.test
1390                file_extensions: [test]
1391                contexts:
1392                  prototype:
1393                    - include: included_from_prototype
1394                  main:
1395                    - match: main
1396                    - match: other
1397                      push: other
1398                  other:
1399                    - match: o
1400                  included_from_prototype:
1401                    - match: p
1402                      scope: p
1403                "#,
1404            true,
1405            None,
1406        )
1407        .unwrap();
1408        builder.add(syntax);
1409        let ss = builder.build();
1410
1411        // "main" and "other" should have context set, "prototype" and "included_from_prototype"
1412        // must not have a prototype set.
1413        assert_prototype_only_on(&["main", "other"], &ss, &ss.syntaxes()[0]);
1414
1415        // Building again should have the same result. The difference is that after the first
1416        // build(), the references have been replaced with Direct references, so the code needs to
1417        // handle that correctly.
1418        let rebuilt = ss.into_builder().build();
1419        assert_prototype_only_on(&["main", "other"], &rebuilt, &rebuilt.syntaxes()[0]);
1420    }
1421
1422    #[test]
1423    fn no_prototype_for_contexts_inline_in_prototype() {
1424        let mut builder = SyntaxSetBuilder::new();
1425        let syntax = SyntaxDefinition::load_from_str(
1426            r#"
1427                name: Test Prototype
1428                scope: source.test
1429                file_extensions: [test]
1430                contexts:
1431                  prototype:
1432                    - match: p
1433                      push:
1434                        - match: p2
1435                  main:
1436                    - match: main
1437                "#,
1438            true,
1439            None,
1440        )
1441        .unwrap();
1442        builder.add(syntax);
1443        let ss = builder.build();
1444
1445        assert_prototype_only_on(&["main"], &ss, &ss.syntaxes()[0]);
1446
1447        let rebuilt = ss.into_builder().build();
1448        assert_prototype_only_on(&["main"], &rebuilt, &rebuilt.syntaxes()[0]);
1449    }
1450
1451    #[test]
1452    fn find_syntax_set_from_line_with_bom() {
1453        // Regression test for #529
1454        let syntax_set = SyntaxSet::load_defaults_newlines();
1455        let syntax_ref = syntax_set
1456            .find_syntax_by_first_line("\u{feff}<?xml version=\"1.0\"?>")
1457            .unwrap();
1458        assert_eq!(syntax_ref.name, "XML");
1459    }
1460
1461    fn assert_ops_contain(ops: &[(usize, ScopeStackOp)], expected: &(usize, ScopeStackOp)) {
1462        assert!(
1463            ops.contains(expected),
1464            "expected operations to contain {:?}: {:?}",
1465            expected,
1466            ops
1467        );
1468    }
1469
1470    fn assert_prototype_only_on(
1471        expected: &[&str],
1472        syntax_set: &SyntaxSet,
1473        syntax: &SyntaxReference,
1474    ) {
1475        for (name, id) in syntax.context_ids() {
1476            if name == "__main" || name == "__start" {
1477                // Skip special contexts
1478                continue;
1479            }
1480            let context = syntax_set.get_context(id).expect("#[cfg(test)]");
1481            if expected.contains(&name.as_str()) {
1482                assert!(
1483                    context.prototype.is_some(),
1484                    "Expected context {} to have prototype",
1485                    name
1486                );
1487            } else {
1488                assert!(
1489                    context.prototype.is_none(),
1490                    "Expected context {} to not have prototype",
1491                    name
1492                );
1493            }
1494        }
1495    }
1496
1497    fn check_send<T: Send>() {}
1498
1499    fn check_sync<T: Sync>() {}
1500
1501    fn syntax_a() -> SyntaxDefinition {
1502        SyntaxDefinition::load_from_str(
1503            r#"
1504            name: A
1505            scope: source.a
1506            file_extensions: [a]
1507            contexts:
1508              main:
1509                - match: 'a'
1510                  scope: a
1511                - match: 'go_b'
1512                  push: scope:source.b#main
1513            "#,
1514            true,
1515            None,
1516        )
1517        .unwrap()
1518    }
1519
1520    fn syntax_b() -> SyntaxDefinition {
1521        SyntaxDefinition::load_from_str(
1522            r#"
1523            name: B
1524            scope: source.b
1525            file_extensions: [b]
1526            contexts:
1527              main:
1528                - match: 'b'
1529                  scope: b
1530            "#,
1531            true,
1532            None,
1533        )
1534        .unwrap()
1535    }
1536}