From 1539bebde3b7d3c5f51fc0fd421e53938138cde3 Mon Sep 17 00:00:00 2001 From: aboyko Date: Tue, 5 Dec 2023 16:33:10 -0500 Subject: [PATCH] Integrate needsConfirmation --- .../vscode/commons/rewrite/ORDocUtils.java | 25 ++++++++---- .../java/rewrite/RewriteRecipeRepository.java | 19 +++++---- .../java/rewrite/RewriteRefactorings.java | 14 +++++-- .../boot/java/rewrite/SpringBootUpgrade.java | 4 +- .../vscode-spring-boot/lib/rewrite.ts | 39 +++++++++++++++---- 5 files changed, 73 insertions(+), 28 deletions(-) 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 432584d45..d8fcf2644 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 @@ -13,8 +13,12 @@ package org.springframework.ide.vscode.commons.rewrite; import java.nio.file.Path; import java.util.ArrayList; import java.util.List; +import java.util.Map; import java.util.Optional; +import java.util.UUID; +import org.eclipse.lsp4j.AnnotatedTextEdit; +import org.eclipse.lsp4j.ChangeAnnotation; import org.eclipse.lsp4j.CreateFile; import org.eclipse.lsp4j.DeleteFile; import org.eclipse.lsp4j.Position; @@ -72,7 +76,7 @@ public class ORDocUtils { } - public static Optional computeTextDocEdit(TextDocument doc, Result result) { + public static Optional computeTextDocEdit(TextDocument doc, Result result, String changeAnnotationId) { TextDocument newDoc = new TextDocument(null, LanguageId.PLAINTEXT, 0, result.getAfter().printAll()); EditList diff = JGitUtils.getDiff(result.getBefore().printAll(), newDoc.get()); @@ -85,26 +89,29 @@ public class ORDocUtils { try { switch(e.getType()) { case DELETE: - TextEdit textEdit = new TextEdit(); + AnnotatedTextEdit textEdit = new AnnotatedTextEdit(); int start = doc.getLineOffset(e.getBeginA()); int end = getStartOfLine(doc, e.getEndA()); textEdit.setRange(new Range(doc.toPosition(start), doc.toPosition(end))); textEdit.setNewText(""); + textEdit.setAnnotationId(changeAnnotationId); textEdits.add(textEdit); break; case INSERT: - textEdit = new TextEdit(); + textEdit = new AnnotatedTextEdit(); Position position = doc.toPosition(doc.getLineOffset(e.getBeginA())); textEdit.setRange(new Range(position, position)); textEdit.setNewText(newDoc.textBetween(newDoc.getLineOffset(e.getBeginB()), getStartOfLine(newDoc, e.getEndB()))); + textEdit.setAnnotationId(changeAnnotationId); textEdits.add(textEdit); break; case REPLACE: - textEdit = new TextEdit(); + textEdit = new AnnotatedTextEdit(); start = doc.getLineOffset(e.getBeginA()); end = getStartOfLine(doc, e.getEndA()); textEdit.setRange(new Range(doc.toPosition(start), doc.toPosition(end))); textEdit.setNewText(newDoc.textBetween(newDoc.getLineOffset(e.getBeginB()), getStartOfLine(newDoc, e.getEndB()))); + textEdit.setAnnotationId(changeAnnotationId); textEdits.add(textEdit); break; case EMPTY: @@ -151,12 +158,14 @@ public class ORDocUtils { return 0; } - public static Optional createWorkspaceEdit(Path absoluteProjectDir, SimpleTextDocumentService documents, List results) { + public static Optional createWorkspaceEdit(Path absoluteProjectDir, SimpleTextDocumentService documents, List results, ChangeAnnotation changeAnnotation) { if (results.isEmpty()) { return Optional.empty(); } WorkspaceEdit we = new WorkspaceEdit(); we.setDocumentChanges(new ArrayList<>()); + final String changeAnnotationId = UUID.randomUUID().toString(); + we.setChangeAnnotations(Map.of(changeAnnotationId, changeAnnotation)); for (Result result : results) { if (result.getBefore() == null) { String docUri = absoluteProjectDir.resolve(result.getAfter().getSourcePath()).toUri().toASCIIString(); @@ -167,7 +176,7 @@ public class ORDocUtils { TextDocumentEdit te = new TextDocumentEdit(); te.setTextDocument(new VersionedTextDocumentIdentifier(docUri, 0)); Position cursor = new Position(0,0); - te.setEdits(List.of(new TextEdit(new Range(cursor, cursor), result.getAfter().printAll()))); + te.setEdits(List.of(new AnnotatedTextEdit(new Range(cursor, cursor), result.getAfter().printAll(), changeAnnotationId))); we.getDocumentChanges().add(Either.forLeft(te)); } else if (result.getAfter() == null) { String docUri = absoluteProjectDir.resolve(result.getBefore().getSourcePath()).toUri().toASCIIString(); @@ -177,9 +186,9 @@ public class ORDocUtils { TextDocument doc = documents.getLatestSnapshot(docUri); if (doc == null) { doc = new TextDocument(docUri, null, 0, result.getBefore().printAll()); - ORDocUtils.computeTextDocEdit(doc, result).ifPresent(te -> we.getDocumentChanges().add(Either.forLeft(te))); + ORDocUtils.computeTextDocEdit(doc, result, changeAnnotationId).ifPresent(te -> we.getDocumentChanges().add(Either.forLeft(te))); } else { - ORDocUtils.computeTextDocEdit(doc, result).ifPresent(te -> we.getDocumentChanges().add(Either.forLeft(te))); + ORDocUtils.computeTextDocEdit(doc, result, changeAnnotationId).ifPresent(te -> we.getDocumentChanges().add(Either.forLeft(te))); } } diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/rewrite/RewriteRecipeRepository.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/rewrite/RewriteRecipeRepository.java index c76417d51..483bc75d4 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/rewrite/RewriteRecipeRepository.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/rewrite/RewriteRecipeRepository.java @@ -34,6 +34,7 @@ import java.util.regex.Pattern; import java.util.stream.Collectors; import org.eclipse.lsp4j.ApplyWorkspaceEditParams; +import org.eclipse.lsp4j.ChangeAnnotation; import org.eclipse.lsp4j.TextDocumentIdentifier; import org.eclipse.lsp4j.WorkspaceEdit; import org.openrewrite.InMemoryExecutionContext; @@ -282,6 +283,7 @@ public class RewriteRecipeRepository { return recipes().thenCompose(recipes -> { String uri = ((JsonElement) params.getArguments().get(0)).getAsString(); JsonElement recipesJson = ((JsonElement) params.getArguments().get(1)); + boolean needsConfirmation = params.getArguments().size() > 2 ? ((JsonElement) params.getArguments().get(2)).getAsBoolean() : false; RecipeDescriptor d = serializationGson.fromJson(recipesJson, RecipeDescriptor.class); @@ -295,14 +297,14 @@ public class RewriteRecipeRepository { || params.getWorkDoneToken().getLeft() == null ? (r.getName() == null ? UUID.randomUUID().toString() : r.getName()) : params.getWorkDoneToken().getLeft(); - return apply(r, uri, progressToken); + return apply(r, uri, progressToken, needsConfirmation); } else { String progressToken = params.getWorkDoneToken() == null || params.getWorkDoneToken().getLeft() == null ? (aggregateRecipe.getName() == null ? UUID.randomUUID().toString() : aggregateRecipe.getName()) : params.getWorkDoneToken().getLeft(); - return apply(aggregateRecipe, uri, progressToken); + return apply(aggregateRecipe, uri, progressToken, needsConfirmation); } }); }); @@ -314,23 +316,24 @@ public class RewriteRecipeRepository { server.onCommand(CMD_REWRITE_RECIPE_EXECUTE, params -> { String recipeId = ((JsonElement) params.getArguments().get(0)).getAsString(); + boolean needsConfirmation = params.getArguments().size() > 1 ? ((JsonElement) params.getArguments().get(1)).getAsBoolean() : false; return getRecipe(recipeId).thenCompose(optRecipe -> { Recipe r = optRecipe.orElseThrow(() -> new IllegalArgumentException("No such recipe exists with name " + recipeId)); final String progressToken = params.getWorkDoneToken() == null || params.getWorkDoneToken().getLeft() == null ? r.getName() : params.getWorkDoneToken().getLeft(); String uri = ((JsonElement) params.getArguments().get(1)).getAsString(); - return apply(r, uri, progressToken); + return apply(r, uri, progressToken, needsConfirmation); }); }); } - CompletableFuture apply(Recipe r, String uri, String progressToken) { + CompletableFuture apply(Recipe r, String uri, String progressToken, boolean needsConfirmation) { final IndefiniteProgressTask progressTask = server.getProgressService().createIndefiniteProgressTask(progressToken, r.getDisplayName(), "Initiated..."); return CompletableFuture.supplyAsync(() -> { return projectFinder.find(new TextDocumentIdentifier(uri)); }).thenCompose(p -> { if (p.isPresent()) { try { - Optional edit = computeWorkspaceEdit(r, p.get(), progressTask); + Optional edit = computeWorkspaceEdit(r, p.get(), progressTask, needsConfirmation); return CompletableFuture.completedFuture(edit).thenCompose(we -> { if (we.isPresent()) { progressTask.progressEvent("Applying document changes..."); @@ -358,7 +361,7 @@ public class RewriteRecipeRepository { }); } - private Optional computeWorkspaceEdit(Recipe r, IJavaProject project, IndefiniteProgressTask progressTask) { + private Optional computeWorkspaceEdit(Recipe r, IJavaProject project, IndefiniteProgressTask progressTask, boolean needsConfirmation) { Path absoluteProjectDir = Paths.get(project.getLocationUri()); progressTask.progressEvent("Parsing files..."); ProjectParser projectParser = createRewriteProjectParser(project, @@ -374,7 +377,9 @@ public class RewriteRecipeRepository { progressTask.progressEvent("Computing changes..."); RecipeRun reciperun = r.run(new InMemoryLargeSourceSet(sources), new InMemoryExecutionContext(e -> log.error("Recipe execution failed", e))); List results = reciperun.getChangeset().getAllResults(); - return ORDocUtils.createWorkspaceEdit(absoluteProjectDir, server.getTextDocumentService(), results); + ChangeAnnotation changeAnnotation = new ChangeAnnotation("Apply Recipe '" + r.getDisplayName() + "'"); + changeAnnotation.setNeedsConfirmation(needsConfirmation); + return ORDocUtils.createWorkspaceEdit(absoluteProjectDir, server.getTextDocumentService(), results, changeAnnotation); } private void reportParseErrors(List parseErrors) { diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/rewrite/RewriteRefactorings.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/rewrite/RewriteRefactorings.java index b25b1270f..e1a19584c 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/rewrite/RewriteRefactorings.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/rewrite/RewriteRefactorings.java @@ -15,12 +15,14 @@ import java.net.URI; import java.nio.file.Paths; import java.util.Collections; import java.util.List; +import java.util.Map; import java.util.Map.Entry; import java.util.Optional; import java.util.UUID; import java.util.concurrent.CompletableFuture; import java.util.stream.Collectors; +import org.eclipse.lsp4j.ChangeAnnotation; import org.eclipse.lsp4j.CodeAction; import org.eclipse.lsp4j.ResourceOperation; import org.eclipse.lsp4j.TextDocumentEdit; @@ -120,22 +122,26 @@ public class RewriteRefactorings implements CodeActionResolver, QuickfixHandler return null; } - private WorkspaceEdit applyRecipe(Recipe r, IJavaProject project, List cus) { + private WorkspaceEdit applyRecipe(Recipe r, IJavaProject project, List cus, boolean needsConfirmation) { List sources = cus.stream().map(cu -> (SourceFile) cu).collect(Collectors.toList()); RecipeRun reciperun = r.run(new InMemoryLargeSourceSet(sources), new InMemoryExecutionContext()); List results = reciperun.getChangeset().getAllResults(); + final String changeAnnotationId = UUID.randomUUID().toString(); List> edits = results.stream().filter(res -> res.getAfter() != null).map(res -> { URI docUri = res.getAfter().getSourcePath().isAbsolute() ? res.getAfter().getSourcePath().toUri() : project.getLocationUri().resolve(res.getAfter().getSourcePath().toString()); TextDocument doc = server.getTextDocumentService().getLatestSnapshot(docUri.toASCIIString()); if (doc == null) { doc = new TextDocument(docUri.toASCIIString(), LanguageId.JAVA, 0, res.getBefore() == null ? "" : res.getBefore().printAll()); } - return ORDocUtils.computeTextDocEdit(doc, res); + return ORDocUtils.computeTextDocEdit(doc, res, changeAnnotationId); }).filter(e -> e.isPresent()).map(e -> e.get()).map(e -> Either.forLeft(e)).collect(Collectors.toList()); if (edits.isEmpty()) { return null; } WorkspaceEdit workspaceEdit = new WorkspaceEdit(); + ChangeAnnotation changeAnnotation = new ChangeAnnotation("Applying Recipe '" + r.getDisplayName() + "'"); + changeAnnotation.setNeedsConfirmation(needsConfirmation); + workspaceEdit.setChangeAnnotations(Map.of(changeAnnotationId, changeAnnotation)); workspaceEdit.setDocumentChanges(edits); return workspaceEdit; } @@ -155,7 +161,7 @@ public class RewriteRefactorings implements CodeActionResolver, QuickfixHandler PercentageProgressTask progress = server.getProgressService().createPercentageProgressTask(UUID.randomUUID().toString(), inputs.size() + 1, data.getLabel()); try { cus = ORAstUtils.parseInputs(jp, inputs, s -> progress.increment()); - return applyRecipe(r, project.get(), cus); + return applyRecipe(r, project.get(), cus, false); } finally { progress.setCurrent(progress.getTotal()); progress.done(); @@ -164,7 +170,7 @@ public class RewriteRefactorings implements CodeActionResolver, QuickfixHandler JavaParser jp = ORAstUtils.createJavaParserBuilder(project.get()).dependsOn(data.getTypeStubs()).build(); List inputs = data.getDocUris().stream().map(URI::create).map(Paths::get).map(p -> ORAstUtils.getParserInput(server.getTextDocumentService(), p)).collect(Collectors.toList()); cus = ORAstUtils.parseInputs(jp, inputs, null); - return applyRecipe(r, project.get(), cus); + return applyRecipe(r, project.get(), cus, false); } }); } diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/rewrite/SpringBootUpgrade.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/rewrite/SpringBootUpgrade.java index 42136537a..a2f7aae92 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/rewrite/SpringBootUpgrade.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/rewrite/SpringBootUpgrade.java @@ -58,6 +58,7 @@ public class SpringBootUpgrade { Assert.isLegal(uri != null, "Project URI parameter must not be 'null'"); Version targetVersion = Version.parse(((JsonElement) params.getArguments().get(1)).getAsString()); Assert.isLegal(targetVersion != null, "Target Spring Boot version must not be 'null'"); + boolean needsConfirmation = params.getArguments().size() > 2 ? ((JsonElement) params.getArguments().get(2)).getAsBoolean() : false; IJavaProject project = projectFinder.find(new TextDocumentIdentifier(uri)).orElse(null); Assert.isLegal(project != null, "No Spring Boot project found for uri: " + uri); @@ -75,7 +76,8 @@ public class SpringBootUpgrade { return recipeRepo.recipes().thenComposeAsync(recipes -> recipeRepo.apply( createUpgradeRecipe(recipes, version, targetVersion), uri, - UUID.randomUUID().toString() + UUID.randomUUID().toString(), + needsConfirmation )); }); } diff --git a/vscode-extensions/vscode-spring-boot/lib/rewrite.ts b/vscode-extensions/vscode-spring-boot/lib/rewrite.ts index a05ec30a8..6b4441b1c 100644 --- a/vscode-extensions/vscode-spring-boot/lib/rewrite.ts +++ b/vscode-extensions/vscode-spring-boot/lib/rewrite.ts @@ -97,6 +97,7 @@ async function showRefactorings(uri: VSCode.Uri, filter: string) { } const choices = await showCurrentPathQuickPick(VSCode.commands.executeCommand('sts/rewrite/list', uri.toString(true), filter).then((cmds: RecipeDescriptor[]) => cmds.map(convertToQuickPickItem)), []); const recipeDescriptors = choices.filter(i => i.selected).map(convertToRecipeDescriptor); + const needsConfirmation = await shwoNeedsConfirmation(); if (recipeDescriptors.length) { const aggregateRecipeDescriptor = recipeDescriptors.length === 1 ? recipeDescriptors[0] : { name: `${recipeDescriptors.length} recipes`, @@ -111,7 +112,7 @@ async function showRefactorings(uri: VSCode.Uri, filter: string) { if (aggregateRecipeDescriptor.estimatedEffortPerOccurrence === 0) { delete aggregateRecipeDescriptor.estimatedEffortPerOccurrence; } - VSCode.commands.executeCommand('sts/rewrite/execute', uri.toString(true), aggregateRecipeDescriptor); + VSCode.commands.executeCommand('sts/rewrite/execute', uri.toString(true), aggregateRecipeDescriptor, needsConfirmation); } else { VSCode.window.showErrorMessage('No Recipes were selected!'); } @@ -137,6 +138,33 @@ function convertToQuickPickItem(i: RecipeDescriptor): RecipeQuickPickItem { }; } +function shwoNeedsConfirmation(): Thenable { + return new Promise((resolve, reject) => { + const previewPick = VSCode.window.createQuickPick(); + previewPick.title = 'Preview Before Applying?'; + previewPick.canSelectMany = false; + + const applyItem = { + label: "Apply", + description: "Apply changes maded by a recipe" + } + const previewItem = { + label: "Preview", + description: "Preview and confirm changes made by a recipe before applying" + } + previewPick.items = [ + applyItem, + previewItem + ]; + previewPick.show(); + + previewPick.onDidAccept(() => { + previewPick.hide(); + resolve(previewPick.selectedItems[0] === previewItem); + }); + }); +} + function showCurrentPathQuickPick(itemsPromise: Thenable, itemsPath: RecipeQuickPickItem[]): Thenable { const quickPick = VSCode.window.createQuickPick(); quickPick.title = 'Loading Recipes...'; @@ -176,13 +204,8 @@ function showCurrentPathQuickPick(itemsPromise: Thenable, }); quickPick.onDidAccept(() => { currentItems.forEach(i => i.selected = quickPick.selectedItems.includes(i)); - if (itemsPath.length) { - itemsPath.pop(); - showCurrentPathQuickPick(Promise.resolve(items), itemsPath).then(resolve, reject); - } else { - quickPick.hide(); - resolve(items); - } + quickPick.hide(); + resolve(items); }); quickPick.onDidChangeSelection(selected => { currentItems.forEach(i => {