esolang/ui/Mainframe.tsx
2022-02-03 15:12:24 +05:30

191 lines
6.3 KiB
TypeScript

import React from "react";
import { CodeEditor, CodeEditorRef } from "../ui/code-editor";
import { InputEditor, InputEditorRef } from "../ui/input-editor";
import { MainLayout } from "../ui/MainLayout";
import { useExecController } from "../ui/use-exec-controller";
import { LanguageProvider, StepExecutionResult } from "../languages/types";
import { OutputViewer, OutputViewerRef } from "../ui/output-viewer";
import { ExecutionControls } from "./execution-controls";
import { RendererRef, RendererWrapper } from "./renderer-wrapper";
import { WorkerRuntimeError } from "../languages/worker-errors";
type Props<RS> = {
langId: string;
langName: string;
provider: LanguageProvider<RS>;
};
/**
* React component that contains and controls the entire IDE.
*
* For performance reasons, Mainframe makes spare use of state hooks. This
* component is rather expensive to render, and will block the main thread on
* small execution intervals if rendered on every execution. All state management
* is delegated to imperatively controlled child components.
*/
export const Mainframe = <RS extends {}>(props: Props<RS>) => {
// Language provider and engine
const providerRef = React.useRef(props.provider);
const execController = useExecController(props.langId);
// Refs for controlling UI components
const codeEditorRef = React.useRef<CodeEditorRef>(null);
const inputEditorRef = React.useRef<InputEditorRef>(null);
const outputEditorRef = React.useRef<OutputViewerRef>(null);
const rendererRef = React.useRef<RendererRef<any>>(null);
// Interval of execution
const [execInterval, setExecInterval] = React.useState(20);
/** Utility that updates UI with the provided execution result */
const updateWithResult = (
result: StepExecutionResult<any>,
error?: WorkerRuntimeError
) => {
rendererRef.current!.updateState(result.rendererState);
codeEditorRef.current!.updateHighlights(result.nextStepLocation);
outputEditorRef.current!.append(result.output);
// Self-modifying programs: update code
if (result.codeEdits != null)
codeEditorRef.current!.editCode(result.codeEdits);
// End of program: reset code to original version
if (!result.nextStepLocation) codeEditorRef.current!.endExecutionMode();
// RuntimeError: print error to output
if (error) outputEditorRef.current!.setError(error);
};
/** Reset and begin a new execution */
const runProgram = async () => {
// Check if controller is free for execution
const readyStates = ["empty", "done"];
if (!readyStates.includes(execController.state)) {
console.error(`Controller not ready: state is ${execController.state}`);
return;
}
// Reset any existing execution state
outputEditorRef.current!.reset();
await execController.resetState();
const error = await execController.prepare(
codeEditorRef.current!.getCode(),
inputEditorRef.current!.getValue()
);
// Check for ParseError, else begin execution
if (error) outputEditorRef.current!.setError(error);
else {
codeEditorRef.current!.startExecutionMode();
await execController.execute(updateWithResult, execInterval);
}
};
/** Pause the ongoing execution */
const pauseExecution = async () => {
// Check if controller is indeed executing code
if (execController.state !== "processing") {
console.error("Controller not processing any code");
return;
}
await execController.pauseExecution();
};
/** Run a single step of execution */
const executeStep = async () => {
// Check if controller is paused
if (execController.state !== "paused") {
console.error("Controller not paused");
return;
}
// Run and update execution states
const response = await execController.executeStep();
updateWithResult(response.result, response.error);
};
/** Resume the currently paused execution */
const resumeExecution = async () => {
// Check if controller is indeed paused
if (execController.state !== "paused") {
console.error("Controller is not paused");
return;
}
// Begin execution
await execController.execute(updateWithResult, execInterval);
};
/** Stop the currently active execution */
const stopExecution = async () => {
// Check if controller has execution
if (!["paused", "processing", "error"].includes(execController.state)) {
console.error("No active execution in controller");
return;
}
// If currently processing, pause execution loop first
if (execController.state === "processing")
await execController.pauseExecution();
// Reset all execution states
await execController.resetState();
rendererRef.current!.updateState(null);
codeEditorRef.current!.updateHighlights(null);
codeEditorRef.current!.endExecutionMode();
};
/** Translate execution controller state to debug controls state */
const getDebugState = () => {
const currState = execController.state;
if (currState === "processing") return "running";
else if (currState === "paused") return "paused";
else if (currState === "error") return "error";
else return "off";
};
return (
<MainLayout
langId={props.langId}
langName={props.langName}
renderEditor={() => (
<CodeEditor
ref={codeEditorRef}
languageId={props.langName}
defaultValue={providerRef.current.sampleProgram}
tokensProvider={providerRef.current.editorTokensProvider}
onValidateCode={execController.validateCode}
onUpdateBreakpoints={(newPoints) =>
execController.updateBreakpoints(newPoints)
}
/>
)}
renderRenderer={() => (
<RendererWrapper
ref={rendererRef}
renderer={providerRef.current.Renderer as any}
/>
)}
renderInput={() => (
<InputEditor
ref={inputEditorRef}
readOnly={execController.state === "processing"}
/>
)}
renderOutput={() => <OutputViewer ref={outputEditorRef} />}
renderExecControls={() => (
<ExecutionControls
state={getDebugState()}
onRun={runProgram}
onPause={pauseExecution}
onResume={resumeExecution}
onStep={executeStep}
onStop={stopExecution}
onChangeInterval={setExecInterval}
/>
)}
/>
);
};