Skip to content

fix(datagrid): show data after Data/Structure/JSON view-mode toggle#981

Merged
datlechin merged 2 commits intomainfrom
fix/datagrid-empty-after-viewmode-toggle
May 4, 2026
Merged

fix(datagrid): show data after Data/Structure/JSON view-mode toggle#981
datlechin merged 2 commits intomainfrom
fix/datagrid-empty-after-viewmode-toggle

Conversation

@datlechin
Copy link
Copy Markdown
Member

Summary

Fixes a regression where toggling between Data, Structure, and JSON view modes left the data grid empty until the user manually refreshed. The data was always intact in the registry — the NSTableView simply never received reloadData() after recreation.

Root cause

DataGridView.makeNSView was calling coordinator.updateCache() at the end of setup. That call primed cachedRowCount and cachedColumnCount to the registry's current row/column counts before NSTableView had rendered anything. The first updateNSView then computed:

let oldRowCount = coordinator.cachedRowCount        // already 1 (set in makeNSView)
let oldColumnCount = coordinator.cachedColumnCount  // already 17

let structureChanged = oldRowCount != rowDisplayCount || oldColumnCount != columnCount
                     // (1 != 1) || (17 != 17) → false

structureChanged = falsereloadData() skipped → NSTableView's internal row count stuck at 0 forever.

Why first open worked: registry was empty during the first makeNSView (rows=0 cols=0), so the cache stayed at (0, 0). When the query result later filled in 14 columns, oldColumnCount=0 != newColumnCount=14 triggered the reload.

Why structure→data broke: the recreated grid found the registry already populated, so the cache primed at (1, 17), and the comparison saw "no structural change."

Why manual refresh fixed it: refresh calls setActiveTableRowsnotifyFullReplaceIfActiveapplyFullReplace(), which directly calls tableView.reloadData(), bypassing the diff entirely.

The cached counts represent what NSTableView has rendered, not what the provider returns. They must not be primed in makeNSView because the table has not yet rendered anything at that point.

Fix

Removed the priming updateCache() call from DataGridView.makeNSView (one-line removal, comment added explaining the invariant). Cached counts now stay at 0 until the first updateNSView, which correctly detects "fresh view, needs initial reload" and calls reloadData().

Related fix

While auditing the area, found a contract bug in TabSessionRegistry.evict(). Its doc comment said it bumps loadEpoch "so SwiftUI's .task(id:) lazy-load re-fires," but only TabSession.loadEpoch (the reference-type mirror) was bumped. The .task(id:) in MainEditorContentView keys on tabManager.selectedTab?.loadEpoch (the value-type QueryTab in the array), which was untouched.

In practice the bug was dormant — eviction only targets non-selected tabs, so when the user navigates back, selectedTabId changes and the task re-fires for that reason. But the documented mechanism didn't actually work.

Both eviction call sites (MainContentCoordinator.evictInactiveRowData and MainContentCoordinator+TabSwitch.evictInactiveTabs) now also bump tabManager.tabs[i].loadEpoch. Doc updated to describe the actual contract: callers must mirror the epoch to the QueryTab themselves.

Audit

Checked all other NSViewRepresentable views in the project (HighlightedSQLTextView, JSONSyntaxTextView, HexDumpDisplayView, HexInputTextView, FilterValueTextField, StartupCommandsEditor, QuerySplitView) for the same priming-vs-render anti-pattern. None affected — they either keep cache and rendered state in lockstep, or rely on updateNSView for initial sync, or use async dispatch.

Test plan

  • xcodebuild build clean.
  • xcodebuild test -only-testing:TableProTests/TabSessionRegistryTests -only-testing:TableProTests/QueryTabManagerTests passes.
  • Manual: open a table tab → wait for data → click Structure → click Data. Data appears immediately, no manual refresh needed.
  • Manual: same flow with JSON mode (Data → JSON → Data).
  • Manual: data still appears correctly on first open of a table tab (regression check).
  • Manual: refresh (Cmd+R) on a table still works.
  • Manual: edit a cell, save, undo — row state still tracks correctly (regression check on the visual-state cache versioning).

Diagnostic methodology

Diagnosed empirically. After exhaustive code reading couldn't pin down the cause, instrumented the four registry mutation points (setTableRows, removeTableRows, evict, unregister/removeAll), the view-mode flip site, and the NSViewRepresentable lifecycle. The captured trace showed tvRows=0 immediately after makeNSView despite rows=1 cols=17 from the provider — pinpointing the structure-change-detector miss. All diagnostic logs reverted before commit.

Files

4 files: 23 insertions, 4 deletions.

@chatgpt-codex-connector
Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@datlechin datlechin merged commit 2212892 into main May 4, 2026
2 checks passed
@datlechin datlechin deleted the fix/datagrid-empty-after-viewmode-toggle branch May 4, 2026 07:27
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