code_mesh_tui/utils/
syntax_highlighter.rs

1use ratatui::{style::Style, text::Span};
2use syntect::{
3    easy::HighlightLines,
4    highlighting::{ThemeSet, Style as SyntectStyle},
5    parsing::SyntaxSet,
6};
7
8use crate::theme::Theme;
9
10/// Syntax highlighter for code files
11pub struct SyntaxHighlighter {
12    syntax_set: SyntaxSet,
13    theme_set: ThemeSet,
14}
15
16impl SyntaxHighlighter {
17    /// Create a new syntax highlighter
18    pub fn new() -> Self {
19        Self {
20            syntax_set: SyntaxSet::load_defaults_newlines(),
21            theme_set: ThemeSet::load_defaults(),
22        }
23    }
24    
25    /// Highlight a line of code
26    pub fn highlight<'a>(&self, line: &'a str, language: &str) -> Vec<Span<'a>> {
27        let syntax = match self.syntax_set.find_syntax_by_name(language) {
28            Some(syntax) => syntax,
29            None => {
30                // Try by file extension
31                let extension = match language {
32                    "rust" => "rs",
33                    "javascript" => "js",
34                    "typescript" => "ts",
35                    "python" => "py",
36                    "bash" => "sh",
37                    _ => language,
38                };
39                
40                match self.syntax_set.find_syntax_by_extension(extension) {
41                    Some(syntax) => syntax,
42                    None => {
43                        // Fallback to plain text
44                        return vec![Span::raw(line)];
45                    }
46                }
47            }
48        };
49        
50        let theme = &self.theme_set.themes["base16-ocean.dark"];
51        let mut highlighter = HighlightLines::new(syntax, theme);
52        
53        match highlighter.highlight_line(line, &self.syntax_set) {
54            Ok(ranges) => {
55                ranges
56                    .into_iter()
57                    .map(|(style, text)| {
58                        let ratatui_style = self.syntect_to_ratatui_style(style);
59                        Span::styled(text, ratatui_style)
60                    })
61                    .collect()
62            }
63            Err(_) => vec![Span::raw(line)],
64        }
65    }
66    
67    /// Convert syntect style to ratatui style
68    fn syntect_to_ratatui_style(&self, style: SyntectStyle) -> Style {
69        let fg = style.foreground;
70        let color = ratatui::style::Color::Rgb(fg.r, fg.g, fg.b);
71        
72        let mut ratatui_style = Style::default().fg(color);
73        
74        if style.font_style.contains(syntect::highlighting::FontStyle::BOLD) {
75            ratatui_style = ratatui_style.add_modifier(ratatui::style::Modifier::BOLD);
76        }
77        
78        if style.font_style.contains(syntect::highlighting::FontStyle::ITALIC) {
79            ratatui_style = ratatui_style.add_modifier(ratatui::style::Modifier::ITALIC);
80        }
81        
82        if style.font_style.contains(syntect::highlighting::FontStyle::UNDERLINE) {
83            ratatui_style = ratatui_style.add_modifier(ratatui::style::Modifier::UNDERLINED);
84        }
85        
86        ratatui_style
87    }
88    
89    /// Get list of supported languages
90    pub fn supported_languages(&self) -> Vec<String> {
91        self.syntax_set
92            .syntaxes()
93            .iter()
94            .map(|syntax| syntax.name.clone())
95            .collect()
96    }
97}
98
99impl Default for SyntaxHighlighter {
100    fn default() -> Self {
101        Self::new()
102    }
103}
104
105#[cfg(test)]
106mod tests {
107    use super::*;
108
109    #[test]
110    fn test_syntax_highlighter_creation() {
111        let highlighter = SyntaxHighlighter::new();
112        let languages = highlighter.supported_languages();
113        assert!(!languages.is_empty());
114        assert!(languages.contains(&"Rust".to_string()));
115    }
116
117    #[test]
118    fn test_basic_highlighting() {
119        let highlighter = SyntaxHighlighter::new();
120        let spans = highlighter.highlight("fn main() {}", "rust");
121        assert!(!spans.is_empty());
122    }
123}