|
| 1 | +# Undo and Redo |
| 2 | + |
| 3 | +Add undo/redo to your buffers using UndoManager or OperationLog. |
| 4 | + |
| 5 | +## Overview |
| 6 | + |
| 7 | +TextBuffer offers two undo strategies. Choose based on whether you need AppKit `UndoManager` integration |
| 8 | +or a portable, `Sendable` undo history. |
| 9 | + |
| 10 | +## UndoManager-Based: Undoable |
| 11 | + |
| 12 | +``Undoable`` is a decorator that wraps any ``Buffer`` and registers inverse actions with Foundation's |
| 13 | +`UndoManager`. It integrates directly with AppKit's Edit menu and the responder chain. |
| 14 | + |
| 15 | +```swift |
| 16 | +let buffer = MutableStringBuffer("Hello") |
| 17 | +let undoable = Undoable(buffer) |
| 18 | + |
| 19 | +undoable.undoGrouping(actionName: "Greet") { |
| 20 | + try! undoable.delete(in: undoable.range) |
| 21 | + try! undoable.insert("Hi, World!") |
| 22 | +} |
| 23 | +print(buffer.content) // "Hi, World!" |
| 24 | + |
| 25 | +undoable.undo() |
| 26 | +print(buffer.content) // "Hello" |
| 27 | + |
| 28 | +undoable.redo() |
| 29 | +print(buffer.content) // "Hi, World!" |
| 30 | +``` |
| 31 | + |
| 32 | +> Warning: You must keep the ``Undoable`` instance alive for undo to work. |
| 33 | +> It removes all registered undo actions from its ``Undoable/undoManager`` on deinitialization |
| 34 | +> to avoid crashes from dangling `unowned` references. |
| 35 | +
|
| 36 | +## OperationLog-Based: TransferableUndoable and SendableRopeBuffer |
| 37 | + |
| 38 | +``OperationLog`` records each mutation as a ``BufferOperation`` value. Undo and redo work by |
| 39 | +replaying operations in reverse or forward order. This makes the history inspectable, |
| 40 | +serializable, and transferable. |
| 41 | + |
| 42 | +### TransferableUndoable |
| 43 | + |
| 44 | +``TransferableUndoable`` is a decorator like ``Undoable``, but backed by ``OperationLog`` |
| 45 | +instead of `UndoManager`. It supports snapshotting and state transfer: |
| 46 | + |
| 47 | +```swift |
| 48 | +let base = RopeBuffer("Document text") |
| 49 | +let buffer = TransferableUndoable(base) |
| 50 | + |
| 51 | +buffer.undoGrouping(actionName: "Edit") { |
| 52 | + try! buffer.replace(range: NSRange(location: 0, length: 8), with: "New") |
| 53 | +} |
| 54 | + |
| 55 | +// Snapshot for transfer across actors |
| 56 | +let snapshot = buffer.sendableSnapshot() |
| 57 | + |
| 58 | +// Restore from snapshot |
| 59 | +buffer.represent(snapshot) |
| 60 | + |
| 61 | +// Bridge to system undo for AppKit menus |
| 62 | +let undoManager = buffer.enableSystemUndoIntegration() |
| 63 | +myWindow.undoManager = undoManager |
| 64 | +``` |
| 65 | + |
| 66 | +### SendableRopeBuffer |
| 67 | + |
| 68 | +``SendableRopeBuffer`` is a `Sendable` value type with ``OperationLog`` built in. |
| 69 | +No decorator needed — undo/redo is part of the buffer itself: |
| 70 | + |
| 71 | +```swift |
| 72 | +var buffer = SendableRopeBuffer("Hello") |
| 73 | +try buffer.insert(", World", at: 5) |
| 74 | + |
| 75 | +buffer.undoGrouping(actionName: "Replace") { buf in |
| 76 | + try! buf.delete(in: buf.range) |
| 77 | + try! buf.insert("Goodbye") |
| 78 | +} |
| 79 | +print(buffer.content) // "Goodbye" |
| 80 | + |
| 81 | +buffer.undo() |
| 82 | +print(buffer.content) // "Hello, World" |
| 83 | + |
| 84 | +buffer.redo() |
| 85 | +print(buffer.content) // "Goodbye" |
| 86 | +``` |
| 87 | + |
| 88 | +## Choosing an Undo Strategy |
| 89 | + |
| 90 | +| Strategy | Type | Sendable | AppKit Integration | Snapshots | |
| 91 | +|----------|------|----------|--------------------|-----------| |
| 92 | +| `UndoManager` | ``Undoable`` | no | built-in | no | |
| 93 | +| `OperationLog` | ``TransferableUndoable`` | no (but can snapshot) | via ``TransferableUndoable/enableSystemUndoIntegration()`` | yes | |
| 94 | +| `OperationLog` | ``SendableRopeBuffer`` | yes | no | is the snapshot | |
| 95 | + |
| 96 | +Use ``Undoable`` when you already have an `UndoManager` (e.g., document-based apps) and want |
| 97 | +zero-configuration AppKit integration. |
| 98 | + |
| 99 | +Use ``TransferableUndoable`` when you need to snapshot buffer state, transfer it across actors, |
| 100 | +or want an inspectable operation history while still wrapping a reference-type buffer. |
| 101 | + |
| 102 | +Use ``SendableRopeBuffer`` when you need a fully self-contained, `Sendable` buffer with undo — |
| 103 | +for example, in background processing or when the buffer itself crosses isolation boundaries. |
0 commit comments