Skip to content

Commit 9fd608d

Browse files
authored
Merge pull request #3352 from processing/connie-editor-conversion-splitoff-codemirror
Convert Editor to functional, splits off CodeMirror code into its own file
2 parents 249e728 + 36c4129 commit 9fd608d

File tree

2 files changed

+500
-400
lines changed

2 files changed

+500
-400
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,282 @@
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';
26+
27+
import { debounce } from 'lodash';
28+
import emmet from '@emmetio/codemirror-plugin';
29+
30+
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+
*/
44+
export default function useCodeMirror({
45+
theme,
46+
lineNumbers,
47+
linewrap,
48+
autocloseBracketsQuotes,
49+
setUnsavedChanges,
50+
setCurrentLine,
51+
hideRuntimeErrorWarning,
52+
updateFileContent,
53+
file,
54+
files,
55+
autorefresh,
56+
isPlaying,
57+
clearConsole,
58+
startSketch,
59+
autocompleteHinter,
60+
fontSize,
61+
onUpdateLinting
62+
}) {
63+
// The codemirror instance.
64+
const cmInstance = useRef();
65+
// 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+
}
92+
93+
// We have to create a ref for the file ID, or else the debouncer
94+
// will old onto an old version of the fileId and just overrwrite the initial file.
95+
const fileId = useRef();
96+
fileId.current = file.id;
97+
98+
// When the file changes, update the file content and save status.
99+
function onChange() {
100+
setUnsavedChanges(true);
101+
hideRuntimeErrorWarning();
102+
updateFileContent(fileId.current, cmInstance.current.getValue());
103+
if (autorefresh && isPlaying) {
104+
clearConsole();
105+
startSketch();
106+
}
107+
}
108+
const debouncedOnChange = debounce(onChange, 1000);
109+
110+
// When the container component enters the DOM, we want this function
111+
// to be called so we can setup the CodeMirror instance with the container.
112+
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+
}
146+
});
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+
}
188+
189+
// When settings change, we pass those changes into CodeMirror.
190+
useEffect(() => {
191+
cmInstance.current.getWrapperElement().style['font-size'] = `${fontSize}px`;
192+
}, [fontSize]);
193+
useEffect(() => {
194+
cmInstance.current.setOption('lineWrapping', linewrap);
195+
}, [linewrap]);
196+
useEffect(() => {
197+
cmInstance.current.setOption('theme', `p5-${theme}`);
198+
}, [theme]);
199+
useEffect(() => {
200+
cmInstance.current.setOption('lineNumbers', lineNumbers);
201+
}, [lineNumbers]);
202+
useEffect(() => {
203+
cmInstance.current.setOption('autoCloseBrackets', autocloseBracketsQuotes);
204+
}, [autocloseBracketsQuotes]);
205+
206+
// Initializes the files as CodeMirror documents.
207+
function initializeDocuments() {
208+
docs.current = {};
209+
files.forEach((currentFile) => {
210+
if (currentFile.name !== 'root') {
211+
docs.current[currentFile.id] = CodeMirror.Doc(
212+
currentFile.content,
213+
getFileMode(currentFile.name)
214+
);
215+
}
216+
});
217+
}
218+
219+
// When the files change, reinitialize the documents.
220+
useEffect(initializeDocuments, [files]);
221+
222+
// When the file changes, we change the file mode and
223+
// make the CodeMirror call to swap out the document.
224+
useEffectWithComparison(
225+
(_, 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;
239+
}
240+
cmInstance.current.focus();
241+
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+
}
249+
},
250+
[file.id]
251+
);
252+
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+
const getContent = () => {
261+
const content = cmInstance.current.getValue();
262+
const updatedFile = Object.assign({}, file, { content });
263+
return updatedFile;
264+
};
265+
266+
const showFind = () => {
267+
cmInstance.current.execCommand('findPersistent');
268+
};
269+
270+
const showReplace = () => {
271+
cmInstance.current.execCommand('replace');
272+
};
273+
274+
return {
275+
setupCodeMirrorOnContainerMounted,
276+
teardownCodeMirror,
277+
cmInstance,
278+
getContent,
279+
showFind,
280+
showReplace
281+
};
282+
}

0 commit comments

Comments
 (0)