turbo-editors/turbo-corepublic Fork 0
v1.0.2
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.

🛟 Updated. 28d5985 · on v1.0.2 · k33g · 19h ago
undo.go · 157 lines · 4.8 KBGo Blame HistoryRaw
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
package buffer

// maxUndoDepth caps the history so that a long editing session cannot grow the
// process without bound. Turbo C offered a handful of undo levels; a thousand
// is generous while still being a hard ceiling.
const maxUndoDepth = 1000

// edit is one reversible change: the span that was replaced, the text that was
// there before, the text that took its place, and where the cursor sat on
// either side.
//
// Storing both sides makes undo and redo the same operation with the two texts
// swapped, which is why there is only one kind of history entry.
type edit struct {
	rng          Range
	old          [][]rune
	new          [][]rune
	cursorBefore Position
	cursorAfter  Position
}

// isInsertion reports whether the edit only added text.
func (e edit) isInsertion() bool { return isEmptyText(e.old) }

// isDeletion reports whether the edit only removed text.
func (e edit) isDeletion() bool { return isEmptyText(e.new) }

// CanUndo reports whether there is anything to undo.
func (b *Buffer) CanUndo() bool { return len(b.undoStack) > 0 }

// CanRedo reports whether there is anything to redo.
func (b *Buffer) CanRedo() bool { return len(b.redoStack) > 0 }

// Undo reverts the most recent change and reports whether it did anything.
// The cursor goes back where it was before that change.
//
//	b := buffer.NewFromString("hello")
//	b.MoveBufferEnd()
//	b.Insert(" world")
//	b.Undo()
//	fmt.Println(b.Text()) // hello
func (b *Buffer) Undo() bool {
	if len(b.undoStack) == 0 {
		return false
	}

	change := b.undoStack[len(b.undoStack)-1]
	b.undoStack = b.undoStack[:len(b.undoStack)-1]

	b.splice(Range{Start: change.rng.Start, End: endOf(change.rng.Start, change.new)}, change.old)
	b.cursor = b.clamp(change.cursorBefore)

	b.redoStack = append(b.redoStack, change)
	b.afterHistoryMove()
	return true
}

// Redo re-applies the change most recently undone and reports whether it did
// anything. Any new edit clears the redo stack, as it does in every editor.
func (b *Buffer) Redo() bool {
	if len(b.redoStack) == 0 {
		return false
	}

	change := b.redoStack[len(b.redoStack)-1]
	b.redoStack = b.redoStack[:len(b.redoStack)-1]

	b.splice(change.rng, change.new)
	b.cursor = b.clamp(change.cursorAfter)

	b.undoStack = append(b.undoStack, change)
	b.afterHistoryMove()
	return true
}

// afterHistoryMove restores the shared state that undo and redo both disturb.
func (b *Buffer) afterHistoryMove() {
	b.modified = true
	b.revision++
	b.ClearSelection()
	b.coalesce = false
}

// record pushes change onto the undo history, merging it into the previous
// entry when it continues a run of typing or of backspaces.
func (b *Buffer) record(change edit) {
	b.redoStack = nil

	if b.coalesce && b.mergeIntoLast(change) {
		b.coalesce = false
		return
	}

	b.undoStack = append(b.undoStack, change)
	if len(b.undoStack) > maxUndoDepth {
		b.undoStack = b.undoStack[len(b.undoStack)-maxUndoDepth:]
	}
	b.coalesce = false
}

// mergeIntoLast folds change into the entry on top of the undo stack and
// reports whether it could. Only single-line runs of typing or of backspaces
// merge; anything else stays a step of its own.
func (b *Buffer) mergeIntoLast(change edit) bool {
	if len(b.undoStack) == 0 {
		return false
	}

	last := &b.undoStack[len(b.undoStack)-1]
	switch {
	case last.isInsertion() && change.isInsertion() && continuesTyping(*last, change):
		last.new[0] = concat(last.new[0], change.new[0])
		last.cursorAfter = change.cursorAfter
		return true
	case last.isDeletion() && change.isDeletion() && continuesBackspacing(*last, change):
		last.old[0] = concat(change.old[0], last.old[0])
		last.rng.Start = change.rng.Start
		// cursorBefore stays where the run of backspaces started, so undoing
		// the whole run puts the cursor back where the user began.
		last.cursorAfter = change.cursorAfter
		return true
	default:
		return false
	}
}

// continuesTyping reports whether change types straight on from where last
// left off, on the same line.
func continuesTyping(last, change edit) bool {
	return isSingleLine(last.new) && isSingleLine(change.new) &&
		change.rng.Start == last.cursorAfter
}

// continuesBackspacing reports whether change deletes the character just
// before the one last deleted, on the same line.
func continuesBackspacing(last, change edit) bool {
	return isSingleLine(last.old) && isSingleLine(change.old) &&
		change.rng.End == last.rng.Start
}

// clearHistory drops the undo and redo stacks, as loading a file does.
func (b *Buffer) clearHistory() {
	b.undoStack = nil
	b.redoStack = nil
	b.coalesce = false
}

// isEmptyText reports whether lines hold no characters at all. Text is always
// at least one line, so "empty" means one line of length zero.
func isEmptyText(lines [][]rune) bool {
	return len(lines) == 1 && len(lines[0]) == 0
}

// isSingleLine reports whether lines hold no line break.
func isSingleLine(lines [][]rune) bool {
	return len(lines) == 1
}