fix(datagrid): keep the cell selection Select All builds, and take a mount's observers off with it - #2681
Merged
Conversation
…mount's observers off with it Claude-Session: https://claude.ai/code/session_01STT2h6Y53XJah8xPXAH42L
This was referenced Sep 8, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Two verified defects found by the collateral hunt during #2667, both in the data grid's mount and selection lifecycle. Independent of each other and of the selection fix in #2679; grouped because they are the same file and the same review.
Select All destroyed the cell selection it had just built
KeyHandlingTableView.selectAll(_:)builds the full-grid cell rectangle and then selects every row. The row write was a bareselectRowIndexes, so it read back as a gesture, andtableViewSelectionDidChangeanswers a gesture arriving over a live cell selection by clearing it. The rectangle was gone before Cmd+A returned.What the user saw: Cmd+C took the whole-row path instead of the cell path, Escape had nothing to cancel and fell through to
super, and the header's column indicators went dark.The fix is the helper already in the same file, three functions away:
selectRowsIntersectingSelection()does the identical build-then-write and wraps its write inwithProgrammaticRowSelection. Cmd+A now does the same.publishRowSelectionstill runs unconditionally, so the row set still reaches the tab.Reordering the two writes would also dodge the guard today, but only because
GridSelectionController.selectAllhappens not to touchselectedRowIndexes. Marking the write says what is meant and matches every other correct call site.A grid mount left its observers registered
makeNSViewregisters threeNotificationCenterblock observers throughattachScrollObservers(live-scroll start and end, and the clip view'sboundsDidChange), and the coordinator'sinitregisters a fourth for accessibility activation.dismantleNSViewremoved neither.releaseData()does remove them, but it only runs on session teardown, anddismantleNSViewruns on every editor-tab switch and every result-mode toggle.NotificationCenterretains a block observer's closure and a coordinator is built fresh per mount, so each mount left four registrations that are never fired again and never reclaimed. Switching among ten tabs fifty times leaves about 2,000.dismantleNSViewnow callsdetachScrollObservers()and a newdetachAccessibilityActivationObserver(), extracted fromreleaseData()so both paths share it. Both are idempotent, so a laterreleaseData()on the same coordinator is still fine.This is a registration leak, not a coordinator leak: every one of those closures captures
selfweakly, so the coordinator itself was always free to deallocate. Nodeinitwas added, becauseTableViewCoordinatoris@MainActorand anonisolated deinitcannot call these methods;dismantleNSViewis the hook that matches the mount.Tests
SelectAllCellSelectionTests(new) andDataGridMountTeardownTests(new), plus the existing selection and gutter suites: 59 cases, all passing.The Select All suite carries a guard case of its own,
harnessPresentsDataColumns.selectAllfalls through to AppKit's own implementation when the grid presents no data columns, and that path selects every row too, so a harness that never reconciled the column pool would have passed the row assertion while never exercising the cell selection at all. That is not hypothetical: the first draft of this suite did exactly that, and the guard is what caught it.Confirmed as a real regression guard: with the
withProgrammaticRowSelectionwrapper reverted,selectAllKeepsTheCellSelectionandescapeAfterSelectAllClearsTheCellSelectionboth fail; with it, all five pass.Verification
buildPASStestPASS, 59 cases overSelectAllCellSelectionTests,DataGridMountTeardownTests,GridSelectionControllerTests,DataGridSelectionTests,PublishedRowSelectionTests,KeyHandlingTableViewCopyTests,DataGridRowGutterTests,TableViewCoordinatorRowCountCacheTests,FocusedColumnResolutionTestsswiftlint --strictclean for this change. Twolegacy_swiftui_aspect_ratioerrors remain inImportFromAppSourcePicker.swiftandSupportView.swift; both are pre-existing onmainand untouched here.Noted, not fixed
focusCell(row:column:)has the same unmarked-write shape and can drop a multi-cell range while tabbing inside it.applyFindMatchandbeginEditingalso write unmarked, but there it is arguably intended: jumping to a match or starting an edit plausibly should replace a stale range. Left alone rather than folded in, since neither is this defect.https://claude.ai/code/session_01STT2h6Y53XJah8xPXAH42L