lv_tui_code_highlight/
highlight.rs1use lv_tui::style::{Color, Style};
2use syntect::easy::HighlightLines;
3use syntect::highlighting::ThemeSet;
4use syntect::parsing::SyntaxSet;
5
6pub struct CodeHighlight {
8 pub lines: Vec<StyledLine>,
9}
10
11#[derive(Debug, Clone)]
13pub struct StyledSpan {
14 pub text: String,
15 pub style: Style,
16}
17
18#[derive(Debug, Clone)]
20pub struct StyledLine {
21 pub spans: Vec<StyledSpan>,
22}
23
24impl CodeHighlight {
25 pub fn new(code: &str, language: &str) -> Self {
29 let ss = load_syntax_set();
30 let ts = ThemeSet::load_defaults();
31 let theme = &ts.themes["base16-ocean.dark"];
32 let syntax = ss.find_syntax_by_token(language)
33 .unwrap_or_else(|| ss.find_syntax_plain_text());
34
35 let mut highlighter = HighlightLines::new(syntax, theme);
36
37 let mut lines = Vec::new();
38
39 for line_str in code.lines() {
40 let ops = highlighter.highlight_line(line_str, &ss).unwrap_or_default();
41 let spans: Vec<StyledSpan> = ops.into_iter().map(|(style, text)| {
42 let mut lv_style = syntect_style_to_lv(&style);
43 if style.font_style.contains(syntect::highlighting::FontStyle::BOLD) {
44 lv_style = lv_style.bold();
45 }
46 StyledSpan { text: text.to_string(), style: lv_style }
47 }).collect();
48 lines.push(StyledLine { spans });
49 }
50
51 Self { lines }
52 }
53
54 pub fn line_count(&self) -> usize { self.lines.len() }
55
56 pub fn render_line(&self, index: usize, buffer: &mut lv_tui::buffer::Buffer, pos: lv_tui::geom::Pos, clip: lv_tui::geom::Rect) {
58 if let Some(line) = self.lines.get(index) {
59 let mut x = pos.x;
60 for span in &line.spans {
61 if span.text.is_empty() { continue; }
62 buffer.write_text(lv_tui::geom::Pos { x, y: pos.y }, clip, &span.text, &span.style);
63 let w: u16 = span.text.chars()
64 .map(|c| unicode_width::UnicodeWidthChar::width(c).unwrap_or(0) as u16)
65 .sum();
66 x = x.saturating_add(w);
67 }
68 }
69 }
70}
71
72fn syntect_style_to_lv(s: &syntect::highlighting::Style) -> Style {
74 let mut style = Style::default();
75 style = style.fg(syntect_color_to_lv(s.foreground));
76 if s.font_style.contains(syntect::highlighting::FontStyle::ITALIC) {
78 style = style.italic();
79 }
80 style
81}
82
83fn syntect_color_to_lv(c: syntect::highlighting::Color) -> Color {
84 let r = c.r;
87 let g = c.g;
88 let b = c.b;
89 let bright = r.max(g).max(b);
90 let dull = r.min(g).min(b);
91 let sat = bright - dull;
92
93 if bright < 40 { return Color::Black; }
94 if bright < 100 { return Color::Gray; }
95
96 if sat < 25 { return if bright > 180 { Color::White } else { Color::Gray }; }
98 if r > g && r > b { return if r > 180 { Color::Red } else { Color::Red }; }
99 if g > r && g > b { return if g > 180 { Color::Green } else { Color::Green }; }
100 if b > r && b > g { return if b > 180 { Color::Blue } else { Color::Blue }; }
101 if r > 180 && g > 180 { return Color::Yellow; }
102 if r > 150 && b > 150 { return Color::Magenta; }
103 if g > 150 && b > 150 { return Color::Cyan; }
104
105 if bright > 180 { Color::White } else { Color::Gray }
106}
107
108fn load_syntax_set() -> SyntaxSet {
109 SyntaxSet::load_defaults_newlines()
110}
111
112#[cfg(test)]
113mod tests {
114 use super::*;
115
116 #[test]
117 fn test_rust_highlight() {
118 let hl = CodeHighlight::new("fn main() {\n println!(\"hello\");\n}\n", "rust");
119 assert!(hl.line_count() >= 3);
120 assert!(!hl.lines[0].spans.is_empty());
122 }
123
124 #[test]
125 fn test_unknown_language() {
126 let hl = CodeHighlight::new("some code", "nonexistent-lang");
127 assert!(hl.line_count() >= 1);
128 }
129
130 #[test]
131 fn test_empty() {
132 let hl = CodeHighlight::new("", "rust");
133 assert_eq!(hl.line_count(), 0);
134 }
135}