1
1
import { useRef , useEffect } from 'react' ;
2
- import CodeMirror from 'codemirror' ;
3
- import 'codemirror/mode/css/css' ;
4
- import 'codemirror/mode/clike/clike' ;
5
- import 'codemirror/addon/selection/active-line' ;
6
- import 'codemirror/addon/lint/lint' ;
7
- import 'codemirror/addon/lint/javascript-lint' ;
8
- import 'codemirror/addon/lint/css-lint' ;
9
- import 'codemirror/addon/lint/html-lint' ;
10
- import 'codemirror/addon/fold/brace-fold' ;
11
- import 'codemirror/addon/fold/comment-fold' ;
12
- import 'codemirror/addon/fold/foldcode' ;
13
- import 'codemirror/addon/fold/foldgutter' ;
14
- import 'codemirror/addon/fold/indent-fold' ;
15
- import 'codemirror/addon/fold/xml-fold' ;
16
- import 'codemirror/addon/comment/comment' ;
17
- import 'codemirror/keymap/sublime' ;
18
- import 'codemirror/addon/search/searchcursor' ;
19
- import 'codemirror/addon/search/matchesonscrollbar' ;
20
- import 'codemirror/addon/search/match-highlighter' ;
21
- import 'codemirror/addon/search/jump-to-line' ;
22
- import 'codemirror/addon/edit/matchbrackets' ;
23
- import 'codemirror/addon/edit/closebrackets' ;
24
- import 'codemirror/addon/selection/mark-selection' ;
25
- import 'codemirror-colorpicker' ;
2
+ import { EditorView , lineNumbers as lineNumbersExt } from '@codemirror/view' ;
3
+ import { closeBrackets } from '@codemirror/autocomplete' ;
4
+
5
+ // TODO: Check what the v6 variants of these addons are.
6
+ // import 'codemirror/addon/search/searchcursor';
7
+ // import 'codemirror/addon/search/matchesonscrollbar';
8
+ // import 'codemirror/addon/search/match-highlighter';
9
+ // import 'codemirror/addon/search/jump-to-line';
26
10
27
11
import { debounce } from 'lodash' ;
28
- import emmet from '@emmetio/codemirror-plugin' ;
29
12
13
+ import {
14
+ getFileMode ,
15
+ createNewFileState ,
16
+ updateFileStates
17
+ } from './stateUtils' ;
30
18
import { useEffectWithComparison } from '../../hooks/custom-hooks' ;
31
- import { metaKey } from '../../../../utils/metaKey' ;
32
- import { showHint } from './hinter' ;
33
- import tidyCode from './tidier' ;
34
- import getFileMode from './utils' ;
35
-
36
- const INDENTATION_AMOUNT = 2 ;
37
-
38
- emmet ( CodeMirror ) ;
39
-
40
- /**
41
- * This is a custom React hook that manages CodeMirror state.
42
- * TODO(Connie Ye): Revisit the linting on file switch.
43
- */
19
+ import tidyCodeWithPrettier from './tidier' ;
20
+
21
+ // ----- GENERAL TODOS (in order of priority) -----
22
+ // - autocomplete (hinter)
23
+ // - p5-javascript
24
+ // - search, find & replace
25
+ // - color themes
26
+ // - javascript color picker (extension works for css but needs to be forked for js)
27
+ // - revisit keymap differences, esp around sublime
28
+ // - emmet doesn't trigger if text is copy pasted in
29
+ // - need to re-implement emmet auto rename tag
30
+ // - color picker should be triggered by metakey cmd k
31
+ // - clike addon
32
+ // ----- QUESTIONS -----
33
+ // do we want shift tab to indent less? existing behavior is explicitly turned off but i think its nice to have
34
+ // do we want any extra emmet functionality? https://www.npmjs.com/package/@emmetio/codemirror6-plugin
35
+
36
+ /** This is a custom React hook that manages CodeMirror state. */
44
37
export default function useCodeMirror ( {
45
38
theme,
46
39
lineNumbers,
@@ -61,34 +54,9 @@ export default function useCodeMirror({
61
54
onUpdateLinting
62
55
} ) {
63
56
// The codemirror instance.
64
- const cmInstance = useRef ( ) ;
57
+ const cmView = useRef ( ) ;
65
58
// The current codemirror files.
66
- const docs = useRef ( ) ;
67
-
68
- function onKeyUp ( ) {
69
- const lineNumber = parseInt ( cmInstance . current . getCursor ( ) . line + 1 , 10 ) ;
70
- setCurrentLine ( lineNumber ) ;
71
- }
72
-
73
- function onKeyDown ( _cm , e ) {
74
- // Show hint
75
- const mode = cmInstance . current . getOption ( 'mode' ) ;
76
- if ( / ^ [ a - z ] $ / i. test ( e . key ) && ( mode === 'css' || mode === 'javascript' ) ) {
77
- showHint ( _cm , autocompleteHinter , fontSize ) ;
78
- }
79
- if ( e . key === 'Escape' ) {
80
- e . preventDefault ( ) ;
81
- const selections = cmInstance . current . listSelections ( ) ;
82
-
83
- if ( selections . length > 1 ) {
84
- const firstPos = selections [ 0 ] . head || selections [ 0 ] . anchor ;
85
- cmInstance . current . setSelection ( firstPos ) ;
86
- cmInstance . current . scrollIntoView ( firstPos ) ;
87
- } else {
88
- cmInstance . current . getInputField ( ) . blur ( ) ;
89
- }
90
- }
91
- }
59
+ const fileStates = useRef ( ) ;
92
60
93
61
// We have to create a ref for the file ID, or else the debouncer
94
62
// will old onto an old version of the fileId and just overrwrite the initial file.
@@ -99,118 +67,96 @@ export default function useCodeMirror({
99
67
function onChange ( ) {
100
68
setUnsavedChanges ( true ) ;
101
69
hideRuntimeErrorWarning ( ) ;
102
- updateFileContent ( fileId . current , cmInstance . current . getValue ( ) ) ;
70
+ updateFileContent ( fileId . current , cmView . current . state . doc . toString ( ) ) ;
103
71
if ( autorefresh && isPlaying ) {
104
72
clearConsole ( ) ;
105
73
startSketch ( ) ;
106
74
}
107
75
}
76
+ // Call onChange at most once every second.
108
77
const debouncedOnChange = debounce ( onChange , 1000 ) ;
109
78
79
+ // This is called when the CM view updates.
80
+ function onViewUpdate ( updateView ) {
81
+ const { state } = updateView ;
82
+
83
+ // TODO - check if need to subtract one
84
+ setCurrentLine ( state . doc . lineAt ( state . selection . main . head ) . number ) ;
85
+
86
+ if ( updateView . docChanged ) {
87
+ debouncedOnChange ( ) ;
88
+ }
89
+ }
90
+
110
91
// When the container component enters the DOM, we want this function
111
92
// to be called so we can setup the CodeMirror instance with the container.
112
93
function setupCodeMirrorOnContainerMounted ( container ) {
113
- cmInstance . current = CodeMirror ( container , {
114
- theme : `p5-${ theme } ` ,
115
- lineNumbers,
116
- styleActiveLine : true ,
117
- inputStyle : 'contenteditable' ,
118
- lineWrapping : linewrap ,
119
- fixedGutter : false ,
120
- foldGutter : true ,
121
- foldOptions : { widget : '\u2026' } ,
122
- gutters : [ 'CodeMirror-foldgutter' , 'CodeMirror-lint-markers' ] ,
123
- keyMap : 'sublime' ,
124
- highlightSelectionMatches : true , // highlight current search match
125
- matchBrackets : true ,
126
- emmet : {
127
- preview : [ 'html' ] ,
128
- markTagPairs : true ,
129
- autoRenameTags : true
130
- } ,
131
- autoCloseBrackets : autocloseBracketsQuotes ,
132
- styleSelectedText : true ,
133
- lint : {
134
- onUpdateLinting,
135
- options : {
136
- asi : true ,
137
- eqeqeq : false ,
138
- '-W041' : false ,
139
- esversion : 11
140
- }
141
- } ,
142
- colorpicker : {
143
- type : 'sketch' ,
144
- mode : 'edit'
145
- }
94
+ cmView . current = new EditorView ( {
95
+ parent : container
146
96
} ) ;
147
-
148
- delete cmInstance . current . options . lint . options . errors ;
149
-
150
- const replaceCommand =
151
- metaKey === 'Ctrl' ? `${ metaKey } -H` : `${ metaKey } -Option-F` ;
152
- cmInstance . current . setOption ( 'extraKeys' , {
153
- Tab : ( tabCm ) => {
154
- if ( ! tabCm . execCommand ( 'emmetExpandAbbreviation' ) ) return ;
155
- // might need to specify and indent more?
156
- const selection = tabCm . doc . getSelection ( ) ;
157
- if ( selection . length > 0 ) {
158
- tabCm . execCommand ( 'indentMore' ) ;
159
- } else {
160
- tabCm . replaceSelection ( ' ' . repeat ( INDENTATION_AMOUNT ) ) ;
161
- }
162
- } ,
163
- Enter : 'emmetInsertLineBreak' ,
164
- Esc : 'emmetResetAbbreviation' ,
165
- [ `Shift-Tab` ] : false ,
166
- [ `${ metaKey } -Enter` ] : ( ) => null ,
167
- [ `Shift-${ metaKey } -Enter` ] : ( ) => null ,
168
- [ `${ metaKey } -F` ] : 'findPersistent' ,
169
- [ `Shift-${ metaKey } -F` ] : ( ) => tidyCode ( cmInstance . current ) ,
170
- [ `${ metaKey } -G` ] : 'findPersistentNext' ,
171
- [ `Shift-${ metaKey } -G` ] : 'findPersistentPrev' ,
172
- [ replaceCommand ] : 'replace' ,
173
- // Cassie Tarakajian: If you don't set a default color, then when you
174
- // choose a color, it deletes characters inline. This is a
175
- // hack to prevent that.
176
- [ `${ metaKey } -K` ] : ( metaCm , event ) =>
177
- metaCm . state . colorpicker . popup_color_picker ( { length : 0 } ) ,
178
- [ `${ metaKey } -.` ] : 'toggleComment' // Note: most adblockers use the shortcut ctrl+.
179
- } ) ;
180
-
181
- // Setup the event listeners on the CodeMirror instance.
182
- cmInstance . current . on ( 'change' , debouncedOnChange ) ;
183
- cmInstance . current . on ( 'keyup' , onKeyUp ) ;
184
- cmInstance . current . on ( 'keydown' , onKeyDown ) ;
185
-
186
- cmInstance . current . getWrapperElement ( ) . style [ 'font-size' ] = `${ fontSize } px` ;
187
97
}
188
98
189
99
// When settings change, we pass those changes into CodeMirror.
100
+ // TODO: There should be a useEffect hook for when the theme changes.
190
101
useEffect ( ( ) => {
191
- cmInstance . current . getWrapperElement ( ) . style [ 'font-size' ] = `${ fontSize } px` ;
102
+ cmView . current . dom . style [ 'font-size' ] = `${ fontSize } px` ;
192
103
} , [ fontSize ] ) ;
193
104
useEffect ( ( ) => {
194
- cmInstance . current . setOption ( 'lineWrapping' , linewrap ) ;
105
+ const reconfigureEffect = ( fileState ) =>
106
+ fileState . lineWrappingCpt . reconfigure (
107
+ linewrap ? EditorView . lineWrapping : [ ]
108
+ ) ;
109
+ updateFileStates ( {
110
+ fileStates : fileStates . current ,
111
+ cmView : cmView . current ,
112
+ file,
113
+ reconfigureEffect
114
+ } ) ;
195
115
} , [ linewrap ] ) ;
196
116
useEffect ( ( ) => {
197
- cmInstance . current . setOption ( 'theme' , `p5-${ theme } ` ) ;
198
- } , [ theme ] ) ;
199
- useEffect ( ( ) => {
200
- cmInstance . current . setOption ( 'lineNumbers' , lineNumbers ) ;
117
+ const reconfigureEffect = ( fileState ) =>
118
+ fileState . lineNumbersCpt . reconfigure ( lineNumbers ? lineNumbersExt ( ) : [ ] ) ;
119
+ updateFileStates ( {
120
+ fileStates : fileStates . current ,
121
+ cmView : cmView . current ,
122
+ file,
123
+ reconfigureEffect
124
+ } ) ;
201
125
} , [ lineNumbers ] ) ;
202
126
useEffect ( ( ) => {
203
- cmInstance . current . setOption ( 'autoCloseBrackets' , autocloseBracketsQuotes ) ;
127
+ const reconfigureEffect = ( fileState ) =>
128
+ fileState . closeBracketsCpt . reconfigure (
129
+ autocloseBracketsQuotes ? closeBrackets ( ) : [ ]
130
+ ) ;
131
+ updateFileStates ( {
132
+ fileStates : fileStates . current ,
133
+ cmView : cmView . current ,
134
+ file,
135
+ reconfigureEffect
136
+ } ) ;
204
137
} , [ autocloseBracketsQuotes ] ) ;
205
138
206
- // Initializes the files as CodeMirror documents .
139
+ // Initializes the files as CodeMirror states .
207
140
function initializeDocuments ( ) {
208
- docs . current = { } ;
141
+ if ( ! fileStates . current ) {
142
+ fileStates . current = { } ;
143
+ }
144
+
209
145
files . forEach ( ( currentFile ) => {
210
- if ( currentFile . name !== 'root' ) {
211
- docs . current [ currentFile . id ] = CodeMirror . Doc (
146
+ if (
147
+ currentFile . name !== 'root' &&
148
+ ! ( currentFile . id in fileStates . current )
149
+ ) {
150
+ fileStates . current [ currentFile . id ] = createNewFileState (
151
+ currentFile . name ,
212
152
currentFile . content ,
213
- getFileMode ( currentFile . name )
153
+ {
154
+ linewrap,
155
+ lineNumbers,
156
+ autocloseBracketsQuotes,
157
+ onUpdateLinting,
158
+ onViewUpdate
159
+ }
214
160
) ;
215
161
}
216
162
} ) ;
@@ -219,64 +165,47 @@ export default function useCodeMirror({
219
165
// When the files change, reinitialize the documents.
220
166
useEffect ( initializeDocuments , [ files ] ) ;
221
167
222
- // When the file changes, we change the file mode and
223
- // make the CodeMirror call to swap out the document.
168
+ // When the file changes, make the CodeMirror call to swap out the document.
224
169
useEffectWithComparison (
225
170
( _ , prevProps ) => {
226
- const fileMode = getFileMode ( file . name ) ;
227
- if ( fileMode === 'javascript' ) {
228
- // Define the new Emmet configuration based on the file mode
229
- const emmetConfig = {
230
- preview : [ 'html' ] ,
231
- markTagPairs : false ,
232
- autoRenameTags : true
233
- } ;
234
- cmInstance . current . setOption ( 'emmet' , emmetConfig ) ;
235
- }
236
- const oldDoc = cmInstance . current . swapDoc ( docs . current [ file . id ] ) ;
237
- if ( prevProps ?. file ) {
238
- docs . current [ prevProps . file . id ] = oldDoc ;
171
+ // We need to save the previous CodeMirror state so we can restore it
172
+ // when we switch back to it.
173
+ const previousState = cmView . current . state ;
174
+ if ( Array . isArray ( prevProps ) && prevProps . length > 0 && previousState ) {
175
+ const prevId = prevProps [ 0 ] ;
176
+ fileStates . current [ prevId ] . cmState = previousState ;
239
177
}
240
- cmInstance . current . focus ( ) ;
241
178
242
- for ( let i = 0 ; i < cmInstance . current . lineCount ( ) ; i += 1 ) {
243
- cmInstance . current . removeLineClass (
244
- i ,
245
- 'background' ,
246
- 'line-runtime-error'
247
- ) ;
248
- }
179
+ const { cmState } = fileStates . current [ file . id ] ;
180
+ cmView . current . setState ( cmState ) ;
249
181
} ,
250
182
[ file . id ]
251
183
) ;
252
184
253
- // Remove the CM listeners on component teardown.
254
- function teardownCodeMirror ( ) {
255
- cmInstance . current . off ( 'keyup' , onKeyUp ) ;
256
- cmInstance . current . off ( 'change' , debouncedOnChange ) ;
257
- cmInstance . current . off ( 'keydown' , onKeyDown ) ;
258
- }
259
-
260
185
const getContent = ( ) => {
261
- const content = cmInstance . current . getValue ( ) ;
186
+ const content = cmView . current . state . doc . toString ( ) ;
262
187
const updatedFile = Object . assign ( { } , file , { content } ) ;
263
188
return updatedFile ;
264
189
} ;
265
190
266
- const showFind = ( ) => {
267
- cmInstance . current . execCommand ( 'findPersistent' ) ;
268
- } ;
269
-
270
- const showReplace = ( ) => {
271
- cmInstance . current . execCommand ( 'replace' ) ;
191
+ // TODO: Add find and replace functionality.
192
+ // const showFind = () => {
193
+ // cmInstance.current.execCommand('findPersistent');
194
+ // };
195
+ // const showReplace = () => {
196
+ // cmInstance.current.execCommand('replace');
197
+ // };
198
+
199
+ const tidyCode = ( ) => {
200
+ const fileMode = getFileMode ( file . name ) ;
201
+ tidyCodeWithPrettier ( cmView . current , fileMode ) ;
272
202
} ;
273
203
274
204
return {
275
205
setupCodeMirrorOnContainerMounted,
276
- teardownCodeMirror,
277
- cmInstance,
278
206
getContent,
279
- showFind,
280
- showReplace
207
+ tidyCode
208
+ // showFind,
209
+ // showReplace
281
210
} ;
282
211
}
0 commit comments