use super::syntax_definition::*;
use super::scope::*;
#[cfg(feature = "metadata")]
use super::metadata::{LoadMetadata, Metadata, RawMetadataEntry};
#[cfg(feature = "yaml-load")]
use super::super::LoadingError;
use std::collections::{HashMap, HashSet, BTreeSet};
use std::path::Path;
#[cfg(feature = "yaml-load")]
use walkdir::WalkDir;
#[cfg(feature = "yaml-load")]
use std::io::Read;
use std::io::{self, BufRead, BufReader};
use std::fs::File;
use std::mem;
use super::regex::Regex;
use crate::parsing::syntax_definition::ContextId;
use once_cell::sync::OnceCell;
#[derive(Debug, Serialize, Deserialize)]
pub struct SyntaxSet {
syntaxes: Vec<SyntaxReference>,
contexts: Vec<Context>,
path_syntaxes: Vec<(String, usize)>,
#[serde(skip_serializing, skip_deserializing, default = "OnceCell::new")]
first_line_cache: OnceCell<FirstLineCache>,
#[cfg(feature = "metadata")]
#[serde(skip, default)]
pub(crate) metadata: Metadata,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct SyntaxReference {
pub name: String,
pub file_extensions: Vec<String>,
pub scope: Scope,
pub first_line_match: Option<String>,
pub hidden: bool,
#[serde(serialize_with = "ordered_map")]
pub variables: HashMap<String, String>,
#[serde(serialize_with = "ordered_map")]
pub(crate) context_ids: HashMap<String, ContextId>,
}
#[derive(Clone, Default)]
pub struct SyntaxSetBuilder {
syntaxes: Vec<SyntaxDefinition>,
path_syntaxes: Vec<(String, usize)>,
#[cfg(feature = "metadata")]
raw_metadata: LoadMetadata,
#[cfg(feature = "metadata")]
existing_metadata: Option<Metadata>,
}
#[cfg(feature = "yaml-load")]
fn load_syntax_file(p: &Path,
lines_include_newline: bool)
-> Result<SyntaxDefinition, LoadingError> {
let mut f = File::open(p)?;
let mut s = String::new();
f.read_to_string(&mut s)?;
SyntaxDefinition::load_from_str(
&s,
lines_include_newline,
p.file_stem().and_then(|x| x.to_str()),
)
.map_err(|e| LoadingError::ParseSyntax(e, Some(format!("{}", p.display()))))
}
impl Clone for SyntaxSet {
fn clone(&self) -> SyntaxSet {
SyntaxSet {
syntaxes: self.syntaxes.clone(),
contexts: self.contexts.clone(),
path_syntaxes: self.path_syntaxes.clone(),
first_line_cache: OnceCell::new(),
#[cfg(feature = "metadata")]
metadata: self.metadata.clone(),
}
}
}
impl Default for SyntaxSet {
fn default() -> Self {
SyntaxSet {
syntaxes: Vec::new(),
contexts: Vec::new(),
path_syntaxes: Vec::new(),
first_line_cache: OnceCell::new(),
#[cfg(feature = "metadata")]
metadata: Metadata::default(),
}
}
}
impl SyntaxSet {
pub fn new() -> SyntaxSet {
SyntaxSet::default()
}
#[cfg(feature = "yaml-load")]
pub fn load_from_folder<P: AsRef<Path>>(folder: P) -> Result<SyntaxSet, LoadingError> {
let mut builder = SyntaxSetBuilder::new();
builder.add_from_folder(folder, false)?;
Ok(builder.build())
}
pub fn syntaxes(&self) -> &[SyntaxReference] {
&self.syntaxes[..]
}
#[cfg(feature = "metadata")]
pub fn set_metadata(&mut self, metadata: Metadata) {
self.metadata = metadata;
}
#[cfg(feature = "metadata")]
pub fn metadata(&self) -> &Metadata {
&self.metadata
}
pub fn find_syntax_by_scope(&self, scope: Scope) -> Option<&SyntaxReference> {
self.syntaxes.iter().rev().find(|&s| s.scope == scope)
}
pub fn find_syntax_by_name<'a>(&'a self, name: &str) -> Option<&'a SyntaxReference> {
self.syntaxes.iter().rev().find(|&s| name == s.name)
}
pub fn find_syntax_by_extension<'a>(&'a self, extension: &str) -> Option<&'a SyntaxReference> {
self.syntaxes.iter().rev().find(|&s| s.file_extensions.iter().any(|e| e.eq_ignore_ascii_case(extension)))
}
pub fn find_syntax_by_token<'a>(&'a self, s: &str) -> Option<&'a SyntaxReference> {
{
let ext_res = self.find_syntax_by_extension(s);
if ext_res.is_some() {
return ext_res;
}
}
self.syntaxes.iter().rev().find(|&syntax| syntax.name.eq_ignore_ascii_case(s))
}
pub fn find_syntax_by_first_line<'a>(&'a self, s: &str) -> Option<&'a SyntaxReference> {
let cache = self.first_line_cache();
for &(ref reg, i) in cache.regexes.iter().rev() {
if reg.search(s, 0, s.len(), None) {
return Some(&self.syntaxes[i]);
}
}
None
}
pub fn find_syntax_by_path<'a>(&'a self, path: &str) -> Option<&'a SyntaxReference> {
let mut slash_path = "/".to_string();
slash_path.push_str(path);
self.path_syntaxes.iter().rev().find(|t| t.0.ends_with(&slash_path) || t.0 == path).map(|&(_,i)| &self.syntaxes[i])
}
pub fn find_syntax_for_file<P: AsRef<Path>>(&self,
path_obj: P)
-> io::Result<Option<&SyntaxReference>> {
let path: &Path = path_obj.as_ref();
let file_name = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
let extension = path.extension().and_then(|x| x.to_str()).unwrap_or("");
let ext_syntax = self.find_syntax_by_extension(file_name).or_else(
|| self.find_syntax_by_extension(extension));
let line_syntax = if ext_syntax.is_none() {
let mut line = String::new();
let f = File::open(path)?;
let mut line_reader = BufReader::new(&f);
line_reader.read_line(&mut line)?;
self.find_syntax_by_first_line(&line)
} else {
None
};
let syntax = ext_syntax.or(line_syntax);
Ok(syntax)
}
pub fn find_syntax_plain_text(&self) -> &SyntaxReference {
self.find_syntax_by_name("Plain Text")
.expect("All syntax sets ought to have a plain text syntax")
}
pub fn into_builder(self) -> SyntaxSetBuilder {
#[cfg(feature = "metadata")]
let SyntaxSet { syntaxes, contexts, path_syntaxes, metadata, .. } = self;
#[cfg(not(feature = "metadata"))]
let SyntaxSet { syntaxes, contexts, path_syntaxes, .. } = self;
let mut context_map = HashMap::with_capacity(contexts.len());
for (index, context) in contexts.into_iter().enumerate() {
context_map.insert(ContextId { index }, context);
}
let mut builder_syntaxes = Vec::with_capacity(syntaxes.len());
for syntax in syntaxes {
let SyntaxReference {
name,
file_extensions,
scope,
first_line_match,
hidden,
variables,
context_ids,
} = syntax;
let mut builder_contexts = HashMap::with_capacity(context_ids.len());
for (name, context_id) in context_ids {
if let Some(context) = context_map.remove(&context_id) {
builder_contexts.insert(name, context);
}
}
let syntax_definition = SyntaxDefinition {
name,
file_extensions,
scope,
first_line_match,
hidden,
variables,
contexts: builder_contexts,
};
builder_syntaxes.push(syntax_definition);
}
SyntaxSetBuilder {
syntaxes: builder_syntaxes,
path_syntaxes,
#[cfg(feature = "metadata")]
existing_metadata: Some(metadata),
#[cfg(feature = "metadata")]
raw_metadata: LoadMetadata::default(),
}
}
#[inline(always)]
pub(crate) fn get_context(&self, context_id: &ContextId) -> &Context {
&self.contexts[context_id.index]
}
fn first_line_cache(&self) -> &FirstLineCache {
self.first_line_cache
.get_or_init(|| FirstLineCache::new(self.syntaxes()))
}
pub fn find_unlinked_contexts(&self) -> BTreeSet<String> {
let SyntaxSet { syntaxes, contexts, .. } = self;
let mut context_map = HashMap::with_capacity(contexts.len());
for (index, context) in contexts.iter().enumerate() {
context_map.insert(ContextId { index }, context);
}
let mut unlinked_contexts = BTreeSet::new();
for syntax in syntaxes {
let SyntaxReference {
name,
scope,
context_ids,
..
} = syntax;
for context_id in context_ids.values() {
if let Some(context) = context_map.remove(context_id) {
Self::find_unlinked_contexts_in_context(name, scope, context, &mut unlinked_contexts);
}
}
}
unlinked_contexts
}
fn find_unlinked_contexts_in_context(
name: &str,
scope: &Scope,
context: &Context,
unlinked_contexts: &mut BTreeSet<String>,
) {
for pattern in context.patterns.iter() {
let maybe_refs_to_check = match pattern {
Pattern::Match(match_pat) => match &match_pat.operation {
MatchOperation::Push(context_refs) => Some(context_refs),
MatchOperation::Set(context_refs) => Some(context_refs),
_ => None,
},
_ => None,
};
for context_ref in maybe_refs_to_check.into_iter().flatten() {
match context_ref {
ContextReference::Direct(_) => {}
_ => {
unlinked_contexts.insert(format!(
"Syntax '{}' with scope '{}' has unresolved context reference {:?}",
name, scope, &context_ref
));
}
}
}
}
}
}
impl SyntaxSetBuilder {
pub fn new() -> SyntaxSetBuilder {
SyntaxSetBuilder::default()
}
pub fn add(&mut self, syntax: SyntaxDefinition) {
self.syntaxes.push(syntax);
}
pub fn syntaxes(&self) -> &[SyntaxDefinition] {
&self.syntaxes[..]
}
#[cfg(feature = "yaml-load")]
pub fn add_plain_text_syntax(&mut self) {
let s = "---\nname: Plain Text\nfile_extensions: [txt]\nscope: text.plain\ncontexts: \
{main: []}";
let syn = SyntaxDefinition::load_from_str(s, false, None).unwrap();
self.syntaxes.push(syn);
}
#[cfg(feature = "yaml-load")]
pub fn add_from_folder<P: AsRef<Path>>(
&mut self,
folder: P,
lines_include_newline: bool
) -> Result<(), LoadingError> {
for entry in WalkDir::new(folder).sort_by(|a, b| a.file_name().cmp(b.file_name())) {
let entry = entry.map_err(LoadingError::WalkDir)?;
if entry.path().extension().map_or(false, |e| e == "sublime-syntax") {
let syntax = load_syntax_file(entry.path(), lines_include_newline)?;
if let Some(path_str) = entry.path().to_str() {
let path = Path::new(path_str);
let path_parts: Vec<_> = path.iter().map(|c| c.to_str().unwrap()).collect();
self.path_syntaxes.push((path_parts.join("/").to_string(), self.syntaxes.len()));
}
self.syntaxes.push(syntax);
}
#[cfg(feature = "metadata")]
{
if entry.path().extension() == Some("tmPreferences".as_ref()) {
match RawMetadataEntry::load(entry.path()) {
Ok(meta) => self.raw_metadata.add_raw(meta),
Err(_err) => (),
}
}
}
}
Ok(())
}
pub fn build(self) -> SyntaxSet {
#[cfg(not(feature = "metadata"))]
let SyntaxSetBuilder { syntaxes: syntax_definitions, path_syntaxes } = self;
#[cfg(feature = "metadata")]
let SyntaxSetBuilder {
syntaxes: syntax_definitions,
path_syntaxes,
raw_metadata,
existing_metadata,
} = self;
let mut syntaxes = Vec::with_capacity(syntax_definitions.len());
let mut all_contexts = Vec::new();
for syntax_definition in syntax_definitions {
let SyntaxDefinition {
name,
file_extensions,
scope,
first_line_match,
hidden,
variables,
contexts,
} = syntax_definition;
let mut context_ids = HashMap::new();
let mut contexts: Vec<(String, Context)> = contexts.into_iter().collect();
contexts.sort_unstable_by(|(name_a, _), (name_b, _)| name_a.cmp(name_b));
for (name, context) in contexts {
let index = all_contexts.len();
context_ids.insert(name, ContextId { index });
all_contexts.push(context);
}
let syntax = SyntaxReference {
name,
file_extensions,
scope,
first_line_match,
hidden,
variables,
context_ids,
};
syntaxes.push(syntax);
}
let mut found_more_backref_includes = true;
for syntax in &syntaxes {
let mut no_prototype = HashSet::new();
let prototype = syntax.context_ids.get("prototype");
if let Some(prototype_id) = prototype {
Self::recursively_mark_no_prototype(syntax, prototype_id, &all_contexts, &mut no_prototype);
}
for context_id in syntax.context_ids.values() {
let mut context = &mut all_contexts[context_id.index];
if let Some(prototype_id) = prototype {
if context.meta_include_prototype && !no_prototype.contains(context_id) {
context.prototype = Some(*prototype_id);
}
}
Self::link_context(&mut context, syntax, &syntaxes);
if context.uses_backrefs {
found_more_backref_includes = true;
}
}
}
while found_more_backref_includes {
found_more_backref_includes = false;
for context_index in 0..all_contexts.len() {
let context = &all_contexts[context_index];
if !context.uses_backrefs && context.patterns.iter().any(|pattern| {
matches!(pattern, Pattern::Include(ContextReference::Direct(id)) if all_contexts[id.index].uses_backrefs)
}) {
let mut context = &mut all_contexts[context_index];
context.uses_backrefs = true;
found_more_backref_includes = true;
}
}
}
#[cfg(feature = "metadata")]
let metadata = match existing_metadata {
Some(existing) => existing.merged_with_raw(raw_metadata),
None => raw_metadata.into(),
};
SyntaxSet {
syntaxes,
contexts: all_contexts,
path_syntaxes,
first_line_cache: OnceCell::new(),
#[cfg(feature = "metadata")]
metadata,
}
}
fn recursively_mark_no_prototype(
syntax: &SyntaxReference,
context_id: &ContextId,
contexts: &[Context],
no_prototype: &mut HashSet<ContextId>,
) {
let first_time = no_prototype.insert(*context_id);
if !first_time {
return;
}
for pattern in &contexts[context_id.index].patterns {
match *pattern {
Pattern::Match(ref match_pat) => {
let maybe_context_refs = match match_pat.operation {
MatchOperation::Push(ref context_refs) |
MatchOperation::Set(ref context_refs) => Some(context_refs),
MatchOperation::Pop | MatchOperation::None => None,
};
if let Some(context_refs) = maybe_context_refs {
for context_ref in context_refs.iter() {
match context_ref {
ContextReference::Inline(ref s) | ContextReference::Named(ref s) => {
if let Some(i) = syntax.context_ids.get(s) {
Self::recursively_mark_no_prototype(syntax, i, contexts, no_prototype);
}
},
ContextReference::Direct(ref id) => {
Self::recursively_mark_no_prototype(syntax, id, contexts, no_prototype);
},
_ => (),
}
}
}
}
Pattern::Include(ref reference) => {
match reference {
ContextReference::Named(ref s) => {
if let Some(id) = syntax.context_ids.get(s) {
Self::recursively_mark_no_prototype(syntax, id, contexts, no_prototype);
}
},
ContextReference::Direct(ref id) => {
Self::recursively_mark_no_prototype(syntax, id, contexts, no_prototype);
},
_ => (),
}
}
}
}
}
fn link_context(context: &mut Context, syntax: &SyntaxReference, syntaxes: &[SyntaxReference]) {
for pattern in &mut context.patterns {
match *pattern {
Pattern::Match(ref mut match_pat) => Self::link_match_pat(match_pat, syntax, syntaxes),
Pattern::Include(ref mut context_ref) => Self::link_ref(context_ref, syntax, syntaxes),
}
}
}
fn link_ref(context_ref: &mut ContextReference, syntax: &SyntaxReference, syntaxes: &[SyntaxReference]) {
use super::syntax_definition::ContextReference::*;
let linked_context_id = match *context_ref {
Named(ref s) | Inline(ref s) => {
if s == "$top_level_main" {
syntax.context_ids.get("main")
} else {
syntax.context_ids.get(s)
}
}
ByScope { scope, ref sub_context } => {
let context_name = sub_context.as_ref().map_or("main", |x| &**x);
syntaxes
.iter()
.rev()
.find(|s| s.scope == scope)
.and_then(|s| s.context_ids.get(context_name))
}
File { ref name, ref sub_context } => {
let context_name = sub_context.as_ref().map_or("main", |x| &**x);
syntaxes
.iter()
.rev()
.find(|s| &s.name == name)
.and_then(|s| s.context_ids.get(context_name))
}
Direct(_) => None,
};
if let Some(context_id) = linked_context_id {
let mut new_ref = Direct(*context_id);
mem::swap(context_ref, &mut new_ref);
}
}
fn link_match_pat(match_pat: &mut MatchPattern, syntax: &SyntaxReference, syntaxes: &[SyntaxReference]) {
let maybe_context_refs = match match_pat.operation {
MatchOperation::Push(ref mut context_refs) |
MatchOperation::Set(ref mut context_refs) => Some(context_refs),
MatchOperation::Pop | MatchOperation::None => None,
};
if let Some(context_refs) = maybe_context_refs {
for context_ref in context_refs.iter_mut() {
Self::link_ref(context_ref, syntax, syntaxes);
}
}
if let Some(ref mut context_ref) = match_pat.with_prototype {
Self::link_ref(context_ref, syntax, syntaxes);
}
}
}
#[derive(Debug)]
struct FirstLineCache {
regexes: Vec<(Regex, usize)>,
}
impl FirstLineCache {
fn new(syntaxes: &[SyntaxReference]) -> FirstLineCache {
let mut regexes = Vec::new();
for (i, syntax) in syntaxes.iter().enumerate() {
if let Some(ref reg_str) = syntax.first_line_match {
let reg = Regex::new(reg_str.into());
regexes.push((reg, i));
}
}
FirstLineCache {
regexes,
}
}
}
#[cfg(feature = "yaml-load")]
#[cfg(test)]
mod tests {
use super::*;
use crate::parsing::{ParseState, Scope, syntax_definition};
use std::collections::HashMap;
#[test]
fn can_load() {
let mut builder = SyntaxSetBuilder::new();
builder.add_from_folder("testdata/Packages", false).unwrap();
let cmake_dummy_syntax = SyntaxDefinition {
name: "CMake".to_string(),
file_extensions: vec!["CMakeLists.txt".to_string(), "cmake".to_string()],
scope: Scope::new("source.cmake").unwrap(),
first_line_match: None,
hidden: false,
variables: HashMap::new(),
contexts: HashMap::new(),
};
builder.add(cmake_dummy_syntax);
builder.add_plain_text_syntax();
let ps = builder.build();
assert_eq!(&ps.find_syntax_by_first_line("#!/usr/bin/env node").unwrap().name,
"JavaScript");
let rails_scope = Scope::new("source.ruby.rails").unwrap();
let syntax = ps.find_syntax_by_name("Ruby on Rails").unwrap();
ps.find_syntax_plain_text();
assert_eq!(&ps.find_syntax_by_extension("rake").unwrap().name, "Ruby");
assert_eq!(&ps.find_syntax_by_extension("RAKE").unwrap().name, "Ruby");
assert_eq!(&ps.find_syntax_by_token("ruby").unwrap().name, "Ruby");
assert_eq!(&ps.find_syntax_by_first_line("lol -*- Mode: C -*- such line").unwrap().name,
"C");
assert_eq!(&ps.find_syntax_for_file("testdata/parser.rs").unwrap().unwrap().name,
"Rust");
assert_eq!(&ps.find_syntax_for_file("testdata/test_first_line.test")
.expect("Error finding syntax for file")
.expect("No syntax found for file")
.name,
"Ruby");
assert_eq!(&ps.find_syntax_for_file(".bashrc").unwrap().unwrap().name,
"Bourne Again Shell (bash)");
assert_eq!(&ps.find_syntax_for_file("CMakeLists.txt").unwrap().unwrap().name,
"CMake");
assert_eq!(&ps.find_syntax_for_file("test.cmake").unwrap().unwrap().name,
"CMake");
assert_eq!(&ps.find_syntax_for_file("Rakefile").unwrap().unwrap().name, "Ruby");
assert!(&ps.find_syntax_by_first_line("derp derp hi lol").is_none());
assert_eq!(&ps.find_syntax_by_path("Packages/Rust/Rust.sublime-syntax").unwrap().name,
"Rust");
assert_eq!(syntax.scope, rails_scope);
let main_context = ps.get_context(&syntax.context_ids["main"]);
let count = syntax_definition::context_iter(&ps, main_context).count();
assert_eq!(count, 109);
}
#[test]
fn can_clone() {
let cloned_syntax_set = {
let mut builder = SyntaxSetBuilder::new();
builder.add(syntax_a());
builder.add(syntax_b());
let syntax_set_original = builder.build();
#[allow(clippy::redundant_clone)] syntax_set_original.clone()
};
let syntax = cloned_syntax_set.find_syntax_by_extension("a").unwrap();
let mut parse_state = ParseState::new(syntax);
let ops = parse_state.parse_line("a go_b b", &cloned_syntax_set).unwrap();
let expected = (7, ScopeStackOp::Push(Scope::new("b").unwrap()));
assert_ops_contain(&ops, &expected);
}
#[test]
fn can_list_added_syntaxes() {
let mut builder = SyntaxSetBuilder::new();
builder.add(syntax_a());
builder.add(syntax_b());
let syntaxes = builder.syntaxes();
assert_eq!(syntaxes.len(), 2);
assert_eq!(syntaxes[0].name, "A");
assert_eq!(syntaxes[1].name, "B");
}
#[test]
fn can_add_more_syntaxes_with_builder() {
let syntax_set_original = {
let mut builder = SyntaxSetBuilder::new();
builder.add(syntax_a());
builder.add(syntax_b());
builder.build()
};
let mut builder = syntax_set_original.into_builder();
let syntax_c = SyntaxDefinition::load_from_str(r#"
name: C
scope: source.c
file_extensions: [c]
contexts:
main:
- match: 'c'
scope: c
- match: 'go_a'
push: scope:source.a#main
"#, true, None).unwrap();
builder.add(syntax_c);
let syntax_set = builder.build();
let syntax = syntax_set.find_syntax_by_extension("c").unwrap();
let mut parse_state = ParseState::new(syntax);
let ops = parse_state.parse_line("c go_a a go_b b", &syntax_set).unwrap();
let expected = (14, ScopeStackOp::Push(Scope::new("b").unwrap()));
assert_ops_contain(&ops, &expected);
}
#[test]
fn can_find_unlinked_contexts() {
let syntax_set = {
let mut builder = SyntaxSetBuilder::new();
builder.add(syntax_a());
builder.add(syntax_b());
builder.build()
};
let unlinked_contexts = syntax_set.find_unlinked_contexts();
assert_eq!(unlinked_contexts.len(), 0);
let syntax_set = {
let mut builder = SyntaxSetBuilder::new();
builder.add(syntax_a());
builder.build()
};
let unlinked_contexts : Vec<String> = syntax_set.find_unlinked_contexts().into_iter().collect();
assert_eq!(unlinked_contexts.len(), 1);
assert_eq!(unlinked_contexts[0], "Syntax 'A' with scope 'source.a' has unresolved context reference ByScope { scope: <source.b>, sub_context: Some(\"main\") }");
}
#[test]
fn can_use_in_multiple_threads() {
use rayon::prelude::*;
let syntax_set = {
let mut builder = SyntaxSetBuilder::new();
builder.add(syntax_a());
builder.add(syntax_b());
builder.build()
};
let lines = vec![
"a a a",
"a go_b b",
"go_b b",
"go_b b b",
];
let results: Vec<Vec<(usize, ScopeStackOp)>> = lines
.par_iter()
.map(|line| {
let syntax = syntax_set.find_syntax_by_extension("a").unwrap();
let mut parse_state = ParseState::new(syntax);
parse_state.parse_line(line, &syntax_set).unwrap()
})
.collect();
assert_ops_contain(&results[0], &(4, ScopeStackOp::Push(Scope::new("a").unwrap())));
assert_ops_contain(&results[1], &(7, ScopeStackOp::Push(Scope::new("b").unwrap())));
assert_ops_contain(&results[2], &(5, ScopeStackOp::Push(Scope::new("b").unwrap())));
assert_ops_contain(&results[3], &(8, ScopeStackOp::Push(Scope::new("b").unwrap())));
}
#[test]
fn is_sync() {
check_sync::<SyntaxSet>();
}
#[test]
fn is_send() {
check_send::<SyntaxSet>();
}
#[test]
fn can_override_syntaxes() {
let syntax_set = {
let mut builder = SyntaxSetBuilder::new();
builder.add(syntax_a());
builder.add(syntax_b());
let syntax_a2 = SyntaxDefinition::load_from_str(r#"
name: A improved
scope: source.a
file_extensions: [a]
first_line_match: syntax\s+a
contexts:
main:
- match: a
scope: a2
- match: go_b
push: scope:source.b#main
"#, true, None).unwrap();
builder.add(syntax_a2);
let syntax_c = SyntaxDefinition::load_from_str(r#"
name: C
scope: source.c
file_extensions: [c]
first_line_match: syntax\s+.*
contexts:
main:
- match: c
scope: c
- match: go_a
push: scope:source.a#main
"#, true, None).unwrap();
builder.add(syntax_c);
builder.build()
};
let mut syntax = syntax_set.find_syntax_by_extension("a").unwrap();
assert_eq!(syntax.name, "A improved");
syntax = syntax_set.find_syntax_by_scope(Scope::new("source.a").unwrap()).unwrap();
assert_eq!(syntax.name, "A improved");
syntax = syntax_set.find_syntax_by_first_line("syntax a").unwrap();
assert_eq!(syntax.name, "C");
let mut parse_state = ParseState::new(syntax);
let ops = parse_state.parse_line("c go_a a", &syntax_set).unwrap();
let expected = (7, ScopeStackOp::Push(Scope::new("a2").unwrap()));
assert_ops_contain(&ops, &expected);
}
#[test]
fn can_parse_issue219() {
let syntax_set = SyntaxSet::load_defaults_newlines().into_builder().build();
let syntax = syntax_set.find_syntax_by_extension("yaml").unwrap();
let mut parse_state = ParseState::new(syntax);
let ops = parse_state.parse_line("# test\n", &syntax_set).unwrap();
let expected = (0, ScopeStackOp::Push(Scope::new("comment.line.number-sign.yaml").unwrap()));
assert_ops_contain(&ops, &expected);
}
#[test]
fn no_prototype_for_contexts_included_from_prototype() {
let mut builder = SyntaxSetBuilder::new();
let syntax = SyntaxDefinition::load_from_str(r#"
name: Test Prototype
scope: source.test
file_extensions: [test]
contexts:
prototype:
- include: included_from_prototype
main:
- match: main
- match: other
push: other
other:
- match: o
included_from_prototype:
- match: p
scope: p
"#, true, None).unwrap();
builder.add(syntax);
let ss = builder.build();
assert_prototype_only_on(&["main", "other"], &ss, &ss.syntaxes()[0]);
let rebuilt = ss.into_builder().build();
assert_prototype_only_on(&["main", "other"], &rebuilt, &rebuilt.syntaxes()[0]);
}
#[test]
fn no_prototype_for_contexts_inline_in_prototype() {
let mut builder = SyntaxSetBuilder::new();
let syntax = SyntaxDefinition::load_from_str(r#"
name: Test Prototype
scope: source.test
file_extensions: [test]
contexts:
prototype:
- match: p
push:
- match: p2
main:
- match: main
"#, true, None).unwrap();
builder.add(syntax);
let ss = builder.build();
assert_prototype_only_on(&["main"], &ss, &ss.syntaxes()[0]);
let rebuilt = ss.into_builder().build();
assert_prototype_only_on(&["main"], &rebuilt, &rebuilt.syntaxes()[0]);
}
fn assert_ops_contain(
ops: &[(usize, ScopeStackOp)],
expected: &(usize, ScopeStackOp)
) {
assert!(ops.contains(expected),
"expected operations to contain {:?}: {:?}", expected, ops);
}
fn assert_prototype_only_on(expected: &[&str], syntax_set: &SyntaxSet, syntax: &SyntaxReference) {
for (name, id) in &syntax.context_ids {
if name == "__main" || name == "__start" {
continue;
}
let context = syntax_set.get_context(id);
if expected.contains(&name.as_str()) {
assert!(context.prototype.is_some(), "Expected context {} to have prototype", name);
} else {
assert!(context.prototype.is_none(), "Expected context {} to not have prototype", name);
}
}
}
fn check_send<T: Send>() {}
fn check_sync<T: Sync>() {}
fn syntax_a() -> SyntaxDefinition {
SyntaxDefinition::load_from_str(
r#"
name: A
scope: source.a
file_extensions: [a]
contexts:
main:
- match: 'a'
scope: a
- match: 'go_b'
push: scope:source.b#main
"#,
true,
None,
).unwrap()
}
fn syntax_b() -> SyntaxDefinition {
SyntaxDefinition::load_from_str(
r#"
name: B
scope: source.b
file_extensions: [b]
contexts:
main:
- match: 'b'
scope: b
"#,
true,
None,
).unwrap()
}
}