Polish extension

This commit is contained in:
aboyko
2024-05-17 15:12:03 -04:00
parent fe953bde5b
commit b50fef99ba
11 changed files with 250 additions and 98 deletions

View File

@@ -2,19 +2,33 @@ import { Uri, commands, window } from "vscode";
import { CLI } from "./extension";
import { enterText, getTargetPomXml } from "./utils";
import path from "path";
import { handleGuideApply } from "./guide";
import { handleGuideApplyWorkspaceEdit } from "./guide";
import { CANCELLED } from "./cli";
export async function handleAiAdd(pom: Uri) {
if (!pom) {
pom = await getTargetPomXml();
let question: string;
try {
if (!pom) {
pom = await getTargetPomXml();
}
question = await enterText({
title: "Question",
prompt: "Enter question to LLM",
});
} catch (error) {
// ignore: cancellation via UI
}
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);
try {
if (question.trim().length > 0) {
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")) {
handleGuideApplyWorkspaceEdit(uri);
}
}
} catch (error) {
if (error !== CANCELLED) {
window.showErrorMessage(error);
}
}
}

View File

@@ -10,6 +10,8 @@ import { WorkspaceEdit } from "vscode-languageclient";
export const SPRING_CLI_TASK_TYPE = 'spring-cli';
export const CANCELLED = "Cancelled";
export class Cli {
get executable(): string {
@@ -46,7 +48,7 @@ export class Cli {
"--file",
uri.fsPath
];
return this.fetchJson("Running guide", uri.fsPath, args, cwd || path.dirname(uri.fsPath));
return this.fetchJson("Running guide", uri.fsPath, args, cwd || path.dirname(uri.fsPath), true);
}
aiAdd(question: string, cwd: string): Thenable<Uri> {
@@ -70,23 +72,31 @@ export class Cli {
return new Promise<Uri>(async (resolve, reject) => {
if (cancellation.isCancellationRequested) {
reject("Cancelled");
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;
let errorMessageInStdOut;
process.stdout.on("data", s => {
const res = /README-\S+.md/.exec(s);
if (res.length) {
if (res && res.length) {
guideFileName = res[0].trim();
} else {
errorMessageInStdOut = s;
}
});
process.stderr.on("data", s => errorMessageChunks.push(s))
process.on("exit", (code) => {
if (code) {
reject(`Failed to get response from LLM. ${errorMessageChunks.join()}`);
if (cancellation.isCancellationRequested) {
reject(CANCELLED);
} else {
const errorMessage = (errorMessageChunks.length == 0 ? errorMessageInStdOut : errorMessageChunks.join()) || "";
reject(`Failed to get response from LLM. ${errorMessage}`);
}
} else {
resolve(Uri.file(path.join(cwd, guideFileName)));
}
@@ -159,12 +169,20 @@ export class Cli {
});
}
commandNew(cwd: string) {
commandNew(cwd: string, commandName?: string, subCommandName?: string) {
const args = [
"command",
"new",
];
return this.exec("Remove Command", undefined, args, cwd);
if (commandName) {
args.push("--command-name")
args.push(commandName);
}
if (subCommandName) {
args.push("--sub-command-name")
args.push(subCommandName);
}
return this.exec("New Command", undefined, args, cwd);
}
commandExecute(metadata: CommandExecuteMetadata, cwd: string) {
@@ -309,7 +327,7 @@ export class Cli {
});
}
private async fetchJson<T>(title: string, message: string, args: string[], cwd?: string) : Promise<T> {
private async fetchJson<T>(title: string, message: string, args: string[], cwd?: string, omitJsonParam?: boolean) : Promise<T> {
return window.withProgress({
location: ProgressLocation.Window,
@@ -323,16 +341,20 @@ export class Cli {
return new Promise<T>(async (resolve, reject) => {
if (cancellation.isCancellationRequested) {
reject("Cancelled");
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(" ")} --json`, processOpts);
const process = this.executable.endsWith(".jar") ? await cp.exec(`java -jar ${this.executable} ${args.join(" ")}`, processOpts) : await cp.exec(`${this.executable} ${args.join(" ")} ${omitJsonParam ? "" : "--json"}`, 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()}`);
if (cancellation.isCancellationRequested) {
reject(CANCELLED);
} else {
reject(`Failed to fetch data: ${dataChunks.join()}`);
}
} else {
try {
resolve(JSON.parse(dataChunks.join()) as T);

View File

@@ -3,6 +3,7 @@ import { enterText } from "./utils";
import { CLI } from "./extension";
import { homedir } from "os";
import { CommandInfo } from "./cli-types";
import { CANCELLED } from "./cli";
interface SubCommandQuickPickItem extends QuickPickItem {
info?: CommandInfo;
@@ -50,22 +51,50 @@ export async function handleCommandAdd(uri?: Uri) {
}
export async function handleCommandRemove(uri?: Uri) {
// Enter CWD for CLI (global vs workspace folder local command)
const cwd = await enterCwd(uri);
// Select the command
const command = await pickCommand(cwd);
// Select the subcommand
const subcommand = (await pickSubCommand(cwd, command))?.label;
return CLI.commandRemove({
command,
subcommand,
cwd
});
try {
// Enter CWD for CLI (global vs workspace folder local command)
const cwd = await enterCwd(uri);
// Select the command
const command = await pickCommand(cwd);
if (!command) {
return;
}
// Select the subcommand
const subcommand = (await pickSubCommand(cwd, command))?.label;
if (!subcommand) {
return;
}
return CLI.commandRemove({
command,
subcommand,
cwd
});
} catch (error) {
if (error !== CANCELLED) {
window.showErrorMessage(error);
}
}
}
export async function handleCommandNew(uri?: Uri) {
const cwd = await enterCwd(uri);
return CLI.commandNew(cwd);
const cmdName = await enterText({
prompt: "Enter Command Name",
title: "Command Name",
defaultValue: "hello"
});
if (!cmdName) {
return;
}
const subCmdName = await enterText({
prompt: "Enter Sub-Command Name",
title: "Sub-Command Name",
defaultValue: "new"
});
if (!subCmdName) {
return;
}
return CLI.commandNew(cwd, cmdName, subCmdName);
}
export async function handleCommandExecute(uri?: Uri) {
@@ -73,39 +102,51 @@ export async function handleCommandExecute(uri?: Uri) {
const cwd = await enterCwd(uri);
// Select the command
const command = await pickCommand(cwd);
if (!command) {
return;
}
// Select the subcommand
const subcommand = (await pickSubCommand(cwd, command));
if (!subcommand) {
return;
}
const params = {};
if (Array.isArray(subcommand?.info?.options)) {
for (const o of subcommand.info.options) {
let value;
if (o.choices) {
const quickPickItems = Object.keys(o.choices).map(s => ({
label: s,
value: o.choices[s]
}) as ChoiceQuickPickItem);
value = (await window.showQuickPick(quickPickItems, { canPickMany: false, ignoreFocusOut: true })).value;
} else {
value = await enterText({
title: o.paramLabel || o.name,
defaultValue: o.defaultValue,
placeholder: o.defaultValue,
prompt: `Enter ${o.paramLabel || o.name}${o.description ? " - " : ""}${o.description}`
});
}
if (value) {
params[o.name] = value;
}
}
try {
if (Array.isArray(subcommand?.info?.options)) {
for (const o of subcommand.info.options) {
let value;
if (o.choices) {
const quickPickItems = Object.keys(o.choices).map(s => ({
label: s,
value: o.choices[s]
}) as ChoiceQuickPickItem);
value = (await window.showQuickPick(quickPickItems, { canPickMany: false, ignoreFocusOut: true })).value;
} else {
value = await enterText({
title: o.paramLabel || o.name,
defaultValue: o.defaultValue,
placeholder: o.defaultValue,
prompt: `Enter ${o.paramLabel || o.name}${o.description ? " - " : ""}${o.description}`
});
}
if (value) {
params[o.name] = value;
}
}
}
return CLI.commandExecute({
command,
subcommand: subcommand.label,
params
}, cwd);
} catch (error) {
if (error !== CANCELLED) {
window.showErrorMessage(error);
}
}
return CLI.commandExecute({
command,
subcommand: subcommand.label,
params
}, cwd);
}
function mapFolderToQuickPickItem(folder: WorkspaceFolder): QuickPickItem {

View File

@@ -6,7 +6,7 @@ import { handleProjectAdd, handleProjectRemove } from "./project";
import { handleCommandAdd, handleCommandExecute, handleCommandNew, handleCommandRemove } from "./command";
import { ExtensionContext, commands, tasks } from "vscode";
import { handleAiAdd } from "./ai";
import { handleGuideApply, handleGuideRun } from "./guide";
import { handleGuideApplyWorkspaceEdit } from "./guide";
export const CLI = new Cli();
@@ -29,8 +29,7 @@ export async function activate(context: ExtensionContext): Promise<void> {
commands.registerCommand('vscode-spring-cli.ai.add', handleAiAdd),
commands.registerCommand('vscode-spring-cli.guide.apply', handleGuideApply),
commands.registerCommand('vscode-spring-cli.guide.run', handleGuideRun),
commands.registerCommand('vscode-spring-cli.guide.apply', handleGuideApplyWorkspaceEdit),
tasks.registerTaskProvider(SPRING_CLI_TASK_TYPE, new CliTaskProvider(CLI))
);

View File

@@ -3,6 +3,10 @@ import { getTargetGuideMardown } from "./utils";
import { CLI } from "./extension";
import { createConverter } from "vscode-languageclient/lib/common/protocolConverter";
import fs from "fs";
import { CANCELLED } from "./cli";
const APPLY = "Apply";
const PREVIEW = "Preview"
const CONVERTER = createConverter(undefined, true, true);
export async function handleGuideApply(uri: Uri) {
@@ -12,23 +16,41 @@ export async function handleGuideApply(uri: Uri) {
return CLI.guideApply(uri);
}
export async function handleGuideRun(uri: Uri) {
if (!uri) {
uri = await getTargetGuideMardown();
}
const lspEdit = await CLI.guideLspEdit(uri);
const workspaceEdit = await CONVERTER.asWorkspaceEdit(lspEdit);
// This is some sort of workaround for undo
// If editors for existing doc not opened then undo isn't properly working
await Promise.all(workspaceEdit.entries().map(async ([uri, edits]) => {
if (fs.existsSync(uri.fsPath)) {
const doc = await workspace.openTextDocument(uri.fsPath);
await window.showTextDocument(doc);
export async function handleGuideApplyWorkspaceEdit(uri: Uri) {
try {
if (!uri) {
uri = await getTargetGuideMardown();
}
}));
const lspEdit = await CLI.guideLspEdit(uri);
if (lspEdit.changeAnnotations && Object.keys(lspEdit.changeAnnotations).length > 0) {
const answer = await window.showInformationMessage("Do you want to apply changes right away or preview before applying?", APPLY, PREVIEW);
if (!answer) {
return;
}
const preview = answer === PREVIEW;
Object.keys(lspEdit.changeAnnotations).forEach(changeId => {
lspEdit.changeAnnotations[changeId].needsConfirmation = preview;
});
}
const workspaceEdit = await CONVERTER.asWorkspaceEdit(lspEdit);
return await workspace.applyEdit(workspaceEdit, {
isRefactoring: true
});
// This is some sort of workaround for undo
// If editors for existing doc not opened then undo isn't properly working
await Promise.all(workspaceEdit.entries().map(async ([uri, edits]) => {
if (fs.existsSync(uri.fsPath)) {
const doc = await workspace.openTextDocument(uri.fsPath);
await window.showTextDocument(doc);
}
}));
return await workspace.applyEdit(workspaceEdit, {
isRefactoring: true
});
} catch (error) {
if (error !== CANCELLED) {
window.showErrorMessage(error);
}
}
}