@@ -0,0 +1,237 @@
1+import AppKit
2+import SwiftUI
3+
4+// MARK: - SQL Syntax Highlighting
5+
6+enum SQLSyntaxHighlighter {
7+
8+ static let keywords: Set<String> = [
9+ "ADD", "ALL", "ALTER", "AND", "AS", "ASC",
10+ "BETWEEN", "BY",
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",
15+ "GROUP", "GROUPING",
16+ "HAVING",
17+ "IF", "IN", "INNER", "INSERT", "INTERSECT", "INTERVAL", "INTO", "IS",
18+ "JOIN",
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",
27+ "VALUES",
28+ "WHEN", "WHERE", "WINDOW", "WITH",
29+ ]
30+
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",
36+ "IIF", "IFNULL",
37+ "JSON_EXTRACT", "JSON_VALUE",
38+ "LAG", "LAST_VALUE", "LEAD", "LENGTH", "LOWER",
39+ "MAX", "MIN",
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",
44+ "UPPER",
45+ ]
46+
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 }
51+
52+ // Reset to default styling
53+ textStorage.addAttributes([
54+ .foregroundColor: NSColor.labelColor,
55+ .font: baseFont,
56+ ], range: fullRange)
57+
58+ let nsText = text as NSString
59+ var protectedRanges: [NSRange] = []
60+
61+ // 1. Comments (-- to end of line)
62+ applyPattern("--[^\n]*", to: textStorage, text: nsText,
63+ color: .secondaryLabelColor, protectedRanges: &protectedRanges)
64+
65+ // 2. Single-quoted strings
66+ applyPattern("'[^']*(?:''[^']*)*'", to: textStorage, text: nsText,
67+ color: .systemRed, protectedRanges: &protectedRanges)
68+
69+ // 3. Double-quoted identifiers
70+ applyPattern("\"[^\"]*(?:\"\"[^\"]*)*\"", to: textStorage, text: nsText,
71+ color: .systemRed, protectedRanges: &protectedRanges)
72+
73+ // 4. Numbers (outside protected ranges)
74+ applyUnprotectedPattern("\\b\\d+(?:\\.\\d+)?\\b", to: textStorage, text: nsText,
75+ color: .systemPurple, protectedRanges: protectedRanges)
76+
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)
82+
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)
88+ }
89+
90+ private static func applyPattern(
91+ _ pattern: String,
92+ to textStorage: NSTextStorage,
93+ text: NSString,
94+ color: NSColor,
95+ protectedRanges: inout [NSRange]
96+ ) {
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)
102+ }
103+ }
104+
105+ private static func applyUnprotectedPattern(
106+ _ pattern: String,
107+ to textStorage: NSTextStorage,
108+ text: NSString,
109+ color: NSColor,
110+ protectedRanges: [NSRange]
111+ ) {
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)
116+ }
117+ }
118+
119+ private static func applyUnprotectedKeywordPattern(
120+ _ pattern: String,
121+ to textStorage: NSTextStorage,
122+ text: NSString,
123+ color: NSColor,
124+ bold: Bool,
125+ baseFont: NSFont,
126+ protectedRanges: [NSRange]
127+ ) {
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)
133+ if bold {
134+ textStorage.addAttribute(.font, value: boldFont, range: match.range)
135+ }
136+ }
137+ }
138+
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) {
143+ return true
144+ }
145+ }
146+ return false
147+ }
148+}
149+
150+// MARK: - JSON Syntax Highlighting
151+
152+enum JSONSyntaxHighlighter {
153+
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)
159+
160+ let attributed = NSMutableAttributedString(string: json, attributes: [
161+ .font: resolvedFont,
162+ .foregroundColor: NSColor.labelColor,
163+ ])
164+
165+ guard fullRange.length > 0 else { return AttributedString(attributed) }
166+
167+ // Track string ranges to avoid coloring numbers/bools inside strings
168+ var stringRanges: [NSRange] = []
169+
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)
175+
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)
179+ if isKey {
180+ attributed.addAttribute(
181+ .font,
182+ value: NSFont.monospacedSystemFont(ofSize: resolvedFont.pointSize, weight: .medium),
183+ range: match.range)
184+ }
185+ }
186+ }
187+
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)
193+ }
194+ }
195+
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)
201+ }
202+ }
203+
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)
209+ }
210+ }
211+
212+ return AttributedString(attributed)
213+ }
214+
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 {
221+ idx += 1
222+ } else {
223+ return ch == 0x3A // ':'
224+ }
225+ }
226+ return false
227+ }
228+
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) {
232+ return true
233+ }
234+ }
235+ return false
236+ }
237+}