Skip to content

Transaction labels for undo redo bug fix#27202

Open
daesunp wants to merge 8 commits intomicrosoft:mainfrom
daesunp:undo-redo-label-bug
Open

Transaction labels for undo redo bug fix#27202
daesunp wants to merge 8 commits intomicrosoft:mainfrom
daesunp:undo-redo-label-bug

Conversation

@daesunp
Copy link
Copy Markdown
Contributor

@daesunp daesunp commented Apr 29, 2026

Description

Fixes a bug when you have pass a label into a transaction, once you perform an undo, the resulting revertible no longer has the label.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 29, 2026

Hi! Thank you for opening this PR. Want me to review it?

Based on the diff (439 lines, 4 files), I've queued these reviewers:

  • Correctness — logic errors, race conditions, lifecycle issues
  • Security — vulnerabilities, secret exposure, injection
  • API Compatibility — breaking changes, release tags, type design
  • Performance — algorithmic regressions, memory leaks
  • Testing — coverage gaps, hollow tests

How this works

  • Adjust the reviewer set by ticking/unticking boxes above. Reviewer toggles alone don't trigger anything.

  • Tick Start review below to dispatch the review fleet.

  • After review finishes, tick Start review again to request another run — it auto-resets after each dispatch.

  • This comment updates as new commits land; your reviewer selections are preserved.

  • Start review

@daesunp daesunp marked this pull request as ready for review April 29, 2026 21:50
@daesunp daesunp requested a review from a team as a code owner April 29, 2026 21:50
Copilot AI review requested due to automatic review settings April 29, 2026 21:50
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes undo/redo metadata so that transaction labels (including nested label trees) are preserved when generating revert commits and cloned revertibles.

Changes:

  • Adds test coverage ensuring undo/redo commits inherit and reuse the original transaction label tree.
  • Captures the active label tree when creating revertibles so revert commits can emit matching labels.
  • Threads the captured label tree through revert/clone paths to keep labels consistent across undo/redo.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

File Description
packages/dds/tree/src/test/shared-tree/schematizingTreeView.spec.ts Adds tests asserting label/label-tree behavior for undo, redo, unlabeled edits, cloning, and nested transactions.
packages/dds/tree/src/shared-tree/treeCheckout.ts Persists label-tree context into revertibles and uses it when applying undo/redo so revert commits emit consistent label metadata.

Comment on lines 729 to 737
// Capture the label tree active when the commit was produced so that the
// resulting revert commit can inherit the same transaction labels.
const revertible = this.createRevertible(
revision,
kind,
this,
onRevertibleDisposed,
this.labelTreeNode,
);
Copy link

Copilot AI Apr 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getRevertible currently reads this.labelTreeNode at the time the listener calls getRevertible(). Since labelTreeNode is mutable shared state (and can change during the same changed emit, e.g. if another listener triggers an undo/redo), this can cause the created revertible to capture the wrong label tree (or none). Capture the label tree for the commit into a local const before emitting the event and close over that snapshot when creating the revertible.

Copilot uses AI. Check for mistakes.
Comment on lines +1383 to +1396
// Inherit the original commit's transaction labels on the revert commit, so that hosts using
// labels to scope undo/redo see the revert grouped with the edit it is inverting.
// The same captured tree is reused for revert-of-revert without introducing new nesting.
this.labelTreeNode = labelTree;
try {
this.#transaction.activeBranch.apply(
change,
kind === CommitKind.Default || kind === CommitKind.Redo
? CommitKind.Undo
: CommitKind.Redo,
);
} finally {
this.labelTreeNode = undefined;
}
Copy link

Copilot AI Apr 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The revert path temporarily assigns this.labelTreeNode = labelTree and then unconditionally clears it to undefined. If an undo/redo is triggered from within a changed event for a labeled transaction, this will clear the original commit’s label tree mid-emit and can affect later listeners (and/or revertible creation) that still expect labelTreeNode to reflect the original commit. Save/restore the previous labelTreeNode (and mostRecentlyClosedLabelNode if relevant) instead of always setting undefined in the finally.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated to keep track of previousLabelTreeNode to restore instead of setting to undefined

Comment on lines 731 to 737
const revertible = this.createRevertible(
revision,
kind,
this,
onRevertibleDisposed,
this.labelTreeNode,
);
Copy link

Copilot AI Apr 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

labelTree is passed/stored by reference, and TransactionLabels.tree is exposed to external listeners as a mutable object. With this change, future undo/redo label metadata will depend on whether any listener mutated the previously-emitted label tree. Consider deep-cloning (or otherwise making immutable) the captured LabelTree when storing it on a revertible so undo/redo labels remain stable.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added cloneLabelTree function to pass in deep clone instead of the labelTree directly

Comment thread packages/dds/tree/src/test/shared-tree/schematizingTreeView.spec.ts Outdated
Copy link
Copy Markdown
Contributor

@Josmithr Josmithr left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks great! Thanks for the fix!

Definitely look at Copilot's suggestions before merging though :)

const { change, revision } = commit;

// Snapshot the label tree for this commit before any listener runs.
const commitLabelTree =
Copy link
Copy Markdown
Contributor

@Josmithr Josmithr Apr 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we do this inside getRevertible to avoid the clone cost when getRevertible isn't called?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getRevertible can't be called more than once, so it shouldn't result in any duplicated computation (I think)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated with suggestion

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I ended up moving this back to outside the factory, as I realized it doesn't cover one of the issues copilot brought up if the labelTree was mutated during a changed emit. I added a unit test to cover this scenario as well.

I also realized that if we have a separately started transaction inside the changed event with a label, that label would be nested into the just closed labelTree as we do not clear it before the changed event is emitted. I don't think this is what we want (I think two separate labelTree roots would make more sense). I added a TODO comment to follow up and fix this as well.

Comment thread packages/dds/tree/src/shared-tree/treeCheckout.ts
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.

3 participants