-
Notifications
You must be signed in to change notification settings - Fork 576
Transaction labels for undo redo bug fix #27202
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 3 commits
f2f66ce
b6a805f
8a564f5
ce4a29a
087c584
27d8fc5
da3e141
4593d94
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -134,6 +134,19 @@ function* collectTreeLabels(node: LabelTree): IterableIterator<unknown> { | |
| } | ||
| } | ||
|
|
||
| /** | ||
| * Deep-clones a {@link LabelTree} so the result is independent of the source. | ||
| * Used when capturing the label tree on a revertible so that subsequent mutations | ||
| * (by the framework or by external listeners reading {@link TransactionLabels.tree}) | ||
| * cannot affect the labels the revertible will emit. | ||
| */ | ||
| function cloneLabelTree(tree: LabelTree): LabelTree { | ||
| return { | ||
| label: tree.label, | ||
| sublabels: tree.sublabels.map(cloneLabelTree), | ||
| }; | ||
| } | ||
|
|
||
| /** | ||
| * Builds the labels set for a change event from the label tree. | ||
| * If the tree exists and contains at least one defined label, returns a set of all | ||
|
|
@@ -713,6 +726,10 @@ export class TreeCheckout implements ITreeCheckout { | |
| const kind = event.type === "append" ? event.kind : CommitKind.Default; | ||
| const { change, revision } = commit; | ||
|
|
||
| // Snapshot the label tree for this commit before any listener runs. | ||
| const commitLabelTree = | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we do this inside
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Updated with suggestion
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 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. |
||
| this.labelTreeNode === undefined ? undefined : cloneLabelTree(this.labelTreeNode); | ||
|
|
||
| const getRevertible = hasSchemaChange(change) | ||
| ? undefined | ||
| : (onRevertibleDisposed?: (revertible: RevertibleAlpha) => void) => { | ||
|
|
@@ -731,6 +748,7 @@ export class TreeCheckout implements ITreeCheckout { | |
| kind, | ||
| this, | ||
| onRevertibleDisposed, | ||
| commitLabelTree, | ||
| ); | ||
|
Comment on lines
760
to
766
|
||
| this.revertibleCommitBranches.set( | ||
| revision, | ||
|
|
@@ -1008,13 +1026,16 @@ export class TreeCheckout implements ITreeCheckout { | |
| * @param kind - The {@link CommitKind} that produced this revertible (e.g., Default, Undo, Redo). | ||
| * @param checkout - The {@link TreeCheckout} instance this revertible belongs to. | ||
| * @param onRevertibleDisposed - Callback function that will be called when the revertible is disposed. | ||
| * @param labelTree - The {@link LabelTree} (if any) active when the original commit was produced. | ||
| * The revert commit inherits these labels so that hosts can scope undo/redo by transaction label. | ||
| * @returns A {@link RevertibleAlpha} object. | ||
| */ | ||
| private createRevertible( | ||
| revision: RevisionTag, | ||
| kind: CommitKind, | ||
| checkout: TreeCheckout, | ||
| onRevertibleDisposed: ((revertible: RevertibleAlpha) => void) | undefined, | ||
| labelTree: LabelTree | undefined, | ||
| ): RevertibleAlpha { | ||
| const commitBranches = checkout.revertibleCommitBranches; | ||
|
|
||
|
|
@@ -1030,7 +1051,7 @@ export class TreeCheckout implements ITreeCheckout { | |
| throw new UsageError("Unable to revert a revertible that has been disposed."); | ||
| } | ||
|
|
||
| const revertMetrics = checkout.revertRevertible(revision, kind); | ||
| const revertMetrics = checkout.revertRevertible(revision, kind, labelTree); | ||
| checkout.logger?.sendTelemetryEvent({ | ||
| eventName: TreeCheckout.revertTelemetryEventName, | ||
| ...revertMetrics, | ||
|
|
@@ -1060,7 +1081,13 @@ export class TreeCheckout implements ITreeCheckout { | |
|
|
||
| targetCheckout.revertibleCommitBranches.set(revision, revertibleBranch.fork()); | ||
|
|
||
| return this.createRevertible(revision, kind, targetCheckout, onRevertibleDisposed); | ||
| return this.createRevertible( | ||
| revision, | ||
| kind, | ||
| targetCheckout, | ||
| onRevertibleDisposed, | ||
| labelTree, | ||
| ); | ||
| }, | ||
| dispose: () => { | ||
| if (revertible.status === RevertibleStatus.Disposed) { | ||
|
|
@@ -1321,7 +1348,11 @@ export class TreeCheckout implements ITreeCheckout { | |
| this.revertibles.delete(revertible); | ||
| } | ||
|
|
||
| private revertRevertible(revision: RevisionTag, kind: CommitKind): RevertMetrics { | ||
| private revertRevertible( | ||
| revision: RevisionTag, | ||
| kind: CommitKind, | ||
| labelTree: LabelTree | undefined, | ||
| ): RevertMetrics { | ||
| this.editLock.checkUnlocked("Reverting a commit"); | ||
| if (this.transaction.size > 0) { | ||
| throw new UsageError("Undo is not yet supported during transactions."); | ||
|
|
@@ -1364,12 +1395,23 @@ export class TreeCheckout implements ITreeCheckout { | |
| ); | ||
| } | ||
|
|
||
| this.#transaction.activeBranch.apply( | ||
| change, | ||
| kind === CommitKind.Default || kind === CommitKind.Redo | ||
| ? CommitKind.Undo | ||
| : CommitKind.Redo, | ||
| ); | ||
| // Install the original commit's label tree so the revert commit's metadata inherits | ||
| // the same labels — letting hosts scope undo/redo by label. Reusing the captured tree | ||
| // (rather than wrapping it) ensures revert-of-revert does not introduce new nesting. | ||
| const previousLabelTreeNode = this.labelTreeNode; | ||
| this.labelTreeNode = labelTree; | ||
| try { | ||
| this.#transaction.activeBranch.apply( | ||
| change, | ||
| kind === CommitKind.Default || kind === CommitKind.Redo | ||
| ? CommitKind.Undo | ||
| : CommitKind.Redo, | ||
| ); | ||
| } finally { | ||
| // Restore rather than clear: a revert triggered from inside another labeled | ||
| // transaction's changed event must not wipe that outer commit's label state. | ||
| this.labelTreeNode = previousLabelTreeNode; | ||
| } | ||
|
|
||
| // Derive some stats about the reversion to return to the caller. | ||
| let revertAge = 0; | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.