@@ -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+
5580type codeBoxAssistant struct {
5681 curCode string
5782 setCode func (any )
@@ -64,13 +89,14 @@ func (cba *codeBoxAssistant) onInput(e *js.Object) {
6489}
6590
6691func (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
156321func (cba * codeBoxAssistant ) getSelection () (int , int ) {
0 commit comments