Skip to content

Commit c70be62

Browse files
committed
add runtime error highlighting
1 parent fdeb7b7 commit c70be62

File tree

3 files changed

+151
-99
lines changed

3 files changed

+151
-99
lines changed

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

Lines changed: 133 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import React, {
66
useMemo
77
} from 'react';
88
import PropTypes from 'prop-types';
9-
import { EditorState, StateEffect, Prec } from '@codemirror/state';
9+
import { EditorState, StateEffect, Prec, StateField } from '@codemirror/state';
1010
import {
1111
EditorView,
1212
keymap,
@@ -22,6 +22,10 @@ import { bracketMatching, foldGutter } from '@codemirror/language';
2222
import { autocompletion, closeBrackets } from '@codemirror/autocomplete';
2323
import { linter, lintGutter } from '@codemirror/lint';
2424
import { standardKeymap } from '@codemirror/commands';
25+
import {
26+
colorPicker,
27+
wrapperClassName
28+
} from '@replit/codemirror-css-color-picker';
2529

2630
import prettier from 'prettier/standalone';
2731
import babelParser from 'prettier/parser-babel';
@@ -46,7 +50,6 @@ import {
4650
findPrevious,
4751
highlightSelectionMatches
4852
} from '@codemirror/search';
49-
import Pickr from '@simonwep/pickr';
5053

5154
import classNames from 'classnames';
5255
import StackTrace from 'stacktrace-js';
@@ -111,24 +114,34 @@ const createThemeExtension = (themeName) => {
111114
return EditorView.theme({}, { dark: themeName === 'dark', themeClass });
112115
};
113116

114-
const ColorPickerWidget = (color, onColorChange) => {
115-
const dom = document.createElement('input');
116-
dom.setAttribute('type', 'color');
117-
dom.setAttribute('value', color);
117+
// Create an effect to add or remove decorations
118+
const addDecorationEffect = StateEffect.define();
119+
const removeDecorationEffect = StateEffect.define();
120+
121+
// Define a StateField to manage decorations
122+
const decorationsField =
123+
StateField.define <
124+
DecorationSet >
125+
{
126+
create() {
127+
return Decoration.none;
128+
},
129+
update(decorations, tr) {
130+
let newDecorations = decorations; // Create a new variable to hold the updated decorations
118131

119-
dom.oninput = (e) => {
120-
onColorChange(e.target.value); // Handle color change
121-
};
132+
tr.effects.forEach((effect) => {
133+
if (effect.is(addDecorationEffect)) {
134+
newDecorations = newDecorations.update({ add: [effect.value] });
135+
}
136+
if (effect.is(removeDecorationEffect)) {
137+
newDecorations = Decoration.none;
138+
}
139+
});
122140

123-
return {
124-
toDOM() {
125-
return dom;
141+
return newDecorations; // Return the new variable instead of reassigning the parameter
126142
},
127-
ignoreEvent() {
128-
return false; // Ensures that the widget doesn't block editor interaction
129-
}
143+
provide: (f) => EditorView.decorations.from(f)
130144
};
131-
};
132145

133146
const Editor = (props) => {
134147
const {
@@ -152,6 +165,11 @@ const Editor = (props) => {
152165
lintWarning,
153166
theme,
154167
hideRuntimeErrorWarning,
168+
files,
169+
setSelectedFile,
170+
expandConsole,
171+
runtimeErrorWarningVisible,
172+
consoleEvents,
155173
autocompleteHinter
156174
} = props;
157175

@@ -187,20 +205,6 @@ const Editor = (props) => {
187205
const docsRef = useRef({}); // Store the documents in a ref
188206
const prevFileIdRef = useRef(null); // Store the previous file ID in a ref
189207

190-
const [decorations, setDecorations] = useState(Decoration.none);
191-
192-
// Function to open color picker
193-
const openColorPicker = (pos, initialColor) => {
194-
const colorPicker = Decoration.widget({
195-
widget: ColorPickerWidget(initialColor, (newColor) => {
196-
console.log('Selected color:', newColor);
197-
// You can apply the color to your code here
198-
}),
199-
side: 1 // Ensures it appears after the position
200-
}).range(pos);
201-
setDecorations(Decoration.set([colorPicker]));
202-
};
203-
204208
useEffect(() => {
205209
// Initialize the Fuse instance only when `hinter` changes
206210
if (hinter && hinter.p5Hinter) {
@@ -265,42 +269,6 @@ const Editor = (props) => {
265269
setCurrentLine(lineNumber);
266270
}, []);
267271

268-
// Initialize Pickr color picker
269-
// const initColorPicker = () => {
270-
// const pickr = Pickr.create({
271-
// el: '#color-picker', // ID for color picker container
272-
// theme: 'nano', // Color picker theme (can be adjusted)
273-
// components: {
274-
// // Define components of the color picker
275-
// preview: true,
276-
// opacity: true,
277-
// hue: true,
278-
// interaction: {
279-
// hex: true,
280-
// rgba: true,
281-
// hsla: true,
282-
// input: true,
283-
// clear: true,
284-
// save: true
285-
// }
286-
// }
287-
// });
288-
289-
// // Listen for color changes
290-
// pickr.on('save', (color) => {
291-
// const colorHex = color.toHEXA().toString();
292-
// const view = viewRef.current;
293-
// if (view) {
294-
// view.dispatch({
295-
// changes: { from: view.state.selection.main.head, insert: colorHex }
296-
// });
297-
// }
298-
// });
299-
// pickr.hide();
300-
301-
// pickrRef.current = pickr;
302-
// };
303-
304272
const customLinterFunction = (view) => {
305273
const diagnostics = [];
306274
const content = view.state.doc.toString(); // Get the content of the editor
@@ -331,13 +299,6 @@ const Editor = (props) => {
331299
return diagnostics;
332300
};
333301

334-
// Open the color picker when `MetaKey + K` is pressed
335-
// const openColorPicker = () => {
336-
// if (pickrRef.current) {
337-
// pickrRef.current.show(); // Show the color picker programmatically
338-
// }
339-
// };
340-
341302
const triggerFindPersistent = () => {
342303
const view = viewRef.current;
343304
if (view) {
@@ -361,6 +322,7 @@ const Editor = (props) => {
361322
}
362323
}
363324
},
325+
// TODO: test emmet
364326
// {
365327
// key: 'Enter',
366328
// run: emmetInsert, // Run Emmet insert on Enter key
@@ -401,11 +363,8 @@ const Editor = (props) => {
401363
run: replaceCommand
402364
},
403365
{
404-
key: `Mod-k`, // Meta + K to trigger color picker
405-
run: () => {
406-
openColorPicker(5, '#ff0000'); // Trigger the color picker
407-
return true; // Prevent default behavior
408-
}
366+
key: `Mod-k`, // TODO: need to find a way to create custom color picker since codemirror 6 doesn't have one
367+
run: () => null
409368
}
410369
]);
411370

@@ -519,14 +478,18 @@ const Editor = (props) => {
519478
}),
520479
EditorView.lineWrapping,
521480
hintExtension,
522-
EditorView.decorations.compute([decorations], (state) => decorations)
481+
// colorPicker,
482+
EditorView.theme({
483+
[`.${wrapperClassName}`]: {
484+
outlineColor: 'transparent'
485+
}
486+
})
487+
// decorationsField
523488
];
524489

525490
useEffect(() => {
526491
if (!editorRef.current) return;
527492

528-
// if (!pickrRef.current) initColorPicker();
529-
530493
const startState = EditorState.create({
531494
doc: file.content,
532495
extensions: [...getCommonExtensions(), createThemeExtension(theme)]
@@ -577,6 +540,77 @@ const Editor = (props) => {
577540
};
578541
}, [fuseRef, pickrRef]);
579542

543+
const prevConsoleEventsLengthRef = useRef(consoleEvents.length);
544+
const errorDecoration = useRef([]);
545+
546+
// Effect to handle runtime error highlighting
547+
useEffect(() => {
548+
if (runtimeErrorWarningVisible) {
549+
const prevConsoleEventsLength = prevConsoleEventsLengthRef.current;
550+
551+
if (consoleEvents.length !== prevConsoleEventsLength) {
552+
// Process new console events
553+
consoleEvents.forEach((consoleEvent) => {
554+
if (consoleEvent.method === 'error') {
555+
const errorObj = { stack: consoleEvent.data[0].toString() };
556+
557+
StackTrace.fromError(errorObj).then((stackLines) => {
558+
expandConsole();
559+
const line = stackLines.find(
560+
(l) => l.fileName && l.fileName.startsWith('/')
561+
);
562+
if (!line) return;
563+
564+
const fileNameArray = line.fileName.split('/');
565+
const fileName = fileNameArray.slice(-1)[0];
566+
const filePath = fileNameArray.slice(0, -1).join('/');
567+
568+
const fileWithError = files.find(
569+
(f) => f.name === fileName && f.filePath === filePath
570+
);
571+
if (fileWithError) {
572+
setSelectedFile(fileWithError.id);
573+
574+
// Create a line decoration for the error
575+
const decoration = Decoration.line({
576+
class: 'line-runtime-error'
577+
}).range(
578+
viewRef.current.state.doc.line(line.lineNumber - 1).from
579+
);
580+
581+
// Apply the decoration to highlight the line
582+
viewRef.current.dispatch({
583+
effects: StateEffect.appendConfig.of(
584+
EditorView.decorations.of(Decoration.set([decoration]))
585+
)
586+
});
587+
588+
// Store the decoration so it can be cleared later
589+
errorDecoration.current = Decoration.set([decoration]);
590+
}
591+
});
592+
}
593+
});
594+
} else if (errorDecoration.current) {
595+
viewRef.current.dispatch({
596+
effects: StateEffect.appendConfig.of(
597+
EditorView.decorations.of(Decoration.none)
598+
)
599+
});
600+
errorDecoration.current = Decoration.none; // Clear the stored decorations
601+
}
602+
603+
prevConsoleEventsLengthRef.current = consoleEvents.length;
604+
}
605+
}, [
606+
runtimeErrorWarningVisible,
607+
consoleEvents,
608+
files,
609+
expandConsole,
610+
setSelectedFile,
611+
viewRef
612+
]);
613+
580614
// Handle file changes
581615
useEffect(() => {
582616
if (!viewRef.current) return;
@@ -617,7 +651,7 @@ const Editor = (props) => {
617651
view.dispatch({
618652
effects: StateEffect.reconfigure.of([...getCommonExtensions(), newTheme])
619653
});
620-
}, [theme]);
654+
}, [props.file.content, theme]);
621655

622656
return (
623657
<section
@@ -675,12 +709,12 @@ Editor.propTypes = {
675709
id: PropTypes.number.isRequired
676710
})
677711
).isRequired,
678-
// consoleEvents: PropTypes.arrayOf(
679-
// PropTypes.shape({
680-
// method: PropTypes.string.isRequired,
681-
// args: PropTypes.arrayOf(PropTypes.string)
682-
// })
683-
// ).isRequired,
712+
consoleEvents: PropTypes.arrayOf(
713+
PropTypes.shape({
714+
method: PropTypes.string.isRequired,
715+
args: PropTypes.arrayOf(PropTypes.string)
716+
})
717+
).isRequired,
684718
updateLintMessage: PropTypes.func.isRequired,
685719
clearLintMessage: PropTypes.func.isRequired,
686720
updateFileContent: PropTypes.func.isRequired,
@@ -698,24 +732,24 @@ Editor.propTypes = {
698732
isPlaying: PropTypes.bool.isRequired,
699733
theme: PropTypes.string.isRequired,
700734
unsavedChanges: PropTypes.bool.isRequired,
701-
// files: PropTypes.arrayOf(
702-
// PropTypes.shape({
703-
// id: PropTypes.string.isRequired,
704-
// name: PropTypes.string.isRequired,
705-
// content: PropTypes.string.isRequired
706-
// })
707-
// ).isRequired,
735+
files: PropTypes.arrayOf(
736+
PropTypes.shape({
737+
id: PropTypes.string.isRequired,
738+
name: PropTypes.string.isRequired,
739+
content: PropTypes.string.isRequired
740+
})
741+
).isRequired,
708742
isExpanded: PropTypes.bool.isRequired,
709743
collapseSidebar: PropTypes.func.isRequired,
710744
// closeProjectOptions: PropTypes.func.isRequired,
711745
expandSidebar: PropTypes.func.isRequired,
712746
clearConsole: PropTypes.func.isRequired,
713747
hideRuntimeErrorWarning: PropTypes.func.isRequired,
714-
// runtimeErrorWarningVisible: PropTypes.bool.isRequired,
748+
runtimeErrorWarningVisible: PropTypes.bool.isRequired,
715749
provideController: PropTypes.func.isRequired,
716-
t: PropTypes.func.isRequired
717-
// setSelectedFile: PropTypes.func.isRequired,
718-
// expandConsole: PropTypes.func.isRequired
750+
t: PropTypes.func.isRequired,
751+
setSelectedFile: PropTypes.func.isRequired,
752+
expandConsole: PropTypes.func.isRequired
719753
};
720754

721755
const mapStateToProps = (state) => ({

package-lock.json

Lines changed: 17 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,7 @@
175175
"@redux-devtools/dock-monitor": "^3.0.1",
176176
"@redux-devtools/log-monitor": "^4.0.2",
177177
"@reduxjs/toolkit": "^1.9.3",
178+
"@replit/codemirror-css-color-picker": "^6.2.0",
178179
"@simonwep/pickr": "^1.9.1",
179180
"@uiw/react-codemirror": "^4.23.1",
180181
"async": "^3.2.3",

0 commit comments

Comments
 (0)