Skip to content

Commit bdff696

Browse files
committed
merge upstream/develop-codemirror-v6 to resolve conflicts
2 parents 34de741 + b5e6852 commit bdff696

File tree

20 files changed

+4900
-32571
lines changed

20 files changed

+4900
-32571
lines changed

client/modules/IDE/actions/ide.js

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -236,23 +236,10 @@ export function hideErrorModal() {
236236
};
237237
}
238238

239-
export function hideRuntimeErrorWarning() {
240-
return {
241-
type: ActionTypes.HIDE_RUNTIME_ERROR_WARNING
242-
};
243-
}
244-
245-
export function showRuntimeErrorWarning() {
246-
return {
247-
type: ActionTypes.SHOW_RUNTIME_ERROR_WARNING
248-
};
249-
}
250-
251239
export function startSketch() {
252240
return (dispatch, getState) => {
253241
dispatch(clearConsole());
254242
dispatch(startVisualSketch());
255-
dispatch(showRuntimeErrorWarning());
256243
const state = getState();
257244
dispatchMessage({
258245
type: MessageTypes.SKETCH,

client/modules/IDE/components/Editor/codemirror.js

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,9 @@ import {
1818
AUTOCOMPLETE_OPTIONS
1919
} from './stateUtils';
2020
import { useEffectWithComparison } from '../../hooks/custom-hooks';
21-
import tidyCodeWithPrettier from './tidier';
21+
import { tidyCodeWithPrettier } from './tidier';
2222

2323
// ----- GENERAL TODOS (in order of priority) -----
24-
// - color themes
2524
// - any features lost in the p5 conversion git merge
2625
// - javascript color picker (extension works for css but needs to be forked for js)
2726
// - revisit keymap differences, esp around sublime
@@ -32,13 +31,11 @@ import tidyCodeWithPrettier from './tidier';
3231

3332
/** This is a custom React hook that manages CodeMirror state. */
3433
export default function useCodeMirror({
35-
theme,
3634
lineNumbers,
3735
linewrap,
3836
autocloseBracketsQuotes,
3937
setUnsavedChanges,
4038
setCurrentLine,
41-
hideRuntimeErrorWarning,
4239
updateFileContent,
4340
file,
4441
files,
@@ -63,7 +60,6 @@ export default function useCodeMirror({
6360
// When the file changes, update the file content and save status.
6461
function onChange() {
6562
setUnsavedChanges(true);
66-
hideRuntimeErrorWarning();
6763
updateFileContent(fileId.current, cmView.current.state.doc.toString());
6864
if (autorefresh && isPlaying) {
6965
clearConsole();
@@ -102,7 +98,6 @@ export default function useCodeMirror({
10298
}
10399

104100
// When settings change, we pass those changes into CodeMirror.
105-
// TODO: There should be a useEffect hook for when the theme changes.
106101
useEffect(() => {
107102
cmView.current.dom.style['font-size'] = `${fontSize}px`;
108103
}, [fontSize]);
@@ -220,6 +215,7 @@ export default function useCodeMirror({
220215
teardownCodeMirror,
221216
getContent,
222217
tidyCode,
223-
showSearch
218+
showSearch,
219+
codemirrorView: cmView
224220
};
225221
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { StateField, StateEffect } from '@codemirror/state';
2+
import { EditorView, Decoration } from '@codemirror/view';
3+
4+
// Effects for communicating with the state field
5+
const ADD_ERROR_DECORATION = StateEffect.define();
6+
const FILTER_ERROR_DECORATION = StateEffect.define();
7+
8+
// An extension for managing error line decorations
9+
// Mostly adapted from the Marked Text demo in https://codemirror.net/docs/migration/
10+
// You can affect this by calling addErrorDecoration and removeErrorDecorations
11+
export const errorDecorationStateField = StateField.define({
12+
// Start with an empty set of decorations
13+
create() {
14+
return Decoration.none;
15+
},
16+
// This is called whenever the editor updates
17+
update(value, transaction) {
18+
// Move the decorations to account for document changes
19+
let newValue = value.map(transaction.changes);
20+
for (let i = 0; i < transaction.effects.length; i++) {
21+
const effect = transaction.effects[i];
22+
if (effect.is(ADD_ERROR_DECORATION))
23+
newValue = newValue.update({ add: effect.value, sort: true });
24+
else if (effect.is(FILTER_ERROR_DECORATION))
25+
newValue = newValue.update({ filter: effect.value });
26+
}
27+
return newValue;
28+
},
29+
// Indicate that this field provides a set of decorations
30+
provide: (f) => EditorView.decorations.from(f)
31+
});
32+
33+
const ERROR_DECORATION = Decoration.line({
34+
class: 'cm-errorLine' // Defined in _editor.scss
35+
});
36+
37+
// Add an error decoration to a specific line number
38+
export function addErrorDecoration(view, lineNumber) {
39+
const docLineNumber = view.state.doc.line(lineNumber);
40+
view.dispatch({
41+
effects: ADD_ERROR_DECORATION.of([
42+
ERROR_DECORATION.range(docLineNumber.from)
43+
])
44+
});
45+
}
46+
47+
// Remove all error decorations
48+
export function removeErrorDecorations(view) {
49+
view.dispatch({
50+
effects: FILTER_ERROR_DECORATION.of(() => false)
51+
});
52+
}

client/modules/IDE/components/Editor/index.jsx

Lines changed: 32 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -32,13 +32,16 @@ import { FolderIcon } from '../../../../common/icons';
3232
import { IconButton } from '../../../../common/IconButton';
3333

3434
import useCodeMirror from './codemirror';
35-
import { useEffectWithComparison } from '../../hooks/custom-hooks';
35+
36+
import {
37+
addErrorDecoration,
38+
removeErrorDecorations
39+
} from './consoleErrorDecoration';
3640

3741
function Editor({
3842
provideController,
3943
files,
4044
file,
41-
theme,
4245
linewrap,
4346
lineNumbers,
4447
closeProjectOptions,
@@ -57,8 +60,6 @@ function Editor({
5760
autocloseBracketsQuotes,
5861
fontSize,
5962
consoleEvents,
60-
hideRuntimeErrorWarning,
61-
runtimeErrorWarningVisible,
6263
expandConsole,
6364
isExpanded,
6465
t,
@@ -85,17 +86,15 @@ function Editor({
8586
const {
8687
setupCodeMirrorOnContainerMounted,
8788
teardownCodeMirror,
88-
// cmInstance,
89+
codemirrorView,
8990
getContent,
9091
tidyCode,
9192
showSearch
9293
} = useCodeMirror({
93-
theme,
9494
lineNumbers,
9595
linewrap,
9696
autocloseBracketsQuotes,
9797
setUnsavedChanges,
98-
hideRuntimeErrorWarning,
9998
updateFileContent,
10099
file,
101100
files,
@@ -132,54 +131,32 @@ function Editor({
132131
};
133132
}, []);
134133

135-
// Updates the error console.
136-
// TODO: Need to revisit this functionality for v6.
137-
useEffectWithComparison(
138-
(_, prevProps) => {
139-
if (runtimeErrorWarningVisible) {
140-
if (
141-
prevProps.consoleEvents &&
142-
consoleEvents.length !== prevProps.consoleEvents.length
143-
) {
144-
consoleEvents.forEach((consoleEvent) => {
145-
if (consoleEvent.method === 'error') {
146-
// It doesn't work if you create a new Error, but this works
147-
// LOL
148-
const errorObj = { stack: consoleEvent.data[0].toString() };
149-
StackTrace.fromError(errorObj).then((stackLines) => {
150-
expandConsole();
151-
const line = stackLines.find(
152-
(l) => l.fileName && l.fileName.startsWith('/')
153-
);
154-
if (!line) return;
155-
const fileNameArray = line.fileName.split('/');
156-
const fileName = fileNameArray.slice(-1)[0];
157-
const filePath = fileNameArray.slice(0, -1).join('/');
158-
const fileWithError = files.find(
159-
(f) => f.name === fileName && f.filePath === filePath
160-
);
161-
setSelectedFile(fileWithError.id);
162-
// cmInstance.current.addLineClass(
163-
// line.lineNumber - 1,
164-
// 'background',
165-
// 'line-runtime-error'
166-
// );
167-
});
168-
}
169-
});
170-
} else {
171-
// for (let i = 0; i < cmInstance.current.lineCount(); i += 1) {
172-
// cmInstance.current.removeLineClass(
173-
// i,
174-
// 'background',
175-
// 'line-runtime-error'
176-
// );
177-
// }
178-
}
179-
}
180-
},
181-
[consoleEvents, runtimeErrorWarningVisible]
182-
);
134+
// Updates the runtime error console.
135+
useEffect(() => {
136+
const consoleErrors = consoleEvents.filter((e) => e.method === 'error');
137+
138+
if (consoleErrors.length > 0) {
139+
const firstError = consoleErrors[0];
140+
const errorObj = { stack: firstError.data[0].toString() };
141+
StackTrace.fromError(errorObj).then((stackLines) => {
142+
expandConsole();
143+
const line = stackLines.find(
144+
(l) => l.fileName && l.fileName.startsWith('/')
145+
);
146+
if (!line) return;
147+
const fileNameArray = line.fileName.split('/');
148+
const fileName = fileNameArray.slice(-1)[0];
149+
const filePath = fileNameArray.slice(0, -1).join('/');
150+
const fileWithError = files.find(
151+
(f) => f.name === fileName && f.filePath === filePath
152+
);
153+
setSelectedFile(fileWithError.id);
154+
addErrorDecoration(codemirrorView.current, line.lineNumber);
155+
});
156+
} else {
157+
removeErrorDecorations(codemirrorView.current);
158+
}
159+
}, [consoleEvents]);
183160

184161
const editorSectionClass = classNames({
185162
editor: true,
@@ -291,7 +268,6 @@ Editor.propTypes = {
291268
startSketch: PropTypes.func.isRequired,
292269
autorefresh: PropTypes.bool.isRequired,
293270
isPlaying: PropTypes.bool.isRequired,
294-
theme: PropTypes.string.isRequired,
295271
files: PropTypes.arrayOf(
296272
PropTypes.shape({
297273
id: PropTypes.string.isRequired,
@@ -304,8 +280,6 @@ Editor.propTypes = {
304280
closeProjectOptions: PropTypes.func.isRequired,
305281
expandSidebar: PropTypes.func.isRequired,
306282
clearConsole: PropTypes.func.isRequired,
307-
hideRuntimeErrorWarning: PropTypes.func.isRequired,
308-
runtimeErrorWarningVisible: PropTypes.bool.isRequired,
309283
provideController: PropTypes.func.isRequired,
310284
t: PropTypes.func.isRequired,
311285
setSelectedFile: PropTypes.func.isRequired,
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import { StateField, RangeSetBuilder } from '@codemirror/state';
2+
import { Decoration, EditorView, WidgetType } from '@codemirror/view';
3+
import { selectedCompletion, completionStatus } from '@codemirror/autocomplete';
4+
5+
class GhostTextWidget extends WidgetType {
6+
constructor(text) {
7+
super();
8+
this.text = text;
9+
}
10+
11+
eq(other) {
12+
return other.text === this.text;
13+
}
14+
15+
toDOM() {
16+
const span = document.createElement('span');
17+
span.className = 'cm-ghostCompletion';
18+
span.textContent = this.text;
19+
return span;
20+
}
21+
22+
ignoreEvent() {
23+
return true;
24+
}
25+
}
26+
27+
function getCurrentWord(state) {
28+
const { from, to, empty } = state.selection.main;
29+
if (!empty) return null;
30+
31+
const line = state.doc.lineAt(from);
32+
const before = line.text.slice(0, from - line.from);
33+
const match = before.match(/\w+$/);
34+
35+
if (!match) return null;
36+
37+
const word = match[0];
38+
return {
39+
text: word,
40+
from: from - word.length,
41+
to
42+
};
43+
}
44+
45+
function buildGhostText(state) {
46+
// only show ghost text if autocomplete is on,
47+
// user is typing, and if preview matches typed text
48+
49+
if (completionStatus(state) !== 'active') return null;
50+
51+
const selected = selectedCompletion(state);
52+
if (!selected) return null;
53+
54+
const word = getCurrentWord(state);
55+
if (!word) return null;
56+
57+
const preview = selected.preview || selected.label;
58+
if (!preview) return null;
59+
60+
if (!preview.toLowerCase().startsWith(word.text.toLowerCase())) return null;
61+
62+
const remainder = preview.slice(word.text.length);
63+
if (!remainder) return null;
64+
65+
return {
66+
pos: word.to,
67+
text: remainder
68+
};
69+
}
70+
71+
const ghostTextField = StateField.define({
72+
create(state) {
73+
return Decoration.none;
74+
},
75+
76+
update(deco, tr) {
77+
const decorationBuilder = new RangeSetBuilder();
78+
const ghost = buildGhostText(tr.state);
79+
80+
if (ghost) {
81+
decorationBuilder.add(
82+
ghost.pos,
83+
ghost.pos,
84+
Decoration.widget({
85+
widget: new GhostTextWidget(ghost.text),
86+
side: 1
87+
})
88+
);
89+
}
90+
91+
return decorationBuilder.finish();
92+
},
93+
94+
provide: (field) => EditorView.decorations.from(field)
95+
});
96+
97+
export const p5CompletionPreviewTheme = EditorView.theme({
98+
'.cm-ghostCompletion': {
99+
opacity: '0.55',
100+
fontStyle: 'italic',
101+
pointerEvents: 'none',
102+
whiteSpace: 'pre'
103+
}
104+
});
105+
106+
export function p5CompletionPreview() {
107+
return [ghostTextField, p5CompletionPreviewTheme];
108+
}

0 commit comments

Comments
 (0)