iced_highlighter/
lib.rs

1//! A syntax highlighter for iced.
2use iced_core as core;
3
4use crate::core::Color;
5use crate::core::font::{self, Font};
6use crate::core::text::highlighter::{self, Format};
7
8use std::ops::Range;
9use std::sync::LazyLock;
10
11use syntect::highlighting;
12use syntect::parsing;
13
14static SYNTAXES: LazyLock<parsing::SyntaxSet> =
15    LazyLock::new(parsing::SyntaxSet::load_defaults_nonewlines);
16
17static THEMES: LazyLock<highlighting::ThemeSet> =
18    LazyLock::new(highlighting::ThemeSet::load_defaults);
19
20const LINES_PER_SNAPSHOT: usize = 50;
21
22/// A syntax highlighter.
23#[derive(Debug)]
24pub struct Highlighter {
25    syntax: &'static parsing::SyntaxReference,
26    highlighter: highlighting::Highlighter<'static>,
27    caches: Vec<(parsing::ParseState, parsing::ScopeStack)>,
28    current_line: usize,
29}
30
31impl highlighter::Highlighter for Highlighter {
32    type Settings = Settings;
33    type Highlight = Highlight;
34
35    type Iterator<'a> =
36        Box<dyn Iterator<Item = (Range<usize>, Self::Highlight)> + 'a>;
37
38    fn new(settings: &Self::Settings) -> Self {
39        let syntax = SYNTAXES
40            .find_syntax_by_token(&settings.token)
41            .unwrap_or_else(|| SYNTAXES.find_syntax_plain_text());
42
43        let highlighter = highlighting::Highlighter::new(
44            &THEMES.themes[settings.theme.key()],
45        );
46
47        let parser = parsing::ParseState::new(syntax);
48        let stack = parsing::ScopeStack::new();
49
50        Highlighter {
51            syntax,
52            highlighter,
53            caches: vec![(parser, stack)],
54            current_line: 0,
55        }
56    }
57
58    fn update(&mut self, new_settings: &Self::Settings) {
59        self.syntax = SYNTAXES
60            .find_syntax_by_token(&new_settings.token)
61            .unwrap_or_else(|| SYNTAXES.find_syntax_plain_text());
62
63        self.highlighter = highlighting::Highlighter::new(
64            &THEMES.themes[new_settings.theme.key()],
65        );
66
67        // Restart the highlighter
68        self.change_line(0);
69    }
70
71    fn change_line(&mut self, line: usize) {
72        let snapshot = line / LINES_PER_SNAPSHOT;
73
74        if snapshot <= self.caches.len() {
75            self.caches.truncate(snapshot);
76            self.current_line = snapshot * LINES_PER_SNAPSHOT;
77        } else {
78            self.caches.truncate(1);
79            self.current_line = 0;
80        }
81
82        let (parser, stack) =
83            self.caches.last().cloned().unwrap_or_else(|| {
84                (
85                    parsing::ParseState::new(self.syntax),
86                    parsing::ScopeStack::new(),
87                )
88            });
89
90        self.caches.push((parser, stack));
91    }
92
93    fn highlight_line(&mut self, line: &str) -> Self::Iterator<'_> {
94        if self.current_line / LINES_PER_SNAPSHOT >= self.caches.len() {
95            let (parser, stack) =
96                self.caches.last().expect("Caches must not be empty");
97
98            self.caches.push((parser.clone(), stack.clone()));
99        }
100
101        self.current_line += 1;
102
103        let (parser, stack) =
104            self.caches.last_mut().expect("Caches must not be empty");
105
106        let ops = parser.parse_line(line, &SYNTAXES).unwrap_or_default();
107
108        Box::new(scope_iterator(ops, line, stack, &self.highlighter))
109    }
110
111    fn current_line(&self) -> usize {
112        self.current_line
113    }
114}
115
116fn scope_iterator<'a>(
117    ops: Vec<(usize, parsing::ScopeStackOp)>,
118    line: &str,
119    stack: &'a mut parsing::ScopeStack,
120    highlighter: &'a highlighting::Highlighter<'static>,
121) -> impl Iterator<Item = (Range<usize>, Highlight)> + 'a {
122    ScopeRangeIterator {
123        ops,
124        line_length: line.len(),
125        index: 0,
126        last_str_index: 0,
127    }
128    .filter_map(move |(range, scope)| {
129        let _ = stack.apply(&scope);
130
131        if range.is_empty() {
132            None
133        } else {
134            Some((
135                range,
136                Highlight(highlighter.style_mod_for_stack(&stack.scopes)),
137            ))
138        }
139    })
140}
141
142/// A streaming syntax highlighter.
143///
144/// It can efficiently highlight an immutable stream of tokens.
145#[derive(Debug)]
146pub struct Stream {
147    syntax: &'static parsing::SyntaxReference,
148    highlighter: highlighting::Highlighter<'static>,
149    commit: (parsing::ParseState, parsing::ScopeStack),
150    state: parsing::ParseState,
151    stack: parsing::ScopeStack,
152}
153
154impl Stream {
155    /// Creates a new [`Stream`] highlighter.
156    pub fn new(settings: &Settings) -> Self {
157        let syntax = SYNTAXES
158            .find_syntax_by_token(&settings.token)
159            .unwrap_or_else(|| SYNTAXES.find_syntax_plain_text());
160
161        let highlighter = highlighting::Highlighter::new(
162            &THEMES.themes[settings.theme.key()],
163        );
164
165        let state = parsing::ParseState::new(syntax);
166        let stack = parsing::ScopeStack::new();
167
168        Self {
169            syntax,
170            highlighter,
171            commit: (state.clone(), stack.clone()),
172            state,
173            stack,
174        }
175    }
176
177    /// Highlights the given line from the last commit.
178    pub fn highlight_line(
179        &mut self,
180        line: &str,
181    ) -> impl Iterator<Item = (Range<usize>, Highlight)> + '_ {
182        self.state = self.commit.0.clone();
183        self.stack = self.commit.1.clone();
184
185        let ops = self.state.parse_line(line, &SYNTAXES).unwrap_or_default();
186        scope_iterator(ops, line, &mut self.stack, &self.highlighter)
187    }
188
189    /// Commits the last highlighted line.
190    pub fn commit(&mut self) {
191        self.commit = (self.state.clone(), self.stack.clone());
192    }
193
194    /// Resets the [`Stream`] highlighter.
195    pub fn reset(&mut self) {
196        self.state = parsing::ParseState::new(self.syntax);
197        self.stack = parsing::ScopeStack::new();
198        self.commit = (self.state.clone(), self.stack.clone());
199    }
200}
201
202/// The settings of a [`Highlighter`].
203#[derive(Debug, Clone, PartialEq)]
204pub struct Settings {
205    /// The [`Theme`] of the [`Highlighter`].
206    ///
207    /// It dictates the color scheme that will be used for highlighting.
208    pub theme: Theme,
209    /// The extension of the file or the name of the language to highlight.
210    ///
211    /// The [`Highlighter`] will use the token to automatically determine
212    /// the grammar to use for highlighting.
213    pub token: String,
214}
215
216/// A highlight produced by a [`Highlighter`].
217#[derive(Debug)]
218pub struct Highlight(highlighting::StyleModifier);
219
220impl Highlight {
221    /// Returns the color of this [`Highlight`].
222    ///
223    /// If `None`, the original text color should be unchanged.
224    pub fn color(&self) -> Option<Color> {
225        self.0.foreground.map(|color| {
226            Color::from_rgba8(color.r, color.g, color.b, color.a as f32 / 255.0)
227        })
228    }
229
230    /// Returns the font of this [`Highlight`].
231    ///
232    /// If `None`, the original font should be unchanged.
233    pub fn font(&self) -> Option<Font> {
234        self.0.font_style.and_then(|style| {
235            let bold = style.contains(highlighting::FontStyle::BOLD);
236            let italic = style.contains(highlighting::FontStyle::ITALIC);
237
238            if bold || italic {
239                Some(Font {
240                    weight: if bold {
241                        font::Weight::Bold
242                    } else {
243                        font::Weight::Normal
244                    },
245                    style: if italic {
246                        font::Style::Italic
247                    } else {
248                        font::Style::Normal
249                    },
250                    ..Font::MONOSPACE
251                })
252            } else {
253                None
254            }
255        })
256    }
257
258    /// Returns the [`Format`] of the [`Highlight`].
259    ///
260    /// It contains both the [`color`] and the [`font`].
261    ///
262    /// [`color`]: Self::color
263    /// [`font`]: Self::font
264    pub fn to_format(&self) -> Format<Font> {
265        Format {
266            color: self.color(),
267            font: self.font(),
268        }
269    }
270}
271
272/// A highlighting theme.
273#[allow(missing_docs)]
274#[derive(Debug, Clone, Copy, PartialEq, Eq)]
275pub enum Theme {
276    SolarizedDark,
277    Base16Mocha,
278    Base16Ocean,
279    Base16Eighties,
280    InspiredGitHub,
281}
282
283impl Theme {
284    /// A static slice containing all the available themes.
285    pub const ALL: &'static [Self] = &[
286        Self::SolarizedDark,
287        Self::Base16Mocha,
288        Self::Base16Ocean,
289        Self::Base16Eighties,
290        Self::InspiredGitHub,
291    ];
292
293    /// Returns `true` if the [`Theme`] is dark, and false otherwise.
294    pub fn is_dark(self) -> bool {
295        match self {
296            Self::SolarizedDark
297            | Self::Base16Mocha
298            | Self::Base16Ocean
299            | Self::Base16Eighties => true,
300            Self::InspiredGitHub => false,
301        }
302    }
303
304    fn key(self) -> &'static str {
305        match self {
306            Theme::SolarizedDark => "Solarized (dark)",
307            Theme::Base16Mocha => "base16-mocha.dark",
308            Theme::Base16Ocean => "base16-ocean.dark",
309            Theme::Base16Eighties => "base16-eighties.dark",
310            Theme::InspiredGitHub => "InspiredGitHub",
311        }
312    }
313}
314
315impl std::fmt::Display for Theme {
316    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
317        match self {
318            Theme::SolarizedDark => write!(f, "Solarized Dark"),
319            Theme::Base16Mocha => write!(f, "Mocha"),
320            Theme::Base16Ocean => write!(f, "Ocean"),
321            Theme::Base16Eighties => write!(f, "Eighties"),
322            Theme::InspiredGitHub => write!(f, "Inspired GitHub"),
323        }
324    }
325}
326
327struct ScopeRangeIterator {
328    ops: Vec<(usize, parsing::ScopeStackOp)>,
329    line_length: usize,
330    index: usize,
331    last_str_index: usize,
332}
333
334impl Iterator for ScopeRangeIterator {
335    type Item = (std::ops::Range<usize>, parsing::ScopeStackOp);
336
337    fn next(&mut self) -> Option<Self::Item> {
338        if self.index > self.ops.len() {
339            return None;
340        }
341
342        let next_str_i = if self.index == self.ops.len() {
343            self.line_length
344        } else {
345            self.ops[self.index].0
346        };
347
348        let range = self.last_str_index..next_str_i;
349        self.last_str_index = next_str_i;
350
351        let op = if self.index == 0 {
352            parsing::ScopeStackOp::Noop
353        } else {
354            self.ops[self.index - 1].1.clone()
355        };
356
357        self.index += 1;
358        Some((range, op))
359    }
360}