use once_cell::sync::Lazy;
use syntect::easy::HighlightLines;
use syntect::highlighting::ThemeSet;
use syntect::parsing::SyntaxSet;
#[cfg(test)]
use syntect::util::as_24_bit_terminal_escaped;
static SYNTAX_SET: Lazy<SyntaxSet> = Lazy::new(SyntaxSet::load_defaults_newlines);
static THEME_SET: Lazy<ThemeSet> = Lazy::new(ThemeSet::load_defaults);
pub struct CodeHighlighter {
state: Option<HighlightLines<'static>>,
}
impl CodeHighlighter {
pub fn new(lang: &str) -> Self {
let syntax = SYNTAX_SET
.find_syntax_by_token(lang)
.or_else(|| SYNTAX_SET.find_syntax_by_extension(lang));
let state = syntax.map(|syn| {
let theme = &THEME_SET.themes["base16-ocean.dark"];
HighlightLines::new(syn, theme)
});
Self { state }
}
#[cfg(test)]
pub fn highlight_line(&mut self, line: &str) -> String {
match self.state.as_mut() {
Some(h) => {
let ranges = h.highlight_line(line, &SYNTAX_SET).unwrap_or_default();
let escaped = as_24_bit_terminal_escaped(&ranges[..], false);
format!("{escaped}\x1b[0m")
}
None => line.to_string(),
}
}
pub fn highlight_spans(&mut self, line: &str) -> Vec<ratatui::text::Span<'static>> {
use ratatui::style::{Color, Style as RStyle};
use ratatui::text::Span;
match self.state.as_mut() {
Some(h) => {
let ranges = h.highlight_line(line, &SYNTAX_SET).unwrap_or_default();
ranges
.into_iter()
.map(|(style, text)| {
let fg =
Color::Rgb(style.foreground.r, style.foreground.g, style.foreground.b);
Span::styled(text.to_string(), RStyle::default().fg(fg))
})
.collect()
}
None => vec![Span::raw(line.to_string())],
}
}
}
pub fn pre_highlight(content: &str, ext: &str) -> Vec<Vec<ratatui::text::Span<'static>>> {
let mut hl = CodeHighlighter::new(ext);
content
.lines()
.map(|line| hl.highlight_spans(line))
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_known_language_highlights() {
let mut h = CodeHighlighter::new("rust");
let result = h.highlight_line("fn main() {}");
assert!(result.contains("\x1b["));
assert!(result.contains("fn"));
}
#[test]
fn test_unknown_language_passthrough() {
let mut h = CodeHighlighter::new("nonexistent_lang_xyz");
let result = h.highlight_line("hello world");
assert_eq!(result, "hello world");
}
#[test]
fn test_python_highlights() {
let mut h = CodeHighlighter::new("python");
let result = h.highlight_line("def hello():");
assert!(result.contains("\x1b["));
}
#[test]
fn test_extension_lookup() {
let mut h = CodeHighlighter::new("rs");
let result = h.highlight_line("let x = 42;");
assert!(result.contains("\x1b["));
}
#[test]
fn test_highlight_spans_rust() {
let mut h = CodeHighlighter::new("rust");
let spans = h.highlight_spans("fn main() {}");
assert!(!spans.is_empty(), "should produce at least one span");
let text: String = spans.iter().map(|s| s.content.as_ref()).collect();
assert!(text.contains("fn"));
assert!(text.contains("main"));
}
#[test]
fn test_highlight_spans_unknown_lang_passthrough() {
let mut h = CodeHighlighter::new("notalang");
let spans = h.highlight_spans("hello world");
assert_eq!(spans.len(), 1);
assert_eq!(spans[0].content.as_ref(), "hello world");
}
#[test]
fn test_pre_highlight_produces_per_line_spans() {
let content = "fn main() {}\nlet x = 42;\n";
let lines = pre_highlight(content, "rs");
assert_eq!(lines.len(), 2, "should produce one Vec<Span> per line");
for line_spans in &lines {
assert!(!line_spans.is_empty());
}
}
#[test]
fn test_pre_highlight_empty_content() {
let lines = pre_highlight("", "rs");
assert!(lines.is_empty());
}
#[test]
fn test_stateful_multiline_string() {
let mut h = CodeHighlighter::new("rust");
let _line1 = h.highlight_spans("let s = \"");
let line2 = h.highlight_spans("hello\"");
assert!(!line2.is_empty());
}
}