use once_cell::sync::Lazy;
use ratatui::{
style::{Color, Style},
text::{Line, Span},
};
use syntect::{
easy::HighlightLines,
highlighting::{FontStyle, Theme, ThemeSet},
parsing::{SyntaxReference, SyntaxSet},
util::LinesWithEndings,
};
static SYNTAX_SET: Lazy<SyntaxSet> = Lazy::new(SyntaxSet::load_defaults_newlines);
static THEME_SET: Lazy<ThemeSet> = Lazy::new(ThemeSet::load_defaults);
fn get_theme() -> &'static Theme {
&THEME_SET.themes["base16-ocean.dark"]
}
fn syntect_to_ratatui_color(color: syntect::highlighting::Color) -> Color {
Color::Rgb(color.r, color.g, color.b)
}
fn syntect_style_to_ratatui(syntect_style: syntect::highlighting::Style) -> Style {
let mut style = Style::default().fg(syntect_to_ratatui_color(syntect_style.foreground));
if syntect_style.font_style.contains(FontStyle::BOLD) {
style = style.add_modifier(ratatui::style::Modifier::BOLD);
}
if syntect_style.font_style.contains(FontStyle::ITALIC) {
style = style.add_modifier(ratatui::style::Modifier::ITALIC);
}
if syntect_style.font_style.contains(FontStyle::UNDERLINE) {
style = style.add_modifier(ratatui::style::Modifier::UNDERLINED);
}
style
}
fn find_syntax(language: &str) -> Option<&'static SyntaxReference> {
if let Some(syntax) = SYNTAX_SET.find_syntax_by_token(language) {
return Some(syntax);
}
let language_lower = language.to_lowercase();
SYNTAX_SET.syntaxes().iter().find(|s| {
s.name.to_lowercase() == language_lower
|| s.file_extensions
.iter()
.any(|ext| ext.to_lowercase() == language_lower)
})
}
pub fn highlight_code(code: &str, language: &str) -> Vec<Line<'static>> {
let syntax = match find_syntax(language) {
Some(s) => s,
None => {
return code
.lines()
.enumerate()
.map(|(idx, line)| {
Line::from(vec![
Span::styled(
format!("{:3} ", idx + 1),
Style::default().fg(Color::DarkGray),
),
Span::styled(format!("β {}", line), Style::default().fg(Color::Gray)),
])
})
.collect();
}
};
let theme = get_theme();
let mut highlighter = HighlightLines::new(syntax, theme);
let mut lines = Vec::new();
for (line_num, line) in LinesWithEndings::from(code).enumerate() {
let ranges = match highlighter.highlight_line(line, &SYNTAX_SET) {
Ok(ranges) => ranges,
Err(_) => {
lines.push(Line::from(vec![
Span::styled(
format!("{:3} ", line_num + 1),
Style::default().fg(Color::DarkGray),
),
Span::styled(
format!("β {}", line.trim_end()),
Style::default().fg(Color::Gray),
),
]));
continue;
}
};
let mut styled_line = vec![Span::styled(
format!("{:3} ", line_num + 1),
Style::default().fg(Color::DarkGray),
)];
styled_line.push(Span::styled("β ", Style::default().fg(Color::DarkGray)));
for (style, text) in ranges {
styled_line.push(Span::styled(
text.to_string(),
syntect_style_to_ratatui(style),
));
}
lines.push(Line::from(styled_line));
}
lines
}
pub fn supported_languages() -> Vec<String> {
SYNTAX_SET
.syntaxes()
.iter()
.map(|s| s.name.clone())
.collect()
}
pub fn is_language_supported(language: &str) -> bool {
find_syntax(language).is_some()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_highlight_rust() {
let code = "fn main() {\n println!(\"Hello, world!\");\n}";
let lines = highlight_code(code, "rust");
assert_eq!(lines.len(), 3);
assert!(!lines[0].spans.is_empty());
}
#[test]
fn test_highlight_python() {
let code = "def hello():\n print(\"Hello, world!\")";
let lines = highlight_code(code, "python");
assert_eq!(lines.len(), 2);
}
#[test]
fn test_highlight_javascript() {
let code = "function hello() {\n console.log(\"Hello\");\n}";
let lines = highlight_code(code, "javascript");
assert_eq!(lines.len(), 3);
}
#[test]
fn test_highlight_unknown_language() {
let code = "some code";
let lines = highlight_code(code, "unknown_language");
assert_eq!(lines.len(), 1);
}
#[test]
fn test_supported_languages() {
let langs = supported_languages();
assert!(!langs.is_empty());
assert!(langs.contains(&"Rust".to_string()));
assert!(langs.contains(&"Python".to_string()));
}
#[test]
fn test_is_language_supported() {
assert!(is_language_supported("rust"));
assert!(is_language_supported("Rust"));
assert!(is_language_supported("python"));
assert!(is_language_supported("javascript"));
assert!(!is_language_supported("not_a_real_language"));
}
#[test]
fn test_empty_code() {
let code = "";
let lines = highlight_code(code, "rust");
assert!(lines.is_empty() || lines.len() == 1);
}
#[test]
fn test_code_with_special_characters() {
let code = "let x = \"Hello, δΈη!\";";
let lines = highlight_code(code, "rust");
assert_eq!(lines.len(), 1);
}
}