1use 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#[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 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#[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 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 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 pub fn commit(&mut self) {
191 self.commit = (self.state.clone(), self.stack.clone());
192 }
193
194 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#[derive(Debug, Clone, PartialEq)]
204pub struct Settings {
205 pub theme: Theme,
209 pub token: String,
214}
215
216#[derive(Debug)]
218pub struct Highlight(highlighting::StyleModifier);
219
220impl Highlight {
221 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 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 pub fn to_format(&self) -> Format<Font> {
265 Format {
266 color: self.color(),
267 font: self.font(),
268 }
269 }
270}
271
272#[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 pub const ALL: &'static [Self] = &[
286 Self::SolarizedDark,
287 Self::Base16Mocha,
288 Self::Base16Ocean,
289 Self::Base16Eighties,
290 Self::InspiredGitHub,
291 ];
292
293 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}