Skip to content

Commit

Permalink
Refactor: Consolidate "active Q# program" logic in VS Code (#1623)
Browse files Browse the repository at this point in the history
This is in preparation for the package work, but I think it's a useful
refactor in its own right. No behavior changes.

There are quite a few features/commands across the VS Code extension*
that are meant to operate on the "currently active Q# project". This is
defined as the Q# document in the currently active editor, and if that
document is associated with a qsharp.json, the entire project.

This code was duplicated across all these commands, and was slightly
different everywhere. There have been bugs where we weren't respecting
some of the common settings (e.g. #1565) and changes where we had to
update each command separately if we changed something about project
semantics (e.g. #1089, #918) . This PR aims to unify all that so we
touch fewer places when we change something about the project compiler
configuration logic.

*Run, Debug, Circuit, Estimate, Histogram commands, Show Documentation,
Get QIR and Submit to Azure.
  • Loading branch information
minestarks authored Jun 19, 2024
1 parent a064c88 commit d815424
Show file tree
Hide file tree
Showing 9 changed files with 210 additions and 143 deletions.
56 changes: 22 additions & 34 deletions vscode/src/circuit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,28 +8,24 @@ import {
IOperationInfo,
IQSharpError,
IRange,
ProgramConfig,
TargetProfile,
getCompilerWorker,
log,
} from "qsharp-lang";
import { Uri, window } from "vscode";
import { basename, isQsharpDocument } from "./common";
import { getTarget, getTargetFriendlyName } from "./config";
import { loadProject } from "./projectSystem";
import { Uri } from "vscode";
import { getTargetFriendlyName } from "./config";
import { clearCommandDiagnostics } from "./diagnostics";
import { FullProgramConfig, getActiveProgram } from "./programConfig";
import { EventType, UserFlowStatus, sendTelemetryEvent } from "./telemetry";
import { getRandomGuid } from "./utils";
import { sendMessageToPanel } from "./webviewPanel";
import { clearCommandDiagnostics } from "./diagnostics";

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

/**
* Input parameters for generating a circuit.
*/
type CircuitParams = {
program: ProgramConfig;
targetProfile: TargetProfile;
program: FullProgramConfig;
operation?: IOperationInfo;
};

Expand Down Expand Up @@ -60,20 +56,16 @@ export async function showCircuitCommand(
const associationId = getRandomGuid();
sendTelemetryEvent(EventType.TriggerCircuit, { associationId }, {});

const editor = window.activeTextEditor;
if (!editor || !isQsharpDocument(editor.document)) {
throw new Error("The currently active window is not a Q# file");
const program = await getActiveProgram();
if (!program.success) {
throw new Error(program.errorMsg);
}

const docUri = editor.document.uri;
const program = await loadProject(docUri);
const targetProfile = getTarget();

sendTelemetryEvent(
EventType.CircuitStart,
{
associationId,
targetProfile,
targetProfile: program.programConfig.profile,
isOperation: (!!operation).toString(),
},
{},
Expand All @@ -82,9 +74,8 @@ export async function showCircuitCommand(
// Generate the circuit and update the panel.
// generateCircuits() takes care of handling timeouts and
// falling back to the simulator for dynamic circuits.
const result = await generateCircuit(extensionUri, docUri, {
program: program,
targetProfile,
const result = await generateCircuit(extensionUri, {
program: program.programConfig,
operation,
});

Expand Down Expand Up @@ -127,15 +118,12 @@ export async function showCircuitCommand(
*/
async function generateCircuit(
extensionUri: Uri,
docUri: Uri,
params: CircuitParams,
): Promise<CircuitOrError> {
const programPath = docUri.path;

// Before we start, reveal the panel with the "calculating" spinner
updateCircuitPanel(
params.targetProfile,
programPath,
params.program.profile,
params.program.projectName,
true, // reveal
{ operation: params.operation, calculating: true },
);
Expand All @@ -152,8 +140,8 @@ async function generateCircuit(
// there was a result comparison (i.e. if this is a dynamic circuit)

updateCircuitPanel(
params.targetProfile,
programPath,
params.program.profile,
params.program.projectName,
false, // reveal
{
operation: params.operation,
Expand All @@ -174,8 +162,8 @@ async function generateCircuit(

if (result.result === "success") {
updateCircuitPanel(
params.targetProfile,
programPath,
params.program.profile,
params.program.projectName,
false, // reveal
{
circuit: result.circuit,
Expand All @@ -193,8 +181,8 @@ async function generateCircuit(
}

updateCircuitPanel(
params.targetProfile,
programPath,
params.program.profile,
params.program.projectName,
false, // reveal
{
errorHtml,
Expand Down Expand Up @@ -256,7 +244,7 @@ async function getCircuitOrError(
try {
const circuit = await worker.getCircuit(
params.program,
params.targetProfile,
params.program.profile,
simulate,
params.operation,
);
Expand Down Expand Up @@ -335,7 +323,7 @@ function errorsToHtml(errors: IQSharpError[]) {

export function updateCircuitPanel(
targetProfile: string,
programPath: string,
projectName: string,
reveal: boolean,
params: {
circuit?: CircuitData;
Expand All @@ -347,7 +335,7 @@ export function updateCircuitPanel(
) {
const title = params?.operation
? `${params.operation.operation} with ${params.operation.totalNumQubits} input qubits`
: basename(programPath) || "Circuit";
: projectName;

// Trim the Q#: prefix from the target profile name - that's meant for the ui text in the status bar
const target = `Target profile: ${getTargetFriendlyName(targetProfile).replace("Q#: ", "")} `;
Expand Down
42 changes: 17 additions & 25 deletions vscode/src/debugger/activate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@
/* eslint-disable @typescript-eslint/no-unused-vars */

import { IDebugServiceWorker, getDebugServiceWorker, log } from "qsharp-lang";
import { qsharpExtensionId, isQsharpDocument } from "../common";
import { QscDebugSession } from "./session";
import { getRandomGuid } from "../utils";

import * as vscode from "vscode";
import { loadProject } from "../projectSystem";
import { qsharpExtensionId } from "../common";
import { clearCommandDiagnostics } from "../diagnostics";
import {
getActiveQSharpDocumentUri,
getProgramForDocument,
} from "../programConfig";
import { getRandomGuid } from "../utils";
import { QscDebugSession } from "./session";

let debugServiceWorkerFactory: () => IDebugServiceWorker;

Expand Down Expand Up @@ -82,10 +84,7 @@ function registerCommands(context: vscode.ExtensionContext) {
return;
}

let targetResource = resource;
if (!targetResource && vscode.window.activeTextEditor) {
targetResource = vscode.window.activeTextEditor.document.uri;
}
const targetResource = resource || getActiveQSharpDocumentUri();

if (targetResource) {
config.programUri = targetResource.toString();
Expand Down Expand Up @@ -116,12 +115,12 @@ class QsDebugConfigProvider implements vscode.DebugConfigurationProvider {
): vscode.ProviderResult<vscode.DebugConfiguration> {
// if launch.json is missing or empty
if (!config.type && !config.request && !config.name) {
const editor = vscode.window.activeTextEditor;
if (editor && isQsharpDocument(editor.document)) {
const docUri = getActiveQSharpDocumentUri();
if (docUri) {
config.type = "qsharp";
config.name = "Launch";
config.request = "launch";
config.programUri = editor.document.uri.toString();
config.programUri = docUri.toString();
config.shots = 1;
config.noDebug = "noDebug" in config ? config.noDebug : false;
config.stopOnEntry = !config.noDebug;
Expand Down Expand Up @@ -149,16 +148,6 @@ class QsDebugConfigProvider implements vscode.DebugConfigurationProvider {
path: fileUri.path,
})
.toString();
} else if (!config.programUri) {
// We shouldn't hit this in practice
log.warn(
"Cannot find a Q# program to debug, defaulting to active editor",
);
// Use the active editor if no program is specified.
const editor = vscode.window.activeTextEditor;
if (editor && isQsharpDocument(editor.document)) {
config.programUri = editor.document.uri.toString();
}
}

log.trace(
Expand Down Expand Up @@ -212,12 +201,15 @@ class InlineDebugAdapterFactory
): Promise<vscode.DebugAdapterDescriptor> {
const worker = debugServiceWorkerFactory();
const uri = vscode.Uri.parse(session.configuration.programUri);
const project = await loadProject(uri);
const program = await getProgramForDocument(uri);
if (!program.success) {
throw new Error(program.errorMsg);
}

const qscSession = new QscDebugSession(
worker,
session.configuration,
project.sources,
project.languageFeatures,
program.programConfig,
);

await qscSession.init(getRandomGuid());
Expand Down
20 changes: 10 additions & 10 deletions vscode/src/debugger/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
IStructStepResult,
QscEventTarget,
StepResultId,
TargetProfile,
log,
} from "qsharp-lang";
import { updateCircuitPanel } from "../circuit";
Expand All @@ -43,6 +44,7 @@ import { createDebugConsoleEventTarget } from "./output";
import { ILaunchRequestArguments } from "./types";
import { escapeHtml } from "markdown-it/lib/common/utils.mjs";
import { isPanelOpen } from "../webviewPanel";
import { FullProgramConfig } from "../programConfig";

const ErrorProgramHasErrors =
"program contains compile errors(s): cannot run. See debug console for more details.";
Expand Down Expand Up @@ -73,14 +75,12 @@ export class QscDebugSession extends LoggingDebugSession {
private failureMessage: string;
private eventTarget: QscEventTarget;
private supportsVariableType = false;
private targetProfile = getTarget();
private revealedCircuit = false;

public constructor(
private debugService: IDebugServiceWorker,
private config: vscode.DebugConfiguration,
private sources: [string, string][],
private languageFeatures: string[],
private program: FullProgramConfig,
) {
super();

Expand All @@ -94,7 +94,7 @@ export class QscDebugSession extends LoggingDebugSession {
this.setDebuggerLinesStartAt1(false);
this.setDebuggerColumnsStartAt1(false);

for (const source of sources) {
for (const source of program.sources) {
const uri = vscode.Uri.parse(source[0], true);

// In Debug Protocol requests, the VS Code debug adapter client
Expand All @@ -112,12 +112,12 @@ export class QscDebugSession extends LoggingDebugSession {
const start = performance.now();
sendTelemetryEvent(EventType.InitializeRuntimeStart, { associationId }, {});
const failureMessage = await this.debugService.loadSource(
this.sources,
this.targetProfile,
this.program.sources,
this.program.profile,
this.config.entry,
this.languageFeatures,
this.program.languageFeatures,
);
for (const [path, _contents] of this.sources) {
for (const [path, _contents] of this.program.sources) {
if (failureMessage == "") {
const locations = await this.debugService.getBreakpoints(path);
log.trace(`init breakpointLocations: %O`, locations);
Expand Down Expand Up @@ -947,8 +947,8 @@ export class QscDebugSession extends LoggingDebugSession {
const circuit = await this.debugService.getCircuit();

updateCircuitPanel(
this.targetProfile,
vscode.Uri.parse(this.sources[0][0]).path,
this.program.profile,
this.program.projectName,
!this.revealedCircuit,
{
circuit,
Expand Down
24 changes: 10 additions & 14 deletions vscode/src/documentation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,39 +2,35 @@
// Licensed under the MIT License.

import { getCompilerWorker } from "qsharp-lang";
import { isQsharpDocument } from "./common";
import { getTarget } from "./config";
import { Uri, window } from "vscode";
import { loadProject } from "./projectSystem";
import { Uri } from "vscode";
import { sendMessageToPanel } from "./webviewPanel";
import { getActiveProgram } from "./programConfig";

export async function showDocumentationCommand(extensionUri: Uri) {
const editor = window.activeTextEditor;
if (!editor || !isQsharpDocument(editor.document)) {
throw new Error("The currently active window is not a Q# file");
const program = await getActiveProgram();
if (!program.success) {
throw new Error(program.errorMsg);
}

const { sources, profile, languageFeatures } = program.programConfig;

// Reveal panel and show 'Loading...' for immediate feedback.
sendMessageToPanel(
"documentation", // This is needed to route the message to the proper panel
true, // Reveal panel
null, // With no message
);

const docUri = editor.document.uri;
const program = await loadProject(docUri);
const targetProfile = getTarget();

// Get API documentation from compiler.
const compilerWorkerScriptPath = Uri.joinPath(
extensionUri,
"./out/compilerWorker.js",
).toString();
const worker = getCompilerWorker(compilerWorkerScriptPath);
const docFiles = await worker.getDocumentation(
program.sources,
targetProfile,
program.languageFeatures,
sources,
profile,
languageFeatures,
);

const documentation: string[] = [];
Expand Down
Loading

0 comments on commit d815424

Please sign in to comment.