| /******************************************************************************* |
| * Copyright (c) 2016, 2018 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) - extracted LanguageClientImpl |
| * Jan Koehnlein (TypeFox) - bug 521744 |
| * Martin Lippert (Pivotal, Inc.) - bug 531030, 527902, 534637 |
| * Kris De Volder (Pivotal, Inc.) - dynamic command registration |
| *******************************************************************************/ |
| package org.eclipse.lsp4e; |
| |
| import com.google.gson.Gson; |
| import com.google.gson.JsonObject; |
| import java.io.File; |
| import java.io.IOException; |
| import java.lang.management.ManagementFactory; |
| import java.net.URI; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.Collections; |
| import java.util.HashMap; |
| import java.util.HashSet; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Map.Entry; |
| import java.util.Set; |
| import java.util.concurrent.CancellationException; |
| import java.util.concurrent.CompletableFuture; |
| import java.util.concurrent.ExecutionException; |
| import java.util.concurrent.ExecutorService; |
| import java.util.concurrent.Executors; |
| import java.util.concurrent.Future; |
| import java.util.concurrent.TimeUnit; |
| import java.util.concurrent.TimeoutException; |
| import java.util.function.Function; |
| import org.eclipse.core.filebuffers.FileBuffers; |
| import org.eclipse.core.filebuffers.IFileBuffer; |
| import org.eclipse.core.filebuffers.IFileBufferListener; |
| import org.eclipse.core.resources.IFile; |
| import org.eclipse.core.resources.IProject; |
| import org.eclipse.core.resources.IResourceChangeEvent; |
| import org.eclipse.core.resources.IResourceDelta; |
| import org.eclipse.core.resources.ResourcesPlugin; |
| import org.eclipse.core.runtime.Assert; |
| import org.eclipse.core.runtime.IPath; |
| import org.eclipse.core.runtime.IProgressMonitor; |
| import org.eclipse.core.runtime.IStatus; |
| import org.eclipse.core.runtime.Path; |
| import org.eclipse.core.runtime.Platform; |
| import org.eclipse.core.runtime.Status; |
| import org.eclipse.core.runtime.content.IContentType; |
| import org.eclipse.core.runtime.jobs.Job; |
| 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.lsp4e.ui.Messages; |
| import org.eclipse.lsp4j.ClientCapabilities; |
| import org.eclipse.lsp4j.CodeActionCapabilities; |
| import org.eclipse.lsp4j.CodeActionKind; |
| import org.eclipse.lsp4j.CodeActionKindCapabilities; |
| import org.eclipse.lsp4j.CodeActionLiteralSupportCapabilities; |
| import org.eclipse.lsp4j.CodeActionOptions; |
| import org.eclipse.lsp4j.CodeLensCapabilities; |
| import org.eclipse.lsp4j.ColorProviderCapabilities; |
| import org.eclipse.lsp4j.CompletionCapabilities; |
| import org.eclipse.lsp4j.CompletionItemCapabilities; |
| import org.eclipse.lsp4j.DefinitionCapabilities; |
| import org.eclipse.lsp4j.DidChangeWorkspaceFoldersParams; |
| import org.eclipse.lsp4j.DocumentHighlightCapabilities; |
| import org.eclipse.lsp4j.DocumentLinkCapabilities; |
| import org.eclipse.lsp4j.DocumentSymbolCapabilities; |
| import org.eclipse.lsp4j.ExecuteCommandCapabilities; |
| import org.eclipse.lsp4j.ExecuteCommandOptions; |
| import org.eclipse.lsp4j.FailureHandlingKind; |
| import org.eclipse.lsp4j.FormattingCapabilities; |
| import org.eclipse.lsp4j.HoverCapabilities; |
| import org.eclipse.lsp4j.InitializeParams; |
| import org.eclipse.lsp4j.InitializedParams; |
| import org.eclipse.lsp4j.MarkupKind; |
| import org.eclipse.lsp4j.RangeFormattingCapabilities; |
| import org.eclipse.lsp4j.ReferencesCapabilities; |
| import org.eclipse.lsp4j.Registration; |
| import org.eclipse.lsp4j.RegistrationParams; |
| import org.eclipse.lsp4j.RenameCapabilities; |
| import org.eclipse.lsp4j.ResourceOperationKind; |
| import org.eclipse.lsp4j.ServerCapabilities; |
| import org.eclipse.lsp4j.SignatureHelpCapabilities; |
| import org.eclipse.lsp4j.SymbolCapabilities; |
| import org.eclipse.lsp4j.SymbolKind; |
| import org.eclipse.lsp4j.SymbolKindCapabilities; |
| import org.eclipse.lsp4j.SynchronizationCapabilities; |
| import org.eclipse.lsp4j.TextDocumentClientCapabilities; |
| import org.eclipse.lsp4j.TextDocumentSyncKind; |
| import org.eclipse.lsp4j.TextDocumentSyncOptions; |
| import org.eclipse.lsp4j.TypeDefinitionCapabilities; |
| import org.eclipse.lsp4j.UnregistrationParams; |
| import org.eclipse.lsp4j.WorkspaceClientCapabilities; |
| import org.eclipse.lsp4j.WorkspaceEditCapabilities; |
| import org.eclipse.lsp4j.WorkspaceFolder; |
| import org.eclipse.lsp4j.WorkspaceFoldersChangeEvent; |
| import org.eclipse.lsp4j.WorkspaceFoldersOptions; |
| import org.eclipse.lsp4j.WorkspaceServerCapabilities; |
| import org.eclipse.lsp4j.jsonrpc.Launcher; |
| import org.eclipse.lsp4j.jsonrpc.MessageConsumer; |
| import org.eclipse.lsp4j.jsonrpc.ResponseErrorException; |
| import org.eclipse.lsp4j.jsonrpc.messages.Either; |
| import org.eclipse.lsp4j.jsonrpc.messages.Message; |
| import org.eclipse.lsp4j.jsonrpc.messages.ResponseErrorCode; |
| import org.eclipse.lsp4j.jsonrpc.messages.ResponseMessage; |
| import org.eclipse.lsp4j.services.LanguageServer; |
| import org.eclipse.swt.widgets.Display; |
| import org.eclipse.ui.PlatformUI; |
| |
| public class LanguageServerWrapper { |
| |
| private IFileBufferListener fileBufferListener = new FileBufferListenerAdapter() { |
| @Override |
| public void bufferDisposed(IFileBuffer buffer) { |
| Path filePath = new Path(buffer.getFileStore().toURI().getPath()); |
| disconnect(filePath); |
| } |
| |
| @Override |
| public void dirtyStateChanged(IFileBuffer buffer, boolean isDirty) { |
| if (isDirty) { |
| return; |
| } |
| Path filePath = new Path(buffer.getFileStore().toURI().getPath()); |
| DocumentContentSynchronizer documentListener = connectedDocuments.get(filePath); |
| if (documentListener != null && documentListener.getModificationStamp() < buffer.getModificationStamp()) { |
| documentListener.documentSaved(buffer.getModificationStamp()); |
| } |
| } |
| |
| }; |
| |
| @NonNull |
| public final LanguageServerDefinition serverDefinition; |
| @Nullable |
| protected final IProject initialProject; |
| @NonNull |
| protected final Set<@NonNull IProject> allWatchedProjects; |
| @NonNull |
| protected Map<@NonNull IPath, @NonNull DocumentContentSynchronizer> connectedDocuments; |
| @Nullable |
| protected final IPath initialPath; |
| |
| protected StreamConnectionProvider lspStreamProvider; |
| private Future<?> launcherFuture; |
| private CompletableFuture<Void> initializeFuture; |
| private LanguageServer languageServer; |
| private ServerCapabilities serverCapabilities; |
| |
| /** |
| * Map containing unregistration handlers for dynamic capability registrations. |
| */ |
| private @NonNull Map<@NonNull String, @NonNull Runnable> dynamicRegistrations = new HashMap<>(); |
| private boolean initiallySupportsWorkspaceFolders = false; |
| |
| /* Backwards compatible constructor */ |
| public LanguageServerWrapper(@NonNull IProject project, @NonNull LanguageServerDefinition serverDefinition) { |
| this(project, serverDefinition, null); |
| } |
| |
| public LanguageServerWrapper(@NonNull LanguageServerDefinition serverDefinition, @Nullable IPath initialPath) { |
| this(null, serverDefinition, initialPath); |
| } |
| |
| /** Unified private constructor to set sensible defaults in all cases */ |
| private LanguageServerWrapper(@Nullable IProject project, @NonNull LanguageServerDefinition serverDefinition, |
| @Nullable IPath initialPath) { |
| this.initialProject = project; |
| this.initialPath = initialPath; |
| this.allWatchedProjects = new HashSet<>(); |
| this.serverDefinition = serverDefinition; |
| this.connectedDocuments = new HashMap<>(); |
| } |
| |
| /** |
| * Starts a language server and triggers initialization. If language server is |
| * started and active, does nothing. If language server is inactive, restart it. |
| * |
| * @throws IOException |
| */ |
| public synchronized void start() throws IOException { |
| Map<IPath, IDocument> filesToReconnect = Collections.emptyMap(); |
| if (this.languageServer != null) { |
| if (isActive()) { |
| return; |
| } else { |
| filesToReconnect = new HashMap<>(); |
| for (Entry<IPath, DocumentContentSynchronizer> entry : this.connectedDocuments.entrySet()) { |
| filesToReconnect.put(entry.getKey(), entry.getValue().getDocument()); |
| } |
| stop(); |
| } |
| } |
| try { |
| if (LoggingStreamConnectionProviderProxy.shouldLog(serverDefinition.id)) { |
| this.lspStreamProvider = new LoggingStreamConnectionProviderProxy( |
| serverDefinition.createConnectionProvider(), serverDefinition.id); |
| } else { |
| this.lspStreamProvider = serverDefinition.createConnectionProvider(); |
| } |
| this.lspStreamProvider.start(); |
| |
| LanguageClientImpl client = serverDefinition.createLanguageClient(); |
| ExecutorService executorService = Executors.newCachedThreadPool(); |
| final InitializeParams initParams = new InitializeParams(); |
| initParams.setProcessId(getCurrentProcessId()); |
| |
| URI rootURI = null; |
| IProject project = this.initialProject; |
| if (project != null && project.exists()) { |
| rootURI = LSPEclipseUtils.toUri(this.initialProject); |
| initParams.setRootUri(rootURI.toString()); |
| initParams.setRootPath(rootURI.getPath()); |
| } else { |
| // This is required due to overzealous static analysis. Dereferencing |
| // this.initialPath directly will trigger a "potential null" |
| // warning/error. Checking for this.initialPath == null is not |
| // enough. |
| final IPath initialPath = this.initialPath; |
| if (initialPath != null) { |
| File projectDirectory = initialPath.toFile(); |
| if (projectDirectory.isFile()) { |
| projectDirectory = projectDirectory.getParentFile(); |
| } |
| initParams.setRootUri(LSPEclipseUtils.toUri(projectDirectory).toString()); |
| } else { |
| initParams.setRootUri(LSPEclipseUtils.toUri(new File("/")).toString()); //$NON-NLS-1$ |
| } |
| } |
| Function<MessageConsumer, MessageConsumer> wrapper = consumer -> (message -> { |
| consumer.consume(message); |
| logMessage(message); |
| URI root = initParams.getRootUri() != null ? URI.create(initParams.getRootUri()) : null; |
| final StreamConnectionProvider currentConnectionProvider = this.lspStreamProvider; |
| if (currentConnectionProvider != null && isActive()) { |
| currentConnectionProvider.handleMessage(message, this.languageServer, root); |
| } |
| }); |
| Launcher<LanguageServer> launcher = serverDefinition.createLauncherBuilder() |
| .setLocalService(client)// |
| .setRemoteInterface(serverDefinition.getServerInterface())// |
| .setInput(lspStreamProvider.getInputStream())// |
| .setOutput(lspStreamProvider.getOutputStream())// |
| .setExecutorService(executorService)// |
| .wrapMessages(wrapper)// |
| .create(); |
| this.languageServer = launcher.getRemoteProxy(); |
| client.connect(languageServer, this); |
| this.launcherFuture = launcher.startListening(); |
| |
| String name = "Eclipse IDE"; //$NON-NLS-1$ |
| if (Platform.getProduct() != null) { |
| name = Platform.getProduct().getName(); |
| } |
| WorkspaceClientCapabilities workspaceClientCapabilities = new WorkspaceClientCapabilities(); |
| workspaceClientCapabilities.setApplyEdit(Boolean.TRUE); |
| workspaceClientCapabilities.setExecuteCommand(new ExecuteCommandCapabilities(Boolean.TRUE)); |
| workspaceClientCapabilities.setSymbol(new SymbolCapabilities(Boolean.TRUE)); |
| workspaceClientCapabilities.setWorkspaceFolders(Boolean.TRUE); |
| WorkspaceEditCapabilities editCapabilities = new WorkspaceEditCapabilities(); |
| editCapabilities.setDocumentChanges(Boolean.TRUE); |
| editCapabilities.setResourceOperations(Arrays.asList(ResourceOperationKind.Create, |
| ResourceOperationKind.Delete, ResourceOperationKind.Rename)); |
| editCapabilities.setFailureHandling(FailureHandlingKind.Undo); |
| workspaceClientCapabilities.setWorkspaceEdit(editCapabilities); |
| TextDocumentClientCapabilities textDocumentClientCapabilities = new TextDocumentClientCapabilities(); |
| textDocumentClientCapabilities |
| .setCodeAction( |
| new CodeActionCapabilities( |
| new CodeActionLiteralSupportCapabilities( |
| new CodeActionKindCapabilities(Arrays.asList(CodeActionKind.QuickFix, |
| CodeActionKind.Refactor, CodeActionKind.RefactorExtract, |
| CodeActionKind.RefactorInline, CodeActionKind.RefactorRewrite, |
| CodeActionKind.Source, CodeActionKind.SourceOrganizeImports))), |
| true)); |
| textDocumentClientCapabilities.setCodeLens(new CodeLensCapabilities()); |
| textDocumentClientCapabilities.setColorProvider(new ColorProviderCapabilities()); |
| CompletionItemCapabilities completionItemCapabilities = new CompletionItemCapabilities(Boolean.TRUE); |
| completionItemCapabilities.setDocumentationFormat(Arrays.asList(MarkupKind.MARKDOWN, MarkupKind.PLAINTEXT)); |
| textDocumentClientCapabilities |
| .setCompletion(new CompletionCapabilities(completionItemCapabilities)); |
| DefinitionCapabilities definitionCapabilities = new DefinitionCapabilities(); |
| definitionCapabilities.setLinkSupport(Boolean.TRUE); |
| textDocumentClientCapabilities.setDefinition(definitionCapabilities); |
| TypeDefinitionCapabilities typeDefinitionCapabilities = new TypeDefinitionCapabilities(); |
| typeDefinitionCapabilities.setLinkSupport(Boolean.TRUE); |
| textDocumentClientCapabilities.setTypeDefinition(typeDefinitionCapabilities); |
| textDocumentClientCapabilities.setDocumentHighlight(new DocumentHighlightCapabilities()); |
| textDocumentClientCapabilities.setDocumentLink(new DocumentLinkCapabilities()); |
| DocumentSymbolCapabilities documentSymbol = new DocumentSymbolCapabilities(); |
| documentSymbol.setHierarchicalDocumentSymbolSupport(true); |
| documentSymbol.setSymbolKind(new SymbolKindCapabilities(Arrays.asList(SymbolKind.Array, SymbolKind.Boolean, |
| SymbolKind.Class, SymbolKind.Constant, SymbolKind.Constructor, SymbolKind.Enum, |
| SymbolKind.EnumMember, SymbolKind.Event, SymbolKind.Field, SymbolKind.File, SymbolKind.Function, |
| SymbolKind.Interface, SymbolKind.Key, SymbolKind.Method, SymbolKind.Module, SymbolKind.Namespace, |
| SymbolKind.Null, SymbolKind.Number, SymbolKind.Object, SymbolKind.Operator, SymbolKind.Package, |
| SymbolKind.Property, SymbolKind.String, SymbolKind.Struct, SymbolKind.TypeParameter, |
| SymbolKind.Variable))); |
| textDocumentClientCapabilities.setDocumentSymbol(documentSymbol); |
| textDocumentClientCapabilities.setFormatting(new FormattingCapabilities(Boolean.TRUE)); |
| HoverCapabilities hoverCapabilities = new HoverCapabilities(); |
| hoverCapabilities.setContentFormat(Arrays.asList(MarkupKind.MARKDOWN, MarkupKind.PLAINTEXT)); |
| textDocumentClientCapabilities.setHover(hoverCapabilities); |
| textDocumentClientCapabilities.setOnTypeFormatting(null); // TODO |
| textDocumentClientCapabilities.setRangeFormatting(new RangeFormattingCapabilities()); |
| textDocumentClientCapabilities.setReferences(new ReferencesCapabilities()); |
| textDocumentClientCapabilities.setRename(new RenameCapabilities()); |
| textDocumentClientCapabilities.setSignatureHelp(new SignatureHelpCapabilities()); |
| textDocumentClientCapabilities |
| .setSynchronization(new SynchronizationCapabilities(Boolean.TRUE, Boolean.TRUE, Boolean.TRUE)); |
| initParams.setCapabilities( |
| new ClientCapabilities(workspaceClientCapabilities, textDocumentClientCapabilities, lspStreamProvider.getExperimentalFeaturesPOJO())); |
| initParams.setClientName(name); |
| |
| initParams.setInitializationOptions(this.lspStreamProvider.getInitializationOptions(rootURI)); |
| initParams.setTrace(this.lspStreamProvider.getTrace(rootURI)); |
| |
| // no then...Async future here as we want this chain of operation to be sequential and |
| // "atomic"-ish |
| initializeFuture = languageServer.initialize(initParams).thenAccept(res -> { |
| serverCapabilities = res.getCapabilities(); |
| this.initiallySupportsWorkspaceFolders = supportsWorkspaceFolders(serverCapabilities); |
| }).thenRun(() -> { |
| this.languageServer.initialized(new InitializedParams()); |
| }); |
| |
| final Map<IPath, IDocument> toReconnect = filesToReconnect; |
| initializeFuture.thenRunAsync(() -> { |
| if (this.initialProject != null) { |
| watchProject(this.initialProject, true); |
| } |
| for (Entry<IPath, IDocument> fileToReconnect : toReconnect.entrySet()) { |
| try { |
| connect(fileToReconnect.getKey(), fileToReconnect.getValue()); |
| } catch (IOException e) { |
| LanguageServerPlugin.logError(e); |
| } |
| } |
| }); |
| FileBuffers.getTextFileBufferManager().addFileBufferListener(fileBufferListener); |
| } catch (Exception ex) { |
| LanguageServerPlugin.logError(ex); |
| stop(); |
| } |
| } |
| |
| private static boolean supportsWorkspaceFolders(ServerCapabilities serverCapabilities) { |
| return serverCapabilities != null && serverCapabilities.getWorkspace() != null |
| && serverCapabilities.getWorkspace().getWorkspaceFolders() != null |
| && Boolean.TRUE.equals(serverCapabilities.getWorkspace().getWorkspaceFolders().getSupported()); |
| } |
| |
| private Integer getCurrentProcessId() { |
| String segment = ManagementFactory.getRuntimeMXBean().getName().split("@")[0]; //$NON-NLS-1$ |
| try { |
| return Integer.valueOf(segment); |
| } catch (NumberFormatException ex) { |
| return null; |
| } |
| } |
| |
| private void logMessage(Message message) { |
| if (message instanceof ResponseMessage && ((ResponseMessage) message).getError() != null |
| && ((ResponseMessage) message).getId() |
| .equals(Integer.toString(ResponseErrorCode.RequestCancelled.getValue()))) { |
| ResponseMessage responseMessage = (ResponseMessage) message; |
| LanguageServerPlugin.logError(new ResponseErrorException(responseMessage.getError())); |
| } else if (LanguageServerPlugin.DEBUG) { |
| LanguageServerPlugin.logInfo(message.getClass().getSimpleName() + '\n' + message.toString()); |
| } |
| } |
| |
| /** |
| * @return whether the underlying connection to language server is still active |
| */ |
| public boolean isActive() { |
| return this.launcherFuture != null && !this.launcherFuture.isDone() && !this.launcherFuture.isCancelled(); |
| } |
| |
| synchronized void stop() { |
| if (this.initializeFuture != null) { |
| this.initializeFuture.cancel(true); |
| this.initializeFuture = null; |
| } |
| |
| this.serverCapabilities = null; |
| this.dynamicRegistrations.clear(); |
| |
| final Future<?> serverFuture = this.launcherFuture; |
| final StreamConnectionProvider provider = this.lspStreamProvider; |
| final LanguageServer languageServerInstance = this.languageServer; |
| |
| Runnable shutdownKillAndStopFutureAndProvider = () -> { |
| if (languageServerInstance != null) { |
| CompletableFuture<Object> shutdown = languageServerInstance.shutdown(); |
| try { |
| shutdown.get(5, TimeUnit.SECONDS); |
| } |
| catch (Exception e) { |
| } |
| } |
| |
| if (serverFuture != null) { |
| serverFuture.cancel(true); |
| } |
| |
| if (languageServerInstance != null) { |
| languageServerInstance.exit(); |
| } |
| |
| if (provider != null) { |
| provider.stop(); |
| } |
| }; |
| |
| CompletableFuture.runAsync(shutdownKillAndStopFutureAndProvider); |
| |
| this.launcherFuture = null; |
| this.lspStreamProvider = null; |
| |
| while (!this.connectedDocuments.isEmpty()) { |
| disconnect(this.connectedDocuments.keySet().iterator().next()); |
| } |
| this.languageServer = null; |
| |
| FileBuffers.getTextFileBufferManager().removeFileBufferListener(fileBufferListener); |
| } |
| |
| /** |
| * |
| * @param file |
| * @param document |
| * @return null if not connection has happened, a future tracking the connection state otherwise |
| * @throws IOException |
| */ |
| public @Nullable CompletableFuture<LanguageServer> connect(@NonNull IFile file, IDocument document) throws IOException { |
| return connect(file.getLocation(), document); |
| } |
| |
| /** |
| * |
| * @param document |
| * @return null if not connection has happened, a future tracking the connection state otherwise |
| * @throws IOException |
| */ |
| public @Nullable CompletableFuture<LanguageServer> connect(IDocument document) throws IOException { |
| IFile file = LSPEclipseUtils.getFile(document); |
| |
| if (file != null && file.exists()) { |
| return connect(file, document); |
| } else { |
| URI uri = LSPEclipseUtils.toUri(document); |
| if (uri != null) { |
| return connect(new Path(LSPEclipseUtils.toUri(document).getPath()), document); |
| } |
| } |
| return null; |
| } |
| |
| protected synchronized void watchProject(IProject project, boolean isInitializationRootProject) { |
| if (this.allWatchedProjects.contains(project)) { |
| return; |
| } |
| if (isInitializationRootProject && !this.allWatchedProjects.isEmpty()) { |
| return; // there can be only one root project |
| } |
| if (!isInitializationRootProject && !supportsWorkspaceFolderCapability()) { |
| // multi project and WorkspaceFolder notifications not supported by this server |
| // instance |
| return; |
| } |
| this.allWatchedProjects.add(project); |
| project.getWorkspace().addResourceChangeListener(event -> { |
| if (project.equals(event.getResource()) && (event.getDelta().getKind() == IResourceDelta.MOVED_FROM |
| || event.getDelta().getKind() == IResourceDelta.REMOVED)) { |
| unwatchProject(project); |
| } |
| }, IResourceChangeEvent.POST_CHANGE); |
| if (supportsWorkspaceFolderCapability()) { |
| WorkspaceFoldersChangeEvent event = new WorkspaceFoldersChangeEvent(); |
| event.getAdded().add(LSPEclipseUtils.toWorkspaceFolder(project)); |
| DidChangeWorkspaceFoldersParams params = new DidChangeWorkspaceFoldersParams(); |
| params.setEvent(event); |
| this.languageServer.getWorkspaceService().didChangeWorkspaceFolders(params); |
| } |
| } |
| |
| private synchronized void unwatchProject(@NonNull IProject project) { |
| this.allWatchedProjects.remove(project); |
| // TODO? disconnect resources? |
| if (supportsWorkspaceFolderCapability()) { |
| WorkspaceFoldersChangeEvent event = new WorkspaceFoldersChangeEvent(); |
| WorkspaceFolder workspaceFolder = LSPEclipseUtils.toWorkspaceFolder(project); |
| event.getRemoved().add(workspaceFolder); |
| DidChangeWorkspaceFoldersParams params = new DidChangeWorkspaceFoldersParams(); |
| params.setEvent(event); |
| this.languageServer.getWorkspaceService().didChangeWorkspaceFolders(params); |
| } |
| } |
| |
| /** |
| * Return the projects being watched. |
| */ |
| public Set<@NonNull IProject> watchedProjects() { |
| return allWatchedProjects; |
| } |
| |
| |
| /** |
| * Check whether this LS is suitable for provided project. Starts the LS if not |
| * already started. |
| * |
| * @return whether this language server can operate on the given project |
| * @since 0.5 |
| */ |
| public boolean canOperate(IProject project) { |
| if (project.equals(this.initialProject) || this.allWatchedProjects.contains(project)) { |
| return true; |
| } |
| |
| return serverDefinition.isSingleton || supportsWorkspaceFolderCapability(); |
| } |
| |
| /** |
| * @return true, if the server supports multi-root workspaces via workspace |
| * folders |
| * @since 0.6 |
| */ |
| private boolean supportsWorkspaceFolderCapability() { |
| if (this.initializeFuture != null) { |
| try { |
| this.initializeFuture.get(1, TimeUnit.SECONDS); |
| } catch (ExecutionException | TimeoutException e) { |
| LanguageServerPlugin.logError(e); |
| } catch (InterruptedException e) { |
| LanguageServerPlugin.logError(e); |
| Thread.currentThread().interrupt(); |
| } |
| } |
| return initiallySupportsWorkspaceFolders || supportsWorkspaceFolders(serverCapabilities); |
| } |
| |
| /** |
| * To make public when we support non IFiles |
| * |
| * @return null if not connection has happened, a future that completes when file is initialized otherwise |
| * @noreference internal so far |
| */ |
| private CompletableFuture<LanguageServer> connect(@NonNull IPath absolutePath, IDocument document) throws IOException { |
| final IPath thePath = Path.fromOSString(absolutePath.toFile().getAbsolutePath()); // should be useless |
| |
| IFile file = ResourcesPlugin.getWorkspace().getRoot().getFileForLocation(thePath); |
| if (file != null && file.exists()) { |
| watchProject(file.getProject(), false); |
| } |
| |
| if (this.connectedDocuments.containsKey(thePath)) { |
| return CompletableFuture.completedFuture(languageServer); |
| } |
| start(); |
| if (this.initializeFuture == null) { |
| return null; |
| } |
| if (document == null) { |
| IFile docFile = (IFile) LSPEclipseUtils.findResourceFor(thePath.toFile().toURI().toString()); |
| document = LSPEclipseUtils.getDocument(docFile); |
| } |
| if (document == null) { |
| return null; |
| } |
| final IDocument theDocument = document; |
| return initializeFuture.thenComposeAsync(theVoid -> { |
| synchronized (connectedDocuments) { |
| if (this.connectedDocuments.containsKey(thePath)) { |
| return CompletableFuture.completedFuture(null); |
| } |
| Either<TextDocumentSyncKind, TextDocumentSyncOptions> syncOptions = initializeFuture == null ? null |
| : this.serverCapabilities.getTextDocumentSync(); |
| TextDocumentSyncKind syncKind = null; |
| if (syncOptions != null) { |
| if (syncOptions.isRight()) { |
| syncKind = syncOptions.getRight().getChange(); |
| } else if (syncOptions.isLeft()) { |
| syncKind = syncOptions.getLeft(); |
| } |
| } |
| DocumentContentSynchronizer listener = new DocumentContentSynchronizer(this, theDocument, syncKind); |
| theDocument.addDocumentListener(listener); |
| LanguageServerWrapper.this.connectedDocuments.put(thePath, listener); |
| return listener.didOpenFuture; |
| } |
| }).thenApply(theVoid -> languageServer); |
| } |
| |
| public void disconnect(IPath path) { |
| DocumentContentSynchronizer documentListener = this.connectedDocuments.remove(path); |
| if (documentListener != null) { |
| documentListener.getDocument().removeDocumentListener(documentListener); |
| documentListener.documentClosed(); |
| } |
| if (this.connectedDocuments.isEmpty()) { |
| stop(); |
| } |
| } |
| |
| public void disconnectContentType(@NonNull IContentType contentType) { |
| List<IPath> pathsToDisconnect = new ArrayList<>(); |
| for (IPath path : connectedDocuments.keySet()) { |
| IFile[] foundFiles = ResourcesPlugin.getWorkspace().getRoot() |
| .findFilesForLocationURI(path.toFile().toURI()); |
| if (foundFiles.length != 0 |
| && LSPEclipseUtils.getFileContentTypes(foundFiles[0]).stream().anyMatch(contentType::equals)) { |
| pathsToDisconnect.add(path); |
| } |
| } |
| for (IPath path : pathsToDisconnect) { |
| disconnect(path); |
| } |
| } |
| |
| /** |
| * checks if the wrapper is already connected to the document at the given path |
| * |
| * @noreference test only |
| */ |
| public boolean isConnectedTo(IPath location) { |
| return connectedDocuments.containsKey(location); |
| } |
| |
| /** |
| * Starts and returns the language server, regardless of if it is initialized. |
| * If not in the UI Thread, will wait to return the initialized server. |
| * |
| * @deprecated use {@link #getInitializedServer()} instead. |
| */ |
| @Deprecated |
| @Nullable |
| public LanguageServer getServer() { |
| CompletableFuture<LanguageServer> languagServerFuture = getInitializedServer(); |
| if (Display.getCurrent() != null) { // UI Thread |
| return this.languageServer; |
| } else { |
| return languagServerFuture.join(); |
| } |
| } |
| |
| /** |
| * Starts the language server and returns a CompletableFuture waiting for the |
| * server to be initialized. If done in the UI stream, a job will be created |
| * displaying that the server is being initialized |
| */ |
| @NonNull |
| public CompletableFuture<LanguageServer> getInitializedServer() { |
| try { |
| start(); |
| } catch (IOException ex) { |
| LanguageServerPlugin.logError(ex); |
| } |
| if (initializeFuture != null && !this.initializeFuture.isDone()) { |
| if (Display.getCurrent() != null) { // UI Thread |
| Job waitForInitialization = new Job(Messages.initializeLanguageServer_job) { |
| @Override |
| protected IStatus run(IProgressMonitor monitor) { |
| initializeFuture.join(); |
| return Status.OK_STATUS; |
| } |
| }; |
| waitForInitialization.setUser(true); |
| waitForInitialization.setSystem(false); |
| PlatformUI.getWorkbench().getProgressService().showInDialog( |
| PlatformUI.getWorkbench().getActiveWorkbenchWindow().getShell(), waitForInitialization); |
| } |
| return initializeFuture.thenApply(r -> this.languageServer); |
| } |
| return CompletableFuture.completedFuture(this.languageServer); |
| } |
| |
| /** |
| * Warning: this is a long running operation |
| * |
| * @return the server capabilities, or null if initialization job didn't |
| * complete |
| */ |
| @Nullable |
| public ServerCapabilities getServerCapabilities() { |
| try { |
| getInitializedServer().get(10, TimeUnit.SECONDS); |
| } catch (TimeoutException e) { |
| LanguageServerPlugin.logError("LanguageServer not initialized after 10s", e); //$NON-NLS-1$ |
| } catch (ExecutionException e) { |
| LanguageServerPlugin.logError(e); |
| } catch (CancellationException e) { |
| LanguageServerPlugin.logError(e); |
| } catch (InterruptedException e) { |
| LanguageServerPlugin.logError(e); |
| Thread.currentThread().interrupt(); |
| } |
| |
| return this.serverCapabilities; |
| } |
| |
| /** |
| * @return The language ID that this wrapper is dealing with if defined in the |
| * content type mapping for the language server |
| */ |
| @Nullable |
| public String getLanguageId(IContentType[] contentTypes) { |
| for (IContentType contentType : contentTypes) { |
| String languageId = serverDefinition.langugeIdMappings.get(contentType); |
| if (languageId != null) { |
| return languageId; |
| } |
| } |
| return null; |
| } |
| |
| void registerCapability(RegistrationParams params) { |
| params.getRegistrations().forEach(reg -> { |
| if ("workspace/didChangeWorkspaceFolders".equals(reg.getMethod())) { //$NON-NLS-1$ |
| Assert.isNotNull(serverCapabilities, |
| "Dynamic capability registration failed! Server not yet initialized?"); //$NON-NLS-1$ |
| if (initiallySupportsWorkspaceFolders) { |
| // Can treat this as a NOP since nothing can disable it dynamically if it was |
| // enabled on initialization. |
| } else if (supportsWorkspaceFolders(serverCapabilities)) { |
| LanguageServerPlugin.logWarning( |
| "Dynamic registration of 'workspace/didChangeWorkspaceFolders' ignored. It was already enabled before", //$NON-NLS-1$ |
| null); |
| } else { |
| addRegistration(reg, () -> setWorkspaceFoldersEnablement(false)); |
| setWorkspaceFoldersEnablement(true); |
| } |
| } else if ("workspace/executeCommand".equals(reg.getMethod())) { //$NON-NLS-1$ |
| Gson gson = new Gson(); // TODO? retrieve the GSon used by LS |
| ExecuteCommandOptions executeCommandOptions = gson.fromJson((JsonObject) reg.getRegisterOptions(), |
| ExecuteCommandOptions.class); |
| List<String> newCommands = executeCommandOptions.getCommands(); |
| if (!newCommands.isEmpty()) { |
| addRegistration(reg, () -> unregisterCommands(newCommands)); |
| registerCommands(newCommands); |
| } |
| } else if ("textDocument/formatting".equals(reg.getMethod())) { //$NON-NLS-1$ |
| final Boolean beforeRegistration = serverCapabilities.getDocumentFormattingProvider(); |
| serverCapabilities.setDocumentFormattingProvider(Boolean.TRUE); |
| addRegistration(reg, () -> serverCapabilities.setDocumentFormattingProvider(beforeRegistration)); |
| } else if ("textDocument/rangeFormatting".equals(reg.getMethod())) { //$NON-NLS-1$ |
| final Boolean beforeRegistration = serverCapabilities.getDocumentRangeFormattingProvider(); |
| serverCapabilities.setDocumentRangeFormattingProvider(Boolean.TRUE); |
| addRegistration(reg, () -> serverCapabilities.setDocumentRangeFormattingProvider(beforeRegistration)); |
| } else if ("textDocument/codeAction".equals(reg.getMethod())){ //$NON-NLS-1$ |
| final Either<Boolean, CodeActionOptions> beforeRegistration = serverCapabilities.getCodeActionProvider(); |
| serverCapabilities.setCodeActionProvider(Boolean.TRUE); |
| addRegistration(reg, () -> serverCapabilities.setCodeActionProvider(beforeRegistration)); |
| } |
| }); |
| } |
| |
| private void addRegistration(@NonNull Registration reg, @NonNull Runnable unregistrationHandler) { |
| String regId = reg.getId(); |
| synchronized (dynamicRegistrations) { |
| Assert.isLegal(!dynamicRegistrations.containsKey(regId), "Registration id is not unique"); //$NON-NLS-1$ |
| dynamicRegistrations.put(regId, unregistrationHandler); |
| } |
| } |
| |
| synchronized void setWorkspaceFoldersEnablement(boolean enable) { |
| if (serverCapabilities == null) { |
| this.serverCapabilities = new ServerCapabilities(); |
| } |
| WorkspaceServerCapabilities workspace = serverCapabilities.getWorkspace(); |
| if (workspace == null) { |
| workspace = new WorkspaceServerCapabilities(); |
| serverCapabilities.setWorkspace(workspace); |
| } |
| WorkspaceFoldersOptions folders = workspace.getWorkspaceFolders(); |
| if (folders == null) { |
| folders = new WorkspaceFoldersOptions(); |
| workspace.setWorkspaceFolders(folders); |
| } |
| folders.setSupported(enable); |
| } |
| |
| synchronized void registerCommands(List<String> newCommands) { |
| ServerCapabilities caps = this.getServerCapabilities(); |
| if (caps != null) { |
| ExecuteCommandOptions commandProvider = caps.getExecuteCommandProvider(); |
| if (commandProvider == null) { |
| commandProvider = new ExecuteCommandOptions(new ArrayList<>()); |
| caps.setExecuteCommandProvider(commandProvider); |
| } |
| List<String> existingCommands = commandProvider.getCommands(); |
| for (String newCmd : newCommands) { |
| Assert.isLegal(!existingCommands.contains(newCmd), "Command already registered '" + newCmd + "'"); //$NON-NLS-1$ //$NON-NLS-2$ |
| existingCommands.add(newCmd); |
| } |
| } else { |
| throw new IllegalStateException("Dynamic command registration failed! Server not yet initialized?"); //$NON-NLS-1$ |
| } |
| } |
| |
| void unregisterCapability(UnregistrationParams params) { |
| params.getUnregisterations().forEach(reg -> { |
| String id = reg.getId(); |
| Runnable unregistrator; |
| synchronized (dynamicRegistrations) { |
| unregistrator = dynamicRegistrations.get(id); |
| dynamicRegistrations.remove(id); |
| } |
| if (unregistrator != null) { |
| unregistrator.run(); |
| } |
| }); |
| } |
| |
| void unregisterCommands(List<String> cmds) { |
| ServerCapabilities caps = this.getServerCapabilities(); |
| if (caps != null) { |
| ExecuteCommandOptions commandProvider = caps.getExecuteCommandProvider(); |
| if (commandProvider != null) { |
| List<String> existingCommands = commandProvider.getCommands(); |
| existingCommands.removeAll(cmds); |
| } |
| } |
| } |
| |
| int getVersion(IFile file) { |
| if (file != null && file.getLocation() != null) { |
| DocumentContentSynchronizer documentContentSynchronizer = connectedDocuments.get(file.getLocation()); |
| if (documentContentSynchronizer != null) { |
| return documentContentSynchronizer.getVersion(); |
| } |
| } |
| return -1; |
| } |
| |
| public boolean canOperate(@NonNull IDocument document) { |
| URI documentUri = LSPEclipseUtils.toUri(document); |
| if (documentUri == null) { |
| return false; |
| } |
| if (this.isConnectedTo(new Path(documentUri.getPath()))) { |
| return true; |
| } |
| if (this.initialProject == null && this.connectedDocuments.isEmpty()) { |
| return true; |
| } |
| IFile file = LSPEclipseUtils.getFile(document); |
| if (file != null && file.exists() && canOperate(file.getProject())) { |
| return true; |
| } |
| return serverDefinition.isSingleton || supportsWorkspaceFolderCapability(); |
| } |
| |
| } |