Skip to content

Commit 3677f33

Browse files
Trying to get undo/redo working
1 parent 0d589cf commit 3677f33

7 files changed

Lines changed: 4363 additions & 6160 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
/playground/playground.js.map
2+
.DS_Store

go.work.sum

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,6 @@ github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3Ee
5757
github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
5858
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
5959
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
60-
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
6160
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
6261
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
6362
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
@@ -116,7 +115,6 @@ github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
116115
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
117116
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
118117
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
119-
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
120118
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
121119
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
122120
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
@@ -212,14 +210,14 @@ github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An
212210
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
213211
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
214212
github.com/spf13/viper v1.8.1/go.mod h1:o0Pch8wJ9BVSWGQMbra6iw0oQ5oktSIBaujf1rJH9Ns=
215-
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
216213
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
214+
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
217215
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
218216
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
219217
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
220218
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
221-
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
222219
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
220+
github.com/visualfc/goembed v0.3.3 h1:pOL02L715tHKsLQVMcZz06tTzRDAHkJKJLRnCA22G9Q=
223221
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
224222
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
225223
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
@@ -259,6 +257,7 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0
259257
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
260258
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
261259
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
260+
golang.org/x/exp/typeparams v0.0.0-20240119083558-1b970713d09a h1:8qmSSA8Gz/1kTrCe0nqR0R3Gb/NDhykzWw2q2mWZydM=
262261
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
263262
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
264263
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
@@ -596,8 +595,8 @@ gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
596595
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
597596
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
598597
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
599-
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
600598
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
599+
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
601600
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
602601
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
603602
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=

playground/internal/editor/undo.go

Lines changed: 150 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,11 @@
11
package editor
22

3-
import "time"
3+
import (
4+
"time"
5+
)
46

57
const undoRedoJoinDuration = time.Second
68

7-
type undoRedoState struct {
8-
prefix int
9-
suffix int
10-
oldCode string
11-
newCode string
12-
oldSel Selection
13-
newSel Selection
14-
}
15-
169
type UndoRedoStack struct {
1710
undos []*undoRedoState
1811
redos []*undoRedoState
@@ -27,63 +20,104 @@ func NewUndoRedoStack() *UndoRedoStack {
2720
}
2821

2922
func (s *UndoRedoStack) PerformUndo(cb CodeBoxWrapper) {
30-
// TODO(grantnelson-wf): Finish implmementing
23+
if maxIndex := len(s.undos) - 1; maxIndex >= 0 {
24+
undoChange := s.undos[maxIndex]
25+
s.undos = s.undos[:maxIndex]
26+
s.redos = append(s.redos, revertChange(cb, undoChange))
27+
}
3128
}
3229

3330
func (s *UndoRedoStack) PerformRedo(cb CodeBoxWrapper) {
34-
// TODO(grantnelson-wf): Finish implmementing
31+
if maxIndex := len(s.redos) - 1; maxIndex >= 0 {
32+
redoChange := s.redos[maxIndex]
33+
s.redos = s.redos[:maxIndex]
34+
s.undos = append(s.undos, revertChange(cb, redoChange))
35+
}
3536
}
3637

37-
func (s *UndoRedoStack) RecordSelectionChange(cb CodeBoxWrapper) {
38-
// TODO(grantnelson-wf): Finish implmementing
38+
// AddBreak makes any prior changes not joined into any future changes.
39+
//
40+
// This should be called when the user makes a change and there is an
41+
// automatic code modification such as auto-formatting or auto-completion.
42+
// This allows the user to unto the autmatic change separately from their
43+
// own change.
44+
//
45+
// TODO(grantnelson-wf): Insert AddBreak into editor where appropriate.
46+
func (s *UndoRedoStack) AddBreak() {
47+
// zeroing time prevents joining with prior changes.
48+
s.lastChange = time.Time{}
49+
}
3950

51+
// RecordSelectionChange records a selection change without a code change
52+
// and adds a break to prevent joining with prior changes.
53+
func (s *UndoRedoStack) RecordSelectionChange(cb CodeBoxWrapper) {
54+
s.lastSel = cb.GetSelection()
55+
s.AddBreak()
4056
}
4157

4258
func (s *UndoRedoStack) RecordCodeChange(cb CodeBoxWrapper, priorCode string) {
43-
//now := s.getTime()
44-
45-
// TODO(grantnelson-wf): Finish implmementing
59+
now := s.getTime()
60+
newSel := cb.GetSelection()
61+
newer := newState(priorCode, cb.Code(), s.lastSel, newSel)
62+
s.lastChange = now
63+
s.lastSel = newSel
64+
s.redos = nil
65+
66+
if now.Sub(s.lastChange) <= undoRedoJoinDuration {
67+
// changes happened close enough in time to consider joining
68+
maxIndex := len(s.undos) - 1
69+
if joined := joinStates(s.undos[maxIndex], newer); joined != nil {
70+
s.undos[maxIndex] = joined
71+
return
72+
}
73+
}
4674

47-
s.redos = nil // clear redo stack on new code change
75+
// cannot join, just add the new state
76+
s.undos = append(s.undos, newer)
4877
}
4978

50-
/*
51-
func (s *UndoRedoStack) joinStates(older, newer *undoRedoState) bool {
52-
if newer.start.Sub(older.start) > undoRedoJoinDuration {
53-
return false // changes happened too far apart in time to join
54-
}
79+
type undoRedoState struct {
80+
// prefix the length of the unchanged prefix
81+
prefix int
5582

56-
if older.prefix > newer.prefix+len(newer.newCode) ||
57-
older.suffix > newer.suffix+len(newer.newCode) {
58-
return false // changes don't overlap so are too far apart in code to join
59-
}
83+
// suffix the length of the unchanged suffix
84+
suffix int
85+
86+
// oldCode the changed portion of the old code
87+
oldCode string
88+
89+
// newCode the changed portion of the new code
90+
newCode string
6091

61-
// TODO(grantnelson-wf): Finish implmementing
92+
// oldSel the selection before the change
93+
oldSel Selection
6294

63-
return true
95+
// newSel the selection after the change
96+
newSel Selection
6497
}
6598

66-
func newState(priorCode, newCode string, priorSel, afterSel Selection, start time.Time) *undoRedoState {
67-
prefixLen, suffixLen := diffTrim(priorCode, newCode)
99+
// newState creates a new undoRedoState representing the change
100+
// from oldCode to newCode, with the given old and new selections.
101+
func newState(oldCode, newCode string, oldSel, newSel Selection) *undoRedoState {
102+
prefixLen, suffixLen := diffTrim(oldCode, newCode)
68103
return &undoRedoState{
69104
prefix: prefixLen,
70105
suffix: suffixLen,
106+
oldCode: oldCode[prefixLen : len(oldCode)-suffixLen],
71107
newCode: newCode[prefixLen : len(newCode)-suffixLen],
72-
prior: priorSel,
73-
after: afterSel,
74-
start: start,
108+
oldSel: oldSel,
109+
newSel: newSel,
75110
}
76111
}
77-
*/
78112

79-
// diffTrim returns the lengths of the common prefix and suffix between
80-
// prior and after strings.
81-
func diffTrim(prior, after string) (int, int) {
82-
priorLen, afterLen := len(prior), len(after)
83-
minLen := min(priorLen, afterLen)
113+
// diffTrim returns the lengths of the common prefix and suffix
114+
// between old and new code.
115+
func diffTrim(oldCode, newCode string) (int, int) {
116+
oldLen, newLen := len(oldCode), len(newCode)
117+
minLen := min(oldLen, newLen)
84118

85119
prefixLen := 0
86-
for prefixLen < minLen && prior[prefixLen] == after[prefixLen] {
120+
for prefixLen < minLen && oldCode[prefixLen] == newCode[prefixLen] {
87121
prefixLen++
88122
}
89123
if prefixLen >= minLen {
@@ -92,15 +126,86 @@ func diffTrim(prior, after string) (int, int) {
92126

93127
suffixLen := 0
94128
minLen -= prefixLen
95-
priorMax, afterMax := priorLen-1, afterLen-1
96-
for suffixLen < minLen && prior[priorMax] == after[afterMax] {
129+
oldMax, newMax := oldLen-1, newLen-1
130+
for suffixLen < minLen && oldCode[oldMax] == newCode[newMax] {
97131
suffixLen++
98-
priorMax--
99-
afterMax--
132+
oldMax--
133+
newMax--
100134
}
101135
return prefixLen, suffixLen
102136
}
103137

138+
// joinStates returns the joined state if the two states can be joined,
139+
// otherwise returns nil.
140+
//
141+
// The two states must be adjacent meaning that the newCode in the older state
142+
// must come from the same code as the oldCode in the newer state.
143+
// If the states overlap or are close enough in the code, they can be joined.
144+
func joinStates(older, newer *undoRedoState) *undoRedoState {
145+
oldDiffLen := len(older.newCode)
146+
newDiffLen := len(newer.oldCode)
147+
148+
// Check that the total length of the code shared between the two states is the same.
149+
if older.prefix+oldDiffLen+older.suffix != newer.prefix+newDiffLen+newer.suffix {
150+
// The states are not sharing a common code base between them.
151+
// This shouldn't happen if the states were recorded correctly,
152+
// but better to be safe.
153+
return nil
154+
}
155+
156+
if older.prefix+oldDiffLen < newer.prefix ||
157+
newer.prefix+newDiffLen < older.prefix {
158+
// There is a gap between the changes so we want to
159+
// keep them as separate states.
160+
return nil
161+
}
162+
163+
// for any of the newer state's old code that extends beyond the older
164+
// state's old code, add it to the joined old code.
165+
joinedOldCode := older.oldCode
166+
if newer.prefix < older.prefix {
167+
joinedOldCode = newer.oldCode[:older.prefix-newer.prefix] + joinedOldCode
168+
}
169+
if newer.prefix+newDiffLen > older.prefix+oldDiffLen {
170+
joinedOldCode += newer.oldCode[(older.prefix+oldDiffLen)-newer.prefix:]
171+
}
172+
173+
// for any of the older state's new code that extends beyond the newer
174+
// state's new code, add it to the joined new code.
175+
joinedNewCode := newer.newCode
176+
if older.prefix < newer.prefix {
177+
joinedNewCode = older.newCode[:newer.prefix-older.prefix] + joinedNewCode
178+
}
179+
if older.prefix+oldDiffLen > newer.prefix+newDiffLen {
180+
joinedNewCode += older.newCode[(newer.prefix+newDiffLen)-older.prefix:]
181+
}
182+
183+
return &undoRedoState{
184+
prefix: min(older.prefix, newer.prefix),
185+
suffix: min(older.suffix, newer.suffix),
186+
oldCode: joinedOldCode,
187+
newCode: joinedNewCode,
188+
oldSel: older.oldSel,
189+
newSel: newer.newSel,
190+
}
191+
}
192+
193+
func revertChange(cb CodeBoxWrapper, state *undoRedoState) *undoRedoState {
194+
code := cb.Code()
195+
oldCode := code[:state.prefix] + state.oldCode + code[len(code)-state.suffix:]
196+
cb.SetCode(state.oldSel, oldCode)
197+
198+
// return the inverse state to undo the revert if needed (i.e. redo)
199+
return &undoRedoState{
200+
prefix: state.prefix,
201+
suffix: state.suffix,
202+
oldCode: state.newCode,
203+
newCode: state.oldCode,
204+
oldSel: state.newSel,
205+
newSel: state.oldSel,
206+
}
207+
}
208+
104209
// TODO(grantnelson-wf): Remove when `min` is available in go1.21.
105210
// See https://pkg.go.dev/builtin#min
106211
func min(a, b int) int {

playground/internal/react/bindings.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,10 @@ var (
5151
ErrRefNotInitialized = errors.New(`react: Ref not initialized`)
5252
)
5353

54-
var ReactDOMClient *js.Object
55-
var React *js.Object
54+
var (
55+
ReactDOMClient *js.Object
56+
React *js.Object
57+
)
5658

5759
func reactDom() *js.Object {
5860
if ReactDOMClient == nil {

playground/internal/react/codeBox.go

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package react
22

33
import (
4+
"fmt"
45
"strconv"
56
"strings"
67

@@ -87,21 +88,20 @@ func (cba *codeBoxAssistant) EmitEvent(event string) {
8788
cba.onSave()
8889
case editor.EscapeEvent:
8990
cba.onEscape()
90-
91-
case editor.UndoEvent, editor.RedoEvent:
92-
println("Not implemented yet: Undo/Redo requested from CodeBox:", event)
93-
// TODO(grantnelson-wf): Implement undo/redo stack since textarea
94-
// only handles undo/redo itself for non-programmatic changes but
95-
// doesn't handle undo/redo for when setCode is called.
96-
97-
// TODO(grantnelson-wf): If it is possible to detect a paste event,
98-
// then indent the pasted code automatically.
99-
91+
case editor.UndoEvent:
92+
// cba.undoRedo.PerformUndo(cba)// TODO: FIX
93+
fmt.Println("PerformUndo called on CodeBox") // TODO: REMOVE
94+
case editor.RedoEvent:
95+
// cba.undoRedo.PerformRedo(cba) // TODO: FIX
96+
fmt.Println("RedoEvent called on CodeBox") // TODO: REMOVE
10097
default:
10198
println("Unknown event was requested to be emitted from CodeBox:", event)
10299
}
103100
}
104101

102+
// TODO(grantnelson-wf): If it is possible to detect a paste event,
103+
// then indent the pasted code automatically.
104+
105105
func (cba *codeBoxAssistant) onInput(e *js.Object) {
106106
cba.setCode(e.Get(`target`).Get(`value`).String())
107107
}

playground/internal/react/playground.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ func Playground() *Element {
5959
Button(`format-button`, `Format`, nil, pa.onFormatClick),
6060
ToggleBox(`format-imports`, `Rewrite imports on Format`, `Imports`, fmtImports, setFmtImports),
6161
ShareUrlControl(shareUrl, pa.onShareClick),
62-
// TODO(grantnelson-wf): Snippet selection control.
62+
// TODO(grantnelson-wf): Add a Snippet selection control / dropdown for loading predefined snippets.
6363
ToggleBox(`color-theme`, `Change color-theme`, ``, lightTheme, setLightTheme),
6464
),
6565
),

0 commit comments

Comments
 (0)