dbtui 0.1.3

Terminal database client with Vim-style navigation
use ratatui::style::{Color, Modifier, Style};
use ratatui::text::Span;

use vimltui::SyntaxHighlighter;

const SQL_KEYWORDS: &[&str] = &[
    "SELECT",
    "FROM",
    "WHERE",
    "INSERT",
    "INTO",
    "UPDATE",
    "DELETE",
    "SET",
    "JOIN",
    "LEFT",
    "RIGHT",
    "INNER",
    "OUTER",
    "FULL",
    "CROSS",
    "ON",
    "AND",
    "OR",
    "NOT",
    "IN",
    "IS",
    "NULL",
    "LIKE",
    "BETWEEN",
    "EXISTS",
    "AS",
    "ORDER",
    "BY",
    "GROUP",
    "HAVING",
    "LIMIT",
    "OFFSET",
    "DISTINCT",
    "UNION",
    "ALL",
    "CREATE",
    "ALTER",
    "DROP",
    "TABLE",
    "INDEX",
    "VIEW",
    "BEGIN",
    "END",
    "COMMIT",
    "ROLLBACK",
    "DECLARE",
    "CURSOR",
    "FETCH",
    "CASE",
    "WHEN",
    "THEN",
    "ELSE",
    "ASC",
    "DESC",
    "COUNT",
    "SUM",
    "AVG",
    "MAX",
    "MIN",
    "PROCEDURE",
    "FUNCTION",
    "PACKAGE",
    "BODY",
    "REPLACE",
    "VALUES",
    "WITH",
    "RECURSIVE",
    "TRIGGER",
    "GRANT",
    "REVOKE",
    "TYPE",
    "RETURN",
    "IF",
    "ELSIF",
    "LOOP",
    "FOR",
    "WHILE",
    "EXIT",
    "EXCEPTION",
    "RAISE",
    "PRAGMA",
    "EXECUTE",
    "IMMEDIATE",
    "BULK",
    "COLLECT",
    "FORALL",
    "OPEN",
    "CLOSE",
    "DBMS_OUTPUT",
    "PUT_LINE",
];

pub struct SqlHighlighter {
    pub keyword: Color,
    pub string: Color,
    pub number: Color,
    pub comment: Color,
    pub operator: Color,
}

impl SqlHighlighter {
    pub fn from_theme(theme: &crate::ui::theme::Theme) -> Self {
        Self {
            keyword: theme.sql_keyword,
            string: theme.sql_string,
            number: theme.sql_number,
            comment: theme.sql_comment,
            operator: theme.sql_operator,
        }
    }
}

impl SyntaxHighlighter for SqlHighlighter {
    fn highlight_line<'a>(&self, line: &'a str, spans: &mut Vec<Span<'a>>) {
        if line.is_empty() {
            return;
        }

        // Check for line comment
        if let Some(comment_pos) = line.find("--") {
            if comment_pos > 0 {
                self.highlight_tokens(&line[..comment_pos], spans);
            }
            spans.push(Span::styled(
                &line[comment_pos..],
                Style::default()
                    .fg(self.comment)
                    .add_modifier(Modifier::ITALIC),
            ));
            return;
        }

        self.highlight_tokens(line, spans);
    }

    fn highlight_segment<'a>(&self, text: &'a str, spans: &mut Vec<Span<'a>>) {
        self.highlight_line(text, spans);
    }
}

impl SqlHighlighter {
    fn highlight_tokens<'a>(&self, text: &'a str, spans: &mut Vec<Span<'a>>) {
        let mut remaining = text;

        while !remaining.is_empty() {
            // Skip leading whitespace
            if remaining.starts_with(|c: char| c.is_whitespace()) {
                let ws_end = remaining
                    .find(|c: char| !c.is_whitespace())
                    .unwrap_or(remaining.len());
                spans.push(Span::raw(&remaining[..ws_end]));
                remaining = &remaining[ws_end..];
                continue;
            }

            // String literal
            if remaining.starts_with('\'') {
                let end = remaining[1..]
                    .find('\'')
                    .map(|p| p + 2)
                    .unwrap_or(remaining.len());
                spans.push(Span::styled(
                    &remaining[..end],
                    Style::default().fg(self.string),
                ));
                remaining = &remaining[end..];
                continue;
            }

            // Number
            if remaining.starts_with(|c: char| c.is_ascii_digit()) {
                let end = remaining
                    .find(|c: char| !c.is_ascii_digit() && c != '.')
                    .unwrap_or(remaining.len());
                spans.push(Span::styled(
                    &remaining[..end],
                    Style::default().fg(self.number),
                ));
                remaining = &remaining[end..];
                continue;
            }

            // Word (potential keyword or identifier)
            if remaining.starts_with(|c: char| c.is_alphanumeric() || c == '_') {
                let end = remaining
                    .find(|c: char| !c.is_alphanumeric() && c != '_')
                    .unwrap_or(remaining.len());
                let word = &remaining[..end];
                let upper = word.to_uppercase();

                if SQL_KEYWORDS.contains(&upper.as_str()) {
                    spans.push(Span::styled(
                        word,
                        Style::default()
                            .fg(self.keyword)
                            .add_modifier(Modifier::BOLD),
                    ));
                } else {
                    spans.push(Span::raw(word));
                }
                remaining = &remaining[end..];
                continue;
            }

            // Operators and punctuation
            let end = remaining
                .find(|c: char| c.is_alphanumeric() || c == '_' || c == '\'' || c.is_whitespace())
                .unwrap_or(remaining.len())
                .max(1);
            spans.push(Span::styled(
                &remaining[..end],
                Style::default().fg(self.operator),
            ));
            remaining = &remaining[end..];
        }
    }
}