# Undo

We provide an `UndoManager` that helps you manage the undo/redo stack and can be
used during concurrent editing. It also supports cursor position transformation.
In the case of concurrent editing, you will want the undo/redo to be **local**,
meaning it should only undo/redo local operations without affecting other
collaborators' edits.

### Why Local Undo/Redo?

If undo/redo is global, it often does not meet expectations. Consider the
following scenario:

- **User A and User B are collaborating**: They are likely editing different
  parts of the document.
- **User A performs an undo**: If this undoes User B's operations, User A might
  see no changes and think the undo failed, as User B might be editing content
  outside of User A's view.
- **User B's perspective**: User B would find their recent edits deleted without
  knowing how it happened.

## Usage

To create an `UndoManager`, you can specify:

- Which local operations are not recorded.
- The merge range for undo operations.
- The stack depth.

```ts twoslash
import { UndoManager, LoroDoc } from "loro-crdt";
const doc = new LoroDoc();
// ---cut---
const undoManager = new UndoManager(doc, {
    maxUndoSteps: 100, // default 100
    mergeInterval: 1000, // default 1000ms
    excludeOriginPrefixes: ["sys:"], // default []
    onPush: (isUndo, range, event) => {
        return { value: null, cursors: [] };
    },
    onPop: (isUndo, value, counterRange) => {
        return;
    },
});
```

## Limitations

It can only track a single peer. When the peer ID of the document changes, it will
clear the undo stack and the redo stack and track the new peer ID.

## Restoring Selections

When utilizing undo/redo functionalities, it is important for our application to
restore the selection to its position prior to the operation that is being
undone or redone. This task is particularly challenging in a collaborative
setting where remote operations can alter the cursor's position (for instance,
if the cursor needs to revert to the 5th position, but remote operations have
added new characters before this position).

<details>
<summary>Challenges</summary>

- **Local undo/redo**: Local undo and redo delete and recreate elements. If we
  use CRDT-based stable positions, they might lock on to the deleted elements,
  while the user's expectation is for the cursor to return to the newly created
  elements after redo.
- **Example**:
  ```
  A fox jumped
  ```
  - Select "fox" and delete it.
  - Undo.
  - After undo, the three characters "fox" should be recreated, and the cursor
    should select these three characters. However, if we use stable positions,
    it would lock onto the initially deleted characters, and after undo, the
    absolute positions obtained would be `start = 2` and `end = 2`.

</details>

### Solution

We support storing [`cursors`](/docs/tutorial/cursor) for each undo/redo action
within the `UndoManager`. The `UndoManager` will adjust the stored cursors to
reflect changes from remote edits or other undo/redo operations, ensuring they
match the current state of the document.

They need be handled by the `onPush` and `onPop` callbacks.

On `onPush`, you can return a list of `Cursor`s that you want to store. On
`onPop`, you can retrieve the stored cursors and use them to restore the
selection.

Typically, you may need to store selections in an undo/redo item in the
following cases:

- When a new local change is applied, we need to record a new undo item.
  Therefore, we must store the selection **before** the operation to be undone.
  - **Purpose**: Storing the selection **before** is crucial because we may lose
    the selection after applying the operation. If the user selects text and
    deletes it, after undo, the `onPop` can retrieve the state of the selected
    deleted content.
- **First undo operation**: Store the current document's selection for the
  corresponding redo item.
  - **Purpose**: After redo, it can return to the initial selection state.

Internally, we also automatically handle the storage and reset of cursors in the
undo/redo loop state.

```ts twoslash
import { LoroDoc, UndoManager, Cursor } from "loro-crdt";
import { expect } from "expect";
// ---cut---
const doc = new LoroDoc();
let cursors: Cursor[] = [];
let poppedCursors: Cursor[] = [];
const undo = new UndoManager(doc, {
  mergeInterval: 0,
  onPop: (isUndo, value, counterRange) => {
    poppedCursors = value.cursors;
  },
  onPush: () => {
    return { value: null, cursors: cursors };
  },
});

doc.getText("text").insert(0, "hello world");
doc.commit();
cursors = [
  doc.getText("text").getCursor(0)!,
  doc.getText("text").getCursor(5)!,
];
// delete "hello ", the cursors should be transformed
doc.getText("text").delete(0, 6);
doc.commit();
expect(poppedCursors.length).toBe(0);
undo.undo();
expect(poppedCursors.length).toBe(2);
expect(doc.toJSON()).toStrictEqual({
  text: "hello world",
});
// expect the cursors to be transformed back
expect(doc.getCursorPos(poppedCursors[0]).offset).toBe(0);
expect(doc.getCursorPos(poppedCursors[1]).offset).toBe(5);
```

<details>
<summary>Implementation</summary>

[The implementation of undo/redo](https://github.com/loro-dev/loro/pull/361)
follows a model similar to ProseMirror/Quill, which are based on OT (Operational
Transformation) algorithms (so we also implement basic OT primitives
internally).

When implementing undo/redo operations, we need to ensure the following
properties:

- Do not undo remote inserts.
- Redo after undo should return to the original state.
- If there is no concurrent editing, undo should return to the previous
  version's state.

Therefore, we have also added some relevant checks in our internal fuzzing tests
to ensure correctness.

</details>

## Demonstration

import { ReactPlayer } from "../../../components/video";
import Caption from "../../../components/caption";

{" "}

<ReactPlayer
  url="/static/collab-undo.mov"
  width={512}
  style={{ maxWidth: "calc(100vw - 40px)" }}
  height={388}
  muted={true}
  loop={true}
  controls={true}
  playing={true}
/>

<Caption>ProseMirror with Loro binding</Caption>

## Understanding the Undo/Redo Stack

The UndoManager maintains two stacks:

1. **Undo Stack**: Contains operations that can be undone
2. **Redo Stack**: Contains operations that were undone and can be redone

### How the Callbacks Work

The `onPush` and `onPop` callbacks are triggered when these stacks change:

- **onPush(isUndo, range, event)**: Called when a new item is pushed to either stack
  - `isUndo: boolean`: `true` for the undo stack, `false` for the redo stack
  - `range: (number, number)`: The operations' counter range that associated with the undo/redo action
  - Returns: An object that can include `value` (any data you want to store) and `cursors` (cursor positions)

- **onPop(isUndo, value)**: Called when an item is popped from either stack
  - `isUndo`: `true` for the undo stack, `false` for the redo stack
  - `value`: The value you returned from `onPush` when this item was created

### Understanding Action Merging

The `mergeInterval` option in the UndoManager controls how closely spaced operations are grouped:

```ts twoslash no_run
import { UndoManager, LoroDoc } from "loro-crdt";
// ---cut---
declare const doc: LoroDoc;
const undoManager = new UndoManager(doc, {
  mergeInterval: 1000, // 1000ms = 1 second (default)
});
```

**How mergeInterval works:**

- Operations occurring within the specified time interval (in milliseconds) will be merged into a single undo action
- Even though these operations are merged, `onPush` events will still be triggered for each individual operation
- When undoing, all merged operations will be undone as a single unit
- A lower value results in more granular undo steps; a higher value creates fewer, more comprehensive undo steps
- Set to `0` to disable merging entirely (every operation becomes a separate undo step)

### Stack Operations Flow

1. When a local transaction is committed, a new undo item is pushed to the undo stack (triggers `onPush` with `isUndo=true`)
2. When `.undo()` is called:
   - An item is popped from the undo stack (triggers `onPop` with `isUndo=true`)
   - A corresponding item is pushed to the redo stack (triggers `onPush` with `isUndo=false`)
3. When `.redo()` is called:
   - An item is popped from the redo stack (triggers `onPop` with `isUndo=false`)
   - A corresponding item is pushed to the undo stack (triggers `onPush` with `isUndo=true`)

### Manual Grouping with `groupStart` and `groupEnd`

Sometimes you need tighter control over how undo units are formed than `mergeInterval` alone can provide. The `UndoManager.groupStart()` and `UndoManager.groupEnd()` methods let you explicitly wrap a sequence of commits so they become a single undo step.

- Call `groupStart()` before you begin a batch of related edits. Any commits that happen before the matching `groupEnd()` will undo together.
- Calling `groupStart()` while another group is active throws an error and leaves the original group in place, which helps catch accidental nesting.
- Remote updates can influence grouping: a conflicting import may force the current group to end early, while non-conflicting imports are merged without breaking the group. Always finish with `groupEnd()`âif the group already auto-closed, the call becomes a no-op.

```ts twoslash no_run
import { LoroDoc, UndoManager } from "loro-crdt";
// ---cut---
const doc = new LoroDoc();
const text = doc.getText("text");
const undoManager = new UndoManager(doc, {});

undoManager.groupStart();

text.update("hello", undefined);
doc.commit();
text.update("hello world", undefined);
doc.commit();

undoManager.groupEnd();

undoManager.undo(); // Reverts both commits in one step
```

### Example: Text Editing with Undo/Redo

Consider a simple text editor that uses Loro for collaboration. Let's walk through what happens during typical editing operations:

```ts twoslash no_run
import { LoroDoc, UndoManager } from "loro-crdt";
// ---cut---
// Create document and undo manager
const doc = new LoroDoc();
const textField = doc.getText("textField");

// Set up undo manager with callbacks to track changes
const undoManager = new UndoManager(doc, {
  // Store cursor positions and any other state you need
  onPush: (isUndo, range) => {
    if (isUndo) {
      console.log("Action recorded for undo");
    } else {
      console.log("Action recorded for redo");
    }
    // Return whatever data you want associated with this action
    return {
      value: { affectedRange: range },
      cursors: [/* your cursor positions */]
    };
  },

  onPop: (isUndo, storedData) => {
    // Access the data you stored during onPush
    const { value, cursors } = storedData;

    if (isUndo) {
      console.log("Retrieving data for undo");
    } else {
      console.log("Retrieving data for redo");
    }

    // Use the stored cursors to restore selection
    // applyStoredCursors(cursors);
  },

  mergeInterval: 0,
});

// User types "Hello"
textField.insert(0, "Hello");
doc.commit();
// â onPush triggered (isUndo=true) - Adds to undo stack

// User types " World"
textField.insert(5, " World");
doc.commit();
// â onPush triggered (isUndo=true) - Adds to undo stack

// User clicks Undo button
undoManager.undo();
// â onPop triggered (isUndo=true) - Removes " World" from document
// â onPush triggered (isUndo=false) - Adds to redo stack

// Document now contains only "Hello"

// User clicks Redo button
undoManager.redo();
// â onPop triggered (isUndo=false) - Retrieves " World" operation
// â onPush triggered (isUndo=true) - Adds back to undo stack

// Document now contains "Hello World" again
```

When the user clicks Undo, two things happen:
1. The last action is popped from the undo stack (removing " World")
2. That action is pushed to the redo stack so it can be redone later

This approach ensures that local changes can be undone without affecting other users' edits, making it ideal for collaborative editing.

### Cursor Efficiency

The built-in cursor solution is optimized for performance and handles collaborative scenarios efficiently, including situations where peers may
change the document concurrently during undo/redo operations. For complex editors like rich text editors, the cursor implementation provides the
best balance of performance and correctness.