Integrate needsConfirmation

This commit is contained in:
aboyko
2023-12-05 16:33:10 -05:00
parent 44310b3b45
commit 1539bebde3
5 changed files with 73 additions and 28 deletions

View File

@@ -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<TextDocumentEdit> computeTextDocEdit(TextDocument doc, Result result) {
public static Optional<TextDocumentEdit> 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<WorkspaceEdit> createWorkspaceEdit(Path absoluteProjectDir, SimpleTextDocumentService documents, List<Result> results) {
public static Optional<WorkspaceEdit> createWorkspaceEdit(Path absoluteProjectDir, SimpleTextDocumentService documents, List<Result> 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)));
}
}

View File

@@ -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<Object> apply(Recipe r, String uri, String progressToken) {
CompletableFuture<Object> 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<WorkspaceEdit> edit = computeWorkspaceEdit(r, p.get(), progressTask);
Optional<WorkspaceEdit> 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<WorkspaceEdit> computeWorkspaceEdit(Recipe r, IJavaProject project, IndefiniteProgressTask progressTask) {
private Optional<WorkspaceEdit> 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<Result> 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<ParseError> parseErrors) {

View File

@@ -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<CompilationUnit> cus) {
private WorkspaceEdit applyRecipe(Recipe r, IJavaProject project, List<CompilationUnit> cus, boolean needsConfirmation) {
List<SourceFile> sources = cus.stream().map(cu -> (SourceFile) cu).collect(Collectors.toList());
RecipeRun reciperun = r.run(new InMemoryLargeSourceSet(sources), new InMemoryExecutionContext());
List<Result> results = reciperun.getChangeset().getAllResults();
final String changeAnnotationId = UUID.randomUUID().toString();
List<Either<TextDocumentEdit, ResourceOperation>> 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.<TextDocumentEdit, ResourceOperation>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<Input> 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);
}
});
}

View File

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

View File

@@ -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<boolean> {
return new Promise((resolve, reject) => {
const previewPick = VSCode.window.createQuickPick<VSCode.QuickPickItem>();
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<RecipeQuickPickItem[]>, itemsPath: RecipeQuickPickItem[]): Thenable<RecipeQuickPickItem[]> {
const quickPick = VSCode.window.createQuickPick<RecipeQuickPickItem>();
quickPick.title = 'Loading Recipes...';
@@ -176,13 +204,8 @@ function showCurrentPathQuickPick(itemsPromise: Thenable<RecipeQuickPickItem[]>,
});
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 => {