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#[derive(Debug, Serialize, Deserialize)]
33pub struct SyntaxSet {
34 syntaxes: Vec<SyntaxReference>,
35 path_syntaxes: Vec<(String, usize)>,
37
38 #[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 #[cfg(feature = "metadata")]
49 #[serde(skip, default)]
50 pub(crate) metadata: Metadata,
51}
52
53#[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 #[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#[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#[derive(Clone, Default)]
95pub struct SyntaxSetBuilder {
96 syntaxes: Vec<SyntaxDefinition>,
97 path_syntaxes: Vec<(String, usize)>,
98 warnings: Vec<String>,
99 lines_include_newline: bool,
103 #[cfg(feature = "metadata")]
104 raw_metadata: LoadMetadata,
105
106 #[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 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 #[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 pub fn warnings(&self) -> &[String] {
181 &self.warnings
182 }
183
184 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 #[cfg(feature = "metadata")]
196 pub fn metadata(&self) -> &Metadata {
197 &self.metadata
198 }
199
200 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 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 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); 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 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 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 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 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 pub fn add(&mut self, syntax: SyntaxDefinition) {
520 self.syntaxes.push(syntax);
521 }
522
523 pub fn syntaxes(&self) -> &[SyntaxDefinition] {
525 &self.syntaxes[..]
526 }
527
528 pub fn warnings(&self) -> &[String] {
533 &self.warnings
534 }
535
536 #[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 #[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 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 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 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 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(), };
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 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 while found_more_backref_includes {
745 found_more_backref_includes = false;
746 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 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 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 #[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 #[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 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 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 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 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 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 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 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 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 for (k, v) in parent_variables {
922 merged_variables.insert(k, v);
923 }
924
925 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 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 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 }
947 ContextMergeMode::Prepend => {
948 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 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 child.contexts.insert(ctx_name, parent_ctx);
989 }
990 }
991
992 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 syntax.hidden = true;
1046
1047 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 #[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 let normalized = extends_path.replace('\\', "/");
1087
1088 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 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 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 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 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 use super::syntax_definition::ContextReference::*;
1232 let linked_context_id = match *context_ref {
1233 Named(ref s) | Inline(ref s) => {
1234 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 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 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 assert_eq!(syntax.scope, rails_scope);
1456 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)] syntax_set_original.clone()
1474 };
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 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 assert_prototype_only_on(&["main", "other"], &ss, &ss.syntaxes()[0]);
1800
1801 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 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 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 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 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 assert!(syntax.context_ids().contains_key("string"));
1978 }
1979
1980 #[test]
1981 fn extends_overrides_context() {
1982 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 #[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 assert!(ss.find_syntax_by_name("UsingProto").is_some());
2581 }
2582
2583 #[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 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 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 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 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 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 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 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 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 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 let ops = state.parse_line("go\n", &ss).unwrap().ops;
2894 let v2_scopes = scopes_at_pos(&ops, 0);
2895
2896 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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}