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