git.iko.soy/parseable-macos
on master help sign in
Fix 5 bugs: history full flag, autocomplete after paren, alert bell color, alerts fallback, editor selection clamp
e7a0ef6iko
ParseableViewer/Services/ParseableClient.swift+6−5
@@ -340,13 +340,14 @@ final class ParseableClient: Sendable {
340 // an unexpected body yields an empty, non-throwing config rather than
340 // an unexpected body yields an empty, non-throwing config rather than
341 // a DecodingError — indistinguishable from a server that genuinely has
341 // a DecodingError — indistinguishable from a server that genuinely has
342 // no alerts. Only fall back to the legacy per-stream endpoint when the
342 // no alerts. Only fall back to the legacy per-stream endpoint when the
343− // body ISN'T a recognizable alerts container (a top-level array, or an
343+ // body ISN'T a recognizable alerts container. Any top-level JSON array
344− // object carrying an "alerts" key). When it is, an empty result is
344+ // OR object is the modern endpoint answering — including an empty object
345+ // `{}` or `{"alerts": []}` meaning "no alerts" — and is authoritative, so
345− // authoritative and must not be replaced with unrelated legacy alerts.
346+ // it must not be replaced with unrelated legacy per-stream alerts. Only a
347+ // non-JSON / unparseable body counts as unrecognized.
346 if config.alerts?.isEmpty ?? true {
348 if config.alerts?.isEmpty ?? true {
347 let json = try? JSONSerialization.jsonObject(with: data)
349 let json = try? JSONSerialization.jsonObject(with: data)
348− let isRecognizedShape = json is [Any]
350+ let isRecognizedShape = json is [Any] || json is [String: Any]
349− || (json as? [String: Any])?["alerts"] != nil
350 if !isRecognizedShape,
351 if !isRecognizedShape,
351 let legacy = try? await legacyAlerts(), !(legacy.alerts?.isEmpty ?? true) {
352 let legacy = try? await legacyAlerts(), !(legacy.alerts?.isEmpty ?? true) {
352 return legacy
353 return legacy
ParseableViewer/ViewModels/QueryViewModel.swift+4−3
@@ -73,7 +73,10 @@ final class QueryViewModel {
73
73
74 // Query history
74 // Query history
75 var queryHistory: [QueryHistoryEntry] = []
75 var queryHistory: [QueryHistoryEntry] = []
76+ // Derived from the (always-trimmed) history so it stays correct after a
77+ // restored full history is loaded at launch, not just after an in-session
78+ // overflow. `addToHistory` trims to `maxHistory`, so count never exceeds it.
76− var historyIsFull = false
79+ var historyIsFull: Bool { queryHistory.count >= Self.maxHistory }
77 private static let maxHistory = 50
80 private static let maxHistory = 50
78
81
79 struct QueryHistoryEntry: Identifiable, Codable, Sendable {
82 struct QueryHistoryEntry: Identifiable, Codable, Sendable {
@@ -711,14 +714,12 @@ final class QueryViewModel {
711 queryHistory.insert(entry, at: 0)
714 queryHistory.insert(entry, at: 0)
712 if queryHistory.count > Self.maxHistory {
715 if queryHistory.count > Self.maxHistory {
713 queryHistory = Array(queryHistory.prefix(Self.maxHistory))
716 queryHistory = Array(queryHistory.prefix(Self.maxHistory))
714− historyIsFull = true
715 }
717 }
716 Self.saveHistory(queryHistory)
718 Self.saveHistory(queryHistory)
717 }
719 }
718
720
719 func clearHistory() {
721 func clearHistory() {
720 queryHistory = []
722 queryHistory = []
721− historyIsFull = false
722 Self.saveHistory([])
723 Self.saveHistory([])
723 }
724 }
724
725
ParseableViewer/Views/AlertsView.swift+5−1
@@ -197,7 +197,11 @@ struct AlertRowView: View {
197 switch alert.state?.lowercased() {
197 switch alert.state?.lowercased() {
198 case "triggered": return .red
198 case "triggered": return .red
199 case "disabled": return .gray
199 case "disabled": return .gray
200+ // A reported-but-unrecognized state is a genuine "needs attention" signal,
201+ // so tint it orange; a missing state is simply unknown and stays neutral so
202+ // the bell icon isn't painted as a warning for every stateless alert.
200− default: return .orange
203+ case .some: return .orange
204+ case nil: return .secondary
201 }
205 }
202 }
206 }
203
207
ParseableViewer/Views/SQLAutoComplete.swift+4−1
@@ -201,7 +201,10 @@ enum SQLCompletionProvider {
201 case "GROUP":
201 case "GROUP":
202 return .afterGroup
202 return .afterGroup
203 default:
203 default:
204+ // After a `,` or an opening `(` the caret continues an argument/column
205+ // list (e.g. `SELECT COUNT(`, `WHERE status IN (`, `SELECT a, `), so
206+ // scan back to the governing keyword to decide column vs table context.
204− if lastToken == "," {
207+ if lastToken == "," || lastToken == "(" {
205 for token in tokens.reversed() {
208 for token in tokens.reversed() {
206 let upper = token.uppercased()
209 let upper = token.uppercased()
207 if ["SELECT", "BY", "WHERE", "HAVING", "ON"].contains(upper) {
210 if ["SELECT", "BY", "WHERE", "HAVING", "ON"].contains(upper) {
ParseableViewer/Views/SQLEditorView.swift+11−3
@@ -61,10 +61,18 @@ struct SQLEditorView: NSViewRepresentable {
61 context.coordinator.applyHighlighting(to: textView)
61 context.coordinator.applyHighlighting(to: textView)
62 // NSRange is UTF-16 based; compare against the NSString length, not
62 // NSRange is UTF-16 based; compare against the NSString length, not
63 // String.count (grapheme count), or wide characters drop the selection.
63 // String.count (grapheme count), or wide characters drop the selection.
64+ // Clamp each range to the new length rather than discarding the ones that
65+ // overflow, so replacing the text with a shorter string keeps the caret
66+ // at the end instead of losing the selection entirely.
64 let length = (textView.string as NSString).length
67 let length = (textView.string as NSString).length
65− let validRanges = selectedRanges.filter { NSMaxRange($0.rangeValue) <= length }
68+ let clampedRanges = selectedRanges.map { value -> NSValue in
69+ let range = value.rangeValue
70+ let location = min(range.location, length)
71+ let len = min(range.length, length - location)
72+ return NSValue(range: NSRange(location: location, length: len))
73+ }
66− if !validRanges.isEmpty {
74+ if !clampedRanges.isEmpty {
67− textView.selectedRanges = validRanges
75+ textView.selectedRanges = clampedRanges
68 }
76 }
69 }
77 }
70 context.coordinator.updateErrorHighlight(in: textView, errorRange: errorRange)
78 context.coordinator.updateErrorHighlight(in: textView, errorRange: errorRange)