Skip to content

Commit f824777

Browse files
Adding line numbers
1 parent ed5c4a7 commit f824777

File tree

4 files changed

+20895
-224
lines changed

4 files changed

+20895
-224
lines changed

playground/internal/react/codeBox.go

Lines changed: 92 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,28 @@
11
package react
22

33
import (
4+
"fmt"
5+
"strconv"
46
"strings"
57

68
"github.com/gopherjs/gopherjs/js"
79
)
810

11+
// CodeBox creates a code editor box React element for editing
12+
// the given code state.
913
func CodeBox(code string, setCode func(string)) *Element {
1014
return CreateElement(codeBoxComponent, Props{
11-
`code`: code,
15+
`curCode`: code,
1216
`setCode`: setCode,
1317
})
1418
}
1519

1620
func codeBoxComponent(props Props) *Element {
1721
cba := &codeBoxAssistant{
18-
code: As[string](props, `code`),
22+
curCode: As[string](props, `curCode`),
1923
setCode: AsFunc(props, `setCode`),
2024
textAreaRef: UseRef(),
25+
lineNumsRef: UseRef(),
2126
}
2227

2328
return Div(Props{
@@ -29,11 +34,20 @@ func codeBoxComponent(props Props) *Element {
2934
`id`: `input`,
3035
},
3136
CreateElement(`textarea`, Props{
37+
`id`: `line-nums`,
38+
`ref`: cba.lineNumsRef,
39+
`value`: cba.getLineNumbers(cba.curCode),
40+
`readOnly`: true,
41+
`disable`: `true`,
42+
}),
43+
CreateElement(`textarea`, Props{
44+
`id`: `code`,
3245
`ref`: cba.textAreaRef,
33-
`defaultValue`: cba.code,
46+
`value`: cba.curCode,
3447
`onInput`: cba.onInput,
3548
`onKeyDown`: cba.onKeyDown,
36-
`id`: `code`,
49+
`onScroll`: cba.onScroll,
50+
`autoFocus`: true,
3751
`autoCorrect`: `off`,
3852
`autoComplete`: `off`,
3953
`autoCapitalize`: `off`,
@@ -44,13 +58,16 @@ func codeBoxComponent(props Props) *Element {
4458
}
4559

4660
type codeBoxAssistant struct {
47-
code string
61+
curCode string
4862
setCode Func
4963
textAreaRef *Ref
64+
lineNumsRef *Ref
5065
}
5166

5267
func (cba *codeBoxAssistant) onInput(e *js.Object) {
53-
cba.setCode(e.Get(`target`).Get(`value`).String())
68+
code := e.Get(`target`).Get(`value`).String()
69+
cba.setCode(code)
70+
//cba.updateLineNumbers(code)
5471
}
5572

5673
func (cba *codeBoxAssistant) onKeyDown(e *js.Object) {
@@ -59,6 +76,12 @@ func (cba *codeBoxAssistant) onKeyDown(e *js.Object) {
5976
}
6077
}
6178

79+
func (cba *codeBoxAssistant) onScroll(e *js.Object) {
80+
scrollTop := e.Get("target").Get("scrollTop").Int()
81+
println("curScrollTop:", scrollTop)
82+
cba.lineNumsRef.Set("scrollTop", scrollTop)
83+
}
84+
6285
func (cba *codeBoxAssistant) handleKeyDown(keyCode int) bool {
6386
toInsert := ``
6487
switch keyCode {
@@ -71,9 +94,8 @@ func (cba *codeBoxAssistant) handleKeyDown(keyCode int) bool {
7194
return false
7295
}
7396

74-
start := cba.textAreaRef.Get(`selectionStart`).Int()
75-
end := cba.textAreaRef.Get(`selectionEnd`).Int()
76-
code := cba.code
97+
start, end := cba.getSelection()
98+
code := cba.curCode
7799

78100
if toInsert == "\n" {
79101
// Add auto-indent for new line.
@@ -92,8 +114,7 @@ func (cba *codeBoxAssistant) handleKeyDown(keyCode int) bool {
92114
newCaret := start + len(toInsert)
93115

94116
cba.setCode(code)
95-
cba.textAreaRef.Set(`selectionStart`, newCaret)
96-
cba.textAreaRef.Set(`selectionEnd`, newCaret)
117+
cba.setSelection(newCaret, code)
97118
return true
98119
}
99120

@@ -109,3 +130,63 @@ func (cba *codeBoxAssistant) currentIndent(start int, code string) string {
109130
}
110131
return code[par:i]
111132
}
133+
134+
func (cba *codeBoxAssistant) getLineNumbers(code string) string {
135+
lines := strings.Count(code, "\n") + 1
136+
size := len(fmt.Sprintf("%d", lines))
137+
var sb strings.Builder
138+
for i := 1; i <= lines; i++ {
139+
sb.WriteString(fmt.Sprintf("%*d", size, i))
140+
if i < lines {
141+
sb.WriteString("\n")
142+
}
143+
}
144+
return sb.String()
145+
}
146+
147+
func (cba *codeBoxAssistant) getSelection() (int, int) {
148+
start := cba.textAreaRef.Get(`selectionStart`).Int()
149+
end := cba.textAreaRef.Get(`selectionEnd`).Int()
150+
return start, end
151+
}
152+
153+
func (cba *codeBoxAssistant) getLineHeight() int {
154+
style := js.Global.Get("window").Call("getComputedStyle", cba.textAreaRef.Current())
155+
// Get line-height property (returns string like "15px")
156+
lineHeightStr := style.Call("getPropertyValue", "line-height").String()
157+
lineHeightStr = strings.TrimSuffix(lineHeightStr, "px")
158+
lineHeight, err := strconv.Atoi(lineHeightStr)
159+
if err != nil {
160+
// Fallback to default if parsing fails (15px is about right for 11pt font)
161+
return 15
162+
}
163+
return lineHeight
164+
}
165+
166+
func (cba *codeBoxAssistant) setSelection(caret int, code string) {
167+
// Pre-update the textarea value so that the caret and scroll can be set
168+
// correctly before the next render so that the next render doesn't reset them.
169+
cba.textAreaRef.Set(`value`, code)
170+
171+
// Set selections
172+
cba.textAreaRef.Set(`selectionStart`, caret)
173+
cba.textAreaRef.Set(`selectionEnd`, caret)
174+
175+
// Auto-scroll to keep caret in view.
176+
curLineNum := strings.Count(code[:caret], "\n")
177+
lineHeight := cba.getLineHeight()
178+
scrollTop := curLineNum * lineHeight
179+
180+
curTop := cba.textAreaRef.Get("scrollTop").Int()
181+
if scrollTop < curTop {
182+
cba.textAreaRef.Set("scrollTop", scrollTop)
183+
} else {
184+
height := cba.textAreaRef.Get("clientHeight").Int()
185+
scrollTop = scrollTop - height + lineHeight
186+
187+
println("curLineNum:", curLineNum, "lineHeight:", lineHeight, "scrollTop:", scrollTop, "curTop:", curTop, "height:", height)
188+
if scrollTop > curTop {
189+
cba.textAreaRef.Set("scrollTop", scrollTop)
190+
}
191+
}
192+
}

playground/internal/react/playground.go

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,21 @@ func Playground() *Element {
66
return CreateElement(playgroundComponent, nil)
77
}
88

9+
var initText = func() string {
10+
const lines = 40
11+
init := ""
12+
for i := 1; i <= lines; i++ {
13+
init += fmt.Sprintf("Hello %d\n", i)
14+
}
15+
return init
16+
}()
17+
918
func playgroundComponent() *Element {
10-
code, setCode := UseState("Hello World")
19+
code, setCode := UseState(initText)
1120

12-
UseEffect(func() {
13-
println(fmt.Sprintf(`Code: %q`, code))
14-
}, code)
21+
//UseEffect(func() {
22+
// println(fmt.Sprintf(`Code: %q`, code))
23+
//}, code)
1524

1625
return Div(Props{
1726
`id`: `playground`,

playground/playground.css

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ a {
1515
}
1616

1717
.box {
18+
display: flex;
19+
align-items: stretch;
1820
position: absolute;
1921
left: 0px;
2022
top: 0px;
@@ -45,6 +47,7 @@ a {
4547
}
4648

4749
#code,
50+
#line-nums,
4851
#output,
4952
pre,
5053
.lines {
@@ -54,16 +57,30 @@ pre,
5457
}
5558

5659
#code {
57-
width: 100%;
58-
height: 100%;
60+
flex: 1;
5961
background: inherit;
6062
border: none;
6163
float: right;
6264
margin: 0;
6365
outline: none;
6466
padding: 0;
67+
padding-left: 5px;
68+
resize: none;
69+
white-space: nowrap;
70+
}
71+
72+
#line-nums {
73+
padding-right: 5px;
74+
background: inherit;
75+
text-align: right;
76+
user-select: none;
77+
min-width: 2em;
78+
border: none;
79+
outline: none;
80+
border-right: 1px solid #000;
81+
white-space: pre-line;
82+
overflow: hidden;
6583
resize: none;
66-
wrap: off;
6784
}
6885

6986
#output {

0 commit comments

Comments
 (0)