/******************************************************************************* * 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 http://www.eclipse.org/legal/epl-v20.html * * Contributors: * Red Hat, Inc. - initial API and implementation * Fraunhofer FOKUS ******************************************************************************/ package com.redhat.devtools.intellij.quarkus.lsp4ij.command.internal; import com.google.gson.Gson; import com.google.gson.JsonArray; import com.google.gson.JsonObject; import com.google.gson.JsonPrimitive; import com.intellij.openapi.actionSystem.ActionManager; import com.intellij.openapi.actionSystem.ActionPlaces; import com.intellij.openapi.actionSystem.AnAction; import com.intellij.openapi.actionSystem.DataContext; import com.intellij.openapi.actionSystem.ex.ActionUtil; import com.intellij.openapi.application.Application; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.editor.Document; import com.intellij.openapi.project.Project; import com.intellij.openapi.vfs.VirtualFile; import com.redhat.devtools.intellij.quarkus.lsp4ij.LSPIJUtils; import com.redhat.devtools.intellij.quarkus.lsp4ij.LanguageServersRegistry; import com.redhat.devtools.intellij.quarkus.lsp4ij.LanguageServiceAccessor; import org.eclipse.lsp4j.Command; import org.eclipse.lsp4j.ExecuteCommandOptions; import org.eclipse.lsp4j.ExecuteCommandParams; import org.eclipse.lsp4j.TextEdit; 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.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; /** * This class provides methods to execute {@link Command} instances. */ public class CommandExecutor { private static final Logger LOGGER = LoggerFactory.getLogger(CommandExecutor.class); private static final String LSP_COMMAND_CATEGORY_ID = "org.eclipse.lsp4e.commandCategory"; //$NON-NLS-1$ private static final String LSP_COMMAND_PARAMETER_TYPE_ID = "org.eclipse.lsp4e.commandParameterType"; //$NON-NLS-1$ private static final String LSP_PATH_PARAMETER_TYPE_ID = "org.eclipse.lsp4e.pathParameterType"; //$NON-NLS-1$ /** * Will execute the given {@code command} either on a language server, * supporting the command, or on the client, if an {@link IHandler} is * registered for the ID of the command (see {@link LSPCommandHandler}). If * {@code command} is {@code null}, then this method will do nothing. If neither * 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 document * 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. */ public static void executeCommand(Project project, Command command, Document document, String languageServerId) { if (command == null) { return; } if (executeCommandServerSide(project, command, languageServerId, document)) { return; } if (executeCommandClientSide(command, document)) { return; } // tentative fallback if (command.getArguments() != null) { WorkspaceEdit edit = createWorkspaceEdit(command.getArguments(), document); LSPIJUtils.applyWorkspaceEdit(edit); } } private static boolean executeCommandServerSide(Project project, Command command, String languageServerId, Document document) { if (languageServerId == null) { return false; } LanguageServersRegistry.LanguageServerDefinition languageServerDefinition = LanguageServersRegistry.getInstance() .getDefinition(languageServerId); if (languageServerDefinition == null) { return false; } try { CompletableFuture<LanguageServer> languageServerFuture = getLanguageServerForCommand(project, command, document, languageServerDefinition); if (languageServerFuture == null) { return false; } // Server can handle command languageServerFuture.thenAcceptAsync(server -> { ExecuteCommandParams params = new ExecuteCommandParams(); params.setCommand(command.getCommand()); params.setArguments(command.getArguments()); server.getWorkspaceService().executeCommand(params); }); return true; } catch (IOException e) { // log and let the code fall through for LSPEclipseUtils to handle LOGGER.warn(e.getLocalizedMessage(), e); return false; } } private static CompletableFuture<LanguageServer> getLanguageServerForCommand(Project project, Command command, Document document, LanguageServersRegistry.LanguageServerDefinition languageServerDefinition) throws IOException { CompletableFuture<LanguageServer> languageServerFuture = LanguageServiceAccessor.getInstance(project) .getInitializedLanguageServer(document, languageServerDefinition, serverCapabilities -> { ExecuteCommandOptions provider = serverCapabilities.getExecuteCommandProvider(); return provider != null && provider.getCommands().contains(command.getCommand()); }); return languageServerFuture; } @SuppressWarnings("unused") // ECJ compiler for some reason thinks handlerService == null is always false private static boolean executeCommandClientSide(Command command, Document document) { Application workbench = ApplicationManager.getApplication(); if (workbench == null) { return false; } URI context = LSPIJUtils.toUri(document); AnAction parameterizedCommand = createEclipseCoreCommand(command, context, workbench); if (parameterizedCommand == null) { return false; } DataContext dataContext = createDataContext(command, context, workbench); ActionUtil.invokeAction(parameterizedCommand, dataContext, ActionPlaces.UNKNOWN, null, null); return true; } private static AnAction createEclipseCoreCommand(Command command, URI context, Application workbench) { // Usually commands are defined via extension point, but we synthesize one on // the fly for the command ID, since we do not want downstream users // having to define them. String commandId = command.getCommand(); return ActionManager.getInstance().getAction(commandId); } private static DataContext createDataContext(Command command, URI context, Application workbench) { return new DataContext() { @Nullable @Override public Object getData(@NotNull String dataId) { if (LSP_COMMAND_PARAMETER_TYPE_ID.equals(dataId)) { return command; } else if (LSP_PATH_PARAMETER_TYPE_ID.equals(dataId)) { return context; } return null; } }; } // TODO consider using Entry/SimpleEntry instead private static final class Pair<K, V> { K key; V value; Pair(K key, V value) { this.key = key; this.value = value; } } // this method may be turned public if needed elsewhere /** * Very empirical and unsafe heuristic to turn unknown command arguments into a * workspace edit... */ private static WorkspaceEdit createWorkspaceEdit(List<Object> commandArguments, Document document) { WorkspaceEdit res = new WorkspaceEdit(); Map<String, List<TextEdit>> changes = new HashMap<>(); res.setChanges(changes); URI initialUri = LSPIJUtils.toUri(document); Pair<URI, List<TextEdit>> currentEntry = new Pair<>(initialUri, new ArrayList<>()); commandArguments.stream().flatMap(item -> { if (item instanceof List) { return ((List<?>) item).stream(); } else { return Collections.singleton(item).stream(); } }).forEach(arg -> { 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) { 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<String, List<TextEdit>> 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); } return res; } }