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> = 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#[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 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#[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 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 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 pub fn commit(&mut self) {
181 self.commit = (self.state.clone(), self.stack.clone());
182 }
183
184 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#[derive(Debug, Clone, PartialEq)]
194pub struct Settings {
195 pub theme: Theme,
199 pub token: String,
204}
205
206#[derive(Debug)]
208pub struct Highlight(highlighting::StyleModifier);
209
210impl Highlight {
211 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 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 pub fn to_format(&self) -> Format<Font> {
255 Format {
256 color: self.color(),
257 font: self.font(),
258 }
259 }
260}
261
262#[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 pub const ALL: &'static [Self] = &[
276 Self::SolarizedDark,
277 Self::Base16Mocha,
278 Self::Base16Ocean,
279 Self::Base16Eighties,
280 Self::InspiredGitHub,
281 ];
282
283 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}