diff --git a/extension/package.json b/extension/package.json index 55b7d75f9..f9089f086 100644 --- a/extension/package.json +++ b/extension/package.json @@ -331,6 +331,16 @@ { "command": "gradle.hideStoppedDaemons", "title": "Hide Stopped Daemons" + }, + { + "command": "gradle.createProject", + "category": "Gradle", + "title": "Create a Gradle Java Project..." + }, + { + "command": "gradle.createProjectAdvanced", + "category": "Gradle", + "title": "Create a Gradle Java Project... (Advanced)" } ], "menus": { @@ -466,6 +476,14 @@ { "command": "gradle.findTask", "when": "gradle:extensionActivated" + }, + { + "command": "gradle.createProject", + "when": "gradle:extensionActivated" + }, + { + "command": "gradle.createProjectAdvanced", + "when": "gradle:extensionActivated" } ], "view/title": [ diff --git a/extension/src/commands/Commands.ts b/extension/src/commands/Commands.ts index 8ef15a92a..58420594a 100644 --- a/extension/src/commands/Commands.ts +++ b/extension/src/commands/Commands.ts @@ -79,6 +79,7 @@ import { RecentTasksTreeDataProvider, } from "../views"; import { Command } from "./Command"; +import { COMMAND_CREATE_PROJECT, COMMAND_CREATE_PROJECT_ADVANCED, CreateProjectCommand } from "./CreateProjectCommand"; import { HideStoppedDaemonsCommand, HIDE_STOPPED_DAEMONS } from "./HideStoppedDaemonsCommand"; import { ShowStoppedDaemonsCommand, SHOW_STOPPED_DAEMONS } from "./ShowStoppedDaemonsCommand"; @@ -99,10 +100,10 @@ export class Commands { private gradleTasksTreeView: vscode.TreeView ) {} - private registerCommand(commandId: string, command: Command): void { + private registerCommand(commandId: string, command: Command, params?: unknown[]): void { this.context.subscriptions.push( instrumentOperationAsVsCodeCommand(commandId, (...args: unknown[]) => { - return command.run(...args); + return command.run(...args, params || []); }) ); } @@ -180,5 +181,7 @@ export class Commands { this.registerCommand(COMMAND_FIND_TASK, new FindTaskCommand(this.gradleTasksTreeView, this.gradleTaskProvider)); this.registerCommand(SHOW_STOPPED_DAEMONS, new ShowStoppedDaemonsCommand(this.gradleDaemonsTreeDataProvider)); this.registerCommand(HIDE_STOPPED_DAEMONS, new HideStoppedDaemonsCommand(this.gradleDaemonsTreeDataProvider)); + this.registerCommand(COMMAND_CREATE_PROJECT, new CreateProjectCommand(this.client), [false]); + this.registerCommand(COMMAND_CREATE_PROJECT_ADVANCED, new CreateProjectCommand(this.client), [true]); } } diff --git a/extension/src/commands/CreateProjectCommand.ts b/extension/src/commands/CreateProjectCommand.ts new file mode 100644 index 000000000..4356ec3ed --- /dev/null +++ b/extension/src/commands/CreateProjectCommand.ts @@ -0,0 +1,107 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +import * as path from "path"; +import * as vscode from "vscode"; +import { GradleClient } from "../client"; +import { getRunTaskCommandCancellationKey } from "../client/CancellationKeys"; +import { selectProjectTypeStep } from "../createProject/SelectProjectTypeStep"; +import { selectScriptDSLStep } from "../createProject/SelectScriptDSLStep"; +import { IProjectCreationMetadata, IProjectCreationStep, ProjectType, StepResult } from "../createProject/types"; +import { Command } from "./Command"; + +export const COMMAND_CREATE_PROJECT = "gradle.createProject"; +export const COMMAND_CREATE_PROJECT_ADVANCED = "gradle.createProjectAdvanced"; + +export class CreateProjectCommand extends Command { + constructor(private client: GradleClient) { + super(); + } + + async run(params: unknown[]): Promise { + if (!params || params[0] === undefined) { + return; + } + const folders = vscode.workspace.workspaceFolders; + const targetFolderUri = await vscode.window.showOpenDialog({ + defaultUri: folders && folders.length ? folders[0].uri : undefined, + title: "Select target Folder", + openLabel: "Select", + canSelectFiles: false, + canSelectFolders: true, + canSelectMany: false, + }); + const isAdvanced = params[0] as boolean; + if (targetFolderUri) { + const metadata: IProjectCreationMetadata = { + isAdvanced: isAdvanced, + totalSteps: isAdvanced ? 5 : 2, + testFramework: undefined, // junit4 + projectType: ProjectType.JAVA_APPLICATION, + targetFolder: targetFolderUri[0].fsPath, + projectName: path.basename(targetFolderUri[0].fsPath), + sourcePackageName: path.basename(targetFolderUri[0].fsPath), + steps: [], + nextStep: isAdvanced ? selectProjectTypeStep : selectScriptDSLStep, + }; + const success = await this.runSteps(metadata); + if (success) { + await this.createProject(metadata); + const openInNewWindow = !(folders && folders.length); + vscode.commands.executeCommand( + "vscode.openFolder", + vscode.Uri.file(metadata.targetFolder), + openInNewWindow + ); + } + } + return; + } + + private async runSteps(metadata: IProjectCreationMetadata): Promise { + let step: IProjectCreationStep | undefined = metadata.nextStep; + while (step !== undefined) { + const result = await step.run(metadata); + switch (result) { + case StepResult.NEXT: + step = metadata.nextStep; + break; + case StepResult.PREVIOUS: + if (metadata.steps.length === 0) { + return false; + } + step = metadata.steps.pop(); + break; + case StepResult.STOP: + return false; // user cancellation + default: + throw new Error("invalid StepResult returned."); + } + } + return true; + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + private async createProject(metadata: IProjectCreationMetadata): Promise { + const cancellationKey = getRunTaskCommandCancellationKey(metadata.targetFolder, "init"); + const args: string[] = ["init"]; + if (!metadata.projectType || !metadata.scriptDSL || !metadata.projectName || !metadata.sourcePackageName) { + return; + } + args.push("--dsl"); + args.push(metadata.scriptDSL); + args.push("--type"); + args.push(metadata.projectType); + if (metadata.testFramework) { + args.push("--test-framework"); + args.push(metadata.testFramework); + } + args.push("--project-name"); + args.push(metadata.projectName); + if (metadata.sourcePackageName) { + args.push("--package"); + args.push(metadata.sourcePackageName); + } + await this.client.runBuild(metadata.targetFolder, cancellationKey, args); + } +} diff --git a/extension/src/createProject/SelectProjectTypeStep.ts b/extension/src/createProject/SelectProjectTypeStep.ts new file mode 100644 index 000000000..1c7b327d9 --- /dev/null +++ b/extension/src/createProject/SelectProjectTypeStep.ts @@ -0,0 +1,77 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +import * as vscode from "vscode"; +import { selectScriptDSLStep } from "./SelectScriptDSLStep"; +import { IProjectCreationMetadata, IProjectCreationStep, ProjectType, StepResult } from "./types"; + +export class SelectProjectTypeStep implements IProjectCreationStep { + public async run(metadata: IProjectCreationMetadata): Promise { + const disposables: vscode.Disposable[] = []; + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const selectProjectTypePromise = new Promise(async (resolve, _reject) => { + const pickBox = vscode.window.createQuickPick(); + pickBox.title = `Create Gradle project: Select project type (${metadata.steps.length + 1}/${ + metadata.totalSteps + })`; + pickBox.placeholder = "Select project type ..."; + pickBox.matchOnDescription = true; + pickBox.ignoreFocusOut = true; + pickBox.items = this.getProjectTypePickItems(); + disposables.push( + pickBox.onDidAccept(async () => { + const selectedType = pickBox.selectedItems[0]; + if (selectedType) { + switch (selectedType.label) { + case "application": + metadata.projectType = ProjectType.JAVA_APPLICATION; + break; + case "library": + metadata.projectType = ProjectType.JAVA_LIBRARY; + break; + case "Gradle plugin": + metadata.projectType = ProjectType.JAVA_GRADLE_PLUGIN; + metadata.totalSteps = 4; // when creating gradle plugin, we shouldn't specify test framework + break; + default: + resolve(StepResult.STOP); + } + metadata.steps.push(selectProjectTypeStep); + metadata.nextStep = selectScriptDSLStep; + resolve(StepResult.NEXT); + } + }), + pickBox.onDidHide(() => { + resolve(StepResult.STOP); + }) + ); + disposables.push(pickBox); + pickBox.show(); + }); + + try { + return await selectProjectTypePromise; + } finally { + disposables.forEach((d) => d.dispose()); + } + } + + private getProjectTypePickItems(): vscode.QuickPickItem[] { + const result: vscode.QuickPickItem[] = []; + result.push({ + label: "application", + description: "A command-line application implemented in Java", + }); + result.push({ + label: "library", + description: "A Java library", + }); + result.push({ + label: "Gradle plugin", + description: "A Gradle plugin implemented in Java", + }); + return result; + } +} + +export const selectProjectTypeStep = new SelectProjectTypeStep(); diff --git a/extension/src/createProject/SelectScriptDSLStep.ts b/extension/src/createProject/SelectScriptDSLStep.ts new file mode 100644 index 000000000..7e43c4e66 --- /dev/null +++ b/extension/src/createProject/SelectScriptDSLStep.ts @@ -0,0 +1,82 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +import * as vscode from "vscode"; +import { selectTestFrameworkStep } from "./SelectTestFrameworkStep"; +import { specifyProjectNameStep } from "./SpecifyProjectNameStep"; +import { IProjectCreationMetadata, IProjectCreationStep, ProjectType, StepResult } from "./types"; + +export class SelectScriptDSLStep implements IProjectCreationStep { + public async run(metadata: IProjectCreationMetadata): Promise { + const disposables: vscode.Disposable[] = []; + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const selectScriptDSLPromise = new Promise(async (resolve, _reject) => { + const pickBox = vscode.window.createQuickPick(); + pickBox.title = `Create Gradle project: Select script DSL (${metadata.steps.length + 1}/${ + metadata.totalSteps + })`; + pickBox.placeholder = "Select build script DSL ..."; + pickBox.matchOnDescription = true; + pickBox.ignoreFocusOut = true; + pickBox.items = this.getScriptDSLPickItems(); + if (metadata.steps.length) { + pickBox.buttons = [vscode.QuickInputButtons.Back]; + disposables.push( + pickBox.onDidTriggerButton((item) => { + if (item === vscode.QuickInputButtons.Back) { + resolve(StepResult.PREVIOUS); + } + }) + ); + } + disposables.push( + pickBox.onDidAccept(() => { + const selectedScriptDSL = pickBox.selectedItems[0]; + if (selectedScriptDSL) { + switch (selectedScriptDSL.label) { + case "Groovy": + metadata.scriptDSL = "groovy"; + break; + case "Kotlin": + metadata.scriptDSL = "kotlin"; + break; + default: + resolve(StepResult.STOP); + } + metadata.steps.push(selectScriptDSLStep); + if (!metadata.isAdvanced || metadata.projectType === ProjectType.JAVA_GRADLE_PLUGIN) { + metadata.nextStep = specifyProjectNameStep; + } else { + metadata.nextStep = selectTestFrameworkStep; + } + resolve(StepResult.NEXT); + } + }), + pickBox.onDidHide(() => { + resolve(StepResult.STOP); + }) + ); + disposables.push(pickBox); + pickBox.show(); + }); + + try { + return await selectScriptDSLPromise; + } finally { + disposables.forEach((d) => d.dispose()); + } + } + + private getScriptDSLPickItems(): vscode.QuickPickItem[] { + const result: vscode.QuickPickItem[] = []; + result.push({ + label: "Groovy", + }); + result.push({ + label: "Kotlin", + }); + return result; + } +} + +export const selectScriptDSLStep = new SelectScriptDSLStep(); diff --git a/extension/src/createProject/SelectTestFrameworkStep.ts b/extension/src/createProject/SelectTestFrameworkStep.ts new file mode 100644 index 000000000..5afcf0cb2 --- /dev/null +++ b/extension/src/createProject/SelectTestFrameworkStep.ts @@ -0,0 +1,90 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +import * as vscode from "vscode"; +import { specifyProjectNameStep } from "./SpecifyProjectNameStep"; +import { IProjectCreationMetadata, IProjectCreationStep, StepResult, TestFramework } from "./types"; + +export class SelectTestFrameworkStep implements IProjectCreationStep { + public async run(metadata: IProjectCreationMetadata): Promise { + const disposables: vscode.Disposable[] = []; + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const selectTestFrameworkPromise = new Promise(async (resolve, _reject) => { + const pickBox = vscode.window.createQuickPick(); + pickBox.title = `Create Gradle project: Select test framework (${metadata.steps.length + 1}/${ + metadata.totalSteps + })`; + pickBox.placeholder = "Select test framework ..."; + pickBox.matchOnDescription = true; + pickBox.ignoreFocusOut = true; + pickBox.items = this.getTestFrameworkPickItems(); + if (metadata.steps.length) { + pickBox.buttons = [vscode.QuickInputButtons.Back]; + disposables.push( + pickBox.onDidTriggerButton((item) => { + if (item === vscode.QuickInputButtons.Back) { + resolve(StepResult.PREVIOUS); + } + }) + ); + } + disposables.push( + pickBox.onDidAccept(() => { + const selectedTestFramework = pickBox.selectedItems[0]; + if (selectedTestFramework) { + switch (selectedTestFramework.label) { + case "JUnit 4": + // junit4 is the default test framework + metadata.testFramework = undefined; + break; + case "TestNG": + metadata.testFramework = TestFramework.TESTNG; + break; + case "Spock": + metadata.testFramework = TestFramework.SPOCK; + break; + case "JUnit Jupiter": + metadata.testFramework = TestFramework.JUNIT_JUPITER; + break; + default: + resolve(StepResult.STOP); + } + metadata.steps.push(selectTestFrameworkStep); + metadata.nextStep = specifyProjectNameStep; + resolve(StepResult.NEXT); + } + }), + pickBox.onDidHide(() => { + resolve(StepResult.STOP); + }) + ); + disposables.push(pickBox); + pickBox.show(); + }); + + try { + return await selectTestFrameworkPromise; + } finally { + disposables.forEach((d) => d.dispose()); + } + } + + private getTestFrameworkPickItems(): vscode.QuickPickItem[] { + const result: vscode.QuickPickItem[] = []; + result.push({ + label: "JUnit 4", + }); + result.push({ + label: "TestNG", + }); + result.push({ + label: "Spock", + }); + result.push({ + label: "JUnit Jupiter", + }); + return result; + } +} + +export const selectTestFrameworkStep = new SelectTestFrameworkStep(); diff --git a/extension/src/createProject/SpecifyProjectNameStep.ts b/extension/src/createProject/SpecifyProjectNameStep.ts new file mode 100644 index 000000000..15d18e3b9 --- /dev/null +++ b/extension/src/createProject/SpecifyProjectNameStep.ts @@ -0,0 +1,66 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +import * as vscode from "vscode"; +import { specifySourcePackageNameStep } from "./SpecifySourcePackageNameStep"; +import { IProjectCreationMetadata, IProjectCreationStep, StepResult } from "./types"; + +export class SpecifyProjectNameStep implements IProjectCreationStep { + public async run(metadata: IProjectCreationMetadata): Promise { + const disposables: vscode.Disposable[] = []; + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const specifyProjectNamePromise = new Promise(async (resolve, _reject) => { + const inputBox = vscode.window.createInputBox(); + inputBox.title = `Create Gradle project: Specify project name (${metadata.steps.length + 1}/${ + metadata.totalSteps + })`; + inputBox.prompt = "Input name of your project."; + inputBox.placeholder = "e.g. " + metadata.projectName; + inputBox.value = metadata.projectName; + inputBox.ignoreFocusOut = true; + const validationMessage: string | undefined = this.isValidProjectName(metadata.projectName); + inputBox.enabled = validationMessage === undefined; + inputBox.validationMessage = validationMessage; + if (metadata.steps.length) { + inputBox.buttons = [vscode.QuickInputButtons.Back]; + disposables.push( + inputBox.onDidTriggerButton((item) => { + if (item === vscode.QuickInputButtons.Back) { + resolve(StepResult.PREVIOUS); + } + }) + ); + } + disposables.push( + inputBox.onDidChangeValue(() => { + const validationMessage: string | undefined = this.isValidProjectName(inputBox.value); + inputBox.enabled = validationMessage === undefined; + inputBox.validationMessage = validationMessage; + }), + inputBox.onDidAccept(async () => { + metadata.projectName = inputBox.value; + metadata.steps.push(specifyProjectNameStep); + metadata.nextStep = !metadata.isAdvanced ? undefined : specifySourcePackageNameStep; + resolve(StepResult.NEXT); + }), + inputBox.onDidHide(() => { + resolve(StepResult.STOP); + }) + ); + disposables.push(inputBox); + inputBox.show(); + }); + + try { + return await specifyProjectNamePromise; + } finally { + disposables.forEach((d) => d.dispose()); + } + } + + private isValidProjectName(value: string): string | undefined { + return value.length > 0 ? undefined : "Invalid Project Name."; + } +} + +export const specifyProjectNameStep = new SpecifyProjectNameStep(); diff --git a/extension/src/createProject/SpecifySourcePackageNameStep.ts b/extension/src/createProject/SpecifySourcePackageNameStep.ts new file mode 100644 index 000000000..d5310df3f --- /dev/null +++ b/extension/src/createProject/SpecifySourcePackageNameStep.ts @@ -0,0 +1,65 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +import * as vscode from "vscode"; +import { IProjectCreationMetadata, IProjectCreationStep, StepResult } from "./types"; + +export class SpecifySourcePackageNameStep implements IProjectCreationStep { + public async run(metadata: IProjectCreationMetadata): Promise { + const disposables: vscode.Disposable[] = []; + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const specifySourcePackageNamePromise = new Promise(async (resolve, _reject) => { + const inputBox = vscode.window.createInputBox(); + const defaultName = metadata.sourcePackageName || ""; + inputBox.title = `Create Gradle project: Specify package name (${metadata.steps.length + 1}/${ + metadata.totalSteps + })`; + inputBox.prompt = "Input source package name of your project."; + inputBox.placeholder = "e.g. " + defaultName; + inputBox.value = defaultName; + inputBox.ignoreFocusOut = true; + const validationMessage: string | undefined = this.isValidSourcePackageName(defaultName); + inputBox.enabled = validationMessage === undefined; + inputBox.validationMessage = validationMessage; + if (metadata.steps.length) { + inputBox.buttons = [vscode.QuickInputButtons.Back]; + disposables.push( + inputBox.onDidTriggerButton((item) => { + if (item === vscode.QuickInputButtons.Back) { + resolve(StepResult.PREVIOUS); + } + }) + ); + } + disposables.push( + inputBox.onDidChangeValue(() => { + const validationMessage: string | undefined = this.isValidSourcePackageName(inputBox.value); + inputBox.enabled = validationMessage === undefined; + inputBox.validationMessage = validationMessage; + }), + inputBox.onDidAccept(async () => { + metadata.sourcePackageName = inputBox.value; + metadata.nextStep = undefined; + resolve(StepResult.NEXT); + }), + inputBox.onDidHide(() => { + resolve(StepResult.STOP); + }) + ); + disposables.push(inputBox); + inputBox.show(); + }); + + try { + return await specifySourcePackageNamePromise; + } finally { + disposables.forEach((d) => d.dispose()); + } + } + + private isValidSourcePackageName(value: string): string | undefined { + return /^[a-z_][a-z0-9_]*(\.[a-z_][a-z0-9_]*)*$/.test(value) ? undefined : "Invalid Source Package Name."; + } +} + +export const specifySourcePackageNameStep = new SpecifySourcePackageNameStep(); diff --git a/extension/src/createProject/types.ts b/extension/src/createProject/types.ts new file mode 100644 index 000000000..1ba282c5b --- /dev/null +++ b/extension/src/createProject/types.ts @@ -0,0 +1,37 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +export interface IProjectCreationMetadata { + isAdvanced: boolean; + totalSteps: number; + projectType?: ProjectType; + scriptDSL?: string; + testFramework?: TestFramework; + projectName: string; // default: folderName + sourcePackageName?: string; //default: folderName + targetFolder: string; + steps: IProjectCreationStep[]; + nextStep?: IProjectCreationStep; +} + +export interface IProjectCreationStep { + run(metadata: IProjectCreationMetadata): Promise; +} + +export enum StepResult { + NEXT, + STOP, + PREVIOUS, +} + +export enum ProjectType { + JAVA_APPLICATION = "java-application", + JAVA_LIBRARY = "java-library", + JAVA_GRADLE_PLUGIN = "java-gradle-plugin", +} + +export enum TestFramework { + TESTNG = "testng", + SPOCK = "spock", + JUNIT_JUPITER = "junit-jupiter", +}