Start Boot LS properly when invoking Rewrite Refactorings command

This commit is contained in:
aboyko
2022-11-11 17:10:32 -05:00
parent 7594a743cf
commit 5b5ac912ad
2 changed files with 112 additions and 155 deletions

View File

@@ -36,11 +36,7 @@ import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.eclipse.lsp4j.ApplyWorkspaceEditParams;
import org.eclipse.lsp4j.Registration;
import org.eclipse.lsp4j.RegistrationParams;
import org.eclipse.lsp4j.TextDocumentIdentifier;
import org.eclipse.lsp4j.Unregistration;
import org.eclipse.lsp4j.UnregistrationParams;
import org.eclipse.lsp4j.WorkspaceEdit;
import org.openrewrite.ExecutionContext;
import org.openrewrite.InMemoryExecutionContext;
@@ -77,9 +73,6 @@ import org.springframework.ide.vscode.commons.rewrite.config.StsEnvironment;
import org.springframework.ide.vscode.commons.rewrite.maven.MavenProjectParser;
import org.springframework.ide.vscode.commons.util.text.TextDocument;
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.GsonBuilder;
import com.google.gson.JsonElement;
@@ -89,15 +82,15 @@ public class RewriteRecipeRepository implements ApplicationContextAware {
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_RECIPE_EXECUTE = "sts/rewrite/recipe/execute";
private static final Logger log = LoggerFactory.getLogger(RewriteRecipeRepository.class);
private static final String WORKSPACE_EXECUTE_COMMAND = "workspace/executeCommand";
private static final Set<String> UNINITIALIZED_SET = Collections.emptySet();
final private SimpleLanguageServer server;
final private Map<String, Recipe> recipes;
final private List<Recipe> globalCommandRecipes;
final private JavaProjectFinder projectFinder;
final private List<RecipeCodeActionDescriptor> codeActionDescriptors;
@@ -108,9 +101,9 @@ public class RewriteRecipeRepository implements ApplicationContextAware {
private CompletableFuture<Void> loaded;
private Set<String> scanFiles = Collections.emptySet();
private Set<String> scanDirs = Collections.emptySet();
private Set<String> recipeFilters = Collections.emptySet();
private Set<String> scanFiles;
private Set<String> scanDirs;
private Set<String> recipeFilters;
private static Gson serializationGson = new GsonBuilder()
.registerTypeAdapter(Duration.class, new DurationTypeConverter())
@@ -120,33 +113,31 @@ public class RewriteRecipeRepository implements ApplicationContextAware {
this.server = server;
this.projectFinder = projectFinder;
this.recipes = new HashMap<>();
this.globalCommandRecipes = new ArrayList<>();
this.codeActionDescriptors = new ArrayList<>();
this.loadListeners = new ListenerList<>();
this.scanDirs = UNINITIALIZED_SET;
this.scanFiles = UNINITIALIZED_SET;
this.recipeFilters = UNINITIALIZED_SET;
server.doOnInitialized(() -> {
this.scanDirs = config.getRecipeDirectories();
this.scanFiles = config.getRecipeFiles();
this.recipeFilters = config.getRecipesFilters();
load().thenAccept(v -> registerCommands());
config.addListener(l -> {
if (!scanDirs.equals(config.getRecipeDirectories())
|| !scanFiles.equals(config.getRecipeFiles())) {
// Eclipse client sends one event for init and the other config changed event due to remote app value expr listener.
// Therefore it is best to store the scanDirs here right after it is received, not during scan process or anything else done async
scanDirs = config.getRecipeDirectories();
scanFiles = config.getRecipeFiles();
load();
}
Set<String> recipeFilterFromConfig = config.getRecipesFilters();
if (!recipeFilters.equals(recipeFilterFromConfig)) {
recipeFilters = recipeFilterFromConfig;
updateGlobalCommandRecipes();
}
});
config.addListener(l -> {
Set<String> recipeFilterFromConfig = config.getRecipesFilters();
if (recipeFilters == UNINITIALIZED_SET || recipeFilters.equals(recipeFilterFromConfig)) {
recipeFilters = recipeFilterFromConfig;
}
if (scanDirs == UNINITIALIZED_SET || !scanDirs.equals(config.getRecipeDirectories())
|| scanFiles == UNINITIALIZED_SET || !scanFiles.equals(config.getRecipeFiles())) {
// Eclipse client sends one event for init and the other config changed event due to remote app value expr listener.
// Therefore it is best to store the scanDirs here right after it is received, not during scan process or anything else done async
scanDirs = config.getRecipeDirectories();
scanFiles = config.getRecipeFiles();
load();
}
});
registerCommands();
}
public CompletableFuture<Void> load() {
return this.loaded = CompletableFuture.runAsync(() -> {
clearRecipes();
@@ -157,7 +148,6 @@ public class RewriteRecipeRepository implements ApplicationContextAware {
private synchronized void clearRecipes() {
recipes.clear();
globalCommandRecipes.clear();
codeActionDescriptors.clear();
}
@@ -172,11 +162,7 @@ public class RewriteRecipeRepository implements ApplicationContextAware {
if (recipes.containsKey(r.getName())) {
log.error("Duplicate ids: '" + r.getName() + "'");
}
recipes.put(r.getName(), r);
if (isAcceptableGlobalCommandRecipe(r)) {
globalCommandRecipes.add(r);
}
recipes.put(r.getName(), r);
}
}
codeActionDescriptors.addAll(env.listCodeActionDescriptors());
@@ -188,15 +174,6 @@ public class RewriteRecipeRepository implements ApplicationContextAware {
}
}
private void updateGlobalCommandRecipes() {
globalCommandRecipes.clear();
for (Recipe r : recipes.values()) {
if (isAcceptableGlobalCommandRecipe(r)) {
globalCommandRecipes.add(r);
}
}
}
private boolean isAcceptableGlobalCommandRecipe(Recipe r) {
for (String filter : recipeFilters) {
if (!filter.isBlank()) {
@@ -318,10 +295,6 @@ public class RewriteRecipeRepository implements ApplicationContextAware {
}
private void registerCommands() {
log.info("Registering commands for rewrite recipes...");
Builder<Object> listBuilder = ImmutableList.builder();
server.onCommand(CMD_REWRITE_LIST, params -> {
JsonElement uri = (JsonElement) params.getArguments().get(0);
return loaded.thenApply(v -> {
@@ -332,59 +305,38 @@ public class RewriteRecipeRepository implements ApplicationContextAware {
}
});
});
listBuilder.add(CMD_REWRITE_LIST);
server.onCommand(CMD_REWRITE_EXECUTE, params -> {
String uri = ((JsonElement) params.getArguments().get(0)).getAsString();
JsonElement recipesJson = ((JsonElement) params.getArguments().get(1));
RecipeDescriptor d = serializationGson.fromJson(recipesJson, RecipeDescriptor.class);
Recipe aggregateRecipe = LoadUtils.createRecipe(d, id -> getRecipe(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);
String progressToken = params.getWorkDoneToken() == null || params.getWorkDoneToken().getLeft() == null ? r.getName() : params.getWorkDoneToken().getLeft();
return apply(r, uri, progressToken);
} else {
String progressToken = params.getWorkDoneToken() == null || params.getWorkDoneToken().getLeft() == null ? aggregateRecipe.getName() : params.getWorkDoneToken().getLeft();
return apply(aggregateRecipe, uri, progressToken);
}
return loaded.thenCompose(v -> {
String uri = ((JsonElement) params.getArguments().get(0)).getAsString();
JsonElement recipesJson = ((JsonElement) params.getArguments().get(1));
RecipeDescriptor d = serializationGson.fromJson(recipesJson, RecipeDescriptor.class);
Recipe aggregateRecipe = LoadUtils.createRecipe(d, id -> getRecipe(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);
String progressToken = params.getWorkDoneToken() == null || params.getWorkDoneToken().getLeft() == null ? r.getName() : params.getWorkDoneToken().getLeft();
return apply(r, uri, progressToken);
} else {
String progressToken = params.getWorkDoneToken() == null || params.getWorkDoneToken().getLeft() == null ? aggregateRecipe.getName() : params.getWorkDoneToken().getLeft();
return apply(aggregateRecipe, uri, progressToken);
}
});
});
listBuilder.add(CMD_REWRITE_EXECUTE);
server.onCommand(CMD_REWRITE_RELOAD, params -> load().thenApply((v) -> "executed"));
listBuilder.add(CMD_REWRITE_RELOAD);
for (Recipe r : globalCommandRecipes) {
listBuilder.add(createGlobalCommand(r));
}
String registrationId = UUID.randomUUID().toString();
RegistrationParams params = new RegistrationParams(ImmutableList.of(
new Registration(registrationId,
WORKSPACE_EXECUTE_COMMAND,
ImmutableMap.of("commands", listBuilder.build())
)
));
server.getClient().registerCapability(params).thenAccept((v) -> {
server.onShutdown(() -> server.getClient().unregisterCapability(new UnregistrationParams(List.of(new Unregistration(registrationId, WORKSPACE_EXECUTE_COMMAND)))));
log.info("Done registering commands for rewrite recipes");
});
}
private String createGlobalCommand(Recipe r) {
String commandId = "sts/rewrite/recipe/" + r.getName();
server.onCommand(commandId, params -> {
server.onCommand(CMD_REWRITE_RECIPE_EXECUTE, params -> {
String recipeId = ((JsonElement) params.getArguments().get(0)).getAsString();
Recipe r = getRecipe(recipeId).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(0)).getAsString();
String uri = ((JsonElement) params.getArguments().get(1)).getAsString();
return apply(r, uri, progressToken);
});
return commandId;
});
}
private CompletableFuture<Object> apply(Recipe r, String uri, String progressToken) {
@@ -416,8 +368,9 @@ public class RewriteRecipeRepository implements ApplicationContextAware {
server.getProgressService().progressDone(progressToken);
throw t;
}
} else {
return CompletableFuture.failedFuture(new IllegalArgumentException("Cannot find Spring Boot project for uri: " + uri));
}
return CompletableFuture.completedFuture(null);
});
}
@@ -441,14 +394,15 @@ public class RewriteRecipeRepository implements ApplicationContextAware {
private List<Recipe> listProjectRefactoringRecipes(String uri) {
if (uri != null) {
Optional<IJavaProject> projectOpt = projectFinder.find(new TextDocumentIdentifier(uri));
if (projectOpt.isPresent()) {
List<Recipe> commandDescriptors = new ArrayList<>(globalCommandRecipes.size());
for (Recipe r : globalCommandRecipes) {
commandDescriptors.add(r);
}
return commandDescriptors;
}
/*
* 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<IJavaProject> projectOpt = projectFinder.find(new TextDocumentIdentifier(uri));
// if (projectOpt.isPresent()) {
return recipes.values().stream().filter(this::isAcceptableGlobalCommandRecipe).collect(Collectors.toList());
// }
}
return Collections.emptyList();
}

View File

@@ -89,9 +89,7 @@ async function liveHoverConnectHandler(uri: VSCode.Uri) {
if (!uri) {
uri = await getTargetPomXml();
}
const cmds: RecipeDescriptor[] = await VSCode.commands.executeCommand('sts/rewrite/list', uri.toString(true));
const choices = cmds.map(convertToQuickPickItem);
await showCurrentPathQuickPick(choices, []);
const choices = await showCurrentPathQuickPick(VSCode.commands.executeCommand('sts/rewrite/list', uri.toString(true)).then((cmds: RecipeDescriptor[]) => cmds.map(convertToQuickPickItem)), []);
const recipeDescriptors = choices.filter(i => i.selected).map(convertToRecipeDescriptor);
if (recipeDescriptors.length) {
const aggregateRecipeDescriptor = recipeDescriptors.length === 1 ? recipeDescriptors[0] : {
@@ -133,56 +131,61 @@ function convertToQuickPickItem(i: RecipeDescriptor): RecipeQuickPickItem {
};
}
function showCurrentPathQuickPick(items: RecipeQuickPickItem[], itemsPath: RecipeQuickPickItem[]): Promise<void> {
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<RecipeQuickPickItem>();
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));
function showCurrentPathQuickPick(itemsPromise: Thenable<RecipeQuickPickItem[]>, itemsPath: RecipeQuickPickItem[]): Thenable<RecipeQuickPickItem[]> {
const quickPick = VSCode.window.createQuickPick<RecipeQuickPickItem>();
quickPick.title = 'Loading Recipes...';
quickPick.canSelectMany = true;
quickPick.busy = true;
quickPick.show();
return itemsPromise.then(items => {
return new Promise((resolve, reject) => {
let currentItems = items;
let parent: RecipeQuickPickItem | undefined;
itemsPath.forEach(p => {
parent = currentItems.find(i => i === p);
currentItems = parent.children;
});
quickPick.items = currentItems;
if (itemsPath.length) {
itemsPath.pop();
showCurrentPathQuickPick(items, itemsPath).then(resolve, reject);
} else {
quickPick.hide();
resolve();
quickPick.buttons = [ ROOT_RECIPES_BUTTON ];
}
});
quickPick.onDidChangeSelection(selected => {
currentItems.forEach(i => {
const isSelectedItem = selected.includes(i);
if (i.selected !== isSelectedItem) {
selectItemRecursively(i, isSelectedItem);
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(Promise.resolve(items), itemsPath).then(resolve, reject);
}
});
updateParentSelection(itemsPath.slice());
quickPick.onDidTriggerButton(b => {
if (b === ROOT_RECIPES_BUTTON) {
currentItems.forEach(i => i.selected = quickPick.selectedItems.includes(i));
itemsPath.splice(0, itemsPath.length);
showCurrentPathQuickPick(Promise.resolve(items), itemsPath).then(resolve, reject);
}
});
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.onDidChangeSelection(selected => {
currentItems.forEach(i => {
const isSelectedItem = selected.includes(i);
if (i.selected !== isSelectedItem) {
selectItemRecursively(i, isSelectedItem);
}
});
updateParentSelection(itemsPath.slice());
});
quickPick.title = 'Select Recipes';
quickPick.busy = false;
});
quickPick.show();
});
}