Skip to main content

syntect/parsing/
metadata.rs

1//! Support for loading `.tmPreferences` metadata files, with information related to indentation
2//! rules, comment markers, and other syntax-specific things.
3
4use 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
22/// A String representation of a [`ScopeSelectors`] instance.
23///
24/// [`ScopeSelectors`]: ../../highlighting/struct.ScopeSelectors.html
25type SelectorString = String;
26
27/// A collection of all loaded metadata
28#[derive(Debug, Default, Clone, Serialize, Deserialize)]
29pub struct Metadata {
30    pub scoped_metadata: Vec<MetadataSet>,
31}
32
33/// Metadata for a particular [`ScopeSelector`]
34///
35/// [`ScopeSelector`]: ../../highlighting/struct.ScopeSelector.html
36#[derive(Debug, Clone, PartialEq)]
37pub struct MetadataSet {
38    /// The raw string representation of this selector. We keep this around
39    /// for serialization; it's easier than trying to rebuild it from the
40    /// parsed `ScopeSelectors`.
41    pub selector_string: SelectorString,
42    /// The scope selector to which this metadata applies
43    pub selector: ScopeSelectors,
44    /// The actual metadata.
45    pub items: MetadataItems,
46}
47
48/// Items loaded from `.tmPreferences` metadata files, for a particular scope.
49/// For more information, see [Metadata Files](http://docs.sublimetext.info/en/latest/reference/metadata.html)
50#[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    /// For convenience; this is the first value in `shell_variables`
63    /// with a key beginning: `TM_COMMENT_START` that doesn't have a
64    /// corresponding `TM_COMMENT_END`.
65    pub line_comment: Option<String>,
66    /// The first pair of `TM_COMMENT_START` and `TM_COMMENT_END` items in
67    /// `shell_variables`, if they exist.
68    pub block_comment: Option<(String, String)>,
69}
70
71/// A type that can be deserialized from a `.tmPreferences` file.
72/// Since multiple files can refer to the same scope, we merge them while loading.
73#[derive(Debug, Clone, Serialize, Deserialize)]
74pub(crate) struct RawMetadataEntry {
75    path: PathBuf,
76    scope: SelectorString,
77    settings: Dict,
78}
79
80/// Convenience type for loading heterogeneous metadata.
81#[derive(Debug, Default, Clone)]
82pub(crate) struct LoadMetadata {
83    loaded: Vec<RawMetadataEntry>,
84}
85
86// all of these are optional, but we don't want to deserialize if
87// we don't have at least _one_ of them present
88const KEYS_WE_USE: &[&str] = &[
89    "increaseIndentPattern",
90    "decreaseIndentPattern",
91    "bracketIndentNextLinePattern",
92    "disableIndentNextLinePattern",
93    "unIndentedLinePattern",
94    "indentParens",
95    "shellVariables",
96];
97
98impl LoadMetadata {
99    /// Adds the provided `RawMetadataEntry`
100    ///
101    /// When creating the final [`Metadata`] object, all [`RawMetadataEntry`] items are sorted by
102    /// path, and items that share a scope selector are merged; last writer wins.
103    ///
104    /// [`Metadata`]: struct.Metadata.html
105    /// [`RawMetadataEntry`]: struct.RawMetadataEntry.html
106    pub fn add_raw(&mut self, raw: RawMetadataEntry) {
107        self.loaded.push(raw);
108    }
109
110    /// Generates a [`MetadataSet`] from a single file
111    ///
112    /// [`MetadataSet`]: struct.MetadataSet.html
113    #[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    /// For a given stack of scopes, returns a [`ScopedMetadata`] object which provides convenient
192    /// access to metadata items which match the stack.
193    ///
194    /// [`ScopedMetadata`]: struct.ScopedMetadata.html
195    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        // we just use this for more useful debug messages
242        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/// A collection of [`MetadataSet`]s which match a given scope selector, sorted by the strength of
294/// the match
295///
296/// # Examples
297///
298/// ```
299/// # #[macro_use] extern crate serde_json;
300/// # use syntect::parsing::*;
301/// # use syntect::highlighting::ScopeSelectors;
302/// # use std::str::FromStr;
303/// #
304/// // given the following two scoped metadata collections:
305///
306/// let one_selector = "source.my_lang";
307/// let one_items = json!({
308///     "increaseIndentPattern": "one increase",
309///     "decreaseIndentPattern": "one decrease",
310/// });
311/// # let one_items = one_items.as_object().cloned().unwrap();
312///
313/// let two_selector = "other.thing";
314/// let two_items = json!({
315///     "increaseIndentPattern": "two increase",
316/// });
317/// # let two_items = two_items.as_object().cloned().unwrap();
318/// #
319/// # let one = MetadataSet::from_raw((one_selector.into(), one_items)).unwrap();
320/// # let two = MetadataSet::from_raw((two_selector.into(), two_items)).unwrap();
321/// #
322/// # let scoped_metadata = vec![one, two];
323/// # let metadata = Metadata { scoped_metadata };
324///
325/// // both of which match this scope stack:
326///
327/// let query_scope = ScopeStack::from_str("source.my_lang other.thing").unwrap();
328/// let scoped = metadata.metadata_for_scope(query_scope.as_slice());
329///
330/// // the better match is used when it has a field,
331/// assert!(scoped.increase_indent("two increase"));
332///
333/// // and the other match is used when it does not.
334/// assert!(scoped.decrease_indent("one decrease"));
335/// ```
336///
337/// [`MetadataSet`]: struct.MetadataSet.html
338#[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        // we stash the path because we use it to determine parse order
433        // when generating the final metadata object; to_string_lossy
434        // is adequate for this purpose.
435        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}