Skip to main content

syntect/highlighting/
highlighter.rs

1//! Iterators and data structures for transforming parsing information into styled text.
2
3// Code based on https://github.com/defuz/sublimate/blob/master/src/core/syntax/highlighter.rs
4// released under the MIT license by @defuz
5
6use std::iter::Iterator;
7use std::ops::Range;
8
9use super::selector::ScopeSelector;
10use super::style::{Color, FontStyle, Style, StyleModifier};
11use super::theme::{Theme, ThemeItem};
12use crate::parsing::{
13    BasicScopeStackOp, MatchPower, Scope, ScopeStack, ScopeStackOp, ATOM_LEN_BITS,
14};
15
16/// Basically a wrapper around a [`Theme`] preparing it to be used for highlighting.
17///
18/// This is part of the API to preserve the possibility of caching matches of the
19/// selectors of the theme on various scope paths or setting up some kind of
20/// accelerator structure.
21///
22/// So for now this does very little but eventually if you keep it around between
23/// highlighting runs it will preserve its cache.
24///
25/// [`Theme`]: struct.Theme.html
26#[derive(Debug)]
27pub struct Highlighter<'a> {
28    theme: &'a Theme,
29    /// Cache of the selectors in the theme that are only one scope
30    /// In most themes this is the majority, hence the usefullness
31    single_selectors: Vec<(Scope, StyleModifier)>,
32    multi_selectors: Vec<(ScopeSelector, StyleModifier)>,
33    // TODO single_cache: HashMap<Scope, StyleModifier, BuildHasherDefault<FnvHasher>>,
34}
35
36/// Keeps a stack of scopes and styles as state between highlighting different lines.
37///
38/// If you are highlighting an entire file you create one of these at the start and use it
39/// all the way to the end.
40///
41/// # Caching
42///
43/// One reason this is exposed is that since it implements `Clone` you can actually cache these
44/// (probably along with a [`ParseState`]) and only re-start highlighting from the point of a
45/// change. You could also do something fancy like only highlight a bit past the end of a user's
46/// screen and resume highlighting when they scroll down on large files.
47///
48/// Alternatively you can save space by caching only the `path` field of this struct then re-create
49/// the `HighlightState` when needed by passing that stack as the `initial_stack` parameter to the
50/// [`new`] method. This takes less space but a small amount of time to re-create the style stack.
51///
52/// **Note:** Caching is for advanced users who have tons of time to maximize performance or want to
53/// do so eventually. It is not recommended that you try caching the first time you implement
54/// highlighting.
55///
56/// [`ParseState`]: ../parsing/struct.ParseState.html
57/// [`new`]: #method.new
58#[derive(Debug, Clone, PartialEq, Eq)]
59pub struct HighlightState {
60    styles: Vec<Style>,
61    single_caches: Vec<ScoredStyle>,
62    pub path: ScopeStack,
63}
64
65/// Highlights a line of parsed code given a [`HighlightState`] and line of changes from the parser.
66///
67/// Yields the [`Style`], the text and well as the `Range` of the text in the source string.
68///
69/// It splits a line of text into different pieces each with a [`Style`]
70///
71/// [`HighlightState`]: struct.HighlightState.html
72/// [`Style`]: struct.Style.html
73#[derive(Debug)]
74pub struct RangedHighlightIterator<'a, 'b> {
75    index: usize,
76    pos: usize,
77    changes: &'a [(usize, ScopeStackOp)],
78    text: &'b str,
79    highlighter: &'a Highlighter<'a>,
80    state: &'a mut HighlightState,
81}
82
83/// Highlights a line of parsed code given a [`HighlightState`] and line of changes from the parser.
84///
85/// This is a backwards compatible shim on top of the [`RangedHighlightIterator`] which only
86/// yields the [`Style`] and the text of the token, not the range.
87///
88/// It splits a line of text into different pieces each with a [`Style`].
89///
90/// [`HighlightState`]: struct.HighlightState.html
91/// [`RangedHighlightIterator`]: struct.RangedHighlightIterator.html
92/// [`Style`]: struct.Style.html
93#[derive(Debug)]
94pub struct HighlightIterator<'a, 'b> {
95    ranged_iterator: RangedHighlightIterator<'a, 'b>,
96}
97
98impl HighlightState {
99    /// Owned vector capacity, excluding the inline value, allocator overhead, and shared data.
100    #[cfg(feature = "owner-census")]
101    pub fn owned_heap_bytes(&self) -> Option<usize> {
102        self.styles
103            .capacity()
104            .checked_mul(std::mem::size_of::<Style>())?
105            .checked_add(
106                self.single_caches
107                    .capacity()
108                    .checked_mul(std::mem::size_of::<ScoredStyle>())?,
109            )?
110            .checked_add(self.path.owned_heap_bytes()?)
111    }
112
113    /// Note that the [`Highlighter`] is not stored; it is used to construct the initial stack
114    /// of styles.
115    ///
116    /// Most of the time you'll want to pass an empty stack as `initial_stack`, but see the docs for
117    /// [`HighlightState`] for a discussion of advanced caching use cases.
118    ///
119    /// [`Highlighter`]: struct.Highlighter.html
120    /// [`HighlightState`]: struct.HighlightState.html
121    pub fn new(highlighter: &Highlighter<'_>, initial_stack: ScopeStack) -> HighlightState {
122        let mut styles = vec![highlighter.get_default()];
123        let mut single_caches = vec![ScoredStyle::from_style(styles[0])];
124        for i in 0..initial_stack.len() {
125            let prefix = initial_stack.bottom_n(i + 1);
126            let new_cache = highlighter.update_single_cache_for_push(&single_caches[i], prefix);
127            styles.push(highlighter.finalize_style_with_multis(&new_cache, prefix));
128            single_caches.push(new_cache);
129        }
130
131        HighlightState {
132            styles,
133            single_caches,
134            path: initial_stack,
135        }
136    }
137}
138
139impl<'a, 'b> RangedHighlightIterator<'a, 'b> {
140    pub fn new(
141        state: &'a mut HighlightState,
142        changes: &'a [(usize, ScopeStackOp)],
143        text: &'b str,
144        highlighter: &'a Highlighter<'_>,
145    ) -> RangedHighlightIterator<'a, 'b> {
146        RangedHighlightIterator {
147            index: 0,
148            pos: 0,
149            changes,
150            text,
151            highlighter,
152            state,
153        }
154    }
155}
156
157impl<'b> Iterator for RangedHighlightIterator<'_, 'b> {
158    type Item = (Style, &'b str, Range<usize>);
159
160    /// Yields the next token of text and the associated `Style` to render that text with.
161    /// the concatenation of the strings in each token will make the original string.
162    fn next(&mut self) -> Option<(Style, &'b str, Range<usize>)> {
163        if self.pos == self.text.len() && self.index >= self.changes.len() {
164            return None;
165        }
166        let (end, command) = if self.index < self.changes.len() {
167            self.changes[self.index].clone()
168        } else {
169            (self.text.len(), ScopeStackOp::Noop)
170        };
171        // println!("{} - {:?}   {}:{}", self.index, self.pos, self.state.path.len(), self.state.styles.len());
172        let style = *self.state.styles.last().unwrap_or(&Style::default());
173        let text = &self.text[self.pos..end];
174        let range = Range {
175            start: self.pos,
176            end,
177        };
178        {
179            // closures mess with the borrow checker's ability to see different struct fields
180            let m_path = &mut self.state.path;
181            let m_styles = &mut self.state.styles;
182            let m_caches = &mut self.state.single_caches;
183            let highlighter = &self.highlighter;
184            m_path
185                .apply_with_hook(&command, |op, cur_stack| {
186                    // println!("{:?} - {:?}", op, cur_stack);
187                    match op {
188                        BasicScopeStackOp::Push(_) => {
189                            // we can push multiple times so this might have changed
190                            let new_cache = {
191                                if let Some(prev_cache) = m_caches.last() {
192                                    highlighter.update_single_cache_for_push(prev_cache, cur_stack)
193                                } else {
194                                    highlighter.update_single_cache_for_push(
195                                        &ScoredStyle::from_style(highlighter.get_default()),
196                                        cur_stack,
197                                    )
198                                }
199                            };
200                            m_styles.push(
201                                highlighter.finalize_style_with_multis(&new_cache, cur_stack),
202                            );
203                            m_caches.push(new_cache);
204                        }
205                        BasicScopeStackOp::Pop => {
206                            m_styles.pop();
207                            m_caches.pop();
208                        }
209                    }
210                })
211                .ok()?;
212        }
213        self.pos = end;
214        self.index += 1;
215        if text.is_empty() {
216            self.next()
217        } else {
218            Some((style, text, range))
219        }
220    }
221}
222impl<'a, 'b> HighlightIterator<'a, 'b> {
223    pub fn new(
224        state: &'a mut HighlightState,
225        changes: &'a [(usize, ScopeStackOp)],
226        text: &'b str,
227        highlighter: &'a Highlighter<'_>,
228    ) -> HighlightIterator<'a, 'b> {
229        HighlightIterator {
230            ranged_iterator: RangedHighlightIterator {
231                index: 0,
232                pos: 0,
233                changes,
234                text,
235                highlighter,
236                state,
237            },
238        }
239    }
240}
241
242impl<'b> Iterator for HighlightIterator<'_, 'b> {
243    type Item = (Style, &'b str);
244
245    /// Yields the next token of text and the associated `Style` to render that text with.
246    /// the concatenation of the strings in each token will make the original string.
247    fn next(&mut self) -> Option<(Style, &'b str)> {
248        self.ranged_iterator.next().map(|e| (e.0, e.1))
249    }
250}
251
252#[derive(Debug, Clone, PartialEq, Eq)]
253pub struct ScoredStyle {
254    pub foreground: (MatchPower, Color),
255    pub background: (MatchPower, Color),
256    pub font_style: (MatchPower, FontStyle),
257}
258
259#[inline]
260fn update_scored<T: Clone>(scored: &mut (MatchPower, T), update: &Option<T>, score: MatchPower) {
261    if score > scored.0 {
262        if let Some(u) = update {
263            scored.0 = score;
264            scored.1 = u.clone();
265        }
266    }
267}
268
269impl ScoredStyle {
270    fn apply(&mut self, other: &StyleModifier, score: MatchPower) {
271        update_scored(&mut self.foreground, &other.foreground, score);
272        update_scored(&mut self.background, &other.background, score);
273        update_scored(&mut self.font_style, &other.font_style, score);
274    }
275
276    fn to_style(&self) -> Style {
277        Style {
278            foreground: self.foreground.1,
279            background: self.background.1,
280            font_style: self.font_style.1,
281        }
282    }
283
284    fn from_style(style: Style) -> ScoredStyle {
285        ScoredStyle {
286            foreground: (MatchPower(-1.0), style.foreground),
287            background: (MatchPower(-1.0), style.background),
288            font_style: (MatchPower(-1.0), style.font_style),
289        }
290    }
291}
292
293impl<'a> Highlighter<'a> {
294    pub fn new(theme: &'a Theme) -> Highlighter<'a> {
295        let mut single_selectors = Vec::new();
296        let mut multi_selectors = Vec::new();
297        for item in &theme.scopes {
298            for sel in &item.scope.selectors {
299                if let Some(scope) = sel.extract_single_scope() {
300                    single_selectors.push((scope, item.style));
301                } else {
302                    multi_selectors.push((sel.clone(), item.style));
303                }
304            }
305        }
306        // So that deeper matching selectors get checked first
307        single_selectors.sort_by(|a, b| b.0.len().cmp(&a.0.len()));
308
309        Highlighter {
310            theme,
311            single_selectors,
312            multi_selectors,
313        }
314    }
315
316    /// The default style in the absence of any matched rules.
317    /// Basically what plain text gets highlighted as.
318    pub fn get_default(&self) -> Style {
319        Style {
320            foreground: self.theme.settings.foreground.unwrap_or(Color::BLACK),
321            background: self.theme.settings.background.unwrap_or(Color::WHITE),
322            font_style: FontStyle::empty(),
323        }
324    }
325
326    fn update_single_cache_for_push(&self, cur: &ScoredStyle, path: &[Scope]) -> ScoredStyle {
327        let mut new_style = cur.clone();
328
329        let last_scope = path[path.len() - 1];
330        for &(scope, ref modif) in self
331            .single_selectors
332            .iter()
333            .filter(|a| a.0.is_prefix_of(last_scope))
334        {
335            let single_score = f64::from(scope.len())
336                * f64::from(ATOM_LEN_BITS * ((path.len() - 1) as u16)).exp2();
337            new_style.apply(modif, MatchPower(single_score));
338        }
339
340        new_style
341    }
342
343    fn finalize_style_with_multis(&self, cur: &ScoredStyle, path: &[Scope]) -> Style {
344        let mut new_style = cur.clone();
345
346        let mult_iter = self
347            .multi_selectors
348            .iter()
349            .filter_map(|(sel, style)| sel.does_match(path).map(|score| (score, style)));
350        for (score, modif) in mult_iter {
351            new_style.apply(modif, score);
352        }
353
354        new_style.to_style()
355    }
356
357    /// Returns the fully resolved style for the given stack.
358    ///
359    /// This operation is convenient but expensive. For reasonable performance,
360    /// the caller should be caching results.
361    pub fn style_for_stack(&self, stack: &[Scope]) -> Style {
362        let mut single_cache = ScoredStyle::from_style(self.get_default());
363        for i in 0..stack.len() {
364            single_cache = self.update_single_cache_for_push(&single_cache, &stack[0..i + 1]);
365        }
366        self.finalize_style_with_multis(&single_cache, stack)
367    }
368
369    /// Returns a [`StyleModifier`] which, if applied to the default style,
370    /// would generate the fully resolved style for this stack.
371    ///
372    /// This is made available to applications that are using syntect styles
373    /// in combination with style information from other sources.
374    ///
375    /// This operation is convenient but expensive. For reasonable performance,
376    /// the caller should be caching results. It's likely slower than [`style_for_stack`].
377    ///
378    /// [`StyleModifier`]: struct.StyleModifier.html
379    /// [`style_for_stack`]: #method.style_for_stack
380    pub fn style_mod_for_stack(&self, path: &[Scope]) -> StyleModifier {
381        let mut matching_items: Vec<(MatchPower, &ThemeItem)> = self
382            .theme
383            .scopes
384            .iter()
385            .filter_map(|item| item.scope.does_match(path).map(|score| (score, item)))
386            .collect();
387        matching_items.sort_by_key(|&(score, _)| score);
388        let sorted = matching_items.iter().map(|(_, item)| item);
389
390        let mut modifier = StyleModifier {
391            background: None,
392            foreground: None,
393            font_style: None,
394        };
395        for item in sorted {
396            modifier = modifier.apply(item.style);
397        }
398        modifier
399    }
400}
401
402#[cfg(all(feature = "default-syntaxes", feature = "default-themes"))]
403#[cfg(test)]
404mod tests {
405    use super::*;
406    use crate::highlighting::{Color, FontStyle, Style, ThemeSet};
407    use crate::parsing::{ParseState, ScopeStack};
408    use crate::utils::testdata;
409
410    #[test]
411    fn can_parse() {
412        let ps = &*testdata::PACKAGES_SYN_SET;
413        let mut state = {
414            let syntax = ps.find_syntax_by_name("Ruby on Rails").unwrap();
415            ParseState::new(syntax)
416        };
417        let ts = ThemeSet::load_defaults();
418        let highlighter = Highlighter::new(&ts.themes["base16-ocean.dark"]);
419
420        let mut highlight_state = HighlightState::new(&highlighter, ScopeStack::new());
421        let line = "module Bob::Wow::Troll::Five; 5; end";
422        let ops = state.parse_line(line, &ps).expect("#[cfg(test)]");
423        let iter = HighlightIterator::new(&mut highlight_state, &ops[..], line, &highlighter);
424        let regions: Vec<(Style, &str)> = iter.collect();
425        // println!("{:#?}", regions);
426        assert_eq!(
427            regions[11],
428            (
429                Style {
430                    foreground: Color {
431                        r: 208,
432                        g: 135,
433                        b: 112,
434                        a: 0xFF,
435                    },
436                    background: Color {
437                        r: 43,
438                        g: 48,
439                        b: 59,
440                        a: 0xFF,
441                    },
442                    font_style: FontStyle::empty(),
443                },
444                "5"
445            )
446        );
447    }
448
449    #[test]
450    fn can_parse_with_highlight_state_from_cache() {
451        let ps = &*testdata::PACKAGES_SYN_SET;
452        let mut state = {
453            let syntax = ps
454                .find_syntax_by_scope(Scope::new("source.python").unwrap())
455                .unwrap();
456            ParseState::new(syntax)
457        };
458        let ts = ThemeSet::load_defaults();
459        let highlighter = Highlighter::new(&ts.themes["base16-ocean.dark"]);
460
461        // We start by parsing a python multiline-comment: """
462        let mut highlight_state = HighlightState::new(&highlighter, ScopeStack::new());
463        let line = r#"""""#;
464        let ops = state.parse_line(line, &ps).expect("#[cfg(test)]");
465        let iter = HighlightIterator::new(&mut highlight_state, &ops[..], line, &highlighter);
466        assert_eq!(1, iter.count());
467        let path = highlight_state.path;
468
469        // We then parse the next line with a highlight state built from the previous state
470        let mut highlight_state = HighlightState::new(&highlighter, path);
471        let line = "multiline comment";
472        let ops = state.parse_line(line, &ps).expect("#[cfg(test)]");
473        let iter = HighlightIterator::new(&mut highlight_state, &ops[..], line, &highlighter);
474        let regions: Vec<(Style, &str)> = iter.collect();
475
476        // We expect the line to be styled as a comment.
477        assert_eq!(
478            regions[0],
479            (
480                Style {
481                    foreground: Color {
482                        // (Comment: #65737E)
483                        r: 101,
484                        g: 115,
485                        b: 126,
486                        a: 0xFF,
487                    },
488                    background: Color {
489                        r: 43,
490                        g: 48,
491                        b: 59,
492                        a: 0xFF,
493                    },
494                    font_style: FontStyle::empty(),
495                },
496                "multiline comment"
497            )
498        );
499    }
500
501    // see issues #133 and #203, this test tests the fixes for those issues
502    #[test]
503    fn tricky_cases() {
504        use crate::highlighting::{ScopeSelectors, ThemeSettings};
505        use crate::parsing::ScopeStack;
506        use std::str::FromStr;
507        let c1 = Color {
508            r: 1,
509            g: 1,
510            b: 1,
511            a: 255,
512        };
513        let c2 = Color {
514            r: 2,
515            g: 2,
516            b: 2,
517            a: 255,
518        };
519        let def_bg = Color {
520            r: 255,
521            g: 255,
522            b: 255,
523            a: 255,
524        };
525        let test_color_scheme = Theme {
526            name: None,
527            author: None,
528            settings: ThemeSettings::default(),
529            scopes: vec![
530                ThemeItem {
531                    scope: ScopeSelectors::from_str("comment.line").unwrap(),
532                    style: StyleModifier {
533                        foreground: Some(c1),
534                        background: None,
535                        font_style: None,
536                    },
537                },
538                ThemeItem {
539                    scope: ScopeSelectors::from_str("comment").unwrap(),
540                    style: StyleModifier {
541                        foreground: Some(c2),
542                        background: None,
543                        font_style: Some(FontStyle::ITALIC),
544                    },
545                },
546                ThemeItem {
547                    scope: ScopeSelectors::from_str("comment.line.rs - keyword").unwrap(),
548                    style: StyleModifier {
549                        foreground: None,
550                        background: Some(c1),
551                        font_style: None,
552                    },
553                },
554                ThemeItem {
555                    scope: ScopeSelectors::from_str("no.match").unwrap(),
556                    style: StyleModifier {
557                        foreground: None,
558                        background: Some(c2),
559                        font_style: Some(FontStyle::UNDERLINE),
560                    },
561                },
562            ],
563        };
564        let highlighter = Highlighter::new(&test_color_scheme);
565
566        use crate::parsing::ScopeStackOp::*;
567        let ops = [
568            // three rules apply at once here, two singles and one multi
569            (0, Push(Scope::new("comment.line.rs").unwrap())),
570            // multi un-applies
571            (1, Push(Scope::new("keyword.control.rs").unwrap())),
572            (2, Pop(1)),
573        ];
574
575        let mut highlight_state = HighlightState::new(&highlighter, ScopeStack::new());
576        let iter = HighlightIterator::new(&mut highlight_state, &ops[..], "abcdef", &highlighter);
577        let regions: Vec<Style> = iter.map(|(s, _)| s).collect();
578
579        // println!("{:#?}", regions);
580        assert_eq!(
581            regions,
582            vec![
583                Style {
584                    foreground: c1,
585                    background: c1,
586                    font_style: FontStyle::ITALIC
587                },
588                Style {
589                    foreground: c1,
590                    background: def_bg,
591                    font_style: FontStyle::ITALIC
592                },
593                Style {
594                    foreground: c1,
595                    background: c1,
596                    font_style: FontStyle::ITALIC
597                },
598            ]
599        );
600
601        let full_stack = ScopeStack::from_str("comment.line.rs keyword.control.rs").unwrap();
602        let full_style = highlighter.style_for_stack(full_stack.as_slice());
603        assert_eq!(
604            full_style,
605            Style {
606                foreground: c1,
607                background: def_bg,
608                font_style: FontStyle::ITALIC
609            }
610        );
611        let full_mod = highlighter.style_mod_for_stack(full_stack.as_slice());
612        assert_eq!(
613            full_mod,
614            StyleModifier {
615                foreground: Some(c1),
616                background: None,
617                font_style: Some(FontStyle::ITALIC)
618            }
619        );
620    }
621
622    #[test]
623    fn test_ranges() {
624        let ps = &*testdata::PACKAGES_SYN_SET;
625        let mut state = {
626            let syntax = ps.find_syntax_by_name("Ruby on Rails").unwrap();
627            ParseState::new(syntax)
628        };
629        let ts = ThemeSet::load_defaults();
630        let highlighter = Highlighter::new(&ts.themes["base16-ocean.dark"]);
631
632        let mut highlight_state = HighlightState::new(&highlighter, ScopeStack::new());
633        let line = "module Bob::Wow::Troll::Five; 5; end";
634        let ops = state.parse_line(line, &ps).expect("#[cfg(test)]");
635        let iter = RangedHighlightIterator::new(&mut highlight_state, &ops[..], line, &highlighter);
636        let regions: Vec<(Style, &str, Range<usize>)> = iter.collect();
637        // println!("{:#?}", regions);
638        assert_eq!(
639            regions[11],
640            (
641                Style {
642                    foreground: Color {
643                        r: 208,
644                        g: 135,
645                        b: 112,
646                        a: 0xFF,
647                    },
648                    background: Color {
649                        r: 43,
650                        g: 48,
651                        b: 59,
652                        a: 0xFF,
653                    },
654                    font_style: FontStyle::empty(),
655                },
656                "5",
657                Range { start: 30, end: 31 }
658            )
659        );
660    }
661}