git.iko.soy/parseable-macos
on master help sign in
Fix 3 bugs: zero-visible columns, column width recompute, completion clarity
commit9c46d1df07eac5ff05381d8581eb05b5f117555c authoriko <mail@iko.soy> dateSun 21 Jun 2026, 00:48 (3 months ago) parent9a66b83
3 files changed+51−32
ParseableViewer/ViewModels/LiveTailViewModel.swift+10 ParseableViewer/Views/LogTableView.swift+27−23 ParseableViewer/Views/SQLAutoComplete.swift+14−9
ParseableViewer/ViewModels/LiveTailViewModel.swift+10
@@ -339,6 +339,16 @@ final class LiveTailViewModel {
339339 hiddenColumns.formUnion(newCols.filter { savedHiddenColumns.contains($0) })
340340 }
341341 }
342+
343+ // The `visibleCount > 1` guard in toggleColumnVisibility only covers
344+ // user toggles. The config-load and incremental-merge paths above can
345+ // hide every known column (e.g. a saved-hidden set that happens to cover
346+ // all columns seen so far), which would render the live-tail table with
347+ // zero columns. Keep at least the first column visible.
348+ if !columnOrder.isEmpty, hiddenColumns.count >= columnOrder.count,
349+ let firstColumn = columnOrder.first {
350+ hiddenColumns.remove(firstColumn)
351+ }
342352 }
343353
344354 private func poll(client: ParseableClient, stream: String) async {
ParseableViewer/Views/LogTableView.swift+27−23
@@ -288,26 +288,14 @@ struct LogTableView: View {
288288 }
289289 }
290290 .onChange(of: records) { _, newRecords in
291− widthTask?.cancel()
292− let cols = columns
293− widthTask = Task.detached(priority: .userInitiated) {
294− let widths = computeColumnWidths(columns: cols, records: newRecords)
295− guard !Task.isCancelled else { return }
296− await MainActor.run {
297− // Preserve any width the user dragged or auto-fit; only fill
298− // in columns that don't have a width yet (e.g. newly appearing
299− // columns). A wholesale overwrite would discard manual resizes
300− // on every re-query/filter/refresh.
301− var merged = columnWidths
302− for (col, width) in widths where merged[col] == nil {
303− merged[col] = width
304− }
305− columnWidths = merged
306− }
307− }
291+ recomputeColumnWidths(columns: columns, records: newRecords)
308292 debouncedRebuildSort()
309293 }
310294 .onChange(of: columns) { _, newColumns in
295+ // New columns (e.g. shown via the Column Manager) need widths
296+ // computed; the width task otherwise only runs on a records
297+ // change, leaving them at the default width until the next query.
298+ recomputeColumnWidths(columns: newColumns, records: records)
311299 // After a stream switch the previous sort column may not exist
312300 // in the new schema, which would silently produce a no-op sort
313301 // (every row compares as .null). Drop it so rows fall back to
@@ -326,17 +314,33 @@ struct LogTableView: View {
326314 .onChange(of: sortColumn) { _, _ in debouncedRebuildSort() }
327315 .onChange(of: sortAscending) { _, _ in debouncedRebuildSort() }
328316 .onAppear {
329− let cols = columns
330− let recs = records
331− widthTask = Task.detached(priority: .userInitiated) {
332− let widths = computeColumnWidths(columns: cols, records: recs)
333− await MainActor.run { columnWidths = widths }
334− }
317+ recomputeColumnWidths(columns: columns, records: records)
335318 rebuildSort()
336319 }
337320 }
338321 }
339322
323+ /// Computes column widths off the main actor and merges them into
324+ /// `columnWidths`, filling only columns that don't yet have a width.
325+ /// Preserves any width the user dragged or auto-fit; a wholesale overwrite
326+ /// would discard manual resizes on every re-query/filter/refresh. Runs on
327+ /// both records changes and columns-only changes so newly shown columns get
328+ /// a fitted width rather than the default.
329+ private func recomputeColumnWidths(columns cols: [String], records recs: [LogRecord]) {
330+ widthTask?.cancel()
331+ widthTask = Task.detached(priority: .userInitiated) {
332+ let widths = computeColumnWidths(columns: cols, records: recs)
333+ guard !Task.isCancelled else { return }
334+ await MainActor.run {
335+ var merged = columnWidths
336+ for (col, width) in widths where merged[col] == nil {
337+ merged[col] = width
338+ }
339+ columnWidths = merged
340+ }
341+ }
342+ }
343+
340344 private func debouncedRebuildSort() {
341345 sortTask?.cancel()
342346 sortTask = Task {
ParseableViewer/Views/SQLAutoComplete.swift+14−9
@@ -10,18 +10,23 @@ struct SQLCompletionItem: Hashable {
1010 case column
1111 }
1212
13+ /// The text inserted into the editor when the completion is accepted. For
14+ /// tables this is the quoted identifier (`"name"`) so the resulting SQL is
15+ /// valid even when the stream name contains spaces or other special chars.
1316 let text: String
1417 let kind: Kind
1518 let detail: String?
16− /// The raw text to insert (without quoting). For tables this equals the
17− /// unquoted stream name so prefix matching works correctly.
18− let insertText: String
19+ /// The bare text used for prefix/exact-match comparison against what the
20+ /// user typed. For tables this is the unquoted stream name, since the typed
21+ /// prefix never includes the surrounding quotes. This is NOT what gets
22+ /// inserted — see `text`.
23+ let matchText: String
1924
20− init(text: String, kind: Kind, detail: String? = nil, insertText: String? = nil) {
25+ init(text: String, kind: Kind, detail: String? = nil, matchText: String? = nil) {
2126 self.text = text
2227 self.kind = kind
2328 self.detail = detail
24− self.insertText = insertText ?? text
29+ self.matchText = matchText ?? text
2530 }
2631
2732 var kindLabel: String {
@@ -99,7 +104,7 @@ enum SQLCompletionProvider {
99104 items.append(SQLCompletionItem(
100105 text: "\"\(name)\"",
101106 kind: .table,
102− insertText: name
107+ matchText: name
103108 ))
104109 }
105110
@@ -131,7 +136,7 @@ enum SQLCompletionProvider {
131136 items.append(SQLCompletionItem(
132137 text: "\"\(name)\"",
133138 kind: .table,
134− insertText: name
139+ matchText: name
135140 ))
136141 }
137142 for field in schemaFields.sorted(by: { $0.name < $1.name })
@@ -141,9 +146,9 @@ enum SQLCompletionProvider {
141146 }
142147
143148 // Don't show the popup when the only match is an exact hit. Compare the
144− // insert text (unquoted), not the display text — a table's display text is
149+ // match text (unquoted), not the display text — a table's display text is
145150 // wrapped in quotes (`"name"`) and would never match the bare prefix.
146− if items.count == 1 && items[0].insertText.uppercased() == uppercasePrefix {
151+ if items.count == 1 && items[0].matchText.uppercased() == uppercasePrefix {
147152 return ([], prefix, range)
148153 }
149154