1use ratatui::style::{Color, Modifier, Style};
2use ratatui::text::{Line, Span};
3use syntect::easy::HighlightLines;
4use syntect::highlighting::{Style as SyntectStyle, ThemeSet};
5use syntect::parsing::SyntaxSet;
6use syntect::util::LinesWithEndings;
7
8pub struct SqlHighlighter {
9 }
11
12impl Clone for SqlHighlighter {
13 fn clone(&self) -> Self {
14 SqlHighlighter {}
15 }
16}
17
18impl Default for SqlHighlighter {
19 fn default() -> Self {
20 Self::new()
21 }
22}
23
24impl SqlHighlighter {
25 #[must_use]
26 pub fn new() -> Self {
27 Self {}
28 }
29
30 #[must_use]
31 pub fn highlight_sql_line(&self, text: &str) -> Line<'static> {
32 let syntax_set = SyntaxSet::load_defaults_newlines();
34 let theme_set = ThemeSet::load_defaults();
35
36 let syntax = syntax_set
38 .find_syntax_by_extension("sql")
39 .unwrap_or_else(|| syntax_set.find_syntax_plain_text());
40
41 let theme = &theme_set.themes["base16-ocean.dark"];
43
44 let mut highlighter = HighlightLines::new(syntax, theme);
45
46 let mut spans = Vec::new();
47
48 if let Ok(ranges) = highlighter.highlight_line(text, &syntax_set) {
50 for (style, text_piece) in ranges {
51 let ratatui_style = self.syntect_to_ratatui_style(style);
52 spans.push(Span::styled(text_piece.to_string(), ratatui_style));
53 }
54 } else {
55 spans.push(Span::raw(text.to_string()));
57 }
58
59 Line::from(spans)
60 }
61
62 #[must_use]
63 pub fn highlight_sql_multiline(&self, text: &str) -> Vec<Line<'static>> {
64 let syntax_set = SyntaxSet::load_defaults_newlines();
65 let theme_set = ThemeSet::load_defaults();
66
67 let syntax = syntax_set
68 .find_syntax_by_extension("sql")
69 .unwrap_or_else(|| syntax_set.find_syntax_plain_text());
70
71 let theme = &theme_set.themes["base16-ocean.dark"];
72 let mut highlighter = HighlightLines::new(syntax, theme);
73
74 let mut lines = Vec::new();
75
76 for line in LinesWithEndings::from(text) {
77 let mut spans = Vec::new();
78
79 if let Ok(ranges) = highlighter.highlight_line(line, &syntax_set) {
80 for (style, text_piece) in ranges {
81 let ratatui_style = self.syntect_to_ratatui_style(style);
82 spans.push(Span::styled(text_piece.to_string(), ratatui_style));
83 }
84 } else {
85 spans.push(Span::raw(line.to_string()));
86 }
87
88 lines.push(Line::from(spans));
89 }
90
91 lines
92 }
93
94 fn syntect_to_ratatui_style(&self, syntect_style: SyntectStyle) -> Style {
95 let mut style = Style::default();
96
97 let fg_color = syntect_style.foreground;
99 let ratatui_color = Color::Rgb(fg_color.r, fg_color.g, fg_color.b);
100 style = style.fg(ratatui_color);
101
102 style
107 }
108
109 #[must_use]
111 pub fn simple_sql_highlight(&self, text: &str) -> Line<'static> {
112 let keywords = [
113 "SELECT", "FROM", "WHERE", "AND", "OR", "IN", "ORDER", "BY", "ASC", "DESC", "INSERT",
114 "UPDATE", "DELETE", "CREATE", "DROP", "ALTER", "TABLE", "INDEX", "GROUP", "HAVING",
115 "LIMIT", "OFFSET", "JOIN", "LEFT", "RIGHT", "INNER", "OUTER", "NULL", "NOT", "IS",
116 "LIKE", "BETWEEN", "EXISTS", "DISTINCT", "AS", "ON",
117 ];
118
119 let linq_methods = [
120 "Contains",
121 "StartsWith",
122 "EndsWith",
123 "Length",
124 "ToUpper",
125 "ToLower",
126 "IsNullOrEmpty",
127 "Trim",
128 "Substring",
129 "IndexOf",
130 "Replace",
131 ];
132
133 let operators = ["=", "!=", "<>", "<", ">", "<=", ">=", "+", "-", "*", "/"];
134 let string_delimiters = ["'", "\""];
135
136 let paren_colors = [
138 Color::Yellow,
139 Color::Cyan,
140 Color::Magenta,
141 Color::Green,
142 Color::Blue,
143 Color::Red,
144 ];
145
146 let mut spans = Vec::new();
147 let mut current_word = String::new();
148 let mut in_string = false;
149 let mut string_delimiter = '\0';
150 let mut paren_depth = 0;
151
152 let chars: Vec<char> = text.chars().collect();
153 let mut i = 0;
154
155 while i < chars.len() {
156 let ch = chars[i];
157 if in_string {
158 current_word.push(ch);
159 if ch == string_delimiter {
160 spans.push(Span::styled(
162 current_word.clone(),
163 Style::default().fg(Color::Green),
164 ));
165 current_word.clear();
166 in_string = false;
167 }
168 i += 1;
169 continue;
170 }
171
172 if string_delimiters.contains(&ch.to_string().as_str()) {
173 if !current_word.is_empty() {
175 self.push_word_span(
176 &mut spans,
177 ¤t_word,
178 &keywords,
179 &operators,
180 &linq_methods,
181 );
182 current_word.clear();
183 }
184 current_word.push(ch);
185 in_string = true;
186 string_delimiter = ch;
187 i += 1;
188 continue;
189 }
190
191 if ch.is_alphabetic() || ch == '_' || (ch.is_numeric() && !current_word.is_empty()) {
192 current_word.push(ch);
193 } else if ch == '.' && !current_word.is_empty() {
194 let mut j = i + 1;
196 let mut method_name = String::new();
197
198 while j < chars.len() && (chars[j].is_alphabetic() || chars[j] == '_') {
200 method_name.push(chars[j]);
201 j += 1;
202 }
203
204 if linq_methods.contains(&method_name.as_str()) {
205 spans.push(Span::raw(current_word.clone())); spans.push(Span::styled(".", Style::default().fg(Color::Magenta))); spans.push(Span::styled(
209 method_name.clone(),
210 Style::default()
211 .fg(Color::Magenta)
212 .add_modifier(Modifier::BOLD),
213 )); current_word.clear();
215 i = j - 1; } else {
217 self.push_word_span(
219 &mut spans,
220 ¤t_word,
221 &keywords,
222 &operators,
223 &linq_methods,
224 );
225 current_word.clear();
226 spans.push(Span::raw("."));
227 }
228 } else {
229 if !current_word.is_empty() {
231 self.push_word_span(
232 &mut spans,
233 ¤t_word,
234 &keywords,
235 &operators,
236 &linq_methods,
237 );
238 current_word.clear();
239 }
240
241 if operators.contains(&ch.to_string().as_str()) {
243 spans.push(Span::styled(
244 ch.to_string(),
245 Style::default().fg(Color::Cyan),
246 ));
247 } else if ch == '(' {
248 let color = paren_colors[paren_depth % paren_colors.len()];
249 spans.push(Span::styled(
250 ch.to_string(),
251 Style::default().fg(color).add_modifier(Modifier::BOLD),
252 ));
253 paren_depth += 1;
254 } else if ch == ')' {
255 paren_depth = paren_depth.saturating_sub(1);
256 let color = paren_colors[paren_depth % paren_colors.len()];
257 spans.push(Span::styled(
258 ch.to_string(),
259 Style::default().fg(color).add_modifier(Modifier::BOLD),
260 ));
261 } else {
262 spans.push(Span::raw(ch.to_string()));
263 }
264 }
265
266 i += 1;
267 }
268
269 if !current_word.is_empty() {
271 if in_string {
272 spans.push(Span::styled(
273 current_word,
274 Style::default().fg(Color::Green),
275 ));
276 } else {
277 self.push_word_span(
278 &mut spans,
279 ¤t_word,
280 &keywords,
281 &operators,
282 &linq_methods,
283 );
284 }
285 }
286
287 Line::from(spans)
288 }
289
290 fn push_word_span(
291 &self,
292 spans: &mut Vec<Span<'static>>,
293 word: &str,
294 keywords: &[&str],
295 operators: &[&str],
296 linq_methods: &[&str],
297 ) {
298 let upper_word = word.to_uppercase();
299
300 if keywords.contains(&upper_word.as_str()) {
301 spans.push(Span::styled(
303 word.to_string(),
304 Style::default().fg(Color::Blue),
305 ));
306 } else if linq_methods.contains(&word) {
307 spans.push(Span::styled(
309 word.to_string(),
310 Style::default()
311 .fg(Color::Magenta)
312 .add_modifier(Modifier::BOLD),
313 ));
314 } else if operators.contains(&word) {
315 spans.push(Span::styled(
317 word.to_string(),
318 Style::default().fg(Color::Cyan),
319 ));
320 } else if word.chars().all(|c| c.is_numeric() || c == '.') {
321 spans.push(Span::styled(
323 word.to_string(),
324 Style::default().fg(Color::Magenta),
325 ));
326 } else {
327 spans.push(Span::raw(word.to_string()));
329 }
330 }
331}