Add pause-resume-stop functionality and UI

This commit is contained in:
Nilay Majorwar
2021-12-15 21:31:25 +05:30
parent 29b243d6f2
commit 13ff6da638
8 changed files with 262 additions and 31 deletions

View File

@ -16,6 +16,7 @@ type Props = {
renderRenderer: () => React.ReactNode;
renderInput: () => React.ReactNode;
renderOutput: () => React.ReactNode;
renderExecControls: () => React.ReactNode;
};
export const MainLayout = (props: Props) => {
@ -48,7 +49,9 @@ export const MainLayout = (props: Props) => {
<MosaicWindow<number>
path={path}
title={WindowTitles[windowId]}
toolbarControls={<span />}
toolbarControls={
windowId === "editor" ? props.renderExecControls() : <span />
}
>
{MOSAIC_MAP[windowId]()}
</MosaicWindow>

View File

@ -6,6 +6,7 @@ import { useExecController } from "../ui/use-exec-controller";
import { DocumentRange, LanguageProvider } from "../engines/types";
import BrainfuckProvider from "../engines/brainfuck";
import { OutputViewer } from "../ui/output-viewer";
import { ExecutionControls } from "./execution-controls";
export const Mainframe = () => {
const codeEditorRef = React.useRef<CodeEditorRef>(null);
@ -20,18 +21,18 @@ export const Mainframe = () => {
DocumentRange | undefined
>();
const testDrive = React.useCallback(async () => {
console.info("=== RUNNING TEST DRIVE ===");
// Check that controller is ready to execute
const readyStates = ["empty", "ready", "done"];
/** 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;
}
// Prepare for execution
// Reset any existing execution state
setOutput("");
setRendererState(null);
await execController.resetState();
await execController.prepare(
codeEditorRef.current!.getValue(),
@ -39,21 +40,65 @@ export const Mainframe = () => {
);
// Begin execution
await execController.executeAll((result) => {
await execController.execute((result) => {
setRendererState(result.rendererState);
setCodeHighlights(result.nextStepLocation || undefined);
setOutput((o) => (o || "") + (result.output || ""));
}, 20);
}, [execController.state]);
}, 1000);
};
React.useEffect(() => {
const handler = (ev: KeyboardEvent) => {
if (!(ev.ctrlKey && ev.code === "KeyY")) return;
testDrive();
};
document.addEventListener("keydown", handler);
return () => document.removeEventListener("keydown", handler);
}, [testDrive]);
/** 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();
};
/** 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((result) => {
setRendererState(result.rendererState);
setCodeHighlights(result.nextStepLocation || undefined);
setOutput((o) => (o || "") + (result.output || ""));
}, 1000);
};
/** Stop the currently active execution */
const stopExecution = async () => {
// Check if controller has execution
if (!["paused", "processing"].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();
setOutput(null);
setRendererState(null);
setCodeHighlights(undefined);
};
/** 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 return "off";
};
return (
<MainLayout
@ -74,6 +119,15 @@ export const Mainframe = () => {
)}
renderInput={() => <InputEditor ref={inputEditorRef} />}
renderOutput={() => <OutputViewer value={output} />}
renderExecControls={() => (
<ExecutionControls
state={getDebugState()}
onRun={runProgram}
onPause={pauseExecution}
onResume={resumeExecution}
onStop={stopExecution}
/>
)}
/>
);
};

76
ui/execution-controls.tsx Normal file
View File

@ -0,0 +1,76 @@
import { Button, ButtonGroup, Icon } from "@blueprintjs/core";
const styles = {
container: {
display: "flex",
alignItems: "center",
paddingRight: 5,
},
};
/** Button for starting code execution */
const RunButton = ({ onClick }: { onClick: () => void }) => (
<Button
small
onClick={onClick}
rightIcon={<Icon icon="play" intent="success" />}
>
Run code
</Button>
);
/** Button group for debugging controls */
const DebugControls = (props: {
paused: boolean;
onPause: () => void;
onResume: () => void;
onStop: () => void;
}) => {
return (
<ButtonGroup>
<Button
small
title={props.paused ? "Pause" : "Resume"}
onClick={props.paused ? props.onResume : props.onPause}
icon={<Icon icon={props.paused ? "play" : "pause"} intent="primary" />}
/>
<Button
small
title="Step"
disabled={!props.paused}
icon={<Icon icon="step-forward" intent="warning" />}
/>
<Button
small
title="Stop"
onClick={props.onStop}
icon={<Icon icon="stop" intent="danger" />}
/>
</ButtonGroup>
);
};
type Props = {
state: "off" | "running" | "paused";
onRun: () => void;
onPause: () => void;
onResume: () => void;
onStop: () => void;
};
export const ExecutionControls = (props: Props) => {
return (
<div style={styles.container}>
{props.state === "off" ? (
<RunButton onClick={props.onRun} />
) : (
<DebugControls
paused={props.state === "paused"}
onPause={props.onPause}
onResume={props.onResume}
onStop={props.onStop}
/>
)}
</div>
);
};

View File

@ -9,8 +9,9 @@ import {
type WorkerState =
| "loading" // Worker is not initialized yet
| "empty" // Worker loaded, no code loaded yet
| "ready" // Code loaded, ready to execute
| "ready" // Ready to start execution
| "processing" // Executing code
| "paused" // Execution currently paused
| "done"; // Program ended, reset now
/**
@ -66,7 +67,7 @@ export const useExecController = <RS>() => {
fnName: string,
res: WorkerResponseData<RS>
): never => {
throw new Error(`Unexpected response on ${fnName}: ${res.toString()}`);
throw new Error(`Unexpected response on ${fnName}: ${JSON.stringify(res)}`);
};
// Initialization and cleanup of web worker
@ -128,11 +129,22 @@ export const useExecController = <RS>() => {
else throwUnexpectedRes("resetState", res);
}, []);
/**
* Pause program execution
*/
const pauseExecution = React.useCallback(async () => {
await requestWorker({ type: "Pause" }, (res) => {
// We don't update state here - that's done by the execution stream instead
if (!(res.type === "ack" && res.data === "pause")) return true;
return false;
});
}, []);
/**
* Execute the code loaded into the engine
* @param onResult Callback used when an execution result is received
*/
const executeAll = React.useCallback(
const execute = React.useCallback(
async (
onResult: (result: StepExecutionResult<RS>) => void,
interval?: number
@ -142,10 +154,13 @@ export const useExecController = <RS>() => {
await requestWorker({ type: "Execute", params: { interval } }, (res) => {
if (res.type !== "result") return true;
onResult(res.data);
if (res.data.nextStepLocation) return true;
// Clean up and terminate response stream
setWorkerState("done");
return false;
if (!res.data.nextStepLocation) {
setWorkerState("done");
return false;
} else if (res.data.signal === "paused") {
setWorkerState("paused");
return false;
} else return true;
});
},
[]
@ -155,7 +170,8 @@ export const useExecController = <RS>() => {
state: workerState,
resetState,
prepare,
executeAll,
pauseExecution,
execute,
updateBreakpoints,
};
};