Skip to content

Commit a44ad32

Browse files
committed
codemirror v5 -> v6
1 parent 9fd608d commit a44ad32

File tree

10 files changed

+1259
-566
lines changed

10 files changed

+1259
-566
lines changed
Lines changed: 121 additions & 192 deletions
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,39 @@
11
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';
2610

2711
import { debounce } from 'lodash';
28-
import emmet from '@emmetio/codemirror-plugin';
2912

13+
import {
14+
getFileMode,
15+
createNewFileState,
16+
updateFileStates
17+
} from './stateUtils';
3018
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. */
4437
export 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

Comments
 (0)