blob: 287ab3110cecad954be8a32f157593d71a0418e0 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2016, 2020 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
* Miro Spoenemann (TypeFox) - added clientImpl and serverInterface attributes
* Alexander Fedorov (ArSysOp) - added parent context to evaluation
*******************************************************************************/
package org.eclipse.lsp4e;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
import org.eclipse.core.expressions.ExpressionConverter;
import org.eclipse.core.expressions.IEvaluationContext;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IConfigurationElement;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Platform;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.content.IContentType;
import org.eclipse.debug.core.ILaunchConfiguration;
import org.eclipse.jdt.annotation.NonNull;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jface.preference.IPersistentPreferenceStore;
import org.eclipse.jface.preference.IPreferenceStore;
import org.eclipse.jface.text.IDocument;
import org.eclipse.lsp4e.enablement.EnablementTester;
import org.eclipse.lsp4e.server.StreamConnectionProvider;
import org.eclipse.lsp4j.jsonrpc.Launcher;
import org.eclipse.lsp4j.services.LanguageServer;
import org.eclipse.ui.IEditorInput;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.handlers.IHandlerService;
import org.eclipse.ui.statushandlers.StatusManager;
import org.osgi.framework.Bundle;
/**
* This registry aims at providing a good language server connection (as {@link StreamConnectionProvider}
* for a given input.
* At the moment, registry content are hardcoded but we'll very soon need a way
* to contribute to it via plugin.xml (for plugin developers) and from Preferences
* (for end-users to directly register a new server).
*
*/
public class LanguageServersRegistry {
private static final String CONTENT_TYPE_TO_LSP_LAUNCH_PREF_KEY = "contentTypeToLSPLauch"; //$NON-NLS-1$
private static final String EXTENSION_POINT_ID = LanguageServerPlugin.PLUGIN_ID + ".languageServer"; //$NON-NLS-1$
private static final String LS_ELEMENT = "server"; //$NON-NLS-1$
private static final String MAPPING_ELEMENT = "contentTypeMapping"; //$NON-NLS-1$
private static final String ID_ATTRIBUTE = "id"; //$NON-NLS-1$
private static final String SINGLETON_ATTRIBUTE = "singleton"; //$NON-NLS-1$
private static final String CONTENT_TYPE_ATTRIBUTE = "contentType"; //$NON-NLS-1$
private static final String LANGUAGE_ID_ATTRIBUTE = "languageId"; //$NON-NLS-1$
private static final String CLASS_ATTRIBUTE = "class"; //$NON-NLS-1$
private static final String CLIENT_IMPL_ATTRIBUTE = "clientImpl"; //$NON-NLS-1$
private static final String SERVER_INTERFACE_ATTRIBUTE = "serverInterface"; //$NON-NLS-1$
private static final String LAUNCHER_BUILDER_ATTRIBUTE = "launcherBuilder"; //$NON-NLS-1$
private static final String LABEL_ATTRIBUTE = "label"; //$NON-NLS-1$
private static final String ENABLED_WHEN_ATTRIBUTE = "enabledWhen"; //$NON-NLS-1$
private static final String ENABLED_WHEN_DESC = "description"; //$NON-NLS-1$
public abstract static class LanguageServerDefinition {
public final @NonNull String id;
public final @NonNull String label;
public final boolean isSingleton;
public final @NonNull Map<IContentType, String> langugeIdMappings;
public LanguageServerDefinition(@NonNull String id, @NonNull String label, boolean isSingleton) {
this.id = id;
this.label = label;
this.isSingleton = isSingleton;
this.langugeIdMappings = new ConcurrentHashMap<>();
}
public void registerAssociation(@NonNull IContentType contentType, @NonNull String languageId) {
this.langugeIdMappings.put(contentType, languageId);
}
public abstract StreamConnectionProvider createConnectionProvider();
public LanguageClientImpl createLanguageClient() {
return new LanguageClientImpl();
}
public Class<? extends LanguageServer> getServerInterface() {
return LanguageServer.class;
}
public <S extends LanguageServer> Launcher.Builder<S> createLauncherBuilder() {
return new Launcher.Builder<S>();
}
}
static class ExtensionLanguageServerDefinition extends LanguageServerDefinition {
private IConfigurationElement extension;
public ExtensionLanguageServerDefinition(IConfigurationElement element) {
super(element.getAttribute(ID_ATTRIBUTE), element.getAttribute(LABEL_ATTRIBUTE), Boolean.parseBoolean(element.getAttribute(SINGLETON_ATTRIBUTE)));
this.extension = element;
}
@Override
public StreamConnectionProvider createConnectionProvider() {
try {
return (StreamConnectionProvider) extension.createExecutableExtension(CLASS_ATTRIBUTE);
} catch (CoreException e) {
StatusManager.getManager().handle(e, LanguageServerPlugin.PLUGIN_ID);
throw new RuntimeException(
"Exception occurred while creating an instance of the stream connection provider", e); //$NON-NLS-1$
}
}
@Override
public LanguageClientImpl createLanguageClient() {
String clientImpl = extension.getAttribute(CLIENT_IMPL_ATTRIBUTE);
if (clientImpl != null && !clientImpl.isEmpty()) {
try {
return (LanguageClientImpl) extension.createExecutableExtension(CLIENT_IMPL_ATTRIBUTE);
} catch (CoreException e) {
StatusManager.getManager().handle(e, LanguageServerPlugin.PLUGIN_ID);
}
}
return super.createLanguageClient();
}
@SuppressWarnings("unchecked")
@Override
public Class<? extends LanguageServer> getServerInterface() {
String serverInterface = extension.getAttribute(SERVER_INTERFACE_ATTRIBUTE);
if (serverInterface != null && !serverInterface.isEmpty()) {
Bundle bundle = Platform.getBundle(extension.getContributor().getName());
if (bundle != null) {
try {
return (Class<? extends LanguageServer>) bundle.loadClass(serverInterface);
} catch (ClassNotFoundException exception) {
StatusManager.getManager().handle(new Status(IStatus.ERROR, LanguageServerPlugin.PLUGIN_ID,
exception.getMessage(), exception));
}
}
}
return super.getServerInterface();
}
@SuppressWarnings("unchecked")
@Override
public <S extends LanguageServer> Launcher.Builder<S> createLauncherBuilder() {
String launcherSupplier = extension.getAttribute(LAUNCHER_BUILDER_ATTRIBUTE);
if (launcherSupplier != null && !launcherSupplier.isEmpty()) {
try {
return (Launcher.Builder<S>) extension.createExecutableExtension(LAUNCHER_BUILDER_ATTRIBUTE);
} catch (CoreException e) {
StatusManager.getManager().handle(e, LanguageServerPlugin.PLUGIN_ID);
}
}
return super.createLauncherBuilder();
}
}
static class LaunchConfigurationLanguageServerDefinition extends LanguageServerDefinition {
final ILaunchConfiguration launchConfiguration;
final Set<String> launchModes;
public LaunchConfigurationLanguageServerDefinition(ILaunchConfiguration launchConfiguration,
Set<String> launchModes) {
super(launchConfiguration.getName(), launchConfiguration.getName(), false);
this.launchConfiguration = launchConfiguration;
this.launchModes = launchModes;
}
@Override
public StreamConnectionProvider createConnectionProvider() {
return new LaunchConfigurationStreamProvider(this.launchConfiguration, launchModes);
}
}
private static LanguageServersRegistry INSTANCE = null;
public static LanguageServersRegistry getInstance() {
if (INSTANCE == null) {
INSTANCE = new LanguageServersRegistry();
}
return INSTANCE;
}
private List<ContentTypeToLanguageServerDefinition> connections = new ArrayList<>();
private IPreferenceStore preferenceStore;
private LanguageServersRegistry() {
this.preferenceStore = LanguageServerPlugin.getDefault().getPreferenceStore();
initialize();
}
private void initialize() {
String prefs = preferenceStore.getString(CONTENT_TYPE_TO_LSP_LAUNCH_PREF_KEY);
if (prefs != null && !prefs.isEmpty()) {
String[] entries = prefs.split(","); //$NON-NLS-1$
for (String entry : entries) {
ContentTypeToLSPLaunchConfigEntry mapping = ContentTypeToLSPLaunchConfigEntry.readFromPreference(entry);
if (mapping != null) {
connections.add(mapping);
}
}
}
Map<String, LanguageServerDefinition> servers = new HashMap<>();
List<ContentTypeMapping> contentTypes = new ArrayList<>();
for (IConfigurationElement extension : Platform.getExtensionRegistry().getConfigurationElementsFor(EXTENSION_POINT_ID)) {
String id = extension.getAttribute(ID_ATTRIBUTE);
if (id != null && !id.isEmpty()) {
if (extension.getName().equals(LS_ELEMENT)) {
servers.put(id, new ExtensionLanguageServerDefinition(extension));
} else if (extension.getName().equals(MAPPING_ELEMENT)) {
IContentType contentType = Platform.getContentTypeManager().getContentType(extension.getAttribute(CONTENT_TYPE_ATTRIBUTE));
String languageId = extension.getAttribute(LANGUAGE_ID_ATTRIBUTE);
EnablementTester expression = null;
if (extension.getChildren(ENABLED_WHEN_ATTRIBUTE) != null) {
IConfigurationElement[] enabledWhenElements = extension.getChildren(ENABLED_WHEN_ATTRIBUTE);
if (enabledWhenElements.length == 1) {
IConfigurationElement enabledWhen = enabledWhenElements[0];
IConfigurationElement[] enabledWhenChildren = enabledWhen.getChildren();
if (enabledWhenChildren.length == 1) {
try {
String description = enabledWhen.getAttribute(ENABLED_WHEN_DESC);
expression = new EnablementTester(this::evaluationContext,
ExpressionConverter.getDefault().perform(enabledWhenChildren[0]),
description);
} catch (CoreException e) {
LanguageServerPlugin.logWarning(e.getMessage(), e);
}
}
}
}
if (contentType != null) {
contentTypes.add(new ContentTypeMapping(contentType, id, languageId, expression));
}
}
}
}
for (ContentTypeMapping mapping : contentTypes) {
LanguageServerDefinition lsDefinition = servers.get(mapping.id);
if (lsDefinition != null) {
registerAssociation(mapping.contentType, lsDefinition, mapping.languageId, mapping.enablement);
} else {
LanguageServerPlugin.logWarning("server '" + mapping.id + "' not available", null); //$NON-NLS-1$ //$NON-NLS-2$
}
}
}
private IEvaluationContext evaluationContext() {
return Optional.ofNullable(PlatformUI.getWorkbench().getService(IHandlerService.class))//
.map(IHandlerService::getCurrentState)//
.orElse(null);
}
private void persistContentTypeToLaunchConfigurationMapping() {
StringBuilder builder = new StringBuilder();
for (ContentTypeToLSPLaunchConfigEntry entry : getContentTypeToLSPLaunches()) {
entry.appendPreferenceTo(builder);
builder.append(',');
}
if (builder.length() > 0) {
builder.deleteCharAt(builder.length() - 1);
}
this.preferenceStore.setValue(CONTENT_TYPE_TO_LSP_LAUNCH_PREF_KEY, builder.toString());
if (this.preferenceStore instanceof IPersistentPreferenceStore) {
try {
((IPersistentPreferenceStore) this.preferenceStore).save();
} catch (IOException e) {
LanguageServerPlugin.logError(e);
}
}
}
/**
* @param contentType
* @return the {@link LanguageServerDefinition}s <strong>directly</strong> associated to the given content-type.
* This does <strong>not</strong> include the one that match transitively as per content-type hierarchy
*/
List<ContentTypeToLanguageServerDefinition> findProviderFor(final @NonNull IContentType contentType) {
return connections.stream()
.filter(entry -> entry.getKey().equals(contentType))
.sorted((mapping1, mapping2) -> {
// this sort should make that the content-type hierarchy is respected
// and the most specialized content-type are placed before the more generic ones
if (mapping1.getKey().isKindOf(mapping2.getKey())) {
return -1;
} else if (mapping2.getKey().isKindOf(mapping1.getKey())) {
return +1;
}
// TODO support "priority" attribute, but it's not made public
return mapping1.getKey().getId().compareTo(mapping2.getKey().getId());
})
.collect(Collectors.toList());
}
public void registerAssociation(@NonNull IContentType contentType, @NonNull ILaunchConfiguration launchConfig, @NonNull Set<String> launchMode) {
ContentTypeToLSPLaunchConfigEntry mapping = new ContentTypeToLSPLaunchConfigEntry(contentType, launchConfig,
launchMode);
connections.add(mapping);
persistContentTypeToLaunchConfigurationMapping();
}
public void registerAssociation(@NonNull IContentType contentType,
@NonNull LanguageServerDefinition serverDefinition, @Nullable String languageId,
EnablementTester enablement) {
if (languageId != null) {
serverDefinition.registerAssociation(contentType, languageId);
}
connections.add(new ContentTypeToLanguageServerDefinition(contentType, serverDefinition, enablement));
}
public void setAssociations(List<ContentTypeToLSPLaunchConfigEntry> wc) {
this.connections.removeIf(ContentTypeToLSPLaunchConfigEntry.class::isInstance);
this.connections.addAll(wc);
persistContentTypeToLaunchConfigurationMapping();
}
public List<ContentTypeToLSPLaunchConfigEntry> getContentTypeToLSPLaunches() {
return this.connections.stream().filter(ContentTypeToLSPLaunchConfigEntry.class::isInstance).map(ContentTypeToLSPLaunchConfigEntry.class::cast).collect(Collectors.toList());
}
public List<ContentTypeToLanguageServerDefinition> getContentTypeToLSPExtensions() {
return this.connections.stream().filter(mapping -> mapping.getValue() instanceof ExtensionLanguageServerDefinition).collect(Collectors.toList());
}
public @Nullable LanguageServerDefinition getDefinition(@NonNull String languageServerId) {
for (ContentTypeToLanguageServerDefinition mapping : this.connections) {
if (mapping.getValue().id.equals(languageServerId)) {
return mapping.getValue();
}
}
return null;
}
/**
* internal class to capture content-type mappings for language servers
*/
private static class ContentTypeMapping {
@NonNull public final String id;
@NonNull public final IContentType contentType;
@Nullable public final String languageId;
@Nullable
public final EnablementTester enablement;
public ContentTypeMapping(@NonNull IContentType contentType, @NonNull String id, @Nullable String languageId,
@Nullable EnablementTester enablement) {
this.contentType = contentType;
this.id = id;
this.languageId = languageId;
this.enablement = enablement;
}
}
/**
* @param file
* @param serverDefinition
* @return whether the given serverDefinition is suitable for the file
*/
public boolean matches(@NonNull IFile file, @NonNull LanguageServerDefinition serverDefinition) {
return getAvailableLSFor(LSPEclipseUtils.getFileContentTypes(file)).contains(serverDefinition);
}
/**
* @param document
* @param serverDefinition
* @return whether the given serverDefinition is suitable for the file
*/
public boolean matches(@NonNull IDocument document, @NonNull LanguageServerDefinition serverDefinition) {
return getAvailableLSFor(LSPEclipseUtils.getDocumentContentTypes(document)).contains(serverDefinition);
}
public boolean canUseLanguageServer(@NonNull IEditorInput editorInput) {
return !getAvailableLSFor(
Arrays.asList(Platform.getContentTypeManager().findContentTypesFor(editorInput.getName()))).isEmpty();
}
public boolean canUseLanguageServer(@NonNull IDocument document) {
List<IContentType> contentTypes = LSPEclipseUtils.getDocumentContentTypes(document);
if (contentTypes.isEmpty()) {
return false;
}
return !getAvailableLSFor(contentTypes).isEmpty();
}
public boolean canUseLanguageServer(@NonNull IFile file) {
return !getAvailableLSFor(LSPEclipseUtils.getFileContentTypes(file)).isEmpty();
}
private Set<LanguageServerDefinition> getAvailableLSFor(Collection<IContentType> contentTypes) {
Set<LanguageServerDefinition> res = new HashSet<>();
for (ContentTypeToLanguageServerDefinition mapping : this.connections) {
if (mapping.isEnabled() && contentTypes.contains(mapping.getKey())) {
res.add(mapping.getValue());
}
}
return res;
}
}