use crate::tui::theme;
use std::sync::OnceLock;
use syntect::easy::HighlightLines;
use syntect::highlighting::{FontStyle, ThemeSet};
use syntect::parsing::SyntaxSet;
use syntect::util::LinesWithEndings;
static SYNTAX_SET: OnceLock<SyntaxSet> = OnceLock::new();
static THEME_SET: OnceLock<ThemeSet> = OnceLock::new();
fn get_syntax_set() -> &'static SyntaxSet {
SYNTAX_SET.get_or_init(SyntaxSet::load_defaults_newlines)
}
fn get_theme_set() -> &'static ThemeSet {
THEME_SET.get_or_init(ThemeSet::load_defaults)
}
fn normalize_language(lang: &str) -> &str {
let lang = lang.split([',', ' ']).next().unwrap_or(lang).trim();
match lang.to_lowercase().as_str() {
"js" => "javascript",
"ts" => "typescript",
"sh" | "shell" => "bash",
"yml" => "yaml",
"py" => "python",
"rb" => "ruby",
"md" => "markdown",
"dockerfile" => "Dockerfile",
_ => lang,
}
}
pub struct HighlightedToken {
pub text: String,
pub fg: (u8, u8, u8),
pub bold: bool,
pub italic: bool,
}
pub fn highlight_code_ansi(code: &str, lang: &str) -> Option<String> {
if !colored::control::SHOULD_COLORIZE.should_colorize() {
return None;
}
let ps = get_syntax_set();
let ts = get_theme_set();
let lang = normalize_language(lang);
let syntax = ps.find_syntax_by_token(lang)?;
let theme = ts.themes.get(theme::detect_theme().syntect_theme)?;
let mut highlighter = HighlightLines::new(syntax, theme);
let mut output = String::new();
for line in LinesWithEndings::from(code) {
let ranges = highlighter.highlight_line(line, ps).ok()?;
for (style, text) in ranges {
let fg = style.foreground;
output.push_str(&format!("\x1b[38;2;{};{};{}m", fg.r, fg.g, fg.b));
if style.font_style.contains(FontStyle::BOLD) {
output.push_str("\x1b[1m");
}
if style.font_style.contains(FontStyle::ITALIC) {
output.push_str("\x1b[3m");
}
output.push_str(text);
output.push_str("\x1b[0m");
}
}
Some(output)
}
pub fn highlight_code_tui(code: &str, lang: &str) -> Option<Vec<Vec<HighlightedToken>>> {
let ps = get_syntax_set();
let ts = get_theme_set();
let lang = normalize_language(lang);
let syntax = ps.find_syntax_by_token(lang)?;
let theme = ts.themes.get(theme::detect_theme().syntect_theme)?;
let mut highlighter = HighlightLines::new(syntax, theme);
let mut lines = Vec::new();
for line in LinesWithEndings::from(code) {
let ranges = highlighter.highlight_line(line, ps).ok()?;
let tokens: Vec<HighlightedToken> = ranges
.into_iter()
.map(|(style, text)| HighlightedToken {
text: text.to_string(),
fg: (style.foreground.r, style.foreground.g, style.foreground.b),
bold: style.font_style.contains(FontStyle::BOLD),
italic: style.font_style.contains(FontStyle::ITALIC),
})
.collect();
lines.push(tokens);
}
Some(lines)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_normalize_language() {
assert_eq!(normalize_language("js"), "javascript");
assert_eq!(normalize_language("ts"), "typescript");
assert_eq!(normalize_language("sh"), "bash");
assert_eq!(normalize_language("rust"), "rust");
assert_eq!(normalize_language("rust,ignore"), "rust");
assert_eq!(normalize_language("rust title=x"), "rust");
}
#[test]
fn test_highlight_known_language() {
colored::control::set_override(true);
let code = "let x = 1;";
let result = highlight_code_ansi(code, "rust");
assert!(result.is_some());
let output = result.unwrap();
assert!(
output.contains("\x1b["),
"Expected ANSI codes, got: {:?}",
output
);
assert!(
output.matches("\x1b[38;2;").count() > 1,
"Expected multiple color codes for different tokens"
);
}
#[test]
fn test_highlight_unknown_language() {
let code = "some code";
let result = highlight_code_ansi(code, "unknown_language_xyz");
assert!(result.is_none());
}
#[test]
fn test_highlight_tui_known_language() {
let code = "fn main() {}";
let result = highlight_code_tui(code, "rust");
assert!(result.is_some());
let lines = result.unwrap();
assert!(!lines.is_empty());
}
#[test]
fn test_highlight_with_alias() {
colored::control::set_override(true);
let code = "const x = 1;";
let result = highlight_code_ansi(code, "js");
assert!(result.is_some());
}
}