@@ -44,6 +44,24 @@ enum SQLSyntaxHighlighter {
47+ // Pre-sorted arrays for completions
48+ static let sortedKeywords: [String] = keywords.sorted()
49+ static let sortedFunctions: [String] = functions.sorted()
51+ // Pre-compiled regexes (created once, reused every keystroke)
52+ private static let commentRegex = try! NSRegularExpression(pattern: "--[^\n]*", options: [.caseInsensitive])
53+ private static let singleQuoteRegex = try! NSRegularExpression(pattern: "'[^']*(?:''[^']*)*'", options: [.caseInsensitive])
54+ private static let doubleQuoteRegex = try! NSRegularExpression(pattern: "\"[^\"]*(?:\"\"[^\"]*)*\"", options: [.caseInsensitive])
55+ private static let numberRegex = try! NSRegularExpression(pattern: "\\b\\d+(?:\\.\\d+)?\\b", options: [])
56+ private static let keywordRegex: NSRegularExpression = {
57+ let pattern = "\\b(?:" + sortedKeywords.joined(separator: "|") + ")\\b"
58+ return try! NSRegularExpression(pattern: pattern, options: [.caseInsensitive])
60+ private static let functionRegex: NSRegularExpression = {
61+ let pattern = "\\b(?:" + sortedFunctions.joined(separator: "|") + ")\\b"
62+ return try! NSRegularExpression(pattern: pattern, options: [.caseInsensitive])
47 /// Applies SQL syntax highlighting to an NSTextStorage.
65 /// Applies SQL syntax highlighting to an NSTextStorage.
48 static func highlight(_ text: String, in textStorage: NSTextStorage, baseFont: NSFont) {
66 static func highlight(_ text: String, in textStorage: NSTextStorage, baseFont: NSFont) {
49 let fullRange = NSRange(location: 0, length: textStorage.length)
67 let fullRange = NSRange(location: 0, length: textStorage.length)
@@ -59,42 +77,39 @@ enum SQLSyntaxHighlighter {
59 var protectedRanges: [NSRange] = []
77 var protectedRanges: [NSRange] = []
61 // 1. Comments (-- to end of line)
79 // 1. Comments (-- to end of line)
62− applyPattern("--[^\n]*", to: textStorage, text: nsText,
80+ applyRegex(commentRegex, to: textStorage, text: nsText,
63− color: .secondaryLabelColor, protectedRanges: &protectedRanges)
81+ color: .secondaryLabelColor, protectedRanges: &protectedRanges)
65 // 2. Single-quoted strings
83 // 2. Single-quoted strings
66− applyPattern("'[^']*(?:''[^']*)*'", to: textStorage, text: nsText,
84+ applyRegex(singleQuoteRegex, to: textStorage, text: nsText,
67− color: .systemRed, protectedRanges: &protectedRanges)
85+ color: .systemRed, protectedRanges: &protectedRanges)
69 // 3. Double-quoted identifiers
87 // 3. Double-quoted identifiers
70− applyPattern("\"[^\"]*(?:\"\"[^\"]*)*\"", to: textStorage, text: nsText,
88+ applyRegex(doubleQuoteRegex, to: textStorage, text: nsText,
71− color: .systemRed, protectedRanges: &protectedRanges)
89+ color: .systemRed, protectedRanges: &protectedRanges)
73 // 4. Numbers (outside protected ranges)
91 // 4. Numbers (outside protected ranges)
74− applyUnprotectedPattern("\\b\\d+(?:\\.\\d+)?\\b", to: textStorage, text: nsText,
92+ applyUnprotectedRegex(numberRegex, to: textStorage, text: nsText,
75− color: .systemPurple, protectedRanges: protectedRanges)
93+ color: .systemPurple, protectedRanges: protectedRanges)
77 // 5. Keywords (outside protected ranges)
95 // 5. Keywords (outside protected ranges)
78− let keywordPattern = "\\b(?:" + keywords.sorted().joined(separator: "|") + ")\\b"
96+ applyUnprotectedKeywordRegex(keywordRegex, to: textStorage, text: nsText,
79− applyUnprotectedKeywordPattern(keywordPattern, to: textStorage, text: nsText,
80− color: .systemBlue, bold: true, baseFont: baseFont,
97+ color: .systemBlue, bold: true, baseFont: baseFont,
81− protectedRanges: protectedRanges)
98+ protectedRanges: protectedRanges)
83 // 6. Functions (outside protected ranges)
100 // 6. Functions (outside protected ranges)
84− let functionPattern = "\\b(?:" + functions.sorted().joined(separator: "|") + ")\\b"
101+ applyUnprotectedKeywordRegex(functionRegex, to: textStorage, text: nsText,
85− applyUnprotectedKeywordPattern(functionPattern, to: textStorage, text: nsText,
86− color: .systemTeal, bold: false, baseFont: baseFont,
102+ color: .systemTeal, bold: false, baseFont: baseFont,
87− protectedRanges: protectedRanges)
103+ protectedRanges: protectedRanges)
90− private static func applyPattern(
106+ private static func applyRegex(
91− _ pattern: String,
107+ _ regex: NSRegularExpression,
92 to textStorage: NSTextStorage,
108 to textStorage: NSTextStorage,
93 text: NSString,
109 text: NSString,
94 color: NSColor,
110 color: NSColor,
95 protectedRanges: inout [NSRange]
111 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))
113 let matches = regex.matches(in: text as String, range: NSRange(location: 0, length: text.length))
99 for match in matches {
114 for match in matches {
100 textStorage.addAttribute(.foregroundColor, value: color, range: match.range)
115 textStorage.addAttribute(.foregroundColor, value: color, range: match.range)
@@ -102,22 +117,21 @@ enum SQLSyntaxHighlighter {
105− private static func applyUnprotectedPattern(
120+ private static func applyUnprotectedRegex(
106− _ pattern: String,
121+ _ regex: NSRegularExpression,
107 to textStorage: NSTextStorage,
122 to textStorage: NSTextStorage,
108 text: NSString,
123 text: NSString,
109 color: NSColor,
124 color: NSColor,
110 protectedRanges: [NSRange]
125 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))
127 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) {
128 for match in matches where !isProtected(match.range, by: protectedRanges) {
115 textStorage.addAttribute(.foregroundColor, value: color, range: match.range)
129 textStorage.addAttribute(.foregroundColor, value: color, range: match.range)
119− private static func applyUnprotectedKeywordPattern(
133+ private static func applyUnprotectedKeywordRegex(
120− _ pattern: String,
134+ _ regex: NSRegularExpression,
121 to textStorage: NSTextStorage,
135 to textStorage: NSTextStorage,
122 text: NSString,
136 text: NSString,
123 color: NSColor,
137 color: NSColor,
@@ -125,7 +139,6 @@ enum SQLSyntaxHighlighter {
125 baseFont: NSFont,
139 baseFont: NSFont,
126 protectedRanges: [NSRange]
140 protectedRanges: [NSRange]
128− guard let regex = try? NSRegularExpression(pattern: pattern, options: [.caseInsensitive]) else { return }
129 let boldFont = NSFont.monospacedSystemFont(ofSize: baseFont.pointSize, weight: .bold)
142 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))
143 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) {
144 for match in matches where !isProtected(match.range, by: protectedRanges) {
@@ -151,6 +164,12 @@ enum SQLSyntaxHighlighter {
152enum JSONSyntaxHighlighter {
165enum JSONSyntaxHighlighter {
167+ // Pre-compiled regexes (created once, reused every call)
168+ private static let stringRegex = try! NSRegularExpression(pattern: "\"(?:[^\"\\\\]|\\\\.)*\"", options: [])
169+ private static let boolRegex = try! NSRegularExpression(pattern: "\\b(true|false)\\b", options: [])
170+ private static let nullRegex = try! NSRegularExpression(pattern: "\\bnull\\b", options: [])
171+ private static let numberRegex = try! NSRegularExpression(pattern: "-?\\b\\d+(?:\\.\\d+)?(?:[eE][+-]?\\d+)?\\b", options: [])
154 /// Creates a syntax-highlighted AttributedString from a JSON string.
173 /// Creates a syntax-highlighted AttributedString from a JSON string.
155 static func highlight(_ json: String, font: NSFont? = nil) -> AttributedString {
174 static func highlight(_ json: String, font: NSFont? = nil) -> AttributedString {
156 let resolvedFont = font ?? NSFont.monospacedSystemFont(ofSize: NSFont.smallSystemFontSize, weight: .regular)
175 let resolvedFont = font ?? NSFont.monospacedSystemFont(ofSize: NSFont.smallSystemFontSize, weight: .regular)
@@ -168,45 +187,37 @@ enum JSONSyntaxHighlighter {
168 var stringRanges: [NSRange] = []
187 var stringRanges: [NSRange] = []
170 // 1. Double-quoted strings — distinguish keys from values
189 // 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)
190+ let matches = stringRegex.matches(in: json, range: fullRange)
173− for match in matches {
191+ for match in matches {
174− stringRanges.append(match.range)
192+ stringRanges.append(match.range)
176− let isKey = isJSONKey(after: match.range, in: nsString)
194+ let isKey = isJSONKey(after: match.range, in: nsString)
177− let color: NSColor = isKey ? .systemCyan : .systemRed
195+ let color: NSColor = isKey ? .systemCyan : .systemRed
178− attributed.addAttribute(.foregroundColor, value: color, range: match.range)
196+ attributed.addAttribute(.foregroundColor, value: color, range: match.range)
179− if isKey {
197+ if isKey {
180− attributed.addAttribute(
198+ attributed.addAttribute(
182− value: NSFont.monospacedSystemFont(ofSize: resolvedFont.pointSize, weight: .medium),
200+ value: NSFont.monospacedSystemFont(ofSize: resolvedFont.pointSize, weight: .medium),
183− range: match.range)
201+ range: match.range)
188 // 2. Booleans (outside strings)
205 // 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)
206+ for match in boolRegex.matches(in: json, range: fullRange)
191− where !isInString(match.range, stringRanges: stringRanges) {
207+ where !isInString(match.range, stringRanges: stringRanges) {
192− attributed.addAttribute(.foregroundColor, value: NSColor.systemOrange, range: match.range)
208+ attributed.addAttribute(.foregroundColor, value: NSColor.systemOrange, range: match.range)
196 // 3. Null (outside strings)
211 // 3. Null (outside strings)
197− if let regex = try? NSRegularExpression(pattern: "\\bnull\\b", options: []) {
198− for match in regex.matches(in: json, range: fullRange)
212+ for match in nullRegex.matches(in: json, range: fullRange)
199− where !isInString(match.range, stringRanges: stringRanges) {
213+ where !isInString(match.range, stringRanges: stringRanges) {
200− attributed.addAttribute(.foregroundColor, value: NSColor.secondaryLabelColor, range: match.range)
214+ attributed.addAttribute(.foregroundColor, value: NSColor.secondaryLabelColor, range: match.range)
204 // 4. Numbers (outside strings)
217 // 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)
218+ for match in numberRegex.matches(in: json, range: fullRange)
207− where !isInString(match.range, stringRanges: stringRanges) {
219+ where !isInString(match.range, stringRanges: stringRanges) {
208− attributed.addAttribute(.foregroundColor, value: NSColor.systemPurple, range: match.range)
220+ attributed.addAttribute(.foregroundColor, value: NSColor.systemPurple, range: match.range)
212 return AttributedString(attributed)
223 return AttributedString(attributed)