turbo-editors/turbo-corepublic Fork 0
v1.0.1
Commits
Clone
git clone https://git.rickub.com/turbo-editors/turbo-core.git
git clone ssh://git@rickub.com/turbo-editors/turbo-core.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

undo.go · 157 lines · 4.8 KBGo Blame HistoryRaw
🛟 Updated. 28d5985 k33g 19h ago1package 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.
6const 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.
14type 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.
23func (e edit) isInsertion() bool { return isEmptyText(e.old) }
24
25// isDeletion reports whether the edit only removed text.
26func (e edit) isDeletion() bool { return isEmptyText(e.new) }
27
28// CanUndo reports whether there is anything to undo.
29func (b *Buffer) CanUndo() bool { return len(b.undoStack) > 0 }
30
31// CanRedo reports whether there is anything to redo.
32func (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
42func (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.
60func (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.
77func (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.
86func (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.
104func (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.
129func 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.
136func 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.
142func (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.
150func isEmptyText(lines [][]rune) bool {
151 return len(lines) == 1 && len(lines[0]) == 0
152}
153
154// isSingleLine reports whether lines hold no line break.
155func isSingleLine(lines [][]rune) bool {
156 return len(lines) == 1
157}