use ratatui::style::{Color, Modifier, Style};
use syntect::easy::HighlightLines;
use syntect::highlighting::{FontStyle, Style as SyntectStyle, ThemeSet};
use syntect::parsing::{SyntaxReference, SyntaxSet};
use crate::studio::theme;
static SYNTAX_SET: std::sync::LazyLock<SyntaxSet> =
std::sync::LazyLock::new(SyntaxSet::load_defaults_newlines);
static THEME_SET: std::sync::LazyLock<ThemeSet> = std::sync::LazyLock::new(ThemeSet::load_defaults);
pub struct SyntaxHighlighter {
syntax: Option<&'static SyntaxReference>,
}
impl SyntaxHighlighter {
#[must_use]
pub fn for_extension(ext: &str) -> Self {
let syntax = SYNTAX_SET.find_syntax_by_extension(ext);
Self { syntax }
}
#[must_use]
pub fn for_path(path: &std::path::Path) -> Self {
let ext = path.extension().and_then(|e| e.to_str()).unwrap_or("");
Self::for_extension(ext)
}
#[must_use]
pub fn is_available(&self) -> bool {
self.syntax.is_some()
}
#[must_use]
pub fn highlight_line(&self, line: &str) -> Vec<(Style, String)> {
let Some(syntax) = self.syntax else {
return vec![(
Style::default().fg(theme::text_primary_color()),
line.to_string(),
)];
};
let Some(theme) = THEME_SET
.themes
.get("base16-ocean.dark")
.or_else(|| THEME_SET.themes.get("InspiredGitHub"))
.or_else(|| THEME_SET.themes.values().next())
else {
return vec![(
Style::default().fg(theme::text_primary_color()),
line.to_string(),
)];
};
let mut highlighter = HighlightLines::new(syntax, theme);
match highlighter.highlight_line(line, &SYNTAX_SET) {
Ok(ranges) => ranges
.into_iter()
.map(|(style, text)| (syntect_to_ratatui(style), text.to_string()))
.collect(),
Err(_) => vec![(
Style::default().fg(theme::text_primary_color()),
line.to_string(),
)],
}
}
#[must_use]
pub fn highlight_lines(&self, lines: &[String]) -> Vec<Vec<(Style, String)>> {
lines.iter().map(|line| self.highlight_line(line)).collect()
}
}
fn syntect_to_ratatui(style: SyntectStyle) -> Style {
let fg = syntect_color_to_silkcircuit(style.foreground);
let mut ratatui_style = Style::default().fg(fg);
if style.font_style.contains(FontStyle::BOLD) {
ratatui_style = ratatui_style.add_modifier(Modifier::BOLD);
}
if style.font_style.contains(FontStyle::ITALIC) {
ratatui_style = ratatui_style.add_modifier(Modifier::ITALIC);
}
if style.font_style.contains(FontStyle::UNDERLINE) {
ratatui_style = ratatui_style.add_modifier(Modifier::UNDERLINED);
}
ratatui_style
}
fn syntect_color_to_silkcircuit(color: syntect::highlighting::Color) -> Color {
let r = color.r;
let g = color.g;
let b = color.b;
let saturation = color_saturation(r, g, b);
let luminance = color_luminance(r, g, b);
if is_purple_ish(r, g, b) {
return theme::accent_primary();
}
if is_green_ish(r, g, b) && saturation > 0.3 {
return theme::success_color();
}
if is_orange_ish(r, g, b) {
return theme::accent_tertiary();
}
if is_cyan_ish(r, g, b) {
return theme::accent_secondary();
}
if is_yellow_ish(r, g, b) {
return theme::warning_color();
}
if saturation < 0.15 && luminance < 0.6 {
return theme::text_muted_color();
}
if luminance > 0.2 {
Color::Rgb(r, g, b)
} else {
theme::text_secondary_color()
}
}
fn color_saturation(r: u8, g: u8, b: u8) -> f32 {
let max = f32::from(r.max(g).max(b));
let min = f32::from(r.min(g).min(b));
if max == 0.0 { 0.0 } else { (max - min) / max }
}
fn color_luminance(r: u8, g: u8, b: u8) -> f32 {
(0.299 * f32::from(r) + 0.587 * f32::from(g) + 0.114 * f32::from(b)) / 255.0
}
fn is_purple_ish(r: u8, g: u8, b: u8) -> bool {
r > 150 && g < 150 && b > 150
}
fn is_green_ish(r: u8, g: u8, b: u8) -> bool {
g > r && g > b && g > 120
}
fn is_orange_ish(r: u8, g: u8, b: u8) -> bool {
r > 180 && g > 80 && g < 180 && b < 150
}
fn is_cyan_ish(r: u8, g: u8, b: u8) -> bool {
r < 150 && g > 150 && b > 150
}
fn is_yellow_ish(r: u8, g: u8, b: u8) -> bool {
r > 180 && g > 180 && b < 150
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_highlighter_rust() {
let highlighter = SyntaxHighlighter::for_extension("rs");
assert!(highlighter.is_available());
let spans = highlighter.highlight_line("fn main() { }");
assert!(!spans.is_empty());
}
#[test]
fn test_highlighter_unknown() {
let highlighter = SyntaxHighlighter::for_extension("xyz_unknown");
assert!(!highlighter.is_available());
}
}