Skip to content

Collaborative undo

Undo that reverses your own last action and leaves everyone else's work alone - which is what users expect, and what a naïve "restore the previous state" undo gets badly wrong in a shared document.

Two replicas, two undo stacks

Each replica has its own undo stack, tracking only its own edits.

Opening two replicas…

The thing worth trying. Type a sentence into A and wait for it to appear in B. Then type a different sentence into B. Now press Undo on A. A's sentence disappears from both panels - because undo is an ordinary edit that synchronizes like any other - while B's sentence is untouched, even though it came later. A's undo stack never contained it.

Undo mine, not yours

Undo in a single-user editor means "put the document back how it was". In a collaborative document that would be a disaster: restoring an earlier state would throw away everything your collaborators did in the meantime, and they would watch their work vanish because somebody else pressed a key.

What users actually expect is selective undo - reverse my last action, leave everybody else's alone - and that is what these two replicas implement.

Note also that consecutive keystrokes collapse into one undo step, so undo removes a word or a phrase rather than a single character. A remote edit arriving between two of your keystrokes ends the current step, which is what keeps the boundaries sensible when several people are working at once.

Why this is not a rewind

An undo here does not restore a previous state. It creates new operations that delete what the action inserted and re-insert what it deleted, and those operations travel to peers exactly like a keystroke. That is why the effect appears on the other replica, why it merges rather than clobbering, and why it can itself be undone.

It is also why an undo can survive a disconnection: take a replica offline, undo something, bring it back, and the undo reaches its peer whenever the connection returns.

// One stack per editing surface, scoped to the text it edits.
var undo = new BcrdtUndoManager(document.Text("body"), new BcrdtUndoManagerOptions
{
    // Adds to the default set, which already tracks untagged (null-origin) edits.
    TrackedOrigins = { typeof(BcrdtTextArea) },
});

undo.Undo();   // removes what I did, not what they did
undo.Redo();
An unhandled error has occurred. Reload 🗙