blob: a5df189e7ae31c74b97ac410e595fbeca28e9dad [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2016, 2019 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
* Lucas Bullen (Red Hat Inc.) - [Bug 517428] Requests sent before initialization
* Martin Lippert (Pivotal Inc.) - bug 531167, 531670, 536258
* Kris De Volder (Pivotal Inc.) - Get language servers by capability predicate.
*******************************************************************************/
package org.eclipse.lsp4e;
import java.io.IOException;
import java.net.URI;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Queue;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.Path;
import org.eclipse.core.runtime.content.IContentDescription;
import org.eclipse.core.runtime.content.IContentType;
import org.eclipse.jdt.annotation.NonNull;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jface.text.IDocument;
import org.eclipse.lsp4e.LanguageServersRegistry.LanguageServerDefinition;
import org.eclipse.lsp4e.server.StreamConnectionProvider;
import org.eclipse.lsp4j.ServerCapabilities;
import org.eclipse.lsp4j.TextDocumentIdentifier;
import org.eclipse.lsp4j.services.LanguageServer;
import org.eclipse.ui.IEditorReference;
import org.eclipse.ui.part.FileEditorInput;
/**
* The entry-point to retrieve a Language Server for a given resource/project.
* Deals with instantiations and caching of underlying
* {@link LanguageServerWrapper}.
*
*/
public class LanguageServiceAccessor {
private LanguageServiceAccessor() {
// this class shouldn't be instantiated
}
private static Set<LanguageServerWrapper> startedServers = new HashSet<>();
private static Map<StreamConnectionProvider, LanguageServerDefinition> providersToLSDefinitions = new HashMap<>();
/**
* This is meant for test code to clear state that might have leaked from other
* tests. It isn't meant to be used in production code.
*/
public static void clearStartedServers() {
synchronized (startedServers) {
startedServers.forEach(LanguageServerWrapper::stop);
startedServers.clear();
}
}
/**
* A bean storing association of a Document/File with a language server.
*/
public static class LSPDocumentInfo {
private final @NonNull URI fileUri;
private final @NonNull IDocument document;
private final @NonNull LanguageServerWrapper wrapper;
private LSPDocumentInfo(@NonNull URI fileUri, @NonNull IDocument document,
@NonNull LanguageServerWrapper wrapper) {
this.fileUri = fileUri;
this.document = document;
this.wrapper = wrapper;
}
public @NonNull IDocument getDocument() {
return this.document;
}
/**
* TODO consider directly returning a {@link TextDocumentIdentifier}
* @return
*/
public @NonNull URI getFileUri() {
return this.fileUri;
}
/**
* Returns the language server, regardless of if it is initialized.
*
* @deprecated use {@link #getInitializedLanguageClient()} instead.
*/
@Deprecated
public LanguageServer getLanguageClient() {
try {
return this.wrapper.getInitializedServer().get();
} catch (ExecutionException e) {
LanguageServerPlugin.logError(e);
return this.wrapper.getServer();
} catch (InterruptedException e) {
LanguageServerPlugin.logError(e);
Thread.currentThread().interrupt();
return this.wrapper.getServer();
}
}
public int getVersion() {
return wrapper.getVersion(LSPEclipseUtils.getFile(document));
}
public CompletableFuture<LanguageServer> getInitializedLanguageClient() {
return this.wrapper.getInitializedServer();
}
public @Nullable ServerCapabilities getCapabilites() {
return this.wrapper.getServerCapabilities();
}
public boolean isActive() {
return this.wrapper.isActive();
}
}
public static @NonNull List<CompletableFuture<LanguageServer>> getInitializedLanguageServers(@NonNull IFile file,
@Nullable Predicate<ServerCapabilities> request) throws IOException {
synchronized (startedServers) {
Collection<LanguageServerWrapper> wrappers = getLSWrappers(file, request);
return wrappers.stream().map(wrapper -> wrapper.getInitializedServer().thenApplyAsync(server -> {
try {
wrapper.connect(file, null);
} catch (IOException e) {
LanguageServerPlugin.logError(e);
}
return server;
})).collect(Collectors.toList());
}
}
public static void disableLanguageServerContentType(
@NonNull ContentTypeToLanguageServerDefinition contentTypeToLSDefinition) {
Optional<LanguageServerWrapper> result = startedServers.stream()
.filter(server -> server.serverDefinition.equals(contentTypeToLSDefinition.getValue())).findFirst();
if (result.isPresent()) {
IContentType contentType = contentTypeToLSDefinition.getKey();
if (contentType != null) {
result.get().disconnectContentType(contentType);
}
}
}
public static void enableLanguageServerContentType(
@NonNull ContentTypeToLanguageServerDefinition contentTypeToLSDefinition,
@NonNull IEditorReference[] editors) {
for (IEditorReference editor : editors) {
try {
if (editor.getEditorInput() instanceof FileEditorInput) {
IFile editorFile = ((FileEditorInput) editor.getEditorInput()).getFile();
IContentType contentType = contentTypeToLSDefinition.getKey();
LanguageServerDefinition lsDefinition = contentTypeToLSDefinition.getValue();
IContentDescription contentDesc = editorFile.getContentDescription();
if (contentTypeToLSDefinition.isEnabled() && contentType != null && contentDesc != null
&& contentType.equals(contentDesc.getContentType())
&& lsDefinition != null) {
try {
getInitializedLanguageServer(editorFile, lsDefinition, capabilities -> true);
} catch (IOException e) {
LanguageServerPlugin.logError(e);
}
}
}
} catch (CoreException e) {
LanguageServerPlugin.logError(e);
}
}
}
/**
* Get the requested language server instance for the given file. Starts the language server if not already started.
* @param file
* @param serverId
* @param capabilitesPredicate a predicate to check capabilities
* @return a LanguageServer for the given file, which is defined with provided server ID and conforms to specified request
* @deprecated use {@link #getInitializedLanguageServer(IFile, LanguageServerDefinition, Predicate)} instead.
*/
@Deprecated
public static LanguageServer getLanguageServer(@NonNull IFile file, @NonNull LanguageServerDefinition lsDefinition,
Predicate<ServerCapabilities> capabilitiesPredicate)
throws IOException {
LanguageServerWrapper wrapper = getLSWrapperForConnection(file.getProject(), lsDefinition);
if (wrapper != null && (capabilitiesPredicate == null
|| wrapper.getServerCapabilities() == null /* null check is workaround for https://github.com/TypeFox/ls-api/issues/47 */
|| capabilitiesPredicate.test(wrapper.getServerCapabilities()))) {
wrapper.connect(file, null);
return wrapper.getServer();
}
return null;
}
/**
* Get the requested language server instance for the given file. Starts the language server if not already started.
* @param file
* @param serverId
* @param capabilitesPredicate a predicate to check capabilities
* @return a LanguageServer for the given file, which is defined with provided server ID and conforms to specified request
*/
public static CompletableFuture<LanguageServer> getInitializedLanguageServer(@NonNull IFile file,
@NonNull LanguageServerDefinition lsDefinition,
Predicate<ServerCapabilities> capabilitiesPredicate)
throws IOException {
LanguageServerWrapper wrapper = getLSWrapperForConnection(file.getProject(), lsDefinition);
if (wrapper != null && (capabilitiesPredicate == null
|| wrapper.getServerCapabilities() == null /* null check is workaround for https://github.com/TypeFox/ls-api/issues/47 */
|| capabilitiesPredicate.test(wrapper.getServerCapabilities()))) {
wrapper.connect(file, null);
return wrapper.getInitializedServer();
}
return null;
}
/**
* TODO we need a similar method for generic IDocument (enabling non-IFiles)
*
* @param file
* @param request
* @return
* @throws IOException
* @noreference This method is currently internal and should only be referenced
* for testing
*/
@NonNull
public static Collection<LanguageServerWrapper> getLSWrappers(@NonNull IFile file,
@Nullable Predicate<ServerCapabilities> request) throws IOException {
LinkedHashSet<LanguageServerWrapper> res = new LinkedHashSet<>();
IProject project = file.getProject();
if (project == null) {
return res;
}
res.addAll(getMatchingStartedWrappers(file, request));
// look for running language servers via content-type
Queue<IContentType> contentTypes = new LinkedList<>();
Set<IContentType> addedContentTypes = new HashSet<>();
contentTypes.addAll(LSPEclipseUtils.getFileContentTypes(file));
addedContentTypes.addAll(contentTypes);
while (!contentTypes.isEmpty()) {
IContentType contentType = contentTypes.poll();
if (contentType == null) {
continue;
}
for (ContentTypeToLanguageServerDefinition mapping : LanguageServersRegistry.getInstance().findProviderFor(contentType)) {
if (mapping != null && mapping.getValue() != null && mapping.isEnabled()) {
LanguageServerWrapper wrapper = getLSWrapperForConnection(project, mapping.getValue());
if (request == null
|| wrapper.getServerCapabilities() == null /* null check is workaround for https://github.com/TypeFox/ls-api/issues/47 */
|| request.test(wrapper.getServerCapabilities())) {
res.add(wrapper);
}
}
}
if (contentType.getBaseType() != null && !addedContentTypes.contains(contentType.getBaseType())) {
addedContentTypes.add(contentType.getBaseType());
contentTypes.add(contentType.getBaseType());
}
}
return res;
}
@NonNull
private static Collection<LanguageServerWrapper> getLSWrappers(@NonNull IDocument document) {
LinkedHashSet<LanguageServerWrapper> res = new LinkedHashSet<>();
IFile file = LSPEclipseUtils.getFile(document);
URI uri = LSPEclipseUtils.toUri(document);
if (uri == null) {
return Collections.emptyList();
}
IPath path = new Path(uri.getPath());
// look for running language servers via content-type
Queue<IContentType> contentTypes = new LinkedList<>();
Set<IContentType> processedContentTypes = new HashSet<>();
contentTypes.addAll(LSPEclipseUtils.getDocumentContentTypes(document));
synchronized (startedServers) {
// already started compatible servers that fit request
res.addAll(startedServers.stream()
.filter(wrapper -> {
try {
return wrapper.isConnectedTo(path) || LanguageServersRegistry.getInstance().matches(document, wrapper.serverDefinition);
} catch (Exception e) {
LanguageServerPlugin.logError(e);
return false;
}
})
.filter(wrapper -> wrapper.canOperate(document))
.collect(Collectors.toList()));
while (!contentTypes.isEmpty()) {
IContentType contentType = contentTypes.poll();
if (contentType == null || processedContentTypes.contains(contentType)) {
continue;
}
for (ContentTypeToLanguageServerDefinition mapping : LanguageServersRegistry.getInstance()
.findProviderFor(contentType)) {
if (mapping == null || !mapping.isEnabled()) {
continue;
}
LanguageServerDefinition serverDefinition = mapping.getValue();
if (serverDefinition == null) {
continue;
}
if (startedServers.stream().anyMatch(wrapper -> wrapper.serverDefinition.equals(serverDefinition)
&& wrapper.canOperate(document))) {
// we already checked a compatible LS with this definition
continue;
}
LanguageServerWrapper wrapper = new LanguageServerWrapper(file != null ? file.getProject() : null,
serverDefinition);
startedServers.add(wrapper);
res.add(wrapper);
}
if (contentType.getBaseType() != null) {
contentTypes.add(contentType.getBaseType());
}
processedContentTypes.add(contentType);
}
return res;
}
}
/**
* Return existing {@link LanguageServerWrapper} for the given connection. If
* not found, create a new one with the given connection and register it for
* this project/content-type.
*
* @param project
* @param serverDefinition
* @return
* @throws IOException
* @Deprecated will be made private soon
* @noreference will be made private soon
* @deprecated
*/
@Deprecated
public static LanguageServerWrapper getLSWrapperForConnection(@NonNull IProject project,
@NonNull LanguageServerDefinition serverDefinition) throws IOException {
LanguageServerWrapper wrapper = null;
synchronized(startedServers) {
for (LanguageServerWrapper startedWrapper : getStartedLSWrappers(project)) {
if (startedWrapper.serverDefinition.equals(serverDefinition)) {
wrapper = startedWrapper;
break;
}
}
if (wrapper == null) {
wrapper = new LanguageServerWrapper(project, serverDefinition);
wrapper.start();
}
startedServers.add(wrapper);
}
return wrapper;
}
private static @NonNull List<LanguageServerWrapper> getStartedLSWrappers(
@NonNull IProject project) {
return startedServers.stream().filter(wrapper -> wrapper.canOperate(project))
.collect(Collectors.toList());
// TODO multi-root: also return servers which support multi-root?
}
private static Collection<LanguageServerWrapper> getMatchingStartedWrappers(@NonNull IFile file,
@Nullable Predicate<ServerCapabilities> request) {
synchronized (startedServers) {
return startedServers.stream().filter(wrapper -> {
return wrapper.isConnectedTo(file.getLocation())
|| (LanguageServersRegistry.getInstance().matches(file, wrapper.serverDefinition)
&& wrapper.canOperate(file.getProject()));
}).filter(wrapper -> request == null
|| (wrapper.getServerCapabilities() == null || request.test(wrapper.getServerCapabilities())))
.collect(Collectors.toList());
}
}
/**
* Gets list of running LS satisfying a capability predicate. This does not
* start any matching language servers, it returns the already running ones.
*
* @param request
* @return list of Language Servers
*/
@NonNull
public static List<@NonNull LanguageServer> getActiveLanguageServers(Predicate<ServerCapabilities> request) {
return getLanguageServers(null, request, true);
}
/**
* Gets list of LS initialized for given project.
*
* @param project
* @param request
* @return list of Language Servers
*/
@NonNull
public static List<@NonNull LanguageServer> getLanguageServers(@NonNull IProject project,
Predicate<ServerCapabilities> request) {
return getLanguageServers(project, request, false);
}
/**
* Gets list of LS initialized for given project
*
* @param onlyActiveLS
* true if this method should return only the already running
* language servers, otherwise previously started language servers
* will be re-activated
* @return list of Language Servers
*/
@NonNull
public static List<@NonNull LanguageServer> getLanguageServers(@Nullable IProject project,
Predicate<ServerCapabilities> request, boolean onlyActiveLS) {
List<@NonNull LanguageServer> serverInfos = new ArrayList<>();
for (LanguageServerWrapper wrapper : startedServers) {
if ((!onlyActiveLS || wrapper.isActive()) && (project == null || wrapper.canOperate(project))) {
@Nullable
LanguageServer server = wrapper.getServer();
if (server == null) {
continue;
}
if (request == null
|| wrapper.getServerCapabilities() == null /* null check is workaround for https://github.com/TypeFox/ls-api/issues/47 */
|| request.test(wrapper.getServerCapabilities())) {
serverInfos.add(server);
}
}
}
return serverInfos;
}
protected static LanguageServerDefinition getLSDefinition(@NonNull StreamConnectionProvider provider) {
return providersToLSDefinitions.get(provider);
}
@NonNull public static List<@NonNull LSPDocumentInfo> getLSPDocumentInfosFor(@NonNull IDocument document, @NonNull Predicate<ServerCapabilities> capabilityRequest) {
URI fileUri = LSPEclipseUtils.toUri(document);
List<LSPDocumentInfo> res = new ArrayList<>();
try {
getLSWrappers(document).stream().filter(wrapper -> wrapper.getServerCapabilities() == null
|| capabilityRequest.test(wrapper.getServerCapabilities())).forEach(wrapper -> {
try {
wrapper.connect(document);
} catch (IOException e) {
LanguageServerPlugin.logError(e);
}
res.add(new LSPDocumentInfo(fileUri, document, wrapper));
});
} catch (final Exception e) {
LanguageServerPlugin.logError(e);
}
return res;
}
/**
*
* @param document
* @param filter
* @return
* @since 0.9
*/
@NonNull
public static CompletableFuture<List<@NonNull LanguageServer>> getLanguageServers(@NonNull IDocument document,
Predicate<ServerCapabilities> filter) {
URI uri = LSPEclipseUtils.toUri(document);
if (uri == null) {
return CompletableFuture.completedFuture(Collections.emptyList());
}
final IPath path = new Path(uri.getPath());
final List<CompletableFuture<?>> serverRequests = new ArrayList<>();
final List<@NonNull LanguageServer> res = Collections.synchronizedList(new ArrayList<>());
try {
for (final LanguageServerWrapper wrapper : getLSWrappers(document)) {
serverRequests.add(wrapper.getInitializedServer().thenAcceptAsync(server -> {
if (server != null && (filter == null || filter.test(wrapper.getServerCapabilities()))) {
try {
wrapper.connect(path, document);
} catch (IOException ex) {
LanguageServerPlugin.logError(ex);
}
res.add(server);
}
}));
}
return CompletableFuture.allOf(serverRequests.toArray(new CompletableFuture[serverRequests.size()]))
.thenApply(theVoid -> res);
} catch (final Exception e) {
LanguageServerPlugin.logError(e);
}
return CompletableFuture.completedFuture(Collections.emptyList());
}
public static boolean checkCapability(LanguageServer languageServer, Predicate<ServerCapabilities> condition) {
return startedServers.stream().filter(wrapper -> wrapper.getServer() == languageServer)
.anyMatch(wrapper -> condition.test(wrapper.getServerCapabilities()));
}
}