Skip to content
Open
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
17 changes: 13 additions & 4 deletions source/npm/qsharp/src/debug-service/debug-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
import { log } from "../log.js";
import type { IServiceProxy, ServiceProtocol } from "../workers/types.js";
import { toWasmProgramConfig } from "../compiler/compiler.js";
import { callAndTransformExceptions } from "../diagnostics.js";

type QscWasm = typeof import("../../lib/web/qsc_wasm.js");

Expand Down Expand Up @@ -112,7 +113,9 @@ export class QSharpDebugService implements IDebugService {
): Promise<IStructStepResult> {
const event_cb = (msg: string) => onCompilerEvent(msg, eventHandler);
const ids = new Uint32Array(bps);
return this.debugService.eval_continue(event_cb, ids);
return await callAndTransformExceptions(async () =>
this.debugService.eval_continue(event_cb, ids),
);
}

async evalNext(
Expand All @@ -121,7 +124,9 @@ export class QSharpDebugService implements IDebugService {
): Promise<IStructStepResult> {
const event_cb = (msg: string) => onCompilerEvent(msg, eventHandler);
const ids = new Uint32Array(bps);
return this.debugService.eval_next(event_cb, ids);
return await callAndTransformExceptions(async () =>
this.debugService.eval_next(event_cb, ids),
);
}

async evalStepIn(
Expand All @@ -130,7 +135,9 @@ export class QSharpDebugService implements IDebugService {
): Promise<IStructStepResult> {
const event_cb = (msg: string) => onCompilerEvent(msg, eventHandler);
const ids = new Uint32Array(bps);
return this.debugService.eval_step_in(event_cb, ids);
return await callAndTransformExceptions(async () =>
this.debugService.eval_step_in(event_cb, ids),
);
}

async evalStepOut(
Expand All @@ -139,7 +146,9 @@ export class QSharpDebugService implements IDebugService {
): Promise<IStructStepResult> {
const event_cb = (msg: string) => onCompilerEvent(msg, eventHandler);
const ids = new Uint32Array(bps);
return this.debugService.eval_step_out(event_cb, ids);
return await callAndTransformExceptions(async () =>
this.debugService.eval_step_out(event_cb, ids),
);
}

async dispose() {
Expand Down
98 changes: 2 additions & 96 deletions source/vscode/src/circuit.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

import { escapeHtml } from "markdown-it/lib/common/utils.mjs";
import {
type CircuitData,
ICompilerWorker,
Expand All @@ -24,8 +23,8 @@ import {
} from "./telemetry";
import { getRandomGuid } from "./utils";
import { sendMessageToPanel } from "./webviewPanel";
import { ICircuitConfig, IPosition } from "../../npm/qsharp/lib/web/qsc_wasm";
import { basename, loadCompilerWorker } from "./common";
import { ICircuitConfig } from "../../npm/qsharp/lib/web/qsc_wasm";
import { errorsToHtml, loadCompilerWorker } from "./common";

const compilerRunTimeoutMs = 1000 * 60 * 5; // 5 minutes

Expand Down Expand Up @@ -405,46 +404,6 @@ function hasAdaptiveComplianceOrUnsupportedRirError(errors: IQSharpError[]) {
return hasResultComparisonError;
}

/**
* Formats an array of compiler/runtime errors into HTML to be presented to the user.
*
* @param errors The list of errors to format.
* @returns The HTML formatted errors, to be set as the inner contents of a container element.
*/
function errorsToHtml(errors: IQSharpError[]) {
let errorHtml = "";
for (const error of errors) {
const { document, diagnostic: diag, stack: rawStack } = error;

const location = documentHtml(false, document, diag.range.start);
const message = escapeHtml(`(${diag.code}) ${diag.message}`).replace(
/\n/g,
"<br/><br/>",
);

errorHtml += `<p>${location}: ${message}<br/></p>`;

if (rawStack) {
const stack = rawStack
.split("\n")
.map((l) => {
// Link-ify the document names in the stack trace
const match = l.match(/^(\s*)at (.*) in (.*):(\d+):(\d+)/);
if (match) {
const [, leadingWs, callable, doc] = match;
return `${leadingWs}at ${escapeHtml(callable)} in ${documentHtml(false, doc)}`;
} else {
return l;
}
})

.join("\n");
errorHtml += `<br/><pre>${stack}</pre>`;
}
}
return errorHtml;
}

export function updateCircuitPanel(
targetProfile: string,
projectName: string,
Expand Down Expand Up @@ -480,56 +439,3 @@ export function updateCircuitPanel(
};
sendMessageToPanel({ panelType: "circuit", id: panelId }, reveal, message);
}

/**
* If the input is a URI, turns it into a document open link.
* Otherwise returns the HTML-escaped input
*/
function documentHtml(
customCommand: boolean,
maybeUri: string,
position?: IPosition,
) {
try {
// If the error location is a document URI, create a link to that document.
// We use the `vscode.open` command (https://code.visualstudio.com/api/references/commands#commands)
// to open the document in the editor.
// The line and column information is displayed, but are not part of the link.
//
// At the time of writing this is the only way we know to create a direct
// link to a Q# document from a Web View.
//
// If we wanted to handle line/column information from the link, an alternate
// implementation might be having our own command that navigates to the correct
// location. Then this would be a link to that command instead. Yet another
// alternative is to have the webview pass a message back to the extension.
const uri = Uri.parse(maybeUri, true);
const fsPath = escapeHtml(basename(uri.path) ?? uri.fsPath);
const lineColumn = position
? escapeHtml(`:${position.line + 1}:${position.character + 1}`)
: "";

const locations = [
{
source: uri,
span: {
start: position,
end: position,
},
},
];

const args = customCommand && position ? [locations] : [uri];
const openCommand =
customCommand && position ? "qsharp-vscode.gotoLocations" : "vscode.open";

const argsStr = encodeURIComponent(JSON.stringify(args));
const openCommandUri = Uri.parse(`command:${openCommand}?${argsStr}`, true);
const title = `${fsPath}${lineColumn}`;
return `<a href="${openCommandUri}">${title}</a>`;
} catch {
// Likely could not parse document URI - it must be a project level error
// or an error from stdlib, use the document name directly
return escapeHtml(maybeUri);
}
}
96 changes: 96 additions & 0 deletions source/vscode/src/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,14 @@
// Licensed under the MIT License.
declare const __PLATFORM__: "browser" | "node";

import { escapeHtml } from "markdown-it/lib/common/utils.mjs";
import { TextDocument, Uri, Range, Location } from "vscode";
import {
getCompilerWorker,
ICompilerWorker,
ILocation,
IPosition,
IQSharpError,
IRange,
IWorkspaceEdit,
VSDiagnostic,
Expand Down Expand Up @@ -156,3 +159,96 @@ export function loadCompilerWorker(extensionUri: vscode.Uri): ICompilerWorker {
export function getPlatformEnv(): string {
return __PLATFORM__;
}

/**
* Formats an array of compiler/runtime errors into HTML to be presented to the user.
*
* @param errors The list of errors to format.
* @returns The HTML formatted errors, to be set as the inner contents of a container element.
*/
export function errorsToHtml(errors: IQSharpError[]) {
let errorHtml = "";
for (const error of errors) {
const { document, diagnostic: diag, stack: rawStack } = error;

const location = documentHtml(false, document, diag.range.start);
const message = escapeHtml(`(${diag.code}) ${diag.message}`).replace(
/\n/g,
"<br/><br/>",
);

errorHtml += `<p>${location}: ${message}<br/></p>`;

if (rawStack) {
const stack = rawStack
.split("\n")
.map((l) => {
// Link-ify the document names in the stack trace
const match = l.match(/^(\s*)at (.*) in (.*):(\d+):(\d+)/);
if (match) {
const [, leadingWs, callable, doc] = match;
return `${leadingWs}at ${escapeHtml(callable)} in ${documentHtml(false, doc)}`;
} else {
return l;
}
})

.join("\n");
errorHtml += `<br/><pre>${stack}</pre>`;
}
}
return errorHtml;
}

/**
* If the input is a URI, turns it into a document open link.
* Otherwise returns the HTML-escaped input
*/
function documentHtml(
customCommand: boolean,
maybeUri: string,
position?: IPosition,
) {
try {
// If the error location is a document URI, create a link to that document.
// We use the `vscode.open` command (https://code.visualstudio.com/api/references/commands#commands)
// to open the document in the editor.
// The line and column information is displayed, but are not part of the link.
//
// At the time of writing this is the only way we know to create a direct
// link to a Q# document from a Web View.
//
// If we wanted to handle line/column information from the link, an alternate
// implementation might be having our own command that navigates to the correct
// location. Then this would be a link to that command instead. Yet another
// alternative is to have the webview pass a message back to the extension.
const uri = Uri.parse(maybeUri, true);
const fsPath = escapeHtml(basename(uri.path) ?? uri.fsPath);
const lineColumn = position
? escapeHtml(`:${position.line + 1}:${position.character + 1}`)
: "";

const locations = [
{
source: uri,
span: {
start: position,
end: position,
},
},
];

const args = customCommand && position ? [locations] : [uri];
const openCommand =
customCommand && position ? "qsharp-vscode.gotoLocations" : "vscode.open";

const argsStr = encodeURIComponent(JSON.stringify(args));
const openCommandUri = Uri.parse(`command:${openCommand}?${argsStr}`, true);
const title = `${fsPath}${lineColumn}`;
return `<a href="${openCommandUri}">${title}</a>`;
} catch {
// Likely could not parse document URI - it must be a project level error
// or an error from stdlib, use the document name directly
return escapeHtml(maybeUri);
}
}
32 changes: 21 additions & 11 deletions source/vscode/src/debugger/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,18 @@ import {
IStructStepResult,
IVariable,
IVariableChild,
QdkDiagnostics,
QscEventTarget,
StepResultId,
log,
} from "qsharp-lang";
import { updateCircuitPanel } from "../circuit";
import { basename, isQdkDocument, toVsCodeRange } from "../common";
import {
basename,
isQdkDocument,
toVsCodeRange,
errorsToHtml,
} from "../common";
import {
DebugEvent,
EventType,
Expand Down Expand Up @@ -1109,24 +1115,28 @@ export class QscDebugSession extends LoggingDebugSession {
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
// a bit of refactoring.
const stack =
error && typeof error === "object" && typeof error.stack === "string"
? escapeHtml(error.stack)
: undefined;
let errorHtml: string | undefined;
if (error instanceof QdkDiagnostics) {
errorHtml = errorsToHtml(error.diagnostics);
} else if (error) {
const message = error instanceof Error ? error.message : String(error);
errorHtml = `<pre>${escapeHtml(message)}</pre>`;
}

const circuit = await this.debugService.getCircuit();
let circuit;
try {
circuit = await this.debugService.getCircuit();
} catch (e) {
log.error("Failed to retrieve circuit from debug service: %O", e);
}

updateCircuitPanel(
this.program.profile,
this.program.projectName,
!this.revealedCircuit,
{
circuit,
errorHtml: stack ? `<pre>${stack}</pre>` : undefined,
errorHtml,
simulated: true,
},
);
Expand Down
Loading
Loading