Skip to content

Multiple Webview Tabs #2093

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Jan 28, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions vscode/src/circuit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,7 @@ export function updateCircuitPanel(
calculating?: boolean;
},
) {
const panelId = params?.operation?.operation || projectName;
const title = params?.operation
? `${params.operation.operation} with ${params.operation.totalNumQubits} input qubits`
: projectName;
Expand All @@ -349,10 +350,9 @@ export function updateCircuitPanel(
};

const message = {
command: "circuit",
props,
};
sendMessageToPanel("circuit", reveal, message);
sendMessageToPanel({ panelType: "circuit", id: panelId }, reveal, message);
}

/**
Expand Down
5 changes: 4 additions & 1 deletion vscode/src/debugger/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -953,7 +953,10 @@ export class QscDebugSession extends LoggingDebugSession {

/* Updates the circuit panel if `showCircuit` is true or if panel is already open */
private async updateCircuit(error?: any) {
if (this.config.showCircuit || isPanelOpen("circuit")) {
if (
this.config.showCircuit ||
isPanelOpen("circuit", this.program.projectName)
) {
// Error returned from the debugger has a message and a stack (which also includes the message).
// We would ideally retrieve the original runtime error, and format it to be consistent
// with the other runtime errors that can be shown in the circuit panel, but that will require
Expand Down
4 changes: 2 additions & 2 deletions vscode/src/documentation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export async function showDocumentationCommand(extensionUri: Uri) {

// Reveal panel and show 'Loading...' for immediate feedback.
sendMessageToPanel(
"documentation", // This is needed to route the message to the proper panel
{ panelType: "documentation" }, // This is needed to route the message to the proper panel
true, // Reveal panel
null, // With no message
);
Expand Down Expand Up @@ -48,7 +48,7 @@ export async function showDocumentationCommand(extensionUri: Uri) {
};

sendMessageToPanel(
"documentation", // This is needed to route the message to the proper panel
{ panelType: "documentation" }, // This is needed to route the message to the proper panel
true, // Reveal panel
message, // And ask it to display documentation
);
Expand Down
7 changes: 5 additions & 2 deletions vscode/src/webview/webview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ window.addEventListener("load", main);

type HistogramState = {
viewType: "histogram";
panelId: string;
buckets: Array<[string, number]>;
shotCount: number;
};
Expand All @@ -47,6 +48,7 @@ type EstimatesState = {

type CircuitState = {
viewType: "circuit";
panelId: string;
props: CircuitProps;
};

Expand All @@ -57,13 +59,13 @@ type DocumentationState = {
};

type State =
| { viewType: "loading" }
| { viewType: "loading"; panelId: string }
| { viewType: "help" }
| HistogramState
| EstimatesState
| CircuitState
| DocumentationState;
const loadingState: State = { viewType: "loading" };
const loadingState: State = { viewType: "loading", panelId: "" };
const helpState: State = { viewType: "help" };
let state: State = loadingState;

Expand Down Expand Up @@ -140,6 +142,7 @@ function onMessage(event: any) {
}
state = {
viewType: "histogram",
panelId: message.panelId,
buckets: message.buckets as Array<[string, number]>,
shotCount: message.shotCount,
};
Expand Down
157 changes: 105 additions & 52 deletions vscode/src/webviewPanel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -172,24 +172,24 @@ export function registerWebViewCommands(context: ExtensionContext) {

log.info("RE params", params);

sendMessageToPanel("estimates", true, {
command: "estimates",
sendMessageToPanel({ panelType: "estimates" }, true, {
calculating: true,
});

const estimatePanel = getOrCreatePanel("estimates");
// Ensure the name is unique
if (panelTypeToPanel["estimates"].state[runName] !== undefined) {
if (estimatePanel.state[runName] !== undefined) {
let idx = 2;
for (;;) {
const newName = `${runName}-${idx}`;
if (panelTypeToPanel["estimates"].state[newName] === undefined) {
if (estimatePanel.state[newName] === undefined) {
runName = newName;
break;
}
idx++;
}
}
panelTypeToPanel["estimates"].state[runName] = true;
estimatePanel.state[runName] = true;

// Start the worker, run the code, and send the results to the webview
log.debug("Starting resource estimates worker.");
Expand Down Expand Up @@ -239,19 +239,17 @@ export function registerWebViewCommands(context: ExtensionContext) {
clearTimeout(compilerTimeout);

const message = {
command: "estimates",
calculating: false,
estimates,
};
sendMessageToPanel("estimates", true, message);
sendMessageToPanel({ panelType: "estimates" }, true, message);
} catch (e: any) {
// Stop the 'calculating' animation
const message = {
command: "estimates",
calculating: false,
estimates: [],
};
sendMessageToPanel("estimates", false, message);
sendMessageToPanel({ panelType: "estimates" }, false, message);

if (timedOut) {
// Show a VS Code popup that a timeout occurred
Expand All @@ -273,10 +271,8 @@ export function registerWebViewCommands(context: ExtensionContext) {

context.subscriptions.push(
commands.registerCommand(`${qsharpExtensionId}.showHelp`, async () => {
const message = {
command: "help",
};
sendMessageToPanel("help", true, message);
const message = {};
sendMessageToPanel({ panelType: "help" }, true, message);
}),
);

Expand All @@ -296,6 +292,8 @@ export function registerWebViewCommands(context: ExtensionContext) {
throw new Error(program.errorMsg);
}

const panelId = program.programConfig.projectName;

// Start the worker, run the code, and send the results to the webview
const worker = getCompilerWorker(compilerWorkerScriptPath);
const compilerTimeout = setTimeout(() => {
Expand All @@ -322,7 +320,11 @@ export function registerWebViewCommands(context: ExtensionContext) {
return;
}

sendMessageToPanel("histogram", true, undefined);
sendMessageToPanel(
{ panelType: "histogram", id: panelId },
true,
undefined,
);

const evtTarget = new QscEventTarget(true);
evtTarget.addEventListener("uiResultsRefresh", () => {
Expand All @@ -336,11 +338,14 @@ export function registerWebViewCommands(context: ExtensionContext) {
buckets.set(strKey, newValue);
}
const message = {
command: "histogram",
buckets: Array.from(buckets.entries()),
shotCount: resultCount,
};
sendMessageToPanel("histogram", false, message);
sendMessageToPanel(
{ panelType: "histogram", id: panelId },
false,
message,
);
});
const start = performance.now();
sendTelemetryEvent(EventType.HistogramStart, { associationId }, {});
Expand Down Expand Up @@ -390,38 +395,68 @@ export function registerWebViewCommands(context: ExtensionContext) {
);
}

type PanelDesc = {
title: string;
panel: QSharpWebViewPanel;
state: any;
};

type PanelType =
| "histogram"
| "estimates"
| "help"
| "circuit"
| "documentation";

const panelTypeToPanel: Record<
PanelType,
{ title: string; panel: QSharpWebViewPanel | undefined; state: any }
> = {
histogram: { title: "Q# Histogram", panel: undefined, state: {} },
estimates: { title: "Q# Estimates", panel: undefined, state: {} },
circuit: { title: "Q# Circuit", panel: undefined, state: {} },
help: { title: "Q# Help", panel: undefined, state: {} },
documentation: {
title: "Q# Documentation",
panel: undefined,
state: {},
},
const panels: Record<PanelType, { [id: string]: PanelDesc }> = {
histogram: {},
estimates: {},
circuit: {},
help: {},
documentation: {},
};

export function sendMessageToPanel(
panelType: PanelType,
reveal: boolean,
message: any,
) {
const panelRecord = panelTypeToPanel[panelType];
if (!panelRecord.panel) {
const panel = window.createWebviewPanel(
const panelTypeToTitle: Record<PanelType, string> = {
histogram: "Q# Histogram",
estimates: "Q# Estimates",
circuit: "Q# Circuit",
help: "Q# Help",
documentation: "Q# Documentation",
};

function getPanel(type: PanelType, id?: string): PanelDesc | undefined {
if (id) {
return panels[type][id];
} else {
return panels[type][""];
}
}

export function isPanelOpen(panelType: PanelType, id?: string): boolean {
return getPanel(panelType, id)?.panel !== undefined;
}

function createPanel(
type: PanelType,
id?: string,
webViewPanel?: WebviewPanel,
): PanelDesc {
if (id == undefined) {
id = "";
}
if (webViewPanel) {
const title = webViewPanel.title;
const panel = new QSharpWebViewPanel(type, webViewPanel, id);
panels[type][id] = { title, panel, state: {} };
return panels[type][id];
} else {
let title = `${panelTypeToTitle[type]}`;
if (type == "circuit" || type == "histogram") {
title = title + ` ${id}`;
}
const newPanel = window.createWebviewPanel(
QSharpWebViewType,
panelRecord.title,
title,
{
viewColumn: ViewColumn.Three,
preserveFocus: true,
Expand All @@ -439,15 +474,29 @@ export function sendMessageToPanel(
},
);

panelRecord.panel = new QSharpWebViewPanel(panelType, panel);
const panel = new QSharpWebViewPanel(type, newPanel, id);
panels[type][id] = { title, panel, state: {} };
return panels[type][id];
}
}

if (reveal) panelRecord.panel.reveal(ViewColumn.Beside);
if (message) panelRecord.panel.sendMessage(message);
function getOrCreatePanel(type: PanelType, id?: string): PanelDesc {
const panel = getPanel(type, id);
if (panel) {
return panel;
} else {
return createPanel(type, id);
}
}

export function isPanelOpen(panelType: PanelType) {
return panelTypeToPanel[panelType].panel !== undefined;
export function sendMessageToPanel(
panel: { panelType: PanelType; id?: string },
reveal: boolean,
message: any,
) {
const panelRecord = getOrCreatePanel(panel.panelType, panel.id);
if (reveal) panelRecord.panel.reveal(ViewColumn.Beside);
if (message) panelRecord.panel.sendMessage(message);
}

export class QSharpWebViewPanel {
Expand All @@ -458,8 +507,9 @@ export class QSharpWebViewPanel {
constructor(
private type: PanelType,
private panel: WebviewPanel,
private id: string,
) {
log.info("Creating webview panel of type", type);
log.info(`Creating webview panel of type ${type} and id ${id}`);
this.panel.onDidDispose(() => this.dispose());

this.panel.webview.html = this._getWebviewContent(this.panel.webview);
Expand Down Expand Up @@ -505,6 +555,8 @@ export class QSharpWebViewPanel {
}

sendMessage(message: any) {
message.command = message.command || this.type;
message.panelId = message.panelId || this.id;
if (this._ready) {
log.debug("Sending message to webview", message);
this.panel.webview.postMessage(message);
Expand Down Expand Up @@ -532,8 +584,11 @@ export class QSharpWebViewPanel {

public dispose() {
log.info("Disposing webview panel", this.type);
panelTypeToPanel[this.type].panel = undefined;
panelTypeToPanel[this.type].state = {};
const panel = getPanel(this.type, this.id);
if (panel) {
panel.state = {};
delete panels[this.type][this.id];
}
this.panel.dispose();
}
}
Expand All @@ -543,6 +598,7 @@ export class QSharpViewViewPanelSerializer implements WebviewPanelSerializer {
log.info("Deserializing webview panel", state);

const panelType: PanelType = state?.viewType;
const id = state?.panelId;

if (
panelType !== "estimates" &&
Expand All @@ -559,14 +615,11 @@ export class QSharpViewViewPanelSerializer implements WebviewPanelSerializer {
return;
}

if (panelTypeToPanel[panelType].panel !== undefined) {
log.error("Panel of type already exists", panelType);
if (getPanel(panelType, id) !== undefined) {
log.error(`Panel of type ${panelType} and id ${id} already exists`);
return;
}

panelTypeToPanel[panelType].panel = new QSharpWebViewPanel(
panelType,
panel,
);
createPanel(panelType, id, panel);
}
}
Loading