[vscode-spring-cli] Basic AI command support

This commit is contained in:
aboyko
2024-01-27 23:51:23 -05:00
parent 48fcde87e3
commit cebd63c697
7 changed files with 171 additions and 29 deletions

View File

@@ -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": {

View File

@@ -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);
}
}

View File

@@ -24,8 +24,9 @@ export async function handleBootAdd(pom?: Uri): Promise<void> {
}
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`)));
}
}

View File

@@ -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<Uri> {
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<Uri>(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<void>(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));

View File

@@ -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<void> {
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<void> {
}

View File

@@ -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);
}

View File

@@ -67,12 +67,12 @@ function getWorkspaceFolderName(file: Uri): string {
}
export async function getTargetPomXml(): Promise<Uri> {
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<Uri> {
return undefined;
}
export async function getTargetGuideMardown(): Promise<Uri> {
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 {
}