Add automatic syntax checker, fix editor bugs
This commit is contained in:
@ -139,6 +139,7 @@ export const Mainframe = <RS extends {}>({ langName, provider }: Props<RS>) => {
|
||||
languageId="brainfuck"
|
||||
defaultValue={providerRef.current.sampleProgram}
|
||||
tokensProvider={providerRef.current.editorTokensProvider}
|
||||
onValidateCode={execController.validateCode}
|
||||
onUpdateBreakpoints={(newPoints) =>
|
||||
execController.updateBreakpoints(newPoints)
|
||||
}
|
||||
|
@ -11,6 +11,8 @@ 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 "../../engines/worker-errors";
|
||||
import { useCodeValidator } from "./use-code-validator";
|
||||
|
||||
// Interface for interacting with the editor
|
||||
export interface CodeEditorRef {
|
||||
@ -27,6 +29,8 @@ type Props = {
|
||||
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;
|
||||
};
|
||||
@ -36,15 +40,17 @@ type Props = {
|
||||
* only the required functionality to the parent container.
|
||||
*/
|
||||
const CodeEditorComponent = (props: Props, ref: React.Ref<CodeEditorRef>) => {
|
||||
const editorRef = React.useRef<EditorInstance | null>(null);
|
||||
const monacoRef = React.useRef<MonacoInstance | null>(null);
|
||||
const [editor, setEditor] = React.useState<EditorInstance | null>(null);
|
||||
const [monaco, setMonaco] = React.useState<MonacoInstance | null>(null);
|
||||
// const editorRef = React.useRef<EditorInstance | null>(null);
|
||||
// const monacoRef = React.useRef<MonacoInstance | null>(null);
|
||||
const highlightRange = React.useRef<string[]>([]);
|
||||
const { isDark } = useDarkMode();
|
||||
|
||||
// Breakpoints
|
||||
useEditorBreakpoints({
|
||||
editor: editorRef.current,
|
||||
monaco: monacoRef.current,
|
||||
editor,
|
||||
monaco,
|
||||
onUpdateBreakpoints: props.onUpdateBreakpoints,
|
||||
});
|
||||
|
||||
@ -54,16 +60,23 @@ const CodeEditorComponent = (props: Props, ref: React.Ref<CodeEditorRef>) => {
|
||||
tokensProvider: props.tokensProvider,
|
||||
});
|
||||
|
||||
// Code validation
|
||||
useCodeValidator({
|
||||
editor,
|
||||
monaco,
|
||||
onValidateCode: props.onValidateCode,
|
||||
});
|
||||
|
||||
/** Update code highlights */
|
||||
const updateHighlights = React.useCallback((hl: DocumentRange | null) => {
|
||||
// Remove previous highlights
|
||||
const prevRange = highlightRange.current;
|
||||
editorRef.current!.deltaDecorations(prevRange, []);
|
||||
editor!.deltaDecorations(prevRange, []);
|
||||
|
||||
// Add new highlights
|
||||
if (!hl) return;
|
||||
const newRange = createHighlightRange(monacoRef.current!, hl);
|
||||
const rangeStr = editorRef.current!.deltaDecorations([], [newRange]);
|
||||
const newRange = createHighlightRange(monaco!, hl);
|
||||
const rangeStr = editor!.deltaDecorations([], [newRange]);
|
||||
highlightRange.current = rangeStr;
|
||||
}, []);
|
||||
|
||||
@ -71,10 +84,10 @@ const CodeEditorComponent = (props: Props, ref: React.Ref<CodeEditorRef>) => {
|
||||
React.useImperativeHandle(
|
||||
ref,
|
||||
() => ({
|
||||
getValue: () => editorRef.current!.getValue(),
|
||||
getValue: () => editor!.getValue(),
|
||||
updateHighlights,
|
||||
}),
|
||||
[]
|
||||
[editor]
|
||||
);
|
||||
|
||||
return (
|
||||
@ -88,8 +101,8 @@ const CodeEditorComponent = (props: Props, ref: React.Ref<CodeEditorRef>) => {
|
||||
}}
|
||||
onMount={(editor, monaco) => {
|
||||
if (!editor || !monaco) throw new Error("Error in initializing editor");
|
||||
editorRef.current = editor;
|
||||
monacoRef.current = monaco;
|
||||
setEditor(editor);
|
||||
setMonaco(monaco);
|
||||
}}
|
||||
options={{ minimap: { enabled: false }, glyphMargin: true }}
|
||||
/>
|
||||
|
@ -1,5 +1,6 @@
|
||||
import monaco from "monaco-editor";
|
||||
import { DocumentRange } from "../../engines/types";
|
||||
import { WorkerParseError } from "../../engines/worker-errors";
|
||||
|
||||
/** Type alias for an instance of Monaco editor */
|
||||
export type EditorInstance = monaco.editor.IStandaloneCodeEditor;
|
||||
@ -41,6 +42,24 @@ export const createBreakpointRange = (
|
||||
return { range, options: { glyphMarginClassName: className } };
|
||||
};
|
||||
|
||||
/** Create Monaco syntax-error marker from message and document range */
|
||||
export const createValidationMarker = (
|
||||
monacoInstance: MonacoInstance,
|
||||
error: WorkerParseError,
|
||||
range: DocumentRange
|
||||
): monaco.editor.IMarkerData => {
|
||||
const location = get1IndexedLocation(range);
|
||||
return {
|
||||
startLineNumber: location.line,
|
||||
endLineNumber: location.line,
|
||||
startColumn: location.charRange?.start || 0,
|
||||
endColumn: location.charRange?.end || 1000,
|
||||
severity: monacoInstance.MarkerSeverity.Error,
|
||||
message: error.message,
|
||||
source: error.name,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Convert a DocumentRange to use 1-indexed values. Used since language engines
|
||||
* use 0-indexed ranges but Monaco requires 1-indexed ranges.
|
||||
|
47
ui/code-editor/use-code-validator.ts
Normal file
47
ui/code-editor/use-code-validator.ts
Normal file
@ -0,0 +1,47 @@
|
||||
import * as React from "react";
|
||||
import { WorkerParseError } from "../../engines/worker-errors";
|
||||
import {
|
||||
createValidationMarker,
|
||||
EditorInstance,
|
||||
MonacoInstance,
|
||||
} from "./monaco-utils";
|
||||
|
||||
/** Constant denoting "owner" of syntax error markers */
|
||||
const MARKER_OWNER = "code-validation";
|
||||
|
||||
/** Delay between user's last edit and sending validation request */
|
||||
const VALIDATE_DELAY = 500;
|
||||
|
||||
type Args = {
|
||||
editor: EditorInstance | null;
|
||||
monaco: MonacoInstance | null;
|
||||
onValidateCode: (code: string) => Promise<WorkerParseError | undefined>;
|
||||
};
|
||||
|
||||
/**
|
||||
* React hook that sets up code validation lifecycle on the Monaco editor.
|
||||
* Code validation is done a fixed delay after user's last edit, and markers
|
||||
* for indicating syntax error are added to the editor.
|
||||
*/
|
||||
export const useCodeValidator = ({ editor, monaco, onValidateCode }: Args) => {
|
||||
const timeoutRef = React.useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
const runValidator = async () => {
|
||||
if (!editor || !monaco) return;
|
||||
const error = await onValidateCode(editor.getValue());
|
||||
if (error)
|
||||
monaco.editor.setModelMarkers(editor.getModel()!, MARKER_OWNER, [
|
||||
createValidationMarker(monaco, error, error.range),
|
||||
]);
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!editor || !monaco) return;
|
||||
const disposer = editor.getModel()!.onDidChangeContent(() => {
|
||||
monaco.editor.setModelMarkers(editor.getModel()!, MARKER_OWNER, []);
|
||||
if (timeoutRef.current) clearTimeout(timeoutRef.current);
|
||||
timeoutRef.current = setTimeout(runValidator, VALIDATE_DELAY);
|
||||
});
|
||||
return () => disposer.dispose();
|
||||
}, [editor, monaco]);
|
||||
};
|
@ -77,9 +77,6 @@ export const useExecController = <RS>(langName: string) => {
|
||||
(async () => {
|
||||
if (workerRef.current) throw new Error("Tried to reinitialize worker");
|
||||
workerRef.current = new Worker(`../workers/${langName}.js`);
|
||||
workerRef.current!.addEventListener("error", (ev) =>
|
||||
console.log("Ev: ", ev)
|
||||
);
|
||||
const res = await requestWorker({ type: "Init" });
|
||||
if (res.type === "ack" && res.data === "init") setWorkerState("empty");
|
||||
else throwUnexpectedRes("init", res);
|
||||
@ -137,6 +134,21 @@ export const useExecController = <RS>(langName: string) => {
|
||||
else throwUnexpectedRes("resetState", res);
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Validate the syntax of the user's program code
|
||||
* @param code Code content of the user's program
|
||||
*/
|
||||
const validateCode = React.useCallback(
|
||||
async (code: string): Promise<WorkerParseError | undefined> => {
|
||||
const res = await requestWorker(
|
||||
{ type: "ValidateCode", params: { code } },
|
||||
(res) => res.type !== "validate"
|
||||
);
|
||||
return res.error;
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
/**
|
||||
* Pause program execution
|
||||
*/
|
||||
@ -206,6 +218,7 @@ export const useExecController = <RS>(langName: string) => {
|
||||
resetState,
|
||||
prepare,
|
||||
pauseExecution,
|
||||
validateCode,
|
||||
execute,
|
||||
executeStep,
|
||||
updateBreakpoints,
|
||||
|
Reference in New Issue
Block a user