Skip to main content

syntect/parsing/
scope.rs

1// see DESIGN.md
2use std::cmp::{min, Ordering};
3use std::collections::HashMap;
4use std::fmt;
5use std::mem;
6use std::str::FromStr;
7use std::sync::{Mutex, MutexGuard};
8
9use once_cell::sync::Lazy;
10use serde::de::{Deserialize, Deserializer, Error, Visitor};
11use serde::ser::{Serialize, Serializer};
12use serde_derive::{Deserialize, Serialize};
13
14/// Scope related errors
15#[derive(Debug, thiserror::Error)]
16#[non_exhaustive]
17pub enum ScopeError {
18    #[error("Tried to restore cleared scopes, but none were cleared")]
19    NoClearedScopesToRestore,
20}
21
22/// Multiplier on the power of 2 for MatchPower. This is only useful if you compute your own
23/// [`MatchPower`] scores
24///
25/// [`MatchPower`]: struct.MatchPower.html
26pub const ATOM_LEN_BITS: u16 = 3;
27
28/// The global scope repo, exposed in case you want to minimize locking and unlocking.
29///
30/// Ths shouldn't be necessary for you to use. See the [`ScopeRepository`] docs.
31///
32/// [`ScopeRepository`]: struct.ScopeRepository.html
33#[deprecated(
34    since = "5.3.0",
35    note = "\
36    Deprecated in anticipation of removal in the next semver-breaking release under the \
37    justification that it's incredibly niche functionality to expose. If you rely on this \
38    functionality then please express your particular use-case in the github issue: \
39    https://github.com/trishume/syntect/issues/575\
40    "
41)]
42pub static SCOPE_REPO: Lazy<Mutex<ScopeRepository>> =
43    Lazy::new(|| Mutex::new(ScopeRepository::new()));
44
45pub(crate) fn lock_global_scope_repo() -> MutexGuard<'static, ScopeRepository> {
46    #[allow(deprecated)]
47    SCOPE_REPO.lock().unwrap()
48}
49
50/// A hierarchy of atoms with semi-standardized names used to accord semantic information to a
51/// specific piece of text.
52///
53/// These are generally written with the atoms separated by dots, and - by convention - atoms are
54/// all lowercase alphanumeric.
55///
56/// Example scopes: `text.plain`, `punctuation.definition.string.begin.ruby`,
57/// `meta.function.parameters.rust`
58///
59/// `syntect` uses an optimized format for storing these that allows super fast comparison and
60/// determining if one scope is a prefix of another. It also always takes 16 bytes of space. It
61/// accomplishes this by using a global repository to store string values and using bit-packed 16
62/// bit numbers to represent and compare atoms. Like "atoms" or "symbols" in other languages. This
63/// means that while comparing and prefix are fast, extracting a string is relatively slower but
64/// ideally should be very rare.
65#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Copy, Default, Hash)]
66pub struct Scope {
67    a: u64,
68    b: u64,
69}
70
71/// Not all strings are valid scopes
72#[derive(Debug, thiserror::Error)]
73#[non_exhaustive]
74pub enum ParseScopeError {
75    /// Due to a limitation of the current optimized internal representation
76    /// scopes can be at most 8 atoms long
77    #[error("Too long scope. Scopes can be at most 8 atoms long.")]
78    TooLong,
79    /// The internal representation uses 16 bits per atom, so if all scopes ever
80    /// used by the program have more than 2^16-2 atoms, things break
81    #[error("Too many atoms. Max 2^16-2 atoms allowed.")]
82    TooManyAtoms,
83}
84
85/// The structure used to keep track of the mapping between scope atom numbers and their string
86/// names
87///
88/// It is only exposed in case you want to lock [`SCOPE_REPO`] and then allocate a bunch of scopes
89/// at once without thrashing the lock. In general, you should just use [`Scope::new()`].
90///
91/// Only [`Scope`]s created by the same repository have valid comparison results.
92///
93/// [`SCOPE_REPO`]: struct.SCOPE_REPO.html
94/// [`Scope::new()`]: struct.Scope.html#method.new
95/// [`Scope`]: struct.Scope.html
96#[derive(Debug)]
97pub struct ScopeRepository {
98    atoms: Vec<String>,
99    atom_index_map: HashMap<String, usize>,
100}
101
102/// A stack/sequence of scopes for representing hierarchies for a given token of text
103///
104/// This is also used within [`ScopeSelectors`].
105///
106/// In Sublime Text, the scope stack at a given point can be seen by pressing `ctrl+shift+p`. Also
107/// see [the TextMate docs](https://manual.macromates.com/en/scope_selectors).
108///
109/// Example for a JS string inside a script tag in a Rails `ERB` file:
110/// `text.html.ruby text.html.basic source.js.embedded.html string.quoted.double.js`
111///
112/// [`ScopeSelectors`]: ../highlighting/struct.ScopeSelectors.html
113#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
114pub struct ScopeStack {
115    clear_stack: Vec<Vec<Scope>>,
116    pub scopes: Vec<Scope>,
117}
118
119#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize)]
120pub enum ClearAmount {
121    TopN(usize),
122    All,
123}
124
125/// A change to a scope stack
126///
127/// Generally, `Noop` is only used internally and you won't need to worry about getting one back
128/// from calling a public function.
129///
130/// The change from a `ScopeStackOp` can be applied via [`ScopeStack::apply`].
131///
132/// [`ScopeStack::apply`]: struct.ScopeStack.html#method.apply
133#[derive(Debug, Clone, PartialEq, Eq)]
134pub enum ScopeStackOp {
135    Push(Scope),
136    Pop(usize),
137    /// Used for the `clear_scopes` feature
138    Clear(ClearAmount),
139    /// Restores cleared scopes
140    Restore,
141    Noop,
142}
143
144/// Used for [`ScopeStack::apply_with_hook`]
145///
146/// [`ScopeStack::apply_with_hook`]: struct.ScopeStack.html#method.apply_with_hook
147#[derive(Debug, Clone, PartialEq, Eq)]
148pub enum BasicScopeStackOp {
149    Push(Scope),
150    Pop,
151}
152
153fn pack_as_u16s(atoms: &[usize]) -> Result<Scope, ParseScopeError> {
154    let mut res = Scope { a: 0, b: 0 };
155
156    for (i, &n) in atoms.iter().enumerate() {
157        if n >= (u16::MAX as usize) - 2 {
158            return Err(ParseScopeError::TooManyAtoms);
159        }
160        let small = (n + 1) as u64; // +1 since we reserve 0 for unused
161
162        if i < 4 {
163            let shift = (3 - i) * 16;
164            res.a |= small << shift;
165        } else {
166            let shift = (7 - i) * 16;
167            res.b |= small << shift;
168        }
169    }
170    Ok(res)
171}
172
173impl ScopeRepository {
174    fn new() -> ScopeRepository {
175        ScopeRepository {
176            atoms: Vec::new(),
177            atom_index_map: HashMap::new(),
178        }
179    }
180
181    pub fn build(&mut self, s: &str) -> Result<Scope, ParseScopeError> {
182        if s.is_empty() {
183            return Ok(Scope { a: 0, b: 0 });
184        }
185        let parts: Vec<usize> = s
186            .trim_end_matches('.')
187            .split('.')
188            .map(|a| self.atom_to_index(a))
189            .collect();
190        if parts.len() > 8 {
191            return Err(ParseScopeError::TooManyAtoms);
192        }
193        pack_as_u16s(&parts[..])
194    }
195
196    pub fn to_string(&self, scope: Scope) -> String {
197        let mut s = String::new();
198        for i in 0..8 {
199            let atom_number = scope.atom_at(i);
200            // println!("atom {} of {:x}-{:x} = {:x}",
201            //     i, scope.a, scope.b, atom_number);
202            if atom_number == 0 {
203                break;
204            }
205            if i != 0 {
206                s.push('.');
207            }
208            s.push_str(self.atom_str(atom_number));
209        }
210        s
211    }
212
213    fn atom_to_index(&mut self, atom: &str) -> usize {
214        if let Some(index) = self.atom_index_map.get(atom) {
215            return *index;
216        }
217
218        self.atoms.push(atom.to_owned());
219        let index = self.atoms.len() - 1;
220        self.atom_index_map.insert(atom.to_owned(), index);
221
222        index
223    }
224
225    /// Return the string for an atom number returned by [`Scope::atom_at`]
226    ///
227    /// [`Scope::atom_at`]: struct.Scope.html#method.atom_at
228    pub fn atom_str(&self, atom_number: u16) -> &str {
229        &self.atoms[(atom_number - 1) as usize]
230    }
231}
232
233impl Scope {
234    /// Parses a `Scope` from a series of atoms separated by dot (`.`) characters
235    ///
236    /// Example: `Scope::new("meta.rails.controller")`
237    pub fn new(s: &str) -> Result<Scope, ParseScopeError> {
238        let mut repo = lock_global_scope_repo();
239        repo.build(s.trim())
240    }
241
242    /// Gets the atom number at a given index.
243    ///
244    /// I can't think of any reason you'd find this useful. It is used internally for turning a
245    /// scope back into a string.
246    pub fn atom_at(self, index: usize) -> u16 {
247        #[allow(clippy::panic)]
248        // The below panic is too much of an edge-case for it to be worth propagating
249        let shifted = if index < 4 {
250            self.a >> ((3 - index) * 16)
251        } else if index < 8 {
252            self.b >> ((7 - index) * 16)
253        } else {
254            panic!("atom index out of bounds {:?}", index);
255        };
256        (shifted & 0xFFFF) as u16
257    }
258
259    #[inline]
260    fn missing_atoms(self) -> u32 {
261        let trail = if self.b == 0 {
262            self.a.trailing_zeros() + 64
263        } else {
264            self.b.trailing_zeros()
265        };
266        trail / 16
267    }
268
269    /// Returns the number of atoms in the scope
270    #[inline(always)]
271    pub fn len(self) -> u32 {
272        8 - self.missing_atoms()
273    }
274
275    pub fn is_empty(self) -> bool {
276        self.len() == 0
277    }
278
279    /// Returns a string representation of this scope
280    ///
281    /// This requires locking a global repo and shouldn't be done frequently.
282    pub fn build_string(self) -> String {
283        let repo = lock_global_scope_repo();
284        repo.to_string(self)
285    }
286
287    /// Tests if this scope is a prefix of another scope. Note that the empty scope is always a
288    /// prefix.
289    ///
290    /// This operation uses bitwise operations and is very fast
291    /// # Examples
292    ///
293    /// ```
294    /// use syntect::parsing::Scope;
295    /// assert!( Scope::new("string").unwrap()
296    ///         .is_prefix_of(Scope::new("string.quoted").unwrap()));
297    /// assert!( Scope::new("string.quoted").unwrap()
298    ///         .is_prefix_of(Scope::new("string.quoted").unwrap()));
299    /// assert!( Scope::new("").unwrap()
300    ///         .is_prefix_of(Scope::new("meta.rails.controller").unwrap()));
301    /// assert!(!Scope::new("source.php").unwrap()
302    ///         .is_prefix_of(Scope::new("source").unwrap()));
303    /// assert!(!Scope::new("source.php").unwrap()
304    ///         .is_prefix_of(Scope::new("source.ruby").unwrap()));
305    /// assert!(!Scope::new("meta.php").unwrap()
306    ///         .is_prefix_of(Scope::new("source.php").unwrap()));
307    /// assert!(!Scope::new("meta.php").unwrap()
308    ///         .is_prefix_of(Scope::new("source.php.wow").unwrap()));
309    /// ```
310    pub fn is_prefix_of(self, s: Scope) -> bool {
311        let pref_missing = self.missing_atoms();
312
313        // TODO: test optimization - use checked shl and then mult carry flag as int by -1
314        let mask: (u64, u64) = if pref_missing == 8 {
315            (0, 0)
316        } else if pref_missing == 4 {
317            (u64::MAX, 0)
318        } else if pref_missing > 4 {
319            (u64::MAX << ((pref_missing - 4) * 16), 0)
320        } else {
321            (u64::MAX, u64::MAX << (pref_missing * 16))
322        };
323
324        // xor to find the difference
325        let ax = (self.a ^ s.a) & mask.0;
326        let bx = (self.b ^ s.b) & mask.1;
327        // println!("{:x}-{:x} is_pref {:x}-{:x}: missing {} mask {:x}-{:x} xor {:x}-{:x}",
328        //     self.a, self.b, s.a, s.b, pref_missing, mask.0, mask.1, ax, bx);
329
330        ax == 0 && bx == 0
331    }
332}
333
334impl FromStr for Scope {
335    type Err = ParseScopeError;
336
337    fn from_str(s: &str) -> Result<Scope, ParseScopeError> {
338        Scope::new(s)
339    }
340}
341
342impl fmt::Display for Scope {
343    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
344        let s = self.build_string();
345        write!(f, "{}", s)
346    }
347}
348
349impl fmt::Debug for Scope {
350    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
351        let s = self.build_string();
352        write!(f, "<{}>", s)
353    }
354}
355
356impl Serialize for Scope {
357    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
358    where
359        S: Serializer,
360    {
361        let s = self.build_string();
362        serializer.serialize_str(&s)
363    }
364}
365
366impl<'de> Deserialize<'de> for Scope {
367    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
368    where
369        D: Deserializer<'de>,
370    {
371        struct ScopeVisitor;
372
373        impl Visitor<'_> for ScopeVisitor {
374            type Value = Scope;
375
376            fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
377                formatter.write_str("a string")
378            }
379
380            fn visit_str<E>(self, v: &str) -> Result<Scope, E>
381            where
382                E: Error,
383            {
384                Scope::new(v).map_err(|e| Error::custom(format!("Invalid scope: {:?}", e)))
385            }
386        }
387
388        deserializer.deserialize_str(ScopeVisitor)
389    }
390}
391
392/// Wrapper to get around the fact Rust `f64` doesn't implement `Ord` and there is no non-NaN
393/// float type
394#[derive(Debug, Copy, Clone, PartialOrd, PartialEq)]
395pub struct MatchPower(pub f64);
396
397impl Eq for MatchPower {}
398
399#[allow(clippy::derive_ord_xor_partial_ord)] // The code works, so let's keep using it
400impl Ord for MatchPower {
401    fn cmp(&self, other: &Self) -> Ordering {
402        self.partial_cmp(other).unwrap()
403    }
404}
405
406impl ScopeStack {
407    /// Bytes in owned heap capacities, including cleared scopes but excluding
408    /// this value, allocator overhead, and the shared scope repository.
409    /// Returns `None` on overflow.
410    pub fn owned_heap_bytes(&self) -> Option<usize> {
411        let bytes = self
412            .scopes
413            .capacity()
414            .checked_mul(mem::size_of::<Scope>())?
415            .checked_add(
416                self.clear_stack
417                    .capacity()
418                    .checked_mul(mem::size_of::<Vec<Scope>>())?,
419            )?;
420        self.clear_stack.iter().try_fold(bytes, |bytes, scopes| {
421            bytes.checked_add(scopes.capacity().checked_mul(mem::size_of::<Scope>())?)
422        })
423    }
424
425    pub fn new() -> ScopeStack {
426        ScopeStack {
427            clear_stack: Vec::new(),
428            scopes: Vec::new(),
429        }
430    }
431
432    /// Note: creating a ScopeStack with this doesn't contain information
433    /// on what to do when `clear_scopes` contexts end.
434    pub fn from_vec(v: Vec<Scope>) -> ScopeStack {
435        ScopeStack {
436            clear_stack: Vec::new(),
437            scopes: v,
438        }
439    }
440
441    #[inline]
442    pub fn push(&mut self, s: Scope) {
443        self.scopes.push(s);
444    }
445
446    #[inline]
447    pub fn pop(&mut self) {
448        self.scopes.pop();
449    }
450
451    /// Modifies this stack according to the operation given
452    ///
453    /// Use this to create a stack from a `Vec` of changes given by the parser.
454    pub fn apply(&mut self, op: &ScopeStackOp) -> Result<(), ScopeError> {
455        self.apply_with_hook(op, |_, _| {})
456    }
457
458    /// Modifies this stack according to the operation given and calls the hook for each basic operation.
459    ///
460    /// Like [`apply`] but calls `hook` for every basic modification (as defined by
461    /// [`BasicScopeStackOp`]). Use this to do things only when the scope stack changes.
462    ///
463    /// [`apply`]: #method.apply
464    /// [`BasicScopeStackOp`]: enum.BasicScopeStackOp.html
465    #[inline]
466    pub fn apply_with_hook<F>(&mut self, op: &ScopeStackOp, mut hook: F) -> Result<(), ScopeError>
467    where
468        F: FnMut(BasicScopeStackOp, &[Scope]),
469    {
470        match *op {
471            ScopeStackOp::Push(scope) => {
472                self.scopes.push(scope);
473                hook(BasicScopeStackOp::Push(scope), self.as_slice());
474            }
475            ScopeStackOp::Pop(count) => {
476                for _ in 0..count {
477                    self.scopes.pop();
478                    hook(BasicScopeStackOp::Pop, self.as_slice());
479                }
480            }
481            ScopeStackOp::Clear(amount) => {
482                let cleared = match amount {
483                    ClearAmount::TopN(n) => {
484                        // don't try to clear more scopes than are on the stack
485                        let to_leave = self.scopes.len() - min(n, self.scopes.len());
486                        self.scopes.split_off(to_leave)
487                    }
488                    ClearAmount::All => {
489                        let mut cleared = Vec::new();
490                        mem::swap(&mut cleared, &mut self.scopes);
491                        cleared
492                    }
493                };
494                let clear_amount = cleared.len();
495                self.clear_stack.push(cleared);
496                for _ in 0..clear_amount {
497                    hook(BasicScopeStackOp::Pop, self.as_slice());
498                }
499            }
500            ScopeStackOp::Restore => match self.clear_stack.pop() {
501                Some(ref mut to_push) => {
502                    for s in to_push {
503                        self.scopes.push(*s);
504                        hook(BasicScopeStackOp::Push(*s), self.as_slice());
505                    }
506                }
507                None => return Err(ScopeError::NoClearedScopesToRestore),
508            },
509            ScopeStackOp::Noop => (),
510        }
511
512        Ok(())
513    }
514
515    /// Prints out each scope in the stack separated by spaces
516    /// and then a newline. Top of the stack at the end.
517    pub fn debug_print(&self, repo: &ScopeRepository) {
518        for s in &self.scopes {
519            print!("{} ", repo.to_string(*s));
520        }
521        println!();
522    }
523
524    /// Returns the bottom `n` elements of the stack.
525    ///
526    /// Equivalent to `&scopes[0..n]` on a `Vec`
527    pub fn bottom_n(&self, n: usize) -> &[Scope] {
528        &self.scopes[0..n]
529    }
530
531    /// Return a slice of the scopes in this stack
532    #[inline]
533    pub fn as_slice(&self) -> &[Scope] {
534        &self.scopes[..]
535    }
536
537    /// Return the height/length of this stack
538    #[inline]
539    pub fn len(&self) -> usize {
540        self.scopes.len()
541    }
542
543    #[inline]
544    pub fn is_empty(&self) -> bool {
545        self.len() == 0
546    }
547
548    /// Checks if this stack as a selector matches the given stack, returning the match score if so
549    ///
550    /// Higher match scores indicate stronger matches. Scores are ordered according to the rules
551    /// found at [https://manual.macromates.com/en/scope_selectors](https://manual.macromates.com/en/scope_selectors)
552    ///
553    /// It accomplishes this ordering through some floating point math ensuring deeper and longer
554    /// matches matter. Unfortunately it is only guaranteed to return perfectly accurate results up
555    /// to stack depths of 17, but it should be reasonably good even afterwards. TextMate has the
556    /// exact same limitation, dunno about Sublime Text.
557    ///
558    /// # Examples
559    /// ```
560    /// use syntect::parsing::{ScopeStack, MatchPower};
561    /// use std::str::FromStr;
562    /// assert_eq!(ScopeStack::from_str("a.b c e.f").unwrap()
563    ///     .does_match(ScopeStack::from_str("a.b c.d e.f.g").unwrap().as_slice()),
564    ///     Some(MatchPower(0o212u64 as f64)));
565    /// assert_eq!(ScopeStack::from_str("a c.d.e").unwrap()
566    ///     .does_match(ScopeStack::from_str("a.b c.d e.f.g").unwrap().as_slice()),
567    ///     None);
568    /// ```
569    pub fn does_match(&self, stack: &[Scope]) -> Option<MatchPower> {
570        let mut sel_index: usize = 0;
571        let mut score: f64 = 0.0;
572        for (i, scope) in stack.iter().enumerate() {
573            let sel_scope = self.scopes[sel_index];
574            if sel_scope.is_prefix_of(*scope) {
575                let len = sel_scope.len();
576                // equivalent to score |= len << (ATOM_LEN_BITS*i) on a large unsigned
577                score += f64::from(len) * f64::from(ATOM_LEN_BITS * (i as u16)).exp2();
578                sel_index += 1;
579                if sel_index >= self.scopes.len() {
580                    return Some(MatchPower(score));
581                }
582            }
583        }
584        None
585    }
586}
587
588impl FromStr for ScopeStack {
589    type Err = ParseScopeError;
590
591    /// Parses a scope stack from a whitespace separated list of scopes.
592    fn from_str(s: &str) -> Result<ScopeStack, ParseScopeError> {
593        let mut scopes = Vec::new();
594        for name in s.split_whitespace() {
595            scopes.push(Scope::from_str(name)?)
596        }
597        Ok(ScopeStack::from_vec(scopes))
598    }
599}
600
601impl fmt::Display for ScopeStack {
602    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
603        for s in &self.scopes {
604            write!(f, "{} ", s)?;
605        }
606        Ok(())
607    }
608}
609
610#[cfg(test)]
611mod tests {
612    use super::*;
613
614    #[test]
615    fn owned_heap_preserves_nested_clears_through_clone() {
616        let source = Scope::new("source.test").unwrap();
617        let outer = Scope::new("meta.outer.test").unwrap();
618        let embedded = Scope::new("source.embedded.test").unwrap();
619        let string = Scope::new("string.quoted.test").unwrap();
620        let mut stack = ScopeStack::from_vec(vec![source, outer]);
621        stack.apply(&ScopeStackOp::Clear(ClearAmount::All)).unwrap();
622        assert!(stack.scopes.is_empty());
623        assert!(stack.owned_heap_bytes().unwrap() >= 2 * mem::size_of::<Scope>());
624        stack.push(embedded);
625        stack.push(string);
626        stack
627            .apply(&ScopeStackOp::Clear(ClearAmount::TopN(1)))
628            .unwrap();
629        let mut restored = stack.clone();
630        assert_eq!(restored, stack);
631        assert_eq!(restored.clear_stack.len(), 2);
632        let minimum = 4 * mem::size_of::<Scope>() + 2 * mem::size_of::<Vec<Scope>>();
633        assert!(restored.owned_heap_bytes().unwrap() >= minimum);
634        for operation in [
635            ScopeStackOp::Restore,
636            ScopeStackOp::Pop(2),
637            ScopeStackOp::Restore,
638        ] {
639            stack.apply(&operation).unwrap();
640            restored.apply(&operation).unwrap();
641            assert_eq!(restored, stack);
642        }
643        assert_eq!(restored.scopes, vec![source, outer]);
644        assert!(restored.clear_stack.is_empty());
645        assert_eq!(
646            restored.owned_heap_bytes(),
647            Some(
648                restored.scopes.capacity() * mem::size_of::<Scope>()
649                    + restored.clear_stack.capacity() * mem::size_of::<Vec<Scope>>()
650            )
651        );
652    }
653
654    #[test]
655    fn misc() {
656        // use std::mem;
657        // use std::rc::{Rc};
658        // use scope::*;
659        // assert_eq!(8, mem::size_of::<Rc<Scope>>());
660        // assert_eq!(Scope::new("source.php"), Scope::new("source.php"));
661    }
662
663    #[test]
664    fn repo_works() {
665        let mut repo = ScopeRepository::new();
666        assert_eq!(
667            repo.build("source.php").unwrap(),
668            repo.build("source.php").unwrap()
669        );
670        assert_eq!(
671            repo.build("source.php.wow.hi.bob.troll.clock.5").unwrap(),
672            repo.build("source.php.wow.hi.bob.troll.clock.5").unwrap()
673        );
674        assert_eq!(repo.build("").unwrap(), repo.build("").unwrap());
675        let s1 = repo.build("").unwrap();
676        assert_eq!(repo.to_string(s1), "");
677        let s2 = repo.build("source.php.wow").unwrap();
678        assert_eq!(repo.to_string(s2), "source.php.wow");
679        assert!(repo.build("source.php").unwrap() != repo.build("source.perl").unwrap());
680        assert!(repo.build("source.php").unwrap() != repo.build("source.php.wagon").unwrap());
681        assert_eq!(
682            repo.build("comment.line.").unwrap(),
683            repo.build("comment.line").unwrap()
684        );
685    }
686
687    #[test]
688    fn global_repo_works() {
689        use std::str::FromStr;
690        assert_eq!(
691            Scope::new("source.php").unwrap(),
692            Scope::new("source.php").unwrap()
693        );
694        assert!(Scope::from_str("1.2.3.4.5.6.7.8").is_ok());
695        assert!(Scope::from_str("1.2.3.4.5.6.7.8.9").is_err());
696    }
697
698    #[test]
699    fn prefixes_work() {
700        assert!(Scope::new("1.2.3.4.5.6.7.8")
701            .unwrap()
702            .is_prefix_of(Scope::new("1.2.3.4.5.6.7.8").unwrap()));
703        assert!(Scope::new("1.2.3.4.5.6")
704            .unwrap()
705            .is_prefix_of(Scope::new("1.2.3.4.5.6.7.8").unwrap()));
706        assert!(Scope::new("1.2.3.4")
707            .unwrap()
708            .is_prefix_of(Scope::new("1.2.3.4.5.6.7.8").unwrap()));
709        assert!(!Scope::new("1.2.3.4.5.6.a")
710            .unwrap()
711            .is_prefix_of(Scope::new("1.2.3.4.5.6.7.8").unwrap()));
712        assert!(!Scope::new("1.2.a.4.5.6.7")
713            .unwrap()
714            .is_prefix_of(Scope::new("1.2.3.4.5.6.7.8").unwrap()));
715        assert!(!Scope::new("1.2.a.4.5.6.7")
716            .unwrap()
717            .is_prefix_of(Scope::new("1.2.3.4.5").unwrap()));
718        assert!(!Scope::new("1.2.a")
719            .unwrap()
720            .is_prefix_of(Scope::new("1.2.3.4.5.6.7.8").unwrap()));
721    }
722
723    #[test]
724    fn matching_works() {
725        use std::str::FromStr;
726        assert_eq!(
727            ScopeStack::from_str("string")
728                .unwrap()
729                .does_match(ScopeStack::from_str("string.quoted").unwrap().as_slice()),
730            Some(MatchPower(0o1u64 as f64))
731        );
732        assert_eq!(
733            ScopeStack::from_str("source")
734                .unwrap()
735                .does_match(ScopeStack::from_str("string.quoted").unwrap().as_slice()),
736            None
737        );
738        assert_eq!(
739            ScopeStack::from_str("a.b e.f")
740                .unwrap()
741                .does_match(ScopeStack::from_str("a.b c.d e.f.g").unwrap().as_slice()),
742            Some(MatchPower(0o202u64 as f64))
743        );
744        assert_eq!(
745            ScopeStack::from_str("c e.f")
746                .unwrap()
747                .does_match(ScopeStack::from_str("a.b c.d e.f.g").unwrap().as_slice()),
748            Some(MatchPower(0o210u64 as f64))
749        );
750        assert_eq!(
751            ScopeStack::from_str("c.d e.f")
752                .unwrap()
753                .does_match(ScopeStack::from_str("a.b c.d e.f.g").unwrap().as_slice()),
754            Some(MatchPower(0o220u64 as f64))
755        );
756        assert_eq!(
757            ScopeStack::from_str("a.b c e.f")
758                .unwrap()
759                .does_match(ScopeStack::from_str("a.b c.d e.f.g").unwrap().as_slice()),
760            Some(MatchPower(0o212u64 as f64))
761        );
762        assert_eq!(
763            ScopeStack::from_str("a c.d")
764                .unwrap()
765                .does_match(ScopeStack::from_str("a.b c.d e.f.g").unwrap().as_slice()),
766            Some(MatchPower(0o021u64 as f64))
767        );
768        assert_eq!(
769            ScopeStack::from_str("a c.d.e")
770                .unwrap()
771                .does_match(ScopeStack::from_str("a.b c.d e.f.g").unwrap().as_slice()),
772            None
773        );
774    }
775}