diff --git a/vscode-extensions/vscode-spring-cli/package.json b/vscode-extensions/vscode-spring-cli/package.json index 5f2bd3166..8c949baee 100644 --- a/vscode-extensions/vscode-spring-cli/package.json +++ b/vscode-extensions/vscode-spring-cli/package.json @@ -31,6 +31,11 @@ "when": "resourceFilename == pom.xml", "command": "vscode-spring-cli.boot.add", "group": "Spring-CLI" + }, + { + "when": "resourceFilename =~ /README-\\S+.md/", + "command": "vscode-spring-cli.guide.apply", + "group": "Spring-CLI" } ], "explorer/context": [ @@ -38,6 +43,11 @@ "when": "resourceFilename == pom.xml", "command": "vscode-spring-cli.boot.add", "group": "Spring-CLI" + }, + { + "when": "resourceFilename =~ /README-\\S+.md/", + "command": "vscode-spring-cli.guide.apply", + "group": "Spring-CLI" } ] }, @@ -91,6 +101,16 @@ "command": "vscode-spring-cli.command.execute", "title": "Execute User Defined Command", "category": "Spring CLI" + }, + { + "command": "vscode-spring-cli.ai.add", + "title": "Add from AI", + "category": "Spring CLI" + }, + { + "command": "vscode-spring-cli.guide.apply", + "title": "Apply Guide", + "category": "Spring CLI" } ], "configuration": { diff --git a/vscode-extensions/vscode-spring-cli/src/ai.ts b/vscode-extensions/vscode-spring-cli/src/ai.ts new file mode 100644 index 000000000..e76f23c6e --- /dev/null +++ b/vscode-extensions/vscode-spring-cli/src/ai.ts @@ -0,0 +1,20 @@ +import { Uri, commands, window } from "vscode"; +import { CLI } from "./extension"; +import { enterText, getTargetPomXml } from "./utils"; +import path from "path"; +import { handleGuideApply } from "./guide"; + +export async function handleAiAdd(pom: Uri) { + if (!pom) { + pom = await getTargetPomXml(); + } + const question = await enterText({ + title: "Question", + prompt: "Enter question to LLM", + }); + const uri = await CLI.aiAdd(question, path.dirname(pom.fsPath)); + await commands.executeCommand("markdown.showPreview", uri); + if ("Yes" === await window.showInformationMessage(`Apply guide '${path.basename(uri.fsPath)}' to the project?`, "Yes", "No")) { + handleGuideApply(uri); + } +} diff --git a/vscode-extensions/vscode-spring-cli/src/boot.ts b/vscode-extensions/vscode-spring-cli/src/boot.ts index e61653a50..f014dfca0 100644 --- a/vscode-extensions/vscode-spring-cli/src/boot.ts +++ b/vscode-extensions/vscode-spring-cli/src/boot.ts @@ -24,8 +24,9 @@ export async function handleBootAdd(pom?: Uri): Promise { } if (metadata.catalog) { await CLI.bootAdd(metadata); - const doc = await workspace.openTextDocument(path.join(metadata.targetFolder, `README-${metadata.catalog}.md`)); - await window.showTextDocument(doc); + // const doc = await workspace.openTextDocument(path.join(metadata.targetFolder, `README-${metadata.catalog}.md`)); + // await window.showTextDocument(doc); + await commands.executeCommand("markdown.showPreview", Uri.file(path.join(metadata.targetFolder, `README-${metadata.catalog}.md`))); } } diff --git a/vscode-extensions/vscode-spring-cli/src/cli.ts b/vscode-extensions/vscode-spring-cli/src/cli.ts index 8a8a8359f..60ac2c117 100644 --- a/vscode-extensions/vscode-spring-cli/src/cli.ts +++ b/vscode-extensions/vscode-spring-cli/src/cli.ts @@ -1,5 +1,5 @@ import { BootAddMetadata, BootNewMetadata, Project, ProjectCatalog, CommandAddMetadata, CommandRemoveMetadata, CommandInfo, CommandExecuteMetadata } from "./cli-types"; -import { CancellationToken, ProcessExecution, ProgressLocation, ProviderResult, Task, TaskProvider, TaskScope, Uri, tasks, window, workspace } from "vscode"; +import { CancellationToken, ProcessExecution, ProgressLocation, ProviderResult, ShellExecution, Task, TaskProvider, TaskScope, Uri, tasks, window, workspace } from "vscode"; import cp from "child_process"; import { homedir } from "os"; import { getWorkspaceRoot } from "./utils"; @@ -27,6 +27,63 @@ export class Cli { return this.fetchJson("Fetching Available Catalogs...", undefined, ["project-catalog", "list-available"]); } + guideApply(uri: Uri, cwd?: string) { + const args = [ + "guide", + "apply", + "--file", + uri.fsPath + ]; + return this.exec("Applying guide", uri.fsPath, args, cwd || path.dirname(uri.fsPath)); + } + + aiAdd(question: string, cwd: string): Thenable { + + const args = [ + "ai", + "add", + "--preview", + "true", + "--description", + `"${question}"` + ]; + + return window.withProgress({ + location: ProgressLocation.Window, + cancellable: true, + title: "Add from AI", + }, (progress, cancellation) => { + + progress.report({ message: question }); + + return new Promise(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 errorMessageChunks = []; + let guideFileName; + process.stdout.on("data", s => { + const res = /README-\S+.md/.exec(s); + if (res.length) { + guideFileName = res[0].trim(); + } + }); + process.stderr.on("data", s => errorMessageChunks.push(s)) + process.on("exit", (code) => { + if (code) { + reject(`Failed to get response from LLM. ${errorMessageChunks.join()}`); + } else { + resolve(Uri.file(path.join(cwd, guideFileName))); + } + }); + }); + }); + + } + commandAdd(metadata: CommandAddMetadata, cwd?: string) { const args = [ "command", @@ -216,6 +273,8 @@ export class Cli { return new Promise(async (resolve, reject) => { const processOpts = { cwd: cwd || getWorkspaceRoot()?.fsPath || homedir() }; + + // const process = this.executable.endsWith(".jar") ? new ShellExecution("java", [ "-jar", this.executable, ...args], processOpts) : new ShellExecution(this.executable, args, processOpts) 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); @@ -225,7 +284,8 @@ export class Cli { const cancelListener = cancellation.onCancellationRequested(() => { cancelListener.dispose(); reject(); - }) + }); + const listener = tasks.onDidEndTaskProcess(e => { if (e.execution === taskExecution) { listener.dispose(); @@ -254,7 +314,7 @@ export class Cli { reject("Cancelled"); } const processOpts = { cwd: cwd || getWorkspaceRoot()?.fsPath || homedir() }; - const process = this.executable.endsWith(".jar") ? await cp.exec(`java -jar ${this.executable} ${args.join(" ")} --json`, processOpts) : await cp.exec(`${this.executable} ${args.join(" ")}`, processOpts); + const process = this.executable.endsWith(".jar") ? await cp.exec(`java -jar ${this.executable} ${args.join(" ")} --json`, processOpts) : await cp.exec(`${this.executable} ${args.join(" ")} --json`, processOpts); cancellation.onCancellationRequested(() => process.kill()); const dataChunks: string[] = []; process.stdout.on("data", s => dataChunks.push(s)); diff --git a/vscode-extensions/vscode-spring-cli/src/extension.ts b/vscode-extensions/vscode-spring-cli/src/extension.ts index 09e102e42..cd3d36012 100644 --- a/vscode-extensions/vscode-spring-cli/src/extension.ts +++ b/vscode-extensions/vscode-spring-cli/src/extension.ts @@ -5,31 +5,37 @@ import { handleCatalogAdd, handleCatalogRemove } from "./project-catalog"; import { handleProjectAdd, handleProjectRemove } from "./project"; import { handleCommandAdd, handleCommandExecute, handleCommandNew, handleCommandRemove } from "./command"; import { ExtensionContext, commands, tasks } from "vscode"; +import { handleAiAdd } from "./ai"; +import { handleGuideApply } from "./guide"; export const CLI = new Cli(); export async function activate(context: ExtensionContext): Promise { - context.subscriptions.push(...[ - - ]); - context.subscriptions.push(commands.registerCommand('vscode-spring-cli.boot.new', handleBootNew)); - context.subscriptions.push(commands.registerCommand('vscode-spring-cli.boot.add', handleBootAdd)); - - context.subscriptions.push(commands.registerCommand('vscode-spring-cli.project-catalog.add', handleCatalogAdd)); - context.subscriptions.push(commands.registerCommand('vscode-spring-cli.project-catalog.remove', handleCatalogRemove)); - - context.subscriptions.push(commands.registerCommand('vscode-spring-cli.project.add', handleProjectAdd)); - context.subscriptions.push(commands.registerCommand('vscode-spring-cli.project.remove', handleProjectRemove)); - - context.subscriptions.push(commands.registerCommand('vscode-spring-cli.command.add', handleCommandAdd)); - context.subscriptions.push(commands.registerCommand('vscode-spring-cli.command.remove', handleCommandRemove)); - context.subscriptions.push(commands.registerCommand('vscode-spring-cli.command.new', handleCommandNew)); - context.subscriptions.push(commands.registerCommand('vscode-spring-cli.command.execute', handleCommandExecute)); - - context.subscriptions.push(tasks.registerTaskProvider(SPRING_CLI_TASK_TYPE, new CliTaskProvider(CLI))); + context.subscriptions.push( + commands.registerCommand('vscode-spring-cli.boot.new', handleBootNew), + commands.registerCommand('vscode-spring-cli.boot.add', handleBootAdd), + + commands.registerCommand('vscode-spring-cli.project-catalog.add', handleCatalogAdd), + commands.registerCommand('vscode-spring-cli.project-catalog.remove', handleCatalogRemove), + + commands.registerCommand('vscode-spring-cli.project.add', handleProjectAdd), + commands.registerCommand('vscode-spring-cli.project.remove', handleProjectRemove), + + commands.registerCommand('vscode-spring-cli.command.add', handleCommandAdd), + commands.registerCommand('vscode-spring-cli.command.remove', handleCommandRemove), + commands.registerCommand('vscode-spring-cli.command.new', handleCommandNew), + commands.registerCommand('vscode-spring-cli.command.execute', handleCommandExecute), + + commands.registerCommand('vscode-spring-cli.ai.add', handleAiAdd), + + commands.registerCommand('vscode-spring-cli.guide.apply', handleGuideApply), + + tasks.registerTaskProvider(SPRING_CLI_TASK_TYPE, new CliTaskProvider(CLI)) + ); } export async function deactivate(): Promise { } + diff --git a/vscode-extensions/vscode-spring-cli/src/guide.ts b/vscode-extensions/vscode-spring-cli/src/guide.ts new file mode 100644 index 000000000..8bc180785 --- /dev/null +++ b/vscode-extensions/vscode-spring-cli/src/guide.ts @@ -0,0 +1,10 @@ +import { Uri } from "vscode"; +import { getTargetGuideMardown } from "./utils"; +import { CLI } from "./extension"; + +export async function handleGuideApply(uri: Uri) { + if (!uri) { + uri = await getTargetGuideMardown(); + } + return CLI.guideApply(uri); +} \ 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 fe72e0760..e5641b0d3 100644 --- a/vscode-extensions/vscode-spring-cli/src/utils.ts +++ b/vscode-extensions/vscode-spring-cli/src/utils.ts @@ -67,12 +67,12 @@ function getWorkspaceFolderName(file: Uri): string { } export async function getTargetPomXml(): Promise { - if (window.activeTextEditor) { - const activeUri = window.activeTextEditor.document.uri; - if ("pom.xml" === path.basename(activeUri.path).toLowerCase()) { - return activeUri; - } - } + // if (window.activeTextEditor) { + // const activeUri = window.activeTextEditor.document.uri; + // if ("pom.xml" === path.basename(activeUri.path).toLowerCase()) { + // return activeUri; + // } + // } const candidates: Uri[] = await workspace.findFiles("**/pom.xml"); if (candidates.length > 0) { @@ -88,6 +88,29 @@ export async function getTargetPomXml(): Promise { return undefined; } +export async function getTargetGuideMardown(): Promise { + if (window.activeTextEditor) { + const activeUri = window.activeTextEditor.document.uri; + if (/README-\S+.md/.test(path.basename(activeUri.path).toLowerCase())) { + return activeUri; + } + } + + const candidates: Uri[] = await workspace.findFiles("**/README-*.md"); + if (candidates.length > 0) { + if (candidates.length === 1) { + return candidates[0]; + } else { + return await window.showQuickPick( + candidates.map((c: Uri) => ({ value: c, label: getRelativePathToWorkspaceFolder(c), description: getWorkspaceFolderName(c) })), + { placeHolder: "Select the target project." }, + ).then(res => res && res.value); + } + } + return undefined; +} + + export function mapProjectToQuickPick(project: Project): QuickPickItem { return { label: project.name, @@ -103,3 +126,5 @@ export function getWorkspaceRoot(): Uri | undefined { } + +