@@ -6,6 +6,7 @@ struct SQLEditorView: NSViewRepresentable {
6 @Binding var text: String
6 @Binding var text: String
7 var streamNames: [String] = []
7 var streamNames: [String] = []
8 var schemaFields: [SchemaField] = []
8 var schemaFields: [SchemaField] = []
9+ var errorRange: NSRange?
10 static let editorFont = NSFont.monospacedSystemFont(ofSize: NSFont.systemFontSize, weight: .regular)
11 static let editorFont = NSFont.monospacedSystemFont(ofSize: NSFont.systemFontSize, weight: .regular)
@@ -38,6 +39,7 @@ struct SQLEditorView: NSViewRepresentable {
39 textView.string = text
40 textView.string = text
40 context.coordinator.applyHighlighting(to: textView)
41 context.coordinator.applyHighlighting(to: textView)
42+ context.coordinator.updateErrorHighlight(in: textView, errorRange: errorRange)
42 scrollView.hasVerticalScroller = true
44 scrollView.hasVerticalScroller = true
43 scrollView.hasHorizontalScroller = false
45 scrollView.hasHorizontalScroller = false
@@ -62,6 +64,7 @@ struct SQLEditorView: NSViewRepresentable {
62 textView.selectedRanges = validRanges
64 textView.selectedRanges = validRanges
67+ context.coordinator.updateErrorHighlight(in: textView, errorRange: errorRange)
67 func makeCoordinator() -> Coordinator {
70 func makeCoordinator() -> Coordinator {
@@ -74,6 +77,11 @@ struct SQLEditorView: NSViewRepresentable {
74 var streamNames: [String]
77 var streamNames: [String]
75 var schemaFields: [SchemaField]
78 var schemaFields: [SchemaField]
80+ /// Tracks the currently applied error underline so we can clear it.
81+ private var lastAppliedErrorRange: NSRange?
82+ /// The text content when the error was applied; used to detect stale ranges.
83+ private var errorAppliedForText: String?
77 private let completionPopup = SQLCompletionPopup()
85 private let completionPopup = SQLCompletionPopup()
78 /// Tracks whether we are inserting a completion so that `textDidChange`
86 /// Tracks whether we are inserting a completion so that `textDidChange`
79 /// doesn't re-trigger the popup for the replacement.
87 /// doesn't re-trigger the popup for the replacement.
@@ -95,6 +103,7 @@ struct SQLEditorView: NSViewRepresentable {
95 func textDidChange(_ notification: Notification) {
103 func textDidChange(_ notification: Notification) {
96 guard let textView = notification.object as? NSTextView else { return }
104 guard let textView = notification.object as? NSTextView else { return }
97 isEditing = true
105 isEditing = true
106+ clearErrorHighlight(in: textView)
98 text.wrappedValue = textView.string
107 text.wrappedValue = textView.string
99 applyHighlighting(to: textView)
108 applyHighlighting(to: textView)
100 isEditing = false
109 isEditing = false
@@ -209,6 +218,60 @@ struct SQLEditorView: NSViewRepresentable {
221+ // MARK: - Error Highlighting
223+ func updateErrorHighlight(in textView: NSTextView, errorRange: NSRange?) {
224+ guard let layoutManager = textView.layoutManager else { return }
225+ let text = textView.string
227+ // If text changed since we last applied an error, clear stale highlight
228+ if let appliedText = errorAppliedForText, appliedText != text {
229+ clearErrorHighlight(in: textView)
232+ guard let range = errorRange else {
233+ if lastAppliedErrorRange != nil {
234+ clearErrorHighlight(in: textView)
239+ // Validate range is within bounds
240+ let fullLength = (text as NSString).length
241+ guard range.location + range.length <= fullLength else { return }
243+ // Don't re-apply the same highlight
244+ if lastAppliedErrorRange == range && errorAppliedForText == text { return }
246+ // Clear any previous highlight first
247+ if lastAppliedErrorRange != nil {
248+ clearErrorHighlight(in: textView)
251+ layoutManager.addTemporaryAttribute(
253+ value: NSUnderlineStyle.thick.rawValue,
254+ forCharacterRange: range
256+ layoutManager.addTemporaryAttribute(
258+ value: NSColor.systemRed,
259+ forCharacterRange: range
262+ lastAppliedErrorRange = range
263+ errorAppliedForText = text
266+ func clearErrorHighlight(in textView: NSTextView) {
267+ guard let layoutManager = textView.layoutManager else { return }
268+ let fullRange = NSRange(location: 0, length: (textView.string as NSString).length)
269+ layoutManager.removeTemporaryAttribute(.underlineStyle, forCharacterRange: fullRange)
270+ layoutManager.removeTemporaryAttribute(.underlineColor, forCharacterRange: fullRange)
271+ lastAppliedErrorRange = nil
272+ errorAppliedForText = nil
212 // MARK: - Helpers
275 // MARK: - Helpers
214 func applyHighlighting(to textView: NSTextView) {
277 func applyHighlighting(to textView: NSTextView) {