Skip to content

Commit d27c3f6

Browse files
committed
Merge remote-tracking branch 'origin/main' into fix/createdb-server-driven
# Conflicts: # CHANGELOG.md # Plugins/PostgreSQLDriverPlugin/PostgreSQLPluginDriver.swift
2 parents 856b116 + 0631570 commit d27c3f6

145 files changed

Lines changed: 6961 additions & 4953 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CHANGELOG.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,30 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1616
- Click a focused cell to start editing without a second click
1717
- Data grid focus ring follows the system accent color and contrast settings
1818
- Data grid cells expose accessibility row and column index ranges to VoiceOver on all dataset sizes
19+
- Data grid column headers announce sort direction and multi-sort priority to VoiceOver
1920
- Multi-cell paste: paste TSV data from the clipboard into the grid starting from the focused cell, grouped as a single undo action
2021
- Shift+Tab navigates to the previous cell in the data grid
2122
- Copy rows writes TSV, HTML table, and plain text to the clipboard for richer paste in spreadsheet apps
2223
- Row drag adds TSV and HTML representations alongside the internal drag type
24+
- AI provider settings allow manually entering a model name when the provider does not return one
2325

2426
### Changed
2527

2628
- Create Database dialog is now driver-driven. Each driver discovers its own valid options (PostgreSQL queries `pg_collation` and `pg_database`, MySQL/MariaDB query `information_schema.character_sets`/`collations`). The hardcoded macOS-flavored locale list is gone. Engines that don't support creation hide the Create button instead of failing on click.
2729
- Introduced TableRows, Row, and Delta value types in TablePro/Models/Query/ as the foundation for the data grid row model rewrite. No callers migrated yet (Phase C.1 of the DataGrid refactor).
30+
- DataGrid columns and cells refactored to use a persistent column pool and typed cell view hierarchy. CPU usage on table switch reduced significantly through proper NSTableView reuse pool retention.
31+
- Data grid column identifiers are now the column name (with positional fallback for duplicate names), so saved widths follow the column across schema changes that shift its position. Identifier resolution moved from static `DataGridView` helpers to a `ColumnIdentitySchema` value type owned by the coordinator.
32+
- `ColumnLayoutStorage` singleton replaced by a `ColumnLayoutPersisting` protocol with an injectable `FileColumnLayoutPersister` default. The coordinator depends on the protocol, not the concrete class, so tests can substitute a fake.
33+
- Column layout save/restore on table-switch (`saveColumnLayoutForTable` / `restoreColumnLayoutForTable`) folded into the data grid coordinator's lifecycle (load on column build, persist on resize/move/dismantle). The standalone `MainContentCoordinator+ColumnLayout` extension is gone; only the visibility orchestration remains. Removes the redundant `hasUserResizedColumns` flag and the external save trigger from the binding setter.
34+
- Data grid header sort indicators are drawn inside a custom `NSTableHeaderCell` instead of overlay `NSImageView` subviews, replacing Unicode arrows that were embedded in the column title string. The cell renders ascending/descending chevrons via SF Symbols (`chevron.up`/`chevron.down`) with a hierarchical tint, so they pick up the correct color in light and dark mode. Removing the overlay subviews lets `NSTableHeaderView`'s native cursor management run unimpeded, so the column resize cursor on hover works without any custom cursor handling. The primary sorted column gets the system header tint via `highlightedTableColumn`, and secondary sort columns show a small priority number to the left of the chevron.
35+
- Data grid header divider taps trigger a column resize instead of sorting the adjacent column. `SortableHeaderView` checks if the click landed within 4 pt of a column edge and forwards the event to `NSTableHeaderView`'s native resize handling.
36+
- Data grid column layout persistence routes through a coordinator callback fired from outside SwiftUI's update cycle, removing the `Task`-based `@Binding` mutation inside `updateNSView` and the `isWritingColumnLayout` re-entry guard.
37+
- Data grid cell reuse resets foreign-key arrow and dropdown chevron button context (target, action, row, column) when the button hides, preventing a stale handler from firing the wrong row if the column toggles between FK-eligible and not.
38+
- `applyColumnOrder` scans only the unsettled tail of the column array per move, halving the constant cost on reorders with many columns.
39+
- Replaced RowBuffer / InMemoryRowProvider / RowDataStore with TableRows / TableRowsStore / TableRowsController. Mutations emit Delta events; the controller drives NSTableView via insertRows / removeRows / reloadData(forRowIndexes:). Sort and the display cache moved off the row provider into the data grid coordinator, keyed by Row.id.
40+
- Routed every TableRows mutation through `mutateActiveTableRows` on MainContentCoordinator so the active ResultSet's snapshot stays in sync with the store. The snapshot now refreshes only when the user switches result sets (saving the outgoing tab, loading the incoming one), so each insert / undo / paste no longer triggers an `@Observable` re-render of the whole editor. Fixes empty cells on Load More and CPU spikes when adding or undoing rows.
41+
- Undo of a cell edit clears the modified-cell highlight: `DataChangeManager.applyDataUndo` now bumps `reloadVersion` so the data grid rebuilds its visual state cache.
42+
- Reloading a table tab keeps cached column metadata (defaults, foreign keys, nullability, enum values) when no fresh schema fetch was needed, so the FK arrow and dropdown chevron stay visible across reloads instead of toggling.
2843
- DataChangeManager extracted a PendingChanges value type that owns cross-collection invariants for cell edits, row insertions, and deletions. DataChangeManager kept undo/redo registration, plugin SQL generation, and the `@Observable` boundary, dropping from ~960 to ~190 lines. The serialization DTO `TabPendingChanges` is renamed to `TabChangeSnapshot` to distinguish it from the live tracker.
2944
- AnyChangeManager uses ChangeManaging protocol instead of closure-based type erasure, removing all runtime `[Any]` downcasts
3045
- Row selection state moved from MainContentView @State to GridSelectionState @Observable class, preventing full view tree invalidation on every row click
@@ -44,6 +59,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
4459
- Date picker popover font follows the data grid font setting
4560
- Data grid undo/redo uses the window's UndoManager instead of a private instance, unifying Cmd+Z across editor and grid
4661
- Right-click during cell editing shows the native text context menu instead of the row menu
62+
- Multiline cell overlay editor discards the in-progress edit when a column resize fires, instead of silently committing partial text
63+
- Data grid cell focus ring redraws when the user toggles Light or Dark mode mid-session, picking up the system's appearance-aware focus indicator color
64+
- Data grid keeps sortedIDs and cachedRowCount paired by calling updateCache() immediately after the SwiftUI bridge writes new sortedIDs to the coordinator, removing a window where the cached count and the sort permutation could disagree
65+
- Display formats memoized per tab on MainContentCoordinator keyed by schema version, smart-detection setting, and format-overrides version, so ValueDisplayDetector.detect runs once per result schema instead of on every SwiftUI body evaluation
4766

4867
### Fixed
4968

TablePro/Core/ChangeTracking/AnyChangeManager.swift

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ protocol ChangeManaging: AnyObject {
77
var reloadVersion: Int { get }
88
var canRedo: Bool { get }
99
var rowChanges: [RowChange] { get }
10+
var insertedRowIndices: Set<Int> { get }
1011
func isRowDeleted(_ rowIndex: Int) -> Bool
1112
func recordCellChange(
1213
rowIndex: Int,
@@ -18,7 +19,6 @@ protocol ChangeManaging: AnyObject {
1819
)
1920
func undoRowDeletion(rowIndex: Int)
2021
func undoRowInsertion(rowIndex: Int)
21-
func consumeChangedRowIndices() -> Set<Int>
2222
}
2323

2424
@Observable
@@ -30,6 +30,7 @@ final class AnyChangeManager {
3030
var reloadVersion: Int { wrapped.reloadVersion }
3131
var canRedo: Bool { wrapped.canRedo }
3232
var rowChanges: [RowChange] { wrapped.rowChanges }
33+
var insertedRowIndices: Set<Int> { wrapped.insertedRowIndices }
3334

3435
func isRowDeleted(_ rowIndex: Int) -> Bool {
3536
wrapped.isRowDeleted(rowIndex)
@@ -61,10 +62,6 @@ final class AnyChangeManager {
6162
wrapped.undoRowInsertion(rowIndex: rowIndex)
6263
}
6364

64-
func consumeChangedRowIndices() -> Set<Int> {
65-
wrapped.consumeChangedRowIndices()
66-
}
67-
6865
init(_ manager: any ChangeManaging) {
6966
self.wrapped = manager
7067
}

TablePro/Core/ChangeTracking/DataChangeManager.swift

Lines changed: 36 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,21 @@ struct UndoResult {
1717
let needsRowRemoval: Bool
1818
let needsRowRestore: Bool
1919
let restoreRow: [String?]?
20+
let delta: Delta
21+
22+
init(
23+
action: UndoAction,
24+
needsRowRemoval: Bool,
25+
needsRowRestore: Bool,
26+
restoreRow: [String?]?,
27+
delta: Delta = .none
28+
) {
29+
self.action = action
30+
self.needsRowRemoval = needsRowRemoval
31+
self.needsRowRestore = needsRowRestore
32+
self.restoreRow = restoreRow
33+
self.delta = delta
34+
}
2035
}
2136

2237
/// Manager for tracking and applying data changes
@@ -63,12 +78,6 @@ final class DataChangeManager: ChangeManaging {
6378
undoManager.setActionName(actionName)
6479
}
6580

66-
// MARK: - Helper Methods
67-
68-
func consumeChangedRowIndices() -> Set<Int> {
69-
pending.consumeChangedRowIndices()
70-
}
71-
7281
// MARK: - Configuration
7382

7483
func clearChanges() {
@@ -259,7 +268,10 @@ final class DataChangeManager: ChangeManaging {
259268
originalDBValue: newValue, newValue: previousValue, originalRow: originalRow
260269
)
261270
}
262-
lastUndoResult = UndoResult(action: action, needsRowRemoval: false, needsRowRestore: false, restoreRow: nil)
271+
lastUndoResult = UndoResult(
272+
action: action, needsRowRemoval: false, needsRowRestore: false, restoreRow: nil,
273+
delta: .cellChanged(row: rowIndex, column: columnIndex)
274+
)
263275
}
264276

265277
private func applyRowInsertionUndo(rowIndex: Int, action: UndoAction) {
@@ -274,12 +286,14 @@ final class DataChangeManager: ChangeManaging {
274286
if pending.isRowInserted(rowIndex) {
275287
_ = pending.undoRowInsertion(rowIndex: rowIndex)
276288
lastUndoResult = UndoResult(
277-
action: action, needsRowRemoval: true, needsRowRestore: false, restoreRow: nil
289+
action: action, needsRowRemoval: true, needsRowRestore: false, restoreRow: nil,
290+
delta: .rowsRemoved(IndexSet(integer: rowIndex))
278291
)
279292
} else {
280293
pending.reinsertRow(rowIndex: rowIndex, columns: columns, savedValues: savedValues)
281294
lastUndoResult = UndoResult(
282-
action: action, needsRowRemoval: false, needsRowRestore: true, restoreRow: savedValues
295+
action: action, needsRowRemoval: false, needsRowRestore: true, restoreRow: savedValues,
296+
delta: .rowsInserted(IndexSet(integer: rowIndex))
283297
)
284298
}
285299
}
@@ -292,12 +306,14 @@ final class DataChangeManager: ChangeManaging {
292306
if pending.isRowDeleted(rowIndex) {
293307
_ = pending.undoRowDeletion(rowIndex: rowIndex)
294308
lastUndoResult = UndoResult(
295-
action: action, needsRowRemoval: false, needsRowRestore: true, restoreRow: originalRow
309+
action: action, needsRowRemoval: false, needsRowRestore: true, restoreRow: originalRow,
310+
delta: .fullReplace
296311
)
297312
} else {
298313
pending.reapplyRowDeletion(rowIndex: rowIndex, originalRow: originalRow)
299314
lastUndoResult = UndoResult(
300-
action: action, needsRowRemoval: true, needsRowRestore: false, restoreRow: nil
315+
action: action, needsRowRemoval: true, needsRowRestore: false, restoreRow: nil,
316+
delta: .fullReplace
301317
)
302318
}
303319
}
@@ -315,14 +331,16 @@ final class DataChangeManager: ChangeManaging {
315331
_ = pending.undoRowDeletion(rowIndex: rowIndex)
316332
}
317333
lastUndoResult = UndoResult(
318-
action: action, needsRowRemoval: false, needsRowRestore: true, restoreRow: nil
334+
action: action, needsRowRemoval: false, needsRowRestore: true, restoreRow: nil,
335+
delta: .fullReplace
319336
)
320337
} else {
321338
for (rowIndex, originalRow) in rows {
322339
pending.reapplyRowDeletion(rowIndex: rowIndex, originalRow: originalRow)
323340
}
324341
lastUndoResult = UndoResult(
325-
action: action, needsRowRemoval: true, needsRowRestore: false, restoreRow: nil
342+
action: action, needsRowRemoval: true, needsRowRestore: false, restoreRow: nil,
343+
delta: .fullReplace
326344
)
327345
}
328346
}
@@ -335,15 +353,18 @@ final class DataChangeManager: ChangeManaging {
335353
}
336354

337355
let firstInserted = rowIndices.first.map { pending.isRowInserted($0) } ?? false
356+
let indices = IndexSet(rowIndices)
338357
if firstInserted {
339358
_ = pending.undoBatchRowInsertion(rowIndices: rowIndices, columnCount: columns.count)
340359
lastUndoResult = UndoResult(
341-
action: action, needsRowRemoval: true, needsRowRestore: false, restoreRow: nil
360+
action: action, needsRowRemoval: true, needsRowRestore: false, restoreRow: nil,
361+
delta: .rowsRemoved(indices)
342362
)
343363
} else {
344364
pending.reinsertBatch(rowIndices: rowIndices, rowValues: rowValues, columns: columns)
345365
lastUndoResult = UndoResult(
346-
action: action, needsRowRemoval: false, needsRowRestore: true, restoreRow: nil
366+
action: action, needsRowRemoval: false, needsRowRestore: true, restoreRow: nil,
367+
delta: .rowsInserted(indices)
347368
)
348369
}
349370
}

TablePro/Core/ChangeTracking/PendingChanges.swift

Lines changed: 0 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ struct PendingChanges: Equatable {
1717
private(set) var insertedRowIndices: Set<Int> = []
1818
private(set) var modifiedCells: [Int: Set<Int>] = [:]
1919
private(set) var insertedRowData: [Int: [String?]] = [:]
20-
private(set) var changedRowIndices: Set<Int> = []
2120

2221
private var changeIndex: [RowChangeKey: Int] = [:]
2322

@@ -77,7 +76,6 @@ struct PendingChanges: Equatable {
7776
if let insertIdx = changeIndex[RowChangeKey(rowIndex: rowIndex, type: .insert)] {
7877
updateInsertedCell(at: insertIdx, columnIndex: columnIndex,
7978
columnName: columnName, newValue: newValue)
80-
changedRowIndices.insert(rowIndex)
8179
return true
8280
}
8381

@@ -93,7 +91,6 @@ struct PendingChanges: Equatable {
9391
changeIndex[updateKey] = changes.count - 1
9492
modifiedCells[rowIndex, default: []].insert(columnIndex)
9593
}
96-
changedRowIndices.insert(rowIndex)
9794
return true
9895
}
9996

@@ -103,7 +100,6 @@ struct PendingChanges: Equatable {
103100
modifiedCells.removeValue(forKey: rowIndex)
104101
appendChange(RowChange(rowIndex: rowIndex, type: .delete, originalRow: originalRow))
105102
deletedRowIndices.insert(rowIndex)
106-
changedRowIndices.insert(rowIndex)
107103
}
108104

109105
mutating func recordRowInsertion(rowIndex: Int, values: [String?]) {
@@ -114,7 +110,6 @@ struct PendingChanges: Equatable {
114110
insertedRowData[rowIndex] = values
115111
appendChange(RowChange(rowIndex: rowIndex, type: .insert, cellChanges: []))
116112
insertedRowIndices.insert(rowIndex)
117-
changedRowIndices.insert(rowIndex)
118113
}
119114

120115
// MARK: - Mutate (cancelling pending edits)
@@ -123,7 +118,6 @@ struct PendingChanges: Equatable {
123118
guard deletedRowIndices.contains(rowIndex) else { return false }
124119
removeChange(rowIndex: rowIndex, type: .delete)
125120
deletedRowIndices.remove(rowIndex)
126-
changedRowIndices.insert(rowIndex)
127121
return true
128122
}
129123

@@ -135,7 +129,6 @@ struct PendingChanges: Equatable {
135129
insertedRowData.removeValue(forKey: rowIndex)
136130

137131
shiftRowIndicesDown(at: rowIndex)
138-
changedRowIndices.insert(rowIndex)
139132
return true
140133
}
141134

@@ -159,7 +152,6 @@ struct PendingChanges: Equatable {
159152
removeChange(rowIndex: rowIndex, type: .insert)
160153
insertedRowIndices.remove(rowIndex)
161154
insertedRowData.removeValue(forKey: rowIndex)
162-
changedRowIndices.insert(rowIndex)
163155
}
164156

165157
let sortedRemoved = validRows.sorted()
@@ -188,7 +180,6 @@ struct PendingChanges: Equatable {
188180
modifiedCells.removeValue(forKey: rowIndex)
189181
appendChange(RowChange(rowIndex: rowIndex, type: .delete, originalRow: originalRow))
190182
deletedRowIndices.insert(rowIndex)
191-
changedRowIndices.insert(rowIndex)
192183
}
193184

194185
/// Re-apply a cell edit during undo replay (skips undo registration).
@@ -213,7 +204,6 @@ struct PendingChanges: Equatable {
213204
if let insertIdx = changeIndex[RowChangeKey(rowIndex: rowIndex, type: .insert)] {
214205
updateInsertedCell(at: insertIdx, columnIndex: columnIndex,
215206
columnName: columnName, newValue: newValue)
216-
changedRowIndices.insert(rowIndex)
217207
return
218208
}
219209

@@ -229,7 +219,6 @@ struct PendingChanges: Equatable {
229219
changeIndex[updateKey] = changes.count - 1
230220
modifiedCells[rowIndex, default: []].insert(columnIndex)
231221
}
232-
changedRowIndices.insert(rowIndex)
233222
}
234223

235224
/// Replace an inserted row's cell value during undo replay (no shift, no undo).
@@ -241,7 +230,6 @@ struct PendingChanges: Equatable {
241230
) {
242231
guard let insertIdx = changeIndex[RowChangeKey(rowIndex: rowIndex, type: .insert)] else { return }
243232
updateInsertedCell(at: insertIdx, columnIndex: columnIndex, columnName: columnName, newValue: newValue)
244-
changedRowIndices.insert(rowIndex)
245233
}
246234

247235
/// Restore a cell's value during undo replay when an existing change matches.
@@ -274,7 +262,6 @@ struct PendingChanges: Equatable {
274262
newValue: previousValue
275263
)
276264
}
277-
changedRowIndices.insert(rowIndex)
278265
}
279266

280267
/// Insert a synthetic .insert RowChange for undo replay (e.g., after redoing a deletion's undo).
@@ -291,7 +278,6 @@ struct PendingChanges: Equatable {
291278
if let savedValues {
292279
insertedRowData[rowIndex] = savedValues
293280
}
294-
changedRowIndices.insert(rowIndex)
295281
}
296282

297283
/// Insert a batch of rows (for undo replay of a batch deletion's undo).
@@ -314,7 +300,6 @@ struct PendingChanges: Equatable {
314300
changes.append(RowChange(rowIndex: rowIndex, type: .insert, cellChanges: cellChanges))
315301
insertedRowIndices.insert(rowIndex)
316302
insertedRowData[rowIndex] = values
317-
changedRowIndices.insert(rowIndex)
318303
}
319304
rebuildChangeIndex()
320305
}
@@ -338,23 +323,14 @@ struct PendingChanges: Equatable {
338323
insertedRowIndices.removeAll()
339324
modifiedCells.removeAll()
340325
insertedRowData.removeAll()
341-
changedRowIndices.removeAll()
342326
}
343327

344-
mutating func consumeChangedRowIndices() -> Set<Int> {
345-
let indices = changedRowIndices
346-
changedRowIndices.removeAll()
347-
return indices
348-
}
349-
350-
/// Replace internal state from a serialized snapshot.
351328
mutating func restore(from snapshot: TabChangeSnapshot) {
352329
changes = snapshot.changes
353330
deletedRowIndices = snapshot.deletedRowIndices
354331
insertedRowIndices = snapshot.insertedRowIndices
355332
modifiedCells = snapshot.modifiedCells
356333
insertedRowData = snapshot.insertedRowData
357-
changedRowIndices = []
358334
rebuildChangeIndex()
359335
}
360336

@@ -471,7 +447,6 @@ struct PendingChanges: Equatable {
471447
if changes[updateIdx].cellChanges.isEmpty {
472448
removeChangeAt(updateIdx)
473449
}
474-
changedRowIndices.insert(rowIndex)
475450
return true
476451
}
477452

@@ -494,7 +469,6 @@ struct PendingChanges: Equatable {
494469
}
495470
modifiedCells = newModifiedCells
496471

497-
changedRowIndices = Set(changedRowIndices.map { $0 >= insertionPoint ? $0 + 1 : $0 })
498472
rebuildChangeIndex()
499473
}
500474

0 commit comments

Comments
 (0)