use std::cell::{Cell, RefCell};
use std::rc::Rc;
use std::sync::OnceLock;
use dinamika_cpu::Color;
use syntect::parsing::{ParseState, Scope, ScopeStack, SyntaxSet};
#[derive(Copy, Clone, Debug, PartialEq, Eq, Default)]
pub enum Language {
#[default]
PlainText,
Rust,
JavaScript,
Python,
C,
Cpp,
Go,
Json,
Html,
Css,
Java,
Bash,
}
impl Language {
fn token(self) -> Option<&'static str> {
Some(match self {
Language::PlainText => return None,
Language::Rust => "rust",
Language::JavaScript => "js",
Language::Python => "python",
Language::C => "c",
Language::Cpp => "c++",
Language::Go => "go",
Language::Json => "json",
Language::Html => "html",
Language::Css => "css",
Language::Java => "java",
Language::Bash => "sh",
})
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct Palette {
foreground: Color,
keyword: Option<Color>,
string: Option<Color>,
comment: Option<Color>,
number: Option<Color>,
function: Option<Color>,
type_: Option<Color>,
constant: Option<Color>,
operator: Option<Color>,
variable: Option<Color>,
punctuation: Option<Color>,
}
impl Default for Palette {
fn default() -> Self {
Palette::new(Color::BLACK)
}
}
impl Palette {
pub fn new(foreground: Color) -> Self {
Palette {
foreground,
keyword: None,
string: None,
comment: None,
number: None,
function: None,
type_: None,
constant: None,
operator: None,
variable: None,
punctuation: None,
}
}
pub fn keyword(mut self, c: Color) -> Self {
self.keyword = Some(c);
self
}
pub fn string(mut self, c: Color) -> Self {
self.string = Some(c);
self
}
pub fn comment(mut self, c: Color) -> Self {
self.comment = Some(c);
self
}
pub fn number(mut self, c: Color) -> Self {
self.number = Some(c);
self
}
pub fn function(mut self, c: Color) -> Self {
self.function = Some(c);
self
}
pub fn type_(mut self, c: Color) -> Self {
self.type_ = Some(c);
self
}
pub fn constant(mut self, c: Color) -> Self {
self.constant = Some(c);
self
}
pub fn operator(mut self, c: Color) -> Self {
self.operator = Some(c);
self
}
pub fn variable(mut self, c: Color) -> Self {
self.variable = Some(c);
self
}
pub fn punctuation(mut self, c: Color) -> Self {
self.punctuation = Some(c);
self
}
fn is_plain(&self) -> bool {
self.keyword.is_none()
&& self.string.is_none()
&& self.comment.is_none()
&& self.number.is_none()
&& self.function.is_none()
&& self.type_.is_none()
&& self.constant.is_none()
&& self.operator.is_none()
&& self.variable.is_none()
&& self.punctuation.is_none()
}
fn color_for(&self, scopes: &[Scope]) -> Color {
for scope in scopes {
let s = scope.build_string();
if s.starts_with("comment") {
return self.comment.unwrap_or(self.foreground);
}
if s.starts_with("string") {
return self.string.unwrap_or(self.foreground);
}
}
for scope in scopes.iter().rev() {
if let Some(color) = classify(&scope.build_string()).and_then(|c| self.category_color(c)) {
return color;
}
}
self.foreground
}
fn category_color(&self, category: Category) -> Option<Color> {
match category {
Category::Keyword => self.keyword,
Category::Number => self.number,
Category::Function => self.function,
Category::Type => self.type_,
Category::Constant => self.constant,
Category::Operator => self.operator,
Category::Variable => self.variable,
Category::Punctuation => self.punctuation,
}
}
}
#[derive(Copy, Clone)]
enum Category {
Keyword,
Number,
Function,
Type,
Constant,
Operator,
Variable,
Punctuation,
}
fn classify(scope: &str) -> Option<Category> {
if scope.starts_with("keyword.operator") {
Some(Category::Operator)
} else if scope.starts_with("keyword") || scope.starts_with("storage") {
Some(Category::Keyword)
} else if scope.starts_with("constant.numeric") {
Some(Category::Number)
} else if scope.starts_with("constant") {
Some(Category::Constant)
} else if scope.starts_with("entity.name.function")
|| scope.starts_with("support.function")
|| scope.starts_with("variable.function")
|| scope.starts_with("meta.function-call")
{
Some(Category::Function)
} else if scope.starts_with("entity.name.type")
|| scope.starts_with("entity.name.class")
|| scope.starts_with("entity.name.struct")
|| scope.starts_with("entity.name.enum")
|| scope.starts_with("entity.name.trait")
|| scope.starts_with("entity.name.namespace")
|| scope.starts_with("support.type")
|| scope.starts_with("support.class")
|| scope.starts_with("entity.other.inherited-class")
{
Some(Category::Type)
} else if scope.starts_with("variable") {
Some(Category::Variable)
} else if scope.starts_with("punctuation") {
Some(Category::Punctuation)
} else {
None
}
}
pub(crate) struct CodeData {
palette: RefCell<Palette>,
language: Cell<Language>,
cache: RefCell<Vec<(CodeKey, Rc<Vec<Color>>)>>,
}
#[derive(Clone, PartialEq)]
struct CodeKey {
text: String,
language: Language,
palette: Palette,
}
const CACHE_CAP: usize = 4;
impl CodeData {
pub(crate) fn new() -> Self {
CodeData {
palette: RefCell::new(Palette::default()),
language: Cell::new(Language::default()),
cache: RefCell::new(Vec::new()),
}
}
pub(crate) fn set_palette(&self, palette: Palette) {
*self.palette.borrow_mut() = palette;
self.cache.borrow_mut().clear();
}
pub(crate) fn set_language(&self, language: Language) {
self.language.set(language);
self.cache.borrow_mut().clear();
}
pub(crate) fn foreground(&self) -> Color {
self.palette.borrow().foreground
}
pub(crate) fn char_colors(&self, text: &str) -> Rc<Vec<Color>> {
let palette = self.palette.borrow().clone();
let language = self.language.get();
if let Some(hit) = self.cache.borrow().iter().find_map(|(k, v)| {
(k.text == text && k.language == language && k.palette == palette).then(|| Rc::clone(v))
}) {
return hit;
}
let colors = Rc::new(compute_colors(text, language, &palette));
let mut cache = self.cache.borrow_mut();
cache.insert(0, (CodeKey { text: text.to_owned(), language, palette }, Rc::clone(&colors)));
cache.truncate(CACHE_CAP);
colors
}
}
fn syntax_set() -> &'static SyntaxSet {
static SET: OnceLock<SyntaxSet> = OnceLock::new();
SET.get_or_init(SyntaxSet::load_defaults_newlines)
}
fn compute_colors(text: &str, language: Language, palette: &Palette) -> Vec<Color> {
let count = text.chars().count();
let foreground = palette.foreground;
let token = match language.token() {
Some(token) if !palette.is_plain() => token,
_ => return vec![foreground; count],
};
let syntax_set = syntax_set();
let syntax = match syntax_set.find_syntax_by_token(token) {
Some(syntax) => syntax,
None => return vec![foreground; count],
};
let mut parse = ParseState::new(syntax);
let mut stack = ScopeStack::new();
let mut colors = Vec::with_capacity(count);
for line in text.split_inclusive('\n') {
let ops = parse.parse_line(line, syntax_set).unwrap_or_default();
let mut ops = ops.into_iter().peekable();
for (byte_offset, _ch) in line.char_indices() {
while let Some(&(offset, _)) = ops.peek() {
if offset <= byte_offset {
let (_, op) = ops.next().unwrap();
let _ = stack.apply(&op);
} else {
break;
}
}
colors.push(palette.color_for(stack.as_slice()));
}
for (_, op) in ops {
let _ = stack.apply(&op);
}
}
colors
}
#[cfg(test)]
mod tests {
use super::*;
fn char_index_of(text: &str, needle: &str) -> usize {
let byte = text.find(needle).expect("the substring is present in the text");
text[..byte].chars().count()
}
#[test]
fn plain_language_paints_everything_with_foreground() {
let fg = Color::from_rgba8(10, 20, 30, 255);
let palette = Palette::new(fg).keyword(Color::from_rgba8(200, 0, 0, 255));
let code = CodeData::new();
code.set_palette(palette);
let text = "fn main() {}";
let colors = code.char_colors(text);
assert_eq!(colors.len(), text.chars().count());
assert!(colors.iter().all(|c| *c == fg), "only the base color was expected");
}
#[test]
fn empty_palette_skips_highlighting() {
let fg = Color::from_rgba8(1, 2, 3, 255);
let code = CodeData::new();
code.set_palette(Palette::new(fg));
code.set_language(Language::Rust);
let colors = code.char_colors("fn main() {}");
assert!(colors.iter().all(|c| *c == fg));
}
#[test]
fn rust_keyword_string_and_comment_take_palette_colors() {
let fg = Color::from_rgba8(212, 212, 212, 255);
let kw = Color::from_rgba8(197, 134, 192, 255);
let st = Color::from_rgba8(206, 145, 120, 255);
let cm = Color::from_rgba8(106, 153, 85, 255);
let palette = Palette::new(fg).keyword(kw).string(st).comment(cm);
let code = CodeData::new();
code.set_palette(palette);
code.set_language(Language::Rust);
let text = "let x = \"hi\"; // note";
let colors = code.char_colors(text);
assert_eq!(colors.len(), text.chars().count());
assert_eq!(colors[char_index_of(text, "let")], kw);
assert_eq!(colors[char_index_of(text, "hi")], st);
assert_eq!(colors[char_index_of(text, "// note")], cm);
assert_eq!(colors[char_index_of(text, "note")], cm);
}
#[test]
fn unset_category_falls_back_to_foreground() {
let fg = Color::from_rgba8(50, 50, 50, 255);
let kw = Color::from_rgba8(0, 100, 200, 255);
let palette = Palette::new(fg).keyword(kw); let code = CodeData::new();
code.set_palette(palette);
code.set_language(Language::Rust);
let text = "let n = 42;";
let colors = code.char_colors(text);
assert_eq!(colors[char_index_of(text, "let")], kw);
assert_eq!(colors[char_index_of(text, "42")], fg, "a number without a rule — in the base color");
}
#[test]
fn colors_align_with_multiline_chars() {
let code = CodeData::new();
code.set_language(Language::Rust);
code.set_palette(Palette::new(Color::BLACK).keyword(Color::WHITE));
let text = "fn a() {}\nfn b() {}\n";
let colors = code.char_colors(text);
assert_eq!(colors.len(), text.chars().count());
}
#[test]
fn cache_returns_same_allocation_for_repeated_calls() {
let code = CodeData::new();
code.set_language(Language::Rust);
code.set_palette(Palette::new(Color::BLACK).keyword(Color::WHITE));
let a = code.char_colors("fn main() {}");
let b = code.char_colors("fn main() {}");
assert!(Rc::ptr_eq(&a, &b), "re-parsing the same text is taken from the cache");
code.set_palette(Palette::new(Color::BLACK).keyword(Color::from_rgba8(1, 1, 1, 255)));
let c = code.char_colors("fn main() {}");
assert!(!Rc::ptr_eq(&a, &c), "after changing the palette — recompute");
}
}