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 SqlHighlighter {
19    pub fn new() -> Self {
20        Self {}
21    }
22
23    pub fn highlight_sql_line(&self, text: &str) -> Line<'static> {
24        // Create syntect objects on-demand
25        let syntax_set = SyntaxSet::load_defaults_newlines();
26        let theme_set = ThemeSet::load_defaults();
27
28        // Find SQL syntax
29        let syntax = syntax_set
30            .find_syntax_by_extension("sql")
31            .unwrap_or_else(|| syntax_set.find_syntax_plain_text());
32
33        // Use a dark theme suitable for terminals
34        let theme = &theme_set.themes["base16-ocean.dark"];
35
36        let mut highlighter = HighlightLines::new(syntax, theme);
37
38        let mut spans = Vec::new();
39
40        // Handle single line highlighting
41        if let Ok(ranges) = highlighter.highlight_line(text, &syntax_set) {
42            for (style, text_piece) in ranges {
43                let ratatui_style = self.syntect_to_ratatui_style(style);
44                spans.push(Span::styled(text_piece.to_string(), ratatui_style));
45            }
46        } else {
47            // Fallback to plain text if highlighting fails
48            spans.push(Span::raw(text.to_string()));
49        }
50
51        Line::from(spans)
52    }
53
54    pub fn highlight_sql_multiline(&self, text: &str) -> Vec<Line<'static>> {
55        let syntax_set = SyntaxSet::load_defaults_newlines();
56        let theme_set = ThemeSet::load_defaults();
57
58        let syntax = syntax_set
59            .find_syntax_by_extension("sql")
60            .unwrap_or_else(|| syntax_set.find_syntax_plain_text());
61
62        let theme = &theme_set.themes["base16-ocean.dark"];
63        let mut highlighter = HighlightLines::new(syntax, theme);
64
65        let mut lines = Vec::new();
66
67        for line in LinesWithEndings::from(text) {
68            let mut spans = Vec::new();
69
70            if let Ok(ranges) = highlighter.highlight_line(line, &syntax_set) {
71                for (style, text_piece) in ranges {
72                    let ratatui_style = self.syntect_to_ratatui_style(style);
73                    spans.push(Span::styled(text_piece.to_string(), ratatui_style));
74                }
75            } else {
76                spans.push(Span::raw(line.to_string()));
77            }
78
79            lines.push(Line::from(spans));
80        }
81
82        lines
83    }
84
85    fn syntect_to_ratatui_style(&self, syntect_style: SyntectStyle) -> Style {
86        let mut style = Style::default();
87
88        // Convert syntect color to ratatui color
89        let fg_color = syntect_style.foreground;
90        let ratatui_color = Color::Rgb(fg_color.r, fg_color.g, fg_color.b);
91        style = style.fg(ratatui_color);
92
93        // Handle background if needed
94        // let bg_color = syntect_style.background;
95        // style = style.bg(Color::Rgb(bg_color.r, bg_color.g, bg_color.b));
96
97        style
98    }
99
100    /// Simple keyword-based highlighting as fallback
101    pub fn simple_sql_highlight(&self, text: &str) -> Line<'static> {
102        let keywords = [
103            "SELECT", "FROM", "WHERE", "AND", "OR", "IN", "ORDER", "BY", "ASC", "DESC", "INSERT",
104            "UPDATE", "DELETE", "CREATE", "DROP", "ALTER", "TABLE", "INDEX", "GROUP", "HAVING",
105            "LIMIT", "OFFSET", "JOIN", "LEFT", "RIGHT", "INNER", "OUTER", "NULL", "NOT", "IS",
106            "LIKE", "BETWEEN", "EXISTS", "DISTINCT", "AS", "ON",
107        ];
108
109        let linq_methods = [
110            "Contains",
111            "StartsWith",
112            "EndsWith",
113            "Length",
114            "ToUpper",
115            "ToLower",
116            "IsNullOrEmpty",
117            "Trim",
118            "Substring",
119            "IndexOf",
120            "Replace",
121        ];
122
123        let operators = ["=", "!=", "<>", "<", ">", "<=", ">=", "+", "-", "*", "/"];
124        let string_delimiters = ["'", "\""];
125
126        // Rainbow colors for nested parentheses
127        let paren_colors = [
128            Color::Yellow,
129            Color::Cyan,
130            Color::Magenta,
131            Color::Green,
132            Color::Blue,
133            Color::Red,
134        ];
135
136        let mut spans = Vec::new();
137        let mut current_word = String::new();
138        let mut in_string = false;
139        let mut string_delimiter = '\0';
140        let mut paren_depth = 0;
141
142        let chars: Vec<char> = text.chars().collect();
143        let mut i = 0;
144
145        while i < chars.len() {
146            let ch = chars[i];
147            if in_string {
148                current_word.push(ch);
149                if ch == string_delimiter {
150                    // End of string
151                    spans.push(Span::styled(
152                        current_word.clone(),
153                        Style::default().fg(Color::Green),
154                    ));
155                    current_word.clear();
156                    in_string = false;
157                }
158                i += 1;
159                continue;
160            }
161
162            if string_delimiters.contains(&ch.to_string().as_str()) {
163                // Start of string
164                if !current_word.is_empty() {
165                    self.push_word_span(
166                        &mut spans,
167                        &current_word,
168                        &keywords,
169                        &operators,
170                        &linq_methods,
171                    );
172                    current_word.clear();
173                }
174                current_word.push(ch);
175                in_string = true;
176                string_delimiter = ch;
177                i += 1;
178                continue;
179            }
180
181            if ch.is_alphabetic() || ch == '_' || (ch.is_numeric() && !current_word.is_empty()) {
182                current_word.push(ch);
183            } else if ch == '.' && !current_word.is_empty() {
184                // Check if this is a method call pattern (identifier.method)
185                let mut j = i + 1;
186                let mut method_name = String::new();
187
188                // Look ahead to see if a method name follows
189                while j < chars.len() && (chars[j].is_alphabetic() || chars[j] == '_') {
190                    method_name.push(chars[j]);
191                    j += 1;
192                }
193
194                if linq_methods.contains(&method_name.as_str()) {
195                    // This is a LINQ method call
196                    spans.push(Span::raw(current_word.clone())); // Column name in default color
197                    spans.push(Span::styled(".", Style::default().fg(Color::Magenta))); // Dot in magenta
198                    spans.push(Span::styled(
199                        method_name.clone(),
200                        Style::default()
201                            .fg(Color::Magenta)
202                            .add_modifier(Modifier::BOLD),
203                    )); // Method in bold magenta
204                    current_word.clear();
205                    i = j - 1; // Skip the method name we just processed
206                } else {
207                    // Regular dot
208                    self.push_word_span(
209                        &mut spans,
210                        &current_word,
211                        &keywords,
212                        &operators,
213                        &linq_methods,
214                    );
215                    current_word.clear();
216                    spans.push(Span::raw("."));
217                }
218            } else {
219                // End of word
220                if !current_word.is_empty() {
221                    self.push_word_span(
222                        &mut spans,
223                        &current_word,
224                        &keywords,
225                        &operators,
226                        &linq_methods,
227                    );
228                    current_word.clear();
229                }
230
231                // Handle operators and punctuation
232                if operators.contains(&ch.to_string().as_str()) {
233                    spans.push(Span::styled(
234                        ch.to_string(),
235                        Style::default().fg(Color::Cyan),
236                    ));
237                } else if ch == '(' {
238                    let color = paren_colors[paren_depth % paren_colors.len()];
239                    spans.push(Span::styled(
240                        ch.to_string(),
241                        Style::default().fg(color).add_modifier(Modifier::BOLD),
242                    ));
243                    paren_depth += 1;
244                } else if ch == ')' {
245                    if paren_depth > 0 {
246                        paren_depth -= 1;
247                    }
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                } else {
254                    spans.push(Span::raw(ch.to_string()));
255                }
256            }
257
258            i += 1;
259        }
260
261        // Handle remaining word
262        if !current_word.is_empty() {
263            if in_string {
264                spans.push(Span::styled(
265                    current_word,
266                    Style::default().fg(Color::Green),
267                ));
268            } else {
269                self.push_word_span(
270                    &mut spans,
271                    &current_word,
272                    &keywords,
273                    &operators,
274                    &linq_methods,
275                );
276            }
277        }
278
279        Line::from(spans)
280    }
281
282    fn push_word_span(
283        &self,
284        spans: &mut Vec<Span<'static>>,
285        word: &str,
286        keywords: &[&str],
287        operators: &[&str],
288        linq_methods: &[&str],
289    ) {
290        let upper_word = word.to_uppercase();
291
292        if keywords.contains(&upper_word.as_str()) {
293            // SQL Keyword
294            spans.push(Span::styled(
295                word.to_string(),
296                Style::default().fg(Color::Blue),
297            ));
298        } else if linq_methods.contains(&word) {
299            // LINQ Method - use bright magenta/purple
300            spans.push(Span::styled(
301                word.to_string(),
302                Style::default()
303                    .fg(Color::Magenta)
304                    .add_modifier(Modifier::BOLD),
305            ));
306        } else if operators.contains(&word) {
307            // Operator
308            spans.push(Span::styled(
309                word.to_string(),
310                Style::default().fg(Color::Cyan),
311            ));
312        } else if word.chars().all(|c| c.is_numeric() || c == '.') {
313            // Number
314            spans.push(Span::styled(
315                word.to_string(),
316                Style::default().fg(Color::Magenta),
317            ));
318        } else {
319            // Regular identifier/text
320            spans.push(Span::raw(word.to_string()));
321        }
322    }
323}