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