blob: eb07fd58182d5f0b287acd24f4172be711e7bf64 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2016-2017 Red Hat Inc. and others.
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Mickael Istria (Red Hat Inc.) - initial implementation
*******************************************************************************/
package org.eclipse.lsp4e.operations.codeactions;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IMarker;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.jdt.annotation.NonNull;
import org.eclipse.jface.resource.JFaceResources;
import org.eclipse.lsp4e.LSPEclipseUtils;
import org.eclipse.lsp4e.LanguageServerPlugin;
import org.eclipse.lsp4e.LanguageServersRegistry;
import org.eclipse.lsp4e.LanguageServersRegistry.LanguageServerDefinition;
import org.eclipse.lsp4e.LanguageServiceAccessor;
import org.eclipse.lsp4e.operations.diagnostics.LSPDiagnosticsToMarkers;
import org.eclipse.lsp4e.ui.Messages;
import org.eclipse.lsp4j.CodeAction;
import org.eclipse.lsp4j.CodeActionContext;
import org.eclipse.lsp4j.CodeActionOptions;
import org.eclipse.lsp4j.CodeActionParams;
import org.eclipse.lsp4j.Command;
import org.eclipse.lsp4j.Diagnostic;
import org.eclipse.lsp4j.ServerCapabilities;
import org.eclipse.lsp4j.TextDocumentIdentifier;
import org.eclipse.lsp4j.jsonrpc.messages.Either;
import org.eclipse.lsp4j.services.LanguageServer;
import org.eclipse.swt.graphics.Image;
import org.eclipse.ui.IMarkerResolution;
import org.eclipse.ui.IMarkerResolution2;
import org.eclipse.ui.IMarkerResolutionGenerator2;
import org.eclipse.ui.internal.progress.ProgressInfoItem;
public class LSPCodeActionMarkerResolution implements IMarkerResolutionGenerator2 {
private static final String LSP_REMEDIATION = "lspCodeActions"; //$NON-NLS-1$
private static final IMarkerResolution2 COMPUTING = new IMarkerResolution2() {
@Override
public void run(IMarker marker) {
// join on Future?
}
@Override
public String getLabel() {
return Messages.computing;
}
@Override
public Image getImage() {
// load class so image is loaded
return JFaceResources.getImage(ProgressInfoItem.class.getPackage().getName() + ".PROGRESS_DEFAULT"); //$NON-NLS-1$
}
@Override
public String getDescription() {
return Messages.computing;
}
};
@Override
public IMarkerResolution[] getResolutions(IMarker marker) {
Object att;
try {
checkMarkerResoultion(marker);
att = marker.getAttribute(LSP_REMEDIATION);
} catch (IOException | CoreException | ExecutionException | TimeoutException e) {
LanguageServerPlugin.logError(e);
return new IMarkerResolution[0];
} catch (InterruptedException e) {
LanguageServerPlugin.logError(e);
Thread.currentThread().interrupt();
return new IMarkerResolution[0];
}
if (att == COMPUTING) {
return new IMarkerResolution[] { COMPUTING };
}
List<Either<Command, CodeAction>> commands = (List<Either<Command, CodeAction>>) att;
if (commands == null) {
return new IMarkerResolution[0];
}
List<IMarkerResolution> res = new ArrayList<>(commands.size());
for (Either<Command, CodeAction> command : commands) {
if (command != null) {
if (command.isLeft()) {
res.add(new CommandMarkerResolution(command.getLeft()));
} else {
res.add(new CodeActionMarkerResolution(command.getRight()));
}
}
}
return res.toArray(new IMarkerResolution[res.size()]);
}
private void checkMarkerResoultion(IMarker marker) throws IOException, CoreException, InterruptedException, ExecutionException, TimeoutException {
if (marker.getAttribute(LSP_REMEDIATION) == null) {
IResource res = marker.getResource();
if (res != null && res.getType() == IResource.FILE) {
IFile file = (IFile)res;
String languageServerId = marker.getAttribute(LSPDiagnosticsToMarkers.LANGUAGE_SERVER_ID, null);
List<CompletableFuture<LanguageServer>> languageServerFutures = new ArrayList<>();
if (languageServerId != null) { // try to use same LS as the one that created the marker
LanguageServerDefinition definition = LanguageServersRegistry.getInstance().getDefinition(languageServerId);
if (definition != null) {
CompletableFuture<LanguageServer> serverFuture = LanguageServiceAccessor
.getInitializedLanguageServer(file, definition,
serverCapabilities -> serverCapabilities == null
|| providesCodeActions(serverCapabilities));
if (serverFuture != null) {
languageServerFutures.add(serverFuture);
}
}
}
if (languageServerFutures.isEmpty()) { // if it's not there, try any other server
languageServerFutures.addAll(LanguageServiceAccessor.getInitializedLanguageServers(file,
capabilities -> {
Either<Boolean, CodeActionOptions> codeActionProvider = capabilities
.getCodeActionProvider();
if (codeActionProvider == null) {
return false;
} else if (codeActionProvider.isLeft()) {
return Boolean.TRUE.equals(codeActionProvider.getLeft());
} else if (codeActionProvider.isRight()) {
return true;
}
return false;
}));
}
List<CompletableFuture<?>> futures = new ArrayList<>();
for (CompletableFuture<LanguageServer> lsf : languageServerFutures) {
marker.setAttribute(LSP_REMEDIATION, COMPUTING);
Diagnostic diagnostic = (Diagnostic)marker.getAttribute(LSPDiagnosticsToMarkers.LSP_DIAGNOSTIC);
CodeActionContext context = new CodeActionContext(Collections.singletonList(diagnostic));
CodeActionParams params = new CodeActionParams();
params.setContext(context);
params.setTextDocument(new TextDocumentIdentifier(LSPEclipseUtils.toUri(marker.getResource()).toString()));
params.setRange(diagnostic.getRange());
CompletableFuture<List<Either<Command, CodeAction>>> codeAction = lsf
.thenComposeAsync(ls -> ls.getTextDocumentService().codeAction(params));
futures.add(codeAction);
codeAction.thenAcceptAsync(actions -> {
try {
marker.setAttribute(LSP_REMEDIATION, actions);
} catch (CoreException e) {
LanguageServerPlugin.logError(e);
}
});
}
// wait a bit to avoid showing too much "Computing" without looking like a freeze
CompletableFuture.allOf(futures.toArray(new CompletableFuture[futures.size()])).get(300, TimeUnit.MILLISECONDS);
}
}
}
static boolean providesCodeActions(@NonNull ServerCapabilities serverCapabilities) {
Either<Boolean, CodeActionOptions> codeActionProvider = serverCapabilities.getCodeActionProvider();
if (codeActionProvider == null) {
return false;
}
if (codeActionProvider.isLeft()) {
return codeActionProvider.getLeft() != null && codeActionProvider.getLeft();
}
if (codeActionProvider.isRight()) {
return codeActionProvider.getRight() != null;
}
return false;
}
@Override
public boolean hasResolutions(IMarker marker) {
try {
checkMarkerResoultion(marker);
Object remediation = marker.getAttribute(LSP_REMEDIATION);
return remediation == COMPUTING || (remediation instanceof Collection && !((Collection<?>)remediation).isEmpty());
} catch (Exception ex) {
LanguageServerPlugin.logError(ex);
}
return false;
}
}