Skip to content

Commit

Permalink
"Import External Q# Package" VS Code command palette command (#2079)
Browse files Browse the repository at this point in the history
This PR formalizes the "registry" md document into a JSON file, and then
adds a command palette command which consumes it. This will hopefully
increase discoverability of the project system, and formalizes our
listing of known libraries a bit more.


https://github.com/user-attachments/assets/20f8dd7b-5b53-4b03-8990-aef41d6b3da7

---------

Co-authored-by: Bill Ticehurst <[email protected]>
Co-authored-by: Mine Starks <[email protected]>
  • Loading branch information
3 people authored Jan 25, 2025
1 parent bd5a09c commit 598fbe3
Show file tree
Hide file tree
Showing 4 changed files with 222 additions and 115 deletions.
22 changes: 0 additions & 22 deletions library/Registry.md

This file was deleted.

2 changes: 1 addition & 1 deletion library/fixed_point/qsharp.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,4 @@
"src/Tests.qs",
"src/Types.qs"
]
}
}
249 changes: 157 additions & 92 deletions vscode/src/createProject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import * as vscode from "vscode";
import { log, samples } from "qsharp-lang";
import { EventType, sendTelemetryEvent } from "./telemetry";
import { qsharpExtensionId } from "./common";
import registryJson from "./registry.json";

export async function initProjectCreator(context: vscode.ExtensionContext) {
context.subscriptions.push(
Expand Down Expand Up @@ -40,7 +41,7 @@ export async function initProjectCreator(context: vscode.ExtensionContext) {

const sample = samples.find((elem) => elem.title === "Minimal");
if (!sample) {
// Should never happen.
// Should never happen, because we bake this sample in.
log.error("Unable to find the Minimal sample");
return;
}
Expand Down Expand Up @@ -177,23 +178,12 @@ export async function initProjectCreator(context: vscode.ExtensionContext) {
repo: string;
ref: string;
path?: string; // Optional, defaults to the root of the repo
refs: undefined;
};
};

type Dependency = LocalProjectRef | GitHubProjectRef;

// TODO: Replace with a list of legitimate known Q# projects on GitHub
const githubProjects: { [name: string]: GitHubProjectRef } = {
// Add a template to the end of the list users can use to easily add their own
"<id>": {
github: {
owner: "<owner>",
repo: "<project>",
ref: "<commit>",
},
},
};

// Given two directory paths, return the relative path from the first to the second
function getRelativeDirPath(from: string, to: string): string {
// Ensure we have something
Expand Down Expand Up @@ -265,93 +255,168 @@ export async function initProjectCreator(context: vscode.ExtensionContext) {
return;
}

// Find all the other Q# projects in the workspace
const projectFiles = (
await vscode.workspace.findFiles("**/qsharp.json")
).filter((file) => file.toString() !== qsharpJsonUri.toString());

const projectChoices: Array<{ name: string; ref: Dependency }> = [];

projectFiles.forEach((file) => {
const dirName = file.path.slice(0, -"/qsharp.json".length);
const relPath = getRelativeDirPath(qsharpJsonDir.path, dirName);
projectChoices.push({
name: dirName.slice(dirName.lastIndexOf("/") + 1),
ref: {
path: relPath,
},
});
});

Object.keys(githubProjects).forEach((name) => {
projectChoices.push({
name: name,
ref: githubProjects[name],
});
});

// Convert any spaces, dashes, dots, tildes, or quotes in project names
// to underscores. (Leave more 'exotic' non-identifier patterns to the user to fix)
//
// Note: At some point we may want to detect/avoid duplicate names, e.g. if the user already
// references a project via 'foo', and they add a reference to a 'foo' on GitHub or in another dir.
projectChoices.forEach(
(val, idx, arr) =>
(arr[idx].name = val.name.replace(/[- "'.~]/g, "_")),
);

const folderIcon = new vscode.ThemeIcon("folder");
const githubIcon = new vscode.ThemeIcon("github");

// Ask the user to pick a project to add as a reference
const projectChoice = await vscode.window.showQuickPick(
projectChoices.map((choice) => {
if ("github" in choice.ref) {
return {
label: choice.name,
detail: `github://${choice.ref.github.owner}/${choice.ref.github.repo}#${choice.ref.github.ref}`,
iconPath: githubIcon,
ref: choice.ref,
};
} else {
return {
label: choice.name,
detail: choice.ref.path,
iconPath: folderIcon,
ref: choice.ref,
};
}
}),
{ placeHolder: "Pick a project to add as a reference" },
const importChoice = await vscode.window.showQuickPick(
["Import from GitHub", "Import from local directory"],
{ placeHolder: "Pick a source to import from" },
);

if (!projectChoice) {
log.info("User cancelled project choice");
if (!importChoice) {
log.info("User cancelled import choice");
return;
}

log.info("User picked project: ", projectChoice);

if (!manifestObj["dependencies"]) manifestObj["dependencies"] = {};
manifestObj["dependencies"][projectChoice.label] = projectChoice.ref;

// Apply the edits to the qsharp.json
const edit = new vscode.WorkspaceEdit();
edit.replace(
qsharpJsonUri,
new vscode.Range(0, 0, qsharpJsonDoc.lineCount, 0),
JSON.stringify(manifestObj, null, 2),
);
if (!(await vscode.workspace.applyEdit(edit))) {
vscode.window.showErrorMessage(
"Unable to update the qsharp.json file. Check the file is writable",
if (importChoice === "Import from GitHub") {
await importPackage(qsharpJsonDoc, qsharpJsonUri, manifestObj, true);
} else {
await importPackage(
qsharpJsonDoc,
qsharpJsonUri,
manifestObj,
false,
qsharpJsonDir,
);
return;
}

// Bring the qsharp.json to the front for the user to save
await vscode.window.showTextDocument(qsharpJsonDoc);
},
),
);

async function importPackage(
qsharpJsonDoc: vscode.TextDocument,
qsharpJsonUri: vscode.Uri,
manifestObj: any,
isGitHub: boolean,
qsharpJsonDir?: vscode.Uri,
) {
let dependencyRef: Dependency;
let label: string;

if (isGitHub) {
const packageChoice = await vscode.window.showQuickPick(
registryJson.knownPackages.map(
(pkg: { name: string; description: string; dependency: object }) => ({
label: pkg.name,
description: pkg.description,
}),
),
{ placeHolder: "Pick a package to import" },
);

if (!packageChoice) {
log.info("User cancelled package choice");
return;
}

const chosenPackage = registryJson.knownPackages.find(
(pkg: { name: string; description: string; dependency: object }) =>
pkg.name === packageChoice.label,
)!;

const versionChoice = await vscode.window.showQuickPick(
chosenPackage.dependency.github.refs.map(({ ref, notes }) => ({
label: ref,
description: notes,
})),
{ placeHolder: "Pick a version to import" },
);

if (!versionChoice) {
log.info("User cancelled version choice");
return;
}

dependencyRef = {
github: {
ref: versionChoice.label,
...chosenPackage.dependency.github,
refs: undefined,
},
};

label = packageChoice.label;
} else {
// Find all the other Q# projects in the workspace
const projectFiles = (
await vscode.workspace.findFiles("**/qsharp.json")
).filter((file) => file.toString() !== qsharpJsonUri.toString());

// Convert any spaces, dashes, dots, tildes, or quotes in project names
// to underscores. (Leave more 'exotic' non-identifier patterns to the user to fix)
//
// Note: At some point we may want to detect/avoid duplicate names, e.g. if the user already
// references a project via 'foo', and they add a reference to a 'foo' on GitHub or in another dir.

const projectChoices = projectFiles.map((file) => {
// normalize the path using the vscode Uri API
const dirUri = vscode.Uri.joinPath(file, "..");
const relPath = getRelativeDirPath(qsharpJsonDir!.path, dirUri.path);
return {
name: dirUri.path.split("/").pop()!,
ref: {
path: relPath,
},
};
});

projectChoices.forEach(
(val, idx, arr) => (arr[idx].name = val.name.replace(/[- "'.~]/g, "_")),
);

const folderIcon = new vscode.ThemeIcon("folder");

// Ask the user to pick a project to add as a reference
const projectChoice = await vscode.window.showQuickPick(
projectChoices.map((choice) => ({
label: choice.name,
detail: choice.ref.path,
iconPath: folderIcon,
ref: choice.ref,
})),
{ placeHolder: "Pick a project to add as a reference" },
);

if (!projectChoice) {
log.info("User cancelled project choice");
return;
}

dependencyRef = projectChoice.ref;
label = projectChoice.label;
}

await updateManifestAndSave(
qsharpJsonDoc,
qsharpJsonUri,
manifestObj,
label,
dependencyRef,
);
}

async function updateManifestAndSave(
qsharpJsonDoc: vscode.TextDocument,
qsharpJsonUri: vscode.Uri,
manifestObj: any,
label: string,
ref: Dependency,
) {
if (!manifestObj["dependencies"]) manifestObj["dependencies"] = {};
manifestObj["dependencies"][label] = ref;

// Apply the edits to the qsharp.json
const edit = new vscode.WorkspaceEdit();
edit.replace(
qsharpJsonUri,
new vscode.Range(0, 0, qsharpJsonDoc.lineCount, 0),
JSON.stringify(manifestObj, null, 4),
);
if (!(await vscode.workspace.applyEdit(edit))) {
await vscode.window.showErrorMessage(
"Unable to update the qsharp.json file. Check the file is writable",
);
return;
}

// Bring the qsharp.json to the front for the user to save
await vscode.window.showTextDocument(qsharpJsonDoc);
}
}
64 changes: 64 additions & 0 deletions vscode/src/registry.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
{
"knownPackages": [
{
"name": "Signed",
"description": "Defines types and functions to work with signed qubit-based integers.",
"dependency": {
"github": {
"owner": "microsoft",
"repo": "qsharp",
"refs": [
{ "ref": "bd5a09c", "notes": "latest stable" },
{ "ref": "main", "notes": "nightly, unstable" }
],
"path": "library/signed"
}
}
},
{
"name": "FixedPoint",
"description": "Types and functions for fixed-point arithmetic with qubits.",
"dependency": {
"github": {
"owner": "microsoft",
"repo": "qsharp",
"refs": [
{ "ref": "bd5a09c", "notes": "latest stable" },
{ "ref": "main", "notes": "nightly, unstable" }
],
"path": "library/signed"
}
}
},
{
"name": "Rotations",
"description": "Defines types and functions to work with rotations.",
"dependency": {
"github": {
"owner": "microsoft",
"repo": "qsharp",
"refs": [
{ "ref": "bd5a09c", "notes": "latest stable" },
{ "ref": "main", "notes": "nightly, unstable" }
],
"path": "library/rotations"
}
}
},
{
"name": "Qtest",
"description": "Utilities for writing and running Q# tests.",
"dependency": {
"github": {
"owner": "microsoft",
"repo": "qsharp",
"refs": [
{ "ref": "bd5a09c", "notes": "latest stable" },
{ "ref": "main", "notes": "nightly, unstable" }
],
"path": "library/qtest"
}
}
}
]
}

0 comments on commit 598fbe3

Please sign in to comment.