// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
///
const vscodeApi = acquireVsCodeApi();
import { render } from "preact";
import DOMPurify from "dompurify";
import {
CircuitPanel,
CircuitProps,
type CircuitModel,
detectThemeChange,
updateStyleSheetTheme,
} from "qsharp-lang/ux";
import stateComputeWorkerSource from "./stateComputeWorker.inline.ts";
window.addEventListener("message", onMessage);
window.addEventListener("load", main);
type CircuitState = {
viewType: "circuit";
props: CircuitProps;
};
type State = { viewType: "loading" } | CircuitState;
const loadingState: State = { viewType: "loading" };
let state: State = loadingState;
type WorkerComputeRequest = {
command: "compute";
requestId: number;
model: CircuitModel;
opts?: {
normalize?: boolean;
minProbThreshold?: number;
maxColumns?: number;
};
};
type WorkerComputeResponse =
| { command: "result"; requestId: number; columns: any }
| {
command: "error";
requestId: number;
error: { name: string; message: string };
};
type ActiveStateComputeWorker = {
requestId: number;
worker: Worker;
blobUrl: string;
reject?: (err: unknown) => void;
};
let activeStateComputeWorker: ActiveStateComputeWorker | null = null;
let stateComputeRequestId = 0;
function disposeActiveStateComputeWorker() {
if (!activeStateComputeWorker) return;
try {
activeStateComputeWorker.worker.terminate();
} catch {
// ignore
}
try {
URL.revokeObjectURL(activeStateComputeWorker.blobUrl);
} catch {
// ignore
}
activeStateComputeWorker = null;
}
function cancelActiveStateComputeWorker(reason: string) {
if (!activeStateComputeWorker) return;
const reject = activeStateComputeWorker.reject;
// Prevent any later cleanup from changing the settled promise.
activeStateComputeWorker.reject = undefined;
disposeActiveStateComputeWorker();
try {
reject?.(
new DOMException(`State compute cancelled (${reason})`, "AbortError"),
);
} catch {
// ignore
}
}
function createStateComputeWorker(): { worker: Worker; blobUrl: string } {
const blobUrl = URL.createObjectURL(
new Blob([stateComputeWorkerSource], { type: "text/javascript" }),
);
return { worker: new Worker(blobUrl), blobUrl };
}
function computeStateVizColumnsInWorker(
model: CircuitModel,
opts?: {
normalize?: boolean;
minProbThreshold?: number;
maxColumns?: number;
},
) {
cancelActiveStateComputeWorker("replaced by new compute request");
const requestId = ++stateComputeRequestId;
const created = createStateComputeWorker();
return new Promise((resolve, reject) => {
activeStateComputeWorker = {
requestId,
worker: created.worker,
blobUrl: created.blobUrl,
reject,
};
created.worker.onerror = (ev: ErrorEvent) => {
if (
!activeStateComputeWorker ||
activeStateComputeWorker.requestId !== requestId
) {
return;
}
disposeActiveStateComputeWorker();
reject(new Error(ev.message || "State compute worker error"));
};
created.worker.onmessage = (ev: MessageEvent) => {
if (
!activeStateComputeWorker ||
activeStateComputeWorker.requestId !== requestId
) {
return;
}
const msg = ev.data as any;
if (!msg || typeof msg !== "object") return;
if (msg.command === "result") {
const columns = msg.columns;
disposeActiveStateComputeWorker();
resolve(columns);
return;
}
if (msg.command === "error") {
const err = new Error(msg.error?.message ?? "Worker error");
(err as any).name = msg.error?.name ?? "Error";
disposeActiveStateComputeWorker();
reject(err);
}
};
created.worker.postMessage({
command: "compute",
requestId,
model,
opts,
} satisfies WorkerComputeRequest);
});
}
function main() {
state = (vscodeApi.getState() as any) || loadingState;
render(, document.body);
window.addEventListener("unload", () => {
cancelActiveStateComputeWorker("webview unload");
});
detectThemeChange(document.body, (isDark) =>
updateStyleSheetTheme(
isDark,
"github-markdown-css",
/(light\.css)|(dark\.css)/,
"light.css",
"dark.css",
),
);
readFromTextDocument();
}
function onMessage(event: any) {
const message = event.data;
if (!message?.command) {
console.error("Unknown message: ", message);
return;
}
switch (message.command) {
case "error": {
const sanitizedMessage = DOMPurify.sanitize(message.props.message);
const sanitizedTitle = DOMPurify.sanitize(message.props.title);
const innerHTML = `
${sanitizedTitle}
${sanitizedMessage}
`;
document.body.innerHTML = innerHTML; // CodeQL [SM04949] message data is not untrusted, handler is running in an extension, and is sanitized.
return;
}
case "circuit":
{
// Only short-circuit if the circuit payload is unchanged
if (state.viewType === "circuit") {
const sameCircuit =
JSON.stringify(state.props.circuit) ===
JSON.stringify(message.props.circuit);
if (sameCircuit) {
return;
}
}
state = {
viewType: "circuit",
...message,
};
}
break;
default:
console.error("Unknown command: ", message.command);
return;
}
vscodeApi.setState(state);
render(, document.body);
}
function readFromTextDocument() {
vscodeApi.postMessage({ command: "read" });
}
function updateTextDocument(circuit: any) {
vscodeApi.postMessage({
command: "update",
text: JSON.stringify(circuit, null, 2),
});
}
function runCircuit() {
vscodeApi.postMessage({ command: "run" });
}
function App({ state }: { state: State }) {
switch (state.viewType) {
case "loading":
return Loading...
;
case "circuit":
return (
);
default:
console.error("Unknown view type in state", state);
return Loading error
;
}
}