diff --git a/headless-services/commons/commons-rewrite/src/main/java/org/springframework/ide/vscode/commons/rewrite/ORDocUtils.java b/headless-services/commons/commons-rewrite/src/main/java/org/springframework/ide/vscode/commons/rewrite/ORDocUtils.java index e046d23d3..e9deeda71 100644 --- a/headless-services/commons/commons-rewrite/src/main/java/org/springframework/ide/vscode/commons/rewrite/ORDocUtils.java +++ b/headless-services/commons/commons-rewrite/src/main/java/org/springframework/ide/vscode/commons/rewrite/ORDocUtils.java @@ -14,6 +14,8 @@ import java.util.ArrayList; import java.util.List; import java.util.Optional; +import org.eclipse.jgit.diff.Edit; +import org.eclipse.jgit.diff.EditList; import org.eclipse.lsp4j.AnnotatedTextEdit; import org.eclipse.lsp4j.CreateFile; import org.eclipse.lsp4j.DeleteFile; @@ -25,8 +27,6 @@ import org.eclipse.lsp4j.VersionedTextDocumentIdentifier; import org.eclipse.lsp4j.WorkspaceEdit; import org.eclipse.lsp4j.jsonrpc.messages.Either; import org.openrewrite.Result; -import org.eclipse.jgit.diff.Edit; -import org.eclipse.jgit.diff.EditList; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.ide.vscode.commons.languageserver.completion.DocumentEdits; @@ -72,10 +72,10 @@ public class ORDocUtils { } - public static Optional computeTextDocEdit(TextDocument doc, Result result, String changeAnnotationId) { - TextDocument newDoc = new TextDocument(null, LanguageId.PLAINTEXT, 0, result.getAfter().printAll()); + public static Optional computeTextDocEdit(TextDocument doc, String oldContent, String newContent, String changeAnnotationId) { + TextDocument newDoc = new TextDocument(null, LanguageId.PLAINTEXT, 0, newContent); - EditList diff = JGitUtils.getDiff(result.getBefore().printAll(), newDoc.get()); + EditList diff = JGitUtils.getDiff(oldContent, newDoc.get()); if (!diff.isEmpty()) { TextDocumentEdit edit = new TextDocumentEdit(); edit.setTextDocument(new VersionedTextDocumentIdentifier(doc.getUri(), doc.getVersion())); @@ -117,6 +117,7 @@ public class ORDocUtils { log.error("Diff conversion failed", ex); } } + System.out.println("edits "+ edit); return Optional.of(edit); } return Optional.empty(); @@ -139,7 +140,7 @@ public class ORDocUtils { edit.setEdits(List.of(te)); return Optional.of(edit); } - return Optional.empty(); + return Optional.empty(); } private static int getStartOfLine(IDocument doc, int lineNumber) { @@ -163,31 +164,56 @@ public class ORDocUtils { for (Result result : results) { if (result.getBefore() == null) { String docUri = result.getAfter().getSourcePath().toUri().toASCIIString(); - CreateFile ro = new CreateFile(); - ro.setUri(docUri); - we.getDocumentChanges().add(Either.forRight(ro)); - - TextDocumentEdit te = new TextDocumentEdit(); - te.setTextDocument(new VersionedTextDocumentIdentifier(docUri, 0)); - Position cursor = new Position(0,0); - te.setEdits(List.of(new AnnotatedTextEdit(new Range(cursor, cursor), result.getAfter().printAll(), changeAnnotationId))); - we.getDocumentChanges().add(Either.forLeft(te)); + createNewFileEdit(docUri, result.getAfter().printAll(), changeAnnotationId, we); } else if (result.getAfter() == null) { String docUri = result.getBefore().getSourcePath().toUri().toASCIIString(); - we.getDocumentChanges().add(Either.forRight(new DeleteFile(docUri))); + createDeleteFileEdit(docUri, we); } else { String docUri = result.getBefore().getSourcePath().toUri().toASCIIString(); - TextDocument doc = documents.getLatestSnapshot(docUri); - if (doc == null) { - doc = new TextDocument(docUri, null, 0, result.getBefore().printAll()); - ORDocUtils.computeTextDocEdit(doc, result, changeAnnotationId).ifPresent(te -> we.getDocumentChanges().add(Either.forLeft(te))); - } else { - ORDocUtils.computeTextDocEdit(doc, result, changeAnnotationId).ifPresent(te -> we.getDocumentChanges().add(Either.forLeft(te))); - } + System.out.println(result.getBefore().getSourcePath().toString()); + createUpdateFileEdit(documents, docUri, result.getBefore().printAll(), result.getAfter().printAll(), changeAnnotationId, we); } } return Optional.of(we); } + public static void createWorkspaceEdit(SimpleTextDocumentService documents, String docUri, String oldContent, String newContent, String changeAnnotationId, WorkspaceEdit we) { + if(oldContent == null) { + createNewFileEdit(docUri, newContent, changeAnnotationId, we); + } else if (newContent == null) { + createDeleteFileEdit(docUri, we); + } else { + createUpdateFileEdit(documents, docUri, oldContent, newContent, changeAnnotationId, we); + } + } + + private static void createNewFileEdit(String docUri, String newContent, String changeAnnotationId, + WorkspaceEdit we) { + CreateFile ro = new CreateFile(); + ro.setUri(docUri); + we.getDocumentChanges().add(Either.forRight(ro)); + + TextDocumentEdit te = new TextDocumentEdit(); + te.setTextDocument(new VersionedTextDocumentIdentifier(docUri, 0)); + Position cursor = new Position(0,0); + te.setEdits(List.of(new AnnotatedTextEdit(new Range(cursor, cursor), newContent, changeAnnotationId))); + we.getDocumentChanges().add(Either.forLeft(te)); + } + + private static void createDeleteFileEdit(String docUri, WorkspaceEdit we) { + we.getDocumentChanges().add(Either.forRight(new DeleteFile(docUri))); + } + + private static void createUpdateFileEdit(SimpleTextDocumentService documents, String docUri, String oldContent, + String newContent, String changeAnnotationId, WorkspaceEdit we) { + TextDocument doc = documents.getLatestSnapshot(docUri); + if (doc == null) { + doc = new TextDocument(docUri, null, 0, oldContent); + ORDocUtils.computeTextDocEdit(doc, oldContent, newContent, changeAnnotationId).ifPresent(te -> we.getDocumentChanges().add(Either.forLeft(te))); + } else { + ORDocUtils.computeTextDocEdit(doc, oldContent, newContent, changeAnnotationId).ifPresent(te -> we.getDocumentChanges().add(Either.forLeft(te))); + } + } + } diff --git a/headless-services/spring-boot-language-server/pom.xml b/headless-services/spring-boot-language-server/pom.xml index 944ad5730..7a9a9a48a 100644 --- a/headless-services/spring-boot-language-server/pom.xml +++ b/headless-services/spring-boot-language-server/pom.xml @@ -148,6 +148,10 @@ org.eclipse.lsp4j org.eclipse.lsp4j.jsonrpc + + xml-apis + xml-apis + @@ -201,6 +205,31 @@ cron-utils 9.2.0 + + org.commonmark + commonmark + 0.22.0 + + + org.apache.maven + maven-model + 3.8.1 + + + javax.xml.bind + jaxb-api + + + + + + com.fasterxml.jackson.dataformat + jackson-dataformat-xml + diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/BootLanguageServerBootApp.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/BootLanguageServerBootApp.java index cfdeb7d4a..a4727daf7 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/BootLanguageServerBootApp.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/BootLanguageServerBootApp.java @@ -49,6 +49,8 @@ import org.springframework.ide.vscode.boot.java.beans.DependsOnDefinitionProvide import org.springframework.ide.vscode.boot.java.beans.NamedDefinitionProvider; import org.springframework.ide.vscode.boot.java.beans.QualifierDefinitionProvider; import org.springframework.ide.vscode.boot.java.beans.ResourceDefinitionProvider; +import org.springframework.ide.vscode.boot.java.conditionalonresource.ConditionalOnResourceDefinitionProvider; +import org.springframework.ide.vscode.boot.java.copilot.ResponseModifier; import org.springframework.ide.vscode.boot.java.data.jpa.queries.DataQueryParameterDefinitionProvider; import org.springframework.ide.vscode.boot.java.data.jpa.queries.JdtDataQuerySemanticTokensProvider; import org.springframework.ide.vscode.boot.java.handlers.BootJavaCodeActionProvider; @@ -72,7 +74,6 @@ import org.springframework.ide.vscode.boot.java.reconcilers.JdtAstReconciler; import org.springframework.ide.vscode.boot.java.reconcilers.JdtReconciler; import org.springframework.ide.vscode.boot.java.utils.CompilationUnitCache; import org.springframework.ide.vscode.boot.java.value.ValueDefinitionProvider; -import org.springframework.ide.vscode.boot.java.conditionalonresource.ConditionalOnResourceDefinitionProvider; import org.springframework.ide.vscode.boot.jdt.ls.JavaProjectsService; import org.springframework.ide.vscode.boot.jdt.ls.JdtLsProjectCache; import org.springframework.ide.vscode.boot.metadata.AdHocSpringPropertyIndexProvider; @@ -417,4 +418,8 @@ public class BootLanguageServerBootApp { return new ModulithService(server, projectFinder, projectObserver, springIndex, reconciler, config); } + @Bean ResponseModifier responseModifier() { + return new ResponseModifier(); + } + } diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/BootJavaLanguageServerComponents.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/BootJavaLanguageServerComponents.java index f3717fdaf..9ba81ff65 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/BootJavaLanguageServerComponents.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/BootJavaLanguageServerComponents.java @@ -31,6 +31,8 @@ import org.springframework.ide.vscode.boot.java.beans.NamedReferencesProvider; import org.springframework.ide.vscode.boot.java.beans.ProfileReferencesProvider; import org.springframework.ide.vscode.boot.java.beans.QualifierReferencesProvider; import org.springframework.ide.vscode.boot.java.conditionals.ConditionalsLiveHoverProvider; +import org.springframework.ide.vscode.boot.java.copilot.CopilotAgentCommandHandler; +import org.springframework.ide.vscode.boot.java.copilot.ResponseModifier; import org.springframework.ide.vscode.boot.java.handlers.BootJavaCodeActionProvider; import org.springframework.ide.vscode.boot.java.handlers.BootJavaCodeLensEngine; import org.springframework.ide.vscode.boot.java.handlers.BootJavaDocumentHighlightEngine; @@ -110,6 +112,7 @@ public class BootJavaLanguageServerComponents implements LanguageServerComponent private final SpringLiveChangeDetectionWatchdog liveChangeDetectionWatchdog; private final ProjectObserver projectObserver; private final CompilationUnitCache cuCache; + private final ResponseModifier responseModifier; private JavaProjectFinder projectFinder; private BootJavaHoverProvider hoverProvider; @@ -121,10 +124,11 @@ public class BootJavaLanguageServerComponents implements LanguageServerComponent private JdtSemanticTokensHandler semanticTokensHandler; private JdtInlayHintsHandler inlayHintsHandler; private SpelSemanticTokens spelSemanticTokens; - + public BootJavaLanguageServerComponents(ApplicationContext appContext) { this.server = appContext.getBean(SimpleLanguageServer.class); this.serverParams = appContext.getBean(BootLanguageServerParams.class); + this.responseModifier = appContext.getBean(ResponseModifier.class); projectFinder = serverParams.projectFinder; projectObserver = serverParams.projectObserver; @@ -166,6 +170,8 @@ public class BootJavaLanguageServerComponents implements LanguageServerComponent // create and handle commands new SpringProcessCommandHandler(server, liveDataService, liveDataLocalProcessConnector, appContext.getBeansOfType(SpringProcessConnectorRemote.class).values()); + new CopilotAgentCommandHandler(server, projectFinder,responseModifier); + docSymbolProvider = params -> springSymbolIndex.getSymbols(params.getTextDocument().getUri()); workspaceService.onWorkspaceSymbol(new BootJavaWorkspaceSymbolHandler(springSymbolIndex, diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/copilot/AbstractInjectMavenActionHandler.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/copilot/AbstractInjectMavenActionHandler.java new file mode 100644 index 000000000..094aad0f4 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/copilot/AbstractInjectMavenActionHandler.java @@ -0,0 +1,95 @@ +package org.springframework.ide.vscode.boot.java.copilot; + +import java.io.BufferedWriter; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; + +import org.jetbrains.annotations.NotNull; +import org.openrewrite.ExecutionContext; +import org.openrewrite.InMemoryExecutionContext; +import org.openrewrite.Recipe; +import org.openrewrite.RecipeRun; +import org.openrewrite.Result; +import org.openrewrite.SourceFile; +import org.openrewrite.internal.InMemoryLargeSourceSet; +import org.openrewrite.maven.MavenParser; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.util.StringUtils; + +public abstract class AbstractInjectMavenActionHandler { + + private static final Logger logger = LoggerFactory.getLogger(AbstractInjectMavenActionHandler.class); + + protected final TemplateEngine templateEngine; + + protected final Map model; + + protected final Path cwd; + + public AbstractInjectMavenActionHandler(TemplateEngine templateEngine, Map model, Path cwd) { + this.templateEngine = templateEngine; + this.model = model; + this.cwd = cwd; + } + + protected static ExecutionContext getExecutionContext() { + Consumer onError = e -> { + logger.error("error in javaParser execution", e); + }; + return new InMemoryExecutionContext(onError); + } + + protected String getTextToUse(String text, String actionName) { + if (!StringUtils.hasText(text)) { + throw new SpringCliException(actionName + " action does not have a value in the 'text:' field."); + } + if (this.templateEngine != null) { + text = this.templateEngine.process(text, model); + } + return text; + } + + @NotNull + protected Path getPomPath() { + Path pomPath = cwd.resolve("pom.xml"); + if (Files.notExists(pomPath)) { + throw new SpringCliException("Could not find pom.xml in " + this.cwd + + ". Make sure you are running the command in the directory that contains a pom.xml file"); + } + return pomPath; + } + + public void exec() { + Path pomPath = getPomPath(); + List resultList = run().getChangeset().getAllResults(); + try { + for (Result result : resultList) { + // write updated file. + try (BufferedWriter sourceFileWriter = Files.newBufferedWriter(pomPath, StandardCharsets.UTF_8)) { + sourceFileWriter.write(result.getAfter().printAllTrimmed()); + } + } + } + catch (IOException ex) { + throw new SpringCliException("Error writing to " + pomPath.toAbsolutePath(), ex); + } + } + + public RecipeRun run() { + List paths = new ArrayList<>(); + paths.add(getPomPath()); + MavenParser mavenParser = MavenParser.builder().build(); + List parsedPomFiles = mavenParser.parse(paths, cwd, getExecutionContext()).toList(); + return createRecipe().run(new InMemoryLargeSourceSet(parsedPomFiles), getExecutionContext()); + } + + protected abstract Recipe createRecipe(); + +} diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/copilot/CopilotAgentCommandHandler.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/copilot/CopilotAgentCommandHandler.java new file mode 100644 index 000000000..5e9ba1c09 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/copilot/CopilotAgentCommandHandler.java @@ -0,0 +1,87 @@ +package org.springframework.ide.vscode.boot.java.copilot; + +import java.io.IOException; +import java.nio.file.Paths; +import java.util.List; +import java.util.concurrent.CompletableFuture; + +import org.eclipse.lsp4j.ExecuteCommandParams; +import org.eclipse.lsp4j.TextDocumentIdentifier; +import org.eclipse.lsp4j.WorkspaceEdit; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.ide.vscode.commons.java.IJavaProject; +import org.springframework.ide.vscode.commons.languageserver.java.JavaProjectFinder; +import org.springframework.ide.vscode.commons.languageserver.util.SimpleLanguageServer; +import org.springframework.ide.vscode.commons.util.text.TextDocument; + +import com.google.gson.JsonElement; +import com.google.j2objc.annotations.Weak; + +public class CopilotAgentCommandHandler { + + private static final Logger log = LoggerFactory.getLogger(CopilotAgentCommandHandler.class); + + private static final String CMD_COPILOT_AGENT_ENHANCERESPONSE = "sts/copilot/agent/enhanceResponse"; + + private static final String CMD_COPILOT_AGENT_LSPEDITS = "sts/copilot/agent/lspEdits"; + + private final SimpleLanguageServer server; + private final JavaProjectFinder projectFinder; + private final ResponseModifier responseModifier; + + + public CopilotAgentCommandHandler(SimpleLanguageServer server, JavaProjectFinder projectFinder, ResponseModifier responseModifier) { + this.server = server; + this.projectFinder = projectFinder; + this.responseModifier = responseModifier; + registerCommands(); + } + + private void registerCommands() { + server.onCommand(CMD_COPILOT_AGENT_ENHANCERESPONSE, (params) -> { + return enhanceResponseHandler(params); + }); + log.info("Registered command handler: {}",CMD_COPILOT_AGENT_ENHANCERESPONSE); + + server.onCommand(CMD_COPILOT_AGENT_LSPEDITS, params -> { + try { + return createLspEdits(params); + } catch (IOException e) { + log.error(e.getMessage()); + } + return null; + }); + } + + private CompletableFuture enhanceResponseHandler(ExecuteCommandParams params) { + log.info("Command Handler: "); + String response = ((JsonElement) params.getArguments().get(0)).getAsString(); + String modifiedResp = responseModifier.modify(response); + return CompletableFuture.completedFuture(modifiedResp); + } + + private CompletableFuture createLspEdits(ExecuteCommandParams params) throws IOException { + log.info("Command Handler for lsp edits: "); + String docURI = ((JsonElement) params.getArguments().get(0)).getAsString(); + String path = ((JsonElement) params.getArguments().get(0)).getAsString(); + String content = ((JsonElement) params.getArguments().get(2)).getAsString(); + TextDocument doc = server.getTextDocumentService().getLatestSnapshot("file:///Users/vudayani/Desktop/spring-petclinic/README-ai-spring-petclinic.md"); + + IJavaProject project = this.projectFinder.find(new TextDocumentIdentifier(docURI)).get(); + System.out.println("Project path "+project.getLocationUri() + " "+project.getLocationUri().getPath().toString()); + List projectArtifacts = computeProjectArtifacts(content); + ProjectArtifactEditGenerator editGenerator = new ProjectArtifactEditGenerator(server.getTextDocumentService(), projectArtifacts, + Paths.get(project.getLocationUri()), docURI); +// Paths.get(project.getLocationUri()), docURI); + WorkspaceEdit we = editGenerator.process().getResult(); + System.out.println("Final: \n "+ we.toString()); + return CompletableFuture.completedFuture(we); + } + + List computeProjectArtifacts(String response) { + ProjectArtifactCreator projectArtifactCreator = new ProjectArtifactCreator(); + List projectArtifacts = projectArtifactCreator.create(response); + return projectArtifacts; + } +} \ No newline at end of file diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/copilot/InjectMavenActionHandler.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/copilot/InjectMavenActionHandler.java new file mode 100644 index 000000000..fdb7fcc6a --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/copilot/InjectMavenActionHandler.java @@ -0,0 +1,93 @@ +package org.springframework.ide.vscode.boot.java.copilot; + +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import org.openrewrite.Recipe; +import org.openrewrite.config.DeclarativeRecipe; +import org.openrewrite.maven.MavenParser; +import org.springframework.ide.vscode.boot.java.copilot.recipe.AddDependencyRecipeFactory; +import org.springframework.ide.vscode.boot.java.copilot.recipe.AddManagedDependencyRecipeFactory; +import org.springframework.ide.vscode.boot.java.copilot.recipe.AddPluginRecipeFactory; +import org.springframework.ide.vscode.boot.java.copilot.recipe.InjectTextMavenRepositoryRecipe; +import org.springframework.ide.vscode.boot.java.copilot.util.MavenBuildPluginReader; +import org.springframework.ide.vscode.boot.java.copilot.util.MavenDependencyReader; +import org.springframework.ide.vscode.boot.java.copilot.util.MavenRepositoryReader; + +public class InjectMavenActionHandler extends AbstractInjectMavenActionHandler { + + private List dependencies; + + private List buildPlugins; + + private List repositories; + + private List dependencyManagements; + + public InjectMavenActionHandler(TemplateEngine templateEngine, Map model, Path cwd) { + super(templateEngine, model, cwd); + this.dependencies = new ArrayList<>(); + this.buildPlugins = new ArrayList<>(); + this.repositories = new ArrayList<>(); + this.dependencyManagements = new ArrayList<>(); + } + + public boolean injectBuildPlugin(InjectMavenBuildPlugin buildPlugin) { + return buildPlugins.add(buildPlugin); + } + + public boolean injectDependency(InjectMavenDependency dependency) { + return dependencies.add(dependency); + } + + public boolean injectRepository(InjectMavenRepository repository) { + return repositories.add(repository); + } + + public boolean injectDependencyManagement(InjectMavenDependencyManagement dependencyManagement) { + return dependencyManagements.add(dependencyManagement); + } + + protected Recipe createRecipe() { + DeclarativeRecipe aggregateRecipe = new DeclarativeRecipe("spring.cli.ai.MavenUpdates", + "Add Pom changes from AI", "", Collections.emptySet(), null, null, false, Collections.emptyList()); + MavenParser mavenParser = MavenParser.builder().build(); + for (InjectMavenDependency d : dependencies) { + String text = getTextToUse(d.getText(), "Inject Maven Dependency"); + MavenDependencyReader mavenDependencyReader = new MavenDependencyReader(); + String[] mavenDependencies = mavenDependencyReader.parseMavenSection(text); + for (String md : mavenDependencies) { + aggregateRecipe.getRecipeList().add(new AddDependencyRecipeFactory().create(md)); + } + } + for (InjectMavenBuildPlugin p : buildPlugins) { + String text = getTextToUse(p.getText(), "Inject Maven Build Plugin"); + MavenBuildPluginReader mavenBuildPluginReader = new MavenBuildPluginReader(); + String[] buildPlugins = mavenBuildPluginReader.parseMavenSection(text); + for (String mp : buildPlugins) { + aggregateRecipe.getRecipeList().add(new AddPluginRecipeFactory().create(mp)); + } + } + for (InjectMavenRepository r : repositories) { + String text = getTextToUse(r.getText(), "Inject Maven Repository"); + MavenRepositoryReader mavenRepositoryReader = new MavenRepositoryReader(); + String[] mavenRepositories = mavenRepositoryReader.parseMavenSection(text); + for (String mr : mavenRepositories) { + aggregateRecipe.getRecipeList().add(new InjectTextMavenRepositoryRecipe(mr)); + } + } + for (InjectMavenDependencyManagement dm : dependencyManagements) { + String text = getTextToUse(dm.getText(), "Inject Maven Dependency Management"); + MavenDependencyReader mavenDependencyReader = new MavenDependencyReader(); + String[] mavenDependencyManagements = mavenDependencyReader.parseMavenSection(text); + for (String mdm : mavenDependencyManagements) { + aggregateRecipe.getRecipeList().add(new AddManagedDependencyRecipeFactory().create(mdm)); + } + } + return aggregateRecipe; + } + +} diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/copilot/InjectMavenBuildPlugin.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/copilot/InjectMavenBuildPlugin.java new file mode 100644 index 000000000..433a7c814 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/copilot/InjectMavenBuildPlugin.java @@ -0,0 +1,29 @@ +package org.springframework.ide.vscode.boot.java.copilot; + +import java.util.Objects; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * @author Mark Pollack + */ +public class InjectMavenBuildPlugin { + + private String text; + + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + public InjectMavenBuildPlugin(@JsonProperty("text") String text) { + this.text = Objects.requireNonNull(text); + } + + public String getText() { + return text; + } + + @Override + public String toString() { + return "InjectMavenBuildPlugin{" + "text='" + text + '\'' + '}'; + } + +} \ No newline at end of file diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/copilot/InjectMavenDependency.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/copilot/InjectMavenDependency.java new file mode 100644 index 000000000..f81df5eb9 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/copilot/InjectMavenDependency.java @@ -0,0 +1,29 @@ +package org.springframework.ide.vscode.boot.java.copilot; + +import java.util.Objects; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * @author Mark Pollack + */ +public class InjectMavenDependency { + + private String text; + + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + public InjectMavenDependency(@JsonProperty("text") String text) { + this.text = Objects.requireNonNull(text); + } + + public String getText() { + return text; + } + + @Override + public String toString() { + return "InjectMavenDependency{" + "text='" + text + '\'' + '}'; + } + +} \ No newline at end of file diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/copilot/InjectMavenDependencyManagement.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/copilot/InjectMavenDependencyManagement.java new file mode 100644 index 000000000..a83cffcbd --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/copilot/InjectMavenDependencyManagement.java @@ -0,0 +1,29 @@ +package org.springframework.ide.vscode.boot.java.copilot; + +import java.util.Objects; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * @author Mark Pollack + */ +public class InjectMavenDependencyManagement { + + private String text; + + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + public InjectMavenDependencyManagement(@JsonProperty("text") String text) { + this.text = Objects.requireNonNull(text); + } + + public String getText() { + return text; + } + + @Override + public String toString() { + return "InjectMavenDependency{" + "text='" + text + '\'' + '}'; + } + +} diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/copilot/InjectMavenRepository.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/copilot/InjectMavenRepository.java new file mode 100644 index 000000000..7fc32d482 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/copilot/InjectMavenRepository.java @@ -0,0 +1,29 @@ +package org.springframework.ide.vscode.boot.java.copilot; + +import java.util.Objects; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * @author Mark Pollack + */ +public class InjectMavenRepository { + + private String text; + + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + public InjectMavenRepository(@JsonProperty("text") String text) { + this.text = Objects.requireNonNull(text); + } + + public String getText() { + return text; + } + + @Override + public String toString() { + return "InjectMavenRepository{" + "text='" + text + '\'' + '}'; + } + +} diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/copilot/MarkdownResponseVisitor.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/copilot/MarkdownResponseVisitor.java new file mode 100644 index 000000000..de3fa246f --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/copilot/MarkdownResponseVisitor.java @@ -0,0 +1,79 @@ +package org.springframework.ide.vscode.boot.java.copilot; + +import java.util.ArrayList; +import java.util.List; + +import org.commonmark.node.AbstractVisitor; +import org.commonmark.node.FencedCodeBlock; + +public class MarkdownResponseVisitor extends AbstractVisitor { + + private List projectArtifacts = new ArrayList<>(); + + @Override + public void visit(FencedCodeBlock fencedCodeBlock) { + String info = fencedCodeBlock.getInfo(); + String code = fencedCodeBlock.getLiteral(); + if (info.equalsIgnoreCase("java")) { + addJavaCode(code); + } + if (info.equalsIgnoreCase("xml")) { + addMavenDependencies(code); + } + if (info.equalsIgnoreCase("properties")) { + addApplicationProperties(code); + } + if (info.equalsIgnoreCase("html")) { + addHtml(code); + } + if (info.isBlank()) { + // sometimes the response doesn't contain + if (code.contains("package")) { + addJavaCode(code); + } + else if (code.contains("")) { + addMavenDependencies(code); + } + } + + } + + private void addHtml(String code) { + addIfNotPresent(new ProjectArtifact(ProjectArtifactType.HTML, code)); + } + + private void addApplicationProperties(String code) { + addIfNotPresent(new ProjectArtifact(ProjectArtifactType.APPLICATION_PROPERTIES, code)); + } + + private void addMavenDependencies(String code) { + addIfNotPresent(new ProjectArtifact(ProjectArtifactType.MAVEN_DEPENDENCIES, code)); + } + + private void addJavaCode(String code) { + if (code.contains("@SpringBootApplication") && code.contains("SpringApplication.run")) { + addIfNotPresent(new ProjectArtifact(ProjectArtifactType.MAIN_CLASS, code)); + } + else if (code.contains("@Test")) { + addIfNotPresent(new ProjectArtifact(ProjectArtifactType.TEST_CODE, code)); + } + else { + addIfNotPresent(new ProjectArtifact(ProjectArtifactType.SOURCE_CODE, code)); + } + } + + private void addIfNotPresent(ProjectArtifact projectArtifact) { + // TODO - investigate why Node has duplicate entries + if (!this.projectArtifacts.contains(projectArtifact)) { + this.projectArtifacts.add(projectArtifact); + } + else { + // System.out.println("duplicate project artifact :("); + } + } + + public List getProjectArtifacts() { + return this.projectArtifacts; + } + +} diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/copilot/ProcessArtifactResult.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/copilot/ProcessArtifactResult.java new file mode 100644 index 000000000..c6aa761fa --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/copilot/ProcessArtifactResult.java @@ -0,0 +1,29 @@ +package org.springframework.ide.vscode.boot.java.copilot; + +import java.util.ArrayList; +import java.util.List; + +public class ProcessArtifactResult { + + List notProcessedArtifacts = new ArrayList<>(); + + private T result; + + public List getNotProcessedArtifacts() { + return notProcessedArtifacts; + } + + public void addToNotProcessed(ProjectArtifact projectArtifact) { + notProcessedArtifacts.add(projectArtifact); + } + + public T getResult() { + return result; + } + + void setResult(T result) { + this.result = result; + } + +} + diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/copilot/ProjectArtifact.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/copilot/ProjectArtifact.java new file mode 100644 index 000000000..8c0df74ce --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/copilot/ProjectArtifact.java @@ -0,0 +1,42 @@ +package org.springframework.ide.vscode.boot.java.copilot; + +import java.util.Objects; + +public class ProjectArtifact { + + private ProjectArtifactType artifactType; + + private String text; + + public ProjectArtifact(ProjectArtifactType artifactType, String text) { + this.artifactType = artifactType; + this.text = text; + } + + public ProjectArtifactType getArtifactType() { + return artifactType; + } + + public String getText() { + return text; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ProjectArtifact that = (ProjectArtifact) o; + return artifactType == that.artifactType && Objects.equals(text, that.text); + } + + @Override + public int hashCode() { + return Objects.hash(artifactType, text); + } + +} + diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/copilot/ProjectArtifactCreator.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/copilot/ProjectArtifactCreator.java new file mode 100644 index 000000000..0f0dbbdb3 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/copilot/ProjectArtifactCreator.java @@ -0,0 +1,20 @@ +package org.springframework.ide.vscode.boot.java.copilot; + +import java.util.List; + +import org.commonmark.node.Node; +import org.commonmark.parser.Parser; + +public class ProjectArtifactCreator { + + public List create(String response) { + Parser parser = Parser.builder().build(); + Node node = parser.parse(response); + + MarkdownResponseVisitor markdownResponseVisitor = new MarkdownResponseVisitor(); + node.accept(markdownResponseVisitor); + + return markdownResponseVisitor.getProjectArtifacts(); + } + +} \ No newline at end of file diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/copilot/ProjectArtifactEditGenerator.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/copilot/ProjectArtifactEditGenerator.java new file mode 100644 index 000000000..4626d57b1 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/copilot/ProjectArtifactEditGenerator.java @@ -0,0 +1,305 @@ +package org.springframework.ide.vscode.boot.java.copilot; + +import java.io.BufferedReader; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.StringReader; +import java.io.StringWriter; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Properties; +import java.util.UUID; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.apache.commons.io.IOUtils; +import org.apache.maven.model.Dependency; +import org.apache.maven.model.Model; +import org.eclipse.lsp4j.ChangeAnnotation; +import org.eclipse.lsp4j.WorkspaceEdit; +import org.openrewrite.Result; +import org.springframework.ide.vscode.boot.java.copilot.util.ClassNameExtractor; +import org.springframework.ide.vscode.boot.java.copilot.util.MavenDependencyReader; +import org.springframework.ide.vscode.boot.java.copilot.util.PomReader; +import org.springframework.ide.vscode.commons.languageserver.util.SimpleTextDocumentService; +import org.springframework.ide.vscode.commons.rewrite.ORDocUtils; +import org.springframework.ide.vscode.commons.util.text.TextDocument; + +public class ProjectArtifactEditGenerator { + + private final List projectArtifacts; + + private final Path projectPath; + + private final String readmeFileName; + + private final Pattern compiledGroupIdPattern; + + private final Pattern compiledArtifactIdPattern; + + private final SimpleTextDocumentService simpleTextDocumentService; + + public ProjectArtifactEditGenerator(SimpleTextDocumentService simpleTextDocumentService, List projectArtifacts, Path projectPath, String readmeFileName) { + this.simpleTextDocumentService = simpleTextDocumentService; + this.projectArtifacts = projectArtifacts; + this.projectPath = projectPath; + this.readmeFileName = readmeFileName; + compiledGroupIdPattern = Pattern.compile("(.*?)"); + compiledArtifactIdPattern = Pattern.compile("(.*?)"); + } + + public ProcessArtifactResult process() throws IOException { + return processArtifacts(projectArtifacts, projectPath); + } + + private ProcessArtifactResult processArtifacts(List projectArtifacts, + Path projectPath) throws IOException { + ProcessArtifactResult processArtifactResult = new ProcessArtifactResult<>(); + String changeAnnotationId = UUID.randomUUID().toString(); + WorkspaceEdit we = new WorkspaceEdit(); + ChangeAnnotation changeAnnotation = new ChangeAnnotation(); + changeAnnotation.setNeedsConfirmation(true); + we.setDocumentChanges(new ArrayList<>()); + we.setChangeAnnotations(Map.of(changeAnnotationId, changeAnnotation)); + for (ProjectArtifact projectArtifact : projectArtifacts) { + // try { + ProjectArtifactType artifactType = projectArtifact.getArtifactType(); + switch (artifactType) { + case SOURCE_CODE: + writeSourceCode(projectArtifact, projectPath, changeAnnotationId, we); + break; + case TEST_CODE: + writeTestCode(projectArtifact, projectPath, changeAnnotationId, we); + break; + case MAVEN_DEPENDENCIES: + writeMavenDependencies(projectArtifact, projectPath,changeAnnotationId, we); + break; + case APPLICATION_PROPERTIES: + writeApplicationProperties(projectArtifact, projectPath, changeAnnotationId, we); + break; + case MAIN_CLASS: + updateMainApplicationClassAnnotations(projectArtifact, projectPath, + changeAnnotationId, we); + break; + case HTML: + writeHtml(projectArtifact, projectPath, changeAnnotationId, we); + break; + default: + processArtifactResult.addToNotProcessed(projectArtifact); + break; + } + } + processArtifactResult.setResult(we); + return processArtifactResult; + } + + private void writeSourceCode(ProjectArtifact projectArtifact, Path projectPath, + String changeAnnotationId, WorkspaceEdit we) throws IOException { + String packageName = this.calculatePackageForArtifact(projectArtifact); + ClassNameExtractor classNameExtractor = new ClassNameExtractor(); + Optional className = classNameExtractor.extractClassName(projectArtifact.getText()); + if (className.isPresent()) { + Path output = resolveSourceFile(projectPath, packageName, className.get() + ".java"); + ORDocUtils.createWorkspaceEdit(simpleTextDocumentService, output.toUri().toASCIIString(), getFileContent(output), projectArtifact.getText(), changeAnnotationId, we); + } + } + + private String getFileContent(Path file) throws IOException { + TextDocument doc = simpleTextDocumentService.getLatestSnapshot(file.toUri().toASCIIString()); + if (doc != null) { + return doc.get(); + } + return null; + } + + private void writeTestCode(ProjectArtifact projectArtifact, Path projectPath, + String changeAnnotationId, WorkspaceEdit we) throws IOException { + // TODO parameterize better to reduce code duplication + String packageName = this.calculatePackageForArtifact(projectArtifact); + ClassNameExtractor classNameExtractor = new ClassNameExtractor(); + Optional className = classNameExtractor.extractClassName(projectArtifact.getText()); + if (className.isPresent()) { + Path output = resolveTestFile(projectPath, packageName, className.get() + ".java"); + ORDocUtils.createWorkspaceEdit(simpleTextDocumentService, output.toUri().toASCIIString(), getFileContent(output), projectArtifact.getText(), changeAnnotationId, we); + } + } + + private void writeMavenDependencies(ProjectArtifact projectArtifact, Path projectPath, String changeAnnotationId, WorkspaceEdit we) { + PomReader pomReader = new PomReader(); + Path currentProjectPomPath = this.projectPath.resolve("pom.xml"); + if (Files.notExists(currentProjectPomPath)) { + throw new SpringCliException("Could not find pom.xml in " + this.projectPath + + ". Make sure you are running the command in the project's root directory."); + } + Model currentModel = pomReader.readPom(currentProjectPomPath.toFile()); + List currentDependencies = currentModel.getDependencies(); + + MavenDependencyReader mavenDependencyReader = new MavenDependencyReader(); + // projectArtifact.getText() contains a list of elements + String[] mavenDependencies = mavenDependencyReader.parseMavenSection(projectArtifact.getText()); + + InjectMavenActionHandler injectMavenActionHandler = new InjectMavenActionHandler(null, new HashMap<>(), + projectPath); + + for (String candidateDependencyText : mavenDependencies) { + if (!candidateDependencyAlreadyPresent(getProjectDependency(candidateDependencyText), + currentDependencies)) { + injectMavenActionHandler.injectDependency(new InjectMavenDependency(candidateDependencyText)); + } + } + + List res = injectMavenActionHandler.run().getChangeset().getAllResults(); + if(!res.isEmpty()) { + WorkspaceEdit workspaceEdit = ORDocUtils.createWorkspaceEdit(simpleTextDocumentService, res, changeAnnotationId).get(); + we.getDocumentChanges().addAll(workspaceEdit.getDocumentChanges()); + } +// return res.isEmpty() ? Collections.emptyList() : +// ORDocUtils.createWorkspaceEdit(simpleTextDocumentService, res, changeAnnotationId).get(); +// convertToEdits(res, changeAnnotationId); + } + +// private List convertToEdits(List allResults, String changeAnnotationId) { +// List edits = new ArrayList<>(); +// for (Result res : allResults) { +// Path p = (res.getBefore() == null) ? res.getAfter().getSourcePath() : res.getBefore().getSourcePath(); +// String uri = p.toUri().toASCIIString(); +// ConversionUtils +// .computeTextDocEdit(uri, res.getBefore().printAll(), res.getAfter().printAll(), changeAnnotationId) +// .ifPresent(edits::add); +// } +// return edits; +// } + + private boolean candidateDependencyAlreadyPresent(ProjectDependency toMergeDependency, + List currentDependencies) { + String candidateGroupId = toMergeDependency.getGroupId(); + String candidateArtifactId = toMergeDependency.getArtifactId(); + boolean candidateDependencyAlreadyPresent = false; + for (Dependency currentDependency : currentDependencies) { + String currentGroupId = currentDependency.getGroupId(); + String currentArtifactId = currentDependency.getArtifactId(); + if (candidateGroupId.equals(currentGroupId) && candidateArtifactId.equals(currentArtifactId)) { + candidateDependencyAlreadyPresent = true; + break; + } + } + return candidateDependencyAlreadyPresent; + + } + + private ProjectDependency getProjectDependency(String xml) { + String groupId = null; + String artifactId = null; + try { + groupId = extractValue(xml, this.compiledGroupIdPattern); + artifactId = extractValue(xml, this.compiledArtifactIdPattern); + + } + catch (Exception ex) { + throw new SpringCliException("Exception processing dependency: " + xml, ex); + } + if (groupId == null || artifactId == null) { + throw new SpringCliException("Could not process dependency: " + xml); + } + return new ProjectDependency(groupId, artifactId); + } + + private static String extractValue(String xml, Pattern compiledPattern) { + Matcher matcher = compiledPattern.matcher(xml); + if (matcher.find()) { + return matcher.group(1); + } + return null; + } + + private void writeApplicationProperties(ProjectArtifact projectArtifact, Path projectPath, + String changeAnnotationId, WorkspaceEdit we) throws IOException { + Path applicationPropertiesPath = projectPath.resolve("src") + .resolve("main") + .resolve("resources") + .resolve("application.properties"); + + Properties srcProperties = new Properties(); + Properties destProperties = new Properties(); + srcProperties.load(IOUtils.toInputStream(projectArtifact.getText(), StandardCharsets.UTF_8)); + if (Files.exists(applicationPropertiesPath)) { + destProperties.load(new FileInputStream(applicationPropertiesPath.toFile())); + } + Properties mergedProperties = PropertyFileUtils.mergeProperties(srcProperties, destProperties); + + StringWriter sw = new StringWriter(); + mergedProperties.store(sw, "updated by spring ai add"); + sw.flush(); + String newContent = sw.getBuffer().toString(); + ORDocUtils.createWorkspaceEdit(simpleTextDocumentService, applicationPropertiesPath.toUri().toASCIIString(), getFileContent(applicationPropertiesPath), newContent, changeAnnotationId, we); + } + + private void updateMainApplicationClassAnnotations(ProjectArtifact projectArtifact, + Path projectPath, String changeAnnotationId, WorkspaceEdit we) { + // TODO mer +// return Collections.emptyList(); + } + + private void writeHtml(ProjectArtifact projectArtifact, Path projectPath, + String changeAnnotationId, WorkspaceEdit we) throws IOException { + String html = projectArtifact.getText(); + String fileName = extractFilenameFromComment(html); + if (fileName != null) { + Path htmlFile = projectPath.resolve(fileName); + ORDocUtils.createWorkspaceEdit(simpleTextDocumentService, fileName, getFileContent(htmlFile), projectArtifact.getText(), changeAnnotationId, we); + } + } + + private String calculatePackageForArtifact(ProjectArtifact projectArtifact) { + String packageToUse = "com.example.ai"; + try (BufferedReader reader = new BufferedReader(new StringReader(projectArtifact.getText()))) { + String firstLine = reader.readLine(); + if (firstLine.contains("package")) { + String regex = "^package\\s+([a-zA-Z_][a-zA-Z0-9_]*(\\.[a-zA-Z_][a-zA-Z0-9_]*)*);"; + Pattern pattern = Pattern.compile(regex, Pattern.MULTILINE); + Matcher matcher = pattern.matcher(firstLine); + // Find the package statement and extract the package name + String packageName = ""; + if (matcher.find()) { + packageToUse = matcher.group(1); + } + } + } + catch (IOException ex) { + throw new SpringCliException( + "Could not parse package name from Project Artifact: " + projectArtifact.getText(), ex); + } + return packageToUse; + } + + private static String extractFilenameFromComment(String content) { + String commentPattern = ""; + Pattern pattern = Pattern.compile(commentPattern); + Matcher matcher = pattern.matcher(content); + if (matcher.find()) { + return matcher.group(1); + } + return null; + } + + public Path resolveSourceFile(Path projectPath, String packageName, String fileName) { + Path sourceDirectory = projectPath.resolve("src").resolve("main").resolve("java"); + return resolvePackage(sourceDirectory, packageName).resolve(fileName); + } + + public Path resolveTestFile(Path projectPath, String packageName, String fileName) { + Path sourceDirectory = projectPath.resolve("src").resolve("test").resolve("java"); + return resolvePackage(sourceDirectory, packageName).resolve(fileName); + } + + private static Path resolvePackage(Path directory, String packageName) { + return directory.resolve(packageName.replace('.', '/')); + } + +} diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/copilot/ProjectArtifactType.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/copilot/ProjectArtifactType.java new file mode 100644 index 000000000..3ce7a5311 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/copilot/ProjectArtifactType.java @@ -0,0 +1,20 @@ +package org.springframework.ide.vscode.boot.java.copilot; + +/** + * The type of artifacts returned from an AI generated response to create code + */ +public enum ProjectArtifactType { + + APPLICATION_PROPERTIES, + + MAIN_CLASS, + + SOURCE_CODE, + + TEST_CODE, + + MAVEN_DEPENDENCIES, + + HTML + +} \ No newline at end of file diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/copilot/ProjectDependency.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/copilot/ProjectDependency.java new file mode 100644 index 000000000..cd313b6ce --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/copilot/ProjectDependency.java @@ -0,0 +1,27 @@ +package org.springframework.ide.vscode.boot.java.copilot; + +public class ProjectDependency { + + private final String groupId; + + private final String artifactId; + + public ProjectDependency(String groupId, String artifactId) { + this.groupId = groupId; + this.artifactId = artifactId; + } + + public String getGroupId() { + return groupId; + } + + public String getArtifactId() { + return artifactId; + } + + @Override + public String toString() { + return "ProjectDependency{" + "groupId='" + groupId + '\'' + ", artifactId='" + artifactId + '\'' + '}'; + } + +} diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/copilot/PropertyFileUtils.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/copilot/PropertyFileUtils.java new file mode 100644 index 000000000..5c6c31e33 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/copilot/PropertyFileUtils.java @@ -0,0 +1,41 @@ +package org.springframework.ide.vscode.boot.java.copilot; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.util.Properties; + +import org.apache.commons.io.IOUtils; + +public final class PropertyFileUtils { + + private PropertyFileUtils() { + } + + public static Properties getPropertyFile() { + File homeDir = new File(System.getProperty("user.home")); + File propertyFile = new File(homeDir, ".openai"); + Properties props = new Properties(); + FileInputStream in = null; + try { + in = new FileInputStream(propertyFile); + props.load(in); + return props; + } + catch (IOException ex) { + throw new SpringCliException("Could not load property file ~/.openai", ex); + } + finally { + IOUtils.closeQuietly(in); + } + } + + public static Properties mergeProperties(Properties... properties) { + Properties mergedProperties = new Properties(); + for (Properties property : properties) { + mergedProperties.putAll(property); + } + return mergedProperties; + } + +} \ No newline at end of file diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/copilot/ResponseModifier.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/copilot/ResponseModifier.java new file mode 100644 index 000000000..1579aef1d --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/copilot/ResponseModifier.java @@ -0,0 +1,66 @@ +package org.springframework.ide.vscode.boot.java.copilot; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.FormatStyle; + +public class ResponseModifier { + + public String modify(String response) { + System.out.println("In response modifier !!"); + return modifyMsyqlDependency(modifyJavax(response)); + } + + private String modifyJavax(String response) { + StringBuilder sb = new StringBuilder(); + sb.append("*Note: The code provided is just an example and may not be suitable for production use.*"); + sb.append(System.lineSeparator()); + sb.append(System.lineSeparator()); + DateTimeFormatter formatter = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM); + sb.append("Generated on " + LocalDateTime.now().format(formatter)); + sb.append(System.lineSeparator()); + sb.append(System.lineSeparator()); + sb.append(response); + String code = sb.toString(); + + if (!code.contains("import javax")) { + return code; + } + return replaceImportStatements(code); + } + + private static String replaceImportStatements(String content) { + String[] lines = content.split(System.lineSeparator()); + StringBuilder updatedContent = new StringBuilder(); + + for (String line : lines) { + if (line.trim().startsWith("import ")) { + String packageName = extractPackageName(line); + if (!packageName.contains("javax.sql.")) { + packageName = packageName.replace("javax", "jakarta"); + } + line = "import " + packageName + ";"; + } + updatedContent.append(line).append(System.lineSeparator()); + } + return updatedContent.toString(); + } + + private static String extractPackageName(String importStatement) { + return importStatement.replace("import", "").replace(";", "").trim(); + } + + private String modifyMsyqlDependency(String response) { + if (!response.contains("mysql-connector-java")) { + return response; + } + else { + String s1 = response.replace("mysql", "com.mysql"); + String s2 = s1.replace("mysql-connector-java", + "mysql-connector-j"); + return s2; + } + + } + +} diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/copilot/SpringCliException.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/copilot/SpringCliException.java new file mode 100644 index 000000000..e8084ffb7 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/copilot/SpringCliException.java @@ -0,0 +1,23 @@ +package org.springframework.ide.vscode.boot.java.copilot; + +@SuppressWarnings("serial") +public class SpringCliException extends RuntimeException { + + /** + * Instantiates a new {@code UpException}. + * @param message the message + */ + public SpringCliException(String message) { + super(message); + } + + /** + * Instantiates a new {@code UpException}. + * @param message the message + * @param cause the cause + */ + public SpringCliException(String message, Throwable cause) { + super(message, cause); + } + +} \ No newline at end of file diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/copilot/TemplateEngine.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/copilot/TemplateEngine.java new file mode 100644 index 000000000..aed710013 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/copilot/TemplateEngine.java @@ -0,0 +1,9 @@ +package org.springframework.ide.vscode.boot.java.copilot; + +import java.util.Map; + +public interface TemplateEngine { + + String process(String template, Map context); + +} diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/copilot/recipe/AbstractRecipeFactory.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/copilot/recipe/AbstractRecipeFactory.java new file mode 100644 index 000000000..d4d1281ce --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/copilot/recipe/AbstractRecipeFactory.java @@ -0,0 +1,56 @@ +/* + * Copyright 2021-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.ide.vscode.boot.java.copilot.recipe; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.dataformat.xml.XmlMapper; + +/** + * @author Fabian Krüger + */ +public class AbstractRecipeFactory { + + private static XmlMapper mapper = new XmlMapper(); + + @org.jetbrains.annotations.Nullable + protected static String getNullOrTextValue(JsonNode jsonNode, String field) { + return (jsonNode.get(field) != null) ? AbstractRecipeFactory.getTextValue(jsonNode, field) : null; + } + + protected static String getTextValue(JsonNode jsonNode, String field) { + try { + JsonNode xmlNode = jsonNode.get(field); + return xmlNode.textValue(); + } + catch (NullPointerException npe) { + throw new RecipeCreationException( + "Could not get text value for field '%s' from: \n%s".formatted(field, jsonNode.toPrettyString())); + } + } + + protected static JsonNode getJsonNode(String mavenDependencySnippet) throws JsonProcessingException { + JsonNode jsonNode = mapper.readTree(mavenDependencySnippet); + return jsonNode; + } + + protected String getTextOrDefaultValue(JsonNode jsonNode, String version, String defaultValue) { + String nullOrTextValue = getNullOrTextValue(jsonNode, version); + return (nullOrTextValue != null) ? nullOrTextValue : defaultValue; + } + +} diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/copilot/recipe/AddDependencyRecipe.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/copilot/recipe/AddDependencyRecipe.java new file mode 100644 index 000000000..a4628c995 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/copilot/recipe/AddDependencyRecipe.java @@ -0,0 +1,86 @@ +/* + * Copyright 2021-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.ide.vscode.boot.java.copilot.recipe; + +import java.util.regex.Pattern; + +import org.openrewrite.ExecutionContext; +import org.openrewrite.Recipe; +import org.openrewrite.TreeVisitor; +import org.openrewrite.internal.lang.Nullable; +import org.openrewrite.maven.AddDependencyVisitor; +import org.openrewrite.maven.table.MavenMetadataFailures; + +/** + * Alternative to {@link org.openrewrite.maven.AddDependency} not doing any version + * checks. It uses the {@link AddDependencyVisitor} and bypasses all checks and other code + * in {@link org.openrewrite.maven.AddManagedDependency}. + * + * @author Fabian Krüger + */ +public class AddDependencyRecipe extends Recipe { + + private final String groupId; + + private final String artifactId; + + private final String version; + + private final String scope; + + private final String type; + + private final @Nullable String classifier; + + private final @Nullable Boolean optional; + + private final Pattern familyRegex; + + private final MavenMetadataFailures metadataFailures; + + public AddDependencyRecipe(String groupId, String artifactId, String version, String scope, String type, + @Nullable String classifier, @Nullable Boolean optional, Pattern familyRegex, + MavenMetadataFailures metadataFailures) { + this.groupId = groupId; + this.artifactId = artifactId; + this.version = version; + this.scope = scope; + this.type = type; + this.classifier = classifier; + this.optional = optional; + this.familyRegex = familyRegex; + this.metadataFailures = metadataFailures; + } + + @Override + public String getDisplayName() { + return "Add dependency '%s:%s'".formatted(groupId, artifactId); + } + + @Override + public String getDescription() { + return getDisplayName(); + } + + @Override + public TreeVisitor getVisitor() { + AddDependencyVisitor addDependencyVisitor = new AddDependencyVisitor(groupId, artifactId, version, null, scope, + true, type, classifier, optional, familyRegex, metadataFailures); + return addDependencyVisitor; + } + +} diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/copilot/recipe/AddDependencyRecipeFactory.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/copilot/recipe/AddDependencyRecipeFactory.java new file mode 100644 index 000000000..4eff5c99e --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/copilot/recipe/AddDependencyRecipeFactory.java @@ -0,0 +1,80 @@ +/* + * Copyright 2021-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.ide.vscode.boot.java.copilot.recipe; + +import java.util.regex.Pattern; + +import org.openrewrite.internal.lang.Nullable; +import org.openrewrite.maven.AddDependency; +import org.openrewrite.maven.table.MavenMetadataFailures; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; + +/** + * @author Fabian Krüger + */ +public class AddDependencyRecipeFactory extends AbstractRecipeFactory { + + /** + * Create {@link AddDependency} recipe from Maven dependency XML snippet. + * + *
+	 * {@code
+	 * 
+	 *  groupId
+	 *  artifactId
+	 *  ${some.version}
+	 *  classifier
+	 *  scope
+	 *  pom
+	 *  true
+	 * 
+	 * }
+	 * 
+ * + */ + public AddDependencyRecipe create(String mavenDependency) { + try { + JsonNode jsonNode = getJsonNode(mavenDependency); + String groupId = getTextValue(jsonNode, "groupId"); + String artifactId = getTextValue(jsonNode, "artifactId"); + String version = getTextOrDefaultValue(jsonNode, "version", "latest"); + @Nullable + String scope = getNullOrTextValue(jsonNode, "scope"); + @Nullable + String type = getNullOrTextValue(jsonNode, "type"); + @Nullable + String classifier = getNullOrTextValue(jsonNode, "classifier"); + @Nullable + Boolean optional = Boolean.parseBoolean(getNullOrTextValue(jsonNode, "optional")); + @Nullable + String familyPattern = null; + Pattern familyRegex = (familyPattern != null) ? Pattern.compile(familyPattern) : null; + MavenMetadataFailures metadataFailures = null; + + AddDependencyRecipe recipe = new AddDependencyRecipe(groupId, artifactId, version, scope, type, classifier, + optional, familyRegex, metadataFailures); + return recipe; + } + catch (JsonProcessingException ex) { + throw new RuntimeException(ex); + } + + } + +} diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/copilot/recipe/AddManagedDependencyRecipe.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/copilot/recipe/AddManagedDependencyRecipe.java new file mode 100644 index 000000000..db4e2e326 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/copilot/recipe/AddManagedDependencyRecipe.java @@ -0,0 +1,70 @@ +/* + * Copyright 2021-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.ide.vscode.boot.java.copilot.recipe; + +import org.openrewrite.ExecutionContext; +import org.openrewrite.Recipe; +import org.openrewrite.TreeVisitor; +import org.openrewrite.maven.AddManagedDependencyVisitor; + +/** + * Alternative to {@link org.openrewrite.maven.AddManagedDependency} that doesn't verify + * the provided version. It uses the {@link AddManagedDependencyVisitor} and bypasses all + * checks and other code in {@link org.openrewrite.maven.AddManagedDependency}. + * + * @author Fabian Krüger + */ +public class AddManagedDependencyRecipe extends Recipe { + + private final String groupId; + + private final String artifactId; + + private final String version; + + private final String scope; + + private final String type; + + private final String classifier; + + public AddManagedDependencyRecipe(String groupId, String artifactId, String version, String scope, String type, + String classifier) { + this.groupId = groupId; + this.artifactId = artifactId; + this.version = version; + this.scope = scope; + this.type = type; + this.classifier = classifier; + } + + @Override + public String getDisplayName() { + return "Add managed dependency '%s:%s'".formatted(groupId, artifactId); + } + + @Override + public String getDescription() { + return getDisplayName(); + } + + @Override + public TreeVisitor getVisitor() { + return new AddManagedDependencyVisitor(groupId, artifactId, version, scope, type, classifier); + } + +} diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/copilot/recipe/AddManagedDependencyRecipeFactory.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/copilot/recipe/AddManagedDependencyRecipeFactory.java new file mode 100644 index 000000000..4ce055431 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/copilot/recipe/AddManagedDependencyRecipeFactory.java @@ -0,0 +1,81 @@ +/* + * Copyright 2021-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.ide.vscode.boot.java.copilot.recipe; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import org.apache.maven.model.Dependency; +import org.openrewrite.internal.lang.Nullable; + +/** + * @author Fabian Krüger + */ +public class AddManagedDependencyRecipeFactory extends AbstractRecipeFactory { + + /** + * Create a {@link AddManagedDependencyRecipe} from a Maven dependency XML snippet. + * + *
+	 * {@code
+	 * 
+	 *  groupId
+	 *  artifactId
+	 *  ${some.version}
+	 *  classifier
+	 *  pom
+	 * 
+	 * }
+	 * 
+ */ + public AddManagedDependencyRecipe create(String mavenDependencySnippet) { + try { + JsonNode jsonNode = getJsonNode(mavenDependencySnippet); + String groupId = getTextValue(jsonNode, "groupId"); + String artifactId = getTextValue(jsonNode, "artifactId"); + @Nullable + String version = getNullOrTextValue(jsonNode, "version"); + @Nullable + String scope = getNullOrTextValue(jsonNode, "scope"); + @Nullable + String classifier = getNullOrTextValue(jsonNode, "classifier"); + @Nullable + String type = getNullOrTextValue(jsonNode, "type"); + AddManagedDependencyRecipe addManagedDependency = new AddManagedDependencyRecipe(groupId, artifactId, + version, scope, type, classifier); + return addManagedDependency; + } + catch (JsonProcessingException ex) { + throw new RuntimeException(ex); + } + } + + /** + * Create a {@link AddManagedDependencyRecipe} from a {@link Dependency}. + */ + public AddManagedDependencyRecipe create(Dependency dependency) { + String groupId = dependency.getGroupId(); + String artifactId = dependency.getArtifactId(); + String version = dependency.getVersion(); + String scope = dependency.getScope(); + String type = dependency.getType(); + String classifier = dependency.getClassifier(); + AddManagedDependencyRecipe addManagedDependency = new AddManagedDependencyRecipe(groupId, artifactId, version, + scope, type, classifier); + return addManagedDependency; + } + +} diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/copilot/recipe/AddPluginRecipeFactory.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/copilot/recipe/AddPluginRecipeFactory.java new file mode 100644 index 000000000..ad35500b2 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/copilot/recipe/AddPluginRecipeFactory.java @@ -0,0 +1,56 @@ +/* + * Copyright 2021-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.ide.vscode.boot.java.copilot.recipe; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import org.openrewrite.internal.lang.Nullable; +import org.openrewrite.maven.AddPlugin; + +/** + * @author Fabian Krüger + */ +public class AddPluginRecipeFactory extends AbstractRecipeFactory { + + /** + * Create {@link AddPlugin} from Maven plugin XML snippet. + */ + public AddPlugin create(String buildPlugin) { + try { + JsonNode jsonNode = getJsonNode(buildPlugin); + String groupId = getTextValue(jsonNode, "groupId"); + String artifactId = getTextValue(jsonNode, "artifactId"); + @Nullable + String version = getNullOrTextValue(jsonNode, "version"); + @Nullable + String configuration = getNullOrTextValue(jsonNode, "configuration"); + @Nullable + String dependencies = getNullOrTextValue(jsonNode, "dependencies"); + @Nullable + String executions = getNullOrTextValue(jsonNode, "executions"); + @Nullable + String filePattern = getNullOrTextValue(jsonNode, "filePattern"); + AddPlugin addPlugin = new AddPlugin(groupId, artifactId, version, configuration, dependencies, executions, + filePattern); + return addPlugin; + } + catch (JsonProcessingException ex) { + throw new RuntimeException(ex); + } + } + +} diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/copilot/recipe/InjectTextMavenRepositoryRecipe.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/copilot/recipe/InjectTextMavenRepositoryRecipe.java new file mode 100644 index 000000000..e04918f33 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/copilot/recipe/InjectTextMavenRepositoryRecipe.java @@ -0,0 +1,73 @@ +/* + * Copyright 2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.ide.vscode.boot.java.copilot.recipe; + +import org.openrewrite.ExecutionContext; +import org.openrewrite.Recipe; +import org.openrewrite.TreeVisitor; +import org.openrewrite.maven.MavenIsoVisitor; +import org.openrewrite.xml.AddToTagVisitor; +import org.openrewrite.xml.XPathMatcher; +import org.openrewrite.xml.tree.Xml; +import org.openrewrite.xml.tree.Xml.Tag; + +public class InjectTextMavenRepositoryRecipe extends Recipe { + + private static final XPathMatcher REPOS_MATCHER = new XPathMatcher("/project/repositories"); + + private String text; + + public InjectTextMavenRepositoryRecipe(String text) { + this.text = text; + } + + @Override + public String getDisplayName() { + return "Add Repository"; + } + + @Override + public String getDescription() { + return getDisplayName(); + } + + public TreeVisitor getVisitor() { + return new MavenIsoVisitor() { + public Xml.Document visitDocument(Xml.Document document, ExecutionContext ctx) { + Xml.Tag root = document.getRoot(); + if (!root.getChild("repositories").isPresent()) { + document = (Xml.Document) (new AddToTagVisitor(root, Tag.build(""))) + .visitNonNull(document, ctx, this.getCursor().getParentOrThrow()); + } + + return super.visitDocument(document, ctx); + } + + public Xml.Tag visitTag(Xml.Tag tag, ExecutionContext ctx) { + Xml.Tag repositories = super.visitTag(tag, ctx); + if (REPOS_MATCHER.matches(this.getCursor())) { + Xml.Tag repositoryTag = Tag.build(text); + repositories = (Xml.Tag) (new AddToTagVisitor(repositories, repositoryTag)) + .visitNonNull(repositories, ctx, this.getCursor().getParentOrThrow()); + this.maybeUpdateModel(); + } + return repositories; + } + }; + } + +} diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/copilot/recipe/RecipeCreationException.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/copilot/recipe/RecipeCreationException.java new file mode 100644 index 000000000..3537d31d3 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/copilot/recipe/RecipeCreationException.java @@ -0,0 +1,28 @@ +/* + * Copyright 2021-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.ide.vscode.boot.java.copilot.recipe; + +/** + * @author Fabian Krüger + */ +public class RecipeCreationException extends RuntimeException { + + public RecipeCreationException(String message) { + super(message); + } + +} diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/copilot/recipe/RecipeUtils.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/copilot/recipe/RecipeUtils.java new file mode 100644 index 000000000..2e2f6878f --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/copilot/recipe/RecipeUtils.java @@ -0,0 +1,49 @@ +/* + * Copyright 2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.ide.vscode.boot.java.copilot.recipe; + +import java.io.BufferedWriter; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; + +import org.openrewrite.Result; +import org.springframework.ide.vscode.boot.java.copilot.SpringCliException; + +/** + * Utilities for recipe execution + */ +public final class RecipeUtils { + + private RecipeUtils() { + } + + public static void writeResults(String recipeName, Path path, List resultList) { + try { + for (Result result : resultList) { + try (BufferedWriter sourceFileWriter = Files.newBufferedWriter(path)) { + sourceFileWriter.write(result.getAfter().printAllTrimmed()); + } + } + } + catch (IOException ex) { + throw new SpringCliException("Could not write recipe results to path = " + path, ex); + } + } + +} diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/copilot/util/AbstractMavenReader.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/copilot/util/AbstractMavenReader.java new file mode 100644 index 000000000..9ce2ce196 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/copilot/util/AbstractMavenReader.java @@ -0,0 +1,128 @@ +package org.springframework.ide.vscode.boot.java.copilot.util; + +import java.io.IOException; +import java.io.InputStream; +import java.io.StringWriter; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.transform.OutputKeys; +import javax.xml.transform.Transformer; +import javax.xml.transform.TransformerException; +import javax.xml.transform.TransformerFactory; +import javax.xml.transform.dom.DOMSource; +import javax.xml.transform.stream.StreamResult; + +import org.apache.commons.io.IOUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.ide.vscode.boot.java.copilot.SpringCliException; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; +import org.xml.sax.ErrorHandler; +import org.xml.sax.SAXException; +import org.xml.sax.SAXParseException; + +public abstract class AbstractMavenReader { + + private static final Logger logger = LoggerFactory.getLogger(AbstractMavenReader.class); + + protected String sectionName; + + /** + * Reads the Document and populates the provided array with individual dependency text + * values for each dependency element. + * @param document The Document to parse + * @param dependencies a list to populate with each dependency text + * @throws TransformerException if the element can't be converted to text + */ + protected void parseDocument(Document document, List dependencies, String tagName) + throws TransformerException { + Element root = document.getDocumentElement(); + NodeList nodeList = root.getElementsByTagName(tagName); + + TransformerFactory tf = TransformerFactory.newInstance(); + Transformer transformer = tf.newTransformer(); + transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes"); + + for (int i = 0; i < nodeList.getLength(); i++) { + Node dependencyNode = nodeList.item(i); + if (dependencyNode.getNodeType() == Node.ELEMENT_NODE) { + Element element = (Element) dependencyNode; + StringWriter writer = new StringWriter(); + transformer.transform(new DOMSource(element), new StreamResult(writer)); + String xml = writer.toString(); + dependencies.add(xml); + } + } + } + + protected Document buildDocument(ErrorHandler handler, InputStream stream) + throws ParserConfigurationException, SAXException, IOException { + + DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); + dbf.setNamespaceAware(true); + DocumentBuilder parser = dbf.newDocumentBuilder(); + parser.setErrorHandler(handler); + return parser.parse(stream); + } + + protected abstract String massageText(String text); + + public String[] parseMavenSection(String text) { + String textToUse = massageText(text); + ErrorHandler errorHandler = new SimpleErrorHandler(logger); + List dependencies = new ArrayList<>(); + try { + InputStream inputStream = IOUtils.toInputStream(textToUse, StandardCharsets.UTF_8); + Document document = buildDocument(errorHandler, inputStream); + parseDocument(document, dependencies, this.sectionName); + } + catch (IOException ex) { + throw new SpringCliException("Cannot parse maven " + this.sectionName + " from string: " + text, ex); + } + catch (SAXException ex) { + throw new SpringCliException("Invalid XML in maven " + this.sectionName + " from string: " + text, ex); + } + catch (ParserConfigurationException ex) { + throw new SpringCliException("Internal error parsing maven " + this.sectionName + " from string:" + text, + ex); + } + catch (TransformerException ex) { + throw new SpringCliException("Internal error transforming Node to text from string:" + text, ex); + } + return dependencies.toArray(new String[0]); + } + + protected class SimpleErrorHandler implements ErrorHandler { + + private final Logger logger; + + public SimpleErrorHandler(Logger logger) { + this.logger = logger; + } + + @Override + public void warning(SAXParseException ex) throws SAXException { + logger.warn("Ignored XML validation warning", ex); + } + + @Override + public void error(SAXParseException ex) throws SAXException { + throw ex; + } + + @Override + public void fatalError(SAXParseException ex) throws SAXException { + throw ex; + } + + } + +} diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/copilot/util/ClassNameExtractor.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/copilot/util/ClassNameExtractor.java new file mode 100644 index 000000000..8583dedce --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/copilot/util/ClassNameExtractor.java @@ -0,0 +1,32 @@ +package org.springframework.ide.vscode.boot.java.copilot.util; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class ClassNameExtractor { + + private String[] patternStrings = { "(?<=\\bclass\\s)\\w+", "(?<=\\binterface\\s)\\w+", "(?<=\\b@interface\\s)\\w+", + "(?<=\\benum\\s)\\w+" }; + + private List patterns = new ArrayList<>(); + + public ClassNameExtractor() { + for (String patternString : patternStrings) { + patterns.add(Pattern.compile(patternString)); + } + } + + public Optional extractClassName(String code) { + for (Pattern pattern : patterns) { + Matcher matcher = pattern.matcher(code); + if (matcher.find()) { + return Optional.of(matcher.group()); + } + } + return Optional.empty(); + } + +} \ No newline at end of file diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/copilot/util/MavenBuildPluginReader.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/copilot/util/MavenBuildPluginReader.java new file mode 100644 index 000000000..171292f82 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/copilot/util/MavenBuildPluginReader.java @@ -0,0 +1,18 @@ +package org.springframework.ide.vscode.boot.java.copilot.util; + +public class MavenBuildPluginReader extends AbstractMavenReader { + + public MavenBuildPluginReader() { + this.sectionName = "plugin"; + } + + protected String massageText(String text) { + if (text.contains("")) { + return text; + } + else { + return "" + text + ""; + } + } + +} \ No newline at end of file diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/copilot/util/MavenDependencyReader.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/copilot/util/MavenDependencyReader.java new file mode 100644 index 000000000..1b5d6fc69 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/copilot/util/MavenDependencyReader.java @@ -0,0 +1,18 @@ +package org.springframework.ide.vscode.boot.java.copilot.util; + +public class MavenDependencyReader extends AbstractMavenReader { + + public MavenDependencyReader() { + this.sectionName = "dependency"; + } + + protected String massageText(String text) { + if (text.contains("")) { + return text; + } + else { + return "" + text + ""; + } + } + +} \ No newline at end of file diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/copilot/util/MavenRepositoryReader.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/copilot/util/MavenRepositoryReader.java new file mode 100644 index 000000000..a61198539 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/copilot/util/MavenRepositoryReader.java @@ -0,0 +1,18 @@ +package org.springframework.ide.vscode.boot.java.copilot.util; + +public class MavenRepositoryReader extends AbstractMavenReader { + + public MavenRepositoryReader() { + this.sectionName = "repository"; + } + + protected String massageText(String text) { + if (text.contains("")) { + return text; + } + else { + return "" + text + ""; + } + } + +} diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/copilot/util/PomReader.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/copilot/util/PomReader.java new file mode 100644 index 000000000..efaf49c6e --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/copilot/util/PomReader.java @@ -0,0 +1,45 @@ +package org.springframework.ide.vscode.boot.java.copilot.util; + +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.io.Reader; +import java.nio.file.Files; + +import org.apache.maven.model.Model; +import org.apache.maven.model.io.xpp3.MavenXpp3Reader; +import org.codehaus.plexus.util.xml.pull.XmlPullParserException; + +/** + * @author Ryan Baxter + */ +public class PomReader { + + /** + * Returns a parsed POM. + */ + public Model readPom(File file) { + File pom = file; + if (file.isDirectory()) { + pom = new File(file, "pom.xml"); + } + if (!pom.exists()) { + return null; + } + String fileText = ""; + try (Reader reader = new FileReader(pom)) { + if (file.isFile()) { + fileText = new String(Files.readAllBytes(file.toPath())); + } + MavenXpp3Reader xpp3Reader = new MavenXpp3Reader(); + return xpp3Reader.read(reader); + } + catch (XmlPullParserException | IOException ex) { + if (file.isFile() && fileText.length() == 0) { + throw new IllegalStateException("File [" + pom.getAbsolutePath() + "] is empty", ex); + } + throw new IllegalStateException("Failed to read file: " + pom.getAbsolutePath(), ex); + } + } + +} diff --git a/vscode-extensions/vscode-spring-boot/lib/Main.ts b/vscode-extensions/vscode-spring-boot/lib/Main.ts index 556ce2641..483335889 100644 --- a/vscode-extensions/vscode-spring-boot/lib/Main.ts +++ b/vscode-extensions/vscode-spring-boot/lib/Main.ts @@ -24,7 +24,6 @@ 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"; @@ -38,7 +37,6 @@ 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 { diff --git a/vscode-extensions/vscode-spring-boot/lib/copilot/guideApply.ts b/vscode-extensions/vscode-spring-boot/lib/copilot/guideApply.ts index dbe63e882..7fe4390bc 100644 --- a/vscode-extensions/vscode-spring-boot/lib/copilot/guideApply.ts +++ b/vscode-extensions/vscode-spring-boot/lib/copilot/guideApply.ts @@ -1,8 +1,8 @@ -import { Uri, workspace, window } from "vscode"; -import { SPRINGCLI } from "../Main"; -import { getTargetGuideMardown } from "./util"; +import { Uri, workspace, window, commands } from "vscode"; +import { getTargetGuideMardown, readResponseFromFile } from "./util"; import { createConverter } from "vscode-languageclient/lib/common/protocolConverter"; import fs from "fs"; +import path from "path"; const CONVERTER = createConverter(undefined, true, true); @@ -13,11 +13,13 @@ export async function applyLspEdit(uri: Uri) { if (!uri) { uri = await getTargetGuideMardown(); } - const lspEdit = await SPRINGCLI.guideLspEdit(uri); + + const fileContent = (await readResponseFromFile(uri)).toString(); + const lspEdit = await commands.executeCommand("sts/copilot/agent/lspEdits", uri.toString(), path.dirname(uri.fsPath), fileContent); const workspaceEdit = await CONVERTER.asWorkspaceEdit(lspEdit); - console.log(lspEdit); await Promise.all(workspaceEdit.entries().map(async ([uri, edits]) => { + console.log(edits); if (fs.existsSync(uri.fsPath)) { const doc = await workspace.openTextDocument(uri.fsPath); await window.showTextDocument(doc); diff --git a/vscode-extensions/vscode-spring-boot/lib/copilot/springBootAgent.ts b/vscode-extensions/vscode-spring-boot/lib/copilot/springBootAgent.ts index 310238394..7c83354ae 100644 --- a/vscode-extensions/vscode-spring-boot/lib/copilot/springBootAgent.ts +++ b/vscode-extensions/vscode-spring-boot/lib/copilot/springBootAgent.ts @@ -3,7 +3,6 @@ import { CancellationToken, chat, ChatContext, ChatRequest, ChatResponseStream, 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; @@ -77,7 +76,7 @@ export default class SpringBootChatAgent { } 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); + const enhancedResponse = await commands.executeCommand("sts/copilot/agent/enhanceResponse", response) as string; await writeResponseToFile(enhancedResponse, bootProjInfo.name, selectedProject.fsPath); } documentContent = await workspace.fs.readFile(targetMarkdownUri); diff --git a/vscode-extensions/vscode-spring-boot/lib/copilot/springCli.ts b/vscode-extensions/vscode-spring-boot/lib/copilot/springCli.ts index b43946ddd..75d4c6098 100644 --- a/vscode-extensions/vscode-spring-boot/lib/copilot/springCli.ts +++ b/vscode-extensions/vscode-spring-boot/lib/copilot/springCli.ts @@ -1,121 +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"; +// 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 const CANCELLED = "Cancelled"; -export class SpringCli { +// export class SpringCli { - get executable(): string { - return workspace.getConfiguration("spring-cli").get("executable") || "spring"; - } +// get executable(): string { +// return workspace.getConfiguration("spring-cli").get("executable") || "spring"; +// } - guideLspEdit(uri: Uri, cwd?: string): Promise { - const args = [ - "guide", - "apply", - "--lsp-edit", - "--file", - uri.fsPath - ]; - return this.fetchJson("Applying guide", uri.fsPath, args, cwd || path.dirname(uri.fsPath), true); - } +// guideLspEdit(uri: Uri, cwd?: string): Promise { +// 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 { - const args = [ - "ai", - "enhance-response", - "--file", - uri.fsPath - ]; - return this.exec("Spring cli ai", "Enhance response", args, cwd); - } +// enhanceResponse(uri: Uri, cwd: string): Thenable { +// 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 { - 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((resolve, reject) => { - process.on("exit", (code) => { - if (code) { - reject(`Failed to execute command: ${dataChunks.join()}`); - } else { - resolve(dataChunks.join()); - } - }); - }); - } +// private async executeCommand(args: string[], cwd?: string): Promise { +// 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((resolve, reject) => { +// process.on("exit", (code) => { +// if (code) { +// reject(`Failed to execute command: ${dataChunks.join()}`); +// } else { +// resolve(dataChunks.join()); +// } +// }); +// }); +// } - private async exec(title: string, message: string, args: string[], cwd?: string): Promise { - return vscode.window.withProgress({ - location: vscode.ProgressLocation.Window, - cancellable: true, - title, - }, async (progress, cancellation) => { +// private async exec(title: string, message: string, args: string[], cwd?: string): Promise { +// return vscode.window.withProgress({ +// location: vscode.ProgressLocation.Window, +// cancellable: true, +// title, +// }, async (progress, cancellation) => { - if (message) { - progress.report({ message }); - } +// if (message) { +// progress.report({ message }); +// } - return new Promise(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); - } - }); - }); - } +// return new Promise(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(title: string, message: string, args: string[], cwd?: string, omitJsonParam?: boolean): Promise { +// private async fetchJson(title: string, message: string, args: string[], cwd?: string, omitJsonParam?: boolean): Promise { - return window.withProgress({ - location: ProgressLocation.Window, - cancellable: true, - title - }, (progress, cancellation) => { +// return window.withProgress({ +// location: ProgressLocation.Window, +// cancellable: true, +// title +// }, (progress, cancellation) => { - if (message) { - progress.report({ message }); - } +// if (message) { +// progress.report({ message }); +// } - return new Promise(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); - } - } - }); - }); - }); - } +// return new Promise(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); +// } +// } +// }); +// }); +// }); +// } -} +// } diff --git a/vscode-extensions/vscode-spring-boot/lib/copilot/util.ts b/vscode-extensions/vscode-spring-boot/lib/copilot/util.ts index 94174170e..14f5cb3ef 100644 --- a/vscode-extensions/vscode-spring-boot/lib/copilot/util.ts +++ b/vscode-extensions/vscode-spring-boot/lib/copilot/util.ts @@ -3,10 +3,6 @@ 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 { if (workspace.workspaceFolders && workspace.workspaceFolders.length) { if (workspace.workspaceFolders.length === 1) { @@ -86,6 +82,10 @@ export async function writeResponseToFile(response: string, appName: string, sel } } +export async function readResponseFromFile(uri: Uri) { + return workspace.fs.readFile(uri); +} + export function isLlmApiReady(): boolean { return version.includes('insider') && new SemVer(version).compare(new SemVer("1.90.0-insider")) >= 0; } \ No newline at end of file