From 12cd09dc63bc4d860490350b704b94348cfd46b9 Mon Sep 17 00:00:00 2001 From: azerr Date: Tue, 2 May 2023 19:36:41 +0200 Subject: [PATCH] perf: Improve start/stop of language servers (#835) Fixes #835 Signed-off-by: azerr --- .../psi/internal/core/ls/PsiUtilsLSImpl.java | 14 +- ...umentToLanguageServerSetupParticipant.java | 5 + .../lsp4ij/DocumentContentSynchronizer.java | 18 +- .../intellij/quarkus/lsp4ij/LSPIJUtils.java | 6 +- .../quarkus/lsp4ij/LanguageServerWrapper.java | 511 ++++++++++-------- .../lsp4ij/LanguageServersRegistry.java | 14 +- .../lsp4ij/LanguageServiceAccessor.java | 3 + .../lsp4ij/ServerExtensionPointBean.java | 3 + .../lsp4ij/internal/SupportedFeatures.java | 160 ++++++ 9 files changed, 478 insertions(+), 256 deletions(-) create mode 100644 src/main/java/com/redhat/devtools/intellij/quarkus/lsp4ij/internal/SupportedFeatures.java diff --git a/src/main/java/com/redhat/devtools/intellij/lsp4mp4ij/psi/internal/core/ls/PsiUtilsLSImpl.java b/src/main/java/com/redhat/devtools/intellij/lsp4mp4ij/psi/internal/core/ls/PsiUtilsLSImpl.java index faa74e547..bc589202c 100644 --- a/src/main/java/com/redhat/devtools/intellij/lsp4mp4ij/psi/internal/core/ls/PsiUtilsLSImpl.java +++ b/src/main/java/com/redhat/devtools/intellij/lsp4mp4ij/psi/internal/core/ls/PsiUtilsLSImpl.java @@ -84,7 +84,7 @@ public IPsiUtils refine(Module module) { @Override public Module getModule(VirtualFile file) { - if (file != null) { + if (file != null && !project.isDisposed()) { return ProjectFileIndex.getInstance(project).getModuleForFile(file, false); } return null; @@ -93,7 +93,7 @@ public Module getModule(VirtualFile file) { @Override public Module getModule(String uri) throws IOException { VirtualFile file = findFile(uri); - return file!=null?getModule(file):null; + return file != null ? getModule(file) : null; } @Override @@ -173,7 +173,7 @@ public int toOffset(Document document, int line, int character) { @Override public int toOffset(PsiFile file, int line, int character) { Document document = PsiDocumentManager.getInstance(file.getProject()).getDocument(file); - return document!=null?toOffset(document, line, character):0; + return document != null ? toOffset(document, line, character) : 0; } @Override @@ -181,7 +181,11 @@ public PsiFile resolveCompilationUnit(String uri) { try { VirtualFile file = findFile(uri); if (file != null) { - return PsiManager.getInstance(getModule(file).getProject()).findFile(file); + Module module = getModule(file); + if (module == null) { + return null; + } + return PsiManager.getInstance(module.getProject()).findFile(file); } } catch (IOException e) { LOGGER.error(e.getLocalizedMessage(), e); @@ -204,7 +208,7 @@ public static ClasspathKind getClasspathKind(VirtualFile file, Module module) { } public static String getProjectURI(Module module) { - return module != null?module.getModuleFilePath():null; + return module != null ? module.getModuleFilePath() : null; } @Override diff --git a/src/main/java/com/redhat/devtools/intellij/quarkus/lsp4ij/ConnectDocumentToLanguageServerSetupParticipant.java b/src/main/java/com/redhat/devtools/intellij/quarkus/lsp4ij/ConnectDocumentToLanguageServerSetupParticipant.java index f178f65a9..50b42eeae 100644 --- a/src/main/java/com/redhat/devtools/intellij/quarkus/lsp4ij/ConnectDocumentToLanguageServerSetupParticipant.java +++ b/src/main/java/com/redhat/devtools/intellij/quarkus/lsp4ij/ConnectDocumentToLanguageServerSetupParticipant.java @@ -38,6 +38,11 @@ public void projectOpened() { project.getMessageBus().connect(project).subscribe(FileEditorManagerListener.FILE_EDITOR_MANAGER, this); } + @Override + public void projectClosed() { + LanguageServiceAccessor.getInstance(project).shutdownAllDispatchers(); + } + @Override public void fileOpened(@NotNull FileEditorManager source, @NotNull VirtualFile file) { Document document = FileDocumentManager.getInstance().getDocument(file); diff --git a/src/main/java/com/redhat/devtools/intellij/quarkus/lsp4ij/DocumentContentSynchronizer.java b/src/main/java/com/redhat/devtools/intellij/quarkus/lsp4ij/DocumentContentSynchronizer.java index 21e660b18..ae93a3fd0 100644 --- a/src/main/java/com/redhat/devtools/intellij/quarkus/lsp4ij/DocumentContentSynchronizer.java +++ b/src/main/java/com/redhat/devtools/intellij/quarkus/lsp4ij/DocumentContentSynchronizer.java @@ -113,8 +113,7 @@ private void sendDidChangeEvents() { DidChangeTextDocumentParams changeParamsToSend = new DidChangeTextDocumentParams(new VersionedTextDocumentIdentifier(), events); changeParamsToSend.getTextDocument().setUri(fileUri.toString()); changeParamsToSend.getTextDocument().setVersion(++version); - languageServerWrapper.getInitializedServer() - .thenAcceptAsync(ls -> ls.getTextDocumentService().didChange(changeParamsToSend)); + languageServerWrapper.sendNotification(ls -> ls.getTextDocumentService().didChange(changeParamsToSend)); } @Override @@ -171,18 +170,21 @@ public void documentSaved(long timestamp) { return; } } - TextDocumentIdentifier identifier = new TextDocumentIdentifier(fileUri.toString()); + final var identifier = LSPIJUtils.toTextDocumentIdentifier(fileUri); DidSaveTextDocumentParams params = new DidSaveTextDocumentParams(identifier, document.getText()); - languageServerWrapper.getInitializedServer().thenAcceptAsync(ls -> ls.getTextDocumentService().didSave(params)); + languageServerWrapper.sendNotification(ls -> ls.getTextDocumentService().didSave(params)); } public void documentClosed() { - // When LS is shut down all documents are being disconnected. No need to send "didClose" message to the LS that is being shut down or not yet started + final var identifier = LSPIJUtils.toTextDocumentIdentifier(fileUri); + // WILL_SAVE_WAIT_UNTIL_TIMEOUT_MAP.remove(identifier.getUri()); + // When LS is shut down all documents are being disconnected. No need to send + // "didClose" message to the LS that is being shut down or not yet started if (languageServerWrapper.isActive()) { - TextDocumentIdentifier identifier = new TextDocumentIdentifier(fileUri.toString()); - DidCloseTextDocumentParams params = new DidCloseTextDocumentParams(identifier); - languageServerWrapper.getInitializedServer().thenAcceptAsync(ls -> ls.getTextDocumentService().didClose(params)); + final var params = new DidCloseTextDocumentParams(identifier); + languageServerWrapper.sendNotification(ls -> ls.getTextDocumentService().didClose(params)); } + //return CompletableFuture.completedFuture(null); } /** diff --git a/src/main/java/com/redhat/devtools/intellij/quarkus/lsp4ij/LSPIJUtils.java b/src/main/java/com/redhat/devtools/intellij/quarkus/lsp4ij/LSPIJUtils.java index 13f4882dc..17ab1ebb6 100644 --- a/src/main/java/com/redhat/devtools/intellij/quarkus/lsp4ij/LSPIJUtils.java +++ b/src/main/java/com/redhat/devtools/intellij/quarkus/lsp4ij/LSPIJUtils.java @@ -65,7 +65,7 @@ private static T toTextDocumentPositionPa param.setPosition(start); TextDocumentIdentifier id = new TextDocumentIdentifier(); if (uri != null) { - id.setUri(uri.toString()); + id.setUri(uri.toASCIIString()); } param.setTextDocument(id); return param; @@ -325,4 +325,8 @@ public static boolean hasCapability(final Either eith } return eitherCapability.isRight() || (eitherCapability.isLeft() && eitherCapability.getLeft()); } + + public static List getWorkspaceFolders() { + return null; + } } diff --git a/src/main/java/com/redhat/devtools/intellij/quarkus/lsp4ij/LanguageServerWrapper.java b/src/main/java/com/redhat/devtools/intellij/quarkus/lsp4ij/LanguageServerWrapper.java index 4934c42ca..a33518c56 100644 --- a/src/main/java/com/redhat/devtools/intellij/quarkus/lsp4ij/LanguageServerWrapper.java +++ b/src/main/java/com/redhat/devtools/intellij/quarkus/lsp4ij/LanguageServerWrapper.java @@ -1,10 +1,12 @@ package com.redhat.devtools.intellij.quarkus.lsp4ij; +import com.google.common.util.concurrent.ThreadFactoryBuilder; import com.google.gson.Gson; import com.google.gson.JsonObject; import com.intellij.AppTopics; import com.intellij.ProjectTopics; import com.intellij.lang.Language; +import com.intellij.openapi.application.ApplicationInfo; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.editor.Document; import com.intellij.openapi.editor.EditorFactory; @@ -17,58 +19,11 @@ import com.intellij.openapi.project.Project; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.util.messages.MessageBusConnection; +import com.redhat.devtools.intellij.quarkus.lsp4ij.internal.SupportedFeatures; import com.redhat.devtools.intellij.quarkus.lsp4ij.server.StreamConnectionProvider; -import org.eclipse.lsp4j.ClientCapabilities; -import org.eclipse.lsp4j.ClientInfo; -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.CodeActionResolveSupportCapabilities; -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.DocumentFormattingOptions; -import org.eclipse.lsp4j.DocumentHighlightCapabilities; -import org.eclipse.lsp4j.DocumentLinkCapabilities; -import org.eclipse.lsp4j.DocumentRangeFormattingOptions; -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.InlayHintCapabilities; -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.WorkspaceFoldersChangeEvent; -import org.eclipse.lsp4j.WorkspaceFoldersOptions; -import org.eclipse.lsp4j.WorkspaceServerCapabilities; +import org.eclipse.lsp4j.*; 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; @@ -85,22 +40,11 @@ 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.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.*; +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Consumer; +import java.util.function.UnaryOperator; public class LanguageServerWrapper { private static final Logger LOGGER = LoggerFactory.getLogger(LanguageServerWrapper.class);//$NON-NLS-1$ @@ -141,17 +85,26 @@ public void beforeDocumentSaving(@NotNull Document document) { protected Map connectedDocuments; @Nullable protected final URI initialPath; + protected final InitializeParams initParams = new InitializeParams(); protected StreamConnectionProvider lspStreamProvider; private Future launcherFuture; private CompletableFuture initializeFuture; private LanguageServer languageServer; + private LanguageClientImpl languageClient; private ServerCapabilities serverCapabilities; + private Timer timer; + private AtomicBoolean stopping = new AtomicBoolean(false); + + private final ExecutorService dispatcher; + + private final ExecutorService listener; /** * Map containing unregistration handlers for dynamic capability registrations. */ - private @Nonnull Map dynamicRegistrations = new HashMap<>(); + private @Nonnull + Map dynamicRegistrations = new HashMap<>(); private boolean initiallySupportsWorkspaceFolders = false; /* Backwards compatible constructor */ @@ -163,7 +116,9 @@ public LanguageServerWrapper(@Nonnull LanguageServersRegistry.LanguageServerDefi this(null, serverDefinition, initialPath); } - /** Unified private constructor to set sensible defaults in all cases */ + /** + * Unified private constructor to set sensible defaults in all cases + */ private LanguageServerWrapper(@Nullable Module project, @Nonnull LanguageServersRegistry.LanguageServerDefinition serverDefinition, @Nullable URI initialPath) { this.initialProject = project; @@ -171,12 +126,56 @@ private LanguageServerWrapper(@Nullable Module project, @Nonnull LanguageServers this.allWatchedProjects = new HashSet<>(); this.serverDefinition = serverDefinition; this.connectedDocuments = new HashMap<>(); + String projectName = (project != null && project.getName() != null && !serverDefinition.isSingleton) ? ("@" + project.getName()) : ""; //$NON-NLS-1$//$NON-NLS-2$ + String dispatcherThreadNameFormat = "LS-" + serverDefinition.id + projectName + "#dispatcher"; //$NON-NLS-1$ //$NON-NLS-2$ + this.dispatcher = Executors + .newSingleThreadExecutor(new ThreadFactoryBuilder().setNameFormat(dispatcherThreadNameFormat).build()); + + // Executor service passed through to the LSP4j layer when we attempt to start the LS. It will be used + // to create a listener that sits on the input stream and processes inbound messages (responses, or server-initiated + // requests). + String listenerThreadNameFormat = "LS-" + serverDefinition.id + projectName + "#listener-%d"; //$NON-NLS-1$ //$NON-NLS-2$ + this.listener = Executors + .newCachedThreadPool(new ThreadFactoryBuilder().setNameFormat(listenerThreadNameFormat).build()); } public Project getProject() { return initialProject.getProject(); } + void stopDispatcher() { + this.dispatcher.shutdownNow(); + + // Only really needed for testing - the listener (an instance of ConcurrentMessageProcessor) should exit + // as soon as the input stream from the LS is closed, and a cached thread pool will recycle idle + // threads after a 60 second timeout - or immediately in response to JVM shutdown. + // If we don't do this then a full test run will generate a lot of threads because we create new + // instances of this class for each test + this.listener.shutdownNow(); + } + + /** + * @return the workspace folder to be announced to the language server + */ + private List getRelevantWorkspaceFolders() { + final var languageClient = this.languageClient; + List folders = null; + if (languageClient != null) { + try { + folders = languageClient.workspaceFolders().get(5, TimeUnit.SECONDS); + } catch (final ExecutionException | TimeoutException ex) { + LOGGER.error("Error while getting workspace folders with language server '" + serverDefinition.id + "'", ex); + } catch (final InterruptedException ex) { + LOGGER.error("Error while getting workspace folders with language server '" + serverDefinition.id + "'", ex); + Thread.currentThread().interrupt(); + } + } + if (folders == null) { + folders = LSPIJUtils.getWorkspaceFolders(); + } + return folders; + } + /** * Starts a language server and triggers initialization. If language server is * started and active, does nothing. If language server is inactive, restart it. @@ -184,175 +183,142 @@ public Project getProject() { * @throws IOException */ public synchronized void start() throws IOException { - Map filesToReconnect = Collections.emptyMap(); + final var filesToReconnect = new HashMap(); if (this.languageServer != null) { if (isActive()) { return; } else { - filesToReconnect = new HashMap<>(); for (Map.Entry 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(initialProject.getProject()); - ExecutorService executorService = Executors.newCachedThreadPool(); - final InitializeParams initParams = new InitializeParams(); - initParams.setProcessId(getCurrentProcessId()); - - URI rootURI = null; - Module project = this.initialProject; - if (project != null) { - rootURI = LSPIJUtils.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 URI initialPath = this.initialPath; - if (initialPath != null) { - File projectDirectory = new File(initialPath); - if (projectDirectory.isFile()) { - projectDirectory = projectDirectory.getParentFile(); - } - initParams.setRootUri(LSPIJUtils.toUri(projectDirectory).toString()); + if (this.initializeFuture == null) { + final URI rootURI = getRootURI(); + this.launcherFuture = new CompletableFuture<>(); + this.initializeFuture = CompletableFuture.supplyAsync(() -> { + if (LoggingStreamConnectionProviderProxy.shouldLog(serverDefinition.id)) { + this.lspStreamProvider = new LoggingStreamConnectionProviderProxy( + serverDefinition.createConnectionProvider(), serverDefinition.id); } else { - initParams.setRootUri(LSPIJUtils.toUri(new File("/")).toString()); //$NON-NLS-1$ + this.lspStreamProvider = serverDefinition.createConnectionProvider(); } - } - Launcher launcher = Launcher.createLauncher(client, - serverDefinition.getServerInterface(), this.lspStreamProvider.getInputStream(), - this.lspStreamProvider.getOutputStream(), executorService, consumer -> (message -> { - try { - 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); - } - } catch (Exception e) { - LOGGER.warn(e.getLocalizedMessage(), e); - } - })); - - this.languageServer = launcher.getRemoteProxy(); - client.connect(languageServer, this); - this.launcherFuture = launcher.startListening(); - - 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(); - CodeActionCapabilities codeAction = new CodeActionCapabilities(new CodeActionLiteralSupportCapabilities( - new CodeActionKindCapabilities(Arrays.asList(CodeActionKind.QuickFix, CodeActionKind.Refactor, - CodeActionKind.RefactorExtract, CodeActionKind.RefactorInline, - CodeActionKind.RefactorRewrite, CodeActionKind.Source, - CodeActionKind.SourceOrganizeImports))), - true); - codeAction.setDataSupport(true); - codeAction.setResolveSupport(new CodeActionResolveSupportCapabilities(List.of("edit"))); //$NON-NLS-1$ - textDocumentClientCapabilities.setCodeAction(codeAction); - - textDocumentClientCapabilities.setCodeLens(new CodeLensCapabilities()); - textDocumentClientCapabilities.setInlayHint(new InlayHintCapabilities()); - 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.setClientInfo(new ClientInfo(CLIENT_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); - }); - initializeFuture.thenRun(() -> { - // Here we call languageServer.initialized which will send to this IJ LSP client several 'client/registerCapability' - // which will call the private method registerCapability(RegistrationParams params) - // See https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#client_registerCapability - this.languageServer.initialized(new InitializedParams()); - }); + initParams.setInitializationOptions(this.lspStreamProvider.getInitializationOptions(rootURI)); + try { + lspStreamProvider.start(); + } catch (IOException e) { + throw new RuntimeException(e); + } + return null; + }).thenRun(() -> { + languageClient = serverDefinition.createLanguageClient(initialProject.getProject()); - final Map toReconnect = filesToReconnect; - initializeFuture.thenRunAsync(() -> { - if (this.initialProject != null) { - watchProject(this.initialProject, true); + initParams.setProcessId((int) ProcessHandle.current().pid()); + + if (rootURI != null) { + initParams.setRootUri(rootURI.toString()); + initParams.setRootPath(rootURI.getPath()); } - for (Map.Entry fileToReconnect : toReconnect.entrySet()) { - try { - connect(fileToReconnect.getKey(), fileToReconnect.getValue()); - } catch (IOException e) { - LOGGER.warn(e.getLocalizedMessage(), e); + + UnaryOperator wrapper = consumer -> (message -> { + logMessage(message); + consumer.consume(message); + final StreamConnectionProvider currentConnectionProvider = this.lspStreamProvider; + if (currentConnectionProvider != null && isActive()) { + currentConnectionProvider.handleMessage(message, this.languageServer, rootURI); } - } - }); - EditorFactory.getInstance().getEventMulticaster().addDocumentListener(fileBufferListener); - messageBusConnection = ApplicationManager.getApplication().getMessageBus().connect(); - messageBusConnection.subscribe(AppTopics.FILE_DOCUMENT_SYNC, fileBufferListener); - } catch (Exception ex) { - LOGGER.warn(ex.getLocalizedMessage(), ex); - stop(); + }); + // initParams.setWorkspaceFolders(getRelevantWorkspaceFolders()); + Launcher launcher = serverDefinition.createLauncherBuilder() // + .setLocalService(languageClient)// + .setRemoteInterface(serverDefinition.getServerInterface())// + .setInput(lspStreamProvider.getInputStream())// + .setOutput(lspStreamProvider.getOutputStream())// + .setExecutorService(listener)// + .wrapMessages(wrapper)// + .create(); + this.languageServer = launcher.getRemoteProxy(); + languageClient.connect(languageServer, this); + this.launcherFuture = launcher.startListening(); + }) + .thenCompose(unused -> initServer(rootURI)) + .thenAccept(res -> { + serverCapabilities = res.getCapabilities(); + this.initiallySupportsWorkspaceFolders = supportsWorkspaceFolders(serverCapabilities); + }).thenRun(() -> { + this.languageServer.initialized(new InitializedParams()); + }).thenRun(() -> { + final Map toReconnect = filesToReconnect; + initializeFuture.thenRunAsync(() -> { + if (this.initialProject != null) { + watchProject(this.initialProject, true); + } + for (Map.Entry fileToReconnect : toReconnect.entrySet()) { + try { + connect(fileToReconnect.getKey(), fileToReconnect.getValue()); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + }); + EditorFactory.getInstance().getEventMulticaster().addDocumentListener(fileBufferListener); + messageBusConnection = ApplicationManager.getApplication().getMessageBus().connect(); + messageBusConnection.subscribe(AppTopics.FILE_DOCUMENT_SYNC, fileBufferListener); + }).exceptionally(e -> { + LOGGER.error("Error while starting language server '" + serverDefinition.id + "'", e); + initializeFuture.completeExceptionally(e); + stop(); + return null; + }); } } + private CompletableFuture initServer(final URI rootURI) { + + final var workspaceClientCapabilities = SupportedFeatures.getWorkspaceClientCapabilities(); + final var textDocumentClientCapabilities = SupportedFeatures.getTextDocumentClientCapabilities(); + + WindowClientCapabilities windowClientCapabilities = SupportedFeatures.getWindowClientCapabilities(); + initParams.setCapabilities(new ClientCapabilities( + workspaceClientCapabilities, + textDocumentClientCapabilities, + windowClientCapabilities, + lspStreamProvider.getExperimentalFeaturesPOJO())); + initParams.setClientInfo(getClientInfo()); + initParams.setTrace(this.lspStreamProvider.getTrace(rootURI)); + + // no then...Async future here as we want this chain of operation to be sequential and "atomic"-ish + return languageServer.initialize(initParams); + } + + private ClientInfo getClientInfo() { + ApplicationInfo applicationInfo = ApplicationInfo.getInstance(); + String versionName = applicationInfo.getVersionName(); + String buildNumber = applicationInfo.getBuild().asString(); + + String intellijVersion = versionName + " (build " + buildNumber + ")"; + return new ClientInfo(CLIENT_NAME, intellijVersion); + } + + @Nullable + private URI getRootURI() { + final Module project = this.initialProject; + if (project != null && !project.isDisposed()) { + return LSPIJUtils.toUri(project); + } + + final URI path = this.initialPath; + if (path != null) { + File projectDirectory = new File(initialPath); + if (projectDirectory.isFile()) { + projectDirectory = projectDirectory.getParentFile(); + } + return LSPIJUtils.toUri(projectDirectory); + } + return null; + } + private static boolean supportsWorkspaceFolders(ServerCapabilities serverCapabilities) { return serverCapabilities != null && serverCapabilities.getWorkspace() != null && serverCapabilities.getWorkspace().getWorkspaceFolders() != null @@ -379,6 +345,24 @@ private void logMessage(Message message) { } } + private void removeStopTimer() { + if (timer != null) { + timer.cancel(); + timer = null; + } + } + + private void startStopTimer() { + timer = new Timer("Stop Language Server Timer"); //$NON-NLS-1$ + + timer.schedule(new TimerTask() { + @Override + public void run() { + stop(); + } + }, TimeUnit.SECONDS.toMillis(this.serverDefinition.lastDocumentDisconnectedTimeout)); + } + /** * @return whether the underlying connection to language server is still active */ @@ -387,6 +371,11 @@ public boolean isActive() { } synchronized void stop() { + final boolean alreadyStopping = this.stopping.getAndSet(true); + if (alreadyStopping) { + return; + } + removeStopTimer(); if (this.initializeFuture != null) { this.initializeFuture.cancel(true); this.initializeFuture = null; @@ -398,14 +387,17 @@ synchronized void stop() { final Future serverFuture = this.launcherFuture; final StreamConnectionProvider provider = this.lspStreamProvider; final LanguageServer languageServerInstance = this.languageServer; + // ResourcesPlugin.getWorkspace().removeResourceChangeListener(workspaceFolderUpdater); Runnable shutdownKillAndStopFutureAndProvider = () -> { if (languageServerInstance != null) { CompletableFuture shutdown = languageServerInstance.shutdown(); try { shutdown.get(5, TimeUnit.SECONDS); - } - catch (Exception e) { + } catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + } catch (Exception ex) { + LOGGER.error("Error while shutdown the language server '" + serverDefinition.id + "'", ex); } } @@ -420,6 +412,7 @@ synchronized void stop() { if (provider != null) { provider.stop(); } + this.stopping.set(false); }; CompletableFuture.runAsync(shutdownKillAndStopFutureAndProvider); @@ -439,23 +432,23 @@ synchronized void stop() { } /** - * * @param file * @param document * @return null if not connection has happened, a future tracking the connection state otherwise * @throws IOException */ - public @Nullable CompletableFuture connect(@Nonnull VirtualFile file, Document document) throws IOException { + public @Nullable + CompletableFuture connect(@Nonnull VirtualFile file, Document document) throws IOException { return connect(LSPIJUtils.toUri(file), document); } /** - * * @param document * @return null if not connection has happened, a future tracking the connection state otherwise * @throws IOException */ - public @Nullable CompletableFuture connect(Document document) throws IOException { + public @Nullable + CompletableFuture connect(Document document) throws IOException { VirtualFile file = LSPIJUtils.getFile(document); if (file != null && file.exists()) { @@ -516,6 +509,28 @@ private synchronized void unwatchProject(@Nonnull Module project) { } } +/* private void watchProjects() { + if (!supportsWorkspaceFolderCapability()) { + return; + } + final LanguageServer currentLS = this.languageServer; + /*new WorkspaceJob("Setting watch projects on server " + serverDefinition.label) { //$NON-NLS-1$ + @Override + public IStatus runInWorkspace(IProgressMonitor monitor) throws CoreException { + WorkspaceFoldersChangeEvent wsFolderEvent = new WorkspaceFoldersChangeEvent(); + wsFolderEvent.getAdded().addAll(getRelevantWorkspaceFolders()); + if (currentLS != null && currentLS == LanguageServerWrapper.this.languageServer) { + currentLS.getWorkspaceService() + .didChangeWorkspaceFolders(new DidChangeWorkspaceFoldersParams(wsFolderEvent)); + } + ResourcesPlugin.getWorkspace().addResourceChangeListener(workspaceFolderUpdater, + IResourceChangeEvent.POST_CHANGE | IResourceChangeEvent.PRE_DELETE); + return Status.OK_STATUS; + } + }.schedule();*/ + /* } + */ + /** * Check whether this LS is suitable for provided project. Starts the LS if not * already started. @@ -533,7 +548,7 @@ public boolean canOperate(Module project) { /** * @return true, if the server supports multi-root workspaces via workspace - * folders + * folders * @since 0.6 */ private boolean supportsWorkspaceFolderCapability() { @@ -557,6 +572,7 @@ private boolean supportsWorkspaceFolderCapability() { * @noreference internal so far */ private CompletableFuture connect(@Nonnull URI absolutePath, Document document) throws IOException { + removeStopTimer(); final URI thePath = absolutePath; // should be useless VirtualFile file = FileDocumentManager.getInstance().getFile(document); @@ -608,7 +624,10 @@ public void disconnect(URI path) { documentListener.getDocument().removeDocumentListener(documentListener); documentListener.documentClosed(); } - if (this.connectedDocuments.isEmpty()) { + if (this.serverDefinition.lastDocumentDisconnectedTimeout != 0) { + removeStopTimer(); + startStopTimer(); + } else { stop(); } } @@ -619,9 +638,9 @@ public void disconnectContentType(@Nonnull Language language) { VirtualFile foundFiles = LSPIJUtils.findResourceFor(path); if (foundFiles != null) { Language fileLanguage = LSPIJUtils.getFileLanguage(foundFiles, initialProject.getProject()); - if (fileLanguage.isKindOf(language)) { - pathsToDisconnect.add(path); - } + if (fileLanguage.isKindOf(language)) { + pathsToDisconnect.add(path); + } } } for (URI path : pathsToDisconnect) { @@ -687,11 +706,23 @@ protected Void compute(@NotNull ProgressIndicator indicator) throws Exception { return CompletableFuture.completedFuture(this.languageServer); } + /** + * Sends a notification to the wrapped language server + * + * @param fn LS notification to send + */ + public void sendNotification(@Nonnull Consumer fn) { + // Enqueues a notification on the dispatch thread associated with the wrapped language server. This + // ensures the interleaving of document updates and other requests in the UI is mirrored in the + // order in which they get dispatched to the server + getInitializedServer().thenAcceptAsync(fn, this.dispatcher); + } + /** * Warning: this is a long running operation * * @return the server capabilities, or null if initialization job didn't - * complete + * complete */ @Nullable public ServerCapabilities getServerCapabilities() { @@ -713,7 +744,7 @@ public ServerCapabilities getServerCapabilities() { /** * @return The language ID that this wrapper is dealing with if defined in the - * content type mapping for the language server + * content type mapping for the language server */ @Nullable public String getLanguageId(Language language) { @@ -753,22 +784,22 @@ void registerCapability(RegistrationParams params) { registerCommands(newCommands); } } else if ("textDocument/formatting".equals(reg.getMethod())) { //$NON-NLS-1$ - final Either documentFormattingProvider = serverCapabilities.getDocumentFormattingProvider(); + final Either documentFormattingProvider = serverCapabilities.getDocumentFormattingProvider(); if (documentFormattingProvider == null || documentFormattingProvider.isLeft()) { serverCapabilities.setDocumentFormattingProvider(Boolean.TRUE); - addRegistration(reg, () -> serverCapabilities.setDocumentFormattingProvider(documentFormattingProvider )); + addRegistration(reg, () -> serverCapabilities.setDocumentFormattingProvider(documentFormattingProvider)); } else { serverCapabilities.setDocumentFormattingProvider(documentFormattingProvider.getRight()); - addRegistration(reg, () -> serverCapabilities.setDocumentFormattingProvider(documentFormattingProvider )); + addRegistration(reg, () -> serverCapabilities.setDocumentFormattingProvider(documentFormattingProvider)); } } else if ("textDocument/rangeFormatting".equals(reg.getMethod())) { //$NON-NLS-1$ - final Either documentRangeFormattingProvider = serverCapabilities.getDocumentRangeFormattingProvider(); + final Either documentRangeFormattingProvider = serverCapabilities.getDocumentRangeFormattingProvider(); if (documentRangeFormattingProvider == null || documentRangeFormattingProvider.isLeft()) { serverCapabilities.setDocumentRangeFormattingProvider(Boolean.TRUE); - addRegistration(reg, () -> serverCapabilities.setDocumentRangeFormattingProvider(documentRangeFormattingProvider )); + addRegistration(reg, () -> serverCapabilities.setDocumentRangeFormattingProvider(documentRangeFormattingProvider)); } else { serverCapabilities.setDocumentRangeFormattingProvider(documentRangeFormattingProvider.getRight()); - addRegistration(reg, () -> serverCapabilities.setDocumentRangeFormattingProvider(documentRangeFormattingProvider )); + addRegistration(reg, () -> serverCapabilities.setDocumentRangeFormattingProvider(documentRangeFormattingProvider)); } } else if ("textDocument/codeAction".equals(reg.getMethod())) { //$NON-NLS-1$ final Either beforeRegistration = serverCapabilities.getCodeActionProvider(); @@ -782,7 +813,7 @@ void registerCapability(RegistrationParams params) { private void addRegistration(@Nonnull Registration reg, @Nonnull Runnable unregistrationHandler) { String regId = reg.getId(); synchronized (dynamicRegistrations) { - assert !dynamicRegistrations.containsKey(regId):"Registration id is not unique"; //$NON-NLS-1$ + assert !dynamicRegistrations.containsKey(regId) : "Registration id is not unique"; //$NON-NLS-1$ dynamicRegistrations.put(regId, unregistrationHandler); } } @@ -814,7 +845,7 @@ synchronized void registerCommands(List newCommands) { } List existingCommands = commandProvider.getCommands(); for (String newCmd : newCommands) { - assert !existingCommands.contains(newCmd):"Command already registered '" + newCmd + "'"; //$NON-NLS-1$ //$NON-NLS-2$ + assert !existingCommands.contains(newCmd) : "Command already registered '" + newCmd + "'"; //$NON-NLS-1$ //$NON-NLS-2$ existingCommands.add(newCmd); } } else { diff --git a/src/main/java/com/redhat/devtools/intellij/quarkus/lsp4ij/LanguageServersRegistry.java b/src/main/java/com/redhat/devtools/intellij/quarkus/lsp4ij/LanguageServersRegistry.java index f5022f58f..e359c24be 100644 --- a/src/main/java/com/redhat/devtools/intellij/quarkus/lsp4ij/LanguageServersRegistry.java +++ b/src/main/java/com/redhat/devtools/intellij/quarkus/lsp4ij/LanguageServersRegistry.java @@ -5,6 +5,7 @@ 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.jsonrpc.Launcher; import org.eclipse.lsp4j.jsonrpc.validation.NonNull; import org.eclipse.lsp4j.services.LanguageServer; import org.slf4j.Logger; @@ -25,15 +26,20 @@ public class LanguageServersRegistry { private static final Logger LOGGER = LoggerFactory.getLogger(LanguageServersRegistry.class); public abstract static class LanguageServerDefinition { + + private static final int DEFAULT_LAST_DOCUMENTED_DISCONNECTED_TIMEOUT = 5; + public final @Nonnull String id; public final @Nonnull String label; public final boolean isSingleton; + public final int lastDocumentDisconnectedTimeout; public final @Nonnull Map languageIdMappings; - public LanguageServerDefinition(@Nonnull String id, @Nonnull String label, boolean isSingleton) { + public LanguageServerDefinition(@Nonnull String id, @Nonnull String label, boolean isSingleton, Integer lastDocumentDisconnectedTimeout) { this.id = id; this.label = label; this.isSingleton = isSingleton; + this.lastDocumentDisconnectedTimeout = lastDocumentDisconnectedTimeout != null ? lastDocumentDisconnectedTimeout : DEFAULT_LAST_DOCUMENTED_DISCONNECTED_TIMEOUT; this.languageIdMappings = new ConcurrentHashMap<>(); } @@ -51,13 +57,17 @@ public Class getServerInterface() { return LanguageServer.class; } + public Launcher.Builder createLauncherBuilder() { + return new Launcher.Builder<>(); + } + } static class ExtensionLanguageServerDefinition extends LanguageServerDefinition { private ServerExtensionPointBean extension; public ExtensionLanguageServerDefinition(ServerExtensionPointBean element) { - super(element.id, element.label, element.singleton); + super(element.id, element.label, element.singleton, element.lastDocumentDisconnectedTimeout); this.extension = element; } diff --git a/src/main/java/com/redhat/devtools/intellij/quarkus/lsp4ij/LanguageServiceAccessor.java b/src/main/java/com/redhat/devtools/intellij/quarkus/lsp4ij/LanguageServiceAccessor.java index f4276bc5a..d4aadc2b8 100644 --- a/src/main/java/com/redhat/devtools/intellij/quarkus/lsp4ij/LanguageServiceAccessor.java +++ b/src/main/java/com/redhat/devtools/intellij/quarkus/lsp4ij/LanguageServiceAccessor.java @@ -62,6 +62,9 @@ public void clearStartedServers() { } } + void shutdownAllDispatchers() { + startedServers.forEach(LanguageServerWrapper::stopDispatcher); + } /** * A bean storing association of a Document/File with a language server. diff --git a/src/main/java/com/redhat/devtools/intellij/quarkus/lsp4ij/ServerExtensionPointBean.java b/src/main/java/com/redhat/devtools/intellij/quarkus/lsp4ij/ServerExtensionPointBean.java index 9364b6794..397392d1d 100644 --- a/src/main/java/com/redhat/devtools/intellij/quarkus/lsp4ij/ServerExtensionPointBean.java +++ b/src/main/java/com/redhat/devtools/intellij/quarkus/lsp4ij/ServerExtensionPointBean.java @@ -30,6 +30,9 @@ public class ServerExtensionPointBean extends BaseKeyedLazyInstance