diff --git a/src/VSCodeExtension/.vscodeignore b/src/VSCodeExtension/.vscodeignore index 5231325bff..ffb0eb5ec9 100644 --- a/src/VSCodeExtension/.vscodeignore +++ b/src/VSCodeExtension/.vscodeignore @@ -3,10 +3,10 @@ out/test/** out/**/*.map src/** -.gitignore +**/.gitignore tsconfig.json vsc-extension-quickstart.md tslint.json Build-Dependencies.ps1 BUILDING.md -*.v.template +**/*.v.template diff --git a/src/VSCodeExtension/package.json.v.template b/src/VSCodeExtension/package.json.v.template index 57cc891af0..ddcd4518ab 100644 --- a/src/VSCodeExtension/package.json.v.template +++ b/src/VSCodeExtension/package.json.v.template @@ -36,7 +36,7 @@ "commands": [ { "command": "quantum.installTemplates", - "title": "Q#: Install project templates" + "title": "Q#: Install command line project templates" }, { @@ -114,6 +114,9 @@ "vscode-extension-telemetry": "0.0.18", "vscode-languageclient": "5.1.0", "which": "1.3.1", + "yeoman-environment": "^2.10.3", + "yeoman-generator": "^2.0.1", + "yosay": "^2.0.1", "@types/fs-extra": "^8.0.0" }, "devDependencies": { @@ -121,6 +124,11 @@ "typescript": "^2.6.1", "tslint": "^5.8.0", "mocha": "^5.2.0", + "@types/yeoman-generator": "^3.1.4", + "@types/yosay": "^0.0.29", + "yeoman-test": "^1.7.0", + "yeoman-assert": "^3.1.0", + "@types/yeoman-environment": "^2.3.3", "@types/node": "^9.6.5", "@types/mocha": "^5.2.0", "@types/which": "1.3.1", diff --git a/src/VSCodeExtension/src/commands.ts b/src/VSCodeExtension/src/commands.ts index 37c6778afb..f45b11f6a2 100644 --- a/src/VSCodeExtension/src/commands.ts +++ b/src/VSCodeExtension/src/commands.ts @@ -10,6 +10,9 @@ import { IPackageInfo } from './packageInfo'; import * as semver from 'semver'; import { promisify } from 'util'; import { oc } from 'ts-optchain'; +import { QSharpGenerator } from './yeoman-generator'; + +import * as yeoman from 'yeoman-environment'; export function registerCommand(context: vscode.ExtensionContext, name: string, action: () => void) { context.subscriptions.push( @@ -22,107 +25,17 @@ export function registerCommand(context: vscode.ExtensionContext, name: string, ) } -function createNewProjectAtUri(dotNetSdk: DotnetInfo, projectType: string, uri: vscode.Uri) { - let proc = cp.spawn( - dotNetSdk.path, - ["new", projectType, "-lang", "Q#", "-o", uri.fsPath] - ); - - let errorMessage = ""; - proc.stderr.on('data', data => { - errorMessage = errorMessage + data; - }); - - proc.on( - 'exit', (code, signal) => { - if (code === 0) { - const openItem = "Open new project..."; - vscode.window.showInformationMessage( - `Successfully created new project at ${uri.fsPath}.`, - openItem - ).then( - (item) => { - if (item === openItem) { - vscode.commands.executeCommand( - "vscode.openFolder", - uri - ).then( - (value) => {}, - (value) => { - vscode.window.showErrorMessage("Could not open new project"); - } - ); - } - } - ); - } else { - // Check if the problem was that the project templates are missing. - // If so, we can give a more helpful error message and offer the - // user to install templates. - if (errorMessage.includes("Q#") && errorMessage.includes("-lang")) { - const installTemplatesItem = "Install project templates and retry"; - vscode.window.showErrorMessage( - "Project creation failed. The Q# project templates may not be installed.", - installTemplatesItem - ) - .then( - item => { - if (item === installTemplatesItem) { - vscode.commands.executeCommand( - "quantum.installTemplates" - ).then( - () => createNewProjectAtUri(dotNetSdk, projectType, uri) - ); - } - } - ); - } else { - vscode.window.showErrorMessage( - `.NET Core SDK exited with code ${code} when creating a new project:\n${errorMessage}` - ); - } - } +export function createNewProject(context: vscode.ExtensionContext) { + let env = yeoman.createEnv(); + env.options.extensionPath = context.extensionPath; + env.registerStub(QSharpGenerator, 'qsharp:app'); + env.run('qsharp:app', (err: null | Error) => { + if (err) { + let errorMessage = err.name + ": " + err.message; + console.log(errorMessage); + vscode.window.showErrorMessage(errorMessage); } - ); -} - -export function createNewProject(dotNetSdk: DotnetInfo) { - const projectTypes: {[key: string]: string} = { - "Standalone console application": "console", - "Quantum library": "classlib", - "Unit testing project": "xunit" - }; - vscode.window.showQuickPick( - Object.keys(projectTypes) - ).then( - projectTypeSelection => { - if (projectTypeSelection === undefined) { - throw undefined; - } - let projectType = projectTypes[projectTypeSelection]; - - vscode.window.showSaveDialog({ - saveLabel: "Create Project" - }).then( - (uri) => { - if (uri !== undefined) { - if (uri.scheme !== "file") { - vscode.window.showErrorMessage( - "New projects must be saved to the filesystem." - ); - throw new Error("URI scheme was not file."); - } - else { - return uri; - } - } else { - throw undefined; - } - } - ) - .then(uri => createNewProjectAtUri(dotNetSdk, projectType, uri)); - } - ); + }); } export function installTemplates(dotNetSdk: DotnetInfo, packageInfo?: IPackageInfo) { diff --git a/src/VSCodeExtension/src/extension.ts b/src/VSCodeExtension/src/extension.ts index f887d417b1..7debb216c8 100644 --- a/src/VSCodeExtension/src/extension.ts +++ b/src/VSCodeExtension/src/extension.ts @@ -63,7 +63,7 @@ export async function activate(context: vscode.ExtensionContext) { context, "quantum.newProject", () => { - requireDotNetSdk(dotNetSdkVersion).then(createNewProject); + createNewProject(context); } ); diff --git a/src/VSCodeExtension/src/yeoman-generator.ts b/src/VSCodeExtension/src/yeoman-generator.ts new file mode 100644 index 0000000000..189610a73c --- /dev/null +++ b/src/VSCodeExtension/src/yeoman-generator.ts @@ -0,0 +1,123 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import * as vscode from 'vscode'; +import * as fs from 'fs-extra'; +import * as path from 'path'; +import yo = require("yeoman-generator"); +import yosay = require("yosay"); + +export class QSharpGenerator extends yo { + + constructor(args : any, opts : any) { + super(args, opts); + console.log( + yosay("Welcome to the Q# generator!") + ); + + this.sourceRoot(path.join(this.options.extensionPath, "templates")); + } + + prompting() { + let done = this.async(); + + // This dictionary maps the public description of the project type to the name + // of the folder with the corresponding template files. + const projectTypes: {[key: string]: string} = { + "Standalone console application": "application", + "Quantum library": "library", + "Unit testing project": "unittest" + }; + + vscode.window.showQuickPick( + Object.keys(projectTypes) + ).then( + projectTypeSelection => { + if (projectTypeSelection === undefined) { + throw undefined; + } + + vscode.window.showSaveDialog({ + saveLabel: "Create Project" + }).then( + (uri) => { + if (uri !== undefined) { + if (uri.scheme !== "file") { + vscode.window.showErrorMessage( + "New projects must be saved to the filesystem." + ); + throw new Error("URI scheme was not file."); + } + else { + return uri; + } + } else { + throw undefined; + } + } + ) + .then(uri => { + this.options.projectType = projectTypes[projectTypeSelection]; + this.options.outputUri = uri; + done(); + }); + } + ); + } + + writing() { + console.log( + yosay("Creating Q# project.") + ); + + let sourceDir = path.join(this.templatePath(), this.options.projectType); + let targetDir = this.options.outputUri.fsPath; + fs.mkdir(targetDir); + + // Namespace is the directory name itself. + let dirs = targetDir.split(path.sep); + + // In case there is a trailing separator. + let namespaceName = dirs.pop() || dirs.pop(); + + fs.readdir(sourceDir, (err, files) => { + if (err){ + throw err; + } + files.forEach( (filename) => { + let destinationName = filename; + let fileExtension = filename.split(".").pop(); + + if (fileExtension && fileExtension.toLowerCase() === "csproj") { + destinationName = namespaceName + ".csproj"; + } + + this.fs.copyTpl( + path.join(sourceDir, filename), + path.join(targetDir, destinationName), + { name: namespaceName } + ); + }); + }); + + const openItem = "Open new project..."; + vscode.window.showInformationMessage( + `Successfully created new project at ${targetDir}.`, + openItem + ).then( + (item) => { + if (item === openItem) { + vscode.commands.executeCommand( + "vscode.openFolder", + this.options.outputUri + ).then( + (value) => { }, + (value) => { + vscode.window.showErrorMessage("Could not open new project"); + } + ); + } + } + ); + } +} diff --git a/src/VSCodeExtension/templates/application/.gitignore b/src/VSCodeExtension/templates/application/.gitignore new file mode 100644 index 0000000000..2b04328876 --- /dev/null +++ b/src/VSCodeExtension/templates/application/.gitignore @@ -0,0 +1,2 @@ +# Generated or copied files +*.csproj diff --git a/src/VSCodeExtension/templates/application/Application.csproj.v.template b/src/VSCodeExtension/templates/application/Application.csproj.v.template new file mode 100644 index 0000000000..2061e46bde --- /dev/null +++ b/src/VSCodeExtension/templates/application/Application.csproj.v.template @@ -0,0 +1,8 @@ + + + + Exe + netcoreapp3.1 + + + diff --git a/src/VSCodeExtension/templates/application/Program.qs b/src/VSCodeExtension/templates/application/Program.qs new file mode 100644 index 0000000000..969e2dcce0 --- /dev/null +++ b/src/VSCodeExtension/templates/application/Program.qs @@ -0,0 +1,10 @@ +namespace <%= name %> { + open Microsoft.Quantum.Canon; + open Microsoft.Quantum.Intrinsic; + + + @EntryPoint() + operation SayHello() : Unit { + Message("Hello quantum world!"); + } +} diff --git a/src/VSCodeExtension/templates/library/.gitignore b/src/VSCodeExtension/templates/library/.gitignore new file mode 100644 index 0000000000..2b04328876 --- /dev/null +++ b/src/VSCodeExtension/templates/library/.gitignore @@ -0,0 +1,2 @@ +# Generated or copied files +*.csproj diff --git a/src/VSCodeExtension/templates/library/Library.csproj.v.template b/src/VSCodeExtension/templates/library/Library.csproj.v.template new file mode 100644 index 0000000000..b5159e47ee --- /dev/null +++ b/src/VSCodeExtension/templates/library/Library.csproj.v.template @@ -0,0 +1,7 @@ + + + + netstandard2.1 + + + diff --git a/src/VSCodeExtension/templates/library/Library.qs b/src/VSCodeExtension/templates/library/Library.qs new file mode 100644 index 0000000000..54a38c7587 --- /dev/null +++ b/src/VSCodeExtension/templates/library/Library.qs @@ -0,0 +1,9 @@ +namespace <%= name %> { + open Microsoft.Quantum.Canon; + open Microsoft.Quantum.Intrinsic; + + + operation SayHello() : Unit { + Message("Hello quantum world!"); + } +} diff --git a/src/VSCodeExtension/templates/unittest/.gitignore b/src/VSCodeExtension/templates/unittest/.gitignore new file mode 100644 index 0000000000..2b04328876 --- /dev/null +++ b/src/VSCodeExtension/templates/unittest/.gitignore @@ -0,0 +1,2 @@ +# Generated or copied files +*.csproj diff --git a/src/VSCodeExtension/templates/unittest/Test.csproj.v.template b/src/VSCodeExtension/templates/unittest/Test.csproj.v.template new file mode 100644 index 0000000000..40bccfea26 --- /dev/null +++ b/src/VSCodeExtension/templates/unittest/Test.csproj.v.template @@ -0,0 +1,16 @@ + + + + netcoreapp3.1 + false + + + + + + + + + + + diff --git a/src/VSCodeExtension/templates/unittest/Tests.qs b/src/VSCodeExtension/templates/unittest/Tests.qs new file mode 100644 index 0000000000..901bb6b366 --- /dev/null +++ b/src/VSCodeExtension/templates/unittest/Tests.qs @@ -0,0 +1,16 @@ +namespace <%= name %> { + open Microsoft.Quantum.Canon; + open Microsoft.Quantum.Diagnostics; + open Microsoft.Quantum.Intrinsic; + + + @Test("QuantumSimulator") + operation AllocateQubit() : Unit { + + using (q = Qubit()) { + Assert([PauliZ], [q], Zero, "Newly allocated qubit must be in |0> state."); + } + + Message("Test passed."); + } +}