sql_cli/sql/
sql_highlighter.rs

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    // Since syntect types don't implement Clone, we'll create them on-demand
10}
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        // Create syntect objects on-demand
33        let syntax_set = SyntaxSet::load_defaults_newlines();
34        let theme_set = ThemeSet::load_defaults();
35
36        // Find SQL syntax
37        let syntax = syntax_set
38            .find_syntax_by_extension("sql")
39            .unwrap_or_else(|| syntax_set.find_syntax_plain_text());
40
41        // Use a dark theme suitable for terminals
42        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        // Handle single line highlighting
49        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            // Fallback to plain text if highlighting fails
56            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        // Convert syntect color to ratatui color
98        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        // Handle background if needed
103        // let bg_color = syntect_style.background;
104        // style = style.bg(Color::Rgb(bg_color.r, bg_color.g, bg_color.b));
105
106        style
107    }
108
109    /// Simple keyword-based highlighting as fallback
110    #[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        // Rainbow colors for nested parentheses
137        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                    // End of string
161                    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                // Start of string
174                if !current_word.is_empty() {
175                    self.push_word_span(
176                        &mut spans,
177                        &current_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                // Check if this is a method call pattern (identifier.method)
195                let mut j = i + 1;
196                let mut method_name = String::new();
197
198                // Look ahead to see if a method name follows
199                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                    // This is a LINQ method call
206                    spans.push(Span::raw(current_word.clone())); // Column name in default color
207                    spans.push(Span::styled(".", Style::default().fg(Color::Magenta))); // Dot in magenta
208                    spans.push(Span::styled(
209                        method_name.clone(),
210                        Style::default()
211                            .fg(Color::Magenta)
212                            .add_modifier(Modifier::BOLD),
213                    )); // Method in bold magenta
214                    current_word.clear();
215                    i = j - 1; // Skip the method name we just processed
216                } else {
217                    // Regular dot
218                    self.push_word_span(
219                        &mut spans,
220                        &current_word,
221                        &keywords,
222                        &operators,
223                        &linq_methods,
224                    );
225                    current_word.clear();
226                    spans.push(Span::raw("."));
227                }
228            } else {
229                // End of word
230                if !current_word.is_empty() {
231                    self.push_word_span(
232                        &mut spans,
233                        &current_word,
234                        &keywords,
235                        &operators,
236                        &linq_methods,
237                    );
238                    current_word.clear();
239                }
240
241                // Handle operators and punctuation
242                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        // Handle remaining word
270        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                    &current_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            // SQL Keyword
302            spans.push(Span::styled(
303                word.to_string(),
304                Style::default().fg(Color::Blue),
305            ));
306        } else if linq_methods.contains(&word) {
307            // LINQ Method - use bright magenta/purple
308            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            // Operator
316            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            // Number
322            spans.push(Span::styled(
323                word.to_string(),
324                Style::default().fg(Color::Magenta),
325            ));
326        } else {
327            // Regular identifier/text
328            spans.push(Span::raw(word.to_string()));
329        }
330    }
331}