Add pause-resume-stop functionality and UI
This commit is contained in:
@ -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>
|
||||
|
@ -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
76
ui/execution-controls.tsx
Normal 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>
|
||||
);
|
||||
};
|
@ -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,
|
||||
};
|
||||
};
|
||||
|
Reference in New Issue
Block a user