11import { 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';
2610
2711import { debounce } from 'lodash' ;
28- import emmet from '@emmetio/codemirror-plugin' ;
2912
13+ import {
14+ getFileMode ,
15+ createNewFileState ,
16+ updateFileStates
17+ } from './stateUtils' ;
3018import { 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. */
4437export default function useCodeMirror ( {
4538 theme,
4639 lineNumbers,
@@ -61,34 +54,9 @@ export default function useCodeMirror({
6154 onUpdateLinting
6255} ) {
6356 // The codemirror instance.
64- const cmInstance = useRef ( ) ;
57+ const cmView = useRef ( ) ;
6558 // 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 ( ) ;
9260
9361 // We have to create a ref for the file ID, or else the debouncer
9462 // will old onto an old version of the fileId and just overrwrite the initial file.
@@ -99,118 +67,96 @@ export default function useCodeMirror({
9967 function onChange ( ) {
10068 setUnsavedChanges ( true ) ;
10169 hideRuntimeErrorWarning ( ) ;
102- updateFileContent ( fileId . current , cmInstance . current . getValue ( ) ) ;
70+ updateFileContent ( fileId . current , cmView . current . state . doc . toString ( ) ) ;
10371 if ( autorefresh && isPlaying ) {
10472 clearConsole ( ) ;
10573 startSketch ( ) ;
10674 }
10775 }
76+ // Call onChange at most once every second.
10877 const debouncedOnChange = debounce ( onChange , 1000 ) ;
10978
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+
11091 // When the container component enters the DOM, we want this function
11192 // to be called so we can setup the CodeMirror instance with the container.
11293 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
14696 } ) ;
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` ;
18797 }
18898
18999 // When settings change, we pass those changes into CodeMirror.
100+ // TODO: There should be a useEffect hook for when the theme changes.
190101 useEffect ( ( ) => {
191- cmInstance . current . getWrapperElement ( ) . style [ 'font-size' ] = `${ fontSize } px` ;
102+ cmView . current . dom . style [ 'font-size' ] = `${ fontSize } px` ;
192103 } , [ fontSize ] ) ;
193104 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+ } ) ;
195115 } , [ linewrap ] ) ;
196116 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+ } ) ;
201125 } , [ lineNumbers ] ) ;
202126 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+ } ) ;
204137 } , [ autocloseBracketsQuotes ] ) ;
205138
206- // Initializes the files as CodeMirror documents .
139+ // Initializes the files as CodeMirror states .
207140 function initializeDocuments ( ) {
208- docs . current = { } ;
141+ if ( ! fileStates . current ) {
142+ fileStates . current = { } ;
143+ }
144+
209145 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 ,
212152 currentFile . content ,
213- getFileMode ( currentFile . name )
153+ {
154+ linewrap,
155+ lineNumbers,
156+ autocloseBracketsQuotes,
157+ onUpdateLinting,
158+ onViewUpdate
159+ }
214160 ) ;
215161 }
216162 } ) ;
@@ -219,64 +165,47 @@ export default function useCodeMirror({
219165 // When the files change, reinitialize the documents.
220166 useEffect ( initializeDocuments , [ files ] ) ;
221167
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.
224169 useEffectWithComparison (
225170 ( _ , 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 ;
239177 }
240- cmInstance . current . focus ( ) ;
241178
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 ) ;
249181 } ,
250182 [ file . id ]
251183 ) ;
252184
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-
260185 const getContent = ( ) => {
261- const content = cmInstance . current . getValue ( ) ;
186+ const content = cmView . current . state . doc . toString ( ) ;
262187 const updatedFile = Object . assign ( { } , file , { content } ) ;
263188 return updatedFile ;
264189 } ;
265190
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 ) ;
272202 } ;
273203
274204 return {
275205 setupCodeMirrorOnContainerMounted,
276- teardownCodeMirror,
277- cmInstance,
278206 getContent,
279- showFind,
280- showReplace
207+ tidyCode
208+ // showFind,
209+ // showReplace
281210 } ;
282211}
0 commit comments