1use std::cell::{Cell, RefCell};
56use std::rc::Rc;
57use std::sync::OnceLock;
58
59use dinamika_cpu::Color;
60use syntect::parsing::{ParseState, Scope, ScopeStack, SyntaxSet};
61
62#[derive(Copy, Clone, Debug, PartialEq, Eq, Default)]
67pub enum Language {
68 #[default]
70 PlainText,
71 Rust,
73 JavaScript,
75 Python,
77 C,
79 Cpp,
81 Go,
83 Json,
85 Html,
87 Css,
89 Java,
91 Bash,
93}
94
95impl Language {
96 fn token(self) -> Option<&'static str> {
99 Some(match self {
100 Language::PlainText => return None,
101 Language::Rust => "rust",
102 Language::JavaScript => "js",
103 Language::Python => "python",
104 Language::C => "c",
105 Language::Cpp => "c++",
106 Language::Go => "go",
107 Language::Json => "json",
108 Language::Html => "html",
109 Language::Css => "css",
110 Language::Java => "java",
111 Language::Bash => "sh",
112 })
113 }
114}
115
116#[derive(Clone, Debug, PartialEq)]
124pub struct Palette {
125 foreground: Color,
127 keyword: Option<Color>,
129 string: Option<Color>,
131 comment: Option<Color>,
133 number: Option<Color>,
135 function: Option<Color>,
137 type_: Option<Color>,
139 constant: Option<Color>,
141 operator: Option<Color>,
143 variable: Option<Color>,
145 punctuation: Option<Color>,
147}
148
149impl Default for Palette {
150 fn default() -> Self {
154 Palette::new(Color::BLACK)
155 }
156}
157
158impl Palette {
159 pub fn new(foreground: Color) -> Self {
162 Palette {
163 foreground,
164 keyword: None,
165 string: None,
166 comment: None,
167 number: None,
168 function: None,
169 type_: None,
170 constant: None,
171 operator: None,
172 variable: None,
173 punctuation: None,
174 }
175 }
176
177 pub fn keyword(mut self, c: Color) -> Self {
179 self.keyword = Some(c);
180 self
181 }
182 pub fn string(mut self, c: Color) -> Self {
184 self.string = Some(c);
185 self
186 }
187 pub fn comment(mut self, c: Color) -> Self {
189 self.comment = Some(c);
190 self
191 }
192 pub fn number(mut self, c: Color) -> Self {
194 self.number = Some(c);
195 self
196 }
197 pub fn function(mut self, c: Color) -> Self {
199 self.function = Some(c);
200 self
201 }
202 pub fn type_(mut self, c: Color) -> Self {
204 self.type_ = Some(c);
205 self
206 }
207 pub fn constant(mut self, c: Color) -> Self {
209 self.constant = Some(c);
210 self
211 }
212 pub fn operator(mut self, c: Color) -> Self {
214 self.operator = Some(c);
215 self
216 }
217 pub fn variable(mut self, c: Color) -> Self {
219 self.variable = Some(c);
220 self
221 }
222 pub fn punctuation(mut self, c: Color) -> Self {
224 self.punctuation = Some(c);
225 self
226 }
227
228 fn is_plain(&self) -> bool {
231 self.keyword.is_none()
232 && self.string.is_none()
233 && self.comment.is_none()
234 && self.number.is_none()
235 && self.function.is_none()
236 && self.type_.is_none()
237 && self.constant.is_none()
238 && self.operator.is_none()
239 && self.variable.is_none()
240 && self.punctuation.is_none()
241 }
242
243 fn color_for(&self, scopes: &[Scope]) -> Color {
252 for scope in scopes {
253 let s = scope.build_string();
254 if s.starts_with("comment") {
255 return self.comment.unwrap_or(self.foreground);
256 }
257 if s.starts_with("string") {
258 return self.string.unwrap_or(self.foreground);
259 }
260 }
261 for scope in scopes.iter().rev() {
262 if let Some(color) = classify(&scope.build_string()).and_then(|c| self.category_color(c)) {
263 return color;
264 }
265 }
266 self.foreground
267 }
268
269 fn category_color(&self, category: Category) -> Option<Color> {
271 match category {
272 Category::Keyword => self.keyword,
273 Category::Number => self.number,
274 Category::Function => self.function,
275 Category::Type => self.type_,
276 Category::Constant => self.constant,
277 Category::Operator => self.operator,
278 Category::Variable => self.variable,
279 Category::Punctuation => self.punctuation,
280 }
281 }
282}
283
284#[derive(Copy, Clone)]
287enum Category {
288 Keyword,
289 Number,
290 Function,
291 Type,
292 Constant,
293 Operator,
294 Variable,
295 Punctuation,
296}
297
298fn classify(scope: &str) -> Option<Category> {
302 if scope.starts_with("keyword.operator") {
303 Some(Category::Operator)
304 } else if scope.starts_with("keyword") || scope.starts_with("storage") {
305 Some(Category::Keyword)
308 } else if scope.starts_with("constant.numeric") {
309 Some(Category::Number)
310 } else if scope.starts_with("constant") {
311 Some(Category::Constant)
312 } else if scope.starts_with("entity.name.function")
313 || scope.starts_with("support.function")
314 || scope.starts_with("variable.function")
315 || scope.starts_with("meta.function-call")
316 {
317 Some(Category::Function)
318 } else if scope.starts_with("entity.name.type")
319 || scope.starts_with("entity.name.class")
320 || scope.starts_with("entity.name.struct")
321 || scope.starts_with("entity.name.enum")
322 || scope.starts_with("entity.name.trait")
323 || scope.starts_with("entity.name.namespace")
324 || scope.starts_with("support.type")
325 || scope.starts_with("support.class")
326 || scope.starts_with("entity.other.inherited-class")
327 {
328 Some(Category::Type)
329 } else if scope.starts_with("variable") {
330 Some(Category::Variable)
331 } else if scope.starts_with("punctuation") {
332 Some(Category::Punctuation)
333 } else {
334 None
335 }
336}
337
338pub(crate) struct CodeData {
347 palette: RefCell<Palette>,
348 language: Cell<Language>,
349 cache: RefCell<Vec<(CodeKey, Rc<Vec<Color>>)>>,
353}
354
355#[derive(Clone, PartialEq)]
357struct CodeKey {
358 text: String,
359 language: Language,
360 palette: Palette,
361}
362
363const CACHE_CAP: usize = 4;
365
366impl CodeData {
367 pub(crate) fn new() -> Self {
370 CodeData {
371 palette: RefCell::new(Palette::default()),
372 language: Cell::new(Language::default()),
373 cache: RefCell::new(Vec::new()),
374 }
375 }
376
377 pub(crate) fn set_palette(&self, palette: Palette) {
379 *self.palette.borrow_mut() = palette;
380 self.cache.borrow_mut().clear();
381 }
382
383 pub(crate) fn set_language(&self, language: Language) {
385 self.language.set(language);
386 self.cache.borrow_mut().clear();
387 }
388
389 pub(crate) fn foreground(&self) -> Color {
392 self.palette.borrow().foreground
393 }
394
395 pub(crate) fn char_colors(&self, text: &str) -> Rc<Vec<Color>> {
399 let palette = self.palette.borrow().clone();
400 let language = self.language.get();
401 if let Some(hit) = self.cache.borrow().iter().find_map(|(k, v)| {
402 (k.text == text && k.language == language && k.palette == palette).then(|| Rc::clone(v))
403 }) {
404 return hit;
405 }
406 let colors = Rc::new(compute_colors(text, language, &palette));
407 let mut cache = self.cache.borrow_mut();
408 cache.insert(0, (CodeKey { text: text.to_owned(), language, palette }, Rc::clone(&colors)));
409 cache.truncate(CACHE_CAP);
410 colors
411 }
412}
413
414fn syntax_set() -> &'static SyntaxSet {
416 static SET: OnceLock<SyntaxSet> = OnceLock::new();
417 SET.get_or_init(SyntaxSet::load_defaults_newlines)
418}
419
420fn compute_colors(text: &str, language: Language, palette: &Palette) -> Vec<Color> {
428 let count = text.chars().count();
429 let foreground = palette.foreground;
430 let token = match language.token() {
431 Some(token) if !palette.is_plain() => token,
432 _ => return vec![foreground; count],
433 };
434 let syntax_set = syntax_set();
435 let syntax = match syntax_set.find_syntax_by_token(token) {
436 Some(syntax) => syntax,
437 None => return vec![foreground; count],
438 };
439
440 let mut parse = ParseState::new(syntax);
441 let mut stack = ScopeStack::new();
442 let mut colors = Vec::with_capacity(count);
443 for line in text.split_inclusive('\n') {
446 let ops = parse.parse_line(line, syntax_set).unwrap_or_default();
447 let mut ops = ops.into_iter().peekable();
448 for (byte_offset, _ch) in line.char_indices() {
449 while let Some(&(offset, _)) = ops.peek() {
451 if offset <= byte_offset {
452 let (_, op) = ops.next().unwrap();
453 let _ = stack.apply(&op);
454 } else {
455 break;
456 }
457 }
458 colors.push(palette.color_for(stack.as_slice()));
459 }
460 for (_, op) in ops {
462 let _ = stack.apply(&op);
463 }
464 }
465 colors
466}
467
468#[cfg(test)]
469mod tests {
470 use super::*;
471
472 fn char_index_of(text: &str, needle: &str) -> usize {
475 let byte = text.find(needle).expect("the substring is present in the text");
476 text[..byte].chars().count()
477 }
478
479 #[test]
480 fn plain_language_paints_everything_with_foreground() {
481 let fg = Color::from_rgba8(10, 20, 30, 255);
483 let palette = Palette::new(fg).keyword(Color::from_rgba8(200, 0, 0, 255));
484 let code = CodeData::new();
485 code.set_palette(palette);
486 let text = "fn main() {}";
487 let colors = code.char_colors(text);
488 assert_eq!(colors.len(), text.chars().count());
489 assert!(colors.iter().all(|c| *c == fg), "only the base color was expected");
490 }
491
492 #[test]
493 fn empty_palette_skips_highlighting() {
494 let fg = Color::from_rgba8(1, 2, 3, 255);
496 let code = CodeData::new();
497 code.set_palette(Palette::new(fg));
498 code.set_language(Language::Rust);
499 let colors = code.char_colors("fn main() {}");
500 assert!(colors.iter().all(|c| *c == fg));
501 }
502
503 #[test]
504 fn rust_keyword_string_and_comment_take_palette_colors() {
505 let fg = Color::from_rgba8(212, 212, 212, 255);
506 let kw = Color::from_rgba8(197, 134, 192, 255);
507 let st = Color::from_rgba8(206, 145, 120, 255);
508 let cm = Color::from_rgba8(106, 153, 85, 255);
509 let palette = Palette::new(fg).keyword(kw).string(st).comment(cm);
510 let code = CodeData::new();
511 code.set_palette(palette);
512 code.set_language(Language::Rust);
513
514 let text = "let x = \"hi\"; // note";
515 let colors = code.char_colors(text);
516 assert_eq!(colors.len(), text.chars().count());
517
518 assert_eq!(colors[char_index_of(text, "let")], kw);
520 assert_eq!(colors[char_index_of(text, "hi")], st);
522 assert_eq!(colors[char_index_of(text, "// note")], cm);
524 assert_eq!(colors[char_index_of(text, "note")], cm);
525 }
526
527 #[test]
528 fn unset_category_falls_back_to_foreground() {
529 let fg = Color::from_rgba8(50, 50, 50, 255);
531 let kw = Color::from_rgba8(0, 100, 200, 255);
532 let palette = Palette::new(fg).keyword(kw); let code = CodeData::new();
534 code.set_palette(palette);
535 code.set_language(Language::Rust);
536
537 let text = "let n = 42;";
538 let colors = code.char_colors(text);
539 assert_eq!(colors[char_index_of(text, "let")], kw);
540 assert_eq!(colors[char_index_of(text, "42")], fg, "a number without a rule — in the base color");
541 }
542
543 #[test]
544 fn colors_align_with_multiline_chars() {
545 let code = CodeData::new();
547 code.set_language(Language::Rust);
548 code.set_palette(Palette::new(Color::BLACK).keyword(Color::WHITE));
549 let text = "fn a() {}\nfn b() {}\n";
550 let colors = code.char_colors(text);
551 assert_eq!(colors.len(), text.chars().count());
552 }
553
554 #[test]
555 fn cache_returns_same_allocation_for_repeated_calls() {
556 let code = CodeData::new();
557 code.set_language(Language::Rust);
558 code.set_palette(Palette::new(Color::BLACK).keyword(Color::WHITE));
559 let a = code.char_colors("fn main() {}");
560 let b = code.char_colors("fn main() {}");
561 assert!(Rc::ptr_eq(&a, &b), "re-parsing the same text is taken from the cache");
562 code.set_palette(Palette::new(Color::BLACK).keyword(Color::from_rgba8(1, 1, 1, 255)));
564 let c = code.char_colors("fn main() {}");
565 assert!(!Rc::ptr_eq(&a, &c), "after changing the palette — recompute");
566 }
567}