@@ -6,7 +6,7 @@ import React, {
66 useMemo
77} from 'react' ;
88import PropTypes from 'prop-types' ;
9- import { EditorState , StateEffect , Prec } from '@codemirror/state' ;
9+ import { EditorState , StateEffect , Prec , StateField } from '@codemirror/state' ;
1010import {
1111 EditorView ,
1212 keymap ,
@@ -22,6 +22,10 @@ import { bracketMatching, foldGutter } from '@codemirror/language';
2222import { autocompletion , closeBrackets } from '@codemirror/autocomplete' ;
2323import { linter , lintGutter } from '@codemirror/lint' ;
2424import { standardKeymap } from '@codemirror/commands' ;
25+ import {
26+ colorPicker ,
27+ wrapperClassName
28+ } from '@replit/codemirror-css-color-picker' ;
2529
2630import prettier from 'prettier/standalone' ;
2731import 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
5154import classNames from 'classnames' ;
5255import 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
133146const 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
721755const mapStateToProps = ( state ) => ( {
0 commit comments