syntect/highlighting/
selector.rs

1/// Code based on <https://github.com/defuz/sublimate/blob/master/src/core/syntax/scope.rs>
2/// released under the MIT license by @defuz
3use crate::parsing::{Scope, ScopeStack, MatchPower, ParseScopeError};
4use std::str::FromStr;
5use serde_derive::{Deserialize, Serialize};
6
7/// A single selector consisting of a stack to match and a possible stack to
8/// exclude from being matched.
9///
10/// You probably want [`ScopeSelectors`] which is this but with union support.
11///
12/// [`ScopeSelectors`]: struct.ScopeSelectors.html
13#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
14pub struct ScopeSelector {
15    pub path: ScopeStack,
16    pub excludes: Vec<ScopeStack>,
17}
18
19/// A selector set that matches anything matched by any of its component selectors.
20///
21/// See [The TextMate Docs](https://manual.macromates.com/en/scope_selectors) for how these work.
22#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
23pub struct ScopeSelectors {
24    /// The selectors, if any of them match, that this matches
25    pub selectors: Vec<ScopeSelector>,
26}
27
28impl ScopeSelector {
29    /// Checks if this selector matches a given scope stack.
30    ///
31    /// See [`ScopeSelectors::does_match`] for more info.
32    ///
33    /// [`ScopeSelectors::does_match`]: struct.ScopeSelectors.html#method.does_match
34    pub fn does_match(&self, stack: &[Scope]) -> Option<MatchPower> {
35        // if there are any exclusions, and any one of them matches, then this selector doesn't match
36        if self.excludes.iter().any(|sel| sel.is_empty() || sel.does_match(stack).is_some()) {
37            return None;
38        }
39        if self.path.is_empty() {
40            // an empty scope selector always matches with a score of 1
41            Some(MatchPower(0o1u64 as f64))
42        } else {
43            self.path.does_match(stack)
44        }
45    }
46
47    /// If this selector is really just a single scope, return it
48    pub fn extract_single_scope(&self) -> Option<Scope> {
49        if self.path.len() > 1 || !self.excludes.is_empty() || self.path.is_empty() {
50            return None;
51        }
52        Some(self.path.as_slice()[0])
53    }
54
55    /// Extract all selectors for generating CSS
56    pub fn extract_scopes(&self) -> Vec<Scope> {
57        self.path.scopes.clone()
58    }
59}
60
61impl FromStr for ScopeSelector {
62    type Err = ParseScopeError;
63
64    /// Parses a scope stack followed optionally by (one or more) " -" and then a scope stack to exclude
65    fn from_str(s: &str) -> Result<ScopeSelector, ParseScopeError> {
66        let mut excludes = Vec::new();
67        let mut path_str: &str = "";
68        for (i, selector) in s.split(" -").enumerate() {
69            if i == 0 {
70                path_str = selector;
71            } else {
72                excludes.push(ScopeStack::from_str(selector)?);
73            }
74        }
75        Ok(ScopeSelector {
76            path: ScopeStack::from_str(path_str)?,
77            excludes,
78        })
79    }
80}
81
82impl ScopeSelectors {
83    /// Checks if any of the given selectors match the given scope stack
84    ///
85    /// If so, it returns a match score. Higher match scores indicate stronger matches. Scores are
86    /// ordered according to the rules found at [https://manual.macromates.com/en/scope_selectors](https://manual.macromates.com/en/scope_selectors).
87    ///
88    /// # Examples
89    ///
90    /// ```
91    /// use syntect::parsing::{ScopeStack, MatchPower};
92    /// use syntect::highlighting::ScopeSelectors;
93    /// use std::str::FromStr;
94    /// assert_eq!(ScopeSelectors::from_str("a.b, a e.f - c k, e.f - a.b").unwrap()
95    ///     .does_match(ScopeStack::from_str("a.b c.d j e.f").unwrap().as_slice()),
96    ///     Some(MatchPower(0o2001u64 as f64)));
97    /// ```
98    pub fn does_match(&self, stack: &[Scope]) -> Option<MatchPower> {
99        self.selectors.iter().filter_map(|sel| sel.does_match(stack)).max()
100    }
101}
102
103impl FromStr for ScopeSelectors {
104    type Err = ParseScopeError;
105
106    /// Parses a series of selectors separated by commas or pipes
107    fn from_str(s: &str) -> Result<ScopeSelectors, ParseScopeError> {
108        let mut selectors = Vec::new();
109        for selector in s.split(&[',', '|'][..]) {
110            selectors.push(ScopeSelector::from_str(selector)?)
111        }
112        Ok(ScopeSelectors { selectors })
113    }
114}
115
116#[cfg(test)]
117mod tests {
118    use super::*;
119    #[test]
120    fn selectors_work() {
121        use std::str::FromStr;
122        let sels = ScopeSelectors::from_str("source.php meta.preprocessor - string.quoted, \
123                                             source string")
124            .unwrap();
125        assert_eq!(sels.selectors.len(), 2);
126        let first_sel = &sels.selectors[0];
127        assert_eq!(format!("{:?}", first_sel),
128                   "ScopeSelector { path: ScopeStack { clear_stack: [], scopes: [<source.php>, <meta.preprocessor>] }, excludes: [ScopeStack { clear_stack: [], scopes: [<string.quoted>] }] }");
129
130        let sels = ScopeSelectors::from_str("source.php meta.preprocessor -string.quoted|\
131                                             source string")
132            .unwrap();
133        assert_eq!(sels.selectors.len(), 2);
134        let first_sel = &sels.selectors[0];
135        assert_eq!(format!("{:?}", first_sel),
136                   "ScopeSelector { path: ScopeStack { clear_stack: [], scopes: [<source.php>, <meta.preprocessor>] }, excludes: [ScopeStack { clear_stack: [], scopes: [<string.quoted>] }] }");
137
138        let sels = ScopeSelectors::from_str("text.xml meta.tag.preprocessor.xml punctuation.separator.key-value.xml")
139            .unwrap();
140        assert_eq!(sels.selectors.len(), 1);
141        let first_sel = &sels.selectors[0];
142        assert_eq!(format!("{:?}", first_sel),
143                   "ScopeSelector { path: ScopeStack { clear_stack: [], scopes: [<text.xml>, <meta.tag.preprocessor.xml>, <punctuation.separator.key-value.xml>] }, excludes: [] }");
144
145        let sels = ScopeSelectors::from_str("text.xml meta.tag.preprocessor.xml punctuation.separator.key-value.xml - text.html - string")
146            .unwrap();
147        assert_eq!(sels.selectors.len(), 1);
148        let first_sel = &sels.selectors[0];
149        assert_eq!(format!("{:?}", first_sel),
150                   "ScopeSelector { path: ScopeStack { clear_stack: [], scopes: [<text.xml>, <meta.tag.preprocessor.xml>, <punctuation.separator.key-value.xml>] }, excludes: [ScopeStack { clear_stack: [], scopes: [<text.html>] }, ScopeStack { clear_stack: [], scopes: [<string>] }] }");
151
152        let sels = ScopeSelectors::from_str("text.xml meta.tag.preprocessor.xml punctuation.separator.key-value.xml - text.html - string, source - comment")
153            .unwrap();
154        assert_eq!(sels.selectors.len(), 2);
155        let first_sel = &sels.selectors[0];
156        assert_eq!(format!("{:?}", first_sel),
157                   "ScopeSelector { path: ScopeStack { clear_stack: [], scopes: [<text.xml>, <meta.tag.preprocessor.xml>, <punctuation.separator.key-value.xml>] }, excludes: [ScopeStack { clear_stack: [], scopes: [<text.html>] }, ScopeStack { clear_stack: [], scopes: [<string>] }] }");
158        let second_sel = &sels.selectors[1];
159        assert_eq!(format!("{:?}", second_sel),
160                   "ScopeSelector { path: ScopeStack { clear_stack: [], scopes: [<source>] }, excludes: [ScopeStack { clear_stack: [], scopes: [<comment>] }] }");
161
162        let sels = ScopeSelectors::from_str(" -a.b|j.g")
163            .unwrap();
164        assert_eq!(sels.selectors.len(), 2);
165        let first_sel = &sels.selectors[0];
166        assert_eq!(format!("{:?}", first_sel),
167                   "ScopeSelector { path: ScopeStack { clear_stack: [], scopes: [] }, excludes: [ScopeStack { clear_stack: [], scopes: [<a.b>] }] }");
168        let second_sel = &sels.selectors[1];
169        assert_eq!(format!("{:?}", second_sel),
170                   "ScopeSelector { path: ScopeStack { clear_stack: [], scopes: [<j.g>] }, excludes: [] }");
171    }
172    #[test]
173    fn matching_works() {
174        use crate::parsing::{ScopeStack, MatchPower};
175        use std::str::FromStr;
176        assert_eq!(ScopeSelectors::from_str("a.b, a e, e.f")
177                       .unwrap()
178                       .does_match(ScopeStack::from_str("a.b e.f").unwrap().as_slice()),
179                   Some(MatchPower(0o20u64 as f64)));
180        assert_eq!(ScopeSelectors::from_str("a.b, a e.f, e.f")
181                       .unwrap()
182                       .does_match(ScopeStack::from_str("a.b e.f").unwrap().as_slice()),
183                   Some(MatchPower(0o21u64 as f64)));
184        assert_eq!(ScopeSelectors::from_str("a.b, a e.f - c j, e.f")
185                       .unwrap()
186                       .does_match(ScopeStack::from_str("a.b c.d j e.f").unwrap().as_slice()),
187                   Some(MatchPower(0o2000u64 as f64)));
188        assert_eq!(ScopeSelectors::from_str("a.b, a e.f - c j, e.f - a.b")
189                       .unwrap()
190                       .does_match(ScopeStack::from_str("a.b c.d j e.f").unwrap().as_slice()),
191                   Some(MatchPower(0o2u64 as f64)));
192        assert_eq!(ScopeSelectors::from_str("a.b, a e.f - c k, e.f - a.b")
193                       .unwrap()
194                       .does_match(ScopeStack::from_str("a.b c.d j e.f").unwrap().as_slice()),
195                   Some(MatchPower(0o2001u64 as f64)));
196        assert_eq!(ScopeSelectors::from_str("a.b|a e.f -d, e.f -a.b")
197                       .unwrap()
198                       .does_match(ScopeStack::from_str("a.b c.d e.f").unwrap().as_slice()),
199                   Some(MatchPower(0o201u64 as f64)));
200    }
201
202    #[test]
203    fn empty_stack_matching_works() {
204        use crate::parsing::{ScopeStack, MatchPower};
205        use std::str::FromStr;
206        assert_eq!(ScopeSelector::from_str(" - a.b")
207                       .unwrap()
208                       .does_match(ScopeStack::from_str("a.b c.d j e.f").unwrap().as_slice()),
209                   None);
210        assert_eq!(ScopeSelector::from_str("")
211                       .unwrap()
212                       .does_match(ScopeStack::from_str("a.b c.d j e.f").unwrap().as_slice()),
213                   Some(MatchPower(0o1u64 as f64)));
214        assert_eq!(ScopeSelector::from_str("")
215                       .unwrap()
216                       .does_match(ScopeStack::from_str("").unwrap().as_slice()),
217                   Some(MatchPower(0o1u64 as f64)));
218        assert_eq!(ScopeSelector::from_str(" - a.b")
219                       .unwrap()
220                       .does_match(ScopeStack::from_str("").unwrap().as_slice()),
221                   Some(MatchPower(0o1u64 as f64)));
222        assert_eq!(ScopeSelector::from_str("a.b - ")
223                       .unwrap()
224                       .does_match(ScopeStack::from_str("").unwrap().as_slice()),
225                   None);
226        assert_eq!(ScopeSelector::from_str("a.b - ")
227                       .unwrap()
228                       .does_match(ScopeStack::from_str("a.b c.d j e.f").unwrap().as_slice()),
229                   None);
230        assert_eq!(ScopeSelector::from_str(" - ")
231                       .unwrap()
232                       .does_match(ScopeStack::from_str("a.b c.d j e.f").unwrap().as_slice()),
233                   None);
234        assert_eq!(ScopeSelector::from_str(" - a.b")
235                       .unwrap()
236                       .does_match(ScopeStack::from_str("a.b c.d j e.f").unwrap().as_slice()),
237                   None);
238        assert_eq!(ScopeSelector::from_str(" - g.h")
239                       .unwrap()
240                       .does_match(ScopeStack::from_str("a.b c.d j e.f").unwrap().as_slice()),
241                   Some(MatchPower(0o1u64 as f64)));
242
243        assert_eq!(ScopeSelector::from_str(" -a.b")
244                       .unwrap()
245                       .does_match(ScopeStack::from_str("a.b c.d j e.f").unwrap().as_slice()),
246                   None);
247        assert_eq!(ScopeSelector::from_str("")
248                       .unwrap()
249                       .does_match(ScopeStack::from_str("a.b c.d j e.f").unwrap().as_slice()),
250                   Some(MatchPower(0o1u64 as f64)));
251        assert_eq!(ScopeSelector::from_str(" -a.b")
252                       .unwrap()
253                       .does_match(ScopeStack::from_str("").unwrap().as_slice()),
254                   Some(MatchPower(0o1u64 as f64)));
255        assert_eq!(ScopeSelector::from_str("a.b -")
256                       .unwrap()
257                       .does_match(ScopeStack::from_str("").unwrap().as_slice()),
258                   None);
259        assert_eq!(ScopeSelector::from_str("a.b -")
260                       .unwrap()
261                       .does_match(ScopeStack::from_str("a.b c.d j e.f").unwrap().as_slice()),
262                   None);
263        assert_eq!(ScopeSelector::from_str(" -")
264                       .unwrap()
265                       .does_match(ScopeStack::from_str("a.b c.d j e.f").unwrap().as_slice()),
266                   None);
267        assert_eq!(ScopeSelector::from_str(" -a.b")
268                       .unwrap()
269                       .does_match(ScopeStack::from_str("a.b c.d j e.f").unwrap().as_slice()),
270                   None);
271        assert_eq!(ScopeSelector::from_str(" -g.h")
272                       .unwrap()
273                       .does_match(ScopeStack::from_str("a.b c.d j e.f").unwrap().as_slice()),
274                   Some(MatchPower(0o1u64 as f64)));
275    }
276
277    #[test]
278    fn multiple_excludes_matching_works() {
279        use crate::parsing::{ScopeStack, MatchPower};
280        use std::str::FromStr;
281        assert_eq!(ScopeSelector::from_str(" - a.b - c.d")
282                       .unwrap()
283                       .does_match(ScopeStack::from_str("a.b c.d j e.f").unwrap().as_slice()),
284                   None);
285        assert_eq!(ScopeSelector::from_str(" - a.b - c.d")
286                       .unwrap()
287                       .does_match(ScopeStack::from_str("").unwrap().as_slice()),
288                   Some(MatchPower(0o1u64 as f64)));
289        assert_eq!(ScopeSelector::from_str("a.b - c.d -e.f")
290                       .unwrap()
291                       .does_match(ScopeStack::from_str("").unwrap().as_slice()),
292                   None);
293        assert_eq!(ScopeSelector::from_str("a.b - c.d -")
294                       .unwrap()
295                       .does_match(ScopeStack::from_str("a.b c.d j e.f").unwrap().as_slice()),
296                   None);
297        assert_eq!(ScopeSelector::from_str(" -g.h - h.i")
298                       .unwrap()
299                       .does_match(ScopeStack::from_str("a.b c.d j e.f").unwrap().as_slice()),
300                   Some(MatchPower(0o1u64 as f64)));
301        assert_eq!(ScopeSelector::from_str("a.b")
302                       .unwrap()
303                       .does_match(ScopeStack::from_str("a.b c.d j e.f").unwrap().as_slice()),
304                   Some(MatchPower(0o2u64 as f64)));
305        assert_eq!(ScopeSelector::from_str("a.b -g.h - h.i")
306                       .unwrap()
307                       .does_match(ScopeStack::from_str("a.b c.d j e.f").unwrap().as_slice()),
308                   Some(MatchPower(0o2u64 as f64)));
309        assert_eq!(ScopeSelector::from_str("c.d")
310                       .unwrap()
311                       .does_match(ScopeStack::from_str("a.b c.d j e.f").unwrap().as_slice()),
312                   Some(MatchPower(0o20u64 as f64)));
313        assert_eq!(ScopeSelector::from_str("c.d - j.g - h.i")
314                       .unwrap()
315                       .does_match(ScopeStack::from_str("a.b c.d j e.f").unwrap().as_slice()),
316                   Some(MatchPower(0o20u64 as f64)));
317        assert_eq!(ScopeSelectors::from_str("j.g| -a.b")
318                       .unwrap()
319                       .does_match(ScopeStack::from_str("a.b c.d j e.f").unwrap().as_slice()),
320                   None);
321        assert_eq!(ScopeSelectors::from_str(" -a.b|j.g")
322                       .unwrap()
323                       .does_match(ScopeStack::from_str("a.b c.d j e.f").unwrap().as_slice()),
324                   None);
325        assert_eq!(ScopeSelectors::from_str(" -a.b,c.d - j.g - h.i")
326                       .unwrap()
327                       .does_match(ScopeStack::from_str("a.b c.d j e.f").unwrap().as_slice()),
328                   Some(MatchPower(0o20u64 as f64)));
329        assert_eq!(ScopeSelectors::from_str(" -a.b, -d.c -f.e")
330                       .unwrap()
331                       .does_match(ScopeStack::from_str("a.b c.d j e.f").unwrap().as_slice()),
332                   Some(MatchPower(0o01u64 as f64)));
333    }
334}