Skip to content

Commit

Permalink
Add a qsharp.json context menu and palette command to add a project r…
Browse files Browse the repository at this point in the history
…eference (#1771)

On invocation, a list of available projects to reference from the local
workspace and GitHub (the GitHub list to be populated as they become
available), or just a template for the user to complete.

On selection, the changes are made to the qsharp.json for the user to
review and save.

After invocation the drop-down appears similar to the below:

<img width="729" alt="image"
src="https://github.com/user-attachments/assets/3da3b381-0aab-4025-a327-f9867d5a38d4">
  • Loading branch information
billti authored Jul 23, 2024
1 parent 4dd3044 commit 4f91ae5
Show file tree
Hide file tree
Showing 2 changed files with 224 additions and 0 deletions.
13 changes: 13 additions & 0 deletions vscode/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,10 @@
{
"command": "qsharp-vscode.populateFilesList",
"when": "resourceFilename == qsharp.json"
},
{
"command": "qsharp-vscode.addProjectReference",
"when": "resourceFilename == qsharp.json"
}
],
"view/title": [
Expand Down Expand Up @@ -241,6 +245,10 @@
{
"command": "qsharp-vscode.populateFilesList",
"when": "resourceFilename == qsharp.json"
},
{
"command": "qsharp-vscode.addProjectReference",
"when": "resourceFilename == qsharp.json"
}
]
},
Expand Down Expand Up @@ -378,6 +386,11 @@
"command": "qsharp-vscode.populateFilesList",
"category": "Q#",
"title": "Populate qsharp.json files list"
},
{
"command": "qsharp-vscode.addProjectReference",
"category": "Q#",
"title": "Add Q# project reference"
}
],
"breakpoints": [
Expand Down
211 changes: 211 additions & 0 deletions vscode/src/createProject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,22 @@ export async function initProjectCreator(context: vscode.ExtensionContext) {
const files: string[] = [];
const srcDir = vscode.Uri.joinPath(qsharpJsonUri, "..", "src");

// Verify the src directory exists
try {
const srcDirStat = await vscode.workspace.fs.stat(srcDir);
if (srcDirStat.type !== vscode.FileType.Directory) {
await vscode.window.showErrorMessage(
"The ./src path is not a directory",
);
return;
}
} catch (err) {
await vscode.window.showErrorMessage(
"The ./src directory does not exist. Create the directory and add .qs files to it",
);
return;
}

async function getQsFilesInDir(dir: vscode.Uri) {
const dirFiles = (await vscode.workspace.fs.readDirectory(dir)).sort(
(a, b) => {
Expand Down Expand Up @@ -149,4 +165,199 @@ export async function initProjectCreator(context: vscode.ExtensionContext) {
},
),
);

type LocalProjectRef = {
path: string; // Absolute or relative path to the project dir
};

type GitHubProjectRef = {
github: {
owner: string;
repo: string;
ref: string;
path?: string; // Optional, defaults to the root of the repo
};
};

type Dependency = LocalProjectRef | GitHubProjectRef;

// TODO: Replace with a list of legitimate known Q# projects on GitHub
const githubProjects: { [name: string]: GitHubProjectRef } = {
assert: {
github: {
owner: "swernli",
repo: "MyQsLib",
ref: "29dc67f",
},
},
// 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
if (!from || !to) throw "Invalid arguments";

// Trim trailing slashes (even from the root "/" case)
if (from.endsWith("/")) from = from.slice(0, -1);
if (to.endsWith("/")) to = to.slice(0, -1);

// Break both paths into their components
const fromParts = from.split("/");
const toParts = to.split("/");

// Remove the common beginning of the paths
while (fromParts[0] === toParts[0]) {
fromParts.shift();
toParts.shift();
}

// Add a .. for each remaining part in the from path
let result = "";
while (fromParts.length) {
result += "../";
fromParts.shift();
}
// Add the remaining path from the to path
result += toParts.join("/");
if (result.endsWith("/")) {
result = result.slice(0, -1);
}
return result;
}

context.subscriptions.push(
vscode.commands.registerCommand(
"qsharp-vscode.addProjectReference",
async (qsharpJsonUri: vscode.Uri | undefined) => {
// If called from the content menu qsharpJsonUri will be the full qsharp.json uri
// If called from the command palette is will be undefined, so use the active editor
log.info("addProjectReference called with", qsharpJsonUri);

qsharpJsonUri =
qsharpJsonUri ?? vscode.window.activeTextEditor?.document.uri;
if (!qsharpJsonUri) {
log.error(
"addProjectReference called, but argument or active editor is not qsharp.json",
);
return;
}

log.debug("Adding project reference to ", qsharpJsonUri.path);

// First, verify the qsharp.json can be opened and is a valid json file
const qsharpJsonDoc =
await vscode.workspace.openTextDocument(qsharpJsonUri);
if (!qsharpJsonDoc) {
log.error("Unable to open the qsharp.json file at ", qsharpJsonDoc);
return;
}
const qsharpJsonDir = vscode.Uri.joinPath(qsharpJsonUri, "..");

let manifestObj: any = {};
try {
manifestObj = JSON.parse(qsharpJsonDoc.getText());
} catch (err: any) {
await vscode.window.showErrorMessage(
`Unable to parse the contents of ${qsharpJsonUri.path}`,
);
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" },
);

if (!projectChoice) {
log.info("User cancelled project 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",
);
return;
}

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

0 comments on commit 4f91ae5

Please sign in to comment.