esolang/ui/code-editor/index.tsx
2022-02-13 19:20:52 +05:30

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);