Skip to content

Commit 381c232

Browse files
hooked up undo/redo
1 parent fbab436 commit 381c232

7 files changed

Lines changed: 248 additions & 136 deletions

File tree

playground/internal/common/selection.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,18 @@ type Selection struct {
1919
func (sel Selection) IsCaret() bool {
2020
return sel.Start == sel.End
2121
}
22+
23+
// Reversed returns true if the selection is backwards
24+
// (i.e. start is greater than end).
25+
// This occurs when a user selects text from right to left or from bottom to top.
26+
func (sel Selection) Reversed() bool {
27+
return sel.Start > sel.End
28+
}
29+
30+
// Normalize returns a new Selection with Start less than or equal to End.
31+
func (sel Selection) Normalize() Selection {
32+
if sel.Reversed() {
33+
return Selection{Start: sel.End, End: sel.Start}
34+
}
35+
return sel
36+
}

playground/internal/common/undoRedo.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,10 @@ type UndoRedoStack interface {
1717

1818
// RecordSelectionChange records a selection change without a code change
1919
// and adds a break to prevent joining with prior changes.
20-
RecordSelectionChange(cb CodeBoxWrapper)
20+
RecordSelectionChange(newSel Selection)
2121

22-
RecordCodeChange(cb CodeBoxWrapper, priorCode string)
22+
// RecordCodeChange records a code change along with the new selection.
23+
// It should be called whenever the code in the editor changes.
24+
// The oldCode in the next call should match the newCode of the prior call.
25+
RecordCodeChange(newSel Selection, oldCode, newCode string)
2326
}

playground/internal/react/bindings.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,9 @@ func UseStateLazy[T any](initialFn func() T) (T, Setter) {
229229
// UseRef creates a mutable ref object that persists for the lifetime of the component.
230230
// The ref object has a `current` property that can hold any value.
231231
// See: https://react.dev/reference/react/useRef
232+
//
233+
// TODO(grantnelson-wf): Having a `holder` feild doesn't seem to work when
234+
// passing Refs through the props. Investigate a better way to do this.
232235
func UseRef() *Ref {
233236
return &Ref{holder: react().Call(`useRef`, nil)}
234237
}

playground/internal/react/codeBox.go

Lines changed: 49 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -29,22 +29,28 @@ func codeBoxComponent(props Props) *Element {
2929
lineNumsRef = UseRef()
3030
)
3131

32-
onInput := UseCallback(func(e *js.Object) {
33-
setCode(e.Get(`target`).Get(`value`).String())
34-
}, []any{})
35-
36-
onKeyDown := UseCallback(func(e *js.Object) {
37-
key := e.Get(`key`).String()
38-
shift := e.Get(`shiftKey`).Bool()
39-
ctrl := e.Get(`metaKey`).Bool() || e.Get(`ctrlKey`).Bool()
40-
cba := &codeBoxAssistant{
32+
newCBA := func() *codeBoxAssistant {
33+
return &codeBoxAssistant{
4134
curCode: curCode,
4235
setCode: setCode,
4336
onSave: onSave,
4437
onEscape: onEscape,
4538
textAreaRef: textAreaRef,
4639
}
47-
if editor.ProcessKeyDown(cba, key, shift, ctrl) {
40+
}
41+
42+
onInput := UseCallback(func(e *js.Object) {
43+
code := e.Get(`target`).Get(`value`).String()
44+
sel := getSelection(textAreaRef)
45+
globals.UndoRedo().RecordCodeChange(sel, curCode, code)
46+
setCode(code)
47+
}, []any{curCode})
48+
49+
onKeyDown := UseCallback(func(e *js.Object) {
50+
key := e.Get(`key`).String()
51+
shift := e.Get(`shiftKey`).Bool()
52+
ctrl := e.Get(`metaKey`).Bool() || e.Get(`ctrlKey`).Bool()
53+
if editor.ProcessKeyDown(newCBA(), key, shift, ctrl) {
4854
e.Call(`preventDefault`)
4955
e.Call(`stopPropagation`)
5056
}
@@ -55,14 +61,19 @@ func codeBoxComponent(props Props) *Element {
5561
lineNumsRef.Set(`scrollTop`, scrollTop)
5662
}, []any{})
5763

64+
onSelect := UseCallback(func(e *js.Object) {
65+
// Don't normalize the selection so that the direction is preserved.
66+
globals.UndoRedo().RecordSelectionChange(getSelection(textAreaRef))
67+
}, []any{})
68+
5869
// TODO(grantnelson-wf): If it is possible to detect a paste event,
59-
// then we could indent the pasted code automatically.
70+
// then maybe we could indent the pasted code automatically to match
71+
// the indent of where it is being pasted.
6072

6173
UseEffect(func() {
6274
// On first render, focus the code textarea.
6375
textAreaRef.Call(`focus`)
64-
textAreaRef.Set(`selectionStart`, 0)
65-
textAreaRef.Set(`selectionEnd`, 0)
76+
setSelection(textAreaRef, common.Selection{})
6677
}, []any{})
6778

6879
lineCount := strings.Count(curCode, "\n") + 1
@@ -87,6 +98,7 @@ func codeBoxComponent(props Props) *Element {
8798
`onInput`: onInput,
8899
`onKeyDown`: onKeyDown,
89100
`onScroll`: onScroll,
101+
`onSelect`: onSelect,
90102
`autoFocus`: true,
91103
`autoCorrect`: `off`,
92104
`autoComplete`: `off`,
@@ -124,16 +136,19 @@ func (cba *codeBoxAssistant) EmitEvent(event common.Event) {
124136
}
125137

126138
func (cba *codeBoxAssistant) GetSelection() common.Selection {
127-
start := cba.textAreaRef.Get(`selectionStart`).Int()
128-
end := cba.textAreaRef.Get(`selectionEnd`).Int()
129-
if start > end {
130-
// Reverse selection so start is always <= end.
131-
return common.Selection{Start: end, End: start}
132-
}
133-
return common.Selection{Start: start, End: end}
139+
return getSelection(cba.textAreaRef).Normalize()
134140
}
135141

136142
func (cba *codeBoxAssistant) SetCode(sel common.Selection, code string) {
143+
// Record the code change for undo/redo.
144+
// Since this is used mostly for when the editor is doing some kind of edit
145+
// (otherwise the key down would be use the default behavior), we can
146+
// assume the code change should not be joined with other changes.
147+
// Use AddBreak to issolate this change.
148+
globals.UndoRedo().AddBreak()
149+
globals.UndoRedo().RecordCodeChange(sel, cba.curCode, code)
150+
globals.UndoRedo().AddBreak()
151+
137152
// Update the code state for react.
138153
cba.setCode(code)
139154

@@ -142,15 +157,12 @@ func (cba *codeBoxAssistant) SetCode(sel common.Selection, code string) {
142157
cba.textAreaRef.Set(`value`, code)
143158

144159
// Match the diretionallity of the prior selection.
145-
oldStart := cba.textAreaRef.Get(`selectionStart`).Int()
146-
oldEnd := cba.textAreaRef.Get(`selectionEnd`).Int()
147-
if oldStart > oldEnd {
160+
if getSelection(cba.textAreaRef).Reversed() {
148161
sel.Start, sel.End = sel.End, sel.Start
149162
}
150163

151164
// Set selections
152-
cba.textAreaRef.Set(`selectionStart`, sel.Start)
153-
cba.textAreaRef.Set(`selectionEnd`, sel.End)
165+
setSelection(cba.textAreaRef, sel)
154166

155167
// Auto-scroll to keep caret in view.
156168
verticallyAutoScroll(cba.textAreaRef, sel.End, code)
@@ -196,6 +208,18 @@ func horizontallyAutoScroll(textAreaRef *Ref, caret int, code string) {
196208
}
197209
}
198210

211+
func getSelection(textAreaRef *Ref) common.Selection {
212+
start := textAreaRef.Get(`selectionStart`).Int()
213+
end := textAreaRef.Get(`selectionEnd`).Int()
214+
return common.Selection{Start: start, End: end}
215+
}
216+
217+
// setSelection sets the selection on the given textarea ref.
218+
func setSelection(textAreaRef *Ref, sel common.Selection) {
219+
textAreaRef.Set(`selectionStart`, sel.Start)
220+
textAreaRef.Set(`selectionEnd`, sel.End)
221+
}
222+
199223
func getLineNumbers(lineCount int) string {
200224
lines := make([]string, lineCount)
201225
for i := 0; i < lineCount; i++ {

playground/internal/undoRedo/stateChange.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package undoRedo
22

3-
import "github.com/gopherjs/gopherjs.github.io/playground/internal/common"
3+
import (
4+
"github.com/gopherjs/gopherjs.github.io/playground/internal/common"
5+
)
46

57
type stateChange struct {
68
// prefix the length of the unchanged prefix

playground/internal/undoRedo/undoRedo.go

Lines changed: 39 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,17 @@ type undoRedoStackImp struct {
1212

1313
// getTime gets the current time.
1414
// It can be replaced when testing.
15-
getTime func() time.Time
16-
joinDur time.Duration
15+
getTime func() time.Time
16+
joinDur time.Duration
17+
joinLimit int
1718

1819
lastChange time.Time
1920
lastSel common.Selection
21+
22+
// doNotRecord prevents recording changes when true.
23+
// This is set to true when performing undo/redo operations
24+
// so that the undo/redo operations themselves are not recorded.
25+
doNotRecord bool
2026
}
2127

2228
func NewUndoRedoStack() common.UndoRedoStack {
@@ -27,39 +33,49 @@ func NewUndoRedoStack() common.UndoRedoStack {
2733
}
2834

2935
func (s *undoRedoStackImp) PerformUndo(cb common.CodeBoxWrapper) {
30-
if maxIndex := len(s.undos) - 1; maxIndex >= 0 {
31-
undoChange := s.undos[maxIndex]
32-
s.undos = s.undos[:maxIndex]
33-
s.redos = append(s.redos, revertChange(cb, undoChange))
34-
}
36+
s.performRevert(cb, &s.undos, &s.redos)
3537
}
3638

3739
func (s *undoRedoStackImp) PerformRedo(cb common.CodeBoxWrapper) {
38-
if maxIndex := len(s.redos) - 1; maxIndex >= 0 {
39-
redoChange := s.redos[maxIndex]
40-
s.redos = s.redos[:maxIndex]
41-
s.undos = append(s.undos, revertChange(cb, redoChange))
40+
s.performRevert(cb, &s.redos, &s.undos)
41+
}
42+
43+
func (s *undoRedoStackImp) performRevert(cb common.CodeBoxWrapper, fromStack, toStack *[]*stateChange) {
44+
if maxIndex := len(*fromStack) - 1; maxIndex >= 0 {
45+
s.doNotRecord = true
46+
defer func() { s.doNotRecord = false }()
47+
48+
redoChange := (*fromStack)[maxIndex]
49+
(*fromStack) = (*fromStack)[:maxIndex]
50+
(*toStack) = append((*toStack), revertChange(cb, redoChange))
4251
}
4352
}
4453

4554
func (s *undoRedoStackImp) AddBreak() {
55+
if s.doNotRecord {
56+
return
57+
}
4658
// zeroing time prevents joining with prior changes.
4759
s.lastChange = time.Time{}
4860
}
4961

50-
func (s *undoRedoStackImp) RecordSelectionChange(cb common.CodeBoxWrapper) {
51-
s.lastSel = cb.GetSelection()
62+
func (s *undoRedoStackImp) RecordSelectionChange(newSel common.Selection) {
63+
if s.doNotRecord || newSel == s.lastSel {
64+
return
65+
}
66+
s.lastSel = newSel
5267
s.AddBreak()
5368
}
5469

55-
func (s *undoRedoStackImp) RecordCodeChange(cb common.CodeBoxWrapper, priorCode string) {
70+
func (s *undoRedoStackImp) RecordCodeChange(newSel common.Selection, oldCode, newCode string) {
71+
if s.doNotRecord {
72+
return
73+
}
5674
now := s.getTime()
57-
newSel := cb.GetSelection()
58-
newer := newState(priorCode, cb.Code(), s.lastSel, newSel)
75+
newer := newState(oldCode, newCode, s.lastSel, newSel)
5976

6077
defer func() {
61-
s.lastChange = now
62-
s.lastSel = newSel
78+
// clear the redo stack on new changes
6379
s.redos = nil
6480
}()
6581

@@ -68,10 +84,15 @@ func (s *undoRedoStackImp) RecordCodeChange(cb common.CodeBoxWrapper, priorCode
6884
maxIndex := len(s.undos) - 1
6985
if joined := joinStates(s.undos[maxIndex], newer); joined != nil {
7086
s.undos[maxIndex] = joined
87+
// don't update the last change time so that a fast diligent typer
88+
// will still have the continuous changes broken up periodically.
89+
s.lastSel = newSel
7190
return
7291
}
7392
}
7493

7594
// cannot join, just add the new state
7695
s.undos = append(s.undos, newer)
96+
s.lastChange = now
97+
s.lastSel = newSel
7798
}

0 commit comments

Comments
 (0)