Replaces all dark-Mocha colours (#1e1e2e base, #313244 surface0, etc.) with the noticeably brighter Frappé variants (#303446 base, #414559 surface0, etc.) across the QSS and every hardcoded colour in Python source files (syntax highlighter, completer popup, log viewer, explain view, table viewer, schema browser, icons, etc.). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
149 lines
6.7 KiB
Python
149 lines
6.7 KiB
Python
"""
|
|
SQL syntax highlighter for QPlainTextEdit.
|
|
Highlights keywords, types, functions, strings, comments, and numbers.
|
|
"""
|
|
import re
|
|
from PyQt6.QtCore import QRegularExpression, Qt
|
|
from PyQt6.QtGui import (
|
|
QSyntaxHighlighter, QTextCharFormat, QColor, QFont
|
|
)
|
|
|
|
|
|
def _fmt(color: str, bold: bool = False, italic: bool = False) -> QTextCharFormat:
|
|
f = QTextCharFormat()
|
|
f.setForeground(QColor(color))
|
|
if bold: f.setFontWeight(QFont.Weight.Bold)
|
|
if italic: f.setFontItalic(True)
|
|
return f
|
|
|
|
|
|
# ── Token categories (Catppuccin Frappé palette) ─────────────────────────────
|
|
_KEYWORD_FMT = _fmt("#8caaee", bold=True) # blue — DDL/DML/control
|
|
_TYPE_FMT = _fmt("#ef9f76") # peach — data types
|
|
_FUNCTION_FMT = _fmt("#a6d189") # green — functions
|
|
_STRING_FMT = _fmt("#a6d189") # green — string literals
|
|
_NUMBER_FMT = _fmt("#ef9f76") # orange — numeric literals
|
|
_COMMENT_FMT = _fmt("#737994", italic=True) # grey — comments
|
|
_OPERATOR_FMT = _fmt("#ca9ee6") # mauve — operators/special
|
|
_STAR_FMT = _fmt("#ca9ee6", bold=True) # mauve — * wildcard
|
|
|
|
# ── Keyword lists ─────────────────────────────────────────────────────────────
|
|
_KEYWORDS = [
|
|
"SELECT", "FROM", "WHERE", "JOIN", "LEFT", "RIGHT", "INNER", "OUTER",
|
|
"FULL", "CROSS", "ON", "AS", "AND", "OR", "NOT", "IN", "LIKE", "BETWEEN",
|
|
"IS", "NULL", "EXISTS", "CASE", "WHEN", "THEN", "ELSE", "END",
|
|
"INSERT", "INTO", "VALUES", "UPDATE", "SET", "DELETE", "MERGE",
|
|
"CREATE", "TABLE", "VIEW", "INDEX", "DATABASE", "SCHEMA", "SEQUENCE",
|
|
"DROP", "ALTER", "ADD", "COLUMN", "PRIMARY", "KEY", "FOREIGN",
|
|
"REFERENCES", "UNIQUE", "DEFAULT", "CONSTRAINT", "CHECK",
|
|
"IF", "EXISTS", "NOT", "TRUNCATE", "RENAME", "MODIFY", "CHANGE",
|
|
"GRANT", "REVOKE", "COMMIT", "ROLLBACK", "SAVEPOINT", "BEGIN",
|
|
"TRANSACTION", "START", "END", "LOCK", "UNLOCK", "EXPLAIN",
|
|
"ANALYZE", "VACUUM", "SHOW", "DESCRIBE", "DESC", "ASC",
|
|
"LIMIT", "OFFSET", "ORDER", "BY", "GROUP", "HAVING", "DISTINCT",
|
|
"UNION", "ALL", "INTERSECT", "EXCEPT", "PARTITION", "OVER",
|
|
"WITH", "RECURSIVE", "USE", "CALL", "EXEC", "EXECUTE",
|
|
"PROCEDURE", "FUNCTION", "TRIGGER", "EVENT", "REPLACE",
|
|
"TRUE", "FALSE", "RETURNING",
|
|
]
|
|
|
|
_TYPES = [
|
|
"INT", "INTEGER", "BIGINT", "SMALLINT", "TINYINT", "MEDIUMINT",
|
|
"FLOAT", "DOUBLE", "DECIMAL", "NUMERIC", "REAL", "MONEY",
|
|
"VARCHAR", "CHAR", "TEXT", "TINYTEXT", "MEDIUMTEXT", "LONGTEXT",
|
|
"BLOB", "TINYBLOB", "MEDIUMBLOB", "LONGBLOB", "BINARY", "VARBINARY",
|
|
"DATE", "DATETIME", "TIMESTAMP", "TIME", "YEAR",
|
|
"BOOLEAN", "BOOL", "BIT", "ENUM", "SET",
|
|
"JSON", "UUID", "SERIAL", "BYTEA", "NVARCHAR", "NCHAR",
|
|
"IMAGE", "XML", "CURSOR", "ROWVERSION", "UNIQUEIDENTIFIER",
|
|
]
|
|
|
|
_FUNCTIONS = [
|
|
"COUNT", "SUM", "AVG", "MIN", "MAX", "COALESCE", "IFNULL", "NULLIF",
|
|
"NOW", "CURDATE", "CURTIME", "DATE", "YEAR", "MONTH", "DAY",
|
|
"CONCAT", "SUBSTRING", "LENGTH", "TRIM", "UPPER", "LOWER",
|
|
"REPLACE", "ROUND", "FLOOR", "CEIL", "ABS", "MOD", "POWER",
|
|
"CAST", "CONVERT", "ISNULL", "ISNUMERIC",
|
|
"ROW_NUMBER", "RANK", "DENSE_RANK", "LAG", "LEAD", "NTILE",
|
|
"FIRST_VALUE", "LAST_VALUE", "NTH_VALUE",
|
|
"STRING_AGG", "GROUP_CONCAT", "ARRAY_AGG",
|
|
"TO_CHAR", "TO_DATE", "TO_NUMBER", "EXTRACT",
|
|
"DATEDIFF", "DATEADD", "DATE_FORMAT", "DATE_TRUNC",
|
|
"IF", "IIF", "DECODE", "GREATEST", "LEAST",
|
|
]
|
|
|
|
|
|
class SQLHighlighter(QSyntaxHighlighter):
|
|
"""Applies syntax colouring to a SQL document."""
|
|
|
|
def __init__(self, document):
|
|
super().__init__(document)
|
|
self._rules: list[tuple] = []
|
|
|
|
def kw_pattern(words: list[str]) -> str:
|
|
return r"\b(?:" + "|".join(words) + r")\b"
|
|
|
|
self._rules = [
|
|
# Keywords (case-insensitive handled via flag)
|
|
(QRegularExpression(kw_pattern(_KEYWORDS),
|
|
QRegularExpression.PatternOption.CaseInsensitiveOption),
|
|
_KEYWORD_FMT),
|
|
# Data types
|
|
(QRegularExpression(kw_pattern(_TYPES),
|
|
QRegularExpression.PatternOption.CaseInsensitiveOption),
|
|
_TYPE_FMT),
|
|
# Functions
|
|
(QRegularExpression(kw_pattern(_FUNCTIONS),
|
|
QRegularExpression.PatternOption.CaseInsensitiveOption),
|
|
_FUNCTION_FMT),
|
|
# Numbers
|
|
(QRegularExpression(r"\b\d+(\.\d+)?\b"), _NUMBER_FMT),
|
|
# Single-quoted strings
|
|
(QRegularExpression(r"'[^'\\]*(?:\\.[^'\\]*)*'"), _STRING_FMT),
|
|
# Double-quoted identifiers
|
|
(QRegularExpression(r'"[^"]*"'), _fmt("#99d1db")),
|
|
# Backtick identifiers (MySQL)
|
|
(QRegularExpression(r"`[^`]*`"), _fmt("#99d1db")),
|
|
# * wildcard
|
|
(QRegularExpression(r"\bSELECT\s+\*|\*(?=\s*FROM)",
|
|
QRegularExpression.PatternOption.CaseInsensitiveOption),
|
|
_STAR_FMT),
|
|
# Operators
|
|
(QRegularExpression(r"[=<>!%&|^~+\-*/]"), _OPERATOR_FMT),
|
|
# Single-line comment --
|
|
(QRegularExpression(r"--[^\n]*"), _COMMENT_FMT),
|
|
# Single-line comment #
|
|
(QRegularExpression(r"#[^\n]*"), _COMMENT_FMT),
|
|
]
|
|
|
|
# Multi-line block comment /* ... */
|
|
self._block_comment_start = QRegularExpression(r"/\*")
|
|
self._block_comment_end = QRegularExpression(r"\*/")
|
|
|
|
def highlightBlock(self, text: str) -> None:
|
|
# Single-line rules
|
|
for pattern, fmt in self._rules:
|
|
it = pattern.globalMatch(text)
|
|
while it.hasNext():
|
|
m = it.next()
|
|
self.setFormat(m.capturedStart(), m.capturedLength(), fmt)
|
|
|
|
# Multi-line block comments
|
|
self.setCurrentBlockState(0)
|
|
start_idx = 0
|
|
if self.previousBlockState() != 1:
|
|
m = self._block_comment_start.match(text)
|
|
start_idx = m.capturedStart() if m.hasMatch() else -1
|
|
|
|
while start_idx >= 0:
|
|
end_m = self._block_comment_end.match(text, start_idx)
|
|
if end_m.hasMatch():
|
|
end_idx = end_m.capturedStart() + end_m.capturedLength()
|
|
self.setFormat(start_idx, end_idx - start_idx, _COMMENT_FMT)
|
|
m = self._block_comment_start.match(text, end_idx)
|
|
start_idx = m.capturedStart() if m.hasMatch() else -1
|
|
else:
|
|
self.setCurrentBlockState(1)
|
|
self.setFormat(start_idx, len(text) - start_idx, _COMMENT_FMT)
|
|
break
|