Skip to content
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

Undo #361

Merged
merged 37 commits into from
May 20, 2024
Merged

Undo #361

merged 37 commits into from
May 20, 2024

Conversation

zxch3n
Copy link
Member

@zxch3n zxch3n commented May 14, 2024

Problem Description

What basic properties need to be satisfied after implementing undo/redo? For a single editor, the properties are clear:

  • Undo should revert to the previous states.
  • Redo should revert to the state before the respective undo.

However, in a collaborative environment, this becomes more complex. According to the behaviors of most collaborative applications, undo should be local, meaning users can only undo their actions while retaining others' edits. (If you want to revert to a previous version, including others' edits, we call that revert, which is also supported in this PR).

  • Undo should not delete content inserted by peers (unless the entire container is deleted).
  • After Redo, the edits made by peers should be restored.

We initially considered directly supporting undo/redo operations on CRDTs, but what if a user suddenly undoes a very old operation? Since we use the REG algorithm, constructing CRDTs for undo computation would require backtracking to a very old version, making it difficult to reclaim historical operations in the future: you never know if a peer will suddenly undo a very old operation. Therefore, we do not plan to support undo/redo operations directly.

However, we found that the OT algorithm has already considered how to undo peer operations while retaining local operations. But as we know, OT is centralized, while CRDTs are decentralized. Would this break the properties of CRDTs? Actually, it won't because we treat the "local" copy as the so-called centralized role in OT, using it only to compute the operations for undo and redo. We don't use OT to solve the conflicts of concurrent edits.

Brief Introduction to the Undo/Redo Algorithm on OT

Our final solution is similar to how we build undo with OT. The basic principles are as follows:

Define an apply(doc: Doc, e: Diff) → Doc function, which applies the changes of e to doc.

Define a compose(a: Diff, b: Diff) → Diff function, so that apply(doc, compose(a, b)) equals apply(apply(doc, a), b).

Define an inverse(a: Diff) → Diff function, which satisfies doc = apply(apply(doc, diff), inverse(diff)).

Define a transform(a: Diff, b: Diff) → Diff function, which has the following property:

apply(apply(doc, b), transform(a, b)) equals apply(apply(doc, a), transform(b, a)), so it can be used in OT to handle concurrent edits: for example, if a and b are two concurrent operations, the transform function can calculate what new operations should be applied to both documents to restore consistency.

image

How to implement Undo Redo using the above primitives?

  • For each local operation diff, add a record inverse(diff) to the undo stack.
  • For each remote merge, let the change to the current document be r. Traverse the undo stack from top to bottom, let the current undo stack's diff be d_i:
    • d_i := transform(d_i, r)
    • r := transform(r, d_i)

The specific principle is shown in the figure below.

image

Limitations

  • Assuming a doc has the version vector = { A: k, …rest }. Undoing operations m..k made by A does not guarantee the result equals the doc with the version vector of { A: m, …rest }.
    • On one hand, the target version might be an illegal version on CRDT (not conforming to a causal order, e.g., some deletions occur before creation events).
    • On the other hand, since we use an OT-like algorithm if the text that rest depends on is deleted between m..k, it is impossible to ideally revert to the previous version of the peer when undoing m..k operations. This issue also exists in OT-based applications. (If needed, you can use our time travel and revert capabilities).
      • In Google Docs, the merge result of the example below is direct “.
      • In Notion, each paragraph is Last Write Win, so the merge result is “”, and after undo, it becomes “Hello”, x is not retained.
      • Apple Note can revert to “Hexllo”.
      • Microsoft Word will become “Hellox”.
Reproduce.the.issue.in.Microsoft.Word.mp4
  • (Improvable) Currently, it can only undo the operations of the current peer in the document, not bound to multiple peers simultaneously.
  • (Fixable) In the current version, undoing a list move is parsed as delete + insert.

Invalid Experimental Ideas

I previously proposed an idea on Twitter to implement Undo/Redo through a DiffCalculator https://x.com/zx_loro/status/1773900851108798795,

However, this idea does not work because although the algorithm can achieve undo, it fails when performing multiple consecutive undos followed by redos. This is because, under this method, recreating the deleted content causes subsequent undo operations to fail at the intended positions. For example, in "Hello world!", if we first highlight "Hello", then delete the entire text, and then perform two undos, the first undo restores "Hello world!", but the second undo needs to highlight "Hello". However, it is bound to the old "Hello" and not the newly created "Hello", thus failing to complete the operation.

@zxch3n zxch3n marked this pull request as draft May 14, 2024 14:23
@zxch3n zxch3n marked this pull request as ready for review May 20, 2024 07:54
@zxch3n zxch3n requested a review from Leeeon233 May 20, 2024 07:54
crates/loro-internal/src/handler.rs Outdated Show resolved Hide resolved
crates/loro-internal/src/loro.rs Outdated Show resolved Hide resolved
@zxch3n zxch3n requested a review from Leeeon233 May 20, 2024 12:54
@zxch3n zxch3n merged commit 321e0e7 into main May 20, 2024
1 check passed
@zxch3n zxch3n deleted the zxch3n/loro-560-undoredo branch May 20, 2024 22:14
@github-actions github-actions bot mentioned this pull request May 20, 2024
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.

None yet

2 participants