import React from "react"; import Editor from "@monaco-editor/react"; import { useEditorLanguageConfig } from "./use-editor-lang-config"; import { DocumentEdit, DocumentRange, MonacoTokensProvider, } from "../../languages/types"; import { createHighlightRange, createMonacoDocEdit, EditorInstance, MonacoInstance, } from "./monaco-utils"; import { useEditorBreakpoints } from "./use-editor-breakpoints"; import darkTheme from "./themes/dark.json"; import lightTheme from "./themes/light.json"; import { useDarkMode } from "../providers/dark-mode-provider"; import { WorkerParseError } from "../../languages/worker-errors"; import { useCodeValidator } from "./use-code-validator"; /** Keeps track of user's original program and modifications done to it */ type ChangeTrackerValue = { /** The user's original program code */ original: string; /** Tracks if code was modified during execution */ changed: boolean; }; // Interface for interacting with the editor export interface CodeEditorRef { /** Get the current text content of the editor */ getCode: () => string; /** Update value of code */ editCode: (edits: DocumentEdit[]) => void; /** Update code highlights */ updateHighlights: (highlights: DocumentRange | null) => void; /** Start execution mode - readonly editor with modifyable contents */ startExecutionMode: () => void; /** End execution mode - reset contents and readonly state */ endExecutionMode: () => void; } type Props = { /** ID of the active language */ languageId: string; /** Default code to display in editor */ defaultValue: string; /** Tokens provider for the language */ tokensProvider?: MonacoTokensProvider; /** Callback to validate code syntax */ onValidateCode: (code: string) => Promise; /** Callback to update debugging breakpoints */ onUpdateBreakpoints: (newBreakpoints: number[]) => void; }; /** * Wrapper around the Monaco editor that reveals * only the required functionality to the parent container. */ const CodeEditorComponent = (props: Props, ref: React.Ref) => { const [editor, setEditor] = React.useState(null); const [monaco, setMonaco] = React.useState(null); const [readOnly, setReadOnly] = React.useState(false); const highlightRange = React.useRef([]); const { isDark } = useDarkMode(); // Code modification tracker, used in execution mode const changeTracker = React.useRef({ original: "", changed: false, }); // Breakpoints useEditorBreakpoints({ editor, monaco, onUpdateBreakpoints: props.onUpdateBreakpoints, }); // Language config useEditorLanguageConfig({ languageId: props.languageId, tokensProvider: props.tokensProvider, }); // Code validation useCodeValidator({ editor, monaco, onValidateCode: props.onValidateCode, }); /** Update code highlights */ const updateHighlights = React.useCallback( (hl: DocumentRange | null) => { if (!editor) return; // Remove previous highlights const prevRange = highlightRange.current; editor.deltaDecorations(prevRange, []); // Add new highlights if (!hl) return; const newRange = createHighlightRange(monaco!, hl); const rangeStr = editor.deltaDecorations([], [newRange]); highlightRange.current = rangeStr; }, [editor] ); // Provide handle to parent for accessing editor contents React.useImperativeHandle( ref, () => ({ getCode: () => editor!.getValue(), editCode: (edits) => { changeTracker.current.changed = true; const monacoEdits = edits.map(createMonacoDocEdit); editor!.getModel()!.applyEdits(monacoEdits); }, updateHighlights, startExecutionMode: () => { changeTracker.current.original = editor!.getValue(); changeTracker.current.changed = false; setReadOnly(true); }, endExecutionMode: () => { setReadOnly(false); if (changeTracker.current.changed) { editor!.getModel()!.setValue(changeTracker.current.original); changeTracker.current.changed = false; } }, }), [editor] ); return ( { monaco.editor.defineTheme("ep-dark", darkTheme as any); monaco.editor.defineTheme("ep-light", lightTheme as any); }} onMount={(editor, monaco) => { if (!editor || !monaco) throw new Error("Error in initializing editor"); setEditor(editor); setMonaco(monaco); }} options={{ minimap: { enabled: false }, glyphMargin: true, readOnly: readOnly, // Self-modifying programs may add control characters to the code. // This option ensures such characters are properly displayed. renderControlCharacters: true, fixedOverflowWidgets: true, }} /> ); }; export const CodeEditor = React.forwardRef(CodeEditorComponent);