Skip to content

Commit ad14c73

Browse files
moving to react 19
1 parent 92c54d2 commit ad14c73

File tree

6 files changed

+525
-116
lines changed

6 files changed

+525
-116
lines changed

playground/index.html

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,12 @@
55
<title>GopherJS Playground</title>
66
<link rel="stylesheet" type="text/css" href="playground.css">
77
<link rel="icon" href="/favicon-gopherjs.png" sizes="any">
8-
9-
<!-- Load React and ReactDOM from CDN -->
10-
<script src="https://unpkg.com/react@18/umd/react.development.js" crossorigin></script>
11-
<script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js" crossorigin></script>
12-
13-
<!-- Load transpiled playgound.go -->
14-
<script src="playground.js" defer></script>
8+
<script type="module">
9+
import React from "https://esm.sh/react@19/?dev";
10+
import ReactDOMClient from "https://esm.sh/react-dom@19/client/?dev";
11+
import "./playground.js";
12+
window.RunPlayground(React, ReactDOMClient);
13+
</script>
1514
</head>
1615
<body>
1716
<div id="playground"></div>

playground/internal/react/bindings.go

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,12 +45,34 @@ type (
4545
)
4646

4747
var (
48-
ErrUndefinedPropKey = errors.New(`react: undefined prop key`)
49-
ErrRefNotInitialized = errors.New(`react: Ref not initialized`)
48+
ErrReactDOMClientNotLoaded = errors.New(`react: ReactDOMClient is not loaded`)
49+
ErrReactNotLoaded = errors.New(`react: React is not loaded`)
50+
ErrUndefinedPropKey = errors.New(`react: undefined prop key`)
51+
ErrRefNotInitialized = errors.New(`react: Ref not initialized`)
5052
)
5153

52-
func reactDom() *js.Object { return js.Global.Get(`ReactDOM`) }
53-
func react() *js.Object { return js.Global.Get(`React`) }
54+
var ReactDOMClient *js.Object
55+
var React *js.Object
56+
57+
func reactDom() *js.Object {
58+
if ReactDOMClient == nil {
59+
ReactDOMClient = js.Global.Get(`ReactDOMClient`)
60+
}
61+
if ReactDOMClient == nil {
62+
panic(ErrReactDOMClientNotLoaded)
63+
}
64+
return ReactDOMClient
65+
}
66+
67+
func react() *js.Object {
68+
if React == nil {
69+
React = js.Global.Get(`React`)
70+
}
71+
if React == nil {
72+
panic(ErrReactNotLoaded)
73+
}
74+
return React
75+
}
5476

5577
func CreateRoot(id string) *Root {
5678
rootElem := js.Global.Get(`document`).Call(`getElementById`, id)

playground/internal/react/codeBox.go

Lines changed: 214 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -25,17 +25,12 @@ func codeBoxComponent(props Props) *Element {
2525
textAreaRef: UseRef(),
2626
lineNumsRef: UseRef(),
2727
}
28+
lineCount := strings.Count(cba.curCode, "\n") + 1
2829

2930
return Div(Props{
3031
`id`: `code-box`,
3132
},
32-
TextArea(Props{
33-
`id`: `line-nums`,
34-
`ref`: cba.lineNumsRef,
35-
`value`: cba.getLineNumbers(),
36-
`readOnly`: true,
37-
`disable`: `true`,
38-
}),
33+
codeLineBox(lineCount, cba.lineNumsRef),
3934
TextArea(Props{
4035
`id`: `code`,
4136
`ref`: cba.textAreaRef,
@@ -52,6 +47,36 @@ func codeBoxComponent(props Props) *Element {
5247
)
5348
}
5449

50+
func codeLineBox(lineCount int, ref *Ref) *Element {
51+
return CreateElement(codeLineBoxComponent, Props{
52+
`lineCount`: lineCount,
53+
`ref`: ref,
54+
})
55+
}
56+
57+
func codeLineBoxComponent(props Props) *Element {
58+
lineCount := int(As[float64](props, `lineCount`))
59+
ref := props[`ref`]
60+
61+
var sb strings.Builder
62+
for i := 1; i <= lineCount; i++ {
63+
sb.WriteString(strconv.Itoa(i))
64+
if i < lineCount {
65+
sb.WriteString("\n")
66+
}
67+
}
68+
69+
print(lineCount) // TODO(grantnelson-wf): Remove
70+
71+
return TextArea(Props{
72+
`id`: `line-nums`,
73+
`ref`: ref,
74+
`value`: sb.String(),
75+
`readOnly`: true,
76+
`disable`: `true`,
77+
})
78+
}
79+
5580
type codeBoxAssistant struct {
5681
curCode string
5782
setCode func(any)
@@ -64,13 +89,14 @@ func (cba *codeBoxAssistant) onInput(e *js.Object) {
6489
}
6590

6691
func (cba *codeBoxAssistant) onKeyDown(e *js.Object) {
67-
// TODO(grantnelson-wf): Maybe handle Shift+Tab for un-indenting the current line.
68-
// TODO(grantnelson-wf): Handle Ctrl+/ for commenting/uncommenting the current line or selection.
69-
// TODO(grantnelson-wf): Handle Ctrl+S for saving the code, just run formatting, or just ignore it.
70-
// TODO(grantnelson-wf): Maybe handle auto-text like `/*` adds `*/`, `"` adds another `"`, `[` adds `]`, etc.
92+
// TODO(grantnelson-wf): Maybe we should handle Ctrl+S for saving the code, just run formatting, or just ignore it.
7193
// TODO(grantnelson-wf): If possible, make escape focus out of the code box since tabs won't switch focus anymore.
72-
if cba.handleKeyDown(e.Get(`keyCode`).Int()) {
94+
key := e.Get(`key`).String()
95+
shift := e.Get(`shiftKey`).Bool()
96+
ctrl := e.Get(`metaKey`).Bool() || e.Get(`ctrlKey`).Bool()
97+
if cba.handleKeyDown(key, shift, ctrl) {
7398
e.Call(`preventDefault`)
99+
e.Call(`stopPropagation`)
74100
}
75101
}
76102

@@ -79,49 +105,169 @@ func (cba *codeBoxAssistant) onScroll(e *js.Object) {
79105
cba.lineNumsRef.Set(`scrollTop`, scrollTop)
80106
}
81107

82-
func (cba *codeBoxAssistant) handleKeyDown(keyCode int) bool {
83-
toInsert := ``
84-
switch keyCode {
85-
case '\t':
86-
// Insert tab character and prevent focus change.
87-
toInsert = "\t"
88-
case '\r':
89-
toInsert = "\n"
108+
func (cba *codeBoxAssistant) handleKeyDown(key string, shift, ctrl bool) bool {
109+
switch key {
110+
case `Tab`:
111+
return cba.handleTab(shift, ctrl)
112+
case `Enter`:
113+
return cba.handleNewline(shift, ctrl)
114+
case "*":
115+
return cba.handleMultilineComment(shift, ctrl)
116+
case "/":
117+
return cba.handleCommentToggle(shift, ctrl)
118+
case "\"":
119+
return cba.insertPair(ctrl, `"`, `"`)
120+
case "'":
121+
return cba.insertPair(ctrl, `'`, `'`)
122+
case "`":
123+
return cba.insertPair(ctrl, "`", "`")
124+
case "(":
125+
return cba.insertPair(ctrl, `(`, `)`)
126+
case "{":
127+
return cba.insertPair(ctrl, `{`, `}`)
128+
case "[":
129+
return cba.insertPair(ctrl, `[`, `]`)
90130
default:
91-
// no special handling so allow default behavior.
131+
return false
132+
}
133+
}
134+
135+
// handleTab handles inserting a tab character and indenting or un-indenting
136+
// the current selected line(s).
137+
func (cba *codeBoxAssistant) handleTab(shift, ctrl bool) bool {
138+
if ctrl {
139+
// Allow default behavior for Ctrl+Tab (focus change).
92140
return false
93141
}
94142

95143
start, end := cba.getSelection()
96-
code := cba.curCode
144+
if !shift && start == end {
145+
// Insert tab character at caret.
146+
cba.insertAtSelection("\t", ``, false)
147+
return true
148+
}
149+
150+
if shift {
151+
// Handle Shift+Tab for un-indenting the current line.
152+
// TODO(grantnelson-wf): Implement
153+
println("TODO: Handle un-indenting selected lines")
154+
return true
155+
}
156+
157+
// Handle indenting selected line(s).
158+
// TODO(grantnelson-wf): Implement
159+
println("TODO: Handle indenting selected lines")
160+
return true
161+
}
162+
163+
func (cba *codeBoxAssistant) handleMultilineComment(shift, ctrl bool) bool {
164+
if !shift || ctrl {
165+
// Allow default behavior for 8 or Ctrl+* (which is usually Shift+Ctrl+8).
166+
return false
167+
}
168+
169+
start, end := cba.getSelection()
170+
if start != end {
171+
// If a selection, just prerform default behavior.
172+
return false
173+
}
174+
175+
if start <= 0 || cba.curCode[start-1] != '/' {
176+
// Not preceded by '/', allow default behavior.
177+
return false
178+
}
97179

98-
if toInsert == "\n" {
99-
// Add auto-indent for new line.
100-
toInsert += cba.currentIndent(start, code)
180+
// Insert '*/' after caret to complete the multi-line comment.
181+
cba.insertAtSelection(`*`, `*/`, false)
182+
return true
183+
}
184+
185+
// handleNewline handles inserting a new line with auto-indent.
186+
func (cba *codeBoxAssistant) handleNewline(shift, ctrl bool) bool {
187+
if shift || ctrl {
188+
// Allow default behavior for Shift+Enter or Ctrl+Enter (new line without indent).
189+
return false
101190
}
102191

103-
var head, tail string
192+
start, end := cba.getSelection()
193+
before := "\n" + cba.indentAt(start)
194+
after := ``
195+
196+
// add extra indent if the character before the selection is an opening brace.
197+
if start > 0 && start <= len(cba.curCode) {
198+
switch cba.curCode[start-1] {
199+
case '{', '(', '[':
200+
before += "\t"
201+
}
202+
}
203+
204+
// add extra after if the character after the selection is a closing brace.
205+
if end >= 0 && end < len(cba.curCode) {
206+
switch cba.curCode[end] {
207+
case '}', ')', ']':
208+
opening := cba.findMatchingOpeningBrace(end)
209+
println("Found matching opening brace at:", opening) // TODO(grantnelson-wf): Remove
210+
if opening >= 0 {
211+
after = "\n" + cba.indentAt(opening)
212+
}
213+
}
214+
}
215+
216+
println("before:", strconv.Quote(before)) // TODO(grantnelson-wf): Remove
217+
println("after: ", strconv.Quote(after)) // TODO(grantnelson-wf): Remove
218+
cba.insertAtSelection(before, after, false)
219+
return true
220+
}
221+
222+
// handleCommentToggle handles toggling comments on the current line or selection.
223+
func (cba *codeBoxAssistant) handleCommentToggle(shift, ctrl bool) bool {
224+
if !ctrl || shift {
225+
// Allow default behavior for '/' without a Ctrl or Shift+/.
226+
return false
227+
}
228+
229+
// TODO(grantnelson-wf): Implement
230+
println("TODO: Handle comment toggle")
231+
return true
232+
}
233+
234+
func (cba *codeBoxAssistant) insertPair(ctrl bool, before, after string) bool {
235+
if ctrl {
236+
// Allow default behavior for Ctrl+key.
237+
return false
238+
}
239+
240+
cba.insertAtSelection(before, after, true)
241+
return true
242+
}
243+
244+
func (cba *codeBoxAssistant) insertAtSelection(before, after string, keepSelection bool) {
245+
start, end := cba.getSelection()
246+
code := cba.curCode
247+
248+
var head, tail, selected string
104249
if start > 0 {
105250
head = code[:start]
106251
}
107252
if end < len(code) {
108253
tail = code[end:]
109254
}
255+
if keepSelection && start > 0 && end < len(code) && start < end {
256+
selected = code[start:end]
257+
}
110258

111-
code = head + toInsert + tail
112-
newCaret := start + len(toInsert)
259+
code = head + before + selected + after + tail
260+
newCaret := start + len(before)
113261

114262
cba.setCode(code)
115263
cba.setSelection(newCaret, code)
116-
return true
117264
}
118265

119-
func (cba *codeBoxAssistant) currentIndent(start int, code string) string {
120-
if start < 0 || start > len(code) {
266+
func (cba *codeBoxAssistant) indentAt(start int) string {
267+
code := cba.curCode
268+
if start <= 0 || start > len(code) {
121269
return ``
122270
}
123-
124-
// get prior line's indent
125271
par := strings.LastIndex(code[:start], "\n") + 1
126272
i := par
127273
for i < start {
@@ -131,26 +277,45 @@ func (cba *codeBoxAssistant) currentIndent(start int, code string) string {
131277
}
132278
i++
133279
}
134-
indent := code[par:i]
135-
136-
// adjust indent based on prior line's last character
137-
switch code[start] {
138-
case '{', '(', '[':
139-
indent += "\t"
140-
}
141-
return indent
280+
return code[par:i]
142281
}
143282

144-
func (cba *codeBoxAssistant) getLineNumbers() string {
145-
lines := strings.Count(cba.curCode, "\n") + 1
146-
var sb strings.Builder
147-
for i := 1; i <= lines; i++ {
148-
sb.WriteString(strconv.Itoa(i))
149-
if i < lines {
150-
sb.WriteString("\n")
283+
// findMatchingOpeningBrace finds the position of the matching opening brace
284+
// for the closing brace at the given caret position.
285+
// Currently this does not account for braces inside strings or comments.
286+
// It returns -1 if no matching opening brace is found.
287+
func (cba *codeBoxAssistant) findMatchingOpeningBrace(caret int) int {
288+
if caret <= 0 || caret > len(cba.curCode) {
289+
return -1
290+
}
291+
pairs := map[byte]byte{
292+
'}': '{',
293+
')': '(',
294+
']': '[',
295+
}
296+
openingBrace, ok := pairs[cba.curCode[caret]]
297+
if !ok {
298+
return -1 // Caret not at a closing brace
299+
}
300+
stack := []byte{openingBrace}
301+
for i := caret - 1; i >= 0; i-- {
302+
c := cba.curCode[i]
303+
switch c {
304+
case '}', ')', ']':
305+
stack = append(stack, pairs[c])
306+
case '{', '(', '[':
307+
if top := len(stack) - 1; stack[top] == c {
308+
stack = stack[:top]
309+
} else {
310+
return -1 // Mismatched brace
311+
}
312+
}
313+
if len(stack) == 0 {
314+
// stack is empty, found the matching opening brace.
315+
return i
151316
}
152317
}
153-
return sb.String()
318+
return -1
154319
}
155320

156321
func (cba *codeBoxAssistant) getSelection() (int, int) {

playground/internal/react/playground.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@ func Playground() *Element {
3636
// code changed so clear share URL
3737
setShareUrl(``)
3838
getLocation().Set(`hash`, ``)
39-
println("Code changed, cleared share URL") // TODO(grantnelson-wf): Remove debug
4039
}, []any{code})
4140

4241
UseEffect(pa.initCode, []any{})

0 commit comments

Comments
 (0)