Skip to content

Commit

Permalink
Multiple Webview Tabs (#2093)
Browse files Browse the repository at this point in the history
Instead of always consuming the last Q# webview tab, open a new webview
tab when activated with a new operation. Activation with the same
operation will still consume/update the last webview for that operation.
Works on circuit and histogram panels.
  • Loading branch information
ScottCarda-MS authored Jan 28, 2025
1 parent e0f4e77 commit 84f2787
Show file tree
Hide file tree
Showing 5 changed files with 118 additions and 59 deletions.
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);
}
}

0 comments on commit 84f2787

Please sign in to comment.