167 lines
5.1 KiB
TypeScript
167 lines
5.1 KiB
TypeScript
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<WorkerParseError | undefined>;
|
|
/** 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<CodeEditorRef>) => {
|
|
const [editor, setEditor] = React.useState<EditorInstance | null>(null);
|
|
const [monaco, setMonaco] = React.useState<MonacoInstance | null>(null);
|
|
const [readOnly, setReadOnly] = React.useState(false);
|
|
const highlightRange = React.useRef<string[]>([]);
|
|
const { isDark } = useDarkMode();
|
|
|
|
// Code modification tracker, used in execution mode
|
|
const changeTracker = React.useRef<ChangeTrackerValue>({
|
|
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 (
|
|
<Editor
|
|
theme={isDark ? "ep-dark" : "ep-light"}
|
|
defaultLanguage={props.languageId}
|
|
defaultValue={props.defaultValue}
|
|
beforeMount={(monaco) => {
|
|
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);
|