Skip to content

Commit 8a21213

Browse files
committed
feat: partial support for v2 syntax [#323]
This adds support for the `extends` keyword. The implementation is very hacky right now: - We now require an optional `base_syntax` when parsing a syntax, if the `base_syntax` is not present and we find an `extends` keyword at the top level, we error out reporting which syntax file we expected. - SyntaxSet collects this error and resolves the syntaxes in a loop until no more syntaxes can be resolved. - We don't handle multiple-inheritance (`extends` might be a list). - We don't re-evaluate the `contexts` as defined in the spec (you can override variables and have that affect the context). - We only handle `extends` for syntaxes added through `SyntaxSet::load_from_folder`.
1 parent b62ffed commit 8a21213

File tree

4 files changed

+191
-12
lines changed

4 files changed

+191
-12
lines changed

src/parsing/syntax_definition.rs

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ pub struct Context {
5151
pub meta_scope: Vec<Scope>,
5252
pub meta_content_scope: Vec<Scope>,
5353
/// This being set false in the syntax file implies this field being set false,
54-
/// but it can also be set falso for contexts that don't include the prototype for other reasons
54+
/// but it can also be set false for contexts that don't include the prototype for other reasons
5555
pub meta_include_prototype: bool,
5656
pub clear_scopes: Option<ClearAmount>,
5757
/// This is filled in by the linker at link time
@@ -75,6 +75,27 @@ impl Context {
7575
prototype: None,
7676
}
7777
}
78+
79+
pub(crate) fn extend(&mut self, other: Context) {
80+
let Context {
81+
meta_scope,
82+
meta_content_scope,
83+
meta_include_prototype,
84+
clear_scopes,
85+
prototype,
86+
uses_backrefs,
87+
patterns,
88+
} = other;
89+
self.meta_scope.extend(meta_scope);
90+
self.meta_content_scope.extend(meta_content_scope);
91+
self.meta_include_prototype = meta_include_prototype;
92+
self.clear_scopes = clear_scopes;
93+
if self.prototype.is_none() || prototype.is_some() {
94+
self.prototype = prototype;
95+
}
96+
self.uses_backrefs |= uses_backrefs;
97+
self.patterns.extend(patterns);
98+
}
7899
}
79100

80101
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]

src/parsing/syntax_set.rs

Lines changed: 87 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ use std::fs::File;
1313
use std::io::{self, BufRead, BufReader};
1414
use std::mem;
1515
use std::path::Path;
16+
use std::path::PathBuf;
1617

1718
use super::regex::Regex;
1819
use crate::parsing::syntax_definition::ContextId;
@@ -83,6 +84,7 @@ pub(crate) struct LazyContexts {
8384
pub struct SyntaxSetBuilder {
8485
syntaxes: Vec<SyntaxDefinition>,
8586
path_syntaxes: Vec<(String, usize)>,
87+
extends_syntaxes: Vec<(PathBuf, String)>,
8688
#[cfg(feature = "metadata")]
8789
raw_metadata: LoadMetadata,
8890

@@ -108,6 +110,23 @@ fn load_syntax_file(
108110
.map_err(|e| LoadingError::ParseSyntax(e, format!("{}", p.display())))
109111
}
110112

113+
#[cfg(feature = "yaml-load")]
114+
fn load_syntax_file_with_extends(
115+
p: &Path,
116+
base_syntax: &SyntaxDefinition,
117+
lines_include_newline: bool,
118+
) -> Result<SyntaxDefinition, LoadingError> {
119+
let s = std::fs::read_to_string(p)?;
120+
121+
SyntaxDefinition::load_from_str_extended(
122+
&s,
123+
Some(base_syntax),
124+
lines_include_newline,
125+
p.file_stem().and_then(|x| x.to_str()),
126+
)
127+
.map_err(|e| LoadingError::ParseSyntax(e, format!("{}", p.display())))
128+
}
129+
111130
impl Clone for SyntaxSet {
112131
fn clone(&self) -> SyntaxSet {
113132
SyntaxSet {
@@ -375,6 +394,7 @@ impl SyntaxSet {
375394
SyntaxSetBuilder {
376395
syntaxes: builder_syntaxes,
377396
path_syntaxes,
397+
extends_syntaxes: Vec::new(),
378398
#[cfg(feature = "metadata")]
379399
existing_metadata: Some(metadata),
380400
#[cfg(feature = "metadata")]
@@ -516,6 +536,8 @@ impl SyntaxSetBuilder {
516536
folder: P,
517537
lines_include_newline: bool,
518538
) -> Result<(), LoadingError> {
539+
use super::ParseSyntaxError;
540+
519541
for entry in crate::utils::walk_dir(folder).sort_by(|a, b| a.file_name().cmp(b.file_name()))
520542
{
521543
let entry = entry.map_err(LoadingError::WalkDir)?;
@@ -524,7 +546,27 @@ impl SyntaxSetBuilder {
524546
.extension()
525547
.map_or(false, |e| e == "sublime-syntax")
526548
{
527-
let syntax = load_syntax_file(entry.path(), lines_include_newline)?;
549+
let syntax = match load_syntax_file(entry.path(), lines_include_newline) {
550+
Ok(syntax) => syntax,
551+
// We are extending another syntax, look it up in the set first
552+
Err(LoadingError::ParseSyntax(
553+
ParseSyntaxError::ExtendsNotFound { name, extends },
554+
_,
555+
)) => {
556+
if let Some(ix) = self
557+
.path_syntaxes
558+
.iter()
559+
.find(|(s, _)| s.ends_with(extends.as_str()))
560+
.map(|(_, ix)| *ix)
561+
{
562+
todo!("lookup {ix} and pass to {name}");
563+
}
564+
self.extends_syntaxes
565+
.push((entry.path().to_path_buf(), extends));
566+
continue;
567+
}
568+
Err(err) => return Err(err),
569+
};
528570
if let Some(path_str) = entry.path().to_str() {
529571
// Split the path up and rejoin with slashes so that syntaxes loaded on Windows
530572
// can still be loaded the same way.
@@ -550,6 +592,45 @@ impl SyntaxSetBuilder {
550592
Ok(())
551593
}
552594

595+
fn resolve_extends(&mut self) {
596+
let mut prev_len = usize::MAX;
597+
// Loop while syntaxes are being resolved
598+
while !self.extends_syntaxes.is_empty() && prev_len > self.extends_syntaxes.len() {
599+
prev_len = self.extends_syntaxes.len();
600+
// Split borrows to make the borrow cheker happy
601+
let syntaxes = &mut self.syntaxes;
602+
let paths = &mut self.path_syntaxes;
603+
// Resolve syntaxes
604+
self.extends_syntaxes.retain(|(path, extends)| {
605+
let Some(ix) = paths
606+
.iter()
607+
.find(|(s, _)| s.ends_with(extends.as_str()))
608+
.map(|(_, ix)| *ix)
609+
else {
610+
return true;
611+
};
612+
let base_syntax = &syntaxes[ix];
613+
// FIXME: don't unwrap
614+
let syntax = load_syntax_file_with_extends(path, base_syntax, false).unwrap();
615+
if let Some(path_str) = path.to_str() {
616+
// Split the path up and rejoin with slashes so that syntaxes loaded on Windows
617+
// can still be loaded the same way.
618+
let path = Path::new(path_str);
619+
let path_parts: Vec<_> = path.iter().map(|c| c.to_str().unwrap()).collect();
620+
paths.push((path_parts.join("/").to_string(), syntaxes.len()));
621+
}
622+
syntaxes.push(syntax);
623+
false
624+
});
625+
}
626+
627+
if !self.extends_syntaxes.is_empty() {
628+
dbg!(&self.path_syntaxes);
629+
dbg!(&self.extends_syntaxes);
630+
todo!("warn, unresolved syntaxes");
631+
}
632+
}
633+
553634
/// Build a [`SyntaxSet`] from the syntaxes that have been added to this
554635
/// builder.
555636
///
@@ -571,16 +652,20 @@ impl SyntaxSetBuilder {
571652
/// directly load the [`SyntaxSet`].
572653
///
573654
/// [`SyntaxSet`]: struct.SyntaxSet.html
574-
pub fn build(self) -> SyntaxSet {
655+
pub fn build(mut self) -> SyntaxSet {
656+
self.resolve_extends();
657+
575658
#[cfg(not(feature = "metadata"))]
576659
let SyntaxSetBuilder {
577660
syntaxes: syntax_definitions,
578661
path_syntaxes,
662+
extends_syntaxes: _,
579663
} = self;
580664
#[cfg(feature = "metadata")]
581665
let SyntaxSetBuilder {
582666
syntaxes: syntax_definitions,
583667
path_syntaxes,
668+
extends_syntaxes: _,
584669
raw_metadata,
585670
existing_metadata,
586671
} = self;

src/parsing/yaml_load.rs

Lines changed: 81 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@ pub enum ParseSyntaxError {
3232
/// Syntaxes must have a context named "main"
3333
#[error("Context 'main' is missing")]
3434
MainMissing,
35+
/// This syntax extends another syntax which is not available
36+
#[error("Syntax for {name} extends {extends}, but {extends} could not be found")]
37+
ExtendsNotFound { name: String, extends: String },
3538
/// Some part of the YAML file is the wrong type (e.g a string but should be a list)
3639
/// Sorry this doesn't give you any way to narrow down where this is.
3740
/// Maybe use Sublime Text to figure it out.
@@ -86,6 +89,15 @@ impl SyntaxDefinition {
8689
s: &str,
8790
lines_include_newline: bool,
8891
fallback_name: Option<&str>,
92+
) -> Result<SyntaxDefinition, ParseSyntaxError> {
93+
SyntaxDefinition::load_from_str_extended(s, None, lines_include_newline, fallback_name)
94+
}
95+
96+
pub(crate) fn load_from_str_extended(
97+
s: &str,
98+
extends: Option<&SyntaxDefinition>,
99+
lines_include_newline: bool,
100+
fallback_name: Option<&str>,
89101
) -> Result<SyntaxDefinition, ParseSyntaxError> {
90102
let docs = match YamlLoader::load_from_str(s) {
91103
Ok(x) => x,
@@ -98,6 +110,7 @@ impl SyntaxDefinition {
98110
let mut scope_repo = SCOPE_REPO.lock().unwrap();
99111
SyntaxDefinition::parse_top_level(
100112
doc,
113+
extends,
101114
scope_repo.deref_mut(),
102115
lines_include_newline,
103116
fallback_name,
@@ -106,20 +119,40 @@ impl SyntaxDefinition {
106119

107120
fn parse_top_level(
108121
doc: &Yaml,
122+
extends: Option<&SyntaxDefinition>,
109123
scope_repo: &mut ScopeRepository,
110124
lines_include_newline: bool,
111125
fallback_name: Option<&str>,
112126
) -> Result<SyntaxDefinition, ParseSyntaxError> {
113127
let h = doc.as_hash().ok_or(ParseSyntaxError::TypeMismatch)?;
114128

115-
let mut variables = HashMap::new();
129+
// Get variables from cloned syntax, will be overritten if the same is present as detailed
130+
// in the spec
131+
let mut variables = extends
132+
.map(|syntax| syntax.variables.clone())
133+
.unwrap_or_default();
116134
if let Ok(map) = get_key(h, "variables", |x| x.as_hash()) {
117135
for (key, value) in map.iter() {
118136
if let (Some(key_str), Some(val_str)) = (key.as_str(), value.as_str()) {
119137
variables.insert(key_str.to_owned(), val_str.to_owned());
120138
}
121139
}
122140
}
141+
142+
let name = get_key(h, "name", |x| x.as_str())
143+
.unwrap_or_else(|_| fallback_name.unwrap_or("Unnamed"))
144+
.to_owned();
145+
// FIXME: extends is allowed to be a list also
146+
let extends = match (get_key(h, "extends", |x| x.as_str()), extends) {
147+
(Ok(base_syntax), None) => {
148+
return Err(ParseSyntaxError::ExtendsNotFound {
149+
name,
150+
extends: base_syntax.to_string(),
151+
})
152+
}
153+
(Ok(_), Some(base_syntax)) => Some(base_syntax),
154+
(Err(_), _) => None,
155+
};
123156
let contexts_hash = get_key(h, "contexts", |x| x.as_hash())?;
124157
let top_level_scope = scope_repo
125158
.build(get_key(h, "scope", |x| x.as_str())?)
@@ -132,7 +165,11 @@ impl SyntaxDefinition {
132165
lines_include_newline,
133166
};
134167

135-
let mut contexts = SyntaxDefinition::parse_contexts(contexts_hash, &mut state)?;
168+
let mut contexts = SyntaxDefinition::parse_contexts(
169+
contexts_hash,
170+
extends.map(|syntax| &syntax.contexts),
171+
&mut state,
172+
)?;
136173
if !contexts.contains_key("main") {
137174
return Err(ParseSyntaxError::MainMissing);
138175
}
@@ -147,9 +184,7 @@ impl SyntaxDefinition {
147184
}
148185

149186
let defn = SyntaxDefinition {
150-
name: get_key(h, "name", |x| x.as_str())
151-
.unwrap_or_else(|_| fallback_name.unwrap_or("Unnamed"))
152-
.to_owned(),
187+
name,
153188
scope: top_level_scope,
154189
file_extensions,
155190
// TODO maybe cache a compiled version of this Regex
@@ -166,9 +201,11 @@ impl SyntaxDefinition {
166201

167202
fn parse_contexts(
168203
map: &Hash,
204+
extends: Option<&HashMap<String, Context>>,
169205
state: &mut ParserState<'_>,
170206
) -> Result<HashMap<String, Context>, ParseSyntaxError> {
171-
let mut contexts = HashMap::new();
207+
// FIXME: contexts need to be re-evaluated with the new values of the variables
208+
let mut contexts = extends.cloned().unwrap_or_default();
172209
for (key, value) in map.iter() {
173210
if let (Some(name), Some(val_vec)) = (key.as_str(), value.as_vec()) {
174211
let is_prototype = name == "prototype";
@@ -194,13 +231,31 @@ impl SyntaxDefinition {
194231
is_prototype: bool,
195232
namer: &mut ContextNamer,
196233
) -> Result<String, ParseSyntaxError> {
234+
enum InsertMode {
235+
Replace,
236+
Prepend,
237+
Append,
238+
}
197239
let mut context = Context::new(!is_prototype);
198240
let name = namer.next();
241+
let mut insert = InsertMode::Replace;
199242

200243
for y in vec.iter() {
201244
let map = y.as_hash().ok_or(ParseSyntaxError::TypeMismatch)?;
202245

203246
let mut is_special = false;
247+
if let Ok(x) = get_key(map, "meta_prepend", |x| x.as_bool()) {
248+
if x {
249+
insert = InsertMode::Prepend;
250+
}
251+
is_special = true;
252+
}
253+
if let Ok(x) = get_key(map, "meta_append", |x| x.as_bool()) {
254+
if x {
255+
insert = InsertMode::Append;
256+
}
257+
is_special = true;
258+
}
204259
if let Ok(x) = get_key(map, "meta_scope", |x| x.as_str()) {
205260
context.meta_scope = str_to_scopes(x, state.scope_repo)?;
206261
is_special = true;
@@ -237,7 +292,26 @@ impl SyntaxDefinition {
237292
}
238293
}
239294

240-
contexts.insert(name.clone(), context);
295+
match insert {
296+
InsertMode::Replace => {
297+
contexts.insert(name.clone(), context);
298+
}
299+
InsertMode::Append => {
300+
contexts
301+
.entry(name.clone())
302+
.and_modify(|ctx| ctx.extend(context.clone()))
303+
.or_insert(context);
304+
}
305+
InsertMode::Prepend => {
306+
contexts
307+
.entry(name.clone())
308+
.and_modify(|ctx| {
309+
context.extend(ctx.clone());
310+
*ctx = context.clone();
311+
})
312+
.or_insert(context);
313+
}
314+
}
241315
Ok(name)
242316
}
243317

@@ -887,7 +961,6 @@ impl<'a> Parser<'a> {
887961
#[cfg(test)]
888962
mod tests {
889963
use super::*;
890-
use crate::parsing::syntax_definition::*;
891964
use crate::parsing::Scope;
892965

893966
#[test]

0 commit comments

Comments
 (0)