Skip to content

Commit

Permalink
feat: add command to update microprofile configuration from LSP4MP
Browse files Browse the repository at this point in the history
Signed-off-by: Fred Bricon <[email protected]>
  • Loading branch information
fbricon committed Sep 5, 2023
1 parent c8810d5 commit 6a8e73f
Show file tree
Hide file tree
Showing 14 changed files with 215 additions and 97 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -22,21 +22,19 @@
import com.intellij.openapi.actionSystem.AnAction;
import com.intellij.openapi.actionSystem.AnActionEvent;
import com.intellij.openapi.actionSystem.DataContext;
import com.intellij.openapi.actionSystem.DataKey;
import com.intellij.openapi.actionSystem.Presentation;
import com.intellij.openapi.actionSystem.impl.SimpleDataContext;
import com.intellij.ui.layout.LCFlags;
import com.intellij.ui.layout.LayoutKt;
import com.redhat.devtools.intellij.lsp4ij.commands.CommandExecutor;
import org.eclipse.lsp4j.Command;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import javax.swing.JComponent;
import java.awt.Component;
import java.util.Map;

public abstract class AbstractLSPInlayProvider implements InlayHintsProvider<NoSettings> {
public static final DataKey<Command> LSP_COMMAND = DataKey.create("com.redhat.devtools.intellij.quarkus.lsp4ij.command");

private SettingsKey<NoSettings> key = new SettingsKey<>("LSP.hints");

Expand Down Expand Up @@ -92,7 +90,7 @@ protected void executeClientCommand(Component source, Command command) {
if (command != null) {
AnAction action = ActionManager.getInstance().getAction(command.getCommand());
if (action != null) {
DataContext context = SimpleDataContext.getSimpleContext(DataKey.create(LSP_COMMAND.getName()), command, DataManager.getInstance().getDataContext(source));
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));
Expand Down
13 changes: 13 additions & 0 deletions src/main/java/com/redhat/devtools/intellij/lsp4ij/LSPIJUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,19 @@ public static Document getDocument(VirtualFile docFile) {
return FileDocumentManager.getInstance().getDocument(docFile);
}

/**
* Returns the @{@link Document} associated to the given @{@link URI}, or <code>null</code> if there's no match.
* @param documentUri the uri of the Document to return
* @return the @{@link Document} associated to <code>documentUri</code>, or <code>null</code>
*/
public static @Nullable Document getDocument(URI documentUri) {
if (documentUri == null) {
return null;
}
VirtualFile documentFile = findResourceFor(documentUri.toASCIIString());
return getDocument(documentFile);
}

public static @Nullable Module getProject(@Nullable VirtualFile file) {
if (file == null) {
return null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ public CompletableFuture<LanguageServer> getInitializedLanguageServer(Document d
throws IOException {
URI initialPath = LSPIJUtils.toUri(document);
LanguageServerWrapper wrapper = getLSWrapperForConnection(document, lsDefinition, initialPath);
if (capabilitiesComply(wrapper, capabilitiesPredicate)) {
if (wrapper != null && capabilitiesComply(wrapper, capabilitiesPredicate)) {
wrapper.connect(document);
return wrapper.getInitializedServer();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,15 @@
* Red Hat, Inc. - initial API and implementation
* Fraunhofer FOKUS
******************************************************************************/
package com.redhat.devtools.intellij.lsp4ij.command.internal;

package com.redhat.devtools.intellij.lsp4ij.commands;

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.*;
import com.intellij.openapi.actionSystem.ex.ActionUtil;
import com.intellij.openapi.actionSystem.impl.SimpleDataContext;
import com.intellij.openapi.application.Application;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.editor.Document;
Expand All @@ -36,7 +33,6 @@
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;

Expand All @@ -55,48 +51,51 @@
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$
public static final DataKey<Command> LSP_COMMAND = DataKey.create("com.redhat.devtools.intellij.quarkus.lsp4ij.command");

public static final DataKey<URI> LSP_COMMAND_DOCUMENT_URI = DataKey.create("com.redhat.devtools.intellij.quarkus.lsp4ij.command.documentUri");

/**
* 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
* supporting the command, or on the client, if an {@link AnAction} is
* registered for the ID of the command. 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 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.
*/
public static void executeCommand(Project project, Command command, Document document,
public static void executeCommand(Project project, Command command, URI documentUri,
String languageServerId) {
if (command == null) {
return;
}
if (executeCommandServerSide(project, command, languageServerId, document)) {
if (executeCommandServerSide(project, command, documentUri, languageServerId)) {
return;
}
if (executeCommandClientSide(command, document)) {
if (executeCommandClientSide(project, command, documentUri)) {
return;
}
// tentative fallback
if (command.getArguments() != null) {
WorkspaceEdit edit = createWorkspaceEdit(command.getArguments(), document);
LSPIJUtils.applyWorkspaceEdit(edit);
if (documentUri != null && command.getArguments() != null) {
Document document = LSPIJUtils.getDocument(documentUri);
if (document != null) {
WorkspaceEdit edit = createWorkspaceEdit(command.getArguments(), document);
LSPIJUtils.applyWorkspaceEdit(edit);
}
}
}

private static boolean executeCommandServerSide(Project project, Command command, String languageServerId,
Document document) {
private static boolean executeCommandServerSide(Project project, Command command,
URI documentUri, String languageServerId) {
if (languageServerId == null) {
return false;
}
Expand All @@ -107,7 +106,7 @@ private static boolean executeCommandServerSide(Project project, Command command
}

try {
CompletableFuture<LanguageServer> languageServerFuture = getLanguageServerForCommand(project, command, document,
CompletableFuture<LanguageServer> languageServerFuture = getLanguageServerForCommand(project, command, documentUri,
languageServerDefinition);
if (languageServerFuture == null) {
return false;
Expand All @@ -130,55 +129,48 @@ private static boolean executeCommandServerSide(Project project, Command command

private static CompletableFuture<LanguageServer> getLanguageServerForCommand(Project project,
Command command,
Document document, LanguageServersRegistry.LanguageServerDefinition languageServerDefinition) throws IOException {
CompletableFuture<LanguageServer> languageServerFuture = LanguageServiceAccessor.getInstance(project)
URI documentUri, LanguageServersRegistry.LanguageServerDefinition languageServerDefinition) throws IOException {
Document document = LSPIJUtils.getDocument(documentUri);
if (document == null) {
return null;
}
return LanguageServiceAccessor.getInstance(project)
//TODO pass documentUri instead of document, but looks like that implies non-trivial refactoring
.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) {
private static boolean executeCommandClientSide(Project project, Command command, URI documentUri) {
Application workbench = ApplicationManager.getApplication();
if (workbench == null) {
return false;
}
URI context = LSPIJUtils.toUri(document);
AnAction parameterizedCommand = createEclipseCoreCommand(command, context, workbench);
AnAction parameterizedCommand = createIDEACoreCommand(command);
if (parameterizedCommand == null) {
return false;
}
DataContext dataContext = createDataContext(command, context, workbench);
DataContext dataContext = createDataContext(project, command, documentUri);
ActionUtil.invokeAction(parameterizedCommand, dataContext, ActionPlaces.UNKNOWN, null, null);
return true;
}

private static AnAction createEclipseCoreCommand(Command command, URI context,
Application workbench) {
private static AnAction createIDEACoreCommand(Command command) {
// 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) {
private static DataContext createDataContext(Project project, Command command, URI documentUri) {

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;
}
};
SimpleDataContext.Builder contextBuilder = SimpleDataContext.builder();
contextBuilder.add(CommonDataKeys.PROJECT, project)
.add(LSP_COMMAND, command)
.add(LSP_COMMAND_DOCUMENT_URI, documentUri);
return contextBuilder.build();
}

// TODO consider using Entry/SimpleEntry instead
Expand All @@ -197,7 +189,7 @@ private static final class Pair<K, V> {
* Very empirical and unsafe heuristic to turn unknown command arguments into a
* workspace edit...
*/
private static WorkspaceEdit createWorkspaceEdit(List<Object> commandArguments, Document document) {
private static WorkspaceEdit createWorkspaceEdit(List<Object> commandArguments, @NotNull Document document) {
WorkspaceEdit res = new WorkspaceEdit();
Map<String, List<TextEdit>> changes = new HashMap<>();
res.setChanges(changes);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,12 @@
import com.intellij.util.IncorrectOperationException;
import com.redhat.devtools.intellij.lsp4ij.LSPIJUtils;
import com.redhat.devtools.intellij.lsp4ij.LanguageServerWrapper;
import com.redhat.devtools.intellij.lsp4ij.commands.CommandExecutor;
import org.apache.commons.lang.StringUtils;
import org.eclipse.lsp4j.*;
import org.eclipse.lsp4j.CodeAction;
import org.eclipse.lsp4j.CodeActionOptions;
import org.eclipse.lsp4j.Command;
import org.eclipse.lsp4j.ServerCapabilities;
import org.eclipse.lsp4j.jsonrpc.messages.Either;
import org.jetbrains.annotations.NotNull;

Expand Down Expand Up @@ -71,6 +75,7 @@ public boolean isAvailable(@NotNull Project project, Editor editor, PsiFile file

@Override
public void invoke(@NotNull Project project, Editor editor, PsiFile file) throws IncorrectOperationException {
String serverId = getLanguageServerWrapper().serverDefinition.id;
if (codeAction != null) {
if (codeAction.getEdit() == null && codeAction.getCommand() == null && isCodeActionResolveSupported()) {
// Unresolved code action "edit" property. Resolve it.
Expand All @@ -80,52 +85,34 @@ public void invoke(@NotNull Project project, Editor editor, PsiFile file) throws
.thenAccept(resolved -> {
ApplicationManager.getApplication().invokeLater(() -> {
DocumentUtil.writeInRunUndoTransparentAction(() -> {
apply(resolved != null ? resolved : codeAction, project);
apply(resolved != null ? resolved : codeAction, project, file, serverId);
});
});
})
);
} else {
apply(codeAction, project);
apply(codeAction, project, file, serverId);
}
} else if (command != null) {
executeCommand(command, project);
executeCommand(command, project, file, serverId);
} else {
// Should never get here
}
}

private void apply(CodeAction codeaction, @NotNull Project project) {
private void apply(CodeAction codeaction, @NotNull Project project, PsiFile file, String serverId ) {
if (codeaction != null) {
if (codeaction.getEdit() != null) {
LSPIJUtils.applyWorkspaceEdit(codeaction.getEdit(), codeaction.getTitle());
}
if (codeaction.getCommand() != null) {
executeCommand(codeaction.getCommand(), project);
executeCommand(codeaction.getCommand(), project, file, serverId);
}
}
}

private void executeCommand(Command command, @NotNull Project project) {
if (!canSupportCommand(command)) {
return;
}
ExecuteCommandParams params = new ExecuteCommandParams();
params.setCommand(command.getCommand());
params.setArguments(command.getArguments());
getLanguageServerWrapper()
.getInitializedServer()
.thenApply(ls -> ls.getWorkspaceService().executeCommand(params)
);
}

private boolean canSupportCommand(Command command) {
ServerCapabilities capabilities = getLanguageServerWrapper().getServerCapabilities();
if (capabilities != null) {
ExecuteCommandOptions provider = capabilities.getExecuteCommandProvider();
return (provider != null && provider.getCommands().contains(command.getCommand()));
}
return false;
private void executeCommand(Command command, @NotNull Project project, PsiFile file, String serverId) {
CommandExecutor.executeCommand(project, command, LSPIJUtils.toUri(file), serverId);
}

private LanguageServerWrapper getLanguageServerWrapper() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
import com.redhat.devtools.intellij.lsp4ij.LSPIJUtils;
import com.redhat.devtools.intellij.lsp4ij.LanguageServerItem;
import com.redhat.devtools.intellij.lsp4ij.LanguageServiceAccessor;
import com.redhat.devtools.intellij.lsp4ij.command.internal.CommandExecutor;
import com.redhat.devtools.intellij.lsp4ij.commands.CommandExecutor;
import com.redhat.devtools.intellij.lsp4ij.operations.completion.snippet.LspSnippetIndentOptions;
import org.apache.commons.lang.StringUtils;
import org.eclipse.lsp4j.*;
Expand All @@ -37,6 +37,7 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.net.URI;
import java.util.*;
import java.util.concurrent.CancellationException;
import java.util.concurrent.ExecutionException;
Expand Down Expand Up @@ -265,25 +266,26 @@ protected void apply(Document document, char trigger, int stateMask, int offset)
// Execute custom command of the completion item if needed
Command command = item.getCommand();
if (command != null) {
executeCustomCommand(command, document);
executeCustomCommand(command, LSPIJUtils.toUri(document));
}
} catch (RuntimeException ex) {
LOGGER.warn(ex.getLocalizedMessage(), ex);
}
}


/**
* Execute custom command of the completion item.
*
* @param document
* @param command
* @param documentUri
*/
private void executeCustomCommand(@NotNull Command command, Document document) {
private void executeCustomCommand(@NotNull Command command, URI documentUri) {
Project project = editor.getProject();
// Execute custom command of the completion item.
LanguageServiceAccessor.getInstance(project)
.resolveServerDefinition(languageServer.getServer()).map(definition -> definition.id)
.ifPresent(id -> {
CommandExecutor.executeCommand(project, command, document, id);
CommandExecutor.executeCommand(project, command, documentUri, id);
});

}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import com.intellij.ide.BrowserUtil;
import com.intellij.openapi.actionSystem.AnAction;
import com.intellij.openapi.actionSystem.AnActionEvent;
import com.redhat.devtools.intellij.lsp4ij.operations.codelens.LSPCodelensInlayProvider;
import com.redhat.devtools.intellij.lsp4ij.commands.CommandExecutor;

import java.util.List;

Expand All @@ -20,7 +20,7 @@ public void actionPerformed(AnActionEvent e) {

private String getURL(AnActionEvent e) {
String url = null;
List<Object> arguments = e.getData(LSPCodelensInlayProvider.LSP_COMMAND).getArguments();
List<Object> arguments = e.getData(CommandExecutor.LSP_COMMAND).getArguments();
if (!arguments.isEmpty()) {
Object arg = arguments.get(0);
if (arg instanceof JsonPrimitive) {
Expand Down
Loading

0 comments on commit 6a8e73f

Please sign in to comment.