From d6ff60a96e0701b3972350a5b4271bab48c6df98 Mon Sep 17 00:00:00 2001 From: aboyko Date: Mon, 18 Dec 2023 20:54:59 -0500 Subject: [PATCH] Minimize size of messages for fetching and executing recipes --- .../.classpath | 2 +- .../.settings/org.eclipse.jdt.core.prefs | 6 +- .../boot/ls/commands/RecipeDescriptor.java | 64 +++---- .../boot/ls/commands/RecipeTreeModel.java | 162 +++++++++------- .../commands/RewriteRefactoringsHandler.java | 60 ++---- .../boot/ls/commands/SelectRecipesDialog.java | 55 ++++-- .../ide/vscode/commons/rewrite/LoadUtils.java | 10 +- .../java/rewrite/RewriteRecipeRepository.java | 175 ++++++++++-------- .../rewrite/RewriteRecipeRepositoryTest.java | 134 ++++++++++++++ .../vscode-spring-boot/lib/rewrite.ts | 60 +++--- .../vscode-spring-boot/package.json | 8 +- 11 files changed, 449 insertions(+), 287 deletions(-) create mode 100644 headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/rewrite/RewriteRecipeRepositoryTest.java diff --git a/eclipse-language-servers/org.springframework.tooling.boot.ls/.classpath b/eclipse-language-servers/org.springframework.tooling.boot.ls/.classpath index bc57d2383..5508535a4 100644 --- a/eclipse-language-servers/org.springframework.tooling.boot.ls/.classpath +++ b/eclipse-language-servers/org.springframework.tooling.boot.ls/.classpath @@ -1,6 +1,6 @@ - + diff --git a/eclipse-language-servers/org.springframework.tooling.boot.ls/.settings/org.eclipse.jdt.core.prefs b/eclipse-language-servers/org.springframework.tooling.boot.ls/.settings/org.eclipse.jdt.core.prefs index 7adc0fb9a..d4540a53f 100644 --- a/eclipse-language-servers/org.springframework.tooling.boot.ls/.settings/org.eclipse.jdt.core.prefs +++ b/eclipse-language-servers/org.springframework.tooling.boot.ls/.settings/org.eclipse.jdt.core.prefs @@ -1,10 +1,10 @@ eclipse.preferences.version=1 org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled -org.eclipse.jdt.core.compiler.codegen.targetPlatform=11 -org.eclipse.jdt.core.compiler.compliance=11 +org.eclipse.jdt.core.compiler.codegen.targetPlatform=17 +org.eclipse.jdt.core.compiler.compliance=17 org.eclipse.jdt.core.compiler.problem.assertIdentifier=error org.eclipse.jdt.core.compiler.problem.enablePreviewFeatures=disabled org.eclipse.jdt.core.compiler.problem.enumIdentifier=error org.eclipse.jdt.core.compiler.problem.reportPreviewFeatures=warning org.eclipse.jdt.core.compiler.release=enabled -org.eclipse.jdt.core.compiler.source=11 +org.eclipse.jdt.core.compiler.source=17 diff --git a/eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/commands/RecipeDescriptor.java b/eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/commands/RecipeDescriptor.java index 2ab085134..fd9eb047d 100644 --- a/eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/commands/RecipeDescriptor.java +++ b/eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/commands/RecipeDescriptor.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2022 VMware, Inc. + * Copyright (c) 2022, 2023 VMware, Inc. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -10,12 +10,16 @@ *******************************************************************************/ package org.springframework.tooling.boot.ls.commands; -import java.net.URI; -import java.time.Duration; import java.util.List; import java.util.Set; -class RecipeDescriptor { +final class RecipeDescriptor { + + enum CheckedState { + UNCHECKED, + CHECKED, + GRAYED + } String name; @@ -25,45 +29,29 @@ class RecipeDescriptor { Set tags; - Duration estimatedEffortPerOccurrence; - List options; List languages; List recipeList; - - URI source; - RecipeDescriptor getCopyWithoutSubRecipes() { - RecipeDescriptor copy = new RecipeDescriptor(); - copy.name = name; - copy.displayName = displayName; - copy.description = description; - copy.tags = tags; - copy.estimatedEffortPerOccurrence = estimatedEffortPerOccurrence; - copy.options = options; - copy.languages = languages; - copy.source = source; - return copy; - } - - static class OptionDescriptor { + boolean hasSubRecipes = false; + + RecipeDescriptor parent; + + CheckedState checked = CheckedState.UNCHECKED; - String name; - - String type; - - String displayName; - - String description; - - String example; - - List valid; - - boolean required; - - Object value; - } + record OptionDescriptor( + String name, + String type, + String displayName, + String description, + String example, + List valid, + boolean required, + Object value + ) {} + + record RecipeSelection(boolean selected, String id, RecipeSelection[] subselection) {} + } \ No newline at end of file diff --git a/eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/commands/RecipeTreeModel.java b/eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/commands/RecipeTreeModel.java index 318c553e7..10324852f 100644 --- a/eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/commands/RecipeTreeModel.java +++ b/eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/commands/RecipeTreeModel.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2022 VMware, Inc. + * Copyright (c) 2022, 2023 VMware, Inc. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -10,55 +10,48 @@ *******************************************************************************/ package org.springframework.tooling.boot.ls.commands; +import java.util.ArrayList; import java.util.Arrays; -import java.util.IdentityHashMap; -import java.util.Map; -import java.util.Objects; -import java.util.stream.Collectors; +import java.util.LinkedList; +import java.util.List; +import java.util.concurrent.CompletableFuture; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.Status; +import org.eclipse.lsp4j.ExecuteCommandParams; +import org.eclipse.lsp4j.services.WorkspaceService; +import org.springframework.tooling.boot.ls.commands.RecipeDescriptor.CheckedState; +import org.springframework.tooling.boot.ls.commands.RecipeDescriptor.RecipeSelection; public class RecipeTreeModel { - public enum CheckedState { - UNCHECKED, - CHECKED, - GRAYED - } + private static final String REWRITE_REFACTORINGS_LIST = "sts/rewrite/list"; + private static final String REWRITE_REFACTORINGS_SUBLIST = "sts/rewrite/sublist"; private RecipeDescriptor[] recipeDescriptors; - private Map parentMap = new IdentityHashMap<>(); - private Map checkedMap = new IdentityHashMap<>(); - RecipeTreeModel(RecipeDescriptor[] recipeDescriptors) { - this.recipeDescriptors = recipeDescriptors; - for (RecipeDescriptor d : recipeDescriptors) { - initParentMap(d); - } - } - - private void initParentMap(RecipeDescriptor d) { - if (d.recipeList != null) { - for (RecipeDescriptor dc : d.recipeList) { - parentMap.put(dc, d); - initParentMap(dc); - } - } + final private WorkspaceService workspaceService; + final private String recipeFilter; + + RecipeTreeModel(WorkspaceService workspaceService, String recipeFilter) { + this.workspaceService = workspaceService; + this.recipeFilter = recipeFilter; } public void check(RecipeDescriptor d) { if (simpleCheck(d)) { - inferCheckedStateFromChildren(parentMap.get(d)); + inferCheckedStateFromChildren(d.parent); } } private boolean simpleCheck(RecipeDescriptor d) { - if (checkedMap.get(d) != CheckedState.CHECKED) { - checkedMap.put(d, CheckedState.CHECKED); - for (RecipeDescriptor dc : d.recipeList) { - simpleCheck(dc); + if (d.checked != CheckedState.CHECKED) { + d.checked = CheckedState.CHECKED; + if (d.recipeList != null) { + for (RecipeDescriptor dc : d.recipeList) { + simpleCheck(dc); + } } return true; } @@ -67,32 +60,29 @@ public class RecipeTreeModel { public void uncheck(RecipeDescriptor d) { if (simpleUncheck(d)) { - inferCheckedStateFromChildren(parentMap.get(d)); + inferCheckedStateFromChildren(d.parent); } } private boolean simpleUncheck(RecipeDescriptor d) { - if (checkedMap.get(d) != CheckedState.UNCHECKED) { - checkedMap.put(d, CheckedState.UNCHECKED); - for (RecipeDescriptor dc : d.recipeList) { - simpleUncheck(dc); + if (d.checked != CheckedState.UNCHECKED) { + d.checked = CheckedState.UNCHECKED; + if (d.recipeList != null) { + for (RecipeDescriptor dc : d.recipeList) { + simpleUncheck(dc); + } } return true; } return false; } - public CheckedState getCheckedState(RecipeDescriptor d) { - CheckedState state = checkedMap.get(d); - return state == null ? CheckedState.UNCHECKED : state; - } - private void inferCheckedStateFromChildren(RecipeDescriptor d) { if (d != null && d.recipeList != null) { boolean all = true; boolean none = true; for (RecipeDescriptor child : d.recipeList) { - CheckedState childState = getCheckedState(child); + CheckedState childState = child.checked; if (childState == CheckedState.UNCHECKED) { all = false; } else { @@ -105,9 +95,9 @@ public class RecipeTreeModel { } else if (none) { inferredState = CheckedState.UNCHECKED; } - if (getCheckedState(d) != inferredState) { - checkedMap.put(d, inferredState); - inferCheckedStateFromChildren(parentMap.get(d)); + if (d.checked != inferredState) { + d.checked = inferredState; + inferCheckedStateFromChildren(d.parent); } } } @@ -116,32 +106,70 @@ public class RecipeTreeModel { return recipeDescriptors; } - public RecipeDescriptor getSelectedRecipeDescriptors() throws CoreException { - RecipeDescriptor[] recipes = Arrays.stream(recipeDescriptors).map(this::copySelectedDescriptor).filter(Objects::nonNull).toArray(RecipeDescriptor[]::new); - if (recipes.length == 0) { - throw new CoreException(Status.error("No recipes selected")); - } else if (recipes.length == 1) { - return recipes[0]; - } else { - RecipeDescriptor aggregate = new RecipeDescriptor(); - aggregate.name = recipes.length + " recipes"; - aggregate.displayName = recipes.length + " recipes"; - aggregate.description = "Multiple recipes to be applied. Number of recipes " + recipes.length; - aggregate.tags = Arrays.stream(recipes).flatMap(r -> r.tags.stream()).collect(Collectors.toSet()); - aggregate.recipeList = Arrays.asList(recipes); - return aggregate; + public RecipeSelection[] getRecipeSelection() throws CoreException { + List rootSelected = new ArrayList<>(); + for (int i = 0; i < recipeDescriptors.length; i++) { + if (recipeDescriptors[i].checked != CheckedState.UNCHECKED) { + rootSelected.add(new RecipeSelection(true, recipeDescriptors[i].name, createRecipeSelection(recipeDescriptors[i]))); + } } + if (rootSelected.isEmpty()) { + throw new CoreException(Status.error("No recipes selected")); + } + return rootSelected.toArray(new RecipeSelection[rootSelected.size()]); } - private RecipeDescriptor copySelectedDescriptor(RecipeDescriptor d) { - if (getCheckedState(d) != CheckedState.UNCHECKED) { - RecipeDescriptor copy = d.getCopyWithoutSubRecipes(); - if (d.recipeList != null) { - copy.recipeList = d.recipeList.stream().map(this::copySelectedDescriptor).filter(Objects::nonNull).collect(Collectors.toList()); - } - return copy; + private RecipeSelection[] createRecipeSelection(RecipeDescriptor d) { + if (d.recipeList != null) { + return d.recipeList.stream() + .map(s -> new RecipeSelection(s.checked != CheckedState.UNCHECKED, s.name, createRecipeSelection(s))) + .toArray(RecipeSelection[]::new); } return null; } - + + CompletableFuture fetchSubrecipes(RecipeDescriptor descriptor) { + RecipeDescriptor d = descriptor; + LinkedList indexPath = new LinkedList<>(); + for (; d.parent != null; d = d.parent) { + indexPath.addFirst(d.parent.recipeList.indexOf(d)); + } + ExecuteCommandParams commandParams = new ExecuteCommandParams(); + commandParams.setCommand(REWRITE_REFACTORINGS_SUBLIST); + commandParams.setArguments(List.of(d.name, indexPath)); + return workspaceService.executeCommand(commandParams).thenAccept(json -> { + RecipeDescriptor[] fetchedDescriptors = RewriteRefactoringsHandler.SERIALIZATION_GSON.fromJson(RewriteRefactoringsHandler.SERIALIZATION_GSON.toJson(json), RecipeDescriptor[].class); + for (RecipeDescriptor fd : fetchedDescriptors) { + fd.parent = descriptor; + fd.checked = descriptor.checked != CheckedState.UNCHECKED ? CheckedState.CHECKED : CheckedState.UNCHECKED; + } + descriptor.recipeList = Arrays.asList(fetchedDescriptors); + }); + } + + CompletableFuture fetchRootRecipes() { + ExecuteCommandParams commandParams = new ExecuteCommandParams(); + commandParams.setCommand(REWRITE_REFACTORINGS_LIST); + commandParams.setArguments(List.of(recipeFilter)); + return workspaceService.executeCommand(commandParams).thenAccept(json -> { + recipeDescriptors = RewriteRefactoringsHandler.SERIALIZATION_GSON.fromJson(RewriteRefactoringsHandler.SERIALIZATION_GSON.toJson(json), RecipeDescriptor[].class); + }); + } + + String getSelectedRecipeDisplayName() { + List rootSelected = new ArrayList<>(); + for (int i = 0; i < recipeDescriptors.length; i++) { + if (recipeDescriptors[i].checked != CheckedState.UNCHECKED) { + rootSelected.add(recipeDescriptors[i]); + } + } + if (rootSelected.isEmpty()) { + return "No Recipes Selected"; + } else if (rootSelected.size() == 1) { + return rootSelected.get(0).displayName; + } else { + return "%s recipes".formatted(rootSelected.size()); + } + } + } diff --git a/eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/commands/RewriteRefactoringsHandler.java b/eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/commands/RewriteRefactoringsHandler.java index f0fb5f86a..113e599ad 100644 --- a/eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/commands/RewriteRefactoringsHandler.java +++ b/eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/commands/RewriteRefactoringsHandler.java @@ -11,10 +11,6 @@ package org.springframework.tooling.boot.ls.commands; import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Type; -import java.time.Duration; -import java.util.ArrayList; -import java.util.Collections; import java.util.List; import org.eclipse.core.commands.AbstractHandler; @@ -37,17 +33,11 @@ import org.eclipse.swt.widgets.Display; import org.eclipse.ui.PlatformUI; import org.eclipse.ui.handlers.HandlerUtil; import org.springframework.tooling.boot.ls.BootLanguageServerPlugin; +import org.springframework.tooling.boot.ls.commands.RecipeDescriptor.RecipeSelection; import org.springsource.ide.eclipse.commons.core.CoreUtil; import com.google.gson.Gson; import com.google.gson.GsonBuilder; -import com.google.gson.JsonDeserializationContext; -import com.google.gson.JsonDeserializer; -import com.google.gson.JsonElement; -import com.google.gson.JsonParseException; -import com.google.gson.JsonPrimitive; -import com.google.gson.JsonSerializationContext; -import com.google.gson.JsonSerializer; @SuppressWarnings("restriction") public class RewriteRefactoringsHandler extends AbstractHandler { @@ -58,25 +48,11 @@ public class RewriteRefactoringsHandler extends AbstractHandler { NON_BOOT_UPGRADE } - private static class DurationTypeConverter implements JsonSerializer, JsonDeserializer { - @Override - public JsonElement serialize(Duration src, Type srcType, JsonSerializationContext context) { - return new JsonPrimitive(src.toNanos()); - } - - @Override - public Duration deserialize(JsonElement json, Type type, JsonDeserializationContext context) - throws JsonParseException { - return Duration.ofNanos(json.getAsLong()); - } - } - - private static Gson serializationGson = new GsonBuilder() - .registerTypeAdapter(Duration.class, new DurationTypeConverter()) + static final Gson SERIALIZATION_GSON = new GsonBuilder() + .setPrettyPrinting() .create(); - private static final String REWRITE_REFACTORINGS_LIST = "sts/rewrite/list"; private static final String REWRITE_REFACTORINGS_EXEC = "sts/rewrite/execute"; private RecipeFilter recipeFilter; @@ -106,41 +82,26 @@ public class RewriteRefactoringsHandler extends AbstractHandler { if (project != null && CoreUtil.promptForProjectSave(project)) { LanguageServerDefinition def = LanguageServersRegistry.getInstance().getDefinition(BootLanguageServerPlugin.BOOT_LS_DEFINITION_ID); Assert.isLegal(def != null, "No definition found for Boot Language Server"); - final String uri = project.getLocationURI().toASCIIString(); - ExecuteCommandParams commandParams = new ExecuteCommandParams(); - commandParams.setCommand(REWRITE_REFACTORINGS_LIST); - commandParams.setArguments(List.of(uri, recipeFilter.toString())); - try { - List allRewriteRecipesJson = new ArrayList<>(); - List syncRecipesJson = Collections.synchronizedList(allRewriteRecipesJson); - LanguageServers.forProject(project).withPreferredServer(def).computeFirst(ls -> - ls.getWorkspaceService().executeCommand(commandParams).thenAccept(or -> { - if (or != null) { - syncRecipesJson.add(or); - } - }) - .thenRun(() -> { - allRewriteRecipesJson.stream().filter(List.class::isInstance).map(List.class::cast).findFirst().ifPresent(obj -> { - RecipeDescriptor[] descriptors = serializationGson.fromJson(serializationGson.toJson(obj), RecipeDescriptor[].class); + LanguageServers.forProject(project).withPreferredServer(def).computeFirst(ls -> { PlatformUI.getWorkbench().getDisplay().asyncExec(() -> { - RecipeTreeModel recipesModel = new RecipeTreeModel(descriptors); + RecipeTreeModel recipesModel = new RecipeTreeModel(ls.getWorkspaceService(), recipeFilter.toString()); int returnCode = new SelectRecipesDialog(Display.getCurrent().getActiveShell(), recipesModel).open(); if (returnCode == Window.OK) { try { - RecipeDescriptor recipeToApply = recipesModel.getSelectedRecipeDescriptors(); + final RecipeSelection[] recipeSelection = recipesModel.getRecipeSelection(); PlatformUI.getWorkbench().getProgressService().run(true, false, monitor -> { try { - monitor.beginTask("Applying recipe '" + recipeToApply.displayName + "'", IProgressMonitor.UNKNOWN); + monitor.beginTask("Applying recipe '%s'...".formatted(recipesModel.getSelectedRecipeDisplayName()), IProgressMonitor.UNKNOWN); ExecuteCommandParams cmdParams = new ExecuteCommandParams(); cmdParams.setCommand(REWRITE_REFACTORINGS_EXEC); cmdParams.setArguments(List.of( uri, - serializationGson.toJsonTree(recipeToApply) + SERIALIZATION_GSON.toJsonTree(recipeSelection) )); ls.getWorkspaceService().executeCommand(cmdParams).get(); @@ -157,8 +118,9 @@ public class RewriteRefactoringsHandler extends AbstractHandler { } } }); - }); - })); + return null; + }); + } catch (Exception e) { throw new ExecutionException("Failed to apply Rewrite recipe(s)", e); } diff --git a/eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/commands/SelectRecipesDialog.java b/eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/commands/SelectRecipesDialog.java index 8719e76ec..efd9e7fb6 100644 --- a/eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/commands/SelectRecipesDialog.java +++ b/eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/commands/SelectRecipesDialog.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2022 VMware, Inc. + * Copyright (c) 2022, 2023 VMware, Inc. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -43,7 +43,7 @@ import org.eclipse.swt.widgets.Shell; import org.eclipse.ui.PlatformUI; import org.osgi.framework.FrameworkUtil; import org.springframework.tooling.boot.ls.commands.RecipeDescriptor.OptionDescriptor; -import org.springframework.tooling.boot.ls.commands.RecipeTreeModel.CheckedState; +import org.springframework.tooling.boot.ls.commands.RecipeDescriptor.CheckedState; @SuppressWarnings("restriction") public class SelectRecipesDialog extends StatusDialog { @@ -51,6 +51,7 @@ public class SelectRecipesDialog extends StatusDialog { private static final int MARGIN = 5; private static final String SELECT_REWRITE_RECIPE_S_FROM_THE_LIST = "Select Rewrite Recipe(s) from the list"; private static String fgStyleSheet; + private static final Object LOADING = new Object(); private RecipeTreeModel model; @@ -76,37 +77,55 @@ public class SelectRecipesDialog extends StatusDialog { @Override public Object[] getElements(Object inputElement) { - if (inputElement instanceof RecipeTreeModel) { - return ((RecipeTreeModel) inputElement).getRecipeDescriptors(); + if (inputElement instanceof RecipeTreeModel model) { + if (model.getRecipeDescriptors() == null) { + model.fetchRootRecipes().thenAccept(v -> { + PlatformUI.getWorkbench().getDisplay().asyncExec(() -> treeViewer.refresh()); + }); + return new Object[] { LOADING }; + } else { + return model.getRecipeDescriptors(); + } + } return new Object[0]; } @Override public Object[] getChildren(Object parentElement) { - if (parentElement instanceof RecipeDescriptor) { - RecipeDescriptor r = (RecipeDescriptor) parentElement; - return r.recipeList.toArray(new RecipeDescriptor[r.recipeList.size()]); + if (parentElement instanceof RecipeDescriptor r) { + if (r.hasSubRecipes) { + if (r.recipeList == null) { + model.fetchSubrecipes(r).thenAccept(v -> PlatformUI.getWorkbench().getDisplay().asyncExec(() -> treeViewer.refresh(r))); + return new Object[] { LOADING }; + } else { + return r.recipeList.toArray(new RecipeDescriptor[r.recipeList.size()]); + } + } } return new RecipeDescriptor[0]; } @Override public Object getParent(Object element) { + if (element instanceof RecipeDescriptor r) { + return r.parent; + } return null; } @Override public boolean hasChildren(Object element) { - if (element instanceof RecipeDescriptor) { - RecipeDescriptor r = (RecipeDescriptor) element; - return r.recipeList != null && !r.recipeList.isEmpty(); + if (element instanceof RecipeDescriptor r) { + return r.hasSubRecipes; } return false; } }); treeViewer.setLabelProvider(LabelProvider.createTextProvider(input -> { - if (input instanceof RecipeDescriptor) { + if (input == LOADING) { + return "Loading..."; + } else if (input instanceof RecipeDescriptor) { return ((RecipeDescriptor)input).displayName; } return "unknown"; @@ -117,7 +136,7 @@ public class SelectRecipesDialog extends StatusDialog { public boolean isGrayed(Object element) { if (element instanceof RecipeDescriptor) { RecipeDescriptor r = (RecipeDescriptor) element; - return model.getCheckedState(r) == CheckedState.GRAYED; + return r.checked == CheckedState.GRAYED; } return false; } @@ -126,7 +145,7 @@ public class SelectRecipesDialog extends StatusDialog { public boolean isChecked(Object element) { if (element instanceof RecipeDescriptor) { RecipeDescriptor r = (RecipeDescriptor) element; - return model.getCheckedState(r) != CheckedState.UNCHECKED; + return r.checked != CheckedState.UNCHECKED; } return false; } @@ -168,7 +187,7 @@ public class SelectRecipesDialog extends StatusDialog { // Replace browser's built-in context menu with none docViewer.setMenu(new Menu(getShell(), SWT.NONE)); - docViewer.setText(wrapHtml("Select a Recipe on the left to read description")); + docViewer.setText(wrapHtml("Select a Recipe on the left to read description")); treeViewer.addSelectionChangedListener(new ISelectionChangedListener() { @@ -200,7 +219,7 @@ public class SelectRecipesDialog extends StatusDialog { } private void updateStatus() { - boolean anythingSelected = Arrays.stream(model.getRecipeDescriptors()).anyMatch(d -> model.getCheckedState(d) != CheckedState.UNCHECKED); + boolean anythingSelected = model.getRecipeDescriptors() != null && Arrays.stream(model.getRecipeDescriptors()).anyMatch(d -> d.checked != CheckedState.UNCHECKED); updateStatus(anythingSelected ? Status.info(SELECT_REWRITE_RECIPE_S_FROM_THE_LIST) : Status.error(SELECT_REWRITE_RECIPE_S_FROM_THE_LIST)); } @@ -211,12 +230,12 @@ public class SelectRecipesDialog extends StatusDialog { sb.append("

"); sb.append("
    "); for (OptionDescriptor option : r.options) { - if (option.value != null) { + if (option.value() != null) { sb.append("
  • "); sb.append("
    ");
    -				sb.append(option.value);
    +				sb.append(option.value());
     				sb.append("
    "); - sb.append(option.description); + sb.append(option.description()); sb.append("
  • "); } } diff --git a/headless-services/commons/commons-rewrite/src/main/java/org/springframework/ide/vscode/commons/rewrite/LoadUtils.java b/headless-services/commons/commons-rewrite/src/main/java/org/springframework/ide/vscode/commons/rewrite/LoadUtils.java index b07f0d6d4..1e4cd42f7 100644 --- a/headless-services/commons/commons-rewrite/src/main/java/org/springframework/ide/vscode/commons/rewrite/LoadUtils.java +++ b/headless-services/commons/commons-rewrite/src/main/java/org/springframework/ide/vscode/commons/rewrite/LoadUtils.java @@ -52,12 +52,18 @@ public class LoadUtils { } public static Recipe createRecipe(RecipeDescriptor d, Function> getRecipeClass) { + return createRecipe(d, getRecipeClass, false); + } + + public static Recipe createRecipe(RecipeDescriptor d, Function> getRecipeClass, boolean shallow) { Class recipeClazz = getRecipeClass == null ? null : getRecipeClass.apply(d.getName()); if (recipeClazz == null || DeclarativeRecipe.class.getName().equals(recipeClazz.getName())) { DeclarativeRecipe recipe = new DeclarativeRecipe(d.getName(), d.getDisplayName(), d.getDescription(), d.getTags(), d.getEstimatedEffortPerOccurrence(), d.getSource(), false, d.getMaintainers()); - for (RecipeDescriptor subDescriptor : d.getRecipeList()) { - recipe.getRecipeList().add(createRecipe(subDescriptor, getRecipeClass)); + if (!shallow) { + for (RecipeDescriptor subDescriptor : d.getRecipeList()) { + recipe.getRecipeList().add(createRecipe(subDescriptor, getRecipeClass)); + } } return recipe; } else { 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 483bc75d4..23621ee15 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 @@ -18,6 +18,7 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.time.Duration; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.List; @@ -72,6 +73,7 @@ import org.springframework.ide.vscode.commons.util.text.TextDocument; import com.google.gson.Gson; import com.google.gson.GsonBuilder; +import com.google.gson.JsonArray; import com.google.gson.JsonElement; public class RewriteRecipeRepository { @@ -99,6 +101,7 @@ public class RewriteRecipeRepository { private static final String CMD_REWRITE_RELOAD = "sts/rewrite/reload"; private static final String CMD_REWRITE_EXECUTE = "sts/rewrite/execute"; private static final String CMD_REWRITE_LIST = "sts/rewrite/list"; + private static final String CMD_REWRITE_SUBLIST = "sts/rewrite/sublist"; private static final String CMD_REWRITE_RECIPE_EXECUTE = "sts/rewrite/recipe/execute"; private static final Logger log = LoggerFactory.getLogger(RewriteRecipeRepository.class); @@ -116,8 +119,9 @@ public class RewriteRecipeRepository { private Set scanDirs; private Set recipeFilters; - private static Gson serializationGson = new GsonBuilder() + static final Gson serializationGson = new GsonBuilder() .registerTypeAdapter(Duration.class, new DurationTypeConverter()) + .setPrettyPrinting() .create(); public RewriteRecipeRepository(SimpleLanguageServer server, JavaProjectFinder projectFinder, BootJavaConfig config) { @@ -265,40 +269,117 @@ public class RewriteRecipeRepository { } private static JsonElement recipeToJson(Recipe r) { - JsonElement jsonElement = serializationGson.toJsonTree(r.getDescriptor()); + RecipeDescriptor descriptor = r.getDescriptor(); + JsonElement jsonElement = serializationGson.toJsonTree(Map.of( + "name", descriptor.getName(), + "displayName", descriptor.getDisplayName(), + "description", descriptor.getDescription(), + "options", descriptor.getOptions(), + "tags", descriptor.getTags(), + "hasSubRecipes", !descriptor.getRecipeList().isEmpty() + )); return jsonElement; } + CompletableFuture> getRootRecipes(Predicate rootFilter) { + return recipes().thenApply(recipesMap -> recipesMap.values().stream().filter(rootFilter).collect(Collectors.toList())); + } + + CompletableFuture> getSubRecipes(String rootRecipeId, List path) { + return recipes().thenApply(recipesMap -> { + Recipe recipe = recipesMap.get(rootRecipeId); + for (int i : path) { + if (i < recipe.getRecipeList().size()) { + recipe = recipe.getRecipeList().get(i); + } else { + return Collections.emptyList(); + } + } + return recipe == null ? Collections.emptyList() : recipe.getRecipeList(); + }); + } + + Recipe createRecipeFromSelection(Recipe original, RecipeSelectionDescriptor[] selection) { + if (selection == null) { + return original; + } else { + boolean sameSubrecipes = true; + List newSubRecipes = new ArrayList<>(selection.length); + for (int i = 0; i < selection.length; i++) { + if (selection[i].selected) { + Recipe originalSubRecipe = original.getRecipeList().get(i); + Recipe newSubRecipe = createRecipeFromSelection(originalSubRecipe, selection[i].subselection()); + newSubRecipes.add(newSubRecipe); + if (sameSubrecipes) { + sameSubrecipes = newSubRecipe == originalSubRecipe; + } + } else { + sameSubrecipes = false; + } + } + if (sameSubrecipes) { + return original; + } else { + @SuppressWarnings("unchecked") + Recipe newRecipe = LoadUtils.createRecipe(original.getDescriptor(), id -> { + try { + return (Class) Class.forName(id); + } catch (ClassNotFoundException e) { + return null; + } + }, true); + newRecipe.getRecipeList().addAll(newSubRecipes); + return newRecipe; + } + } + } + private void registerCommands() { server.onCommand(CMD_REWRITE_LIST, params -> { - JsonElement uri = (JsonElement) params.getArguments().get(0); - RecipeFilter f = params.getArguments().size() > 1 ? RecipeFilter.valueOf(((JsonElement) params.getArguments().get(1)).getAsString()) : RecipeFilter.ALL; - return listProjectRefactoringRecipes(uri.getAsString()).thenApply(recipes -> recipes.stream() - .filter(RECIPE_LIST_FILTERS.get(f)) - .map(RewriteRecipeRepository::recipeToJson) - .collect(Collectors.toList())); + RecipeFilter f = params.getArguments().size() > 0 ? RecipeFilter.valueOf(((JsonElement) params.getArguments().get(0)).getAsString()) : RecipeFilter.ALL; + + return getRootRecipes(r -> isAcceptableGlobalCommandRecipe(r) && RECIPE_LIST_FILTERS.get(f).test(r)).thenApply(recipes -> recipes.stream() + .map(RewriteRecipeRepository::recipeToJson) + .collect(Collectors.toList())); + }); + + server.onCommand(CMD_REWRITE_SUBLIST, params -> { + String rootRecipeId = ((JsonElement) params.getArguments().get(0)).getAsString(); + JsonArray path = (JsonArray) params.getArguments().get(1); + + return getSubRecipes(rootRecipeId, path.asList().stream().map(j -> j.getAsInt()).collect(Collectors.toList())).thenApply(recipes -> recipes.stream() + .map(RewriteRecipeRepository::recipeToJson) + .collect(Collectors.toList())); }); server.onCommand(CMD_REWRITE_EXECUTE, params -> { - return recipes().thenCompose(recipes -> { + return recipes().thenCompose(recipesMap -> { 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); - - Recipe aggregateRecipe = LoadUtils.createRecipe(d, id -> Optional.ofNullable(recipes.get(id)).map(r -> r.getClass()).orElse(null)); - - if (aggregateRecipe instanceof DeclarativeRecipe && aggregateRecipe.getRecipeList().isEmpty()) { - throw new RuntimeException("No recipes found to perform!"); - } else if (aggregateRecipe.getRecipeList().size() == 1) { - Recipe r = aggregateRecipe.getRecipeList().get(0); + RecipeSelectionDescriptor[] descriptors = serializationGson.fromJson(recipesJson, RecipeSelectionDescriptor[].class); + List recipes = Arrays.stream(descriptors).map(d -> createRecipeFromSelection(recipesMap.get(d.id()), d.subselection())).collect(Collectors.toList()); + if (recipes.size() == 1) { + Recipe r = recipes.get(0); String progressToken = params.getWorkDoneToken() == null || params.getWorkDoneToken().getLeft() == null ? (r.getName() == null ? UUID.randomUUID().toString() : r.getName()) : params.getWorkDoneToken().getLeft(); return apply(r, uri, progressToken, needsConfirmation); } else { + String name = recipes.size() + " recipes"; + DeclarativeRecipe aggregateRecipe = new DeclarativeRecipe( + name, + name, + recipes.stream().map(r -> r.getDescription()).collect(Collectors.joining("\n")), + recipes.stream().flatMap(r -> r.getTags().stream()).collect(Collectors.toSet()), + null, + null, + false, + Collections.emptyList() + ); + aggregateRecipe.getRecipeList().addAll(recipes); String progressToken = params.getWorkDoneToken() == null || params.getWorkDoneToken().getLeft() == null ? (aggregateRecipe.getName() == null ? UUID.randomUUID().toString() @@ -397,21 +478,6 @@ public class RewriteRecipeRepository { } } - private CompletableFuture> listProjectRefactoringRecipes(String uri) { - if (uri != null) { - /* - * When LS started on listing rewrite recipes project lookup may not find any projects as classpath might still be resolving. - * Therefore, it is best probably to list the available recipes and figure out if it is a Spring Boot project recipes is applied to - * and if not throw an exception. - */ -// Optional projectOpt = projectFinder.find(new TextDocumentIdentifier(uri)); -// if (projectOpt.isPresent()) { - return recipes().thenApply(recipes -> recipes.values().stream().filter(this::isAcceptableGlobalCommandRecipe).collect(Collectors.toList())); -// } - } - return CompletableFuture.completedFuture(Collections.emptyList()); - } - private static ProjectParser createRewriteProjectParser(IJavaProject jp, Function inputProvider) { switch (jp.getProjectBuild().getType()) { case ProjectBuild.MAVEN_PROJECT_TYPE: @@ -438,47 +504,6 @@ public class RewriteRecipeRepository { return s; } -// 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 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()); -// } -// } -// } - + record RecipeSelectionDescriptor(boolean selected, String id, RecipeSelectionDescriptor[] subselection) {}; + } diff --git a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/rewrite/RewriteRecipeRepositoryTest.java b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/rewrite/RewriteRecipeRepositoryTest.java new file mode 100644 index 000000000..08220e7af --- /dev/null +++ b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/rewrite/RewriteRecipeRepositoryTest.java @@ -0,0 +1,134 @@ +/******************************************************************************* + * Copyright (c) 2023 VMware, Inc. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * VMware, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.vscode.boot.java.rewrite; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.List; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.openrewrite.Recipe; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Import; +import org.springframework.ide.vscode.boot.bootiful.BootLanguageServerTest; +import org.springframework.ide.vscode.boot.bootiful.SymbolProviderTestConf; +import org.springframework.ide.vscode.boot.java.rewrite.RewriteRecipeRepository.RecipeSelectionDescriptor; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +@ExtendWith(SpringExtension.class) +@BootLanguageServerTest +@Import(SymbolProviderTestConf.class) +public class RewriteRecipeRepositoryTest { + + @Autowired RewriteRecipeRepository recipeRepo; + + @Test + void listSubRecipe() throws Exception { + List recipes = recipeRepo.getSubRecipes("org.openrewrite.java.spring.boot3.UpgradeSpringBoot_3_1", List.of()).get(); + assertEquals(8, recipes.size()); + + recipes = recipeRepo.getSubRecipes("org.openrewrite.java.spring.boot3.UpgradeSpringBoot_3_1", List.of(0, 2)).get(); + assertEquals(13, recipes.size()); + + recipes = recipeRepo.getSubRecipes("org.openrewrite.java.spring.boot3.UpgradeSpringBoot_3_1",List.of(0, 345)).get(); + assertEquals(0, recipes.size()); + + recipes = recipeRepo.getSubRecipes("Hohoho", List.of()).get(); + assertEquals(0, recipes.size()); + + } + + @Test + void listRootRecipes() throws Exception { + List recipes = recipeRepo.getRootRecipes(r -> true).get(); + assertThat(recipes.size()).isGreaterThan(5); + + recipes = recipeRepo.getRootRecipes(r -> r.getName().startsWith("Hohoho")).get(); + assertEquals(0, recipes.size()); + } + + @Test + void createRecipeFromSelectionDescriptor() throws Exception { + RecipeSelectionDescriptor descriptor = new RecipeSelectionDescriptor(true, "org.openrewrite.java.spring.boot3.UpgradeSpringBoot_3_1", new RecipeSelectionDescriptor[] { + new RecipeSelectionDescriptor(true, "org.openrewrite.java.spring.boot3.UpgradeSpringBoot_3_0", new RecipeSelectionDescriptor[] { // pick boot 3.0 + new RecipeSelectionDescriptor(true, "org.openrewrite.java.spring.boot2.UpgradeSpringBoot_2_7", null), // pick boot 2.7 + new RecipeSelectionDescriptor(false, "", null), + new RecipeSelectionDescriptor(true, "org.openrewrite.java.migrate.UpgradeToJava17", new RecipeSelectionDescriptor[] { // pick Java 17 + new RecipeSelectionDescriptor(true, "", null), // java 11 + new RecipeSelectionDescriptor(true, "", null), // java 17 + new RecipeSelectionDescriptor(false, "", null), + new RecipeSelectionDescriptor(false, "", null), + new RecipeSelectionDescriptor(false, "", null), + new RecipeSelectionDescriptor(false, "", null), + new RecipeSelectionDescriptor(false, "", null), + new RecipeSelectionDescriptor(true, "", null), // text blocks + new RecipeSelectionDescriptor(false, "", null), + new RecipeSelectionDescriptor(false, "", null), + new RecipeSelectionDescriptor(false, "", null), + new RecipeSelectionDescriptor(false, "", null), + new RecipeSelectionDescriptor(false, "", null), + }), + new RecipeSelectionDescriptor(false, "", null), + new RecipeSelectionDescriptor(false, "", null), + new RecipeSelectionDescriptor(false, "", null), + new RecipeSelectionDescriptor(false, "", null), + new RecipeSelectionDescriptor(false, "", null), + new RecipeSelectionDescriptor(false, "", null), + new RecipeSelectionDescriptor(false, "", null), + new RecipeSelectionDescriptor(false, "", null), + new RecipeSelectionDescriptor(false, "", null), + new RecipeSelectionDescriptor(false, "", null), + new RecipeSelectionDescriptor(false, "", null), + new RecipeSelectionDescriptor(false, "", null), + new RecipeSelectionDescriptor(false, "", null), + new RecipeSelectionDescriptor(false, "", null), + new RecipeSelectionDescriptor(false, "", null), + new RecipeSelectionDescriptor(false, "", null), + new RecipeSelectionDescriptor(false, "", null), + new RecipeSelectionDescriptor(false, "", null), + }), + new RecipeSelectionDescriptor(false, "", null), + new RecipeSelectionDescriptor(false, "", null), + new RecipeSelectionDescriptor(false, "", null), + new RecipeSelectionDescriptor(true, "", null), + new RecipeSelectionDescriptor(false, "", null), + new RecipeSelectionDescriptor(false, "", null) + }); + + Recipe boot31Recipes = recipeRepo.getRecipe("org.openrewrite.java.spring.boot3.UpgradeSpringBoot_3_1").get().get(); + + Recipe boot31 = recipeRepo.createRecipeFromSelection(boot31Recipes, descriptor.subselection()); + + assertEquals(2, boot31.getRecipeList().size()); + + Recipe security61 = boot31.getRecipeList().get(1); + assertThat(security61.getRecipeList().size()).isGreaterThan(5); + + Recipe boot30 = boot31.getRecipeList().get(0); + assertEquals("org.openrewrite.java.spring.boot3.UpgradeSpringBoot_3_0", boot30.getName()); + assertEquals(2, boot30.getRecipeList().size()); + + Recipe boot27 = boot30.getRecipeList().get(0); + assertThat(boot27.getRecipeList().size()).isGreaterThan(10); + + Recipe java17 = boot30.getRecipeList().get(1); + assertThat(java17.getName()).isEqualTo("org.openrewrite.java.migrate.UpgradeToJava17"); + assertThat(java17.getRecipeList().size()).isEqualTo(3); + + assertThat(java17.getRecipeList().get(0).getName()).isEqualTo("org.openrewrite.java.migrate.Java8toJava11"); + assertThat(java17.getRecipeList().get(1).getName()).isEqualTo("org.openrewrite.java.migrate.JavaVersion17"); + assertThat(java17.getRecipeList().get(2).getName()).isEqualTo("org.openrewrite.java.migrate.lang.UseTextBlocks"); + + } + +} diff --git a/vscode-extensions/vscode-spring-boot/lib/rewrite.ts b/vscode-extensions/vscode-spring-boot/lib/rewrite.ts index 6b4441b1c..d83d3d1fc 100644 --- a/vscode-extensions/vscode-spring-boot/lib/rewrite.ts +++ b/vscode-extensions/vscode-spring-boot/lib/rewrite.ts @@ -11,11 +11,8 @@ interface RecipeDescriptor { displayName: string; description: string; tags: string[]; - estimatedEffortPerOccurrence: number; options: OptionDescriptor[]; - languages: string[]; - recipeList: RecipeDescriptor[]; - source: string; + hasSubRecipes: boolean; } interface OptionDescriptor { @@ -29,10 +26,16 @@ interface OptionDescriptor { value: any; } +interface RecipeSelectionDescriptor { + selected: boolean; + id: string; + subselection: RecipeSelectionDescriptor[]; +} + interface RecipeQuickPickItem extends VSCode.QuickPickItem{ readonly id: string; selected: boolean; - children: RecipeQuickPickItem[], + children: RecipeQuickPickItem[] | undefined, readonly recipeDescriptor: RecipeDescriptor; } @@ -95,45 +98,33 @@ async function showRefactorings(uri: VSCode.Uri, filter: string) { if (!uri) { uri = await getTargetPomXml(); } - 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 choices = await showCurrentPathQuickPick(VSCode.commands.executeCommand('sts/rewrite/list', filter).then((cmds: RecipeDescriptor[]) => cmds.map(d => convertToQuickPickItem(d, false))), []); + const recipeDescriptors = choices.filter(i => i.selected).map(convertToRecipeSelectionDescriptor); const needsConfirmation = await shwoNeedsConfirmation(); if (recipeDescriptors.length) { - const aggregateRecipeDescriptor = recipeDescriptors.length === 1 ? recipeDescriptors[0] : { - name: `${recipeDescriptors.length} recipes`, - displayName: `${recipeDescriptors.length} recipes`, - description: recipeDescriptors.map(d => d.description).join('\n'), - tags: [...new Set(recipeDescriptors.flatMap(d => d.tags))], - languages: [...new Set(recipeDescriptors.flatMap(d => d.languages))], - options: [], - recipeList: recipeDescriptors, - estimatedEffortPerOccurrence: recipeDescriptors.filter(d => d.estimatedEffortPerOccurrence).map(d => d.estimatedEffortPerOccurrence).reduce((p, c) => p + c, 0) - }; - if (aggregateRecipeDescriptor.estimatedEffortPerOccurrence === 0) { - delete aggregateRecipeDescriptor.estimatedEffortPerOccurrence; - } - VSCode.commands.executeCommand('sts/rewrite/execute', uri.toString(true), aggregateRecipeDescriptor, needsConfirmation); + VSCode.commands.executeCommand('sts/rewrite/execute', uri.toString(true), recipeDescriptors, needsConfirmation); } else { VSCode.window.showErrorMessage('No Recipes were selected!'); } } -function convertToRecipeDescriptor(i: RecipeQuickPickItem): RecipeDescriptor { +function convertToRecipeSelectionDescriptor(i: RecipeQuickPickItem): RecipeSelectionDescriptor { return { - ...i.recipeDescriptor, - recipeList: i.children.filter(c => c.selected).map(convertToRecipeDescriptor) + selected: i.selected, + id: i.id, + subselection: i.children ? i.children.map(convertToRecipeSelectionDescriptor) : undefined }; } -function convertToQuickPickItem(i: RecipeDescriptor): RecipeQuickPickItem { +function convertToQuickPickItem(i: RecipeDescriptor, selected?: boolean): RecipeQuickPickItem { return { id: i.name, label: i.displayName, detail: i.options.filter(o => !!o.value).map(o => `${o.name}: ${JSON.stringify(o.value)}`).join('\n\n'), description: i.description, - selected: false, - children: i.recipeList ? i.recipeList.map(convertToQuickPickItem) : [], - buttons: i.recipeList && i.recipeList.length ? [ SUB_RECIPES_BUTTON ] : undefined, + selected: !!selected, + children: undefined, + buttons: i.hasSubRecipes ? [ SUB_RECIPES_BUTTON ] : undefined, recipeDescriptor: i }; } @@ -188,7 +179,7 @@ function showCurrentPathQuickPick(itemsPromise: Thenable, if (e.button === SUB_RECIPES_BUTTON) { currentItems.forEach(i => i.selected = quickPick.selectedItems.includes(i)); itemsPath.push(e.item); - showCurrentPathQuickPick(Promise.resolve(items), itemsPath).then(resolve, reject); + showCurrentPathQuickPick(navigateToSubRecipes(e.item, itemsPath).then(() => items), itemsPath).then(resolve, reject); } }); quickPick.onDidTriggerButton(b => { @@ -222,6 +213,17 @@ function showCurrentPathQuickPick(itemsPromise: Thenable, }); } +async function navigateToSubRecipes(item: RecipeQuickPickItem, itemsPath: RecipeQuickPickItem[]) { + if (!item.children) { + const indexPath = []; + for (let i = 1; i < itemsPath.length; i++) { + indexPath.push(itemsPath[i - 1].children.indexOf(itemsPath[i])); + } + const recipeDescriptors: RecipeDescriptor[] = await VSCode.commands.executeCommand('sts/rewrite/sublist', itemsPath[0].id, indexPath); + item.children = recipeDescriptors.map(d => convertToQuickPickItem(d, item.selected)); + } +} + function updateParentSelection(hierarchy: RecipeQuickPickItem[]): void { if (hierarchy.length) { const parent = hierarchy.pop(); diff --git a/vscode-extensions/vscode-spring-boot/package.json b/vscode-extensions/vscode-spring-boot/package.json index 8f0c9a7d8..7485ae262 100644 --- a/vscode-extensions/vscode-spring-boot/package.json +++ b/vscode-extensions/vscode-spring-boot/package.json @@ -12,7 +12,7 @@ "license": "EPL-1.0", "engines": { "npm": ">=6.0.0", - "vscode": "^1.67.0" + "vscode": "^1.75.0" }, "categories": [ "Programming Languages", @@ -26,8 +26,6 @@ "application-yaml" ], "activationEvents": [ - "onCommand:vscode-spring-boot.rewrite.list.refactorings", - "onCommand:vscode-spring-boot.rewrite.list.boot-upgrades", "onCommand:vscode-spring-boot.ls.start" ], "contributes": { @@ -1029,9 +1027,9 @@ }, "devDependencies": { "@types/node": "^18.8.0", - "@types/vscode": "1.67.0", + "@types/vscode": "1.75.0", "typescript": "^4.8.0", - "vsce": "^2.11.0" + "vsce": "^2.15.0" }, "extensionDependencies": [ "redhat.java"