Skip to content

refactor(coordinator): RowOperations migrates to selectedTabAndIndex + fix delete-row visual regression#940

Merged
datlechin merged 2 commits intomainfrom
refactor/coordinator-rowoperations-helpers
Apr 29, 2026
Merged

refactor(coordinator): RowOperations migrates to selectedTabAndIndex + fix delete-row visual regression#940
datlechin merged 2 commits intomainfrom
refactor/coordinator-rowoperations-helpers

Conversation

@datlechin
Copy link
Copy Markdown
Member

Summary

Two related changes in the same area:

1. RowOperations migration (continuation of PR #939)

Migrates MainContentCoordinator+RowOperations to use selectedTabAndIndex (added in #939) and selectedTab:

  • Write-back paths (need the index to mutate tabManager.tabs[idx]): addNewRow, deleteSelectedRows, duplicateSelectedRow, handleUndoResult, pasteRows, updateCellInTab use selectedTabAndIndex.
  • Read-only paths: copySelectedRowsToClipboard, copySelectedRowsWithHeaders, copySelectedRowsAsJson use selectedTab (drops the unused index entirely — also tightens safety since they previously did unchecked tabs[index] after just an optional check).
  • undoInsertRow only needs the tabId → uses selectedTab?.id.

Behavior unchanged. The migration also adds a missing tab.tableContext.tableName != nil guard to addNewRow and duplicateSelectedRow (was previously a separate guard line; now folded into the main guard).

2. Bug fix: delete row didn't show red background until user clicked elsewhere

Discovered while smoke-testing the migration. Pressing Delete on an existing (database-backed) row updated the change tracker correctly but the row's red background didn't appear until the user clicked a different row.

Root cause: PR #938 (Phase D-b) removed the versionChanged branch in reloadAndSyncSelection. That branch used to reload visible rows on any changeManager.reloadVersion bump — that's how the visual state cache changes were propagated to NSTableView's cell views. After D-b, invalidateCachesForUndoRedo() cleared the displayCache and rebuilt the rowVisualStateCache correctly, but never told NSTableView to re-query, so visible cell views kept rendering the pre-delete state.

Fix: invalidateCachesForUndoRedo now also calls tableView.reloadData(forRowIndexes: visibleRows, columnIndexes: allCols) — the call the dropped versionChanged branch used to do implicitly. Single-place change in DataGridCoordinator. Same fix covers undo/redo of cell edits, deletes, and inserts (all go through this path).

Test plan

  • Build clean
  • Targeted tests pass (TableRowsMutationTests, SortCacheInvalidationTests, EvictionTests, QueryTabManagerSelectedTabAndIndexTests)
  • Manual: Cmd+Shift+N add row → focuses new row, undo removes
  • Manual: Cmd+Backspace delete row → red background appears immediately ← regression fix
  • Manual: Cmd+D duplicate row → focuses dup, undo removes
  • Manual: Cmd+C / Cmd+V copy + paste rows → inserts new rows
  • Manual: copy with headers (right-click menu) — TSV with header line
  • Manual: copy as JSON — JSON in clipboard
  • Manual: cell edit → yellow modified marker → undo clears marker

Stat

+28 / −41 LOC across two files.

… fix invalidateCachesForUndoRedo regression

Two changes:

1. RowOperations migration. Six methods (addNewRow, deleteSelectedRows, duplicateSelectedRow, handleUndoResult, pasteRows, updateCellInTab) used the selectedTabIndex + bounds-check + tabs[index] pattern; now use selectedTabAndIndex which returns (tab, index) atomically. Three read-only methods (copySelectedRowsToClipboard, copySelectedRowsWithHeaders, copySelectedRowsAsJson) drop the index entirely and use selectedTab. undoInsertRow only needs the tabId so it uses selectedTab?.id. Behavior unchanged.

2. Bug fix discovered during RowOperations smoke test: pressing Delete on an existing row didn't show the red background until the user clicked another row. Phase D-b (PR #938) dropped the versionChanged branch in reloadAndSyncSelection, which used to reload visible rows on any changeManager.reloadVersion bump. invalidateCachesForUndoRedo cleared the displayCache and rebuilt the visualStateCache but never told NSTableView to re-query, so visible cell views kept rendering the pre-change state. Adding a reloadData(forRowIndexes: visibleRows, columnIndexes: allCols) call after the cache invalidation restores the lost behavior. Same root cause covers undo/redo of cell edits, deletes, and inserts.
…y/undo paths

Two follow-ups to the review:

1. Read-only copy paths (copySelectedRowsToClipboard, copySelectedRowsWithHeaders, copySelectedRowsAsJson) and undoInsertRow now use selectedTabAndIndex instead of selectedTab. selectedTab has a fallback to tabs.first when selectedTabId is nil — the old selectedTabIndex pattern did not. Switching to selectedTabAndIndex (with _ for unused index) restores strict equivalence with the pre-refactor behavior. Closes the subtle init-transient divergence the review flagged.

2. New RowOperationsDispatchTests covers the dispatch wiring restored by this PR's bug fix:
   - softDeleteDispatchesInvalidate: deleteSelectedRows on existing rows fires invalidateCachesForUndoRedo (the path that triggers the visible-rows reload). Without this, a future refactor could silently re-drop the reload (as Phase D-b accidentally did).
   - physicalDeleteDispatchesDelta: deleting an inserted row fires applyDelta and bypasses invalidate — guards the branching in deleteSelectedRows so the two paths don't get tangled.
@datlechin datlechin merged commit cf97b96 into main Apr 29, 2026
2 checks passed
@datlechin datlechin deleted the refactor/coordinator-rowoperations-helpers branch April 29, 2026 05:58
datlechin added a commit that referenced this pull request Apr 29, 2026
…across all extensions (#941)

* refactor(coordinator): sweep selectedTabIndex + bounds-check pattern across all extensions

Migrates 12 files / 30+ call sites of the duplicated 'guard let tabIndex = tabManager.selectedTabIndex, tabIndex < tabManager.tabs.count' pattern to selectedTabAndIndex (write-back paths) or selectedTab (read-only paths). Both helpers were introduced in PR #939 and #940; this PR completes the migration outside Pagination and RowOperations.

Files touched:
- MainContentCoordinator.swift: runQuery, executeTableTabQueryDirectly, loadQueryIntoEditor, insertQueryFromAI, runExplainQuery, executeQueryInternal, handleSort, EXPLAIN error fallback
- MainContentCoordinator+ClickHouse.swift: runVariantExplain
- MainContentCoordinator+ColumnVisibility.swift: saveColumnVisibilityToTab
- MainContentCoordinator+Discard.swift: handleDiscard (two sites)
- MainContentCoordinator+ExecuteAll.swift: runAllStatements
- MainContentCoordinator+Favorites.swift: insertFavorite, runFavoriteInNewTab
- MainContentCoordinator+Filtering.swift: applyFilters, clearFiltersAndReload, restoreFiltersForTable
- MainContentCoordinator+FKNavigation.swift: navigateToFKReference (four sites)
- MainContentCoordinator+LoadMore.swift: loadMoreRows, fetchAllRows
- MainContentCoordinator+Navigation.swift: openTableTab (four sites), promotePreviewTab
- MainContentCoordinator+QueryParameters.swift: executeQueryWithParameters, executeQueryInternalParameterized, executeMultipleStatementsWithParameters
- MainContentCoordinator+Refresh.swift: handleRefresh (three sites)
- MainContentCommandActions.swift: formatQuery, toggleResults, previousResultTab, nextResultTab

Behavior unchanged. Each migration preserves the original tab-not-selected and out-of-bounds short-circuits via selectedTabAndIndex's atomic check. Read-only paths use selectedTab (no fallback divergence — these don't use the index, so the strict-bounds requirement is moot).

The dispatchParameterizedStatements / dispatchStatements helpers (called via tabIndex parameter, not selectedTabIndex) were left alone — they take the index from the caller's already-validated lookup. ExecuteAll's tabIndex parameter pattern preserved.

Smoke-tested across query execution, EXPLAIN, table open from sidebar, FK navigation, filter apply/clear, refresh, discard dialog, pagination, format query, toggle results, insert favorite. Targeted tests pass.

* refactor(coordinator): standardize selectedTabAndIndex destructuring as (tab, tabIndex)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant