| 🛟 Updated. 28d5985 k33g 16h ago | 1 | package buffer |
| 2 | |
| 3 | // maxUndoDepth caps the history so that a long editing session cannot grow the |
| 4 | // process without bound. Turbo C offered a handful of undo levels; a thousand |
| 5 | // is generous while still being a hard ceiling. |
| 6 | const maxUndoDepth = 1000 |
| 7 | |
| 8 | // edit is one reversible change: the span that was replaced, the text that was |
| 9 | // there before, the text that took its place, and where the cursor sat on |
| 10 | // either side. |
| 11 | // |
| 12 | // Storing both sides makes undo and redo the same operation with the two texts |
| 13 | // swapped, which is why there is only one kind of history entry. |
| 14 | type edit struct { |
| 15 | rng Range |
| 16 | old [][]rune |
| 17 | new [][]rune |
| 18 | cursorBefore Position |
| 19 | cursorAfter Position |
| 20 | } |
| 21 | |
| 22 | // isInsertion reports whether the edit only added text. |
| 23 | func (e edit) isInsertion() bool { return isEmptyText(e.old) } |
| 24 | |
| 25 | // isDeletion reports whether the edit only removed text. |
| 26 | func (e edit) isDeletion() bool { return isEmptyText(e.new) } |
| 27 | |
| 28 | // CanUndo reports whether there is anything to undo. |
| 29 | func (b *Buffer) CanUndo() bool { return len(b.undoStack) > 0 } |
| 30 | |
| 31 | // CanRedo reports whether there is anything to redo. |
| 32 | func (b *Buffer) CanRedo() bool { return len(b.redoStack) > 0 } |
| 33 | |
| 34 | // Undo reverts the most recent change and reports whether it did anything. |
| 35 | // The cursor goes back where it was before that change. |
| 36 | // |
| 37 | // b := buffer.NewFromString("hello") |
| 38 | // b.MoveBufferEnd() |
| 39 | // b.Insert(" world") |
| 40 | // b.Undo() |
| 41 | // fmt.Println(b.Text()) // hello |
| 42 | func (b *Buffer) Undo() bool { |
| 43 | if len(b.undoStack) == 0 { |
| 44 | return false |
| 45 | } |
| 46 | |
| 47 | change := b.undoStack[len(b.undoStack)-1] |
| 48 | b.undoStack = b.undoStack[:len(b.undoStack)-1] |
| 49 | |
| 50 | b.splice(Range{Start: change.rng.Start, End: endOf(change.rng.Start, change.new)}, change.old) |
| 51 | b.cursor = b.clamp(change.cursorBefore) |
| 52 | |
| 53 | b.redoStack = append(b.redoStack, change) |
| 54 | b.afterHistoryMove() |
| 55 | return true |
| 56 | } |
| 57 | |
| 58 | // Redo re-applies the change most recently undone and reports whether it did |
| 59 | // anything. Any new edit clears the redo stack, as it does in every editor. |
| 60 | func (b *Buffer) Redo() bool { |
| 61 | if len(b.redoStack) == 0 { |
| 62 | return false |
| 63 | } |
| 64 | |
| 65 | change := b.redoStack[len(b.redoStack)-1] |
| 66 | b.redoStack = b.redoStack[:len(b.redoStack)-1] |
| 67 | |
| 68 | b.splice(change.rng, change.new) |
| 69 | b.cursor = b.clamp(change.cursorAfter) |
| 70 | |
| 71 | b.undoStack = append(b.undoStack, change) |
| 72 | b.afterHistoryMove() |
| 73 | return true |
| 74 | } |
| 75 | |
| 76 | // afterHistoryMove restores the shared state that undo and redo both disturb. |
| 77 | func (b *Buffer) afterHistoryMove() { |
| 78 | b.modified = true |
| 79 | b.revision++ |
| 80 | b.ClearSelection() |
| 81 | b.coalesce = false |
| 82 | } |
| 83 | |
| 84 | // record pushes change onto the undo history, merging it into the previous |
| 85 | // entry when it continues a run of typing or of backspaces. |
| 86 | func (b *Buffer) record(change edit) { |
| 87 | b.redoStack = nil |
| 88 | |
| 89 | if b.coalesce && b.mergeIntoLast(change) { |
| 90 | b.coalesce = false |
| 91 | return |
| 92 | } |
| 93 | |
| 94 | b.undoStack = append(b.undoStack, change) |
| 95 | if len(b.undoStack) > maxUndoDepth { |
| 96 | b.undoStack = b.undoStack[len(b.undoStack)-maxUndoDepth:] |
| 97 | } |
| 98 | b.coalesce = false |
| 99 | } |
| 100 | |
| 101 | // mergeIntoLast folds change into the entry on top of the undo stack and |
| 102 | // reports whether it could. Only single-line runs of typing or of backspaces |
| 103 | // merge; anything else stays a step of its own. |
| 104 | func (b *Buffer) mergeIntoLast(change edit) bool { |
| 105 | if len(b.undoStack) == 0 { |
| 106 | return false |
| 107 | } |
| 108 | |
| 109 | last := &b.undoStack[len(b.undoStack)-1] |
| 110 | switch { |
| 111 | case last.isInsertion() && change.isInsertion() && continuesTyping(*last, change): |
| 112 | last.new[0] = concat(last.new[0], change.new[0]) |
| 113 | last.cursorAfter = change.cursorAfter |
| 114 | return true |
| 115 | case last.isDeletion() && change.isDeletion() && continuesBackspacing(*last, change): |
| 116 | last.old[0] = concat(change.old[0], last.old[0]) |
| 117 | last.rng.Start = change.rng.Start |
| 118 | // cursorBefore stays where the run of backspaces started, so undoing |
| 119 | // the whole run puts the cursor back where the user began. |
| 120 | last.cursorAfter = change.cursorAfter |
| 121 | return true |
| 122 | default: |
| 123 | return false |
| 124 | } |
| 125 | } |
| 126 | |
| 127 | // continuesTyping reports whether change types straight on from where last |
| 128 | // left off, on the same line. |
| 129 | func continuesTyping(last, change edit) bool { |
| 130 | return isSingleLine(last.new) && isSingleLine(change.new) && |
| 131 | change.rng.Start == last.cursorAfter |
| 132 | } |
| 133 | |
| 134 | // continuesBackspacing reports whether change deletes the character just |
| 135 | // before the one last deleted, on the same line. |
| 136 | func continuesBackspacing(last, change edit) bool { |
| 137 | return isSingleLine(last.old) && isSingleLine(change.old) && |
| 138 | change.rng.End == last.rng.Start |
| 139 | } |
| 140 | |
| 141 | // clearHistory drops the undo and redo stacks, as loading a file does. |
| 142 | func (b *Buffer) clearHistory() { |
| 143 | b.undoStack = nil |
| 144 | b.redoStack = nil |
| 145 | b.coalesce = false |
| 146 | } |
| 147 | |
| 148 | // isEmptyText reports whether lines hold no characters at all. Text is always |
| 149 | // at least one line, so "empty" means one line of length zero. |
| 150 | func isEmptyText(lines [][]rune) bool { |
| 151 | return len(lines) == 1 && len(lines[0]) == 0 |
| 152 | } |
| 153 | |
| 154 | // isSingleLine reports whether lines hold no line break. |
| 155 | func isSingleLine(lines [][]rune) bool { |
| 156 | return len(lines) == 1 |
| 157 | } |