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
|
package editor
import tea "github.com/charmbracelet/bubbletea"
// Undo/redo is snapshot-based: each checkpoint captures the buffer, cursor,
// scroll, and selection anchor before a change. Consecutive typing coalesces
// into one group; every structural op (newline, delete, kill, paste, cut) is
// its own group, per TASK-006.
// maxUndo bounds the history so a long session can't grow it without limit.
const maxUndo = 500
// editKind classifies a key for grouping. kindType groups coalesce; kindStructural
// groups never do.
type editKind int
const (
kindNone editKind = iota
kindType
kindStructural
)
// snapshot is a restorable point-in-time copy of the editable state.
type snapshot struct {
lines []string
cursor Position
scroll int
anchor *Position
}
func (e *Editor) snapshot() snapshot {
ls := make([]string, len(e.Lines))
copy(ls, e.Lines)
var a *Position
if e.anchor != nil {
c := *e.anchor
a = &c
}
return snapshot{lines: ls, cursor: e.Cursor, scroll: e.Scroll, anchor: a}
}
func (e *Editor) restore(s snapshot) {
ls := make([]string, len(s.lines))
copy(ls, s.lines)
e.Lines = ls
e.invalidate()
e.Cursor = s.cursor
e.Scroll = s.scroll
if s.anchor != nil {
c := *s.anchor
e.anchor = &c
} else {
e.anchor = nil
}
e.Dirty = true
e.setGoal()
e.followCursor()
}
// sameContent reports whether the buffer text matches s (cursor/selection ignored).
func (e *Editor) sameContent(s snapshot) bool {
if len(s.lines) != len(e.Lines) {
return false
}
for i := range s.lines {
if s.lines[i] != e.Lines[i] {
return false
}
}
return true
}
// pushUndoSnapshot appends s to the bounded undo stack.
func (e *Editor) pushUndoSnapshot(s snapshot) {
e.undo = append(e.undo, s)
if len(e.undo) > maxUndo {
e.undo = e.undo[len(e.undo)-maxUndo:]
}
}
// resetHistory clears all undo/redo state (called when the buffer is replaced).
func (e *Editor) resetHistory() {
e.undo = nil
e.redo = nil
e.lastKind = kindNone
}
// PushUndo records the current state as a structural checkpoint. The app calls
// it before driving an edit through InsertText/DeleteSelection (paste, cut).
func (e *Editor) PushUndo() {
e.pushUndoSnapshot(e.snapshot())
e.redo = nil
e.lastKind = kindStructural
}
// Undo reverts the most recent edit group, restoring cursor and selection.
func (e *Editor) Undo() {
if len(e.undo) == 0 {
return
}
cur := e.snapshot()
s := e.undo[len(e.undo)-1]
e.undo = e.undo[:len(e.undo)-1]
e.redo = append(e.redo, cur)
e.restore(s)
e.lastKind = kindNone
}
// Redo reapplies the most recently undone edit group.
func (e *Editor) Redo() {
if len(e.redo) == 0 {
return
}
cur := e.snapshot()
s := e.redo[len(e.redo)-1]
e.redo = e.redo[:len(e.redo)-1]
e.pushUndoSnapshot(cur)
e.restore(s)
e.lastKind = kindNone
}
// editKindOf reports a key's grouping kind and whether it mutates the buffer.
func editKindOf(k tea.KeyMsg) (editKind, bool) {
switch k.Type {
case tea.KeyRunes:
if k.Alt {
switch string(k.Runes) {
case "d": // Alt+D deletes the next word
return kindStructural, true
case "s", "i", "c", "k": // inline markdown formatting (TASK-009)
return kindStructural, true
case "x": // toggle the checkbox on the current line (TASK-023)
return kindStructural, true
}
return kindNone, false // Alt+b/f are word motions
}
return kindType, true
case tea.KeySpace:
return kindType, true
case tea.KeyEnter, tea.KeyTab, tea.KeyShiftTab:
return kindStructural, true
case tea.KeyUp, tea.KeyDown:
if k.Alt { // Alt+Up/Down move (reorder) the current line (TASK-024)
return kindStructural, true
}
return kindNone, false
case tea.KeyBackspace, tea.KeyDelete, tea.KeyCtrlU, tea.KeyCtrlK, tea.KeyCtrlW:
return kindStructural, true
default:
return kindNone, false
}
}
|