From 9ad22afdef7351a86fb7a47f5eb16f401ac5ebb2 Mon Sep 17 00:00:00 2001 From: aboyko Date: Thu, 18 Jan 2024 17:04:17 -0500 Subject: [PATCH] [vscode-spring-cli]: project add and remove commands --- .../vscode-spring-cli/package.json | 10 +++ .../vscode-spring-cli/src/boot.ts | 89 ++++++++++--------- .../vscode-spring-cli/src/cli-types.ts | 4 +- .../vscode-spring-cli/src/cli.ts | 62 +++++++++---- .../vscode-spring-cli/src/extension.ts | 6 ++ .../vscode-spring-cli/src/project-catalog.ts | 8 +- .../vscode-spring-cli/src/project.ts | 56 ++++++++++++ .../vscode-spring-cli/src/utils.ts | 17 +++- .../vscode-spring-cli/tsconfig.json | 3 +- 9 files changed, 189 insertions(+), 66 deletions(-) create mode 100644 vscode-extensions/vscode-spring-cli/src/project.ts diff --git a/vscode-extensions/vscode-spring-cli/package.json b/vscode-extensions/vscode-spring-cli/package.json index d00e1d322..6373fb51a 100644 --- a/vscode-extensions/vscode-spring-cli/package.json +++ b/vscode-extensions/vscode-spring-cli/package.json @@ -61,6 +61,16 @@ "command": "vscode-spring-cli.project-catalog.remove", "title": "Remove Project Catalog", "category": "Spring CLI" + }, + { + "command": "vscode-spring-cli.project.add", + "title": "Add Project", + "category": "Spring CLI" + }, + { + "command": "vscode-spring-cli.project.remove", + "title": "Remove Project", + "category": "Spring CLI" } ] }, diff --git a/vscode-extensions/vscode-spring-cli/src/boot.ts b/vscode-extensions/vscode-spring-cli/src/boot.ts index 2def648c6..0c4cf58ae 100644 --- a/vscode-extensions/vscode-spring-cli/src/boot.ts +++ b/vscode-extensions/vscode-spring-cli/src/boot.ts @@ -1,7 +1,7 @@ import * as path from "path"; import { BootAddMetadata, BootNewMetadata, Project } from "./cli-types"; import { CLI } from "./extension"; -import { enterText, getTargetPomXml, openDialogForFolder } from "./utils"; +import { enterText, getTargetPomXml, mapProjectToQuickPick, openDialogForFolder } from "./utils"; import vscode, { QuickPickItem, QuickPickItemKind } from 'vscode'; import fs from 'fs' @@ -19,7 +19,7 @@ export async function handleBootAdd(pom?: vscode.Uri): Promise { return; } if (!metadata.catalogType) { - metadata.catalogType = (await vscode.window.showQuickPick(CLI.projectList().map(mapProjectTypetoQuickPick), { canPickMany: false }))?.label; + metadata.catalogType = (await vscode.window.showQuickPick(CLI.projectList().then(ps => ps.map(mapProjectToQuickPick)), { canPickMany: false, ignoreFocusOut: true }))?.label; } return CLI.bootAdd(metadata); } @@ -31,43 +31,54 @@ export async function handleBootNew(targetFolder?: string): Promise { metadata.targetFolder = targetFolder || (await openDialogForFolder({title: "Select Parent Folder"})).fsPath; // Select project type from the list of available types - metadata.catalogId = (await vscode.window.showQuickPick(CLI.projectList().map(mapProjectTypetoQuickPick), { canPickMany: false }))?.label; + metadata.catalogId = (await vscode.window.showQuickPick(CLI.projectList().then(ps => ps.map(mapProjectToQuickPick)), { canPickMany: false }))?.label; - metadata.name = await enterText({ - title: "Project Name", - prompt: "Enter Project Name", - defaultValue: metadata.name || metadata.catalogId, - validate: value => { - if (!/^[a-z_][a-z0-9_]*(-[a-z_][a-z0-9_]*)*$/.test(value)) { - return "Invalid Project Name"; + if (!metadata.catalogId) { + // Cancelled + return; + } + + try { + metadata.name = await enterText({ + title: "Project Name", + prompt: "Enter Project Name", + defaultValue: metadata.name || metadata.catalogId, + validate: value => { + if (!/^[a-z_][a-z0-9_]*(-[a-z_][a-z0-9_]*)*$/.test(value)) { + return "Invalid Project Name"; + } + if (fs.existsSync(path.resolve(metadata.targetFolder, value))) { + return "Folder or file with such name exists under selected parent folder"; + } } - if (fs.existsSync(path.resolve(metadata.targetFolder, value))) { - return "Folder or file with such name exists under selected parent folder"; - } - } - }); + }); + + metadata.artifactId = await enterText({ + title: "Artifact Id", + prompt: "Enter Artifact Id", + defaultValue: metadata.name, + validate: value => (/^[a-z_][a-z0-9_]*(-[a-z_][a-z0-9_]*)*$/.test(value)) ? undefined : "Invalid Artifact Id" + }); - metadata.artifactId = await enterText({ - title: "Artifact Id", - prompt: "Enter Artifact Id", - defaultValue: metadata.name, - validate: value => (/^[a-z_][a-z0-9_]*(-[a-z_][a-z0-9_]*)*$/.test(value)) ? undefined : "Invalid Artifact Id" - }); + metadata.groupId = await enterText({ + title: "Group Id", + prompt: "Enter Group Id", + defaultValue: "com.example", + validate: value => (/^[a-z_][a-z0-9_]*(\.[a-z0-9_]+)*$/.test(value)) ? undefined : "Invalid Group Id" + }); + + // Root package name + metadata.rootPackage = await enterText({ + title: "Root Package Name", + prompt: "Enter Root Package Name", + defaultValue: `${metadata.groupId}.${metadata.name.replace("-", ".")}`, + validate: value => (/^[a-z][a-z0-9_]*(\.[a-z0-9_]+)+[0-9a-z_]$/.test(value)) ? undefined : "Invalid Package Name" + }); + } catch (error) { + // Cancelled + return; + } - metadata.groupId = await enterText({ - title: "Group Id", - prompt: "Enter Group Id", - defaultValue: "com.example", - validate: value => (/^[a-z_][a-z0-9_]*(\.[a-z0-9_]+)*$/.test(value)) ? undefined : "Invalid Group Id" - }); - - // Root package name - metadata.rootPackage = await enterText({ - title: "Root Package Name", - prompt: "Enter Root Package Name", - defaultValue: `${metadata.groupId}.${metadata.name.replace("-", ".")}`, - validate: value => (/^[a-z][a-z0-9_]*(\.[a-z0-9_]+)+[0-9a-z_]$/.test(value)) ? undefined : "Invalid Package Name" - }); // Create project and open in the workspace or new window if (metadata.name && metadata.catalogId && metadata.targetFolder) { @@ -95,14 +106,6 @@ export async function handleBootNew(targetFolder?: string): Promise { } -function mapProjectTypetoQuickPick(metadata: Project): QuickPickItem { - return { - label: metadata.id, - kind: QuickPickItemKind.Default, - description: metadata.description, - detail: metadata.tags.toString() - }; -} async function specifyOpenMethod(hasOpenFolder: boolean, projectLocation: vscode.Uri): Promise { const candidates: string[] = [ diff --git a/vscode-extensions/vscode-spring-cli/src/cli-types.ts b/vscode-extensions/vscode-spring-cli/src/cli-types.ts index 2b5bd4468..15684385d 100644 --- a/vscode-extensions/vscode-spring-cli/src/cli-types.ts +++ b/vscode-extensions/vscode-spring-cli/src/cli-types.ts @@ -1,8 +1,8 @@ export interface Project { - id: string; + name: string; url: string; description?: string; - catalogId?: string; + catalog?: string; tags?: string[] } diff --git a/vscode-extensions/vscode-spring-cli/src/cli.ts b/vscode-extensions/vscode-spring-cli/src/cli.ts index 7e9eb1a7e..1b7fd28f3 100644 --- a/vscode-extensions/vscode-spring-cli/src/cli.ts +++ b/vscode-extensions/vscode-spring-cli/src/cli.ts @@ -5,30 +5,30 @@ const SPRING_CLI_TASK_TYPE = 'spring-cli'; export class Cli { - projectList() : Project[] { - return [ + projectList() : Thenable { + return Promise.resolve([ { - id: 'web', + name: 'web', description: 'Hello, World RESTful web service.', url: 'https://github.com/rd-1-2022/rest-service', - catalogId: 'gs', + catalog: 'gs', tags: ['java-17', 'boot-3.1.x', 'rest', 'web'] }, { - id: 'jpa', + name: 'jpa', description: 'Learn how to work with JPA data persistence using Spring Data JPA.', url: 'https://github.com/rd-1-2022/rpt-spring-data-jpa', - catalogId: 'gs', + catalog: 'gs', tags: ['java-17', 'boot-3.1.x', 'jpa', 'h2'] }, { - id: 'scheduling', + name: 'scheduling', description: 'How to schedule tasks', url: 'https://github.com/rd-1-2022/rpt-spring-scheduling-tasks', - catalogId: 'gs', + catalog: 'gs', tags: ['scheduling'] } - ]; + ]); } projectCatalogList(): Thenable { @@ -82,7 +82,7 @@ export class Cli { args.push("--tags"); args.push(catalog.tags.join(",")); } - return this.exec("Add Project Catalog", `"${catalog.name}"`, args); + return this.exec("Add Project Catalog", `'${catalog.name}'`, args); } projectCatalogRemove(name: string): Promise { @@ -92,7 +92,37 @@ export class Cli { "--name", name, ]; - return this.exec("Remove Project Catalog", `"${name}"`, args); + return this.exec("Remove Project Catalog", `'${name}'`, args); + } + + projectAdd(project: Project) { + const args = [ + "project", + "add", + "--name", + project.name, + "--url", + project.url, + ]; + if (project.description) { + args.push("--description"); + args.push(project.description); + } + if (project.tags) { + args.push("--tags"); + args.push(project.tags.join(",")); + } + return this.exec("Add Project", `'${project.name}'`, args); + } + + projectRemove(name: string) { + const args = [ + "project", + "remove", + "--name", + name + ]; + return this.exec("Remove Project", `'${name}'`, args); } bootNew(metadata: BootNewMetadata): Promise { @@ -100,21 +130,21 @@ export class Cli { "boot", "new", "--name", - `"${metadata.name}"`, + metadata.name, "--from", - `"${metadata.catalogId}"` + metadata.catalogId ]; if (metadata.groupId) { args.push("--group-id") - args.push(`"${metadata.groupId}"`); + args.push(metadata.groupId); } if (metadata.artifactId) { args.push("--artifact-id") - args.push(`"${metadata.artifactId}"`); + args.push(metadata.artifactId); } if (metadata.rootPackage) { args.push("--package-name"); - args.push(`"${metadata.rootPackage}"`); + args.push(metadata.rootPackage); } return this.exec("New Boot Project", `'${metadata.catalogId}'`, args, metadata.targetFolder); } diff --git a/vscode-extensions/vscode-spring-cli/src/extension.ts b/vscode-extensions/vscode-spring-cli/src/extension.ts index e751a9a14..c195d1125 100644 --- a/vscode-extensions/vscode-spring-cli/src/extension.ts +++ b/vscode-extensions/vscode-spring-cli/src/extension.ts @@ -3,14 +3,20 @@ import * as vscode from "vscode"; import { Cli } from "./cli"; import { handleBootAdd, handleBootNew } from "./boot"; import { handleCatalogAdd, handleCatalogRemove } from "./project-catalog"; +import { handleProjectAdd, handleProjectRemove } from "./project"; export const CLI = new Cli(); export async function activate(context: vscode.ExtensionContext): Promise { + vscode.commands.registerCommand('vscode-spring-cli.boot.new', handleBootNew); vscode.commands.registerCommand('vscode-spring-cli.boot.add', handleBootAdd); + vscode.commands.registerCommand('vscode-spring-cli.project-catalog.add', handleCatalogAdd); vscode.commands.registerCommand('vscode-spring-cli.project-catalog.remove', handleCatalogRemove); + + vscode.commands.registerCommand('vscode-spring-cli.project.add', handleProjectAdd); + vscode.commands.registerCommand('vscode-spring-cli.project.remove', handleProjectRemove); } export async function deactivate(): Promise { diff --git a/vscode-extensions/vscode-spring-cli/src/project-catalog.ts b/vscode-extensions/vscode-spring-cli/src/project-catalog.ts index c2b022646..1132db3fd 100644 --- a/vscode-extensions/vscode-spring-cli/src/project-catalog.ts +++ b/vscode-extensions/vscode-spring-cli/src/project-catalog.ts @@ -42,7 +42,13 @@ export async function handleCatalogAdd() { title: "URL", prompt: "Enter URL:", placeholder: "https://github.com/my-org/my-project-catalog", - validate: v => /[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)?/gi.test(v) ? "" : "Invalid URL value" + validate: v => { + try { + Uri.parse(v, true); + } catch (error) { + return "Invalid URL value" + } + } }); const description = await enterText({ title: "Description", diff --git a/vscode-extensions/vscode-spring-cli/src/project.ts b/vscode-extensions/vscode-spring-cli/src/project.ts new file mode 100644 index 000000000..008595859 --- /dev/null +++ b/vscode-extensions/vscode-spring-cli/src/project.ts @@ -0,0 +1,56 @@ +import { Uri, window } from "vscode"; +import { enterText, mapProjectToQuickPick } from "./utils"; + +import { CLI } from "./extension"; + +export async function handleProjectAdd() { + try { + const currentProjectNames = (await CLI.projectList()).map(p => p.name); + const name = await enterText({ + title: "Name", + prompt: "Enter Name:", + validate: v => { + if (!/^\S+$/.test(v)) { + return "Invalid Project Catalog Name"; + } + if (currentProjectNames.includes(v)) { + return "Name alreasy exists" + } + } + }); + const url = await enterText({ + title: "URL", + prompt: "Enter URL:", + placeholder: "https://github.com/my-org/my-project", + validate: v => { + try { + Uri.parse(v, true); + } catch (error) { + return "Invalid URL value" + } + } + }); + const description = await enterText({ + title: "Description", + prompt: "Enter Description:" + }); + const tags = (await enterText({ + title: "Tags", + prompt: "Enter Tags as strings separated by spaces and/or commas", + placeholder: "java, spring, eureka, config" + })).split(/(,)?\s+/); + } catch (error) { + // Ignore error - must have been cancelled + } + +} + +export async function handleProjectRemove() { + const name = (await window.showQuickPick(CLI.projectList().then(ps => ps.map(mapProjectToQuickPick)), { + canPickMany: false, + ignoreFocusOut: true, + }))?.label; + if (name) { + return CLI.projectRemove(name); + } +} \ No newline at end of file diff --git a/vscode-extensions/vscode-spring-cli/src/utils.ts b/vscode-extensions/vscode-spring-cli/src/utils.ts index 8ce82966a..1a65acb85 100644 --- a/vscode-extensions/vscode-spring-cli/src/utils.ts +++ b/vscode-extensions/vscode-spring-cli/src/utils.ts @@ -1,5 +1,6 @@ -import { InputBox, OpenDialogOptions, Uri, WorkspaceFolder, window, workspace } from "vscode"; +import { InputBox, OpenDialogOptions, QuickPickItem, QuickPickItemKind, Uri, WorkspaceFolder, window, workspace } from "vscode"; import path from "path" +import { Project } from "./cli-types"; export async function openDialogForFolder(customOptions: OpenDialogOptions): Promise { const options: OpenDialogOptions = { @@ -24,7 +25,7 @@ export function enterText(opts: { defaultValue?: string, placeholder?: string }): Promise { - return new Promise((resolve) => { + return new Promise((resolve, reject) => { const inputBox: InputBox = window.createInputBox(); inputBox.title = opts.title; inputBox.placeholder = opts.placeholder; @@ -36,10 +37,11 @@ export function enterText(opts: { }); inputBox.onDidAccept(() => { if (!inputBox.validationMessage) { + resolve(inputBox.value); inputBox.hide(); } }); - inputBox.onDidHide(() => resolve(inputBox.value)); + inputBox.onDidHide(() => reject("cancelled")); inputBox.show(); }); } @@ -86,3 +88,12 @@ export async function getTargetPomXml(): Promise { return undefined; } +export function mapProjectToQuickPick(project: Project): QuickPickItem { + return { + label: project.name, + kind: QuickPickItemKind.Default, + description: project.description + }; +} + + diff --git a/vscode-extensions/vscode-spring-cli/tsconfig.json b/vscode-extensions/vscode-spring-cli/tsconfig.json index e006bc6bf..2c57563ce 100644 --- a/vscode-extensions/vscode-spring-cli/tsconfig.json +++ b/vscode-extensions/vscode-spring-cli/tsconfig.json @@ -17,6 +17,7 @@ "./**/*.ts" ], "exclude": [ - "node_modules" + "node_modules", + "out" ] } \ No newline at end of file