1use std::collections::BTreeMap;
5use std::fs::File;
6use std::io::BufReader;
7use std::path::PathBuf;
8use std::str::FromStr;
9
10use serde::de::{Deserialize, Deserializer};
11use serde::ser::{Serialize, Serializer};
12use serde_derive::{Deserialize, Serialize};
13
14use super::super::highlighting::settings::*;
15use super::super::highlighting::ScopeSelectors;
16use super::super::LoadingError;
17use super::regex::Regex;
18use super::scope::{MatchPower, Scope};
19
20type Dict = serde_json::Map<String, Settings>;
21
22type SelectorString = String;
26
27#[derive(Debug, Default, Clone, Serialize, Deserialize)]
29pub struct Metadata {
30 pub scoped_metadata: Vec<MetadataSet>,
31}
32
33#[derive(Debug, Clone, PartialEq)]
37pub struct MetadataSet {
38 pub selector_string: SelectorString,
42 pub selector: ScopeSelectors,
44 pub items: MetadataItems,
46}
47
48#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
51#[serde(rename_all = "camelCase")]
52#[non_exhaustive]
53pub struct MetadataItems {
54 pub increase_indent_pattern: Option<Regex>,
55 pub decrease_indent_pattern: Option<Regex>,
56 pub bracket_indent_next_line_pattern: Option<Regex>,
57 pub disable_indent_next_line_pattern: Option<Regex>,
58 pub unindented_line_pattern: Option<Regex>,
59 pub indent_parens: Option<bool>,
60 #[serde(default)]
61 pub shell_variables: BTreeMap<String, String>,
62 pub line_comment: Option<String>,
66 pub block_comment: Option<(String, String)>,
69}
70
71#[derive(Debug, Clone, Serialize, Deserialize)]
74pub(crate) struct RawMetadataEntry {
75 path: PathBuf,
76 scope: SelectorString,
77 settings: Dict,
78}
79
80#[derive(Debug, Default, Clone)]
82pub(crate) struct LoadMetadata {
83 loaded: Vec<RawMetadataEntry>,
84}
85
86const KEYS_WE_USE: &[&str] = &[
89 "increaseIndentPattern",
90 "decreaseIndentPattern",
91 "bracketIndentNextLinePattern",
92 "disableIndentNextLinePattern",
93 "unIndentedLinePattern",
94 "indentParens",
95 "shellVariables",
96];
97
98impl LoadMetadata {
99 pub fn add_raw(&mut self, raw: RawMetadataEntry) {
107 self.loaded.push(raw);
108 }
109
110 #[cfg(test)]
114 pub(crate) fn quick_load(path: &str) -> Result<MetadataSet, LoadingError> {
115 let mut loaded = Self::default();
116 let raw = RawMetadataEntry::load(path)?;
117 loaded.add_raw(raw);
118 let mut metadata: Metadata = loaded.into();
119 Ok(metadata.scoped_metadata.pop().unwrap())
120 }
121}
122
123impl From<LoadMetadata> for Metadata {
124 fn from(src: LoadMetadata) -> Metadata {
125 let LoadMetadata { mut loaded } = src;
126 loaded.sort_unstable_by(|a, b| a.path.cmp(&b.path));
127
128 let mut scoped_metadata: BTreeMap<SelectorString, Dict> = BTreeMap::new();
129
130 for RawMetadataEntry {
131 scope,
132 settings,
133 path,
134 } in loaded
135 {
136 let scoped_settings = scoped_metadata.entry(scope.clone()).or_insert_with(|| {
137 let mut d = Dict::new();
138 d.insert(
139 "source_file_path".to_string(),
140 path.to_string_lossy().into(),
141 );
142 d
143 });
144
145 for (key, value) in settings {
146 if !KEYS_WE_USE.contains(&key.as_str()) {
147 continue;
148 }
149
150 if key.as_str() == "shellVariables" {
151 append_vars(scoped_settings, value, &scope);
152 } else {
153 scoped_settings.insert(key, value);
154 }
155 }
156 }
157
158 let scoped_metadata = scoped_metadata
159 .into_iter()
160 .flat_map(|r| MetadataSet::from_raw(r).map_err(|e| eprintln!("{}", e)))
161 .collect();
162 Metadata { scoped_metadata }
163 }
164}
165
166fn append_vars(obj: &mut Dict, vars: Settings, scope: &str) {
167 #[derive(Deserialize)]
168 struct KeyPair {
169 name: String,
170 value: Settings,
171 }
172 #[derive(Deserialize)]
173 struct ShellVars(Vec<KeyPair>);
174
175 let shell_vars = obj
176 .entry(String::from("shellVariables"))
177 .or_insert_with(|| Dict::new().into())
178 .as_object_mut()
179 .unwrap();
180 match serde_json::from_value::<ShellVars>(vars) {
181 Ok(vars) => {
182 for KeyPair { name, value } in vars.0 {
183 shell_vars.insert(name, value);
184 }
185 }
186 Err(e) => eprintln!("malformed shell variables for scope {}, {:}", scope, e),
187 }
188}
189
190impl Metadata {
191 pub fn metadata_for_scope(&self, scope: &[Scope]) -> ScopedMetadata<'_> {
196 let mut metadata_matches = self
197 .scoped_metadata
198 .iter()
199 .filter_map(|meta_set| {
200 meta_set
201 .selector
202 .does_match(scope)
203 .map(|score| (score, meta_set))
204 })
205 .collect::<Vec<_>>();
206
207 metadata_matches.sort_unstable_by(|a, b| b.0.cmp(&a.0));
208 ScopedMetadata {
209 items: metadata_matches,
210 }
211 }
212
213 pub(crate) fn merged_with_raw(self, raw: LoadMetadata) -> Metadata {
214 let Metadata {
215 mut scoped_metadata,
216 } = self;
217 let mut final_items: BTreeMap<String, MetadataSet> = scoped_metadata
218 .drain(..)
219 .map(|ms| (ms.selector_string.clone(), ms))
220 .collect();
221 let Metadata { scoped_metadata } = raw.into();
222 for item in scoped_metadata {
223 final_items.insert(item.selector_string.clone(), item);
224 }
225
226 let scoped_metadata: Vec<MetadataSet> = final_items.into_values().collect();
227
228 Metadata { scoped_metadata }
229 }
230}
231
232impl MetadataSet {
233 const COMMENT_KEYS: &'static [(&'static str, &'static str)] = &[
234 ("TM_COMMENT_START", "TM_COMMENT_END"),
235 ("TM_COMMENT_START_2", "TM_COMMENT_END_2"),
236 ("TM_COMMENT_START_3", "TM_COMMENT_END_3"),
237 ];
238
239 pub fn from_raw(tuple: (SelectorString, Dict)) -> Result<MetadataSet, String> {
240 let (selector_string, mut settings) = tuple;
241 let path = settings
243 .remove("source_file_path")
244 .map(|v| v.to_string())
245 .unwrap_or_else(|| String::from("(path missing)"));
246
247 if !KEYS_WE_USE.iter().any(|key| settings.contains_key(*key)) {
248 return Err(format!("skipping {}", path));
249 }
250
251 let line_comment = settings
252 .get("shellVariables")
253 .and_then(|v| v.as_object())
254 .and_then(MetadataSet::get_line_comment_marker);
255 let block_comment = settings
256 .get("shellVariables")
257 .and_then(|v| v.as_object())
258 .and_then(MetadataSet::get_block_comment_markers);
259
260 let mut items: MetadataItems =
261 serde_json::from_value(settings.into()).map_err(|e| format!("{}: {:?}", path, e))?;
262 items.line_comment = line_comment;
263 items.block_comment = block_comment;
264
265 let selector =
266 ScopeSelectors::from_str(&selector_string).map_err(|e| format!("{}, {:?}", path, e))?;
267 Ok(MetadataSet {
268 selector_string,
269 selector,
270 items,
271 })
272 }
273
274 fn get_line_comment_marker(vars: &Dict) -> Option<String> {
275 MetadataSet::COMMENT_KEYS
276 .iter()
277 .find(|(b, e)| vars.contains_key(*b) && !vars.contains_key(*e))
278 .and_then(|(b, _e)| vars.get(*b).map(|v| v.as_str().unwrap().to_string()))
279 }
280
281 fn get_block_comment_markers(vars: &Dict) -> Option<(String, String)> {
282 MetadataSet::COMMENT_KEYS
283 .iter()
284 .find(|(b, e)| vars.contains_key(*b) && vars.contains_key(*e))
285 .map(|(b, e)| {
286 let b_str = vars.get(*b).unwrap().as_str().unwrap().to_string();
287 let e_str = vars.get(*e).unwrap().as_str().unwrap().to_string();
288 (b_str, e_str)
289 })
290 }
291}
292
293#[derive(Debug, Clone)]
339pub struct ScopedMetadata<'a> {
340 pub items: Vec<(MatchPower, &'a MetadataSet)>,
341}
342
343impl<'a> ScopedMetadata<'a> {
344 pub fn unindented_line(&self, line: &str) -> bool {
345 self.best_match(|ind| {
346 ind.unindented_line_pattern
347 .as_ref()
348 .map(|p| p.is_match(line))
349 })
350 .unwrap_or(false)
351 }
352
353 pub fn decrease_indent(&self, line: &str) -> bool {
354 self.best_match(|ind| {
355 ind.decrease_indent_pattern
356 .as_ref()
357 .map(|p| p.is_match(line))
358 })
359 .unwrap_or(false)
360 }
361
362 pub fn increase_indent(&self, line: &str) -> bool {
363 self.best_match(|ind| {
364 ind.increase_indent_pattern
365 .as_ref()
366 .map(|p| p.is_match(line))
367 })
368 .unwrap_or(false)
369 }
370
371 pub fn bracket_increase(&self, line: &str) -> bool {
372 self.best_match(|ind| {
373 ind.bracket_indent_next_line_pattern
374 .as_ref()
375 .map(|p| p.is_match(line))
376 })
377 .unwrap_or(false)
378 }
379
380 pub fn disable_indent_next_line(&self, line: &str) -> bool {
381 self.best_match(|ind| {
382 ind.disable_indent_next_line_pattern
383 .as_ref()
384 .map(|p| p.is_match(line))
385 })
386 .unwrap_or(false)
387 }
388
389 pub fn line_comment(&self) -> Option<&str> {
390 let idx = self
391 .items
392 .iter()
393 .position(|m| m.1.items.line_comment.is_some())?;
394 self.items[idx].1.items.line_comment.as_deref()
395 }
396
397 pub fn block_comment(&self) -> Option<(&str, &str)> {
398 let idx = self
399 .items
400 .iter()
401 .position(|m| m.1.items.block_comment.is_some())?;
402 self.items[idx]
403 .1
404 .items
405 .block_comment
406 .as_ref()
407 .map(|(a, b)| (a.as_str(), b.as_str()))
408 }
409
410 fn best_match<T, F>(&self, f: F) -> Option<T>
411 where
412 F: FnMut(&MetadataItems) -> Option<T>,
413 {
414 self.items
415 .iter()
416 .map(|(_, meta_set)| &meta_set.items)
417 .flat_map(f)
418 .next()
419 }
420
421 pub fn is_empty(&self) -> bool {
422 self.items.is_empty()
423 }
424}
425
426impl RawMetadataEntry {
427 pub fn load<P: Into<PathBuf>>(path: P) -> Result<Self, LoadingError> {
428 let path: PathBuf = path.into();
429 let file = File::open(&path)?;
430 let file = BufReader::new(file);
431 let mut contents = read_plist(file)?;
432 contents
436 .as_object_mut()
437 .and_then(|obj| obj.insert("path".into(), path.to_string_lossy().into()));
438 Ok(serde_json::from_value(contents)?)
439 }
440}
441
442#[derive(Serialize, Deserialize)]
443struct MetaSetSerializable {
444 selector_string: String,
445 items: Option<MetadataItems>,
446}
447
448impl Serialize for MetadataSet {
449 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
450 where
451 S: Serializer,
452 {
453 let MetadataSet {
454 selector_string,
455 items,
456 ..
457 } = self.clone();
458 let inner = MetaSetSerializable {
459 selector_string,
460 items: Some(items),
461 };
462 inner.serialize(serializer)
463 }
464}
465
466impl<'de> Deserialize<'de> for MetadataSet {
467 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
468 where
469 D: Deserializer<'de>,
470 {
471 use serde::de::Error;
472 let inner = MetaSetSerializable::deserialize(deserializer)?;
473 let MetaSetSerializable {
474 selector_string,
475 items,
476 } = inner;
477 let selector = ScopeSelectors::from_str(&selector_string)
478 .map_err(|e| Error::custom(format!("{:?}", e)))?;
479 let items = items.ok_or_else(|| Error::custom("no metadata items".to_string()))?;
480 Ok(MetadataSet {
481 selector_string,
482 selector,
483 items,
484 })
485 }
486}
487
488#[cfg(test)]
489mod tests {
490 use super::*;
491 use crate::parsing::SyntaxSet;
492 use std::path::Path;
493
494 #[test]
495 fn load_raw() {
496 let comments_file: &str = "testdata/Packages/Go/Comments.tmPreferences";
497 assert!(Path::new(comments_file).exists());
498
499 let r = RawMetadataEntry::load(comments_file);
500 assert!(r.is_ok());
501
502 let indent_file: &str = "testdata/Packages/Go/Indentation Rules.tmPreferences";
503 assert!(Path::new(indent_file).exists());
504
505 let r = RawMetadataEntry::load(indent_file).unwrap();
506 assert_eq!(r.scope, "source.go");
507
508 let indent_file: &str = "testdata/Packages/Rust/RustIndent.tmPreferences";
509 assert!(Path::new(indent_file).exists());
510
511 let r = RawMetadataEntry::load(indent_file).unwrap();
512 assert_eq!(r.scope, "source.rust");
513 }
514
515 #[test]
516 fn load_groups() {
517 let mut loaded = LoadMetadata::default();
518 let indent_file: &str = "testdata/Packages/Rust/RustIndent.tmPreferences";
519 let raw = RawMetadataEntry::load(indent_file).expect("failed to load indent metadata");
520 loaded.add_raw(raw);
521 let comment_file: &str = "testdata/Packages/Rust/RustComment.tmPreferences";
522 let raw = RawMetadataEntry::load(comment_file).expect("failed to load comment metadata");
523 loaded.add_raw(raw);
524
525 let metadata: Metadata = loaded.into();
526 assert_eq!(metadata.scoped_metadata.len(), 1);
527
528 let rust_meta = metadata.scoped_metadata.first().unwrap();
529 assert!(rust_meta.selector_string == "source.rust");
530 assert!(rust_meta.items.increase_indent_pattern.is_some());
531 assert!(rust_meta.items.line_comment.is_some());
532 }
533
534 #[test]
535 fn parse_yaml_meta() {
536 let path = "testdata/Packages/YAML/Indentation Rules.tmPreferences";
537 let metaset = LoadMetadata::quick_load(path).unwrap();
538 assert!(metaset.items.increase_indent_pattern.is_some());
539 assert!(metaset.items.decrease_indent_pattern.is_some());
540 assert!(metaset.items.bracket_indent_next_line_pattern.is_none());
541 }
542
543 #[test]
544 fn load_shell_vars() {
545 let path = "testdata/Packages/AppleScript/Comments.tmPreferences";
546 let metadata = LoadMetadata::quick_load(path).unwrap();
547 assert!(metadata
548 .items
549 .shell_variables
550 .get("TM_COMMENT_START")
551 .is_some());
552 assert!(metadata
553 .items
554 .shell_variables
555 .get("TM_COMMENT_END")
556 .is_none());
557 assert!(metadata
558 .items
559 .shell_variables
560 .get("TM_COMMENT_START_2")
561 .is_some());
562 assert!(metadata
563 .items
564 .shell_variables
565 .get("TM_COMMENT_START_3")
566 .is_some());
567 assert!(metadata
568 .items
569 .shell_variables
570 .get("TM_COMMENT_END_3")
571 .is_some());
572 assert!(metadata
573 .items
574 .shell_variables
575 .get("TM_COMMENT_DISABLE_INDENT_3")
576 .is_some());
577 assert!(metadata.items.line_comment.is_some());
578 assert!(metadata.items.block_comment.is_some());
579 assert!(metadata.items.increase_indent_pattern.is_none());
580 }
581
582 #[test]
583 fn indent_rust() {
584 let ps = SyntaxSet::load_from_folder("testdata/Packages/Rust").unwrap();
585
586 let rust_scopes = [Scope::new("source.rust").unwrap()];
587 let indent_ctx = ps.metadata.metadata_for_scope(&rust_scopes);
588
589 assert_eq!(indent_ctx.items.len(), 1, "failed to load rust metadata");
590 assert!(indent_ctx.increase_indent("struct This {"));
591 assert!(!indent_ctx.increase_indent("struct This }"));
592 assert!(indent_ctx.decrease_indent(" }"));
593 assert!(!indent_ctx.decrease_indent("struct This {"));
594 assert!(!indent_ctx.decrease_indent("struct This {}"));
595 assert!(!indent_ctx.increase_indent("struct This {}"));
596 }
597}