[vscode-spring-cli] prepare to fetch data from CLI. Polish commands: boot, project, project-catalog

This commit is contained in:
aboyko
2024-01-19 15:21:49 -05:00
parent 9ad22afdef
commit 9195ad08e4
9 changed files with 420 additions and 241 deletions

View File

@@ -2,26 +2,29 @@ import * as path from "path";
import { BootAddMetadata, BootNewMetadata, Project } from "./cli-types";
import { CLI } from "./extension";
import { enterText, getTargetPomXml, mapProjectToQuickPick, openDialogForFolder } from "./utils";
import vscode, { QuickPickItem, QuickPickItemKind } from 'vscode';
import { Uri, commands, window, workspace } from 'vscode';
import fs from 'fs'
const OPEN_IN_NEW_WORKSPACE = "Open";
const OPEN_IN_CURRENT_WORKSPACE = "Add to Workspace";
export async function handleBootAdd(pom?: vscode.Uri): Promise<void> {
export async function handleBootAdd(pom?: Uri): Promise<void> {
const metadata: BootAddMetadata = {};
pom = pom || await getTargetPomXml();
if (pom) {
metadata.targetFolder = path.dirname(pom.fsPath);
}
if (!metadata.targetFolder) {
vscode.window.showErrorMessage("Spring-CLI Boot Add command requires a target project");
window.showErrorMessage("Spring-CLI Boot Add command requires a target project");
return;
}
if (!metadata.catalogType) {
metadata.catalogType = (await vscode.window.showQuickPick(CLI.projectList().then(ps => ps.map(mapProjectToQuickPick)), { canPickMany: false, ignoreFocusOut: true }))?.label;
if (!metadata.catalog) {
const fetchProjects = CLI.projectList().then(ps => ps.map(mapProjectToQuickPick));
metadata.catalog = (await window.showQuickPick(fetchProjects, { canPickMany: false, ignoreFocusOut: true }))?.label;
}
if (metadata.catalog) {
return CLI.bootAdd(metadata);
}
return CLI.bootAdd(metadata);
}
export async function handleBootNew(targetFolder?: string): Promise<void> {
@@ -31,7 +34,8 @@ export async function handleBootNew(targetFolder?: string): Promise<void> {
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().then(ps => ps.map(mapProjectToQuickPick)), { canPickMany: false }))?.label;
const fetchProjects = CLI.projectList().then(ps => ps.map(mapProjectToQuickPick));
metadata.catalogId = (await window.showQuickPick(fetchProjects, { canPickMany: false }))?.label;
if (!metadata.catalogId) {
// Cancelled
@@ -43,7 +47,7 @@ export async function handleBootNew(targetFolder?: string): Promise<void> {
title: "Project Name",
prompt: "Enter Project Name",
defaultValue: metadata.name || metadata.catalogId,
validate: value => {
validate: async value => {
if (!/^[a-z_][a-z0-9_]*(-[a-z_][a-z0-9_]*)*$/.test(value)) {
return "Invalid Project Name";
}
@@ -57,22 +61,22 @@ export async function handleBootNew(targetFolder?: string): Promise<void> {
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"
validate: async 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"
validate: async 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"
defaultValue: `${metadata.groupId}.${metadata.name.toLowerCase().replace(/(-|_)+/g, ".")}`,
validate: async value => (/^[a-z][a-z0-9_]*(\.[a-z0-9_]+)+[0-9a-z_]$/.test(value)) ? undefined : "Invalid Package Name"
});
} catch (error) {
// Cancelled
@@ -85,21 +89,21 @@ export async function handleBootNew(targetFolder?: string): Promise<void> {
await CLI.bootNew(metadata);
// Open project either is the same workspace or new workspace
const hasOpenFolder = vscode.workspace.workspaceFolders !== undefined || vscode.workspace.rootPath !== undefined;
const hasOpenFolder = workspace.workspaceFolders !== undefined || workspace.rootPath !== undefined;
const pathToOpen = path.resolve(metadata.targetFolder, metadata.name);
// Don't prompt to open projectLocation if it's already a currently opened folder
if (hasOpenFolder && (vscode.workspace.workspaceFolders.some(folder => folder.uri.fsPath === pathToOpen) || vscode.workspace.rootPath === pathToOpen)) {
if (hasOpenFolder && (workspace.workspaceFolders.some(folder => folder.uri.fsPath === pathToOpen) || workspace.rootPath === pathToOpen)) {
return;
}
const choice = await specifyOpenMethod(hasOpenFolder, vscode.Uri.file(pathToOpen))
const choice = await specifyOpenMethod(hasOpenFolder, Uri.file(pathToOpen))
if (choice === OPEN_IN_NEW_WORKSPACE) {
vscode.commands.executeCommand("vscode.openFolder", vscode.Uri.file(pathToOpen), hasOpenFolder);
commands.executeCommand("vscode.openFolder", Uri.file(pathToOpen), hasOpenFolder);
} else if (choice === OPEN_IN_CURRENT_WORKSPACE) {
if (!vscode.workspace.workspaceFolders.find((workspaceFolder) => workspaceFolder.uri && pathToOpen.startsWith(workspaceFolder.uri.fsPath))) {
vscode.workspace.updateWorkspaceFolders(vscode.workspace.workspaceFolders.length, null, { uri: vscode.Uri.file(pathToOpen) });
if (!workspace.workspaceFolders.find((workspaceFolder) => workspaceFolder.uri && pathToOpen.startsWith(workspaceFolder.uri.fsPath))) {
workspace.updateWorkspaceFolders(workspace.workspaceFolders.length, null, { uri: Uri.file(pathToOpen) });
}
}
}
@@ -107,10 +111,10 @@ export async function handleBootNew(targetFolder?: string): Promise<void> {
}
async function specifyOpenMethod(hasOpenFolder: boolean, projectLocation: vscode.Uri): Promise<string> {
async function specifyOpenMethod(hasOpenFolder: boolean, projectLocation: Uri): Promise<string> {
const candidates: string[] = [
OPEN_IN_NEW_WORKSPACE,
hasOpenFolder ? OPEN_IN_CURRENT_WORKSPACE : undefined,
].filter(Boolean);
return await vscode.window.showInformationMessage(`Successfully generated. Location: ${projectLocation.fsPath}`, ...candidates);
return await window.showInformationMessage(`Successfully generated. Location: ${projectLocation.fsPath}`, ...candidates);
}

View File

@@ -17,7 +17,7 @@ export interface BootNewMetadata {
export interface BootAddMetadata {
targetFolder?: string;
catalogType?: string;
catalog?: string;
}
export interface ProjectCatalog {

View File

@@ -1,68 +1,31 @@
import { BootAddMetadata, BootNewMetadata, Project, ProjectCatalog } from "./cli-types";
import vscode, { TaskScope } from "vscode";
import { ProcessExecution, ProgressLocation, Task, TaskScope, Uri, env, tasks, window, workspace } from "vscode";
import cp from "child_process";
import { homedir } from "os";
import { getWorkspaceRoot } from "./utils";
const SPRING_CLI_TASK_TYPE = 'spring-cli';
export class Cli {
projectList() : Thenable<Project[]> {
return Promise.resolve([
{
name: 'web',
description: 'Hello, World RESTful web service.',
url: 'https://github.com/rd-1-2022/rest-service',
catalog: 'gs',
tags: ['java-17', 'boot-3.1.x', 'rest', 'web']
},
{
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',
catalog: 'gs',
tags: ['java-17', 'boot-3.1.x', 'jpa', 'h2']
},
{
name: 'scheduling',
description: 'How to schedule tasks',
url: 'https://github.com/rd-1-2022/rpt-spring-scheduling-tasks',
catalog: 'gs',
tags: ['scheduling']
}
]);
private get executable(): string {
return workspace.getConfiguration("spring-cli").get("executable") || "spring";
}
projectCatalogList(): Thenable<ProjectCatalog[]> {
return Promise.resolve([
{
name: "gs",
url: "https://github.com/rd-1-2022/spring-gs-catalog",
description: "Getting Started Catalog",
tags: ["java-17", "boot-3.1"]
},
{
name: "ai-azure",
url: "https://github.com/rd-1-2022/ai-azure-catalog",
description: "Azure OpenAI Catalog",
tags: ["java-17", "boot-3.1.x", "ai", "azure"]
}
]);
isBorderLine(s: string) {
return !/(\s|\S)+/.test(s);
}
projectCatalogListAvailable(): Thenable<ProjectCatalog[]> {
return Promise.resolve([
{
name: "ai-azure",
url: "https://github.com/rd-1-2022/ai-azure-catalog",
description: "Azure OpenAI Catalog",
tags: ["java-17", "boot-3.1.x", "ai", "azure"]
},
{
name: "dapr",
url: "https://github.com/ciberkleid/spring-cli-dapr-catalog",
description: "Dapr Catalog",
tags: ["java-17", "boot-3.1.x", "dapr"]
}
]);
projectList() : Promise<Project[]> {
return this.fetch("Fetching projects...", undefined, ["project", "list-json"]);
}
projectCatalogList(): Promise<ProjectCatalog[]> {
return this.fetch("Fetching Catalogs...", undefined, ["project-catalog", "list-json"]);
}
projectCatalogListAvailable(): Promise<ProjectCatalog[]> {
return this.fetch("Fetching Available Catalogs...", undefined, ["project-catalog", "list-available-json"]);
}
projectCatalogAdd(catalog: ProjectCatalog): Promise<void> {
@@ -154,15 +117,15 @@ export class Cli {
"boot",
"add",
"--from",
metadata.catalogType
metadata.catalog
];
return this.exec("Add to Boot Project", `'${metadata.catalogType}'`, args, metadata.targetFolder);
return this.exec("Add to Boot Project", `'${metadata.catalog}'`, args, metadata.targetFolder);
}
private async exec(title: string, message: string, args: string[], cwd?: string): Promise<void> {
return vscode.window.withProgress({
location: vscode.ProgressLocation.Window,
return window.withProgress({
location: ProgressLocation.Window,
cancellable: true,
title
}, (progress, cancellation) => {
@@ -170,9 +133,10 @@ export class Cli {
progress.report({message});
return new Promise<void>(async (resolve, reject) => {
const process = new vscode.ProcessExecution('spring', args, { cwd });
const task = new vscode.Task({ type: SPRING_CLI_TASK_TYPE}, cwd ? vscode.workspace.getWorkspaceFolder(vscode.Uri.file(cwd)) : TaskScope.Global, `${title}: ${message}`, SPRING_CLI_TASK_TYPE, process);
const taskExecution = await vscode.tasks.executeTask(task);
const processOpts = { cwd: cwd || getWorkspaceRoot()?.fsPath || homedir() };
const process = this.executable.endsWith(".jar") ? new ProcessExecution("java", [ "-jar", this.executable, ...args], processOpts) : new ProcessExecution(this.executable, args, processOpts);
const task = new Task({ type: SPRING_CLI_TASK_TYPE}, cwd ? workspace.getWorkspaceFolder(Uri.file(cwd)) : TaskScope.Global, `${title}: ${message}`, SPRING_CLI_TASK_TYPE, process);
const taskExecution = await tasks.executeTask(task);
if (cancellation.isCancellationRequested) {
reject();
}
@@ -180,7 +144,7 @@ export class Cli {
cancelListener.dispose();
reject();
})
const listener = vscode.tasks.onDidEndTaskProcess(e => {
const listener = tasks.onDidEndTaskProcess(e => {
if (e.execution === taskExecution) {
listener.dispose();
resolve();
@@ -191,5 +155,41 @@ export class Cli {
});
}
private async fetch<T>(title: string, message: string, args: string[], cwd?: string) : Promise<T> {
return window.withProgress({
location: ProgressLocation.Window,
cancellable: true,
title
}, (progress, cancellation) => {
if (message) {
progress.report({message});
}
return new Promise<T>(async (resolve, reject) => {
if (cancellation.isCancellationRequested) {
reject("Cancelled");
}
const processOpts = { cwd: cwd || getWorkspaceRoot()?.fsPath || homedir() };
const process = this.executable.endsWith(".jar") ? await cp.exec(`java -jar ${this.executable} ${args.join(" ")}`, processOpts) : await cp.exec(`${this.executable} ${args.join(" ")}`, processOpts);
cancellation.onCancellationRequested(() => process.kill());
const dataChunks: string[] = [];
process.stdout.on("data", s => dataChunks.push(s));
process.on("exit", (code) => {
if (code) {
reject(`Failed to fetch data: ${dataChunks.join()}`);
} else {
try {
resolve(JSON.parse(dataChunks.join()) as T);
} catch (error) {
reject(error);
}
}
});
});
});
}
}

View File

@@ -3,7 +3,7 @@ import { CLI } from "./extension";
import { window, QuickPickItem, Uri } from "vscode";
import { enterText } from "./utils";
interface ProjectCatalogQuickPick extends QuickPickItem {
interface ProjectCatalogQuickPickItem extends QuickPickItem {
projectCatalog: ProjectCatalog;
}
@@ -18,18 +18,18 @@ export async function handleCatalogAdd() {
let currentCatalogNames = [];
// Select from available project catalog - currentle added project ctalogs
let catalog = await pickCatalog(async () => {
const [available, current] = await Promise.all([CLI.projectCatalogListAvailable(), CLI.projectCatalogList()]);
const itemsPromise = Promise.all([CLI.projectCatalogListAvailable(), CLI.projectCatalogList()]).then(([available, current]) => {
const currentCatalogNames = current.map(c => c.name);
return [CUSTOM_CATALOG, ...available.filter(a => !currentCatalogNames.includes(a.name))];
return [CUSTOM_CATALOG, ...available.filter(a => !currentCatalogNames.includes(a.name))].map(mapCatalogToQuickPickItem);
});
let catalog = (await window.showQuickPick(itemsPromise, { ignoreFocusOut: true, canPickMany: false}))?.projectCatalog;
if (catalog === CUSTOM_CATALOG) {
// No available catalog selected enter the catalog manually
const name = await enterText({
title: "Name",
prompt: "Enter Name:",
validate: v => {
validate: async v => {
if (!/^\S+$/.test(v)) {
return "Invalid Project Catalog Name";
}
@@ -42,7 +42,7 @@ export async function handleCatalogAdd() {
title: "URL",
prompt: "Enter URL:",
placeholder: "https://github.com/my-org/my-project-catalog",
validate: v => {
validate: async v => {
try {
Uri.parse(v, true);
} catch (error) {
@@ -68,33 +68,17 @@ export async function handleCatalogAdd() {
}
export async function handleCatalogRemove() {
const catalog = await pickCatalog(CLI.projectCatalogList);
const itemsPromise = CLI.projectCatalogList().then(catalogs => catalogs.map(mapCatalogToQuickPickItem));
const catalog = (await window.showQuickPick(itemsPromise, { ignoreFocusOut: true, canPickMany: false}))?.projectCatalog;
if (catalog) {
return CLI.projectCatalogRemove(catalog.name);
}
}
async function pickCatalog(fetch: () => Thenable<ProjectCatalog[]>): Promise<ProjectCatalog | undefined> {
return new Promise(async (resolve, reject) => {
const quickPick = window.createQuickPick<ProjectCatalogQuickPick>();
quickPick.busy = true;
quickPick.title = "Loading Project Catalogs...";
quickPick.canSelectMany = false;
quickPick.show();
const catalogs = await fetch();
quickPick.items = catalogs.map(c => ({
label: c.name,
description: c.description,
details: c.tags ? c.tags.join(", ") : undefined,
projectCatalog: c
}));
quickPick.title = "Select Project Catalog";
quickPick.busy = false;
quickPick.onDidAccept(() => {
resolve(quickPick.selectedItems.length ? quickPick.selectedItems[0].projectCatalog : undefined);
quickPick.hide();
});
});
}
function mapCatalogToQuickPickItem(c: ProjectCatalog): ProjectCatalogQuickPickItem {
return {
label: c.name,
description: c.description,
projectCatalog: c
};
}

View File

@@ -5,15 +5,15 @@ import { CLI } from "./extension";
export async function handleProjectAdd() {
try {
const currentProjectNames = (await CLI.projectList()).map(p => p.name);
const currentProjectNamesPromise = CLI.projectList().then(projects => projects.map(p => p.name));
const name = await enterText({
title: "Name",
prompt: "Enter Name:",
validate: v => {
validate: async v => {
if (!/^\S+$/.test(v)) {
return "Invalid Project Catalog Name";
}
if (currentProjectNames.includes(v)) {
if ((await currentProjectNamesPromise).includes(v)) {
return "Name alreasy exists"
}
}
@@ -22,7 +22,7 @@ export async function handleProjectAdd() {
title: "URL",
prompt: "Enter URL:",
placeholder: "https://github.com/my-org/my-project",
validate: v => {
validate: async v => {
try {
Uri.parse(v, true);
} catch (error) {
@@ -39,6 +39,14 @@ export async function handleProjectAdd() {
prompt: "Enter Tags as strings separated by spaces and/or commas",
placeholder: "java, spring, eureka, config"
})).split(/(,)?\s+/);
if (name && url) {
return CLI.projectAdd({
name,
url,
description,
tags
});
}
} catch (error) {
// Ignore error - must have been cancelled
}
@@ -46,7 +54,8 @@ export async function handleProjectAdd() {
}
export async function handleProjectRemove() {
const name = (await window.showQuickPick(CLI.projectList().then(ps => ps.map(mapProjectToQuickPick)), {
const deferred = CLI.projectList().then(ps => ps.map(mapProjectToQuickPick));
const name = (await window.showQuickPick(deferred, {
canPickMany: false,
ignoreFocusOut: true,
}))?.label;

View File

@@ -1,6 +1,7 @@
import { InputBox, OpenDialogOptions, QuickPickItem, QuickPickItemKind, 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";
import debounce from "lodash.debounce";
export async function openDialogForFolder(customOptions: OpenDialogOptions): Promise<Uri> {
const options: OpenDialogOptions = {
@@ -21,7 +22,7 @@ export async function openDialogForFolder(customOptions: OpenDialogOptions): Pro
export function enterText(opts: {
title: string,
prompt: string,
validate?: (v: string) => string | undefined,
validate?: (v: string) => Promise<string | undefined>,
defaultValue?: string,
placeholder?: string
}): Promise<string> {
@@ -32,10 +33,9 @@ export function enterText(opts: {
inputBox.prompt = opts.prompt;
inputBox.value = opts.defaultValue;
inputBox.ignoreFocusOut = true;
inputBox.onDidChangeValue(() => {
inputBox.validationMessage = opts.validate ? opts.validate(inputBox.value) : undefined;
});
inputBox.onDidAccept(() => {
inputBox.onDidChangeValue(debounce(async v => inputBox.validationMessage = opts.validate ? await opts.validate(inputBox.value) : undefined, 300));
inputBox.onDidAccept(async () => {
inputBox.validationMessage = opts.validate ? await opts.validate(inputBox.value) : undefined;
if (!inputBox.validationMessage) {
resolve(inputBox.value);
inputBox.hide();
@@ -96,4 +96,10 @@ export function mapProjectToQuickPick(project: Project): QuickPickItem {
};
}
export function getWorkspaceRoot(): Uri | undefined {
if (workspace.workspaceFolders && workspace.workspaceFolders.length) {
return workspace.workspaceFolders[0].uri
}
}