use std::sync::LazyLock;
use ratatui::style::{Color, Modifier, Style};
use ratatui::text::{Line, Span};
use syntect::easy::HighlightLines;
use syntect::highlighting::{self, ThemeSet};
use syntect::parsing::SyntaxSet;
static HIGHLIGHTER: LazyLock<SyntaxHighlighter> = LazyLock::new(SyntaxHighlighter::new);
const LANG_ALIASES: &[(&str, &str)] = &[
("sh", "Bourne Again Shell (bash)"),
("bash", "Bourne Again Shell (bash)"),
("js", "JavaScript"),
("javascript", "JavaScript"),
("ts", "TypeScript"),
("typescript", "TypeScript"),
("py", "Python"),
("python", "Python"),
("yml", "YAML"),
("yaml", "YAML"),
("json", "JSON"),
("rust", "Rust"),
("rs", "Rust"),
("go", "Go"),
("c", "C"),
("cpp", "C++"),
("rb", "Ruby"),
("ruby", "Ruby"),
("toml", "TOML"),
("md", "Markdown"),
("markdown", "Markdown"),
("sql", "SQL"),
("html", "HTML"),
("css", "CSS"),
("xml", "XML"),
("java", "Java"),
];
pub struct SyntaxHighlighter {
syntax_set: SyntaxSet,
theme_set: ThemeSet,
}
impl SyntaxHighlighter {
fn new() -> Self {
Self {
syntax_set: SyntaxSet::load_defaults_newlines(),
theme_set: ThemeSet::load_defaults(),
}
}
fn find_syntax(&self, lang: &str) -> Option<&syntect::parsing::SyntaxReference> {
let lang_lower = lang.to_lowercase();
for &(alias, canonical) in LANG_ALIASES {
if alias == lang_lower
&& let Some(syn) = self.syntax_set.find_syntax_by_name(canonical)
{
return Some(syn);
}
}
if let Some(syn) = self.syntax_set.find_syntax_by_name(lang) {
return Some(syn);
}
self.syntax_set.find_syntax_by_extension(lang)
}
}
#[must_use]
pub fn highlight_code(code: &str, lang: &str) -> Vec<Line<'static>> {
let highlighter = &*HIGHLIGHTER;
let Some(syntax) = highlighter.find_syntax(lang) else {
return highlight_plain(code);
};
let Some(theme) = highlighter.theme_set.themes.get("base16-ocean.dark") else {
return highlight_plain(code);
};
let mut h = HighlightLines::new(syntax, theme);
let mut lines = Vec::new();
for line_str in code.lines() {
let Ok(regions) = h.highlight_line(line_str, &highlighter.syntax_set) else {
return highlight_plain(code);
};
let spans: Vec<Span<'static>> = regions
.into_iter()
.map(|(style, text)| {
let fg = syntect_to_ratatui_color(style.foreground);
let mut ratatui_style = Style::default().fg(fg);
if style.font_style.contains(highlighting::FontStyle::BOLD) {
ratatui_style = ratatui_style.add_modifier(Modifier::BOLD);
}
if style.font_style.contains(highlighting::FontStyle::ITALIC) {
ratatui_style = ratatui_style.add_modifier(Modifier::ITALIC);
}
if style
.font_style
.contains(highlighting::FontStyle::UNDERLINE)
{
ratatui_style = ratatui_style.add_modifier(Modifier::UNDERLINED);
}
Span::styled(text.to_string(), ratatui_style)
})
.collect();
lines.push(Line::from(spans));
}
lines
}
fn highlight_plain(code: &str) -> Vec<Line<'static>> {
let style = Style::default().fg(Color::Rgb(200, 200, 200));
code.lines()
.map(|line| Line::from(Span::styled(line.to_string(), style)))
.collect()
}
fn syntect_to_ratatui_color(color: highlighting::Color) -> Color {
Color::Rgb(color.r, color.g, color.b)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn highlight_rust_returns_non_empty() {
let lines = highlight_code("fn main() {}", "rust");
assert!(!lines.is_empty(), "rust highlighting should produce lines");
}
#[test]
fn highlight_python_returns_non_empty() {
let lines = highlight_code("print('hello')", "python");
assert!(
!lines.is_empty(),
"python highlighting should produce lines"
);
}
#[test]
fn highlight_unknown_lang_falls_back() {
let lines = highlight_code("some code here", "unknown-lang");
assert!(!lines.is_empty(), "unknown lang should fall back to plain");
let first = &lines[0];
let span = &first.spans[0];
assert_eq!(span.style.fg, Some(Color::Rgb(200, 200, 200)));
}
#[test]
fn highlight_empty_code() {
let lines = highlight_code("", "rust");
assert!(lines.len() <= 1);
}
#[test]
fn highlight_multiline_code() {
let code = "fn main() {\n println!(\"hello\");\n}";
let lines = highlight_code(code, "rust");
assert_eq!(lines.len(), 3);
}
#[test]
fn lang_alias_sh() {
let lines = highlight_code("echo hello", "sh");
assert!(!lines.is_empty());
}
#[test]
fn lang_alias_js() {
let lines = highlight_code("console.log('hi')", "js");
assert!(!lines.is_empty());
}
#[test]
fn lang_alias_py() {
let lines = highlight_code("x = 1", "py");
assert!(!lines.is_empty());
}
#[test]
fn lang_alias_yml() {
let lines = highlight_code("key: value", "yml");
assert!(!lines.is_empty());
}
}