Skip to content

feat(text-editor): add SharedTree-based undo/redo with transactions#26338

Open
brrichards wants to merge 9 commits intomicrosoft:mainfrom
brrichards:text-undo-redo
Open

feat(text-editor): add SharedTree-based undo/redo with transactions#26338
brrichards wants to merge 9 commits intomicrosoft:mainfrom
brrichards:text-undo-redo

Conversation

@brrichards
Copy link
Contributor

@brrichards brrichards commented Feb 2, 2026

Summary

  • Adds undo/redo support to the formatted text editor using SharedTree's Revertible API
  • Wraps Quill editor operations in transactions for atomic undo/redo behavior. Uses the Alpha transaction API

Changes

Undo/Redo Implementation (undoRedo.ts)

  • New createUndoRedoStacks() function that listens to commitApplied events
  • Maintains separate undo and redo stacks of Revertible objects
  • Correctly handles CommitKind.Undo vs CommitKind.Default to route revertibles to the right stack
  • Clears redo stack when new edits occur after an undo
  • Properly disposes revertibles on cleanup
  • Undo/redo stack creation is handled at app level

Transaction Support (quillFormattedView.tsx)

  • Wraps all Quill delta operations in Tree.runTransaction() so complex edits (delete + insert + format) undo/redo as a single unit
  • Disabled Quill's built-in history module to avoid conflicts
  • Added undo/redo toolbar buttons with custom handlers. Buttons are disabled if the respective stack is empty

Copilot AI review requested due to automatic review settings February 2, 2026 17:36
Copy link
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

This pull request adds undo/redo functionality to the formatted text editor using SharedTree's Revertible API and wraps Quill operations in transactions for atomic undo/redo behavior.

Changes:

  • New undoRedo.ts module with createUndoRedoStacks() function for managing undo/redo stacks
  • Transaction wrapping in quillFormattedView.tsx using Tree.runTransaction() for atomic operations
  • API updates to pass treeViewEvents to FormattedMainView and expose undo/redo methods via ref handle
  • Test updates to accommodate new API and add basic undo/redo tests

Reviewed changes

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

File Description
examples/data-objects/text-editor/src/formatted/undoRedo.ts New file implementing undo/redo stack management using SharedTree's Revertible API
examples/data-objects/text-editor/src/formatted/quillFormattedView.tsx Wraps Quill delta operations in transactions, disables built-in Quill history, adds undo/redo toolbar handlers, exposes undo/redo via ref handle
examples/data-objects/text-editor/src/test/textEditor.test.tsx Updates all FormattedMainView usages to pass treeViewEvents, adds two new undo/redo tests
examples/data-objects/text-editor/src/app.tsx Updates view component signatures to receive treeView parameter and passes treeViewEvents to FormattedMainView

if (op.attributes) {
root.formatRange(cpPos, cpCount, quillAttrsToPartial(op.attributes));
// Wrap all tree mutations in a transaction so they undo/redo as one atomic unit
TreeAlpha.branch(root)?.runTransaction(() => {
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't think we want to no-op if the node is not part of a branch.

If the node is unhydrated, this code should still be able to work (and if it did require a branch, it should error if there isn't one rather than doing a no-op).

Maybe something like (TreeAlpha.branch(root)?.runTransaction ?? (f) => f()) )(() => {

@noencke: it would be nice if we had an easy way to group edits if a node is part of a branch, but do the edits anyway if not part of a branch. Such an API couldn't support rollback via return value, but might be handy. I think this is a usability gap with the new branch based API.

Copy link
Contributor Author

@brrichards brrichards Feb 6, 2026

Choose a reason for hiding this comment

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

What if we used an if else. So if the node is unhyrdrated we apply the edits directly. Something like:

const branch = TreeAlpha.branch(root);
const applyDelta = (): void => {
    // ... all changes (unchanged) ...
};
if (branch === undefined) {
    applyDelta()
else {
    branch.runTransaction(applyDelta);
}

I saw this being used in tableSchema.ts for unhydrated nodes and looks like it could be used here as well

Copy link
Contributor

@noencke noencke Feb 6, 2026

Choose a reason for hiding this comment

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

I will attempt to address this in my next revision of the APIs. If we have branch() return either a tree view in the hydrated case (as it currently does) or a new "unhydrated tree branch" in the unhydrated case, then we can have runTransaction live on both and you can do TreeAlpha.branch(node).runTransaction regardless of whether node is hydrated or not. In the unhydrated case, the "transaction" will not actually be a transaction, it will just run the edits as normal.

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