diff --git a/TablePro/Views/Main/Child/MainEditorContentView.swift b/TablePro/Views/Main/Child/MainEditorContentView.swift index 933ce6265..aca0adcbc 100644 --- a/TablePro/Views/Main/Child/MainEditorContentView.swift +++ b/TablePro/Views/Main/Child/MainEditorContentView.swift @@ -18,14 +18,6 @@ private struct SortedRowsCache { let resultVersion: Int } -/// Per-tab row provider cache entry — groups all cache-invalidation keys together -private struct RowProviderCacheEntry { - let provider: InMemoryRowProvider - let resultVersion: Int - let metadataVersion: Int - let sortState: SortState -} - /// Main editor content with tab bar and content switching struct MainEditorContentView: View { // MARK: - Dependencies @@ -69,8 +61,7 @@ struct MainEditorContentView: View { @State private var sortCache: [UUID: SortedRowsCache] = [:] - // Per-tab row provider cache — avoids recreation on every SwiftUI render. - @State private var tabProviderCache: [UUID: RowProviderCacheEntry] = [:] + @State private var providerCache = RowProviderCache() @State private var cachedChangeManager: AnyChangeManager? @State private var erDiagramViewModels: [UUID: ERDiagramViewModel] = [:] @State private var serverDashboardViewModels: [UUID: ServerDashboardViewModel] = [:] @@ -127,7 +118,7 @@ struct MainEditorContentView: View { favoriteDialogQuery = FavoriteDialogQuery(query: query) } .onChange(of: tabManager.tabIds) { _, newIds in - guard !sortCache.isEmpty || !tabProviderCache.isEmpty || !erDiagramViewModels.isEmpty + guard !sortCache.isEmpty || !providerCache.isEmpty || !erDiagramViewModels.isEmpty || !serverDashboardViewModels.isEmpty else { coordinator.cleanupSortCache(openTabIds: Set(newIds)) return @@ -135,18 +126,20 @@ struct MainEditorContentView: View { let openTabIds = Set(newIds) sortCache = sortCache.filter { openTabIds.contains($0.key) } coordinator.cleanupSortCache(openTabIds: openTabIds) - tabProviderCache = tabProviderCache.filter { openTabIds.contains($0.key) } + providerCache.retain(tabIds: openTabIds) erDiagramViewModels = erDiagramViewModels.filter { openTabIds.contains($0.key) } serverDashboardViewModels = serverDashboardViewModels.filter { openTabIds.contains($0.key) } } - .onChange(of: tabManager.selectedTabId) { _, newId in + .onChange(of: tabManager.selectedTabId) { _, _ in updateHasQueryText() - guard let newId, let tab = tabManager.selectedTab else { return } - let cached = tabProviderCache[newId] - if cached?.resultVersion != tab.resultVersion - || cached?.metadataVersion != tab.metadataVersion - { + guard let tab = tabManager.selectedTab else { return } + if providerCache.provider( + for: tab.id, + resultVersion: tab.resultVersion, + metadataVersion: tab.metadataVersion, + sortState: tab.sortState + ) == nil { cacheRowProvider(for: tab) } } @@ -157,7 +150,7 @@ struct MainEditorContentView: View { cacheRowProvider(for: tab) } coordinator.onTeardown = { [self] in - tabProviderCache.removeAll() + providerCache.removeAll() sortCache.removeAll() cachedChangeManager = nil } @@ -570,32 +563,33 @@ struct MainEditorContentView: View { private func rowProvider(for tab: QueryTab) -> InMemoryRowProvider { if tab.rowBuffer.isEvicted { - Task { @MainActor in tabProviderCache.removeValue(forKey: tab.id) } + providerCache.remove(for: tab.id) return makeRowProvider(for: tab) } - if let entry = tabProviderCache[tab.id], - entry.resultVersion == tab.resultVersion, - entry.metadataVersion == tab.metadataVersion, - entry.sortState == tab.sortState - { - return entry.provider + if let cached = providerCache.provider( + for: tab.id, + resultVersion: tab.resultVersion, + metadataVersion: tab.metadataVersion, + sortState: tab.sortState + ) { + return cached } let provider = makeRowProvider(for: tab) - Task { @MainActor in - tabProviderCache[tab.id] = RowProviderCacheEntry( - provider: provider, - resultVersion: tab.resultVersion, - metadataVersion: tab.metadataVersion, - sortState: tab.sortState - ) - } + providerCache.store( + provider, + for: tab.id, + resultVersion: tab.resultVersion, + metadataVersion: tab.metadataVersion, + sortState: tab.sortState + ) return provider } private func cacheRowProvider(for tab: QueryTab) { let provider = makeRowProvider(for: tab) - tabProviderCache[tab.id] = RowProviderCacheEntry( - provider: provider, + providerCache.store( + provider, + for: tab.id, resultVersion: tab.resultVersion, metadataVersion: tab.metadataVersion, sortState: tab.sortState diff --git a/TablePro/Views/Results/DataGridView+RowActions.swift b/TablePro/Views/Results/DataGridView+RowActions.swift index 9dd6dcba9..78a71655b 100644 --- a/TablePro/Views/Results/DataGridView+RowActions.swift +++ b/TablePro/Views/Results/DataGridView+RowActions.swift @@ -74,28 +74,7 @@ extension TableViewCoordinator { @MainActor func setCellValueAtColumn(_ value: String?, at rowIndex: Int, columnIndex: Int) { - guard let tableView = tableView else { return } - guard columnIndex >= 0 && columnIndex < rowProvider.columns.count else { return } - - let columnName = rowProvider.columns[columnIndex] - let oldValue = rowProvider.value(atRow: rowIndex, column: columnIndex) - let originalRow = rowProvider.rowValues(at: rowIndex) ?? [] - - changeManager.recordCellChange( - rowIndex: rowIndex, - columnIndex: columnIndex, - columnName: columnName, - oldValue: oldValue, - newValue: value, - originalRow: originalRow - ) - - rowProvider.updateValue(value, at: rowIndex, columnIndex: columnIndex) - - let tableColumnIndex = columnIndex + 1 - tableView.reloadData( - forRowIndexes: IndexSet(integer: rowIndex), - columnIndexes: IndexSet(integer: tableColumnIndex)) + commitCellEdit(row: rowIndex, columnIndex: columnIndex, newValue: value) } func copyCellValue(at rowIndex: Int, columnIndex: Int) { diff --git a/TablePro/Views/Results/DataGridView+TypePicker.swift b/TablePro/Views/Results/DataGridView+TypePicker.swift index 534713947..5a915ac75 100644 --- a/TablePro/Views/Results/DataGridView+TypePicker.swift +++ b/TablePro/Views/Results/DataGridView+TypePicker.swift @@ -30,13 +30,7 @@ extension TableViewCoordinator { currentValue: currentValue, onCommit: { newValue in guard let self else { return } - self.commitPopoverEdit( - tableView: tableView, - row: row, - column: column, - columnIndex: columnIndex, - newValue: newValue - ) + self.commitPopoverEdit(row: row, columnIndex: columnIndex, newValue: newValue) }, onDismiss: dismiss ) diff --git a/TablePro/Views/Results/Extensions/DataGridView+CellCommit.swift b/TablePro/Views/Results/Extensions/DataGridView+CellCommit.swift new file mode 100644 index 000000000..b833d61f3 --- /dev/null +++ b/TablePro/Views/Results/Extensions/DataGridView+CellCommit.swift @@ -0,0 +1,35 @@ +// +// DataGridView+CellCommit.swift +// TablePro +// + +import AppKit + +extension TableViewCoordinator { + func commitCellEdit(row: Int, columnIndex: Int, newValue: String?) { + guard let tableView else { return } + guard columnIndex >= 0 && columnIndex < rowProvider.columns.count else { return } + + let oldValue = rowProvider.value(atRow: row, column: columnIndex) + guard oldValue != newValue else { return } + + let columnName = rowProvider.columns[columnIndex] + changeManager.recordCellChange( + rowIndex: row, + columnIndex: columnIndex, + columnName: columnName, + oldValue: oldValue, + newValue: newValue, + originalRow: rowProvider.rowValues(at: row) ?? [] + ) + + rowProvider.updateValue(newValue, at: row, columnIndex: columnIndex) + delegate?.dataGridDidEditCell(row: row, column: columnIndex, newValue: newValue) + + let tableColumnIndex = DataGridView.tableColumnIndex(for: columnIndex) + tableView.reloadData( + forRowIndexes: IndexSet(integer: row), + columnIndexes: IndexSet(integer: tableColumnIndex) + ) + } +} diff --git a/TablePro/Views/Results/Extensions/DataGridView+Editing.swift b/TablePro/Views/Results/Extensions/DataGridView+Editing.swift index b5f3d0679..0e21d41f4 100644 --- a/TablePro/Views/Results/Extensions/DataGridView+Editing.swift +++ b/TablePro/Views/Results/Extensions/DataGridView+Editing.swift @@ -78,24 +78,7 @@ extension TableViewCoordinator { } func commitOverlayEdit(row: Int, columnIndex: Int, newValue: String) { - let oldValue = rowProvider.value(atRow: row, column: columnIndex) - guard oldValue != newValue else { return } - - let columnName = rowProvider.columns[columnIndex] - changeManager.recordCellChange( - rowIndex: row, - columnIndex: columnIndex, - columnName: columnName, - oldValue: oldValue, - newValue: newValue, - originalRow: rowProvider.rowValues(at: row) ?? [] - ) - - rowProvider.updateValue(newValue, at: row, columnIndex: columnIndex) - delegate?.dataGridDidEditCell(row: row, column: columnIndex, newValue: newValue) - - let tableColumnIndex = DataGridView.tableColumnIndex(for: columnIndex) - tableView?.reloadData(forRowIndexes: IndexSet(integer: row), columnIndexes: IndexSet(integer: tableColumnIndex)) + commitCellEdit(row: row, columnIndex: columnIndex, newValue: newValue) } func handleOverlayTabNavigation(row: Int, column: Int, forward: Bool) { @@ -159,24 +142,7 @@ extension TableViewCoordinator { let oldValue = rowProvider.value(atRow: row, column: columnIndex) let newValue: String? = rawInput.isEmpty && oldValue == nil ? nil : rawInput - guard oldValue != newValue else { return true } - - let columnName = rowProvider.columns[columnIndex] - changeManager.recordCellChange( - rowIndex: row, - columnIndex: columnIndex, - columnName: columnName, - oldValue: oldValue, - newValue: newValue, - originalRow: rowProvider.rowValues(at: row) ?? [] - ) - - rowProvider.updateValue(newValue, at: row, columnIndex: columnIndex) - delegate?.dataGridDidEditCell(row: row, column: columnIndex, newValue: newValue) - - Task { @MainActor in - tableView.reloadData(forRowIndexes: IndexSet(integer: row), columnIndexes: IndexSet(integer: column)) - } + commitCellEdit(row: row, columnIndex: columnIndex, newValue: newValue) (control as? CellTextField)?.restoreTruncatedDisplay() diff --git a/TablePro/Views/Results/Extensions/DataGridView+Popovers.swift b/TablePro/Views/Results/Extensions/DataGridView+Popovers.swift index dd042164f..daa89d264 100644 --- a/TablePro/Views/Results/Extensions/DataGridView+Popovers.swift +++ b/TablePro/Views/Results/Extensions/DataGridView+Popovers.swift @@ -22,24 +22,8 @@ extension TableViewCoordinator { value: currentValue, columnType: columnType ) { [weak self] newValue in - guard let self = self else { return } - let oldValue = self.rowProvider.value(atRow: row, column: columnIndex) - guard oldValue != newValue else { return } - - let columnName = self.rowProvider.columns[columnIndex] - self.changeManager.recordCellChange( - rowIndex: row, - columnIndex: columnIndex, - columnName: columnName, - oldValue: oldValue, - newValue: newValue, - originalRow: self.rowProvider.rowValues(at: row) ?? [] - ) - - self.rowProvider.updateValue(newValue, at: row, columnIndex: columnIndex) - self.delegate?.dataGridDidEditCell(row: row, column: columnIndex, newValue: newValue) - - tableView.reloadData(forRowIndexes: IndexSet(integer: row), columnIndexes: IndexSet(integer: column)) + guard let self else { return } + self.commitCellEdit(row: row, columnIndex: columnIndex, newValue: newValue) } } @@ -61,13 +45,7 @@ extension TableViewCoordinator { connectionId: connectionId, databaseType: databaseType, onCommit: { newValue in - self?.commitPopoverEdit( - tableView: tableView, - row: row, - column: column, - columnIndex: columnIndex, - newValue: newValue - ) + self?.commitPopoverEdit(row: row, columnIndex: columnIndex, newValue: newValue) }, onDismiss: dismiss ) @@ -129,13 +107,7 @@ extension TableViewCoordinator { initialValue: currentValue, columnName: columnName, onCommit: { newValue in - self?.commitPopoverEdit( - tableView: tableView, - row: row, - column: column, - columnIndex: columnIndex, - newValue: newValue - ) + self?.commitPopoverEdit(row: row, columnIndex: columnIndex, newValue: newValue) }, onDismiss: dismiss, onPopOut: { currentText in @@ -145,13 +117,7 @@ extension TableViewCoordinator { columnName: columnName, isEditable: true, onCommit: { newValue in - self?.commitPopoverEdit( - tableView: tableView, - row: row, - column: column, - columnIndex: columnIndex, - newValue: newValue - ) + self?.commitPopoverEdit(row: row, columnIndex: columnIndex, newValue: newValue) } ) } @@ -173,13 +139,7 @@ extension TableViewCoordinator { HexEditorContentView( initialValue: currentValue, onCommit: { newValue in - self?.commitPopoverEdit( - tableView: tableView, - row: row, - column: column, - columnIndex: columnIndex, - newValue: newValue - ) + self?.commitPopoverEdit(row: row, columnIndex: columnIndex, newValue: newValue) }, onDismiss: dismiss ) @@ -210,7 +170,7 @@ extension TableViewCoordinator { currentValue: currentValue, isNullable: isNullable, onCommit: { newValue in - self?.commitPopoverEdit(tableView: tableView, row: row, column: column, columnIndex: columnIndex, newValue: newValue) + self?.commitPopoverEdit(row: row, columnIndex: columnIndex, newValue: newValue) }, onDismiss: dismiss ) @@ -244,7 +204,7 @@ extension TableViewCoordinator { allowedValues: allowedValues, initialSelections: selections, onCommit: { newValue in - self?.commitPopoverEdit(tableView: tableView, row: row, column: column, columnIndex: columnIndex, newValue: newValue) + self?.commitPopoverEdit(row: row, columnIndex: columnIndex, newValue: newValue) }, onDismiss: dismiss ) @@ -299,44 +259,22 @@ extension TableViewCoordinator { } @objc func dropdownMenuItemSelected(_ sender: NSMenuItem) { - guard let tableView = pendingDropdownTableView else { return } commitPopoverEdit( - tableView: tableView, row: pendingDropdownRow, - column: pendingDropdownColumn + 1, columnIndex: pendingDropdownColumn, newValue: sender.title ) } @objc func dropdownMenuNullSelected(_ sender: NSMenuItem) { - guard let tableView = pendingDropdownTableView else { return } commitPopoverEdit( - tableView: tableView, row: pendingDropdownRow, - column: pendingDropdownColumn + 1, columnIndex: pendingDropdownColumn, newValue: nil ) } - func commitPopoverEdit(tableView: NSTableView, row: Int, column: Int, columnIndex: Int, newValue: String?) { - let oldValue = rowProvider.value(atRow: row, column: columnIndex) - guard oldValue != newValue else { return } - - let columnName = rowProvider.columns[columnIndex] - changeManager.recordCellChange( - rowIndex: row, - columnIndex: columnIndex, - columnName: columnName, - oldValue: oldValue, - newValue: newValue, - originalRow: rowProvider.rowValues(at: row) ?? [] - ) - - rowProvider.updateValue(newValue, at: row, columnIndex: columnIndex) - delegate?.dataGridDidEditCell(row: row, column: columnIndex, newValue: newValue) - - tableView.reloadData(forRowIndexes: IndexSet(integer: row), columnIndexes: IndexSet(integer: column)) + func commitPopoverEdit(row: Int, columnIndex: Int, newValue: String?) { + commitCellEdit(row: row, columnIndex: columnIndex, newValue: newValue) } } diff --git a/TablePro/Views/Results/RowProviderCache.swift b/TablePro/Views/Results/RowProviderCache.swift new file mode 100644 index 000000000..1f8fe7bf0 --- /dev/null +++ b/TablePro/Views/Results/RowProviderCache.swift @@ -0,0 +1,60 @@ +import Foundation + +@MainActor +final class RowProviderCache { + private struct Entry { + let provider: InMemoryRowProvider + let resultVersion: Int + let metadataVersion: Int + let sortState: SortState + } + + private var entries: [UUID: Entry] = [:] + + func provider( + for tabId: UUID, + resultVersion: Int, + metadataVersion: Int, + sortState: SortState + ) -> InMemoryRowProvider? { + guard let entry = entries[tabId], + entry.resultVersion == resultVersion, + entry.metadataVersion == metadataVersion, + entry.sortState == sortState + else { + return nil + } + return entry.provider + } + + func store( + _ provider: InMemoryRowProvider, + for tabId: UUID, + resultVersion: Int, + metadataVersion: Int, + sortState: SortState + ) { + entries[tabId] = Entry( + provider: provider, + resultVersion: resultVersion, + metadataVersion: metadataVersion, + sortState: sortState + ) + } + + func remove(for tabId: UUID) { + entries.removeValue(forKey: tabId) + } + + func retain(tabIds: Set) { + entries = entries.filter { tabIds.contains($0.key) } + } + + func removeAll() { + entries.removeAll() + } + + var isEmpty: Bool { + entries.isEmpty + } +}