Line data Source code
1 : use ratatui::style::Color;
2 : use std::path::Path;
3 : use syntect::easy::HighlightLines;
4 : use syntect::highlighting::ThemeSet;
5 : use syntect::parsing::SyntaxSet;
6 :
7 : pub struct SyntaxHighlighter {
8 : syntax_set: SyntaxSet,
9 : theme_set: ThemeSet,
10 : }
11 :
12 : impl SyntaxHighlighter {
13 35 : pub fn new() -> Self {
14 35 : Self {
15 35 : syntax_set: SyntaxSet::load_defaults_newlines(),
16 35 : theme_set: ThemeSet::load_defaults(),
17 35 : }
18 35 : }
19 :
20 : /// Get a highlighter for a specific file that can be used to highlight multiple lines sequentially
21 11 : pub fn create_highlighter(&self, file_path: &Path) -> FileHighlighter<'_> {
22 11 : let syntax = self
23 11 : .syntax_set
24 11 : .find_syntax_for_file(file_path)
25 11 : .ok()
26 11 : .flatten()
27 11 : .unwrap_or_else(|| self.syntax_set.find_syntax_plain_text());
28 :
29 11 : let theme = &self.theme_set.themes["base16-ocean.dark"];
30 :
31 11 : FileHighlighter {
32 11 : highlighter: HighlightLines::new(syntax, theme),
33 11 : syntax_set: &self.syntax_set,
34 11 : }
35 11 : }
36 :
37 : #[allow(dead_code)]
38 1 : pub fn detect_language(&self, file_path: &Path) -> Option<String> {
39 1 : self.syntax_set
40 1 : .find_syntax_for_file(file_path)
41 1 : .ok()
42 1 : .flatten()
43 1 : .map(|s| s.name.clone())
44 1 : }
45 : }
46 :
47 : pub struct FileHighlighter<'a> {
48 : highlighter: HighlightLines<'a>,
49 : syntax_set: &'a SyntaxSet,
50 : }
51 :
52 : impl<'a> FileHighlighter<'a> {
53 : /// Highlight a single line (must be called sequentially for proper context)
54 30 : pub fn highlight_line(&mut self, line: &str) -> Vec<(Color, String)> {
55 30 : let mut result = Vec::new();
56 :
57 30 : if let Ok(ranges) = self.highlighter.highlight_line(line, self.syntax_set) {
58 134 : for (style, text) in ranges {
59 134 : let color = syntect_color_to_ratatui(style.foreground);
60 134 : result.push((color, text.to_string()));
61 134 : }
62 0 : }
63 :
64 30 : result
65 30 : }
66 : }
67 :
68 : /// Convert syntect color to ratatui color
69 : /// Uses 256-color palette for better terminal compatibility (macOS Terminal.app)
70 134 : fn syntect_color_to_ratatui(color: syntect::highlighting::Color) -> Color {
71 134 : rgb_to_ansi256(color.r, color.g, color.b)
72 134 : }
73 :
74 : /// Convert RGB color to nearest ANSI 256-color palette index
75 : /// This provides better compatibility with terminals that don't support 24-bit true color
76 136 : fn rgb_to_ansi256(r: u8, g: u8, b: u8) -> Color {
77 : // Check if it's a grayscale color
78 136 : let max_diff = r.abs_diff(g).max(g.abs_diff(b)).max(r.abs_diff(b));
79 :
80 136 : if max_diff < 10 {
81 : // It's grayscale - use the 24 grayscale colors (232-255)
82 : // Map 0-255 to 0-23
83 1 : let gray_index = ((r as u16 * 23) / 255) as u8;
84 1 : return Color::Indexed(232 + gray_index);
85 135 : }
86 :
87 : // Map to 6x6x6 color cube (16-231)
88 : // Each component is mapped to 0-5
89 135 : let r_index = (r as u16 * 5 / 255) as u8;
90 135 : let g_index = (g as u16 * 5 / 255) as u8;
91 135 : let b_index = (b as u16 * 5 / 255) as u8;
92 :
93 135 : let index = 16 + 36 * r_index + 6 * g_index + b_index;
94 135 : Color::Indexed(index)
95 136 : }
96 :
97 : impl Default for SyntaxHighlighter {
98 1 : fn default() -> Self {
99 1 : Self::new()
100 1 : }
101 : }
102 :
103 : #[cfg(test)]
104 : #[path = "../tests/syntax.rs"]
105 : mod tests;
|