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::{MatchPower, ParseScopeError, Scope, ScopeStack};
4use serde_derive::{Deserialize, Serialize};
5use std::str::FromStr;
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
37            .excludes
38            .iter()
39            .any(|sel| sel.is_empty() || sel.does_match(stack).is_some())
40        {
41            return None;
42        }
43        if self.path.is_empty() {
44            // an empty scope selector always matches with a score of 1
45            Some(MatchPower(0o1u64 as f64))
46        } else {
47            self.path.does_match(stack)
48        }
49    }
50
51    /// If this selector is really just a single scope, return it
52    pub fn extract_single_scope(&self) -> Option<Scope> {
53        if self.path.len() > 1 || !self.excludes.is_empty() || self.path.is_empty() {
54            return None;
55        }
56        Some(self.path.as_slice()[0])
57    }
58
59    /// Extract all selectors for generating CSS
60    pub fn extract_scopes(&self) -> Vec<Scope> {
61        self.path.scopes.clone()
62    }
63}
64
65impl FromStr for ScopeSelector {
66    type Err = ParseScopeError;
67
68    /// Parses a scope stack followed optionally by (one or more) " -" and then a scope stack to exclude
69    fn from_str(s: &str) -> Result<ScopeSelector, ParseScopeError> {
70        let mut excludes = Vec::new();
71        let mut path_str: &str = "";
72        for (i, selector) in s.split(" -").enumerate() {
73            if i == 0 {
74                path_str = selector;
75            } else {
76                excludes.push(ScopeStack::from_str(selector)?);
77            }
78        }
79        Ok(ScopeSelector {
80            path: ScopeStack::from_str(path_str)?,
81            excludes,
82        })
83    }
84}
85
86impl ScopeSelectors {
87    /// Checks if any of the given selectors match the given scope stack
88    ///
89    /// If so, it returns a match score. Higher match scores indicate stronger matches. Scores are
90    /// ordered according to the rules found at [https://manual.macromates.com/en/scope_selectors](https://manual.macromates.com/en/scope_selectors).
91    ///
92    /// # Examples
93    ///
94    /// ```
95    /// use syntect::parsing::{ScopeStack, MatchPower};
96    /// use syntect::highlighting::ScopeSelectors;
97    /// use std::str::FromStr;
98    /// assert_eq!(ScopeSelectors::from_str("a.b, a e.f - c k, e.f - a.b").unwrap()
99    ///     .does_match(ScopeStack::from_str("a.b c.d j e.f").unwrap().as_slice()),
100    ///     Some(MatchPower(0o2001u64 as f64)));
101    /// ```
102    pub fn does_match(&self, stack: &[Scope]) -> Option<MatchPower> {
103        self.selectors
104            .iter()
105            .filter_map(|sel| sel.does_match(stack))
106            .max()
107    }
108}
109
110impl FromStr for ScopeSelectors {
111    type Err = ParseScopeError;
112
113    /// Parses a series of selectors separated by commas or pipes
114    fn from_str(s: &str) -> Result<ScopeSelectors, ParseScopeError> {
115        let mut selectors = Vec::new();
116        for selector in s.split(&[',', '|'][..]) {
117            selectors.push(ScopeSelector::from_str(selector)?)
118        }
119        Ok(ScopeSelectors { selectors })
120    }
121}
122
123#[cfg(test)]
124mod tests {
125    use super::*;
126    #[test]
127    fn selectors_work() {
128        use std::str::FromStr;
129        let sels = ScopeSelectors::from_str(
130            "source.php meta.preprocessor - string.quoted, \
131                                             source string",
132        )
133        .unwrap();
134        assert_eq!(sels.selectors.len(), 2);
135        let first_sel = &sels.selectors[0];
136        assert_eq!(format!("{:?}", first_sel),
137                   "ScopeSelector { path: ScopeStack { clear_stack: [], scopes: [<source.php>, <meta.preprocessor>] }, excludes: [ScopeStack { clear_stack: [], scopes: [<string.quoted>] }] }");
138
139        let sels = ScopeSelectors::from_str(
140            "source.php meta.preprocessor -string.quoted|\
141                                             source string",
142        )
143        .unwrap();
144        assert_eq!(sels.selectors.len(), 2);
145        let first_sel = &sels.selectors[0];
146        assert_eq!(format!("{:?}", first_sel),
147                   "ScopeSelector { path: ScopeStack { clear_stack: [], scopes: [<source.php>, <meta.preprocessor>] }, excludes: [ScopeStack { clear_stack: [], scopes: [<string.quoted>] }] }");
148
149        let sels = ScopeSelectors::from_str(
150            "text.xml meta.tag.preprocessor.xml punctuation.separator.key-value.xml",
151        )
152        .unwrap();
153        assert_eq!(sels.selectors.len(), 1);
154        let first_sel = &sels.selectors[0];
155        assert_eq!(format!("{:?}", first_sel),
156                   "ScopeSelector { path: ScopeStack { clear_stack: [], scopes: [<text.xml>, <meta.tag.preprocessor.xml>, <punctuation.separator.key-value.xml>] }, excludes: [] }");
157
158        let sels = ScopeSelectors::from_str("text.xml meta.tag.preprocessor.xml punctuation.separator.key-value.xml - text.html - string")
159            .unwrap();
160        assert_eq!(sels.selectors.len(), 1);
161        let first_sel = &sels.selectors[0];
162        assert_eq!(format!("{:?}", first_sel),
163                   "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>] }] }");
164
165        let sels = ScopeSelectors::from_str("text.xml meta.tag.preprocessor.xml punctuation.separator.key-value.xml - text.html - string, source - comment")
166            .unwrap();
167        assert_eq!(sels.selectors.len(), 2);
168        let first_sel = &sels.selectors[0];
169        assert_eq!(format!("{:?}", first_sel),
170                   "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>] }] }");
171        let second_sel = &sels.selectors[1];
172        assert_eq!(format!("{:?}", second_sel),
173                   "ScopeSelector { path: ScopeStack { clear_stack: [], scopes: [<source>] }, excludes: [ScopeStack { clear_stack: [], scopes: [<comment>] }] }");
174
175        let sels = ScopeSelectors::from_str(" -a.b|j.g").unwrap();
176        assert_eq!(sels.selectors.len(), 2);
177        let first_sel = &sels.selectors[0];
178        assert_eq!(format!("{:?}", first_sel),
179                   "ScopeSelector { path: ScopeStack { clear_stack: [], scopes: [] }, excludes: [ScopeStack { clear_stack: [], scopes: [<a.b>] }] }");
180        let second_sel = &sels.selectors[1];
181        assert_eq!(
182            format!("{:?}", second_sel),
183            "ScopeSelector { path: ScopeStack { clear_stack: [], scopes: [<j.g>] }, excludes: [] }"
184        );
185    }
186    #[test]
187    fn matching_works() {
188        use crate::parsing::{MatchPower, ScopeStack};
189        use std::str::FromStr;
190        assert_eq!(
191            ScopeSelectors::from_str("a.b, a e, e.f")
192                .unwrap()
193                .does_match(ScopeStack::from_str("a.b e.f").unwrap().as_slice()),
194            Some(MatchPower(0o20u64 as f64))
195        );
196        assert_eq!(
197            ScopeSelectors::from_str("a.b, a e.f, e.f")
198                .unwrap()
199                .does_match(ScopeStack::from_str("a.b e.f").unwrap().as_slice()),
200            Some(MatchPower(0o21u64 as f64))
201        );
202        assert_eq!(
203            ScopeSelectors::from_str("a.b, a e.f - c j, e.f")
204                .unwrap()
205                .does_match(ScopeStack::from_str("a.b c.d j e.f").unwrap().as_slice()),
206            Some(MatchPower(0o2000u64 as f64))
207        );
208        assert_eq!(
209            ScopeSelectors::from_str("a.b, a e.f - c j, e.f - a.b")
210                .unwrap()
211                .does_match(ScopeStack::from_str("a.b c.d j e.f").unwrap().as_slice()),
212            Some(MatchPower(0o2u64 as f64))
213        );
214        assert_eq!(
215            ScopeSelectors::from_str("a.b, a e.f - c k, e.f - a.b")
216                .unwrap()
217                .does_match(ScopeStack::from_str("a.b c.d j e.f").unwrap().as_slice()),
218            Some(MatchPower(0o2001u64 as f64))
219        );
220        assert_eq!(
221            ScopeSelectors::from_str("a.b|a e.f -d, e.f -a.b")
222                .unwrap()
223                .does_match(ScopeStack::from_str("a.b c.d e.f").unwrap().as_slice()),
224            Some(MatchPower(0o201u64 as f64))
225        );
226    }
227
228    #[test]
229    fn empty_stack_matching_works() {
230        use crate::parsing::{MatchPower, ScopeStack};
231        use std::str::FromStr;
232        assert_eq!(
233            ScopeSelector::from_str(" - a.b")
234                .unwrap()
235                .does_match(ScopeStack::from_str("a.b c.d j e.f").unwrap().as_slice()),
236            None
237        );
238        assert_eq!(
239            ScopeSelector::from_str("")
240                .unwrap()
241                .does_match(ScopeStack::from_str("a.b c.d j e.f").unwrap().as_slice()),
242            Some(MatchPower(0o1u64 as f64))
243        );
244        assert_eq!(
245            ScopeSelector::from_str("")
246                .unwrap()
247                .does_match(ScopeStack::from_str("").unwrap().as_slice()),
248            Some(MatchPower(0o1u64 as f64))
249        );
250        assert_eq!(
251            ScopeSelector::from_str(" - a.b")
252                .unwrap()
253                .does_match(ScopeStack::from_str("").unwrap().as_slice()),
254            Some(MatchPower(0o1u64 as f64))
255        );
256        assert_eq!(
257            ScopeSelector::from_str("a.b - ")
258                .unwrap()
259                .does_match(ScopeStack::from_str("").unwrap().as_slice()),
260            None
261        );
262        assert_eq!(
263            ScopeSelector::from_str("a.b - ")
264                .unwrap()
265                .does_match(ScopeStack::from_str("a.b c.d j e.f").unwrap().as_slice()),
266            None
267        );
268        assert_eq!(
269            ScopeSelector::from_str(" - ")
270                .unwrap()
271                .does_match(ScopeStack::from_str("a.b c.d j e.f").unwrap().as_slice()),
272            None
273        );
274        assert_eq!(
275            ScopeSelector::from_str(" - a.b")
276                .unwrap()
277                .does_match(ScopeStack::from_str("a.b c.d j e.f").unwrap().as_slice()),
278            None
279        );
280        assert_eq!(
281            ScopeSelector::from_str(" - g.h")
282                .unwrap()
283                .does_match(ScopeStack::from_str("a.b c.d j e.f").unwrap().as_slice()),
284            Some(MatchPower(0o1u64 as f64))
285        );
286
287        assert_eq!(
288            ScopeSelector::from_str(" -a.b")
289                .unwrap()
290                .does_match(ScopeStack::from_str("a.b c.d j e.f").unwrap().as_slice()),
291            None
292        );
293        assert_eq!(
294            ScopeSelector::from_str("")
295                .unwrap()
296                .does_match(ScopeStack::from_str("a.b c.d j e.f").unwrap().as_slice()),
297            Some(MatchPower(0o1u64 as f64))
298        );
299        assert_eq!(
300            ScopeSelector::from_str(" -a.b")
301                .unwrap()
302                .does_match(ScopeStack::from_str("").unwrap().as_slice()),
303            Some(MatchPower(0o1u64 as f64))
304        );
305        assert_eq!(
306            ScopeSelector::from_str("a.b -")
307                .unwrap()
308                .does_match(ScopeStack::from_str("").unwrap().as_slice()),
309            None
310        );
311        assert_eq!(
312            ScopeSelector::from_str("a.b -")
313                .unwrap()
314                .does_match(ScopeStack::from_str("a.b c.d j e.f").unwrap().as_slice()),
315            None
316        );
317        assert_eq!(
318            ScopeSelector::from_str(" -")
319                .unwrap()
320                .does_match(ScopeStack::from_str("a.b c.d j e.f").unwrap().as_slice()),
321            None
322        );
323        assert_eq!(
324            ScopeSelector::from_str(" -a.b")
325                .unwrap()
326                .does_match(ScopeStack::from_str("a.b c.d j e.f").unwrap().as_slice()),
327            None
328        );
329        assert_eq!(
330            ScopeSelector::from_str(" -g.h")
331                .unwrap()
332                .does_match(ScopeStack::from_str("a.b c.d j e.f").unwrap().as_slice()),
333            Some(MatchPower(0o1u64 as f64))
334        );
335    }
336
337    #[test]
338    fn multiple_excludes_matching_works() {
339        use crate::parsing::{MatchPower, ScopeStack};
340        use std::str::FromStr;
341        assert_eq!(
342            ScopeSelector::from_str(" - a.b - c.d")
343                .unwrap()
344                .does_match(ScopeStack::from_str("a.b c.d j e.f").unwrap().as_slice()),
345            None
346        );
347        assert_eq!(
348            ScopeSelector::from_str(" - a.b - c.d")
349                .unwrap()
350                .does_match(ScopeStack::from_str("").unwrap().as_slice()),
351            Some(MatchPower(0o1u64 as f64))
352        );
353        assert_eq!(
354            ScopeSelector::from_str("a.b - c.d -e.f")
355                .unwrap()
356                .does_match(ScopeStack::from_str("").unwrap().as_slice()),
357            None
358        );
359        assert_eq!(
360            ScopeSelector::from_str("a.b - c.d -")
361                .unwrap()
362                .does_match(ScopeStack::from_str("a.b c.d j e.f").unwrap().as_slice()),
363            None
364        );
365        assert_eq!(
366            ScopeSelector::from_str(" -g.h - h.i")
367                .unwrap()
368                .does_match(ScopeStack::from_str("a.b c.d j e.f").unwrap().as_slice()),
369            Some(MatchPower(0o1u64 as f64))
370        );
371        assert_eq!(
372            ScopeSelector::from_str("a.b")
373                .unwrap()
374                .does_match(ScopeStack::from_str("a.b c.d j e.f").unwrap().as_slice()),
375            Some(MatchPower(0o2u64 as f64))
376        );
377        assert_eq!(
378            ScopeSelector::from_str("a.b -g.h - h.i")
379                .unwrap()
380                .does_match(ScopeStack::from_str("a.b c.d j e.f").unwrap().as_slice()),
381            Some(MatchPower(0o2u64 as f64))
382        );
383        assert_eq!(
384            ScopeSelector::from_str("c.d")
385                .unwrap()
386                .does_match(ScopeStack::from_str("a.b c.d j e.f").unwrap().as_slice()),
387            Some(MatchPower(0o20u64 as f64))
388        );
389        assert_eq!(
390            ScopeSelector::from_str("c.d - j.g - h.i")
391                .unwrap()
392                .does_match(ScopeStack::from_str("a.b c.d j e.f").unwrap().as_slice()),
393            Some(MatchPower(0o20u64 as f64))
394        );
395        assert_eq!(
396            ScopeSelectors::from_str("j.g| -a.b")
397                .unwrap()
398                .does_match(ScopeStack::from_str("a.b c.d j e.f").unwrap().as_slice()),
399            None
400        );
401        assert_eq!(
402            ScopeSelectors::from_str(" -a.b|j.g")
403                .unwrap()
404                .does_match(ScopeStack::from_str("a.b c.d j e.f").unwrap().as_slice()),
405            None
406        );
407        assert_eq!(
408            ScopeSelectors::from_str(" -a.b,c.d - j.g - h.i")
409                .unwrap()
410                .does_match(ScopeStack::from_str("a.b c.d j e.f").unwrap().as_slice()),
411            Some(MatchPower(0o20u64 as f64))
412        );
413        assert_eq!(
414            ScopeSelectors::from_str(" -a.b, -d.c -f.e")
415                .unwrap()
416                .does_match(ScopeStack::from_str("a.b c.d j e.f").unwrap().as_slice()),
417            Some(MatchPower(0o01u64 as f64))
418        );
419    }
420}