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