1use std::path::Path;
2use syntect::easy::HighlightLines;
3use syntect::highlighting::{ThemeSet, Style};
4use syntect::parsing::SyntaxSet;
5use syntect::util::{as_24_bit_terminal_escaped, LinesWithEndings};
6use ratatui::style::{Color, Modifier};
7
8pub struct SyntaxHighlighter {
9 syntax_set: SyntaxSet,
10 theme_set: ThemeSet,
11}
12
13impl Default for SyntaxHighlighter {
14 fn default() -> Self {
15 Self::new()
16 }
17}
18
19impl SyntaxHighlighter {
20 pub fn new() -> Self {
21 Self {
22 syntax_set: SyntaxSet::load_defaults_newlines(),
23 theme_set: ThemeSet::load_defaults(),
24 }
25 }
26
27 pub fn get_language_from_path<P: AsRef<Path>>(&self, path: P) -> Option<String> {
28 let path = path.as_ref();
29
30 if let Some(ext) = path.extension().and_then(|s| s.to_str()) {
32 if let Some(syntax) = self.syntax_set.find_syntax_by_extension(ext) {
33 return Some(syntax.name.clone());
34 }
35 }
36
37 if let Some(filename) = path.file_name().and_then(|s| s.to_str()) {
39 if let Some(syntax) = self.syntax_set.find_syntax_by_name(filename) {
40 return Some(syntax.name.clone());
41 }
42
43 match filename.to_lowercase().as_str() {
45 "dockerfile" => return Some("Dockerfile".to_string()),
46 "makefile" => return Some("Makefile".to_string()),
47 "cargo.toml" | "pyproject.toml" => return Some("TOML".to_string()),
48 "package.json" => return Some("JSON".to_string()),
49 _ => {}
50 }
51 }
52
53 None
55 }
56
57 pub fn highlight_line(&self, line: &str, language: &str, _line_number: usize) -> Vec<(ratatui::style::Style, String)> {
58 let syntax = match self.syntax_set.find_syntax_by_name(language) {
59 Some(syntax) => syntax,
60 None => return vec![(ratatui::style::Style::default(), line.to_string())],
61 };
62
63 let theme = match self.theme_set.themes.get("base16-ocean.dark") {
64 Some(theme) => theme,
65 None => &self.theme_set.themes["InspiredGitHub"],
66 };
67
68 let mut highlighter = HighlightLines::new(syntax, theme);
69
70 match highlighter.highlight_line(line, &self.syntax_set) {
71 Ok(ranges) => {
72 let mut result = Vec::new();
73 for (style, text) in ranges {
74 let ratatui_style = self.convert_syntect_style_to_ratatui(style);
75 result.push((ratatui_style, text.to_string()));
76 }
77 result
78 }
79 Err(_) => vec![(ratatui::style::Style::default(), line.to_string())],
80 }
81 }
82
83 pub fn highlight_code(&self, code: &str, language: &str) -> Vec<Vec<(ratatui::style::Style, String)>> {
84 let syntax = match self.syntax_set.find_syntax_by_name(language) {
85 Some(syntax) => syntax,
86 None => return code.lines().map(|line| vec![(ratatui::style::Style::default(), line.to_string())]).collect(),
87 };
88
89 let theme = match self.theme_set.themes.get("base16-ocean.dark") {
90 Some(theme) => theme,
91 None => &self.theme_set.themes["InspiredGitHub"],
92 };
93
94 let mut highlighter = HighlightLines::new(syntax, theme);
95 let mut result = Vec::new();
96
97 for line in LinesWithEndings::from(code) {
98 match highlighter.highlight_line(line, &self.syntax_set) {
99 Ok(ranges) => {
100 let mut line_result = Vec::new();
101 for (style, text) in ranges {
102 let ratatui_style = self.convert_syntect_style_to_ratatui(style);
103 line_result.push((ratatui_style, text.to_string()));
104 }
105 result.push(line_result);
106 }
107 Err(_) => {
108 result.push(vec![(ratatui::style::Style::default(), line.to_string())]);
109 }
110 }
111 }
112
113 result
114 }
115
116 pub fn get_terminal_highlighted(&self, code: &str, language: &str) -> String {
117 let syntax = match self.syntax_set.find_syntax_by_name(language) {
118 Some(syntax) => syntax,
119 None => return code.to_string(),
120 };
121
122 let theme = match self.theme_set.themes.get("base16-ocean.dark") {
123 Some(theme) => theme,
124 None => &self.theme_set.themes["InspiredGitHub"],
125 };
126
127 let mut highlighter = HighlightLines::new(syntax, theme);
128 let mut result = String::new();
129
130 for line in LinesWithEndings::from(code) {
131 match highlighter.highlight_line(line, &self.syntax_set) {
132 Ok(ranges) => {
133 let escaped = as_24_bit_terminal_escaped(&ranges[..], false);
134 result.push_str(&escaped);
135 }
136 Err(_) => {
137 result.push_str(line);
138 }
139 }
140 }
141
142 result
143 }
144
145 fn convert_syntect_style_to_ratatui(&self, style: Style) -> ratatui::style::Style {
146 let mut ratatui_style = ratatui::style::Style::default();
147
148 if style.foreground.a > 0 {
150 ratatui_style = ratatui_style.fg(Color::Rgb(
151 style.foreground.r,
152 style.foreground.g,
153 style.foreground.b,
154 ));
155 }
156
157 if style.background.a > 0 {
159 ratatui_style = ratatui_style.bg(Color::Rgb(
160 style.background.r,
161 style.background.g,
162 style.background.b,
163 ));
164 }
165
166 if style.font_style.contains(syntect::highlighting::FontStyle::BOLD) {
168 ratatui_style = ratatui_style.add_modifier(Modifier::BOLD);
169 }
170 if style.font_style.contains(syntect::highlighting::FontStyle::ITALIC) {
171 ratatui_style = ratatui_style.add_modifier(Modifier::ITALIC);
172 }
173 if style.font_style.contains(syntect::highlighting::FontStyle::UNDERLINE) {
174 ratatui_style = ratatui_style.add_modifier(Modifier::UNDERLINED);
175 }
176
177 ratatui_style
178 }
179
180 pub fn get_common_languages() -> Vec<&'static str> {
181 vec![
182 "Rust",
183 "Python",
184 "JavaScript",
185 "TypeScript",
186 "Java",
187 "C",
188 "C++",
189 "C#",
190 "Go",
191 "Swift",
192 "Kotlin",
193 "PHP",
194 "Ruby",
195 "HTML",
196 "CSS",
197 "SCSS",
198 "JSON",
199 "YAML",
200 "TOML",
201 "XML",
202 "Markdown",
203 "Bash",
204 "Fish",
205 "Zsh",
206 "PowerShell",
207 "Dockerfile",
208 "Makefile",
209 "SQL",
210 "GraphQL",
211 ]
212 }
213}
214
215pub fn is_likely_text_file<P: AsRef<Path>>(path: P) -> bool {
217 let path = path.as_ref();
218
219 if let Some(ext) = path.extension().and_then(|s| s.to_str()) {
220 let ext = ext.to_lowercase();
221 matches!(ext.as_str(),
222 "rs" | "py" | "js" | "ts" | "jsx" | "tsx" | "java" | "kt" | "swift" |
224 "go" | "c" | "cpp" | "cc" | "cxx" | "h" | "hpp" | "cs" | "php" | "rb" |
225
226 "html" | "htm" | "css" | "scss" | "sass" | "less" | "vue" | "svelte" |
228
229 "json" | "yaml" | "yml" | "toml" | "xml" | "ini" | "conf" | "config" |
231 "env" | "properties" | "cfg" | "plist" |
232
233 "md" | "txt" | "rst" | "adoc" | "tex" | "rtf" |
235
236 "sh" | "bash" | "zsh" | "fish" | "ps1" | "bat" | "cmd" |
238
239 "sql" | "graphql" | "dockerfile" | "makefile" | "cmake" | "log"
241 )
242 } else {
243 if let Some(filename) = path.file_name().and_then(|s| s.to_str()) {
245 matches!(filename.to_lowercase().as_str(),
246 "dockerfile" | "makefile" | "cmake" | "readme" | "license" |
247 "changelog" | "authors" | "contributors" | "todo" | "news"
248 )
249 } else {
250 false
251 }
252 }
253}