copilot spring boot agent implementation
This commit is contained in:
@@ -6,7 +6,8 @@ import {
|
||||
window,
|
||||
workspace,
|
||||
ExtensionContext,
|
||||
Uri
|
||||
Uri,
|
||||
lm
|
||||
} from 'vscode';
|
||||
|
||||
import * as commons from '@pivotal-tools/commons-vscode';
|
||||
@@ -22,6 +23,11 @@ import * as setLogLevelUi from './set-log-levels-ui';
|
||||
import { startTestJarSupport } from "./test-jar-launch";
|
||||
import { startPropertiesConversionSupport } from "./convert-props-yaml";
|
||||
import { activateCopilotFeatures } from "./copilot";
|
||||
import * as springBootAgent from './copilot/springBootAgent';
|
||||
import { SpringCli } from './copilot/springCli';
|
||||
import { applyLspEdit } from "./copilot/guideApply";
|
||||
import { isLlmApiReady } from "./copilot/util";
|
||||
import CopilotRequest, { logger } from "./copilot/copilotRequest";
|
||||
|
||||
const PROPERTIES_LANGUAGE_ID = "spring-boot-properties";
|
||||
const YAML_LANGUAGE_ID = "spring-boot-properties-yaml";
|
||||
@@ -32,6 +38,7 @@ const JPA_QUERY_PROPERTIES_LANGUAGE_ID = "jpa-query-properties";
|
||||
|
||||
const STOP_ASKING = "Stop Asking";
|
||||
|
||||
export const SPRINGCLI = new SpringCli();
|
||||
/** Called when extension is activated */
|
||||
export function activate(context: ExtensionContext): Thenable<ExtensionAPI> {
|
||||
|
||||
@@ -162,9 +169,15 @@ export function activate(context: ExtensionContext): Thenable<ExtensionAPI> {
|
||||
rewrite.activate(client, options, context);
|
||||
setLogLevelUi.activate(client, options, context);
|
||||
startPropertiesConversionSupport(context);
|
||||
if(isLlmApiReady)
|
||||
activateSpringBootParticipant(context);
|
||||
else
|
||||
window.showInformationMessage("Spring Boot chat participant is not available. Please use the vscode insiders version 1.90.0 or above and make sure all `lm` API is enabled.");
|
||||
|
||||
registerMiscCommands(context);
|
||||
|
||||
commands.registerCommand('vscode-spring-boot.agent.apply', applyLspEdit);
|
||||
|
||||
return new ApiManager(client).api;
|
||||
});
|
||||
}
|
||||
@@ -192,3 +205,13 @@ function registerMiscCommands(context: ExtensionContext) {
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
async function activateSpringBootParticipant(context: ExtensionContext) {
|
||||
const model = (await lm.selectChatModels(CopilotRequest.DEFAULT_MODEL_SELECTOR))?.[0];
|
||||
if (!model) {
|
||||
const models = await lm.selectChatModels();
|
||||
logger.error(`Not a suitable model. The available models are: [${models.map(m => m.name).join(', ')}]. Please make sure you have installed the latest "GitHub Copilot Chat" (v0.16.0 or later) and all \`lm\` API is enabled.`);
|
||||
return;
|
||||
}
|
||||
springBootAgent.activate(context);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
import { LanguageModelChatRequestOptions, LanguageModelChatSelector, CancellationToken, Disposable, LanguageModelChatMessage, window, ProgressLocation, lm, LogOutputChannel, LanguageModelChatMessageRole, LanguageModelError } from "vscode";
|
||||
|
||||
export const logger: LogOutputChannel = window.createOutputChannel("Spring tools agent", { log: true });
|
||||
|
||||
export default class CopilotRequest {
|
||||
|
||||
public static readonly DEFAULT_END_MARK = '<|endofresponse|>';
|
||||
public static readonly DEFAULT_MAX_ROUNDS = 2;
|
||||
public static readonly DEFAULT_MODEL_SELECTOR: LanguageModelChatSelector = { vendor: 'copilot', family: 'gpt-3.5-turbo' };
|
||||
public static readonly DEFAULT_MODEL_OPTIONS: LanguageModelChatRequestOptions = { modelOptions: {} };
|
||||
|
||||
public static readonly NOT_CANCELLABLE: CancellationToken = { isCancellationRequested: false, onCancellationRequested: () => Disposable.from() };
|
||||
|
||||
public constructor(
|
||||
private readonly systemMessagesOrPrompts: LanguageModelChatMessage[] = [],
|
||||
private readonly modelSelector: LanguageModelChatSelector = CopilotRequest.DEFAULT_MODEL_SELECTOR,
|
||||
private readonly modelOptions: LanguageModelChatRequestOptions = CopilotRequest.DEFAULT_MODEL_OPTIONS,
|
||||
private readonly endMark: string = CopilotRequest.DEFAULT_END_MARK,
|
||||
private readonly maxRounds: number = CopilotRequest.DEFAULT_MAX_ROUNDS,
|
||||
) {
|
||||
}
|
||||
|
||||
public async chatRequest(userMessage: LanguageModelChatMessage[], modelOptions: LanguageModelChatRequestOptions = CopilotRequest.DEFAULT_MODEL_OPTIONS, cancellationToken: CancellationToken = CopilotRequest.NOT_CANCELLABLE): Promise<string> {
|
||||
const messages = [...this.systemMessagesOrPrompts];
|
||||
let answer: string = '';
|
||||
let rounds: number = 0;
|
||||
|
||||
return window.withProgress({
|
||||
location: ProgressLocation.Window,
|
||||
title: "Copilot request",
|
||||
cancellable: true
|
||||
}, async (progress, cancellation) => {
|
||||
progress.report({ message: "processing..." });
|
||||
|
||||
if (cancellation.isCancellationRequested) {
|
||||
logger.info("Chat request cancelled");
|
||||
return 'Chat request cancelled';
|
||||
}
|
||||
const _send = async (message: LanguageModelChatMessage[]): Promise<boolean> => {
|
||||
rounds++;
|
||||
let response: string = '';
|
||||
messages.push(...message);
|
||||
try {
|
||||
messages.forEach(m => logger.info(m.content));
|
||||
response = await this.sendRequest(messages, modelOptions, cancellationToken);
|
||||
answer += response;
|
||||
logger.info(`Response: \n`, response);
|
||||
} catch (e) {
|
||||
if (e instanceof LanguageModelError) {
|
||||
logger.error(e.message, e.code);
|
||||
throw e;
|
||||
} else {
|
||||
const cause = e.cause || e;
|
||||
logger.error(`Failed to chat with copilot`, e.message, e.stack);
|
||||
throw cause;
|
||||
}
|
||||
|
||||
}
|
||||
messages.push(new LanguageModelChatMessage(LanguageModelChatMessageRole.Assistant, response));
|
||||
return answer.trim().endsWith(this.endMark);
|
||||
}
|
||||
let completeResponse: boolean = await _send(userMessage);
|
||||
while (!completeResponse && rounds < this.maxRounds) {
|
||||
completeResponse = await _send([new LanguageModelChatMessage(LanguageModelChatMessageRole.User, `continue your response from where you left off, or end your response with "//${this.endMark}" to finish the conversation.`)]);
|
||||
}
|
||||
logger.debug('rounds', rounds);
|
||||
return answer.replace("//"+this.endMark, "");
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
private async selectModel() {
|
||||
const model = (await lm.selectChatModels(this.modelSelector))?.[0];
|
||||
if (!model) {
|
||||
const models = await lm.selectChatModels();
|
||||
throw new Error(`No suitable model, available models: [${models.map(m => m.name).join(', ')}]. Please make sure you have installed the latest "GitHub Copilot Chat" (v0.16.0 or later).`);
|
||||
}
|
||||
return model;
|
||||
}
|
||||
|
||||
private async sendRequest(messages: LanguageModelChatMessage[], modelOptions: LanguageModelChatRequestOptions, cancellationToken: CancellationToken) {
|
||||
const response = [];
|
||||
const model = await this.selectModel();
|
||||
const chatResponse = await model.sendRequest(messages, modelOptions ?? this.modelOptions, cancellationToken);
|
||||
for await (const fragment of chatResponse.text) {
|
||||
response.push(fragment);
|
||||
}
|
||||
return response.join('');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { Uri, workspace, window } from "vscode";
|
||||
import { SPRINGCLI } from "../Main";
|
||||
import { getTargetGuideMardown } from "./util";
|
||||
import { createConverter } from "vscode-languageclient/lib/common/protocolConverter";
|
||||
import fs from "fs";
|
||||
|
||||
|
||||
const CONVERTER = createConverter(undefined, true, true);
|
||||
const CANCELLED = "Cancelled";
|
||||
|
||||
export async function applyLspEdit(uri: Uri) {
|
||||
try {
|
||||
if (!uri) {
|
||||
uri = await getTargetGuideMardown();
|
||||
}
|
||||
const lspEdit = await SPRINGCLI.guideLspEdit(uri);
|
||||
const workspaceEdit = await CONVERTER.asWorkspaceEdit(lspEdit);
|
||||
console.log(lspEdit);
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
import CopilotRequest from "./copilotRequest";
|
||||
import { CancellationToken, chat, ChatContext, ChatRequest, ChatResponseStream, ChatResult, commands, ExtensionContext, l10n, LanguageModelChatMessage, LanguageModelChatMessageRole, Uri, workspace } from "vscode";
|
||||
import { systemBoot2Prompt, systemBoot3Prompt, systemPrompt } from "./system-ai-prompt";
|
||||
import { userPrompt } from "./user-ai-prompt";
|
||||
import { getWorkspaceRoot, writeResponseToFile } from "./util";
|
||||
import { SPRINGCLI } from "../Main";
|
||||
|
||||
const PARTICIPANT_ID = 'springboot.agent';
|
||||
const SYSTEM_PROMPT = systemPrompt;
|
||||
const USER_PROMPT = userPrompt;
|
||||
|
||||
interface BootProjectInfo {
|
||||
name: string;
|
||||
uri: string;
|
||||
mainClass: string;
|
||||
buildTool: string;
|
||||
springBootVersion: string;
|
||||
javaVersion: string;
|
||||
}
|
||||
|
||||
interface SpringBootChatAgentResult extends ChatResult {
|
||||
metadata: {
|
||||
command: string;
|
||||
}
|
||||
}
|
||||
|
||||
export default class SpringBootChatAgent {
|
||||
|
||||
copilotRequest: CopilotRequest;
|
||||
|
||||
constructor(copilotRequest: CopilotRequest) {
|
||||
this.copilotRequest = copilotRequest;
|
||||
}
|
||||
|
||||
public async handlePrompts(request: ChatRequest, context: ChatContext, stream: ChatResponseStream, cancellationToken: CancellationToken): Promise<SpringBootChatAgentResult> {
|
||||
|
||||
const selectedProject = (await getWorkspaceRoot());
|
||||
if(!selectedProject) {
|
||||
stream.markdown('No project selected from the workspace');
|
||||
return;
|
||||
}
|
||||
const selectedProjectUri = Uri.file(selectedProject?.fsPath).toString();
|
||||
|
||||
// Fetch project related information from the Spring boot language server
|
||||
const bootProjInfo = await commands.executeCommand("sts/spring-boot/bootProjectInfo", selectedProjectUri) as BootProjectInfo;
|
||||
const projectContext = `
|
||||
Use the following project information for the solution: Please suggest code compatible with the project version.
|
||||
|
||||
Main Spring project name: ${bootProjInfo.name}
|
||||
Root Package name: ${bootProjInfo.mainClass.substring(0, bootProjInfo.mainClass.lastIndexOf('.'))}
|
||||
Build tool: ${bootProjInfo.buildTool}
|
||||
Spring boot version: ${bootProjInfo.springBootVersion}
|
||||
Java version: ${bootProjInfo.javaVersion}
|
||||
User prompt: ${request.prompt}
|
||||
`;
|
||||
|
||||
// Enhance prompt with project information and user prompt. Provide spring boot 3 speicifc context when necessary
|
||||
const messages = [
|
||||
LanguageModelChatMessage.User(projectContext),
|
||||
bootProjInfo.springBootVersion.startsWith('3') ? LanguageModelChatMessage.User(systemBoot3Prompt) : LanguageModelChatMessage.User(systemBoot2Prompt),
|
||||
LanguageModelChatMessage.User('User Input: ' +request.prompt),
|
||||
|
||||
];
|
||||
|
||||
stream.progress('Generating code....This will take a few minutes');
|
||||
|
||||
// Chat request to copilot LLM
|
||||
const response = await this.copilotRequest.chatRequest(messages, {}, cancellationToken);
|
||||
|
||||
// write the response to markdown file
|
||||
const targetMarkdownUri = await writeResponseToFile(response, bootProjInfo.name, selectedProject.fsPath);
|
||||
|
||||
let documentContent;
|
||||
|
||||
if (!targetMarkdownUri) {
|
||||
documentContent = 'Note: The code provided is just an example and may not be suitable for production use. \n ' + response;
|
||||
} else {
|
||||
// modify the response from copilot LLM i.e. make response Boot 3 compliant if necessary
|
||||
if (bootProjInfo.springBootVersion.startsWith('3')) {
|
||||
const enhancedResponse = await SPRINGCLI.enhanceResponse(targetMarkdownUri, selectedProject.fsPath);
|
||||
await writeResponseToFile(enhancedResponse, bootProjInfo.name, selectedProject.fsPath);
|
||||
}
|
||||
documentContent = await workspace.fs.readFile(targetMarkdownUri);
|
||||
}
|
||||
|
||||
const chatResponse = Buffer.from(documentContent).toString();
|
||||
stream.markdown(chatResponse);
|
||||
stream.button({
|
||||
command: 'vscode-spring-boot.agent.apply',
|
||||
title: l10n.t('Preview Changes')
|
||||
});
|
||||
return { metadata: { command: '' } };
|
||||
}
|
||||
}
|
||||
|
||||
export function activate(
|
||||
context: ExtensionContext
|
||||
) {
|
||||
const systemPrompts: LanguageModelChatMessage[] = [
|
||||
new LanguageModelChatMessage(LanguageModelChatMessageRole.User, SYSTEM_PROMPT),
|
||||
// new LanguageModelChatMessage(LanguageModelChatMessageRole.User, USER_PROMPT)
|
||||
];
|
||||
const copilotRequest = new CopilotRequest(systemPrompts);
|
||||
const springBootChatAgent = new SpringBootChatAgent(copilotRequest);
|
||||
|
||||
const agent = chat.createChatParticipant(PARTICIPANT_ID, async (request, context, progress, token) => {
|
||||
return springBootChatAgent.handlePrompts(request, context, progress, token);
|
||||
});
|
||||
agent.iconPath = Uri.joinPath(context.extensionUri, 'readme-imgs', 'spring-tools-icon.png');
|
||||
}
|
||||
121
vscode-extensions/vscode-spring-boot/lib/copilot/springCli.ts
Normal file
121
vscode-extensions/vscode-spring-boot/lib/copilot/springCli.ts
Normal file
@@ -0,0 +1,121 @@
|
||||
import { ProgressLocation, Uri, window, workspace } from "vscode";
|
||||
import cp from "child_process";
|
||||
import * as vscode from 'vscode';
|
||||
import { homedir } from "os";
|
||||
import { getWorkspaceRoot, getWorkspaceRootPath } from "./util";
|
||||
import path from "path";
|
||||
import { WorkspaceEdit } from "vscode-languageclient";
|
||||
|
||||
export const CANCELLED = "Cancelled";
|
||||
|
||||
export class SpringCli {
|
||||
|
||||
get executable(): string {
|
||||
return workspace.getConfiguration("spring-cli").get("executable") || "spring";
|
||||
}
|
||||
|
||||
guideLspEdit(uri: Uri, cwd?: string): Promise<WorkspaceEdit> {
|
||||
const args = [
|
||||
"guide",
|
||||
"apply",
|
||||
"--lsp-edit",
|
||||
"--file",
|
||||
uri.fsPath
|
||||
];
|
||||
return this.fetchJson("Applying guide", uri.fsPath, args, cwd || path.dirname(uri.fsPath), true);
|
||||
}
|
||||
|
||||
enhanceResponse(uri: Uri, cwd: string): Thenable<string> {
|
||||
const args = [
|
||||
"ai",
|
||||
"enhance-response",
|
||||
"--file",
|
||||
uri.fsPath
|
||||
];
|
||||
return this.exec("Spring cli ai", "Enhance response", args, cwd);
|
||||
}
|
||||
|
||||
private async executeCommand(args: string[], cwd?: string): Promise<string> {
|
||||
const processOpts = { cwd: cwd || (await 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);
|
||||
const dataChunks: string[] = [];
|
||||
process.stdout.on("data", s => dataChunks.push(s));
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
process.on("exit", (code) => {
|
||||
if (code) {
|
||||
reject(`Failed to execute command: ${dataChunks.join()}`);
|
||||
} else {
|
||||
resolve(dataChunks.join());
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private async exec<T>(title: string, message: string, args: string[], cwd?: string): Promise<T> {
|
||||
return vscode.window.withProgress({
|
||||
location: vscode.ProgressLocation.Window,
|
||||
cancellable: true,
|
||||
title,
|
||||
}, async (progress, cancellation) => {
|
||||
|
||||
if (message) {
|
||||
progress.report({ message });
|
||||
}
|
||||
|
||||
return new Promise<T>(async (resolve, reject) => {
|
||||
if (cancellation.isCancellationRequested) {
|
||||
reject("Cancelled");
|
||||
}
|
||||
try {
|
||||
const output: string = await this.executeCommand(args, cwd);
|
||||
resolve(output as T);
|
||||
} catch (error) {
|
||||
console.error(`Error: ${error}`);
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private async fetchJson<T>(title: string, message: string, args: string[], cwd?: string, omitJsonParam?: boolean): 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 || getWorkspaceRootPath()?.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(" ")} ${omitJsonParam ? "" : "--json"}`, processOpts);
|
||||
cancellation.onCancellationRequested(() => process.kill());
|
||||
const dataChunks: string[] = [];
|
||||
process.stdout.on("data", s => dataChunks.push(s));
|
||||
process.on("exit", (code) => {
|
||||
if (code) {
|
||||
if (cancellation.isCancellationRequested) {
|
||||
reject(CANCELLED);
|
||||
} else {
|
||||
reject(`Failed to fetch data: ${dataChunks.join()}`);
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
resolve(JSON.parse(dataChunks.join()) as T);
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import CopilotRequest from "./copilotRequest"
|
||||
|
||||
export const systemPrompt = `**Your task is to create Java source code for a Spring boot application. Follow these guidelines:**
|
||||
- IMPORTANT: CONCLUDE YOUR RESPONSE WITH THE MARKER \"//${CopilotRequest.DEFAULT_END_MARK}\" TO INDICATE END OF RESPONSE.
|
||||
- Generate a pom.xml snippet that includes the necessary Spring Boot Starter dependencies such as "spring-boot-starter-jpa".
|
||||
- Organize code into appropriate package. Use the Package name as the root package and place the files in sub-packages accordingly.
|
||||
- Include import statements in all code files.
|
||||
- Generate architectural layers (Controller, Service, Repository, Entity) code as appropriate.
|
||||
- If no entity or domain object is specified in the description, use a Person entity with the properties name and phone number.
|
||||
- Generate constructors, property getters, and setters for the entity or domain object (e.g., "getName" and "setName" for the name property).
|
||||
- Add any required annotations to the main application class (class with the @SpringBootApplication annotation).
|
||||
- When generating markdown for code blocks in the final response, include an appropriate value for the "info" field.
|
||||
- Provide application.properties file with sample configurations.
|
||||
- Include unit tests for each architectural layer (Controller, Service, Repository) as appropriate.
|
||||
- Include an integration test if there are multiple architectural layers.
|
||||
`
|
||||
|
||||
export const systemBoot3Prompt = `For Spring boot 3 and above:
|
||||
- IMPORTANT: For JPA related applications, the 'javax' package has been replaced with 'jakarta' package. All JPA-related imports should use 'jakarta.persistence' instead of 'javax.persistence'.
|
||||
\`\`\`
|
||||
"""
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.GeneratedValue;
|
||||
|
||||
@Entity
|
||||
public class Person {
|
||||
}
|
||||
\`\`\`
|
||||
- The coordinates of the MySQL JDBC driver have changed from "mysql:mysql-connector-java" to "com.mysql:mysql-connector-j". If you are using the MySQL JDBC driver, update its coordinates accordingly.
|
||||
Only when the generated code requires a MySQL JDBC driver, the MySQL JDBC driver should be "com.mysql:mysql-connector-j". This is a change from the older "mysql:mysql-connector-java".
|
||||
|
||||
Please ensure to use the "com.mysql:mysql-connector-j" MySQL JDBC driver in pom.xml. Here is the maven dependency to be used in this case:
|
||||
\`\`\`
|
||||
<!-- MySQL Connector -->
|
||||
<dependency>
|
||||
<groupId>com.mysql</groupId>
|
||||
<artifactId>mysql-connector-j</artifactId>
|
||||
</dependency>
|
||||
\`\`\`
|
||||
`
|
||||
|
||||
|
||||
export const systemBoot2Prompt = `
|
||||
- All JPA-related imports should use 'javax.persistence'
|
||||
- The MySQL JDBC driver coordinates are "mysql:mysql-connector-java"
|
||||
`
|
||||
@@ -0,0 +1,9 @@
|
||||
export const userPrompt = `Create the Spring Java application with the following project information
|
||||
|
||||
Ensure the solution meets the following additional criteria
|
||||
|
||||
1. Include import statements in all code files
|
||||
2. Generate getter and setters in entity classes
|
||||
3. Provide the necessary Spring Boot Starter dependencies in the pom.xml file
|
||||
4. When generating markdown for the 'application.properties' file in Spring Boot, please ensure that the markdown 'info' field is set to 'properties' for that specific section.
|
||||
`
|
||||
91
vscode-extensions/vscode-spring-boot/lib/copilot/util.ts
Normal file
91
vscode-extensions/vscode-spring-boot/lib/copilot/util.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
import path from "path";
|
||||
import { Uri, WorkspaceFolder, version, window, workspace } from "vscode";
|
||||
import fs from "fs";
|
||||
import { SemVer } from "semver";
|
||||
|
||||
export function getExecutable(): string {
|
||||
return workspace.getConfiguration("spring-cli").get("executable") || "spring";
|
||||
}
|
||||
|
||||
export async function getWorkspaceRoot(): Promise<Uri | undefined> {
|
||||
if (workspace.workspaceFolders && workspace.workspaceFolders.length) {
|
||||
if (workspace.workspaceFolders.length === 1) {
|
||||
return workspace.workspaceFolders[0].uri;
|
||||
} else {
|
||||
return await window.showQuickPick(
|
||||
workspace.workspaceFolders.map((c: WorkspaceFolder) => ({ value: c.uri, label: getRelativePathToWorkspaceFolder(c.uri), description: getWorkspaceFolderName(c.uri) })),
|
||||
{ placeHolder: "Select the target project." },
|
||||
).then(res => res && res.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function getWorkspaceRootPath(): Uri | undefined {
|
||||
if (workspace.workspaceFolders && workspace.workspaceFolders.length) {
|
||||
return workspace.workspaceFolders[0].uri
|
||||
}
|
||||
}
|
||||
|
||||
function getRelativePathToWorkspaceFolder(file: Uri): string {
|
||||
if (file) {
|
||||
const wf: WorkspaceFolder = workspace.getWorkspaceFolder(file);
|
||||
if (wf) {
|
||||
return path.relative(wf.uri.fsPath, file.fsPath);
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
function getWorkspaceFolderName(file: Uri): string {
|
||||
if (file) {
|
||||
const wf: WorkspaceFolder = workspace.getWorkspaceFolder(file);
|
||||
if (wf) {
|
||||
return wf.name;
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
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 async function writeResponseToFile(response: string, appName: string, selectedProject: string) {
|
||||
const readmeFilePath = path.resolve(selectedProject, `README-ai-${appName}.md`);
|
||||
if (fs.existsSync(readmeFilePath)) {
|
||||
try {
|
||||
fs.unlinkSync(readmeFilePath);
|
||||
} catch (ex) {
|
||||
throw new Error(`Could not delete readme file: ${readmeFilePath}, ${ex}`);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
fs.writeFileSync(readmeFilePath, response);
|
||||
return Uri.file(readmeFilePath);
|
||||
} catch (ex) {
|
||||
throw new Error(`Could not write readme file: ${readmeFilePath}, ${ex}`);
|
||||
}
|
||||
}
|
||||
|
||||
export function isLlmApiReady(): boolean {
|
||||
return version.includes('insider') && new SemVer(version).compare(new SemVer("1.90.0-insider")) >= 0;
|
||||
}
|
||||
@@ -30,7 +30,24 @@
|
||||
"activationEvents": [
|
||||
"onCommand:vscode-spring-boot.ls.start"
|
||||
],
|
||||
"enabledApiProposals": [
|
||||
"chatVariableResolver"
|
||||
],
|
||||
"contributes": {
|
||||
"chatParticipants": [
|
||||
{
|
||||
"id": "springboot.agent",
|
||||
"fullName": "Spring boot",
|
||||
"name": "springboot",
|
||||
"description": "Spring boot chat agent",
|
||||
"commands": [
|
||||
{
|
||||
"name": "add",
|
||||
"description": "Add a new spring module to the exisiting project"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"javaExtensions": [
|
||||
"./jars/io.projectreactor.reactor-core.jar",
|
||||
"./jars/org.reactivestreams.reactive-streams.jar",
|
||||
@@ -248,6 +265,12 @@
|
||||
"command": "vscode-spring-boot.query.explain",
|
||||
"title": "Explain Spel expressions, Spring Data queries, and AOP annotations (using Copilot)",
|
||||
"category": "Spring Boot"
|
||||
},
|
||||
{
|
||||
"command": "vscode-spring-boot.agent.apply",
|
||||
"title": "Apply Changes",
|
||||
"category": "Spring Boot Agent",
|
||||
"enablement": "false"
|
||||
}
|
||||
],
|
||||
"configuration": [
|
||||
@@ -433,6 +456,17 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "spring-cli",
|
||||
"title": "Spring CLI",
|
||||
"properties": {
|
||||
"spring-cli.executable": {
|
||||
"type": "string",
|
||||
"default": "spring",
|
||||
"description": "Spring CLI executable. Either name of the executable if it's on the PATH environment variable or absolute path to the spring CLI executable (or JAR for dev purposes)"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "ls",
|
||||
"title": "Language Server",
|
||||
@@ -1334,6 +1368,7 @@
|
||||
"@pivotal-tools/commons-vscode": "file:../commons-vscode/pivotal-tools-commons-vscode-0.2.4.tgz",
|
||||
"portfinder": "1.0.32",
|
||||
"ps-list": "^7.2.0",
|
||||
"semver": "^7.6.2",
|
||||
"vscode-languageclient": "^9.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 2.6 KiB |
Reference in New Issue
Block a user