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