Skip to main content

rustik_highlight/
theme.rs

1//! Theme compilation and style-span generation.
2//!
3//! Themes resolve TextMate-style selectors into compact [`Style`] values, then
4//! apply those styles to token scope spans one line at a time. The hot paths
5//! reuse caller-owned output buffers and an optional per-grammar style cache so
6//! editors can restyle visible text without allocating for every token.
7
8use std::cmp::Reverse;
9use std::{convert::Infallible, ptr, str::FromStr};
10
11use crate::Error;
12use crate::grammar::{Grammar, ScopeId, ScopeSpan};
13use crate::raw::{RawStyle, RawTheme};
14use crate::util::{next_char_boundary, previous_char_boundary, trim_line_end};
15
16/// Immutable compiled theme data.
17#[derive(Debug)]
18pub struct Theme {
19    /// Theme name.
20    pub name: String,
21    /// Default style for unscoped text.
22    pub default: Style,
23    /// Compiled selector rules.
24    rules: Vec<ThemeRule>,
25}
26
27/// One selector-to-style rule compiled from a theme.
28#[derive(Debug)]
29struct ThemeRule {
30    /// TextMate-style scope selector matched against token scopes.
31    selector: String,
32    /// Style applied when the selector matches.
33    style: Style,
34}
35
36/// Cached resolved styles indexed by a grammar's scope ids.
37///
38/// The cache is keyed by the addresses of the theme and grammar plus the
39/// grammar's scope count, so a `LineBuffer` can be reused across many lines and
40/// even different grammars without recomputing styles for every token.
41#[derive(Debug, Default)]
42pub(crate) struct StyleCache {
43    /// Identity of the theme, grammar, and scope count this cache was built for.
44    key: Option<CacheKey>,
45    /// Resolved styles indexed by [`ScopeId`].
46    styles: Vec<Style>,
47}
48
49/// Identity tuple used to validate a [`StyleCache`].
50type CacheKey = (usize, usize, usize);
51
52/// Per-line scratch buffer used while applying a theme.
53#[derive(Debug, Default)]
54pub(crate) struct StyleScratch {
55    /// Sorted byte boundaries that split a line into uniformly styled segments.
56    boundaries: Vec<usize>,
57}
58
59/// Byte range for one fully resolved style segment.
60#[derive(Clone, Copy, Debug, Eq, PartialEq)]
61struct Segment {
62    /// Start byte in the line.
63    start: usize,
64    /// End byte in the line.
65    end: usize,
66}
67
68/// Fully styled span produced by applying a [`Theme`] to scope spans.
69#[derive(Clone, Copy, Debug, Eq, PartialEq)]
70pub struct StyleSpan {
71    /// Start byte in the line.
72    pub start: usize,
73    /// End byte in the line.
74    pub end: usize,
75    /// The most specific scope covering this styled span, if any.
76    pub scope: Option<ScopeId>,
77    /// Resolved style.
78    pub style: Style,
79}
80
81/// RGB color parsed from a theme.
82#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
83pub struct Rgb {
84    /// Red channel.
85    pub r: u8,
86    /// Green channel.
87    pub g: u8,
88    /// Blue channel.
89    pub b: u8,
90}
91
92/// Error returned when parsing an RGB color.
93#[derive(Clone, Copy, Debug, Eq, PartialEq)]
94pub struct ParseRgbError;
95
96/// Bitset of font style flags.
97#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
98pub struct FontStyle(u8);
99
100/// Resolved style for a token.
101#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
102pub struct Style {
103    /// Optional foreground color.
104    pub foreground: Option<Rgb>,
105    /// Font style flags.
106    pub font_style: FontStyle,
107}
108
109impl Theme {
110    /// Compiles a raw theme.
111    pub fn compile(raw: &RawTheme) -> Self {
112        let mut default = Style::default();
113        let mut rules = Vec::new();
114        let raw_rules = raw.settings.as_deref().or(raw.token_colors.as_deref());
115
116        for rule in raw_rules.unwrap_or_default() {
117            let style = Style::from_raw(&rule.settings);
118            let selectors = rule.scope_selectors();
119            if selectors.is_empty() {
120                default = style;
121            } else {
122                rules.extend(
123                    selectors
124                        .into_iter()
125                        .map(|selector| ThemeRule { selector, style }),
126                );
127            }
128        }
129        Self {
130            name: raw.name.clone(),
131            default,
132            rules,
133        }
134    }
135
136    /// Parses and compiles a theme from JSON.
137    pub fn parse(input: &str) -> Result<Self, Error> {
138        input.parse()
139    }
140
141    /// Resolves a style for a scope string.
142    ///
143    /// The most specific selector wins, with ties broken by longer selectors.
144    pub fn style_for_scope(&self, scope: &str) -> Style {
145        let mut style = self.default;
146        let mut best = 0;
147        for rule in &self.rules {
148            if rule.matches(scope) && rule.selector.len() >= best {
149                best = rule.selector.len();
150                style = rule.style.merge(style);
151            }
152        }
153        style
154    }
155
156    /// Applies this theme to scope spans and returns a freshly allocated vector.
157    pub fn style_spans(
158        &self,
159        grammar: &Grammar,
160        line: &str,
161        scopes: &[ScopeSpan],
162    ) -> Vec<StyleSpan> {
163        let mut cache = StyleCache::default();
164        let mut scratch = StyleScratch::default();
165        let mut output = Vec::new();
166
167        cache.refresh(self, grammar);
168        self.style_line_into(grammar, line, scopes, &cache, &mut scratch, &mut output);
169        output
170    }
171
172    /// Applies this theme one line at a time using caller-owned scratch storage.
173    pub(crate) fn style_line_into(
174        &self,
175        grammar: &Grammar,
176        line: &str,
177        scopes: &[ScopeSpan],
178        cache: &StyleCache,
179        scratch: &mut StyleScratch,
180        output: &mut Vec<StyleSpan>,
181    ) {
182        output.clear();
183        let line = trim_line_end(line);
184        if line.is_empty() {
185            return;
186        }
187        scratch.collect_boundaries(line, scopes);
188
189        for segment in scratch.segments() {
190            output.push(self.style_segment(grammar, scopes, cache, segment));
191        }
192    }
193
194    /// Resolves a single styled segment from its covering scope spans.
195    fn style_segment(
196        &self,
197        grammar: &Grammar,
198        scopes: &[ScopeSpan],
199        cache: &StyleCache,
200        segment: Segment,
201    ) -> StyleSpan {
202        let Some(span) = segment.best_covering(scopes, grammar) else {
203            return segment.styled(None, self.default);
204        };
205        let style = cache
206            .style_for(span.scope)
207            .unwrap_or_else(|| self.style_for_scope_id(grammar, span.scope));
208        segment.styled(Some(span.scope), style)
209    }
210
211    /// Resolves a style for an interned scope id without going through the cache.
212    fn style_for_scope_id(&self, grammar: &Grammar, scope: ScopeId) -> Style {
213        grammar
214            .scopes
215            .get(scope.index())
216            .map_or(self.default, |name| self.style_for_scope(name))
217    }
218}
219
220impl FromStr for Theme {
221    type Err = Error;
222
223    /// Parses and compiles a theme from JSON.
224    fn from_str(input: &str) -> Result<Self, Self::Err> {
225        let raw = RawTheme::from_str(input)?;
226        Ok(Self::compile(&raw))
227    }
228}
229
230impl ThemeRule {
231    /// Returns whether this rule's selector applies to a concrete scope.
232    fn matches(&self, scope: &str) -> bool {
233        scope == self.selector
234            || (scope.len() > self.selector.len()
235                && scope.starts_with(&self.selector)
236                && scope.as_bytes().get(self.selector.len()) == Some(&b'.'))
237    }
238}
239
240impl StyleCache {
241    /// Refreshes cached scope styles for a theme and grammar when needed.
242    pub(crate) fn refresh(&mut self, theme: &Theme, grammar: &Grammar) {
243        let key = (
244            ptr::from_ref(theme).addr(),
245            ptr::from_ref(grammar).addr(),
246            grammar.scopes.len(),
247        );
248        if self.key == Some(key) {
249            return;
250        }
251        self.styles.clear();
252        self.styles.extend(
253            grammar
254                .scopes
255                .iter()
256                .map(|scope| theme.style_for_scope(scope)),
257        );
258        self.key = Some(key);
259    }
260
261    /// Returns the cached style for an interned scope id, if available.
262    fn style_for(&self, scope: ScopeId) -> Option<Style> {
263        self.styles.get(scope.index()).copied()
264    }
265}
266
267impl StyleScratch {
268    /// Clears per-line scratch storage while retaining allocations.
269    pub(crate) fn clear_line(&mut self) {
270        self.boundaries.clear();
271    }
272
273    /// Collects sorted byte boundaries for the style segments in a line.
274    fn collect_boundaries(&mut self, line: &str, scopes: &[ScopeSpan]) {
275        self.boundaries.clear();
276        self.boundaries.push(0);
277        self.boundaries.push(line.len());
278
279        for span in scopes {
280            let start = next_char_boundary(line, span.start);
281            let end = previous_char_boundary(line, span.end);
282            if start < end {
283                self.boundaries.push(start);
284                self.boundaries.push(end);
285            }
286        }
287        self.boundaries.sort_unstable();
288        self.boundaries.dedup();
289    }
290
291    /// Iterates over the non-empty segments produced by the collected boundaries.
292    fn segments(&self) -> impl Iterator<Item = Segment> + '_ {
293        self.boundaries.windows(2).filter_map(|window| {
294            let [start, end] = *window else {
295                return None;
296            };
297            (start < end).then_some(Segment { start, end })
298        })
299    }
300}
301
302impl Segment {
303    /// Builds a styled span from this segment.
304    fn styled(self, scope: Option<ScopeId>, style: Style) -> StyleSpan {
305        StyleSpan {
306            start: self.start,
307            end: self.end,
308            scope,
309            style,
310        }
311    }
312
313    /// Picks the smallest scope span covering this segment, breaking ties by selector specificity.
314    fn best_covering<'a>(self, spans: &'a [ScopeSpan], grammar: &Grammar) -> Option<&'a ScopeSpan> {
315        spans
316            .iter()
317            .filter(|span| span.start <= self.start && span.end >= self.end)
318            .min_by_key(|span| {
319                let scope_len = grammar
320                    .scopes
321                    .get(span.scope.index())
322                    .map_or(0, String::len);
323                (span.end - span.start, Reverse(scope_len))
324            })
325    }
326}
327
328impl FromStr for Rgb {
329    type Err = ParseRgbError;
330
331    /// Parses a six-digit hex color of the form `#rrggbb`.
332    fn from_str(input: &str) -> Result<Self, Self::Err> {
333        let hex = input.strip_prefix('#').ok_or(ParseRgbError)?;
334        if hex.len() != 6 {
335            return Err(ParseRgbError);
336        }
337        let r = u8::from_str_radix(&hex[0..2], 16).map_err(|_| ParseRgbError)?;
338        let g = u8::from_str_radix(&hex[2..4], 16).map_err(|_| ParseRgbError)?;
339        let b = u8::from_str_radix(&hex[4..6], 16).map_err(|_| ParseRgbError)?;
340
341        Ok(Self { r, g, b })
342    }
343}
344
345impl FontStyle {
346    /// Bold text.
347    pub const BOLD: Self = Self(1 << 0);
348    /// Italic text.
349    pub const ITALIC: Self = Self(1 << 1);
350    /// Underlined text.
351    pub const UNDERLINE: Self = Self(1 << 2);
352    /// Strikethrough text.
353    pub const STRIKETHROUGH: Self = Self(1 << 3);
354
355    /// Returns an empty font-style set.
356    pub const fn empty() -> Self {
357        Self(0)
358    }
359
360    /// Returns true when all flags in `other` are set.
361    pub const fn contains(self, other: Self) -> bool {
362        (self.0 & other.0) == other.0
363    }
364
365    /// Adds all flags from `other`.
366    pub fn insert(&mut self, other: Self) {
367        self.0 |= other.0;
368    }
369}
370
371impl FromStr for FontStyle {
372    type Err = Infallible;
373
374    /// Parses a space-separated list of TextMate font-style flags.
375    fn from_str(input: &str) -> Result<Self, Self::Err> {
376        let mut style = Self::empty();
377        for flag in input.split_whitespace() {
378            match flag {
379                "bold" => style.insert(Self::BOLD),
380                "italic" => style.insert(Self::ITALIC),
381                "underline" => style.insert(Self::UNDERLINE),
382                "strikethrough" => style.insert(Self::STRIKETHROUGH),
383                _ => {}
384            }
385        }
386        Ok(style)
387    }
388}
389
390impl Style {
391    /// Builds a runtime style from raw TextMate settings.
392    fn from_raw(raw: &RawStyle) -> Self {
393        let mut style = Self::default();
394        if let Some(foreground) = &raw.foreground {
395            style.foreground = foreground.parse().ok();
396        }
397        if let Some(flags) = &raw.font_style {
398            style.font_style = flags.parse().unwrap_or_else(|error| match error {});
399        }
400        style
401    }
402
403    /// Returns `self` overlaid on `base`.
404    pub fn merge(self, base: Self) -> Self {
405        let mut font_style = base.font_style;
406        font_style.insert(self.font_style);
407        Self {
408            foreground: self.foreground.or(base.foreground),
409            font_style,
410        }
411    }
412
413    /// Returns this style with a foreground color.
414    pub const fn fg(mut self, color: Rgb) -> Self {
415        self.foreground = Some(color);
416        self
417    }
418
419    /// Returns this style with bold enabled.
420    pub fn bold(mut self) -> Self {
421        self.font_style.insert(FontStyle::BOLD);
422        self
423    }
424
425    /// Returns this style with italic enabled.
426    pub fn italic(mut self) -> Self {
427        self.font_style.insert(FontStyle::ITALIC);
428        self
429    }
430
431    /// Returns this style with underline enabled.
432    pub fn underline(mut self) -> Self {
433        self.font_style.insert(FontStyle::UNDERLINE);
434        self
435    }
436
437    /// Returns this style with strikethrough enabled.
438    pub fn strikethrough(mut self) -> Self {
439        self.font_style.insert(FontStyle::STRIKETHROUGH);
440        self
441    }
442}