Minimize size of messages for fetching and executing recipes

This commit is contained in:
aboyko
2023-12-18 20:54:59 -05:00
parent f74f3ec291
commit d6ff60a96e
11 changed files with 449 additions and 287 deletions

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-11"/>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-17"/>
<classpathentry kind="con" path="org.eclipse.pde.core.requiredPlugins"/>
<classpathentry kind="src" path="src/"/>
<classpathentry kind="output" path="target/classes"/>

View File

@@ -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

View File

@@ -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<String> tags;
Duration estimatedEffortPerOccurrence;
List<OptionDescriptor> options;
List<String> languages;
List<RecipeDescriptor> 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<String> valid;
boolean required;
Object value;
}
record OptionDescriptor(
String name,
String type,
String displayName,
String description,
String example,
List<String> valid,
boolean required,
Object value
) {}
record RecipeSelection(boolean selected, String id, RecipeSelection[] subselection) {}
}

View File

@@ -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<RecipeDescriptor, RecipeDescriptor> parentMap = new IdentityHashMap<>();
private Map<RecipeDescriptor, CheckedState> 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<RecipeSelection> 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<Void> fetchSubrecipes(RecipeDescriptor descriptor) {
RecipeDescriptor d = descriptor;
LinkedList<Integer> 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<Void> 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<RecipeDescriptor> 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());
}
}
}

View File

@@ -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<Duration>, JsonDeserializer<Duration> {
@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<Object> allRewriteRecipesJson = new ArrayList<>();
List<Object> 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);
}

View File

@@ -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("</p>");
sb.append("<ul>");
for (OptionDescriptor option : r.options) {
if (option.value != null) {
if (option.value() != null) {
sb.append("<li>");
sb.append("<pre>");
sb.append(option.value);
sb.append(option.value());
sb.append("</pre>");
sb.append(option.description);
sb.append(option.description());
sb.append("</li>");
}
}