From 500253dc98179e4d9629043f9f58cc55c2c14447 Mon Sep 17 00:00:00 2001 From: azerr Date: Wed, 6 Dec 2023 10:46:45 +0100 Subject: [PATCH] fix: instantiate JsonObject with the classloader of the IJ plugin which defines the action Signed-off-by: azerr --- .../lsp4ij/commands/CommandExecutor.java | 190 +++++++++++------- .../devtools/lsp4ij/commands/GsonManager.java | 67 ++++++ .../operations/AbstractLSPInlayProvider.java | 8 +- .../LSPLazyCodeActionIntentionAction.java | 2 +- .../completion/LSPCompletionProposal.java | 2 +- 5 files changed, 189 insertions(+), 80 deletions(-) create mode 100644 src/main/java/com/redhat/devtools/lsp4ij/commands/GsonManager.java diff --git a/src/main/java/com/redhat/devtools/lsp4ij/commands/CommandExecutor.java b/src/main/java/com/redhat/devtools/lsp4ij/commands/CommandExecutor.java index e9ece29da..39d339b1a 100644 --- a/src/main/java/com/redhat/devtools/lsp4ij/commands/CommandExecutor.java +++ b/src/main/java/com/redhat/devtools/lsp4ij/commands/CommandExecutor.java @@ -11,10 +11,8 @@ ******************************************************************************/ package com.redhat.devtools.lsp4ij.commands; -import com.google.gson.Gson; -import com.google.gson.JsonArray; -import com.google.gson.JsonObject; -import com.google.gson.JsonPrimitive; +import com.google.gson.*; +import com.intellij.ide.DataManager; import com.intellij.openapi.actionSystem.*; import com.intellij.openapi.actionSystem.ex.ActionUtil; import com.intellij.openapi.actionSystem.impl.SimpleDataContext; @@ -33,20 +31,22 @@ import org.eclipse.lsp4j.WorkspaceEdit; import org.eclipse.lsp4j.services.LanguageServer; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.awt.*; import java.io.IOException; import java.net.URI; import java.util.ArrayList; -import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.CompletableFuture; +import java.util.stream.Stream; /** - * This class provides methods to execute {@link Command} instances. + * This class provides methods to execute LSP {@link Command} instances. */ public class CommandExecutor { private static final Logger LOGGER = LoggerFactory.getLogger(CommandExecutor.class); @@ -63,25 +63,23 @@ public class CommandExecutor { * the server, nor the client are able to handle the command explicitly, a * heuristic method will try to interpret the command locally. * - * @param command - * the LSP Command to be executed. If {@code null} this method will - * do nothing. - * @param documentUri - * the URI of the document for which the command was created - * @param languageServerId - * the ID of the language server for which the {@code command} is - * applicable. If {@code null}, the command will not be executed on - * the language server. + * @param command the LSP Command to be executed. If {@code null} this method will + * do nothing. + * @param documentUri the URI of the document for which the command was created + * @param project the project. + * @param languageServerId the ID of the language server for which the {@code command} is + * applicable. If {@code null}, the command will not be executed on + * the language server. */ - public static void executeCommand(Project project, Command command, URI documentUri, - String languageServerId) { + public static void executeCommand(@Nullable Command command, @Nullable URI documentUri, + @NotNull Project project, @Nullable String languageServerId) { if (command == null) { return; } - if (executeCommandServerSide(project, command, documentUri, languageServerId)) { + if (executeCommandServerSide(command, documentUri, project, languageServerId)) { return; } - if (executeCommandClientSide(project, command, documentUri)) { + if (executeCommandClientSide(command, null, project, null)) { return; } // tentative fallback @@ -94,8 +92,19 @@ public static void executeCommand(Project project, Command command, URI document } } - private static boolean executeCommandServerSide(Project project, Command command, - URI documentUri, String languageServerId) { + /** + * Execute LSP command on server side. + * + * @param command the LSP Command to be executed. If {@code null} this method will + * do nothing. + * @param documentUri the URI of the document for which the command was created + * @param project the project. + * @param languageServerId the ID of the language server for which the {@code command} is + * applicable. + * @return true if the LSP command on server side has been executed successfully and false otherwise. + */ + private static boolean executeCommandServerSide(@NotNull Command command, @Nullable URI documentUri, + @NotNull Project project, @Nullable String languageServerId) { if (languageServerId == null) { return false; } @@ -142,17 +151,28 @@ private static CompletableFuture getLanguageServerForCommand(Pro }); } - private static boolean executeCommandClientSide(Project project, Command command, URI documentUri) { + /** + * Execute LSP command on server side. + * + * @param command the LSP Command to be executed. If {@code null} this method will + * do nothing. + * @param documentUri the URI of the document for which the command was created + * @param project the project. + * @param source the component which has triggered the command and null otherwise. + * @return true if the LSP command on server side has been executed successfully and false otherwise. + */ + public static boolean executeCommandClientSide(@NotNull Command command, @Nullable URI documentUri, + @NotNull Project project, @Nullable Component source) { Application workbench = ApplicationManager.getApplication(); if (workbench == null) { return false; } - AnAction parameterizedCommand = createIDEACoreCommand(command); - if (parameterizedCommand == null) { + AnAction action = createIDEACoreCommand(command); + if (action == null) { return false; } - DataContext dataContext = createDataContext(project, command, documentUri); - ActionUtil.invokeAction(parameterizedCommand, dataContext, ActionPlaces.UNKNOWN, null, null); + DataContext dataContext = createDataContext(documentUri, command, action, source, project); + ActionUtil.invokeAction(action, dataContext, ActionPlaces.UNKNOWN, null, null); return true; } @@ -164,15 +184,44 @@ private static AnAction createIDEACoreCommand(Command command) { return ActionManager.getInstance().getAction(commandId); } - private static DataContext createDataContext(Project project, Command command, URI documentUri) { - + private static DataContext createDataContext(URI documentUri, Command command, AnAction action, Component source, Project project) { SimpleDataContext.Builder contextBuilder = SimpleDataContext.builder(); - contextBuilder.add(CommonDataKeys.PROJECT, project) - .add(LSP_COMMAND, command) - .add(LSP_COMMAND_DOCUMENT_URI, documentUri); + if (source != null) { + contextBuilder.setParent(DataManager.getInstance().getDataContext(source)); + } + ensureArgumentsIsInProperClassloader(command, action.getClass().getClassLoader()); + contextBuilder + .add(CommonDataKeys.PROJECT, project) + .add(LSP_COMMAND, command); + if (documentUri != null) { + contextBuilder.add(LSP_COMMAND_DOCUMENT_URI, documentUri); + } return contextBuilder.build(); } + private static void ensureArgumentsIsInProperClassloader(Command command, ClassLoader classLoader) { + List arguments = command.getArguments(); + if (arguments == null || arguments.isEmpty()) { + return; + } + for (int i = 0; i < arguments.size(); i++) { + Object arg = arguments.get(i); + if (arg instanceof JsonElement elt) { + // At this step, JsonElement is an instance coming from the LSP4IJ plugin class loader. + // If external plugin which consumes LSP4IJ and declare a gson dependency, it will have + // ClasCastException error which command arguments will be used. + // In this case, JsonElement requires to be updated by creating a new JsonElement with the external plugin class loader. + Object newElt = GsonManager.getJsonElementFromClassloader(elt, classLoader); + if (newElt != null) { + arguments.set(i, newElt); + } else { + // Here, the JsonElement class loader should be valid, no need to create new instances of JsonElement. + return; + } + } + } + } + // TODO consider using Entry/SimpleEntry instead private static final class Pair { K key; @@ -185,6 +234,7 @@ private static final class Pair { } // this method may be turned public if needed elsewhere + /** * Very empirical and unsafe heuristic to turn unknown command arguments into a * workspace edit... @@ -199,58 +249,56 @@ private static WorkspaceEdit createWorkspaceEdit(List commandArguments, if (item instanceof List) { return ((List) item).stream(); } else { - return Collections.singleton(item).stream(); + return Stream.of(item); } }).forEach(arg -> { - if (arg instanceof String) { + if (arg instanceof String) { + changes.put(currentEntry.key.toString(), currentEntry.value); + VirtualFile resource = LSPIJUtils.findResourceFor((String) arg); + if (resource != null) { + currentEntry.key = LSPIJUtils.toUri(resource); + currentEntry.value = new ArrayList<>(); + } + } else if (arg instanceof WorkspaceEdit) { + changes.putAll(((WorkspaceEdit) arg).getChanges()); + } else if (arg instanceof TextEdit) { + currentEntry.value.add((TextEdit) arg); + } else if (arg instanceof Map) { + Gson gson = new Gson(); // TODO? retrieve the GSon used by LS + TextEdit edit = gson.fromJson(gson.toJson(arg), TextEdit.class); + if (edit != null) { + currentEntry.value.add(edit); + } + } else if (arg instanceof JsonPrimitive json) { + if (json.isString()) { changes.put(currentEntry.key.toString(), currentEntry.value); - VirtualFile resource = LSPIJUtils.findResourceFor((String) arg); + VirtualFile resource = LSPIJUtils.findResourceFor(json.getAsString()); if (resource != null) { currentEntry.key = LSPIJUtils.toUri(resource); currentEntry.value = new ArrayList<>(); } - } else if (arg instanceof WorkspaceEdit) { - changes.putAll(((WorkspaceEdit) arg).getChanges()); - } else if (arg instanceof TextEdit) { - currentEntry.value.add((TextEdit) arg); - } else if (arg instanceof Map) { - Gson gson = new Gson(); // TODO? retrieve the GSon used by LS - TextEdit edit = gson.fromJson(gson.toJson(arg), TextEdit.class); + } + } else if (arg instanceof JsonArray array) { + Gson gson = new Gson(); // TODO? retrieve the GSon used by LS + array.forEach(elt -> { + TextEdit edit = gson.fromJson(gson.toJson(elt), TextEdit.class); if (edit != null) { currentEntry.value.add(edit); } - } else if (arg instanceof JsonPrimitive) { - JsonPrimitive json = (JsonPrimitive) arg; - if (json.isString()) { - changes.put(currentEntry.key.toString(), currentEntry.value); - VirtualFile resource = LSPIJUtils.findResourceFor(json.getAsString()); - if (resource != null) { - currentEntry.key = LSPIJUtils.toUri(resource); - currentEntry.value = new ArrayList<>(); - } - } - } else if (arg instanceof JsonArray) { - Gson gson = new Gson(); // TODO? retrieve the GSon used by LS - JsonArray array = (JsonArray) arg; - array.forEach(elt -> { - TextEdit edit = gson.fromJson(gson.toJson(elt), TextEdit.class); - if (edit != null) { - currentEntry.value.add(edit); - } - }); - } else if (arg instanceof JsonObject) { - Gson gson = new Gson(); // TODO? retrieve the GSon used by LS - WorkspaceEdit wEdit = gson.fromJson((JsonObject) arg, WorkspaceEdit.class); - Map> entries = wEdit.getChanges(); - if (wEdit != null && !entries.isEmpty()) { - changes.putAll(entries); - } else { - TextEdit edit = gson.fromJson((JsonObject) arg, TextEdit.class); - if (edit != null && edit.getRange() != null) { - currentEntry.value.add(edit); - } + }); + } else if (arg instanceof JsonObject) { + Gson gson = new Gson(); // TODO? retrieve the GSon used by LS + WorkspaceEdit wEdit = gson.fromJson((JsonObject) arg, WorkspaceEdit.class); + Map> entries = wEdit.getChanges(); + if (wEdit != null && !entries.isEmpty()) { + changes.putAll(entries); + } else { + TextEdit edit = gson.fromJson((JsonObject) arg, TextEdit.class); + if (edit != null && edit.getRange() != null) { + currentEntry.value.add(edit); } } + } }); if (!currentEntry.value.isEmpty()) { changes.put(currentEntry.key.toString(), currentEntry.value); diff --git a/src/main/java/com/redhat/devtools/lsp4ij/commands/GsonManager.java b/src/main/java/com/redhat/devtools/lsp4ij/commands/GsonManager.java new file mode 100644 index 000000000..7c236438f --- /dev/null +++ b/src/main/java/com/redhat/devtools/lsp4ij/commands/GsonManager.java @@ -0,0 +1,67 @@ +/******************************************************************************* + * Copyright (c) 2020 Red Hat, Inc. + * Distributed under license by Red Hat, Inc. All rights reserved. + * This program is made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, + * and is available at https://www.eclipse.org/legal/epl-v20.html + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + * Fraunhofer FOKUS + ******************************************************************************/ +package com.redhat.devtools.lsp4ij.commands; + +import com.google.gson.JsonElement; +import com.google.gson.JsonParser; +import com.intellij.ide.plugins.cl.PluginClassLoader; + +import java.lang.reflect.Method; + +/** + * Gson manager. + */ +public class GsonManager { + + private GsonManager() { + + } + + /** + * Returns the JsonElement instance with the given class loader and null otherwise. + * + * @param elt the JsonElement with the lsp4ij plugin class loader. + * @param actionClassLoader the IJ action class loader. + * @return the JsonElement instance with the given class loader and null otherwise. + */ + public static Object getJsonElementFromClassloader(JsonElement elt, ClassLoader actionClassLoader) { + if (elt.getClass().getClassLoader() == actionClassLoader || !(actionClassLoader instanceof PluginClassLoader)) { + // - the JsonElement class has the same class loader as the IJ Action class loader + // - or the action class loader is not a PluginClassLoader + // --> do nothing + return null; + } + try { + Class jsonParserClass = ((PluginClassLoader) actionClassLoader).tryLoadingClass(JsonParser.class.getName(), true); + if (jsonParserClass != null) { + try { + // Try to get static method JsonParser#parseString from the new version of Gson + // public static JsonElement parseString(String json) throws JsonSyntaxException { + Method parseString = jsonParserClass.getDeclaredMethod("parseString", String.class); + return parseString.invoke(jsonParserClass, elt.toString()); + } catch (Exception e) { + // Old version of Gson + try { + // public JsonElement parse(String json) throws JsonSyntaxException { + Method parse = jsonParserClass.getDeclaredMethod("parse", String.class); + return parse.invoke(jsonParserClass.getDeclaredConstructor().newInstance(), elt.toString()); + } catch (Exception e1) { + + } + } + } + } catch (Exception e) { + + } + return null; + } +} diff --git a/src/main/java/com/redhat/devtools/lsp4ij/operations/AbstractLSPInlayProvider.java b/src/main/java/com/redhat/devtools/lsp4ij/operations/AbstractLSPInlayProvider.java index f8b52840b..8105f01ee 100644 --- a/src/main/java/com/redhat/devtools/lsp4ij/operations/AbstractLSPInlayProvider.java +++ b/src/main/java/com/redhat/devtools/lsp4ij/operations/AbstractLSPInlayProvider.java @@ -165,13 +165,7 @@ public boolean isLanguageSupported(@NotNull Language language) { protected void executeClientCommand(@NotNull Component source, @NotNull Command command, @NotNull Project project) { if (command != null) { - AnAction action = ActionManager.getInstance().getAction(command.getCommand()); - if (action != null) { - DataContext context = SimpleDataContext.getSimpleContext(CommandExecutor.LSP_COMMAND, command, DataManager.getInstance().getDataContext(source)); - action.actionPerformed(new AnActionEvent(null, context, - ActionPlaces.UNKNOWN, new Presentation(), - ActionManager.getInstance(), 0)); - } + CommandExecutor.executeCommandClientSide(command, null, project, source); } } diff --git a/src/main/java/com/redhat/devtools/lsp4ij/operations/codeactions/LSPLazyCodeActionIntentionAction.java b/src/main/java/com/redhat/devtools/lsp4ij/operations/codeactions/LSPLazyCodeActionIntentionAction.java index 40e98298e..edb602f96 100644 --- a/src/main/java/com/redhat/devtools/lsp4ij/operations/codeactions/LSPLazyCodeActionIntentionAction.java +++ b/src/main/java/com/redhat/devtools/lsp4ij/operations/codeactions/LSPLazyCodeActionIntentionAction.java @@ -112,7 +112,7 @@ private void apply(CodeAction codeaction, @NotNull Project project, PsiFile file } private void executeCommand(Command command, @NotNull Project project, PsiFile file, String serverId) { - CommandExecutor.executeCommand(project, command, LSPIJUtils.toUri(file), serverId); + CommandExecutor.executeCommand(command, LSPIJUtils.toUri(file), project, serverId); } private LanguageServerWrapper getLanguageServerWrapper() { diff --git a/src/main/java/com/redhat/devtools/lsp4ij/operations/completion/LSPCompletionProposal.java b/src/main/java/com/redhat/devtools/lsp4ij/operations/completion/LSPCompletionProposal.java index e95f4b8b6..402a28ec0 100644 --- a/src/main/java/com/redhat/devtools/lsp4ij/operations/completion/LSPCompletionProposal.java +++ b/src/main/java/com/redhat/devtools/lsp4ij/operations/completion/LSPCompletionProposal.java @@ -285,7 +285,7 @@ private void executeCustomCommand(@NotNull Command command, URI documentUri) { LanguageServiceAccessor.getInstance(project) .resolveServerDefinition(languageServer.getServer()).map(definition -> definition.id) .ifPresent(id -> { - CommandExecutor.executeCommand(project, command, documentUri, id); + CommandExecutor.executeCommand(command, documentUri, project, id); }); }