package com.redhat.devtools.intellij.quarkus.lsp4ij; import com.intellij.openapi.components.ServiceManager; import com.intellij.openapi.editor.Document; import com.intellij.openapi.editor.Editor; import com.intellij.openapi.fileTypes.FileType; import com.intellij.openapi.module.Module; import com.intellij.openapi.project.Project; import com.intellij.openapi.vfs.VirtualFile; import com.redhat.devtools.intellij.quarkus.lsp4ij.server.StreamConnectionProvider; import org.eclipse.lsp4j.ServerCapabilities; import org.eclipse.lsp4j.services.LanguageServer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.annotation.Nonnull; import javax.annotation.Nullable; 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; public class LanguageServiceAccessor { private static final Logger LOGGER = LoggerFactory.getLogger(LanguageServiceAccessor.class); private final Project project; public static LanguageServiceAccessor getInstance(Project project) { return ServiceManager.getService(project, LanguageServiceAccessor.class); } private LanguageServiceAccessor(Project project) { this.project = project; } private Set<LanguageServerWrapper> startedServers = new HashSet<>(); private Map<StreamConnectionProvider, LanguageServersRegistry.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 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 Document document; private final @Nonnull LanguageServerWrapper wrapper; private LSPDocumentInfo(@Nonnull URI fileUri, @Nonnull Document document, @Nonnull LanguageServerWrapper wrapper) { this.fileUri = fileUri; this.document = document; this.wrapper = wrapper; } public @Nonnull Document 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) { LOGGER.warn(e.getLocalizedMessage(), e); return this.wrapper.getServer(); } catch (InterruptedException e) { LOGGER.warn(e.getLocalizedMessage(), e); Thread.currentThread().interrupt(); return this.wrapper.getServer(); } } public int getVersion() { return wrapper.getVersion(LSPIJUtils.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 @Nonnull List<CompletableFuture<LanguageServer>> getInitializedLanguageServers(@Nonnull VirtualFile 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) { LOGGER.warn(e.getLocalizedMessage(), e); } return server; })).collect(Collectors.toList()); } } public void disableLanguageServerContentType( @Nonnull ContentTypeToLanguageServerDefinition contentTypeToLSDefinition) { Optional<LanguageServerWrapper> result = startedServers.stream() .filter(server -> server.serverDefinition.equals(contentTypeToLSDefinition.getValue())).findFirst(); if (result.isPresent()) { FileType contentType = contentTypeToLSDefinition.getKey(); if (contentType != null) { result.get().disconnectContentType(contentType); } } } public void enableLanguageServerContentType( @Nonnull ContentTypeToLanguageServerDefinition contentTypeToLSDefinition, @Nonnull Editor[] editors) { for (Editor editor : editors) { VirtualFile editorFile = LSPIJUtils.getFile(editor.getDocument()); FileType contentType = contentTypeToLSDefinition.getKey(); LanguageServersRegistry.LanguageServerDefinition lsDefinition = contentTypeToLSDefinition.getValue(); FileType contentDesc = editorFile.getFileType(); if (contentTypeToLSDefinition.isEnabled() && contentType != null && contentDesc != null && contentType.equals(contentDesc) && lsDefinition != null) { try { getInitializedLanguageServer(editorFile, lsDefinition, capabilities -> true); } catch (IOException e) { LOGGER.warn(e.getLocalizedMessage(), e); } } } } /** * Get the requested language server instance for the given file. Starts the language server if not already started. * * @param file * @param lsDefinition * @param capabilitiesPredicate 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 LanguageServer getLanguageServer(@Nonnull VirtualFile file, @Nonnull LanguageServersRegistry.LanguageServerDefinition lsDefinition, Predicate<ServerCapabilities> capabilitiesPredicate) throws IOException { LanguageServerWrapper wrapper = getLSWrapperForConnection(LSPIJUtils.getProject(file), lsDefinition, LSPIJUtils.toUri(file)); if (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 lsDefinition * @param capabilitiesPredicate 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 CompletableFuture<LanguageServer> getInitializedLanguageServer(@Nonnull VirtualFile file, @Nonnull LanguageServersRegistry.LanguageServerDefinition lsDefinition, Predicate<ServerCapabilities> capabilitiesPredicate) throws IOException { LanguageServerWrapper wrapper = getLSWrapperForConnection(LSPIJUtils.getProject(file), lsDefinition, LSPIJUtils.toUri(file)); if (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; } /** * Get the requested language server instance for the given document. Starts the * language server if not already started. * * @param document the document for which the initialized LanguageServer shall be returned * @param serverId the ID of the LanguageServer to be returned * @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. If * {@code capabilitesPredicate} does not test positive for the server's * capabilities, {@code null} is returned. */ public CompletableFuture<LanguageServer> getInitializedLanguageServer(Document document, LanguageServersRegistry.LanguageServerDefinition lsDefinition, Predicate<ServerCapabilities> capabilitiesPredicate) throws IOException { URI initialPath = LSPIJUtils.toUri(document); LanguageServerWrapper wrapper = getLSWrapperForConnection(document, lsDefinition, initialPath); if (capabilitiesComply(wrapper, capabilitiesPredicate)) { wrapper.connect(document); return wrapper.getInitializedServer(); } return null; } /** * Checks if the given {@code wrapper}'s capabilities comply with the given * {@code capabilitiesPredicate}. * * @param wrapper * the server that's capabilities are tested with * {@code capabilitiesPredicate} * @param capabilitiesPredicate * predicate testing the capabilities of {@code wrapper}. * @return The result of applying the capabilities of {@code wrapper} to * {@code capabilitiesPredicate}, or {@code false} if * {@code capabilitiesPredicate == null} or * {@code wrapper.getServerCapabilities() == null} */ private static boolean capabilitiesComply(LanguageServerWrapper wrapper, Predicate<ServerCapabilities> capabilitiesPredicate) { return capabilitiesPredicate == null || wrapper.getServerCapabilities() == null /* null check is workaround for https://github.com/TypeFox/ls-api/issues/47 */ || capabilitiesPredicate.test(wrapper.getServerCapabilities()); } /** * 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 Collection<LanguageServerWrapper> getLSWrappers(@Nonnull VirtualFile file, @Nullable Predicate<ServerCapabilities> request) throws IOException { LinkedHashSet<LanguageServerWrapper> res = new LinkedHashSet<>(); Module project = LSPIJUtils.getProject(file); if (project == null) { return res; } res.addAll(getMatchingStartedWrappers(file, request)); // look for running language servers via content-type Queue<FileType> contentTypes = new LinkedList<>(); Set<FileType> addedContentTypes = new HashSet<>(); contentTypes.addAll(LSPIJUtils.getFileContentTypes(file)); addedContentTypes.addAll(contentTypes); while (!contentTypes.isEmpty()) { FileType 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(), LSPIJUtils.toUri(file)); 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); } } } } return res; } @Nonnull private Collection<LanguageServerWrapper> getLSWrappers(@Nonnull Document document) { LinkedHashSet<LanguageServerWrapper> res = new LinkedHashSet<>(); VirtualFile file = LSPIJUtils.getFile(document); URI uri = LSPIJUtils.toUri(document); if (uri == null) { return Collections.emptyList(); } URI path = uri; // look for running language servers via content-type Queue<FileType> contentTypes = new LinkedList<>(); Set<FileType> processedContentTypes = new HashSet<>(); contentTypes.addAll(LSPIJUtils.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) { LOGGER.warn(e.getLocalizedMessage(), e); return false; } }) .filter(wrapper -> wrapper.canOperate(document)) .collect(Collectors.toList())); while (!contentTypes.isEmpty()) { FileType contentType = contentTypes.poll(); if (contentType == null || processedContentTypes.contains(contentType)) { continue; } for (ContentTypeToLanguageServerDefinition mapping : LanguageServersRegistry.getInstance() .findProviderFor(contentType)) { if (mapping == null || !mapping.isEnabled()) { continue; } LanguageServersRegistry.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; } final Module fileProject = file != null ? LSPIJUtils.getProject(file) : null; if (fileProject != null) { LanguageServerWrapper wrapper = new LanguageServerWrapper(fileProject, serverDefinition); startedServers.add(wrapper); res.add(wrapper); } } 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 LanguageServerWrapper getLSWrapperForConnection(@Nonnull Module project, @Nonnull LanguageServersRegistry.LanguageServerDefinition serverDefinition) throws IOException { return getLSWrapperForConnection(project, serverDefinition, null); } @Deprecated private LanguageServerWrapper getLSWrapperForConnection(@Nonnull Module project, @Nonnull LanguageServersRegistry.LanguageServerDefinition serverDefinition, @Nullable URI initialPath) throws IOException { LanguageServerWrapper wrapper = null; synchronized (startedServers) { for (LanguageServerWrapper startedWrapper : getStartedLSWrappers(project)) { if (startedWrapper.serverDefinition.equals(serverDefinition)) { wrapper = startedWrapper; break; } } if (wrapper == null) { wrapper = project != null ? new LanguageServerWrapper(project, serverDefinition) : new LanguageServerWrapper(serverDefinition, initialPath); wrapper.start(); } startedServers.add(wrapper); } return wrapper; } private LanguageServerWrapper getLSWrapperForConnection(Document document, LanguageServersRegistry.LanguageServerDefinition serverDefinition, URI initialPath) throws IOException { LanguageServerWrapper wrapper = null; synchronized (startedServers) { for (LanguageServerWrapper startedWrapper : getStartedLSWrappers(document)) { if (startedWrapper.serverDefinition.equals(serverDefinition)) { wrapper = startedWrapper; break; } } if (wrapper == null) { wrapper = new LanguageServerWrapper(serverDefinition, initialPath); wrapper.start(); } startedServers.add(wrapper); } return wrapper; } private @Nonnull List<LanguageServerWrapper> getStartedLSWrappers( @Nonnull Module project) { return startedServers.stream().filter(wrapper -> wrapper.canOperate(project)) .collect(Collectors.toList()); // TODO multi-root: also return servers which support multi-root? } private List<LanguageServerWrapper> getStartedLSWrappers( Document document) { return getStartedLSWrappers(wrapper -> wrapper.canOperate(document)); } private List<LanguageServerWrapper> getStartedLSWrappers(Predicate<LanguageServerWrapper> predicate) { return startedServers.stream().filter(predicate) .collect(Collectors.toList()); // TODO multi-root: also return servers which support multi-root? } private Collection<LanguageServerWrapper> getMatchingStartedWrappers(@Nonnull VirtualFile file, @Nullable Predicate<ServerCapabilities> request) { synchronized (startedServers) { return startedServers.stream().filter(wrapper -> wrapper.isConnectedTo(LSPIJUtils.toUri(file)) || (LanguageServersRegistry.getInstance().matches(file, wrapper.serverDefinition) && wrapper.canOperate(LSPIJUtils.getProject(file)))).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 List<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 List<LanguageServer> getLanguageServers(@Nonnull Module 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 List<LanguageServer> getLanguageServers(@Nullable Module project, Predicate<ServerCapabilities> request, boolean onlyActiveLS) { List<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 LanguageServersRegistry.LanguageServerDefinition getLSDefinition(@Nonnull StreamConnectionProvider provider) { return providersToLSDefinitions.get(provider); } @Nonnull public List<LSPDocumentInfo> getLSPDocumentInfosFor(@Nonnull Document document, @Nonnull Predicate<ServerCapabilities> capabilityRequest) { URI fileUri = LSPIJUtils.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) { LOGGER.warn(e.getLocalizedMessage(), e); } res.add(new LSPDocumentInfo(fileUri, document, wrapper)); }); } catch (final Exception e) { LOGGER.warn(e.getLocalizedMessage(), e); } return res; } /** * @param document * @param filter * @return * @since 0.9 */ @Nonnull public CompletableFuture<List<LanguageServer>> getLanguageServers(@Nonnull Document document, Predicate<ServerCapabilities> filter) { URI uri = LSPIJUtils.toUri(document); if (uri == null) { return CompletableFuture.completedFuture(Collections.emptyList()); } final List<LanguageServer> res = Collections.synchronizedList(new ArrayList<>()); try { return CompletableFuture.allOf(getLSWrappers(document).stream().map(wrapper -> wrapper.getInitializedServer().thenComposeAsync(server -> { if (server != null && (filter == null || filter.test(wrapper.getServerCapabilities()))) { try { return wrapper.connect(document); } catch (IOException ex) { LOGGER.warn(ex.getLocalizedMessage(), ex); } } return CompletableFuture.completedFuture(null); }).thenAccept(server -> { if (server != null) { res.add(server); } })).toArray(CompletableFuture[]::new)).thenApply(theVoid -> res); } catch (final Exception e) { LOGGER.warn(e.getLocalizedMessage(), e); } return CompletableFuture.completedFuture(Collections.emptyList()); } public boolean checkCapability(LanguageServer languageServer, Predicate<ServerCapabilities> condition) { return startedServers.stream().filter(wrapper -> wrapper.isActive() && wrapper.getServer() == languageServer) .anyMatch(wrapper -> condition.test(wrapper.getServerCapabilities())); } public Optional<LanguageServersRegistry.LanguageServerDefinition> resolveServerDefinition(LanguageServer languageServer) { synchronized (startedServers) { return startedServers.stream().filter(wrapper -> languageServer.equals(wrapper.getServer())).findFirst().map(wrapper -> wrapper.serverDefinition); } } }