diff --git a/TablePro/Core/ChangeTracking/AnyChangeManager.swift b/TablePro/Core/ChangeTracking/AnyChangeManager.swift index cfa0c1ac3..200cde5e2 100644 --- a/TablePro/Core/ChangeTracking/AnyChangeManager.swift +++ b/TablePro/Core/ChangeTracking/AnyChangeManager.swift @@ -19,7 +19,6 @@ protocol ChangeManaging: AnyObject { ) func undoRowDeletion(rowIndex: Int) func undoRowInsertion(rowIndex: Int) - func consumeChangedRowIndices() -> Set } @Observable @@ -63,10 +62,6 @@ final class AnyChangeManager { wrapped.undoRowInsertion(rowIndex: rowIndex) } - func consumeChangedRowIndices() -> Set { - wrapped.consumeChangedRowIndices() - } - init(_ manager: any ChangeManaging) { self.wrapped = manager } diff --git a/TablePro/Core/ChangeTracking/DataChangeManager.swift b/TablePro/Core/ChangeTracking/DataChangeManager.swift index c0d12c513..5587136a8 100644 --- a/TablePro/Core/ChangeTracking/DataChangeManager.swift +++ b/TablePro/Core/ChangeTracking/DataChangeManager.swift @@ -78,12 +78,6 @@ final class DataChangeManager: ChangeManaging { undoManager.setActionName(actionName) } - // MARK: - Helper Methods - - func consumeChangedRowIndices() -> Set { - pending.consumeChangedRowIndices() - } - // MARK: - Configuration func clearChanges() { diff --git a/TablePro/Core/ChangeTracking/PendingChanges.swift b/TablePro/Core/ChangeTracking/PendingChanges.swift index 286c25b3c..c1fd6f9b3 100644 --- a/TablePro/Core/ChangeTracking/PendingChanges.swift +++ b/TablePro/Core/ChangeTracking/PendingChanges.swift @@ -17,7 +17,6 @@ struct PendingChanges: Equatable { private(set) var insertedRowIndices: Set = [] private(set) var modifiedCells: [Int: Set] = [:] private(set) var insertedRowData: [Int: [String?]] = [:] - private(set) var changedRowIndices: Set = [] private var changeIndex: [RowChangeKey: Int] = [:] @@ -77,7 +76,6 @@ struct PendingChanges: Equatable { if let insertIdx = changeIndex[RowChangeKey(rowIndex: rowIndex, type: .insert)] { updateInsertedCell(at: insertIdx, columnIndex: columnIndex, columnName: columnName, newValue: newValue) - changedRowIndices.insert(rowIndex) return true } @@ -93,7 +91,6 @@ struct PendingChanges: Equatable { changeIndex[updateKey] = changes.count - 1 modifiedCells[rowIndex, default: []].insert(columnIndex) } - changedRowIndices.insert(rowIndex) return true } @@ -103,7 +100,6 @@ struct PendingChanges: Equatable { modifiedCells.removeValue(forKey: rowIndex) appendChange(RowChange(rowIndex: rowIndex, type: .delete, originalRow: originalRow)) deletedRowIndices.insert(rowIndex) - changedRowIndices.insert(rowIndex) } mutating func recordRowInsertion(rowIndex: Int, values: [String?]) { @@ -114,7 +110,6 @@ struct PendingChanges: Equatable { insertedRowData[rowIndex] = values appendChange(RowChange(rowIndex: rowIndex, type: .insert, cellChanges: [])) insertedRowIndices.insert(rowIndex) - changedRowIndices.insert(rowIndex) } // MARK: - Mutate (cancelling pending edits) @@ -123,7 +118,6 @@ struct PendingChanges: Equatable { guard deletedRowIndices.contains(rowIndex) else { return false } removeChange(rowIndex: rowIndex, type: .delete) deletedRowIndices.remove(rowIndex) - changedRowIndices.insert(rowIndex) return true } @@ -135,7 +129,6 @@ struct PendingChanges: Equatable { insertedRowData.removeValue(forKey: rowIndex) shiftRowIndicesDown(at: rowIndex) - changedRowIndices.insert(rowIndex) return true } @@ -159,7 +152,6 @@ struct PendingChanges: Equatable { removeChange(rowIndex: rowIndex, type: .insert) insertedRowIndices.remove(rowIndex) insertedRowData.removeValue(forKey: rowIndex) - changedRowIndices.insert(rowIndex) } let sortedRemoved = validRows.sorted() @@ -188,7 +180,6 @@ struct PendingChanges: Equatable { modifiedCells.removeValue(forKey: rowIndex) appendChange(RowChange(rowIndex: rowIndex, type: .delete, originalRow: originalRow)) deletedRowIndices.insert(rowIndex) - changedRowIndices.insert(rowIndex) } /// Re-apply a cell edit during undo replay (skips undo registration). @@ -213,7 +204,6 @@ struct PendingChanges: Equatable { if let insertIdx = changeIndex[RowChangeKey(rowIndex: rowIndex, type: .insert)] { updateInsertedCell(at: insertIdx, columnIndex: columnIndex, columnName: columnName, newValue: newValue) - changedRowIndices.insert(rowIndex) return } @@ -229,7 +219,6 @@ struct PendingChanges: Equatable { changeIndex[updateKey] = changes.count - 1 modifiedCells[rowIndex, default: []].insert(columnIndex) } - changedRowIndices.insert(rowIndex) } /// Replace an inserted row's cell value during undo replay (no shift, no undo). @@ -241,7 +230,6 @@ struct PendingChanges: Equatable { ) { guard let insertIdx = changeIndex[RowChangeKey(rowIndex: rowIndex, type: .insert)] else { return } updateInsertedCell(at: insertIdx, columnIndex: columnIndex, columnName: columnName, newValue: newValue) - changedRowIndices.insert(rowIndex) } /// Restore a cell's value during undo replay when an existing change matches. @@ -274,7 +262,6 @@ struct PendingChanges: Equatable { newValue: previousValue ) } - changedRowIndices.insert(rowIndex) } /// Insert a synthetic .insert RowChange for undo replay (e.g., after redoing a deletion's undo). @@ -291,7 +278,6 @@ struct PendingChanges: Equatable { if let savedValues { insertedRowData[rowIndex] = savedValues } - changedRowIndices.insert(rowIndex) } /// Insert a batch of rows (for undo replay of a batch deletion's undo). @@ -314,7 +300,6 @@ struct PendingChanges: Equatable { changes.append(RowChange(rowIndex: rowIndex, type: .insert, cellChanges: cellChanges)) insertedRowIndices.insert(rowIndex) insertedRowData[rowIndex] = values - changedRowIndices.insert(rowIndex) } rebuildChangeIndex() } @@ -338,23 +323,14 @@ struct PendingChanges: Equatable { insertedRowIndices.removeAll() modifiedCells.removeAll() insertedRowData.removeAll() - changedRowIndices.removeAll() } - mutating func consumeChangedRowIndices() -> Set { - let indices = changedRowIndices - changedRowIndices.removeAll() - return indices - } - - /// Replace internal state from a serialized snapshot. mutating func restore(from snapshot: TabChangeSnapshot) { changes = snapshot.changes deletedRowIndices = snapshot.deletedRowIndices insertedRowIndices = snapshot.insertedRowIndices modifiedCells = snapshot.modifiedCells insertedRowData = snapshot.insertedRowData - changedRowIndices = [] rebuildChangeIndex() } @@ -471,7 +447,6 @@ struct PendingChanges: Equatable { if changes[updateIdx].cellChanges.isEmpty { removeChangeAt(updateIdx) } - changedRowIndices.insert(rowIndex) return true } @@ -494,7 +469,6 @@ struct PendingChanges: Equatable { } modifiedCells = newModifiedCells - changedRowIndices = Set(changedRowIndices.map { $0 >= insertionPoint ? $0 + 1 : $0 }) rebuildChangeIndex() } diff --git a/TablePro/Core/SchemaTracking/StructureChangeManager.swift b/TablePro/Core/SchemaTracking/StructureChangeManager.swift index ba968bc5e..4a992c33e 100644 --- a/TablePro/Core/SchemaTracking/StructureChangeManager.swift +++ b/TablePro/Core/SchemaTracking/StructureChangeManager.swift @@ -18,9 +18,6 @@ final class StructureChangeManager: ChangeManaging { var hasChanges: Bool { !pendingChanges.isEmpty } var reloadVersion: Int = 0 - // Track which rows changed since last reload for granular updates - private(set) var changedRowIndices: Set = [] - // Current state (loaded from database) private(set) var currentColumns: [EditableColumnDefinition] = [] private(set) var currentIndexes: [EditableIndexDefinition] = [] @@ -48,13 +45,6 @@ final class StructureChangeManager: ChangeManaging { var canUndo: Bool { undoManager.canUndo } var canRedo: Bool { undoManager.canRedo } - /// Consume and clear changed row indices (for granular table reloads) - func consumeChangedRowIndices() -> Set { - let indices = changedRowIndices - changedRowIndices.removeAll() - return indices - } - // MARK: - Load Schema func loadSchema( @@ -260,9 +250,6 @@ final class StructureChangeManager: ChangeManaging { undoManager.setActionName(String(localized: "Delete Column")) pendingChanges[key] = .deleteColumn(column) trackChangeKey(key) - if let rowIndex = workingColumns.firstIndex(where: { $0.id == id }) { - changedRowIndices.insert(rowIndex) - } } else { let rowIndex = workingColumns.firstIndex(where: { $0.id == id }) if let column = workingColumns.first(where: { $0.id == id }) { @@ -271,11 +258,6 @@ final class StructureChangeManager: ChangeManaging { } undoManager.setActionName(String(localized: "Delete Column")) } - if let rowIndex { - for i in rowIndex.. var lastIdentity: DataGridIdentity? - var lastReloadVersion: Int = 0 - var lastReapplyVersion: Int = -1 private(set) var cachedRowCount: Int = 0 private(set) var cachedColumnCount: Int = 0 private(set) var enumOrSetColumns: Set = [] @@ -340,6 +338,7 @@ final class TableViewCoordinator: NSObject, NSTableViewDelegate, NSTableViewData guard row >= 0, row < tableView.numberOfRows else { return } guard tableColumn >= 0, tableColumn < tableView.numberOfColumns else { return } invalidateDisplayCache(forDisplayRow: row, column: column) + rebuildVisualStateCache() tableView.reloadData( forRowIndexes: IndexSet(integer: row), columnIndexes: IndexSet(integer: tableColumn) @@ -359,6 +358,7 @@ final class TableViewCoordinator: NSObject, NSTableViewDelegate, NSTableViewData invalidateDisplayCache(forDisplayRow: position.row, column: position.column) } guard !rowSet.isEmpty, !colSet.isEmpty else { return } + rebuildVisualStateCache() tableView.reloadData(forRowIndexes: rowSet, columnIndexes: colSet) case .rowsInserted(let indices): guard !indices.isEmpty else { return } diff --git a/TablePro/Views/Results/DataGridView.swift b/TablePro/Views/Results/DataGridView.swift index ef07c1329..4b0c877b1 100644 --- a/TablePro/Views/Results/DataGridView.swift +++ b/TablePro/Views/Results/DataGridView.swift @@ -23,7 +23,6 @@ struct RowVisualState { } struct DataGridIdentity: Equatable { - let reloadVersion: Int let schemaVersion: Int let metadataVersion: Int let paginationVersion: Int @@ -35,9 +34,8 @@ struct DataGridIdentity: Equatable { let primaryKeyColumns: [String] let hiddenColumns: Set - init(reloadVersion: Int, schemaVersion: Int, metadataVersion: Int, paginationVersion: Int, + init(schemaVersion: Int, metadataVersion: Int, paginationVersion: Int, rowCount: Int, columnCount: Int, isEditable: Bool, configuration: DataGridConfiguration) { - self.reloadVersion = reloadVersion self.schemaVersion = schemaVersion self.metadataVersion = metadataVersion self.paginationVersion = paginationVersion @@ -233,7 +231,6 @@ struct DataGridView: NSViewRepresentable { let columnCount = latestRows.columns.count let currentIdentity = DataGridIdentity( - reloadVersion: changeManager.reloadVersion, schemaVersion: schemaVersion, metadataVersion: metadataVersion, paginationVersion: paginationVersion, @@ -262,7 +259,6 @@ struct DataGridView: NSViewRepresentable { tableView.usesAlternatingRowBackgroundColors = settings.showAlternateRows } - let versionChanged = coordinator.lastReloadVersion != changeManager.reloadVersion let metadataChanged = previousIdentity.map { $0.metadataVersion != metadataVersion } ?? false let oldRowCount = coordinator.cachedRowCount let oldColumnCount = coordinator.cachedColumnCount @@ -328,7 +324,6 @@ struct DataGridView: NSViewRepresentable { coordinator: coordinator, tableRows: latestRows, needsFullReload: needsFullReload, - versionChanged: versionChanged, metadataChanged: metadataChanged, paginationChanged: paginationChanged ) @@ -502,7 +497,6 @@ struct DataGridView: NSViewRepresentable { coordinator: TableViewCoordinator, tableRows: TableRows, needsFullReload: Bool, - versionChanged: Bool, metadataChanged: Bool = false, paginationChanged: Bool = false ) { @@ -527,21 +521,8 @@ struct DataGridView: NSViewRepresentable { tableView.reloadData(forRowIndexes: visibleRows, columnIndexes: fkColumnIndices) } } - } else if versionChanged { - let changedRows = changeManager.consumeChangedRowIndices() - if changedRows.count > 500 { - tableView.reloadData() - } else if !changedRows.isEmpty { - let rowIndexSet = IndexSet(changedRows) - let columnIndexSet = IndexSet(integersIn: 0.. 0 { tableView.scrollRowToVisible(0) } diff --git a/TablePro/Views/Results/Extensions/DataGridView+CellCommit.swift b/TablePro/Views/Results/Extensions/DataGridView+CellCommit.swift index 7860afda3..559eac04d 100644 --- a/TablePro/Views/Results/Extensions/DataGridView+CellCommit.swift +++ b/TablePro/Views/Results/Extensions/DataGridView+CellCommit.swift @@ -39,15 +39,12 @@ extension TableViewCoordinator { } delegate?.dataGridDidEditCell(row: row, column: columnIndex, newValue: newValue) invalidateDisplayCache() + rebuildVisualStateCache() + let tableColumnIndex = DataGridView.tableColumnIndex(for: columnIndex) if storageRow != nil, case .cellChanged = delta { - let displayDelta: Delta = .cellChanged( - row: row, - column: DataGridView.tableColumnIndex(for: columnIndex) - ) - tableRowsController.apply(displayDelta) + tableRowsController.apply(.cellChanged(row: row, column: tableColumnIndex)) } else { - let tableColumnIndex = DataGridView.tableColumnIndex(for: columnIndex) tableView.reloadData( forRowIndexes: IndexSet(integer: row), columnIndexes: IndexSet(integer: tableColumnIndex) diff --git a/TableProTests/Core/ChangeTracking/AnyChangeManagerTests.swift b/TableProTests/Core/ChangeTracking/AnyChangeManagerTests.swift index 3a221296a..72e8eceb0 100644 --- a/TableProTests/Core/ChangeTracking/AnyChangeManagerTests.swift +++ b/TableProTests/Core/ChangeTracking/AnyChangeManagerTests.swift @@ -92,15 +92,6 @@ struct AnyChangeManagerTests { #expect(wrapper.isRowDeleted(100) == false) } - @Test("StructureChangeManager wrapper: consumeChangedRowIndices returns empty set") - func structureManagerConsumeChangedRowIndicesEmpty() { - let structureManager = StructureChangeManager() - let wrapper = AnyChangeManager(structureManager) - - let indices = wrapper.consumeChangedRowIndices() - #expect(indices.isEmpty) - } - @Test("StructureChangeManager wrapper: hasChanges forwards correctly when false") func structureManagerHasChangesForwardsFalse() { let structureManager = StructureChangeManager() diff --git a/TableProTests/Core/ChangeTracking/DataChangeManagerExtendedTests.swift b/TableProTests/Core/ChangeTracking/DataChangeManagerExtendedTests.swift index 585d1b3a9..6230c9e75 100644 --- a/TableProTests/Core/ChangeTracking/DataChangeManagerExtendedTests.swift +++ b/TableProTests/Core/ChangeTracking/DataChangeManagerExtendedTests.swift @@ -671,21 +671,6 @@ struct DataChangeManagerExtendedTests { #expect(manager.changes.count == 1) } - @Test("changedRowIndices includes all operation types") - func changedRowIndicesIncludesAllOperationTypes() { - let manager = makeManager() - manager.recordCellChange( - rowIndex: 0, columnIndex: 1, columnName: "name", - oldValue: "Alice", newValue: "Bob" - ) - manager.recordRowDeletion(rowIndex: 1, originalRow: ["2", "Charlie", "c@test.com"]) - manager.recordRowInsertion(rowIndex: 2, values: ["x", "y", "z"]) - let changed = manager.consumeChangedRowIndices() - #expect(changed.contains(0)) - #expect(changed.contains(1)) - #expect(changed.contains(2)) - } - @Test("configureForTable with triggerReload false does not increment reloadVersion") func configureForTableNoTriggerReload() { let manager = DataChangeManager() diff --git a/TableProTests/Core/ChangeTracking/DataChangeManagerTests.swift b/TableProTests/Core/ChangeTracking/DataChangeManagerTests.swift index 86b8d3328..aa70a39f4 100644 --- a/TableProTests/Core/ChangeTracking/DataChangeManagerTests.swift +++ b/TableProTests/Core/ChangeTracking/DataChangeManagerTests.swift @@ -237,26 +237,6 @@ struct DataChangeManagerTests { #expect(manager.changes[1].rowIndex == 1) } - @Test("changedRowIndices contains the changed row") - func changedRowIndicesTracksChanges() async { - let manager = DataChangeManager() - manager.configureForTable( - tableName: "users", - columns: ["id", "name"], - primaryKeyColumns: ["id"] - ) - - manager.recordCellChange( - rowIndex: 5, - columnIndex: 1, - columnName: "name", - oldValue: "Alice", - newValue: "Bob" - ) - - #expect(manager.consumeChangedRowIndices().contains(5)) - } - // MARK: - Row Deletion Tests @Test("Record row deletion makes hasChanges true") @@ -338,61 +318,6 @@ struct DataChangeManagerTests { #expect(manager.hasChanges) } - // MARK: - consumeChangedRowIndices Tests - - @Test("consumeChangedRowIndices returns the set of changed indices") - func consumeReturnsChangedIndices() async { - let manager = DataChangeManager() - manager.configureForTable( - tableName: "users", - columns: ["id", "name"], - primaryKeyColumns: ["id"] - ) - - manager.recordCellChange( - rowIndex: 0, - columnIndex: 1, - columnName: "name", - oldValue: "Alice", - newValue: "Bob" - ) - manager.recordCellChange( - rowIndex: 2, - columnIndex: 1, - columnName: "name", - oldValue: "Charlie", - newValue: "Dave" - ) - - let consumed = manager.consumeChangedRowIndices() - - #expect(consumed.contains(0)) - #expect(consumed.contains(2)) - #expect(consumed.count == 2) - } - - @Test("consumeChangedRowIndices clears indices after consuming") - func consumeClearsIndices() async { - let manager = DataChangeManager() - manager.configureForTable( - tableName: "users", - columns: ["id", "name"], - primaryKeyColumns: ["id"] - ) - - manager.recordCellChange( - rowIndex: 0, - columnIndex: 1, - columnName: "name", - oldValue: "Alice", - newValue: "Bob" - ) - - _ = manager.consumeChangedRowIndices() - - #expect(manager.consumeChangedRowIndices().isEmpty) - } - // MARK: - clearChanges Tests @Test("clearChanges removes all changes") diff --git a/TableProTests/Core/ChangeTracking/PendingChangesTests.swift b/TableProTests/Core/ChangeTracking/PendingChangesTests.swift index b1f2cc2a8..ddcfba70f 100644 --- a/TableProTests/Core/ChangeTracking/PendingChangesTests.swift +++ b/TableProTests/Core/ChangeTracking/PendingChangesTests.swift @@ -220,71 +220,6 @@ struct PendingChangesSnapshotTests { } } -@Suite("PendingChanges - changedRowIndices tracking") -struct PendingChangesChangedRowIndicesTests { - @Test("revertUpdateCell records the row as changed") - func revertUpdateCellMarksChanged() { - var pending = PendingChanges() - pending.recordCellChange( - rowIndex: 4, columnIndex: 1, columnName: "name", - oldValue: "A", newValue: "B" - ) - _ = pending.consumeChangedRowIndices() - - pending.revertUpdateCell( - rowIndex: 4, columnIndex: 1, columnName: "name", previousValue: "A" - ) - #expect(pending.consumeChangedRowIndices().contains(4)) - } - - @Test("undoRowDeletion records the row as changed") - func undoRowDeletionMarksChanged() { - var pending = PendingChanges() - pending.recordRowDeletion(rowIndex: 7, originalRow: ["a"]) - _ = pending.consumeChangedRowIndices() - - _ = pending.undoRowDeletion(rowIndex: 7) - #expect(pending.consumeChangedRowIndices().contains(7)) - } - - @Test("undoRowInsertion records the row as changed") - func undoRowInsertionMarksChanged() { - var pending = PendingChanges() - pending.recordRowInsertion(rowIndex: 2, values: ["x"]) - _ = pending.consumeChangedRowIndices() - - _ = pending.undoRowInsertion(rowIndex: 2) - #expect(pending.consumeChangedRowIndices().contains(2)) - } - - @Test("reapplyRowDeletion records the row as changed") - func reapplyRowDeletionMarksChanged() { - var pending = PendingChanges() - _ = pending.consumeChangedRowIndices() - pending.reapplyRowDeletion(rowIndex: 3, originalRow: ["a"]) - #expect(pending.consumeChangedRowIndices().contains(3)) - } - - @Test("reapplyCellChange records the row as changed") - func reapplyCellChangeMarksChanged() { - var pending = PendingChanges() - _ = pending.consumeChangedRowIndices() - pending.reapplyCellChange( - rowIndex: 5, columnIndex: 1, columnName: "name", - originalDBValue: nil, newValue: "X", originalRow: nil - ) - #expect(pending.consumeChangedRowIndices().contains(5)) - } - - @Test("reinsertRow records the row as changed") - func reinsertRowMarksChanged() { - var pending = PendingChanges() - _ = pending.consumeChangedRowIndices() - pending.reinsertRow(rowIndex: 1, columns: ["a"], savedValues: ["v"]) - #expect(pending.consumeChangedRowIndices().contains(1)) - } -} - @Suite("PendingChanges - clear and consume") struct PendingChangesLifecycleTests { @Test("Clear empties all internal state") @@ -303,16 +238,4 @@ struct PendingChangesLifecycleTests { #expect(!pending.isCellModified(rowIndex: 0, columnIndex: 1)) } - @Test("Consuming changedRowIndices empties the set") - func consumeChangedRows() { - var pending = PendingChanges() - pending.recordCellChange( - rowIndex: 3, columnIndex: 1, columnName: "name", - oldValue: "a", newValue: "b" - ) - let first = pending.consumeChangedRowIndices() - #expect(first == [3]) - let second = pending.consumeChangedRowIndices() - #expect(second.isEmpty) - } } diff --git a/TableProTests/Views/Results/DataGridIdentityTests.swift b/TableProTests/Views/Results/DataGridIdentityTests.swift index d57044fdb..3d6da157a 100644 --- a/TableProTests/Views/Results/DataGridIdentityTests.swift +++ b/TableProTests/Views/Results/DataGridIdentityTests.swift @@ -12,7 +12,6 @@ import Testing @Suite("DataGridIdentity") struct DataGridIdentityTests { private func makeIdentity( - reloadVersion: Int = 1, schemaVersion: Int = 2, metadataVersion: Int = 3, paginationVersion: Int = 0, @@ -30,7 +29,6 @@ struct DataGridIdentityTests { config.primaryKeyColumns = primaryKeyColumns config.hiddenColumns = hiddenColumns return DataGridIdentity( - reloadVersion: reloadVersion, schemaVersion: schemaVersion, metadataVersion: metadataVersion, paginationVersion: paginationVersion, @@ -46,11 +44,6 @@ struct DataGridIdentityTests { #expect(makeIdentity() == makeIdentity()) } - @Test("Different reloadVersion produces unequal identities") - func differentReloadVersion() { - #expect(makeIdentity(reloadVersion: 1) != makeIdentity(reloadVersion: 2)) - } - @Test("Different schemaVersion produces unequal identities") func differentSchemaVersion() { #expect(makeIdentity(schemaVersion: 2) != makeIdentity(schemaVersion: 3))