@@ -0,0 +1,237 @@
4+// MARK: - SQL Syntax Highlighting
6+enum SQLSyntaxHighlighter {
8+ static let keywords: Set<String> = [
9+ "ADD", "ALL", "ALTER", "AND", "AS", "ASC",
11+ "CASE", "CAST", "CREATE", "CROSS", "CUBE", "CURRENT",
12+ "DELETE", "DESC", "DISTINCT", "DROP",
13+ "ELSE", "END", "EXCEPT", "EXISTS", "EXTRACT",
14+ "FALSE", "FETCH", "FILTER", "FIRST", "FOLLOWING", "FOR", "FROM", "FULL",
17+ "IF", "IN", "INNER", "INSERT", "INTERSECT", "INTERVAL", "INTO", "IS",
19+ "LATERAL", "LEFT", "LIKE", "LIMIT",
20+ "NATURAL", "NEXT", "NOT", "NULL",
21+ "OFFSET", "ON", "ONLY", "OR", "ORDER", "OUTER", "OVER",
22+ "PARTITION", "PERCENT", "PRECEDING",
23+ "RANGE", "RECURSIVE", "RIGHT", "ROLLUP", "ROW", "ROWS",
24+ "SELECT", "SET", "SETS",
25+ "TABLE", "THEN", "TOP", "TRUE",
26+ "UNBOUNDED", "UNION", "UPDATE", "USING",
28+ "WHEN", "WHERE", "WINDOW", "WITH",
31+ static let functions: Set<String> = [
32+ "ABS", "ARRAY_AGG", "AVG",
33+ "CEIL", "COALESCE", "CONCAT", "COUNT",
34+ "DATE", "DATE_TRUNC", "DENSE_RANK",
35+ "FIRST_VALUE", "FLOOR",
37+ "JSON_EXTRACT", "JSON_VALUE",
38+ "LAG", "LAST_VALUE", "LEAD", "LENGTH", "LOWER",
40+ "NOW", "NTH_VALUE", "NTILE", "NULLIF",
41+ "RANK", "REPLACE", "ROUND", "ROW_NUMBER",
42+ "STRING_AGG", "SUBSTRING", "SUM",
43+ "TIME", "TIMESTAMP", "TO_CHAR", "TO_DATE", "TO_TIMESTAMP", "TRIM",
47+ /// Applies SQL syntax highlighting to an NSTextStorage.
48+ static func highlight(_ text: String, in textStorage: NSTextStorage, baseFont: NSFont) {
49+ let fullRange = NSRange(location: 0, length: textStorage.length)
50+ guard fullRange.length > 0 else { return }
52+ // Reset to default styling
53+ textStorage.addAttributes([
54+ .foregroundColor: NSColor.labelColor,
58+ let nsText = text as NSString
59+ var protectedRanges: [NSRange] = []
61+ // 1. Comments (-- to end of line)
62+ applyPattern("--[^\n]*", to: textStorage, text: nsText,
63+ color: .secondaryLabelColor, protectedRanges: &protectedRanges)
65+ // 2. Single-quoted strings
66+ applyPattern("'[^']*(?:''[^']*)*'", to: textStorage, text: nsText,
67+ color: .systemRed, protectedRanges: &protectedRanges)
69+ // 3. Double-quoted identifiers
70+ applyPattern("\"[^\"]*(?:\"\"[^\"]*)*\"", to: textStorage, text: nsText,
71+ color: .systemRed, protectedRanges: &protectedRanges)
73+ // 4. Numbers (outside protected ranges)
74+ applyUnprotectedPattern("\\b\\d+(?:\\.\\d+)?\\b", to: textStorage, text: nsText,
75+ color: .systemPurple, protectedRanges: protectedRanges)
77+ // 5. Keywords (outside protected ranges)
78+ let keywordPattern = "\\b(?:" + keywords.sorted().joined(separator: "|") + ")\\b"
79+ applyUnprotectedKeywordPattern(keywordPattern, to: textStorage, text: nsText,
80+ color: .systemBlue, bold: true, baseFont: baseFont,
81+ protectedRanges: protectedRanges)
83+ // 6. Functions (outside protected ranges)
84+ let functionPattern = "\\b(?:" + functions.sorted().joined(separator: "|") + ")\\b"
85+ applyUnprotectedKeywordPattern(functionPattern, to: textStorage, text: nsText,
86+ color: .systemTeal, bold: false, baseFont: baseFont,
87+ protectedRanges: protectedRanges)
90+ private static func applyPattern(
92+ to textStorage: NSTextStorage,
95+ protectedRanges: inout [NSRange]
97+ guard let regex = try? NSRegularExpression(pattern: pattern, options: [.caseInsensitive]) else { return }
98+ let matches = regex.matches(in: text as String, range: NSRange(location: 0, length: text.length))
99+ for match in matches {
100+ textStorage.addAttribute(.foregroundColor, value: color, range: match.range)
101+ protectedRanges.append(match.range)
105+ private static func applyUnprotectedPattern(
107+ to textStorage: NSTextStorage,
110+ protectedRanges: [NSRange]
112+ guard let regex = try? NSRegularExpression(pattern: pattern, options: []) else { return }
113+ let matches = regex.matches(in: text as String, range: NSRange(location: 0, length: text.length))
114+ for match in matches where !isProtected(match.range, by: protectedRanges) {
115+ textStorage.addAttribute(.foregroundColor, value: color, range: match.range)
119+ private static func applyUnprotectedKeywordPattern(
121+ to textStorage: NSTextStorage,
126+ protectedRanges: [NSRange]
128+ guard let regex = try? NSRegularExpression(pattern: pattern, options: [.caseInsensitive]) else { return }
129+ let boldFont = NSFont.monospacedSystemFont(ofSize: baseFont.pointSize, weight: .bold)
130+ let matches = regex.matches(in: text as String, range: NSRange(location: 0, length: text.length))
131+ for match in matches where !isProtected(match.range, by: protectedRanges) {
132+ textStorage.addAttribute(.foregroundColor, value: color, range: match.range)
134+ textStorage.addAttribute(.font, value: boldFont, range: match.range)
139+ private static func isProtected(_ range: NSRange, by protectedRanges: [NSRange]) -> Bool {
140+ for protected in protectedRanges {
141+ if range.location >= protected.location &&
142+ NSMaxRange(range) <= NSMaxRange(protected) {
150+// MARK: - JSON Syntax Highlighting
152+enum JSONSyntaxHighlighter {
154+ /// Creates a syntax-highlighted AttributedString from a JSON string.
155+ static func highlight(_ json: String, font: NSFont? = nil) -> AttributedString {
156+ let resolvedFont = font ?? NSFont.monospacedSystemFont(ofSize: NSFont.smallSystemFontSize, weight: .regular)
157+ let nsString = json as NSString
158+ let fullRange = NSRange(location: 0, length: nsString.length)
160+ let attributed = NSMutableAttributedString(string: json, attributes: [
161+ .font: resolvedFont,
162+ .foregroundColor: NSColor.labelColor,
165+ guard fullRange.length > 0 else { return AttributedString(attributed) }
167+ // Track string ranges to avoid coloring numbers/bools inside strings
168+ var stringRanges: [NSRange] = []
170+ // 1. Double-quoted strings — distinguish keys from values
171+ if let regex = try? NSRegularExpression(pattern: "\"(?:[^\"\\\\]|\\\\.)*\"", options: []) {
172+ let matches = regex.matches(in: json, range: fullRange)
173+ for match in matches {
174+ stringRanges.append(match.range)
176+ let isKey = isJSONKey(after: match.range, in: nsString)
177+ let color: NSColor = isKey ? .systemCyan : .systemRed
178+ attributed.addAttribute(.foregroundColor, value: color, range: match.range)
180+ attributed.addAttribute(
182+ value: NSFont.monospacedSystemFont(ofSize: resolvedFont.pointSize, weight: .medium),
188+ // 2. Booleans (outside strings)
189+ if let regex = try? NSRegularExpression(pattern: "\\b(true|false)\\b", options: []) {
190+ for match in regex.matches(in: json, range: fullRange)
191+ where !isInString(match.range, stringRanges: stringRanges) {
192+ attributed.addAttribute(.foregroundColor, value: NSColor.systemOrange, range: match.range)
196+ // 3. Null (outside strings)
197+ if let regex = try? NSRegularExpression(pattern: "\\bnull\\b", options: []) {
198+ for match in regex.matches(in: json, range: fullRange)
199+ where !isInString(match.range, stringRanges: stringRanges) {
200+ attributed.addAttribute(.foregroundColor, value: NSColor.secondaryLabelColor, range: match.range)
204+ // 4. Numbers (outside strings)
205+ if let regex = try? NSRegularExpression(pattern: "-?\\b\\d+(?:\\.\\d+)?(?:[eE][+-]?\\d+)?\\b", options: []) {
206+ for match in regex.matches(in: json, range: fullRange)
207+ where !isInString(match.range, stringRanges: stringRanges) {
208+ attributed.addAttribute(.foregroundColor, value: NSColor.systemPurple, range: match.range)
212+ return AttributedString(attributed)
215+ /// Checks whether a matched string token is a JSON key by scanning forward for a colon.
216+ private static func isJSONKey(after range: NSRange, in text: NSString) -> Bool {
217+ var idx = NSMaxRange(range)
218+ while idx < text.length {
219+ let ch = text.character(at: idx)
220+ if ch == 0x20 || ch == 0x09 || ch == 0x0A || ch == 0x0D {
223+ return ch == 0x3A // ':'
229+ private static func isInString(_ range: NSRange, stringRanges: [NSRange]) -> Bool {
230+ for sr in stringRanges {
231+ if range.location >= sr.location && NSMaxRange(range) <= NSMaxRange(sr) {