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
commite7a0ef6e5f5819f2f808afeda7c00c3a6c952380 authoriko <mail@iko.soy> dateSun 21 Jun 2026, 12:08 (3 months ago) parent8ac672a
5 files changed+30−13
ParseableViewer/Services/ParseableClient.swift+6−5 ParseableViewer/ViewModels/QueryViewModel.swift+4−3 ParseableViewer/Views/AlertsView.swift+5−1 ParseableViewer/Views/SQLAutoComplete.swift+4−1 ParseableViewer/Views/SQLEditorView.swift+11−3
ParseableViewer/Services/ParseableClient.swift+6−5
@@ -340,13 +340,14 @@ final class ParseableClient: Sendable {
340340 // an unexpected body yields an empty, non-throwing config rather than
341341 // a DecodingError — indistinguishable from a server that genuinely has
342342 // 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
344− // object carrying an "alerts" key). When it is, an empty result is
345− // authoritative and must not be replaced with unrelated legacy alerts.
343+ // body ISN'T a recognizable alerts container. Any top-level JSON array
344+ // OR object is the modern endpoint answering — including an empty object
345+ // `{}` or `{"alerts": []}` meaning "no alerts" — and is authoritative, so
346+ // it must not be replaced with unrelated legacy per-stream alerts. Only a
347+ // non-JSON / unparseable body counts as unrecognized.
346348 if config.alerts?.isEmpty ?? true {
347349 let json = try? JSONSerialization.jsonObject(with: data)
348− let isRecognizedShape = json is [Any]
349− || (json as? [String: Any])?["alerts"] != nil
350+ let isRecognizedShape = json is [Any] || json is [String: Any]
350351 if !isRecognizedShape,
351352 let legacy = try? await legacyAlerts(), !(legacy.alerts?.isEmpty ?? true) {
352353 return legacy
ParseableViewer/ViewModels/QueryViewModel.swift+4−3
@@ -73,7 +73,10 @@ final class QueryViewModel {
7373
7474 // Query history
7575 var queryHistory: [QueryHistoryEntry] = []
76− var historyIsFull = false
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.
79+ var historyIsFull: Bool { queryHistory.count >= Self.maxHistory }
7780 private static let maxHistory = 50
7881
7982 struct QueryHistoryEntry: Identifiable, Codable, Sendable {
@@ -711,14 +714,12 @@ final class QueryViewModel {
711714 queryHistory.insert(entry, at: 0)
712715 if queryHistory.count > Self.maxHistory {
713716 queryHistory = Array(queryHistory.prefix(Self.maxHistory))
714− historyIsFull = true
715717 }
716718 Self.saveHistory(queryHistory)
717719 }
718720
719721 func clearHistory() {
720722 queryHistory = []
721− historyIsFull = false
722723 Self.saveHistory([])
723724 }
724725
ParseableViewer/Views/AlertsView.swift+5−1
@@ -197,7 +197,11 @@ struct AlertRowView: View {
197197 switch alert.state?.lowercased() {
198198 case "triggered": return .red
199199 case "disabled": return .gray
200− default: return .orange
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.
203+ case .some: return .orange
204+ case nil: return .secondary
201205 }
202206 }
203207
ParseableViewer/Views/SQLAutoComplete.swift+4−1
@@ -201,7 +201,10 @@ enum SQLCompletionProvider {
201201 case "GROUP":
202202 return .afterGroup
203203 default:
204− if lastToken == "," {
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.
207+ if lastToken == "," || lastToken == "(" {
205208 for token in tokens.reversed() {
206209 let upper = token.uppercased()
207210 if ["SELECT", "BY", "WHERE", "HAVING", "ON"].contains(upper) {
ParseableViewer/Views/SQLEditorView.swift+11−3
@@ -61,10 +61,18 @@ struct SQLEditorView: NSViewRepresentable {
6161 context.coordinator.applyHighlighting(to: textView)
6262 // NSRange is UTF-16 based; compare against the NSString length, not
6363 // 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.
6467 let length = (textView.string as NSString).length
65− let validRanges = selectedRanges.filter { NSMaxRange($0.rangeValue) <= length }
66− if !validRanges.isEmpty {
67− textView.selectedRanges = validRanges
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+ }
74+ if !clampedRanges.isEmpty {
75+ textView.selectedRanges = clampedRanges
6876 }
6977 }
7078 context.coordinator.updateErrorHighlight(in: textView, errorRange: errorRange)