From 184e77afae3e868159428632efcbbffa254b4fa2 Mon Sep 17 00:00:00 2001 From: BoykoAlex Date: Tue, 31 May 2022 10:14:33 -0400 Subject: [PATCH] Hierarchical Recipes UI for vscode --- .../rewrite/spring-boot3-config-upgrade.yml | 2 +- .../META-INF/rewrite/spring-boot3-upgrade.yml | 15 +- .../META-INF/rewrite/spring-data3-upgrade.yml | 12 +- .../java/rewrite/RewriteRecipeRepository.java | 154 +++++++++++++----- .../vscode-spring-boot/lib/rewrite.ts | 129 +++++++++++++-- 5 files changed, 250 insertions(+), 62 deletions(-) diff --git a/headless-services/commons/commons-rewrite/src/main/resources/META-INF/rewrite/spring-boot3-config-upgrade.yml b/headless-services/commons/commons-rewrite/src/main/resources/META-INF/rewrite/spring-boot3-config-upgrade.yml index f954384ed..cb68d4b00 100644 --- a/headless-services/commons/commons-rewrite/src/main/resources/META-INF/rewrite/spring-boot3-config-upgrade.yml +++ b/headless-services/commons/commons-rewrite/src/main/resources/META-INF/rewrite/spring-boot3-config-upgrade.yml @@ -15,7 +15,7 @@ # # --- type: specs.openrewrite.org/v1beta/recipe -name: org.openrewrite.java.spring.boot2.SpringBootProperties_3_0 +name: org.openrewrite.java.spring.boot3.SpringBootProperties_3_0 displayName: Migrate Spring Boot properties to 3.0 description: Migrate properties found in `application.properties` and `application.yml`. recipeList: diff --git a/headless-services/commons/commons-rewrite/src/main/resources/META-INF/rewrite/spring-boot3-upgrade.yml b/headless-services/commons/commons-rewrite/src/main/resources/META-INF/rewrite/spring-boot3-upgrade.yml index 7ea3e0682..38c1735c6 100644 --- a/headless-services/commons/commons-rewrite/src/main/resources/META-INF/rewrite/spring-boot3-upgrade.yml +++ b/headless-services/commons/commons-rewrite/src/main/resources/META-INF/rewrite/spring-boot3-upgrade.yml @@ -7,6 +7,17 @@ displayName: Upgrade to Spring Boot 3.0 from 2.x description: 'Upgrade to Spring Boot 3.0 from prior 2.x version.' recipeList: # Upgrade 3.0.x from 2.x + - org.openrewrite.java.spring.boot3.MavenPomUpgrade + - org.openrewrite.java.spring.boot3.data.UpgradeSpringData_3_0 +# - org.openrewrite.java.spring.boot3.SpringBootProperties_3_0 +--- +######################################################################################################################## +# SpringBoot 3_0 Maven Pom +type: specs.openrewrite.org/v1beta/recipe +name: org.openrewrite.java.spring.boot3.MavenPomUpgrade +displayName: Upgrade Maven Pom to Spring Boot 3.0 from 2.x +description: 'Upgrade Maven Pom to Spring Boot 3.0 from prior 2.x version.' +recipeList: - org.openrewrite.maven.UpgradeDependencyVersion: groupId: org.springframework.boot artifactId: "*" @@ -20,7 +31,5 @@ recipeList: key: 'java.version' newValue: 17 addIfMissing: true - - - org.openrewrite.java.spring.data.UpgradeSpringData_3_0 - - org.openrewrite.java.spring.boot2.SpringBootProperties_3_0 + \ No newline at end of file diff --git a/headless-services/commons/commons-rewrite/src/main/resources/META-INF/rewrite/spring-data3-upgrade.yml b/headless-services/commons/commons-rewrite/src/main/resources/META-INF/rewrite/spring-data3-upgrade.yml index b4153ba45..06540dac6 100644 --- a/headless-services/commons/commons-rewrite/src/main/resources/META-INF/rewrite/spring-data3-upgrade.yml +++ b/headless-services/commons/commons-rewrite/src/main/resources/META-INF/rewrite/spring-data3-upgrade.yml @@ -1,9 +1,19 @@ ######################################################################################################################## # Spring Data 3.0 type: specs.openrewrite.org/v1beta/recipe -name: org.openrewrite.java.spring.data.UpgradeSpringData_3_0 +name: org.openrewrite.java.spring.boot3.data.UpgradeSpringData_3_0 displayName: Upgrade to Spring Data 3.0 description: 'Upgrade to Spring Data to 3.0 from any prior version.' +recipeList: + - org.openrewrite.java.spring.boot3.data.SwitchToJakarta + +--- +######################################################################################################################## +# Spring Data 3.0 javax -> jakarta +type: specs.openrewrite.org/v1beta/recipe +name: org.openrewrite.java.spring.boot3.data.SwitchToJakarta +displayName: Switch to Jakarta JPA +description: 'Switch to Jakarta JPA from Javax' recipeList: - org.springframework.ide.vscode.commons.rewrite.maven.ChangeDependencyClassifier: groupId: org.ehcache 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 eef2e2f89..12a7f90b0 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 @@ -14,6 +14,7 @@ import java.io.File; import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.List; @@ -38,6 +39,7 @@ import org.openrewrite.Recipe; import org.openrewrite.Result; import org.openrewrite.SourceFile; import org.openrewrite.Validated; +import org.openrewrite.config.DeclarativeRecipe; import org.openrewrite.config.Environment; import org.openrewrite.java.JavaParser; import org.openrewrite.maven.MavenParser; @@ -54,6 +56,7 @@ import org.springframework.ide.vscode.commons.rewrite.maven.MavenProjectParser; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList.Builder; import com.google.common.collect.ImmutableMap; +import com.google.gson.Gson; import com.google.gson.JsonElement; public class RewriteRecipeRepository { @@ -135,6 +138,39 @@ public class RewriteRecipeRepository { }); listBuilder.add("sts/rewrite/list"); + server.onCommand("sts/rewrite/execute", params -> { + String uri = ((JsonElement) params.getArguments().get(0)).getAsString(); + JsonElement recipesJson = ((JsonElement) params.getArguments().get(1)); + RecipeDescriptor[] recipeDescriptors = new Gson().fromJson(recipesJson, RecipeDescriptor[].class); + List convertedRecipes = Arrays.stream(recipeDescriptors) + .filter(rd -> rd.selected) + .filter(rd -> recipes.containsKey(rd.id)) + .map(rd -> convert(recipes.get(rd.id), rd)) + .collect(Collectors.toList()); + if (convertedRecipes.isEmpty()) { + throw new RuntimeException("Not recipes to execute!"); + } else if (convertedRecipes.size() == 1) { + Recipe r = convertedRecipes.get(0); + String progressToken = params.getWorkDoneToken() == null || params.getWorkDoneToken().getLeft() == null ? r.getName() : params.getWorkDoneToken().getLeft(); + return apply(r, uri, progressToken); + } else { + Recipe multiRecipe = new Recipe() { + + @Override + public String getDisplayName() { + return convertedRecipes.size() + " recipes"; + } + + }; + for (Recipe r : convertedRecipes) { + multiRecipe.doNext(r); + } + String progressToken = params.getWorkDoneToken() == null || params.getWorkDoneToken().getLeft() == null ? multiRecipe.getName() : params.getWorkDoneToken().getLeft(); + return apply(multiRecipe, uri, progressToken); + } + }); + listBuilder.add("sts/rewrite/execute"); + for (Recipe r : globalCommandRecipes) { listBuilder.add(createGlobalCommand(r)); } @@ -159,44 +195,47 @@ public class RewriteRecipeRepository { String commandId = "sts/rewrite/recipe/" + r.getName(); server.onCommand(commandId, params -> { final String progressToken = params.getWorkDoneToken() == null || params.getWorkDoneToken().getLeft() == null ? r.getName() : params.getWorkDoneToken().getLeft(); - return CompletableFuture.supplyAsync(() -> { - JsonElement uri = (JsonElement) params.getArguments().get(0); - server.getProgressService().progressEvent(progressToken, r.getDisplayName() + ": initiated..."); - return projectFinder.find(new TextDocumentIdentifier(uri.getAsString())); - }).thenCompose(p -> { - if (p.isPresent()) { - try { - Optional edit = apply(r, p.get()); - return CompletableFuture.completedFuture(edit).thenCompose(we -> { - if (we.isPresent()) { - server.getProgressService().progressEvent(progressToken, - r.getDisplayName() + ": applying document changes..."); - return server.getClient().applyEdit(new ApplyWorkspaceEditParams(we.get(), r.getDisplayName())).thenCompose(res -> { - if (res.isApplied()) { - server.getProgressService().progressEvent(progressToken, null); - return CompletableFuture.completedFuture("success"); - } else { - server.getProgressService().progressEvent(progressToken, null); - return CompletableFuture.completedFuture(null); - } - }); - } else { - server.getProgressService().progressEvent(progressToken, null); - return CompletableFuture.completedFuture(null); - } - }); - } catch (Throwable t) { - server.getProgressService().progressEvent(progressToken, null); - throw t; - } - } - return CompletableFuture.completedFuture(null); - }); - + String uri = ((JsonElement) params.getArguments().get(0)).getAsString(); + return apply(r, uri, progressToken); }); return commandId; } + private CompletableFuture apply(Recipe r, String uri, String progressToken) { + return CompletableFuture.supplyAsync(() -> { + server.getProgressService().progressEvent(progressToken, r.getDisplayName() + ": initiated..."); + return projectFinder.find(new TextDocumentIdentifier(uri)); + }).thenCompose(p -> { + if (p.isPresent()) { + try { + Optional edit = apply(r, p.get()); + return CompletableFuture.completedFuture(edit).thenCompose(we -> { + if (we.isPresent()) { + server.getProgressService().progressEvent(progressToken, + r.getDisplayName() + ": applying document changes..."); + return server.getClient().applyEdit(new ApplyWorkspaceEditParams(we.get(), r.getDisplayName())).thenCompose(res -> { + if (res.isApplied()) { + server.getProgressService().progressEvent(progressToken, null); + return CompletableFuture.completedFuture("success"); + } else { + server.getProgressService().progressEvent(progressToken, null); + return CompletableFuture.completedFuture(null); + } + }); + } else { + server.getProgressService().progressEvent(progressToken, null); + return CompletableFuture.completedFuture(null); + } + }); + } catch (Throwable t) { + server.getProgressService().progressEvent(progressToken, null); + throw t; + } + } + return CompletableFuture.completedFuture(null); + }); + } + private Optional apply(Recipe r, IJavaProject project) { Path absoluteProjectDir = Paths.get(project.getLocationUri()); server.getProgressService().progressEvent(r.getName(), r.getDisplayName() + ": parsing files..."); @@ -215,7 +254,7 @@ public class RewriteRecipeRepository { if (projectOpt.isPresent()) { List commandDescriptors = new ArrayList<>(globalCommandRecipes.size()); for (Recipe r : globalCommandRecipes) { - commandDescriptors.add(new RecipeDescriptor(r.getName(), r.getDisplayName(), r.getDescription())); + commandDescriptors.add(new RecipeDescriptor(r)); } return commandDescriptors; } @@ -247,17 +286,48 @@ public class RewriteRecipeRepository { .map(file -> file.getAbsoluteFile().toPath()).collect(Collectors.toList()); } } + + private static Recipe convert(Recipe r, RecipeDescriptor d) { + try { + if (d.selected) { + if (d.children != null && !d.children.isEmpty()) { + Recipe recipe = r instanceof DeclarativeRecipe ? new DeclarativeRecipe(r.getName(), r.getDisplayName(), r.getDescription(), r.getTags(), r.getEstimatedEffortPerOccurrence(), null) + : r.getClass().getDeclaredConstructor().newInstance(); + int i = 0; + for (Recipe sr : r.getRecipeList()) { + Recipe convertedSubRecipe = convert(sr, d.children.get(i++)); + if (convertedSubRecipe != null) { + recipe.doNext(convertedSubRecipe); + } + } + return recipe; + } else { + return r; + } + } + } catch (Exception e) { + log.error("", e); + } + return null; + } @SuppressWarnings("unused") private static class RecipeDescriptor { String id; String label; - String description; - public RecipeDescriptor(String id, String label, String description) { - this.id = id; - this.label = label; - this.description = description; - } + String detail; + List children; + boolean selected; + + RecipeDescriptor(Recipe r) { + this.id = r.getName(); + this.label = r.getDisplayName(); + this.detail = r.getDescription(); + List subRecipes = r.getRecipeList(); + if (r instanceof DeclarativeRecipe && !subRecipes.isEmpty() && (subRecipes.size() > 1 || subRecipes.get(0) instanceof DeclarativeRecipe)) { + this.children = r.getRecipeList().stream().map(sr -> new RecipeDescriptor(sr)).collect(Collectors.toList()); + } + } } - + } diff --git a/vscode-extensions/vscode-spring-boot/lib/rewrite.ts b/vscode-extensions/vscode-spring-boot/lib/rewrite.ts index 5241a05a9..d2817273a 100644 --- a/vscode-extensions/vscode-spring-boot/lib/rewrite.ts +++ b/vscode-extensions/vscode-spring-boot/lib/rewrite.ts @@ -6,11 +6,15 @@ import * as path from "path"; interface RewriteCommandInfo { id: string; label: string; - description: string + detail?: string; + children: RewriteCommandInfo[]; + selected: boolean; } -interface RewriteCommandQuickPickItem extends VSCode.QuickPickItem { +interface RecipeQuickPickItem extends VSCode.QuickPickItem{ id: string; + children: RecipeQuickPickItem[]; + selected: boolean; } function getWorkspaceFolderName(file: VSCode.Uri): string { @@ -20,7 +24,7 @@ function getWorkspaceFolderName(file: VSCode.Uri): string { return wf.name; } } - return ""; + return ''; } function getRelativePathToWorkspaceFolder(file: VSCode.Uri): string { @@ -30,7 +34,7 @@ function getRelativePathToWorkspaceFolder(file: VSCode.Uri): string { return path.relative(wf.uri.fsPath, file.fsPath); } } - return ""; + return ''; } async function getTargetPomXml(): Promise { @@ -55,24 +59,119 @@ async function getTargetPomXml(): Promise { return undefined; } +const ROOT_RECIPES_BUTTON: VSCode.QuickInputButton = { + iconPath: new VSCode.ThemeIcon('home'), + tooltip: 'Root Recipes' +} + +const SUB_RECIPES_BUTTON: VSCode.QuickInputButton = { + iconPath: new VSCode.ThemeIcon('type-hierarchy'), + tooltip: 'Sub-Recipes' +} async function liveHoverConnectHandler(uri: VSCode.Uri) { if (!uri) { uri = await getTargetPomXml(); } const cmds: RewriteCommandInfo[] = await VSCode.commands.executeCommand('sts/rewrite/list', uri.toString(true)); - const choices: RewriteCommandQuickPickItem[] = cmds.map(d => { - return { - id: d.id, - label: d.label, - description: d.description, - }; - }); - if (choices) { - const picked = await VSCode.window.showQuickPick(choices); - if (picked) { - VSCode.commands.executeCommand(`sts/rewrite/recipe/${picked.id}`, uri.toString(true)) + const choices = cmds.map(convertToQuickPickItem); + await showCurrentPathQuickPick(choices, []); + const recipesInfo = choices.filter(i => i.selected).map(convertToRecipeDescriptor); + if (recipesInfo.length) { + VSCode.commands.executeCommand('sts/rewrite/execute', uri.toString(true), recipesInfo); + } else { + VSCode.window.showErrorMessage('No Recipes were selected!'); + } +} + +function convertToRecipeDescriptor(i: RecipeQuickPickItem): RewriteCommandInfo { + return { + id: i.id, + label: i.label, + selected: i.selected, + children: i.children.map(convertToRecipeDescriptor) + }; +} + +function convertToQuickPickItem(i: RewriteCommandInfo): RecipeQuickPickItem { + return { + id: i.id, + label: i.label, + detail: i.detail, + selected: i.selected, + children: i.children ? i.children.map(convertToQuickPickItem) : [], + buttons: i.children && i.children.length ? [ SUB_RECIPES_BUTTON ] : undefined, + }; +} + +function showCurrentPathQuickPick(items: RecipeQuickPickItem[], itemsPath: RecipeQuickPickItem[]): Promise { + return new Promise((resolve, reject) => { + let currentItems = items; + let parent: RecipeQuickPickItem | undefined; + itemsPath.forEach(p => { + parent = currentItems.find(i => i === p); + currentItems = parent.children; + }); + const quickPick = VSCode.window.createQuickPick(); + quickPick.items = currentItems; + quickPick.title = 'Select Recipes'; + quickPick.canSelectMany = true; + if (itemsPath.length) { + quickPick.buttons = [ ROOT_RECIPES_BUTTON ]; } + quickPick.selectedItems = currentItems.filter(i => i.selected); + quickPick.onDidTriggerItemButton(e => { + if (e.button === SUB_RECIPES_BUTTON) { + currentItems.forEach(i => i.selected = quickPick.selectedItems.includes(i)); + itemsPath.push(e.item); + showCurrentPathQuickPick(items, itemsPath).then(resolve, reject); + } + }); + quickPick.onDidTriggerButton(b => { + if (b === ROOT_RECIPES_BUTTON) { + currentItems.forEach(i => i.selected = quickPick.selectedItems.includes(i)); + itemsPath.splice(0, itemsPath.length); + showCurrentPathQuickPick(items, itemsPath).then(resolve, reject); + } + }); + quickPick.onDidAccept(() => { + currentItems.forEach(i => i.selected = quickPick.selectedItems.includes(i)); + if (itemsPath.length) { + itemsPath.pop(); + showCurrentPathQuickPick(items, itemsPath).then(resolve, reject); + } else { + quickPick.hide(); + resolve(); + } + }); + quickPick.onDidChangeSelection(selected => { + currentItems.forEach(i => { + const isSelectedItem = selected.includes(i); + if (i.selected !== isSelectedItem) { + selectItemRecursively(i, isSelectedItem); + } + }); + updateParentSelection(itemsPath.slice()); + }); + quickPick.show(); + }); +} + +function updateParentSelection(hierarchy: RecipeQuickPickItem[]): void { + if (hierarchy.length) { + const parent = hierarchy.pop(); + const isSelected = !!parent.children.find(i => i.selected); + if (parent.selected !== isSelected) { + parent.selected = isSelected; + updateParentSelection(hierarchy) + } + } +} + +function selectItemRecursively(i: RecipeQuickPickItem, isSelectedItem: boolean) { + i.selected = isSelectedItem; + if (i.children) { + i.children.forEach(c => selectItemRecursively(c, isSelectedItem)); } }