fix(grid): shift selection indices on step insert/remove - #146
Merged
Conversation
The grid caches the current selection as a list of step indices. When a step was inserted or removed at or before a selected step, the control's own selection model shifted to keep the same items selected, but the cached indices were left untouched and went stale. Consumers that read the cache (copy, cut, delete, paste insert position) then acted on the wrong steps. Maintain the cache in the shared surface instead of relying on a control signal. OnMutation already runs on every mutation and carries the signal payload, so shift the cached indices arithmetically there: insert shifts survivors up, remove drops the removed set and shifts survivors down, and a full rebuild clears the selection. This holds for both the canonical DataGrid, which exposes no index-shift signal, and the transposed list, and it keeps the invariant even for the orientation-flipped surface whose view is detached. The selection is snapshotted at the top of OnMutation and re-applied after the collection mutates, guarded by a sequence-equality check, so a mid-mutation push from the transposed list does not double-shift. The drop-only clamp it replaces is removed.
mrcsin
marked this pull request as ready for review
July 22, 2026 14:15
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.
Problem
The grid caches the current selection as a list of step indices
(
SelectedStepIndices). When a step was inserted or removed at or before aselected step, the control's own selection model shifted to keep the same
items selected, but the cached indices were left stale. Consumers that read
the cache — copy, cut, delete, and the paste insert position — then operated
on the wrong steps.
The bug is a correctness/data-integrity defect. It is largely latent in the
current single-window UI, because every consumer command reselects after
mutating and undo/redo clears the selection, so no clickable gesture leaves a
stale cache for a later read. It becomes reachable the moment an "insert
above" command, a second window on the same recipe, or a consumer without a
follow-up reselect is added. The fix makes the invariant hold unconditionally.
What changed
RecipeGridSurfaceBase.OnMutationnow maintains the cache itself instead ofrelying on a control-level signal:
ShiftSelection(selection, signal)shifts the cached indices by themutation signal — insert shifts survivors up, remove drops the removed set
and shifts survivors down, a full rebuild clears the selection.
OnMutationand re-applied afterthe collection mutates, guarded by a sequence-equality check, so a
mid-mutation push from the transposed list cannot double-shift.
(including
StepActionChanged, whose item reference is swapped in place)deliberately leave the cache untouched, so the existing reselect path governs
them.
ReconcileSelectionWithItemsclamp is removed.This works for both grids, including the canonical
DataGrid, which exposes noindex-shift signal, and for the orientation-flipped surface whose view is
detached.
How it was verified
SelectionShifttests across both grids: insert/remove before,after, and at the selection; multi-select block shifts; multi-insert
+countand the multi-remove survivor offset;
RecipeReplacedclears;ChangeStepActionnon-clobber; and a consumer end-to-end test that deletes through the shifted
cache and asserts the originally selected step is the one removed, by content.
selection indices after a remove that pushes mid-mutation.
dotnet format --verify-no-changesclean; analyser clean on the changed file.No clickable manual repro exists in the shipped single-window UI (see Problem);
the automated tests are the evidence.
Per-task commits before collapse
🤖 Generated with Claude Code