use std::sync::LazyLock;
use syntect::easy::HighlightLines;
use syntect::highlighting::ThemeSet;
use syntect::parsing::{SyntaxReference, SyntaxSet};
#[cfg(test)]
use syntect::util::as_24_bit_terminal_escaped;
static SYNTAX_SET: LazyLock<SyntaxSet> = LazyLock::new(two_face::syntax::extra_newlines);
static THEME_SET: LazyLock<ThemeSet> = LazyLock::new(ThemeSet::load_defaults);
fn find_syntax(lang: &str) -> Option<&'static SyntaxReference> {
let ss = &*SYNTAX_SET;
let patched = match lang {
"csharp" | "c-sharp" => "c#",
"golang" => "go",
"python3" => "python",
"shell" => "bash",
other => other,
};
if let Some(s) = ss.find_syntax_by_token(patched) {
return Some(s);
}
if let Some(s) = ss.find_syntax_by_name(patched) {
return Some(s);
}
let lower = patched.to_ascii_lowercase();
if let Some(s) = ss
.syntaxes()
.iter()
.find(|s| s.name.to_ascii_lowercase() == lower)
{
return Some(s);
}
ss.find_syntax_by_extension(lang)
}
pub const MAX_HIGHLIGHT_BYTES: usize = 512 * 1024;
pub const MAX_HIGHLIGHT_LINES: usize = 10_000;
pub fn exceeds_highlight_limits(total_bytes: usize, total_lines: usize) -> bool {
total_bytes > MAX_HIGHLIGHT_BYTES || total_lines > MAX_HIGHLIGHT_LINES
}
pub struct CodeHighlighter {
state: Option<HighlightLines<'static>>,
}
impl CodeHighlighter {
pub fn new(lang: &str) -> Self {
if !crate::theme::syntax_highlight_enabled() {
return Self { state: None };
}
let state = find_syntax(lang).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 highlight_inline(snippet: &str, lang: &str) -> Vec<ratatui::text::Span<'static>> {
let flat = flatten_for_inline(snippet);
let mut hl = CodeHighlighter::new(lang);
hl.highlight_spans(&flat)
}
fn flatten_for_inline(s: &str) -> String {
s.chars()
.map(|c| match c {
'\n' | '\r' | '\t' => ' ',
other => other,
})
.collect()
}
#[cfg(test)]
mod inline_tests {
use super::*;
#[test]
fn flatten_collapses_newlines_and_tabs() {
assert_eq!(flatten_for_inline("a\nb\tc\rd"), "a b c d");
}
#[test]
fn flatten_passthrough_when_no_specials() {
assert_eq!(flatten_for_inline("hello world"), "hello world");
}
#[test]
fn highlight_inline_bash_produces_colored_spans() {
if !crate::theme::syntax_highlight_enabled() {
return;
}
let spans = highlight_inline("ls -la /tmp", "bash");
assert!(
spans.len() >= 2,
"expected multiple spans for bash, got {}",
spans.len()
);
let combined: String = spans.iter().map(|s| s.content.as_ref()).collect();
assert_eq!(combined, "ls -la /tmp");
}
#[test]
fn highlight_inline_unknown_lang_falls_back_to_plain() {
let spans = highlight_inline("anything goes", "notalang");
assert_eq!(spans.len(), 1);
assert_eq!(spans[0].content.as_ref(), "anything goes");
}
#[test]
fn highlight_inline_flattens_multiline_input() {
let spans = highlight_inline("echo hi\necho bye", "bash");
let combined: String = spans.iter().map(|s| s.content.as_ref()).collect();
assert!(
!combined.contains('\n'),
"newline leaked into header: {combined:?}"
);
}
}
pub fn pre_highlight(content: &str, ext: &str) -> Vec<Vec<ratatui::text::Span<'static>>> {
let line_count = content.lines().count();
if exceeds_highlight_limits(content.len(), line_count) {
return content
.lines()
.map(|line| vec![ratatui::text::Span::raw(line.to_string())])
.collect();
}
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());
}
#[test]
fn test_size_guardrail_below_threshold() {
assert!(!exceeds_highlight_limits(1024, 5));
}
#[test]
fn test_size_guardrail_byte_cap() {
assert!(exceeds_highlight_limits(MAX_HIGHLIGHT_BYTES + 1, 1));
}
#[test]
fn test_size_guardrail_line_cap() {
assert!(exceeds_highlight_limits(1, MAX_HIGHLIGHT_LINES + 1));
}
#[test]
fn test_pre_highlight_falls_back_for_huge_input() {
let big = "x\n".repeat(MAX_HIGHLIGHT_LINES + 5);
let lines = pre_highlight(&big, "rs");
assert_eq!(lines.len(), MAX_HIGHLIGHT_LINES + 5);
for spans in &lines {
assert_eq!(spans.len(), 1, "expected plain fallback, got highlighted");
}
}
#[test]
fn toml_resolves_with_two_face() {
assert!(
find_syntax("toml").is_some(),
"TOML syntax missing — two-face dep may have been removed or downgraded"
);
}
#[test]
fn typescript_resolves_with_two_face() {
assert!(find_syntax("ts").is_some(), "TypeScript (.ts) not found");
assert!(find_syntax("tsx").is_some(), "TypeScript (.tsx) not found");
}
#[test]
fn common_languages_all_resolve() {
let must_resolve = [
("rs", "Rust"),
("py", "Python"),
("js", "JavaScript"),
("ts", "TypeScript"),
("toml", "TOML"),
("json", "JSON"),
("yaml", "YAML"),
("md", "Markdown"),
("sh", "Bash"),
("go", "Go"),
("html", "HTML"),
("css", "CSS"),
("kt", "Kotlin"),
("swift", "Swift"),
("lua", "Lua"),
("proto", "Protobuf"),
];
for (ext, name) in must_resolve {
assert!(
find_syntax(ext).is_some(),
"{name} (.{ext}) not found in SYNTAX_SET — two-face may be misconfigured"
);
}
}
#[test]
fn llm_aliases_resolve_via_patching() {
assert!(find_syntax("golang").is_some(), "golang alias broken");
assert!(find_syntax("python3").is_some(), "python3 alias broken");
assert!(find_syntax("shell").is_some(), "shell alias broken");
assert!(find_syntax("csharp").is_some(), "csharp alias broken");
assert!(find_syntax("c-sharp").is_some(), "c-sharp alias broken");
}
#[test]
fn toml_produces_colored_spans() {
if !crate::theme::syntax_highlight_enabled() {
return;
}
let mut h = CodeHighlighter::new("toml");
let spans = h.highlight_spans("[package]\nname = \"koda\"");
assert!(
spans.len() > 1,
"TOML should produce multiple colored spans, got: {spans:?}"
);
}
}