use lv_tui::style::{Color, Style};
use syntect::easy::HighlightLines;
use syntect::highlighting::ThemeSet;
use syntect::parsing::SyntaxSet;
pub struct CodeHighlight {
pub lines: Vec<StyledLine>,
}
#[derive(Debug, Clone)]
pub struct StyledSpan {
pub text: String,
pub style: Style,
}
#[derive(Debug, Clone)]
pub struct StyledLine {
pub spans: Vec<StyledSpan>,
}
impl CodeHighlight {
pub fn new(code: &str, language: &str) -> Self {
let ss = load_syntax_set();
let ts = ThemeSet::load_defaults();
let theme = &ts.themes["base16-ocean.dark"];
let syntax = ss.find_syntax_by_token(language)
.unwrap_or_else(|| ss.find_syntax_plain_text());
let mut highlighter = HighlightLines::new(syntax, theme);
let mut lines = Vec::new();
for line_str in code.lines() {
let ops = highlighter.highlight_line(line_str, &ss).unwrap_or_default();
let spans: Vec<StyledSpan> = ops.into_iter().map(|(style, text)| {
let mut lv_style = syntect_style_to_lv(&style);
if style.font_style.contains(syntect::highlighting::FontStyle::BOLD) {
lv_style = lv_style.bold();
}
StyledSpan { text: text.to_string(), style: lv_style }
}).collect();
lines.push(StyledLine { spans });
}
Self { lines }
}
pub fn line_count(&self) -> usize { self.lines.len() }
pub fn render_line(&self, index: usize, buffer: &mut lv_tui::buffer::Buffer, pos: lv_tui::geom::Pos, clip: lv_tui::geom::Rect) {
if let Some(line) = self.lines.get(index) {
let mut x = pos.x;
for span in &line.spans {
if span.text.is_empty() { continue; }
buffer.write_text(lv_tui::geom::Pos { x, y: pos.y }, clip, &span.text, &span.style);
let w: u16 = span.text.chars()
.map(|c| unicode_width::UnicodeWidthChar::width(c).unwrap_or(0) as u16)
.sum();
x = x.saturating_add(w);
}
}
}
}
fn syntect_style_to_lv(s: &syntect::highlighting::Style) -> Style {
let mut style = Style::default();
style = style.fg(syntect_color_to_lv(s.foreground));
if s.font_style.contains(syntect::highlighting::FontStyle::ITALIC) {
style = style.italic();
}
style
}
fn syntect_color_to_lv(c: syntect::highlighting::Color) -> Color {
let r = c.r;
let g = c.g;
let b = c.b;
let bright = r.max(g).max(b);
let dull = r.min(g).min(b);
let sat = bright - dull;
if bright < 40 { return Color::Black; }
if bright < 100 { return Color::Gray; }
if sat < 25 { return if bright > 180 { Color::White } else { Color::Gray }; }
if r > g && r > b { return if r > 180 { Color::Red } else { Color::Red }; }
if g > r && g > b { return if g > 180 { Color::Green } else { Color::Green }; }
if b > r && b > g { return if b > 180 { Color::Blue } else { Color::Blue }; }
if r > 180 && g > 180 { return Color::Yellow; }
if r > 150 && b > 150 { return Color::Magenta; }
if g > 150 && b > 150 { return Color::Cyan; }
if bright > 180 { Color::White } else { Color::Gray }
}
fn load_syntax_set() -> SyntaxSet {
SyntaxSet::load_defaults_newlines()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_rust_highlight() {
let hl = CodeHighlight::new("fn main() {\n println!(\"hello\");\n}\n", "rust");
assert!(hl.line_count() >= 3);
assert!(!hl.lines[0].spans.is_empty());
}
#[test]
fn test_unknown_language() {
let hl = CodeHighlight::new("some code", "nonexistent-lang");
assert!(hl.line_count() >= 1);
}
#[test]
fn test_empty() {
let hl = CodeHighlight::new("", "rust");
assert_eq!(hl.line_count(), 0);
}
}