From 19aef5609ed3380227083b0b9287a9e82662da14 Mon Sep 17 00:00:00 2001 From: azerr Date: Tue, 9 May 2023 09:43:56 +0200 Subject: [PATCH] feat: Provide a language servers console like vscode (#838) Fixes #838 Signed-off-by: azerr --- build.gradle | 1 - .../psi/internal/core/ls/PsiUtilsLSImpl.java | 16 +- .../microprofile/lang/MicroProfileIcons.java | 26 + .../lang/MicroProfileServerIconProvider.java | 29 + .../quarkus/lang/QuarkusIconProvider.java | 7 +- .../lang/QuarkusServerIconProvider.java | 29 + .../quarkus/lsp/QuarkusLanguageClient.java | 5 + ...umentToLanguageServerSetupParticipant.java | 45 +- .../lsp4ij/DocumentContentSynchronizer.java | 5 +- .../intellij/quarkus/lsp4ij/LSPIJUtils.java | 2 +- .../quarkus/lsp4ij/LanguageClientImpl.java | 3 + .../LanguageServerIconProviderDefinition.java | 32 + .../quarkus/lsp4ij/LanguageServerWrapper.java | 633 +++++++++++------- .../lsp4ij/LanguageServersRegistry.java | 32 +- .../lsp4ij/LanguageServiceAccessor.java | 4 + .../LoggingStreamConnectionProviderProxy.java | 202 ------ .../lsp4ij/ServerExtensionPointBean.java | 3 + .../quarkus/lsp4ij/ServerIconProvider.java | 8 + .../ServerIconProviderExtensionPointBean.java | 39 ++ .../console/LSPConsoleToolWindowFactory.java | 42 ++ .../console/LSPConsoleToolWindowPanel.java | 155 +++++ .../lsp4ij/console/SimpleCardLayoutPanel.java | 89 +++ .../explorer/LanguageServerExplorer.java | 127 ++++ ...nguageServerExplorerLifecycleListener.java | 292 ++++++++ .../LanguageServerProcessTreeNode.java | 99 +++ .../explorer/LanguageServerTreeNode.java | 54 ++ .../explorer/LanguageServerTreeRenderer.java | 120 ++++ .../lsp4ij/console/explorer/ServerStatus.java | 27 + .../lsp4ij/internal/SupportedFeatures.java | 160 +++++ .../LanguageServerLifecycleListener.java | 53 ++ .../LanguageServerLifecycleManager.java | 145 ++++ .../NullLanguageServerLifecycleManager.java | 28 + .../codelens/LSPCodelensInlayProvider.java | 7 +- .../diagnostics/LSPDiagnosticsForServer.java | 2 +- .../server/JavaProcessCommandBuilder.java | 4 +- .../settings/LanguageServerConfigurable.java | 17 +- .../lsp4ij/settings/LanguageServerView.java | 15 +- .../quarkus/lsp4ij/settings/ServerTrace.java | 36 + ...=> UserDefinedLanguageServerSettings.java} | 49 +- .../intellij/qute/lsp/QuteLanguageClient.java | 5 + src/main/resources/META-INF/lsp.xml | 24 +- .../messages/LanguageServerBundle.properties | 18 + .../microprofile_icon_rgb_16px_default.png | Bin 0 -> 460 bytes 43 files changed, 2152 insertions(+), 537 deletions(-) create mode 100644 src/main/java/com/redhat/devtools/intellij/microprofile/lang/MicroProfileIcons.java create mode 100644 src/main/java/com/redhat/devtools/intellij/microprofile/lang/MicroProfileServerIconProvider.java create mode 100644 src/main/java/com/redhat/devtools/intellij/quarkus/lang/QuarkusServerIconProvider.java create mode 100644 src/main/java/com/redhat/devtools/intellij/quarkus/lsp4ij/LanguageServerIconProviderDefinition.java delete mode 100644 src/main/java/com/redhat/devtools/intellij/quarkus/lsp4ij/LoggingStreamConnectionProviderProxy.java create mode 100644 src/main/java/com/redhat/devtools/intellij/quarkus/lsp4ij/ServerIconProvider.java create mode 100644 src/main/java/com/redhat/devtools/intellij/quarkus/lsp4ij/ServerIconProviderExtensionPointBean.java create mode 100644 src/main/java/com/redhat/devtools/intellij/quarkus/lsp4ij/console/LSPConsoleToolWindowFactory.java create mode 100644 src/main/java/com/redhat/devtools/intellij/quarkus/lsp4ij/console/LSPConsoleToolWindowPanel.java create mode 100644 src/main/java/com/redhat/devtools/intellij/quarkus/lsp4ij/console/SimpleCardLayoutPanel.java create mode 100644 src/main/java/com/redhat/devtools/intellij/quarkus/lsp4ij/console/explorer/LanguageServerExplorer.java create mode 100644 src/main/java/com/redhat/devtools/intellij/quarkus/lsp4ij/console/explorer/LanguageServerExplorerLifecycleListener.java create mode 100644 src/main/java/com/redhat/devtools/intellij/quarkus/lsp4ij/console/explorer/LanguageServerProcessTreeNode.java create mode 100644 src/main/java/com/redhat/devtools/intellij/quarkus/lsp4ij/console/explorer/LanguageServerTreeNode.java create mode 100644 src/main/java/com/redhat/devtools/intellij/quarkus/lsp4ij/console/explorer/LanguageServerTreeRenderer.java create mode 100644 src/main/java/com/redhat/devtools/intellij/quarkus/lsp4ij/console/explorer/ServerStatus.java create mode 100644 src/main/java/com/redhat/devtools/intellij/quarkus/lsp4ij/internal/SupportedFeatures.java create mode 100644 src/main/java/com/redhat/devtools/intellij/quarkus/lsp4ij/lifecycle/LanguageServerLifecycleListener.java create mode 100644 src/main/java/com/redhat/devtools/intellij/quarkus/lsp4ij/lifecycle/LanguageServerLifecycleManager.java create mode 100644 src/main/java/com/redhat/devtools/intellij/quarkus/lsp4ij/lifecycle/NullLanguageServerLifecycleManager.java create mode 100644 src/main/java/com/redhat/devtools/intellij/quarkus/lsp4ij/settings/ServerTrace.java rename src/main/java/com/redhat/devtools/intellij/quarkus/lsp4ij/settings/{LanguageServerSettingsState.java => UserDefinedLanguageServerSettings.java} (58%) create mode 100644 src/main/resources/microprofile_icon_rgb_16px_default.png diff --git a/build.gradle b/build.gradle index 9e25c5435..3e99ddd71 100644 --- a/build.gradle +++ b/build.gradle @@ -180,7 +180,6 @@ task copyDeps(type: Copy) { runIde { systemProperties['com.redhat.devtools.intellij.telemetry.mode'] = 'disabled' - systemProperties['com.redhat.devtools.intellij.quarkus.trace'] = 'true' } runIdeForUiTests { 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 42d97c63e..f7601f232 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 @@ -83,7 +83,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; @@ -92,7 +92,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 @@ -171,7 +171,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 @@ -179,7 +179,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); @@ -202,7 +206,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 @@ -214,4 +218,4 @@ public String toUri(PsiFile typeRoot) { public boolean isHiddenGeneratedElement(PsiElement element) { return PsiUtils.isHiddenGeneratedElement(element); } -} +} \ No newline at end of file diff --git a/src/main/java/com/redhat/devtools/intellij/microprofile/lang/MicroProfileIcons.java b/src/main/java/com/redhat/devtools/intellij/microprofile/lang/MicroProfileIcons.java new file mode 100644 index 000000000..bda053f74 --- /dev/null +++ b/src/main/java/com/redhat/devtools/intellij/microprofile/lang/MicroProfileIcons.java @@ -0,0 +1,26 @@ +/******************************************************************************* + * Copyright (c) 2023 Red Hat Inc. and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 + * which is available at https://www.apache.org/licenses/LICENSE-2.0. + * + * SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 + * + * Contributors: + * Red Hat Inc. - initial API and implementation + *******************************************************************************/ +package com.redhat.devtools.intellij.microprofile.lang; + +import com.intellij.openapi.util.IconLoader; + +import javax.swing.*; + +/** + * MicroProfile icons. + */ +public class MicroProfileIcons { + + public static final Icon MicroProfile = IconLoader.findIcon("/microprofile_icon_rgb_16px_default.png", MicroProfileIcons.class); +} diff --git a/src/main/java/com/redhat/devtools/intellij/microprofile/lang/MicroProfileServerIconProvider.java b/src/main/java/com/redhat/devtools/intellij/microprofile/lang/MicroProfileServerIconProvider.java new file mode 100644 index 000000000..7b5dd5d83 --- /dev/null +++ b/src/main/java/com/redhat/devtools/intellij/microprofile/lang/MicroProfileServerIconProvider.java @@ -0,0 +1,29 @@ +/******************************************************************************* + * Copyright (c) 2023 Red Hat Inc. and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 + * which is available at https://www.apache.org/licenses/LICENSE-2.0. + * + * SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 + * + * Contributors: + * Red Hat Inc. - initial API and implementation + *******************************************************************************/ +package com.redhat.devtools.intellij.microprofile.lang; + +import com.redhat.devtools.intellij.quarkus.lsp4ij.ServerIconProvider; + +import javax.swing.*; + +/** + * MicroProfile icon provider for MicroProfile LS. + */ +public class MicroProfileServerIconProvider implements ServerIconProvider { + + @Override + public Icon getIcon() { + return MicroProfileIcons.MicroProfile; + } +} diff --git a/src/main/java/com/redhat/devtools/intellij/quarkus/lang/QuarkusIconProvider.java b/src/main/java/com/redhat/devtools/intellij/quarkus/lang/QuarkusIconProvider.java index 6e657b0f0..1d089b9a7 100644 --- a/src/main/java/com/redhat/devtools/intellij/quarkus/lang/QuarkusIconProvider.java +++ b/src/main/java/com/redhat/devtools/intellij/quarkus/lang/QuarkusIconProvider.java @@ -18,10 +18,13 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import javax.swing.Icon; +import javax.swing.*; +/** + * Quarkus icon provider. + */ public class QuarkusIconProvider extends IconProvider { - private static final Icon QUARKUS_ICON = IconLoader.findIcon("/quarkus_icon_rgb_16px_default.png", QuarkusIconProvider.class); + public static final Icon QUARKUS_ICON = IconLoader.findIcon("/quarkus_icon_rgb_16px_default.png", QuarkusIconProvider.class); @Nullable @Override diff --git a/src/main/java/com/redhat/devtools/intellij/quarkus/lang/QuarkusServerIconProvider.java b/src/main/java/com/redhat/devtools/intellij/quarkus/lang/QuarkusServerIconProvider.java new file mode 100644 index 000000000..c19c2d797 --- /dev/null +++ b/src/main/java/com/redhat/devtools/intellij/quarkus/lang/QuarkusServerIconProvider.java @@ -0,0 +1,29 @@ +/******************************************************************************* + * Copyright (c) 2023 Red Hat Inc. and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 + * which is available at https://www.apache.org/licenses/LICENSE-2.0. + * + * SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 + * + * Contributors: + * Red Hat Inc. - initial API and implementation + *******************************************************************************/ +package com.redhat.devtools.intellij.quarkus.lang; + +import com.redhat.devtools.intellij.quarkus.lsp4ij.ServerIconProvider; + +import javax.swing.*; + +/** + * Quarkus server icon provider used by Qute LS. + */ +public class QuarkusServerIconProvider implements ServerIconProvider { + + @Override + public Icon getIcon() { + return QuarkusIconProvider.QUARKUS_ICON; + } +} diff --git a/src/main/java/com/redhat/devtools/intellij/quarkus/lsp/QuarkusLanguageClient.java b/src/main/java/com/redhat/devtools/intellij/quarkus/lsp/QuarkusLanguageClient.java index 695973da1..01bb887b8 100644 --- a/src/main/java/com/redhat/devtools/intellij/quarkus/lsp/QuarkusLanguageClient.java +++ b/src/main/java/com/redhat/devtools/intellij/quarkus/lsp/QuarkusLanguageClient.java @@ -71,6 +71,11 @@ public QuarkusLanguageClient(Project project) { QuarkusProjectService.getInstance(project); } + @Override + public void dispose() { + connection.disconnect(); + } + private void sendPropertiesChangeEvent(List scope, Set uris) { MicroProfileLanguageServerAPI server = (MicroProfileLanguageServerAPI) getLanguageServer(); if (server != null) { 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 a5108841f..363462df7 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 @@ -16,30 +16,25 @@ import com.intellij.openapi.fileEditor.FileEditorManager; import com.intellij.openapi.fileEditor.FileEditorManagerListener; import com.intellij.openapi.project.Project; +import com.intellij.openapi.project.ProjectManagerListener; import com.intellij.openapi.vfs.VirtualFile; +import com.redhat.devtools.intellij.quarkus.lsp4ij.lifecycle.LanguageServerLifecycleManager; import org.jetbrains.annotations.NotNull; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.IOException; -import java.net.URI; /** * Track file opened / closed to start language servers / disconnect file from language servers. */ -public class ConnectDocumentToLanguageServerSetupParticipant implements ProjectComponent, FileEditorManagerListener { - - private static final Logger LOGGER = LoggerFactory.getLogger(ConnectDocumentToLanguageServerSetupParticipant.class); +public class ConnectDocumentToLanguageServerSetupParticipant implements ProjectManagerListener, FileEditorManagerListener { - private Project project; - - public ConnectDocumentToLanguageServerSetupParticipant(Project project) { - this.project = project; + @Override + public void projectOpened(@NotNull Project project) { + project.getMessageBus().connect().subscribe(FileEditorManagerListener.FILE_EDITOR_MANAGER, this); } @Override - public void projectOpened() { - project.getMessageBus().connect(project).subscribe(FileEditorManagerListener.FILE_EDITOR_MANAGER, this); + public void projectClosing(@NotNull Project project) { + LanguageServerLifecycleManager.getInstance(project).dispose(); + LanguageServiceAccessor.getInstance(project).shutdownAllDispatchers(); } @Override @@ -52,26 +47,4 @@ public void fileOpened(@NotNull FileEditorManager source, @NotNull VirtualFile f } } - @Override - public void fileClosed(@NotNull FileEditorManager source, @NotNull VirtualFile file) { - URI uri = LSPIJUtils.toUri(file); - if (uri != null) { - try { - // TODO: revisit this code, because it can restart language servers - // when a diagnostics is published after the file is closed and the project is closed - // See https://github.com/redhat-developer/intellij-quarkus/issues/840 - // Remove the cached file wrapper if needed - LSPVirtualFileWrapper.dispose(file); - // Disconnect the given file from all language servers - LanguageServiceAccessor.getInstance(source.getProject()) - .getLSWrappers(file, capabilities -> true) - .forEach( - wrapper -> wrapper.disconnect(uri) - ); - } catch (Exception e) { - LOGGER.warn("Error while disconnecting the file '" + uri + "' from all language servers", e); - } - } - } - } 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..51e9bb3d3 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 @@ -181,7 +180,7 @@ public void documentClosed() { if (languageServerWrapper.isActive()) { TextDocumentIdentifier identifier = new TextDocumentIdentifier(fileUri.toString()); DidCloseTextDocumentParams params = new DidCloseTextDocumentParams(identifier); - languageServerWrapper.getInitializedServer().thenAcceptAsync(ls -> ls.getTextDocumentService().didClose(params)); + languageServerWrapper.sendNotification(ls -> ls.getTextDocumentService().didClose(params)); } } 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 7cd4b13cf..ad90e4fab 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 @@ -62,7 +62,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; diff --git a/src/main/java/com/redhat/devtools/intellij/quarkus/lsp4ij/LanguageClientImpl.java b/src/main/java/com/redhat/devtools/intellij/quarkus/lsp4ij/LanguageClientImpl.java index cedbed81c..6e57cffe5 100644 --- a/src/main/java/com/redhat/devtools/intellij/quarkus/lsp4ij/LanguageClientImpl.java +++ b/src/main/java/com/redhat/devtools/intellij/quarkus/lsp4ij/LanguageClientImpl.java @@ -100,4 +100,7 @@ public CompletableFuture> workspaceFolders() { return CompletableFuture.completedFuture(res); } + public void dispose() { + + } } diff --git a/src/main/java/com/redhat/devtools/intellij/quarkus/lsp4ij/LanguageServerIconProviderDefinition.java b/src/main/java/com/redhat/devtools/intellij/quarkus/lsp4ij/LanguageServerIconProviderDefinition.java new file mode 100644 index 000000000..886223c1b --- /dev/null +++ b/src/main/java/com/redhat/devtools/intellij/quarkus/lsp4ij/LanguageServerIconProviderDefinition.java @@ -0,0 +1,32 @@ +/******************************************************************************* + * Copyright (c) 2023 Red Hat Inc. and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 + * which is available at https://www.apache.org/licenses/LICENSE-2.0. + * + * SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 + * + * Contributors: + * Red Hat Inc. - initial API and implementation + *******************************************************************************/ +package com.redhat.devtools.intellij.quarkus.lsp4ij; + +import javax.swing.*; + +/** + * Definition for server icon provider. + */ +public class LanguageServerIconProviderDefinition { + + private final ServerIconProviderExtensionPointBean extension; + + public LanguageServerIconProviderDefinition(ServerIconProviderExtensionPointBean extension) { + this.extension = extension; + } + + public Icon getIcon() { + return extension.getInstance().getIcon(); + } +} 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..b8d731315 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; @@ -12,68 +14,25 @@ import com.intellij.openapi.editor.event.DocumentListener; import com.intellij.openapi.fileEditor.FileDocumentManager; import com.intellij.openapi.fileEditor.FileDocumentManagerListener; +import com.intellij.openapi.fileEditor.FileEditorManager; +import com.intellij.openapi.fileEditor.FileEditorManagerListener; import com.intellij.openapi.module.Module; import com.intellij.openapi.project.ModuleListener; 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.lifecycle.LanguageServerLifecycleManager; +import com.redhat.devtools.intellij.quarkus.lsp4ij.internal.SupportedFeatures; +import com.redhat.devtools.intellij.quarkus.lsp4ij.lifecycle.NullLanguageServerLifecycleManager; 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 com.redhat.devtools.intellij.quarkus.lsp4ij.settings.ServerTrace; +import com.redhat.devtools.intellij.quarkus.lsp4ij.settings.UserDefinedLanguageServerSettings; +import org.eclipse.lsp4j.*; +import org.eclipse.lsp4j.jsonrpc.JsonRpcException; import org.eclipse.lsp4j.jsonrpc.Launcher; -import org.eclipse.lsp4j.jsonrpc.ResponseErrorException; +import org.eclipse.lsp4j.jsonrpc.MessageConsumer; 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.jetbrains.annotations.NotNull; import org.slf4j.Logger; @@ -85,28 +44,17 @@ 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$ private static final String CLIENT_NAME = "IntelliJ"; - class Listener implements DocumentListener, FileDocumentManagerListener { + class Listener implements DocumentListener, FileDocumentManagerListener, FileEditorManagerListener { @Override public void documentChanged(@NotNull DocumentEvent event) { URI uri = LSPIJUtils.toUri(event.getDocument()); @@ -126,6 +74,23 @@ public void beforeDocumentSaving(@NotNull Document document) { disconnect(uri); }*/ } + + + @Override + public void fileClosed(@NotNull FileEditorManager source, @NotNull VirtualFile file) { + URI uri = LSPIJUtils.toUri(file); + if (uri != null) { + try { + // Remove the cached file wrapper if needed + LSPVirtualFileWrapper.dispose(file); + // Disconnect the given file from all language servers + disconnect(uri); + } catch (Exception e) { + LOGGER.warn("Error while disconnecting the file '" + uri + "' from all language servers", e); + } + } + } + } private Listener fileBufferListener = new Listener(); @@ -141,17 +106,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 final 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 +137,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 +147,57 @@ 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) { + // FIXME + // 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,173 +205,197 @@ 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(); + if (this.initializeFuture == null) { + final boolean shouldLog = shouldLog(serverDefinition.id); + 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 {*/ + this.lspStreamProvider = serverDefinition.createConnectionProvider(); + //} + initParams.setInitializationOptions(this.lspStreamProvider.getInitializationOptions(rootURI)); + try { + // Starting process... + if (shouldLog) { + getLanguageServerLifecycleManager().onStartingProcess(this); + } + lspStreamProvider.start(); + // End process with success + if (shouldLog) { + getLanguageServerLifecycleManager().onStartedProcess(this, null); } - initParams.setRootUri(LSPIJUtils.toUri(projectDirectory).toString()); - } else { - initParams.setRootUri(LSPIJUtils.toUri(new File("/")).toString()); //$NON-NLS-1$ + } catch (IOException e) { + // End process with error + if (shouldLog) { + getLanguageServerLifecycleManager().onStartedProcess(this, e); + } + throw new RuntimeException(e); } - } - 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()); - }); - - final Map toReconnect = filesToReconnect; - initializeFuture.thenRunAsync(() -> { - if (this.initialProject != null) { - watchProject(this.initialProject, true); + return null; + }).thenRun(() -> { + languageClient = serverDefinition.createLanguageClient(initialProject.getProject()); + initParams.setProcessId((int) ProcessHandle.current().pid()); + + if (rootURI != null) { + initParams.setRootUri(rootURI.toString()); + initParams.setRootPath(rootURI.getPath()); } - for (Map.Entry fileToReconnect : toReconnect.entrySet()) { + + UnaryOperator wrapper = consumer -> (message -> { + if (shouldLog) { + logMessage(message); + } try { - connect(fileToReconnect.getKey(), fileToReconnect.getValue()); - } catch (IOException e) { - LOGGER.warn(e.getLocalizedMessage(), e); + consumer.consume(message); } - } - }); - 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(); + catch(JsonRpcException e) { + // When shutdown or exit is called, the pipe can be closed, in this case the exception must be ignored: + if (!isIgnoreException(e)) { + throw e; + } + } + final StreamConnectionProvider currentConnectionProvider = this.lspStreamProvider; + if (currentConnectionProvider != null && isActive()) { + currentConnectionProvider.handleMessage(message, this.languageServer, rootURI); + } + }); + // 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); + messageBusConnection.subscribe(FileEditorManagerListener.FILE_EDITOR_MANAGER, fileBufferListener); + + if (shouldLog) { + getLanguageServerLifecycleManager().onStartedLanguageServer(this, null); + } + + }).exceptionally(e -> { + LOGGER.error("Error while starting language server '" + serverDefinition.id + "'", e); + initializeFuture.completeExceptionally(e); + if (shouldLog) { + getLanguageServerLifecycleManager().onStartedLanguageServer(this, e); + } + stop(); + return null; + }); + } + } + + private boolean isIgnoreException(JsonRpcException e) { + if (!isStopping()) { + // The language server is not stopping, don't ignore the error + return false; } + if (JsonRpcException.indicatesStreamClosed(e)) { + return true; + } + return e.getCause() != null && "The pipe is being closed".equals(e.getCause().getMessage()); + } + + /** + * Returns whether currently created connections should be logged to file or the + * standard error stream. + * + * @return If connections should be logged + */ + private static boolean shouldLog(String serverId) { + UserDefinedLanguageServerSettings.LanguageServerDefinitionSettings settings = UserDefinedLanguageServerSettings.getInstance().getLanguageServerSettings(serverId); + if (settings == null) { + return false; + } + ServerTrace serverTrace = settings.getServerTrace(); + return serverTrace != null && serverTrace != ServerTrace.off; + } + + 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) { @@ -369,6 +414,9 @@ private Integer getCurrentProcessId() { } private void logMessage(Message message) { + getLanguageServerLifecycleManager().logLSPMessage(message, this); + + /*System.out.println(message); if (message instanceof ResponseMessage && ((ResponseMessage) message).getError() != null && ((ResponseMessage) message).getId() .equals(Integer.toString(ResponseErrorCode.RequestCancelled.getValue()))) { @@ -376,9 +424,31 @@ private void logMessage(Message message) { LOGGER.warn("", new ResponseErrorException(responseMessage.getError())); } else if (LOGGER.isDebugEnabled()) { LOGGER.info(message.getClass().getSimpleName() + '\n' + message.toString()); + }*/ + } + + private void removeStopTimer() { + if (timer != null) { + timer.cancel(); + timer = null; + + getLanguageServerLifecycleManager().onStartedLanguageServer(this, null); } } + private void startStopTimer() { + timer = new Timer("Stop Language Server Timer"); //$NON-NLS-1$ + + getLanguageServerLifecycleManager().onStoppingLanguageServer(this); + + 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 */ @@ -386,7 +456,25 @@ public boolean isActive() { return this.launcherFuture != null && !this.launcherFuture.isDone() && !this.launcherFuture.isCancelled(); } + /** + * Returns true if the language server is stopping and false otherwise. + * + * @return true if the language server is stopping and false otherwise. + */ + public boolean isStopping() { + return this.stopping.get(); + } + synchronized void stop() { + final boolean alreadyStopping = this.stopping.getAndSet(true); + if (alreadyStopping) { + return; + } + getLanguageServerLifecycleManager().onStoppingLanguageServer(this); + removeStopTimer(); + if (this.languageClient != null) { + this.languageClient.dispose(); + } if (this.initializeFuture != null) { this.initializeFuture.cancel(true); this.initializeFuture = null; @@ -398,14 +486,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 +511,10 @@ synchronized void stop() { if (provider != null) { provider.stop(); } + this.stopping.set(false); + + getLanguageServerLifecycleManager().onStoppedLanguageServer(this, null); + }; CompletableFuture.runAsync(shutdownKillAndStopFutureAndProvider); @@ -431,6 +526,7 @@ synchronized void stop() { disconnect(this.connectedDocuments.keySet().iterator().next()); } this.languageServer = null; + this.languageClient = null; EditorFactory.getInstance().getEventMulticaster().removeDocumentListener(fileBufferListener); if (messageBusConnection != null) { @@ -439,23 +535,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 +612,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 +651,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 +675,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); @@ -609,7 +728,12 @@ public void disconnect(URI path) { documentListener.documentClosed(); } if (this.connectedDocuments.isEmpty()) { - stop(); + if (this.serverDefinition.lastDocumentDisconnectedTimeout != 0 && !ApplicationManager.getApplication().isUnitTestMode()) { + removeStopTimer(); + startStopTimer(); + } else { + stop(); + } } } @@ -619,9 +743,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 +811,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 +849,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 +889,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 +918,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 +950,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 { @@ -871,4 +1007,11 @@ public boolean canOperate(@Nonnull Document document) { return serverDefinition.isSingleton || supportsWorkspaceFolderCapability(); } -} + private LanguageServerLifecycleManager getLanguageServerLifecycleManager() { + Project project = initialProject.getProject(); + if (project.isDisposed()) { + return NullLanguageServerLifecycleManager.INSTANCE; + } + return LanguageServerLifecycleManager.getInstance(project); + } +} \ No newline at end of file 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 8248962b6..01f0ece86 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 @@ -15,6 +15,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; @@ -22,6 +23,7 @@ import javax.annotation.Nonnull; import javax.annotation.Nullable; +import javax.swing.*; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; @@ -39,17 +41,22 @@ 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 @Nonnull Map languageIdMappings; - public String description; + public final String description; + public final int lastDocumentDisconnectedTimeout; - public LanguageServerDefinition(@Nonnull String id, @Nonnull String label, String description, boolean isSingleton) { + public LanguageServerDefinition(@Nonnull String id, @Nonnull String label, String description, boolean isSingleton, Integer lastDocumentDisconnectedTimeout) { this.id = id; this.label = label; this.description = description; this.isSingleton = isSingleton; + this.lastDocumentDisconnectedTimeout = lastDocumentDisconnectedTimeout != null && lastDocumentDisconnectedTimeout > 0 ? lastDocumentDisconnectedTimeout : DEFAULT_LAST_DOCUMENTED_DISCONNECTED_TIMEOUT; this.languageIdMappings = new ConcurrentHashMap<>(); } @@ -72,13 +79,16 @@ public Class getServerInterface() { return LanguageServer.class; } + public Launcher.Builder createLauncherBuilder() { + return new Launcher.Builder<>(); + } } static class ExtensionLanguageServerDefinition extends LanguageServerDefinition { - private ServerExtensionPointBean extension; + private final ServerExtensionPointBean extension; public ExtensionLanguageServerDefinition(ServerExtensionPointBean element) { - super(element.id, element.label, element.description, element.singleton); + super(element.id, element.label, element.description, element.singleton, element.lastDocumentDisconnectedTimeout); this.extension = element; } @@ -129,7 +139,9 @@ public static LanguageServersRegistry getInstance() { return INSTANCE; } - private List connections = new ArrayList<>(); + private final List connections = new ArrayList<>(); + + private Map serverIcons = new HashMap<>(); private LanguageServersRegistry() { initialize(); @@ -150,6 +162,9 @@ private void initialize() { } } + for (ServerIconProviderExtensionPointBean extension : ServerIconProviderExtensionPointBean.EP_NAME.getExtensions()) { + serverIcons.put(extension.serverId, new LanguageServerIconProviderDefinition(extension)); + } for (LanguageMapping mapping : languageMappings) { LanguageServerDefinition lsDefinition = servers.get(mapping.languageId); @@ -161,6 +176,11 @@ private void initialize() { } } + public Icon getServerIcon(String serverId) { + LanguageServerIconProviderDefinition iconProvider = serverIcons.get(serverId); + return iconProvider != null ? iconProvider.getIcon() : null; + } + /** * @param contentType * @return the {@link LanguageServerDefinition}s directly associated to the given content-type. @@ -252,7 +272,5 @@ public Set getAllDefinitions() { .collect(Collectors.toSet()); } - - } 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..d4967dcf5 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. @@ -607,4 +610,5 @@ public Optional resolveServerD return startedServers.stream().filter(wrapper -> languageServer.equals(wrapper.getServer())).findFirst().map(wrapper -> wrapper.serverDefinition); } } + } diff --git a/src/main/java/com/redhat/devtools/intellij/quarkus/lsp4ij/LoggingStreamConnectionProviderProxy.java b/src/main/java/com/redhat/devtools/intellij/quarkus/lsp4ij/LoggingStreamConnectionProviderProxy.java deleted file mode 100644 index d0299b4bf..000000000 --- a/src/main/java/com/redhat/devtools/intellij/quarkus/lsp4ij/LoggingStreamConnectionProviderProxy.java +++ /dev/null @@ -1,202 +0,0 @@ -package com.redhat.devtools.intellij.quarkus.lsp4ij; - -import com.redhat.devtools.intellij.quarkus.lsp4ij.server.StreamConnectionProvider; -import org.eclipse.lsp4j.jsonrpc.messages.Message; -import org.eclipse.lsp4j.services.LanguageServer; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import javax.annotation.Nullable; -import java.io.File; -import java.io.FilterInputStream; -import java.io.FilterOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.net.URI; -import java.nio.file.Files; -import java.nio.file.StandardOpenOption; - -//TODO: implement LoggingStreamConnectionProviderProxy fully (preferences) -public class LoggingStreamConnectionProviderProxy implements StreamConnectionProvider { - private static final Logger LOGGER = LoggerFactory.getLogger(LoggingStreamConnectionProviderProxy.class); - - private final StreamConnectionProvider provider; - private InputStream inputStream; - private OutputStream outputStream; - private InputStream errorStream; - private final String id; - private File logFile; - private boolean logToFile = true; - private boolean logToConsole = false; - - - /** - * Returns whether currently created connections should be logged to file or the - * standard error stream. - * - * @return If connections should be logged - */ - public static boolean shouldLog(String serverId) { - return Boolean.getBoolean("com.redhat.devtools.intellij.quarkus.trace"); - } - - public LoggingStreamConnectionProviderProxy(StreamConnectionProvider provider, String serverId) { - this.provider = provider; - this.id = serverId; - this.logFile = getLogFile(); - } - - @Override - public void start() throws IOException { - provider.start(); - } - - @Override - public InputStream getInputStream() { - if (inputStream != null) { - return inputStream; - } - if (provider.getInputStream() != null) { - inputStream = new FilterInputStream(provider.getInputStream()) { - @Override - public int read(byte[] b, int off, int len) throws IOException { - int bytes = super.read(b, off, len); - byte[] payload = new byte[bytes]; - System.arraycopy(b, off, payload, 0, bytes); - if (logToConsole || logToFile) { - String s = "\n[t=" + System.currentTimeMillis() + "] " + id + " to LSP4E:\n" + new String(payload); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ - if (logToConsole) { - logToConsole(s); - } - if (logToFile) { - logToFile(s); - } - } - return bytes; - } - }; - } - return inputStream; - } - - @Override - public OutputStream getOutputStream() { - if (outputStream != null) { - return outputStream; - } - if (provider.getOutputStream() != null) { - outputStream = new FilterOutputStream(provider.getOutputStream()) { - @Override - public void write(byte[] b) throws IOException { - if (logToConsole || logToFile) { - String s = "\n[t=" + System.currentTimeMillis() + "] LSP4E to " + id + ":\n" + new String(b); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ - if (logToConsole) { - logToConsole(s); - } - if (logToFile) { - logToFile(s); - } - } - super.write(b); - } - }; - } - return outputStream; - } - - @Nullable - @Override - public InputStream getErrorStream() { - if (errorStream != null) { - return errorStream; - } - if (provider.getErrorStream() != null) { - errorStream = new FilterInputStream(provider.getErrorStream()) { - @Override - public int read(byte[] b, int off, int len) throws IOException { - int bytes = super.read(b, off, len); - byte[] payload = new byte[bytes]; - System.arraycopy(b, off, payload, 0, bytes); - if (logToConsole || logToFile) { - String s = "\n[t=" + System.currentTimeMillis() + "] Error from " + id + ":\n" + new String(payload); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ - if (logToConsole) { - logToConsole(s); - } - if (logToFile) { - logToFile(s); - } - } - return bytes; - } - }; - } - return errorStream; - } - - @Override - public void stop() { - provider.stop(); - } - - @Override - public InputStream forwardCopyTo(InputStream input, OutputStream output) { - return provider.forwardCopyTo(input, output); - } - - @Override - public String getTrace(URI rootUri) { - return provider.getTrace(rootUri); - } - - @Override - public Object getInitializationOptions(URI rootUri) { - return provider.getInitializationOptions(rootUri); - } - - @Override - public Object getExperimentalFeaturesPOJO() { - return provider.getExperimentalFeaturesPOJO(); - } - - @Override - public void handleMessage(Message message, LanguageServer languageServer, URI rootURI) { - provider.handleMessage(message, languageServer, rootURI); - } - - private void logToConsole(String string) { - System.out.println(string); - } - - private void logToFile(String string) { - if (logFile == null) { - return; - } - if (!logFile.exists()) { - try { - if (!logFile.createNewFile()) { - throw new IOException(String.format("Failed to create file %s", logFile.toString())); //$NON-NLS-1$ - } - } catch (IOException e) { - LOGGER.warn(e.getLocalizedMessage(), e); - } - } - try { - Files.write(logFile.toPath(), string.getBytes(), StandardOpenOption.APPEND); - } catch (IOException e) { - LOGGER.warn(e.getLocalizedMessage(), e); - } - } - - private File getLogFile() { - if (logFile != null) { - return logFile; - } - File file = new File(id + ".log"); //$NON-NLS-1$ - if (file.exists() && !(file.isFile() && file.canWrite())) { - return null; - } - return file.getAbsoluteFile(); - } - -} 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 f7d31f222..1a379d8d6 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 @@ -34,6 +34,9 @@ public class ServerExtensionPointBean extends BaseKeyedLazyInstance { + + public static final ExtensionPointName EP_NAME = ExtensionPointName.create("com.redhat.devtools.intellij.quarkus.serverIconProvider"); + + @Attribute("serverId") + public String serverId; + + @Attribute("class") + public String clazz; + + @Override + protected @Nullable String getImplementationClassName() { + return clazz; + } + +} diff --git a/src/main/java/com/redhat/devtools/intellij/quarkus/lsp4ij/console/LSPConsoleToolWindowFactory.java b/src/main/java/com/redhat/devtools/intellij/quarkus/lsp4ij/console/LSPConsoleToolWindowFactory.java new file mode 100644 index 000000000..1f68bf212 --- /dev/null +++ b/src/main/java/com/redhat/devtools/intellij/quarkus/lsp4ij/console/LSPConsoleToolWindowFactory.java @@ -0,0 +1,42 @@ +/******************************************************************************* + * Copyright (c) 2023 Red Hat Inc. and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 + * which is available at https://www.apache.org/licenses/LICENSE-2.0. + * + * SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 + * + * Contributors: + * Red Hat Inc. - initial API and implementation + *******************************************************************************/ +package com.redhat.devtools.intellij.quarkus.lsp4ij.console; + +import com.intellij.openapi.project.DumbAware; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.Disposer; +import com.intellij.openapi.wm.ToolWindow; +import com.intellij.openapi.wm.ToolWindowFactory; +import com.intellij.ui.content.Content; +import com.intellij.ui.content.ContentManager; +import com.redhat.devtools.intellij.quarkus.lsp4ij.LanguageServerBundle; +import org.jetbrains.annotations.NotNull; + +/** + * Language server console factory. + * + * @author Angelo ZERR + */ +public class LSPConsoleToolWindowFactory implements ToolWindowFactory { + + @Override + public void createToolWindowContent(@NotNull Project project, @NotNull ToolWindow toolWindow) { + LSPConsoleToolWindowPanel consoleWindow = new LSPConsoleToolWindowPanel(project); + ContentManager contentManager = toolWindow.getContentManager(); + Content content = contentManager.getFactory().createContent(consoleWindow, + LanguageServerBundle.message("lsp.console.title"), false); + //Disposer.register(contentManager, consoleWindow); + contentManager.addContent(content); + } +} diff --git a/src/main/java/com/redhat/devtools/intellij/quarkus/lsp4ij/console/LSPConsoleToolWindowPanel.java b/src/main/java/com/redhat/devtools/intellij/quarkus/lsp4ij/console/LSPConsoleToolWindowPanel.java new file mode 100644 index 000000000..5b4651d36 --- /dev/null +++ b/src/main/java/com/redhat/devtools/intellij/quarkus/lsp4ij/console/LSPConsoleToolWindowPanel.java @@ -0,0 +1,155 @@ +/******************************************************************************* + * Copyright (c) 2023 Red Hat Inc. and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 + * which is available at https://www.apache.org/licenses/LICENSE-2.0. + * + * SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 + * + * Contributors: + * Red Hat Inc. - initial API and implementation + *******************************************************************************/ +package com.redhat.devtools.intellij.quarkus.lsp4ij.console; + +import com.intellij.execution.filters.TextConsoleBuilderFactory; +import com.intellij.execution.ui.ConsoleView; +import com.intellij.execution.ui.ConsoleViewContentType; +import com.intellij.openapi.Disposable; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.ui.SimpleToolWindowPanel; +import com.intellij.ui.CardLayoutPanel; +import com.intellij.ui.OnePixelSplitter; +import com.intellij.ui.components.JBScrollPane; +import com.redhat.devtools.intellij.quarkus.lsp4ij.console.explorer.LanguageServerExplorer; +import com.redhat.devtools.intellij.quarkus.lsp4ij.console.explorer.LanguageServerProcessTreeNode; + +import javax.swing.*; + +/** + * LSP consoles + */ +public class LSPConsoleToolWindowPanel extends SimpleToolWindowPanel implements Disposable { + + private final Project project; + + private LanguageServerExplorer explorer; + + private ConsolesPanel consoles; + private boolean disposed; + + public LSPConsoleToolWindowPanel(Project project) { + super(false, true); + this.project = project; + createUI(); + } + + private void createUI() { + explorer = new LanguageServerExplorer(this); + var scrollPane = new JBScrollPane(explorer); + this.consoles = new ConsolesPanel(); + var splitPane = createSplitPanel(scrollPane, consoles); + super.setContent(splitPane); + super.revalidate(); + super.repaint(); + } + + public Project getProject() { + return project; + } + + private static JComponent createSplitPanel(JComponent left, JComponent right) { + OnePixelSplitter splitter = new OnePixelSplitter(false, 0.15f); + splitter.setShowDividerControls(true); + splitter.setHonorComponentsMinimumSize(true); + splitter.setFirstComponent(left); + splitter.setSecondComponent(right); + return splitter; + } + + public void selectConsole(LanguageServerProcessTreeNode processTreeNode) { + if (consoles == null || isDisposed()) { + return; + } + consoles.select(processTreeNode, true); + } + + /** + * A card-panel that displays panels for each language server instances. + */ + private class ConsolesPanel extends CardLayoutPanel { + + @Override + protected LanguageServerProcessTreeNode prepare(LanguageServerProcessTreeNode key) { + return key; + } + + @Override + protected LSPConsoleToolWindowPanel.ConsoleOrErrorPanel create(LanguageServerProcessTreeNode key) { + if (isDisposed() || LSPConsoleToolWindowPanel.this.isDisposed()) { + return null; + } + return new LSPConsoleToolWindowPanel.ConsoleOrErrorPanel(); + } + + @Override + public void dispose() { + removeAll(); + } + + @Override + protected void dispose(LanguageServerProcessTreeNode key, LSPConsoleToolWindowPanel.ConsoleOrErrorPanel value) { + if (value != null) { + value.dispose(); + } + } + } + + private class ConsoleOrErrorPanel extends SimpleCardLayoutPanel { + + private static final String NAME_VIEW_CONSOLE = "console"; + + private final ConsoleView consoleView; + + public ConsoleOrErrorPanel() { + consoleView = createConsoleView(project); + add(consoleView.getComponent(), NAME_VIEW_CONSOLE); + showConsole(); + } + + private void showConsole() { + show(NAME_VIEW_CONSOLE); + } + + public void showMessage(String message) { + consoleView.print(message, ConsoleViewContentType.SYSTEM_OUTPUT); + } + } + + private ConsoleView createConsoleView(Project project) { + var builder = TextConsoleBuilderFactory.getInstance().createBuilder(project); + builder.setViewer(true); + return builder.getConsole(); + } + + public void showMessage(LanguageServerProcessTreeNode processTreeNode, String message) { + if (isDisposed()) { + return; + } + var consoleOrErrorPanel = consoles.getValue(processTreeNode, true); + if (consoleOrErrorPanel != null) { + consoleOrErrorPanel.showMessage(message); + } + } + + @Override + public void dispose() { + disposed = true; + explorer.dispose(); + } + + private boolean isDisposed() { + return disposed || project.isDisposed(); + } +} diff --git a/src/main/java/com/redhat/devtools/intellij/quarkus/lsp4ij/console/SimpleCardLayoutPanel.java b/src/main/java/com/redhat/devtools/intellij/quarkus/lsp4ij/console/SimpleCardLayoutPanel.java new file mode 100644 index 000000000..2c5b1a7b3 --- /dev/null +++ b/src/main/java/com/redhat/devtools/intellij/quarkus/lsp4ij/console/SimpleCardLayoutPanel.java @@ -0,0 +1,89 @@ +/******************************************************************************* + * Copyright (c) 2023 Red Hat Inc. and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 + * which is available at https://www.apache.org/licenses/LICENSE-2.0. + * + * SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 + * + * Contributors: + * Red Hat Inc. - initial API and implementation + *******************************************************************************/ +package com.redhat.devtools.intellij.quarkus.lsp4ij.console; + +import com.intellij.openapi.Disposable; +import com.intellij.util.ui.JBInsets; + +import java.awt.*; +import javax.swing.JComponent; +import javax.swing.JPanel; + +abstract class SimpleCardLayoutPanel extends JPanel implements Disposable { + + protected volatile boolean isDisposed = false; + + private CardLayout cardLayout; + + public SimpleCardLayoutPanel() { + this(new CardLayout()); + } + + public SimpleCardLayoutPanel(CardLayout cardLayout) { + super(cardLayout); + this.cardLayout = cardLayout; + } + + private Component visibleComponent() { + for (var component : getComponents()) { + if (component.isVisible()) return component; + } + return null; + } + + public void show(String name) { + cardLayout.show(this, name); + } + + @Override + public void dispose() { + if (!isDisposed) { + isDisposed = true; + removeAll(); + } + } + + @Override + public void doLayout() { + var bounds = new Rectangle(getWidth(), getHeight()); + JBInsets.removeFrom(bounds, getInsets()); + for (var component : getComponents()) { + component.setBounds(bounds); + } + } + + @Override + public Dimension getPreferredSize() { + var component = isPreferredSizeSet() ? null : visibleComponent(); + if (component == null) { + return super.getPreferredSize(); + } + // preferred size of a visible component plus border insets of this panel + var size = component.getPreferredSize(); + JBInsets.addTo(size, getInsets()); // add border of this panel + return size; + } + + @Override + public Dimension getMinimumSize() { + var component = isMinimumSizeSet() ? null : visibleComponent(); + if (component == null) { + return super.getMinimumSize(); + } + // minimum size of a visible component plus border insets of this panel + var size = component.getMinimumSize(); + JBInsets.addTo(size, getInsets()); + return size; + } +} \ No newline at end of file diff --git a/src/main/java/com/redhat/devtools/intellij/quarkus/lsp4ij/console/explorer/LanguageServerExplorer.java b/src/main/java/com/redhat/devtools/intellij/quarkus/lsp4ij/console/explorer/LanguageServerExplorer.java new file mode 100644 index 000000000..0b7d3a24d --- /dev/null +++ b/src/main/java/com/redhat/devtools/intellij/quarkus/lsp4ij/console/explorer/LanguageServerExplorer.java @@ -0,0 +1,127 @@ +/******************************************************************************* + * Copyright (c) 2023 Red Hat Inc. and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 + * which is available at https://www.apache.org/licenses/LICENSE-2.0. + * + * SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 + * + * Contributors: + * Red Hat Inc. - initial API and implementation + *******************************************************************************/ +package com.redhat.devtools.intellij.quarkus.lsp4ij.console.explorer; + +import com.intellij.openapi.Disposable; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.ui.SimpleToolWindowPanel; +import com.intellij.ui.AnimatedIcon; +import com.intellij.ui.treeStructure.Tree; +import com.redhat.devtools.intellij.quarkus.lsp4ij.LanguageServersRegistry; +import com.redhat.devtools.intellij.quarkus.lsp4ij.console.LSPConsoleToolWindowPanel; +import com.redhat.devtools.intellij.quarkus.lsp4ij.lifecycle.LanguageServerLifecycleListener; +import com.redhat.devtools.intellij.quarkus.lsp4ij.lifecycle.LanguageServerLifecycleManager; + +import javax.swing.tree.DefaultMutableTreeNode; +import javax.swing.tree.DefaultTreeModel; +import javax.swing.tree.TreePath; + +/** + * Language server explorer which shows language servers and their process. + * + * @author Angelo ZERR + */ +public class LanguageServerExplorer extends SimpleToolWindowPanel implements Disposable { + + private final LSPConsoleToolWindowPanel panel; + + private final Tree tree; + private final LanguageServerExplorerLifecycleListener listener; + private boolean disposed; + + public LanguageServerExplorer(LSPConsoleToolWindowPanel panel) { + super(true, false); + this.panel = panel; + listener = new LanguageServerExplorerLifecycleListener(this); + LanguageServerLifecycleManager.getInstance(panel.getProject()) + .addLanguageServerLifecycleListener(listener); + tree = buildTree(); + this.setContent(tree); + } + + private void onLanguageServerSelected(LanguageServerProcessTreeNode processTreeNode) { + if (isDisposed()) { + return; + } + panel.selectConsole(processTreeNode); + } + + /** + * Builds the Language server tree + * + * @return Tree object of all language servers + */ + private Tree buildTree() { + + DefaultMutableTreeNode top = new DefaultMutableTreeNode("Language servers"); + + Tree tree = new Tree(top); + tree.setRootVisible(false); + + // Fill tree will all language server definitions + LanguageServersRegistry.getInstance().getAllDefinitions() + .forEach(serverDefinition -> top.add(new LanguageServerTreeNode(serverDefinition))); + + tree.setCellRenderer(new LanguageServerTreeRenderer()); + + tree.addTreeSelectionListener(l -> { + TreePath selectionPath = tree.getSelectionPath(); + Object selectedItem = selectionPath != null ? selectionPath.getLastPathComponent() : null; + if (selectedItem instanceof LanguageServerProcessTreeNode) { + LanguageServerProcessTreeNode node = (LanguageServerProcessTreeNode) selectedItem; + onLanguageServerSelected(node); + } + }); + + tree.putClientProperty(AnimatedIcon.ANIMATION_IN_RENDERER_ALLOWED, true); + + ((DefaultTreeModel) tree.getModel()).reload(top); + return tree; + } + + public Tree getTree() { + return tree; + } + + @Override + public void dispose() { + this.disposed = true; + LanguageServerLifecycleManager.getInstance(panel.getProject()) + .removeLanguageServerLifecycleListener(listener); + } + + public boolean isDisposed() { + return disposed || getProject().isDisposed() || listener.isDisposed(); + } + + public void showMessage(LanguageServerProcessTreeNode processTreeNode, String message) { + panel.showMessage(processTreeNode, message); + } + + public DefaultTreeModel getTreeModel() { + return (DefaultTreeModel) tree.getModel(); + } + + public void selectAndExpand(DefaultMutableTreeNode treeNode) { + var treePath = new TreePath(treeNode.getPath()); + tree.setSelectionPath(treePath); + if (!tree.isExpanded(treePath)) { + tree.expandPath(treePath); + } + } + + public Project getProject() { + return panel.getProject(); + } +} diff --git a/src/main/java/com/redhat/devtools/intellij/quarkus/lsp4ij/console/explorer/LanguageServerExplorerLifecycleListener.java b/src/main/java/com/redhat/devtools/intellij/quarkus/lsp4ij/console/explorer/LanguageServerExplorerLifecycleListener.java new file mode 100644 index 000000000..c70f23c07 --- /dev/null +++ b/src/main/java/com/redhat/devtools/intellij/quarkus/lsp4ij/console/explorer/LanguageServerExplorerLifecycleListener.java @@ -0,0 +1,292 @@ +/******************************************************************************* + * Copyright (c) 2023 Red Hat Inc. and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 + * which is available at https://www.apache.org/licenses/LICENSE-2.0. + * + * SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 + * + * Contributors: + * Red Hat Inc. - initial API and implementation + *******************************************************************************/ +package com.redhat.devtools.intellij.quarkus.lsp4ij.console.explorer; + +import com.intellij.openapi.application.ApplicationManager; +import com.redhat.devtools.intellij.quarkus.lsp4ij.LanguageServerWrapper; +import com.redhat.devtools.intellij.quarkus.lsp4ij.lifecycle.LanguageServerLifecycleListener; +import com.redhat.devtools.intellij.quarkus.lsp4ij.settings.ServerTrace; +import com.redhat.devtools.intellij.quarkus.lsp4ij.settings.UserDefinedLanguageServerSettings; +import org.eclipse.lsp4j.jsonrpc.messages.Message; +import org.eclipse.lsp4j.jsonrpc.messages.NotificationMessage; +import org.eclipse.lsp4j.jsonrpc.messages.RequestMessage; +import org.eclipse.lsp4j.jsonrpc.messages.ResponseMessage; + +import javax.swing.tree.DefaultMutableTreeNode; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Language server listener to refresh the language server explorer according to the server state and fill the LSP console. + * + * @author Angelo ZERR + */ +public class LanguageServerExplorerLifecycleListener implements LanguageServerLifecycleListener { + + private static final DateTimeFormatter dateTracePattern = DateTimeFormatter.ofPattern("hh:mm:ss a"); + + private boolean disposed; + + private static class LSPRequestInfo { + + public final String method; + + public final long startTime; + + public LSPRequestInfo(String method, long startTime) { + this.method = method; + this.startTime = startTime; + } + } + + private final Map> pendingRequests = new ConcurrentHashMap<>(10); + + private final LanguageServerExplorer explorer; + + public LanguageServerExplorerLifecycleListener(LanguageServerExplorer explorer) { + this.explorer = explorer; + } + + @Override + public void handleStartingProcess(LanguageServerWrapper languageServer) { + invokeLater(() -> { + if (isDisposed()) { + return; + } + LanguageServerProcessTreeNode processTreeNode = findLanguageServerProcessTreeNode(languageServer); + if (processTreeNode != null) { + processTreeNode.setServerStatus(ServerStatus.startingProcess); + explorer.selectAndExpand(processTreeNode); + } + }); + } + + @Override + public void handleStartedProcess(LanguageServerWrapper languageServer, Throwable exception) { + invokeLater(() -> { + if (isDisposed()) { + return; + } + LanguageServerProcessTreeNode processTreeNode = findLanguageServerProcessTreeNode(languageServer); + if (processTreeNode != null) { + processTreeNode.setServerStatus(ServerStatus.startedProcess); + } + }); + } + + @Override + public void handleStartedLanguageServer(LanguageServerWrapper languageServer, Throwable exception) { + invokeLater(() -> { + if (explorer.isDisposed()) { + return; + } + LanguageServerProcessTreeNode processTreeNode = findLanguageServerProcessTreeNode(languageServer); + if (processTreeNode != null) { + processTreeNode.setServerStatus(ServerStatus.started); + } + }); + } + + @Override + public void handleLSPMessage(Message message, LanguageServerWrapper languageServer) { + if (explorer.isDisposed()) { + return; + } + ServerTrace serverTrace = getServerTrace(languageServer.serverDefinition.id); + if (serverTrace == ServerTrace.off) { + return; + } + + StringBuilder formattedMessage = new StringBuilder(); + fillHeaderTrace(formattedMessage); + if (message instanceof RequestMessage) { + // [Trace - 12:27:33 AM] Sending request 'initialize - (0)'. + // Params: { + String id = ((RequestMessage) message).getId(); + String method = ((RequestMessage) message).getMethod(); + registerLSPRequest(id, new LSPRequestInfo(method, System.currentTimeMillis()), languageServer); + formattedMessage.append(" Sending request '") + .append(method) + .append(" - (") + .append(id) + .append(")'."); + } else if (message instanceof ResponseMessage) { + // [Trace - 12:27:35 AM] Received response 'initialize - (0)' in 1921ms. + String id = ((ResponseMessage) message).getId(); + LSPRequestInfo requestInfo = unregisterLSPRequest(id, languageServer); + String method = requestInfo != null ? requestInfo.method : ""; + formattedMessage.append(" Received response '") + .append(method) + .append(" - (") + .append(id) + .append(")'"); + if (requestInfo != null) { + formattedMessage.append(" in "); + formattedMessage.append(System.currentTimeMillis() - requestInfo.startTime); + formattedMessage.append("ms"); + } + formattedMessage.append("."); + } else if (message instanceof NotificationMessage) { + // [Trace - 12:27:35 AM] Sending notification 'initialized'. + String method = ((NotificationMessage) message).getMethod(); + formattedMessage.append(" Sending notification '") + .append(method) + .append("'."); + } + if (serverTrace == ServerTrace.verbose) { + formattedMessage.append("\n"); + formattedMessage.append(message.toString()); + formattedMessage.append("\n"); + } + formattedMessage.append("\n"); + + invokeLater(() -> showMessage(languageServer, formattedMessage.toString())); + } + + private static void fillHeaderTrace(StringBuilder formattedMessage) { + LocalDateTime datetime = LocalDateTime.now(); + String dateAsString = datetime.format(dateTracePattern); + formattedMessage.append("[Trace") + .append(" - ") + .append(dateAsString) + .append("]"); + } + + private void registerLSPRequest(String id, LSPRequestInfo lspRequestInfo, LanguageServerWrapper languageServer) { + Map cache = getLSPRequestCacheFor(languageServer); + synchronized (cache) { + cache.put(id, lspRequestInfo); + } + } + + private LSPRequestInfo unregisterLSPRequest(String id, LanguageServerWrapper languageServer) { + Map cache = getLSPRequestCacheFor(languageServer); + synchronized (cache) { + return cache.remove(id); + } + } + + private Map getLSPRequestCacheFor(LanguageServerWrapper languageServer) { + Map cache = pendingRequests.get(languageServer); + if (cache != null) { + return cache; + } + synchronized (pendingRequests) { + cache = pendingRequests.get(languageServer); + if (cache != null) { + return cache; + } + cache = new HashMap<>(); + pendingRequests.put(languageServer, cache); + return cache; + } + } + + @Override + public void handleStoppingLanguageServer(LanguageServerWrapper languageServer) { + invokeLater(() -> { + if (explorer.isDisposed()) { + return; + } + LanguageServerProcessTreeNode processTreeNode = findLanguageServerProcessTreeNode(languageServer); + if (processTreeNode != null) { + processTreeNode.setServerStatus(ServerStatus.stopping); + } + }); + } + + @Override + public void handleStoppedLanguageServer(LanguageServerWrapper languageServer, Throwable exception) { + invokeLater(() -> { + if (explorer.isDisposed()) { + return; + } + LanguageServerProcessTreeNode processTreeNode = findLanguageServerProcessTreeNode(languageServer); + if (processTreeNode != null) { + processTreeNode.setServerStatus(ServerStatus.stopped); + } + }); + } + + private static ServerTrace getServerTrace(String languageServerId) { + ServerTrace serverTrace = null; + UserDefinedLanguageServerSettings.LanguageServerDefinitionSettings settings = UserDefinedLanguageServerSettings.getInstance().getLanguageServerSettings(languageServerId); + if (settings != null) { + serverTrace = settings.getServerTrace(); + } + return serverTrace != null ? serverTrace : ServerTrace.off; + } + + private LanguageServerTreeNode findLanguageServerTreeNode(LanguageServerWrapper languageServer) { + var tree = explorer.getTree(); + DefaultMutableTreeNode top = (DefaultMutableTreeNode) tree.getModel().getRoot(); + for (int i = 0; i < top.getChildCount(); i++) { + LanguageServerTreeNode node = (LanguageServerTreeNode) top.getChildAt(i); + if (node.getServerDefinition().equals(languageServer.serverDefinition)) { + return node; + } + } + return null; + } + + private LanguageServerProcessTreeNode findLanguageServerProcessTreeNode(LanguageServerWrapper languageServer) { + LanguageServerTreeNode node = findLanguageServerTreeNode(languageServer); + if (node == null) { + return null; + } + var processTreeNode = node.getActiveProcessTreeNode(); + if (processTreeNode == null) { + var treeModel = explorer.getTreeModel(); + processTreeNode = new LanguageServerProcessTreeNode(languageServer, treeModel); + node.add(processTreeNode); + } + return processTreeNode; + } + + private void showMessage(LanguageServerWrapper languageServer, String message) { + if (explorer.isDisposed()) { + return; + } + LanguageServerProcessTreeNode processTreeNode = findLanguageServerProcessTreeNode(languageServer); + if (processTreeNode != null) { + if (processTreeNode.getServerStatus() == null) { + processTreeNode.setServerStatus(ServerStatus.started); + explorer.selectAndExpand(processTreeNode); + } + explorer.showMessage(processTreeNode, message); + } + } + + public boolean isDisposed() { + return disposed; + } + + @Override + public void dispose() { + disposed = true; + pendingRequests.clear(); + } + + private static void invokeLater(Runnable runnable) { + if (ApplicationManager.getApplication().isDispatchThread()) { + runnable.run(); + } else { + ApplicationManager.getApplication().invokeLater(runnable); + } + } + +} diff --git a/src/main/java/com/redhat/devtools/intellij/quarkus/lsp4ij/console/explorer/LanguageServerProcessTreeNode.java b/src/main/java/com/redhat/devtools/intellij/quarkus/lsp4ij/console/explorer/LanguageServerProcessTreeNode.java new file mode 100644 index 000000000..5fa97f856 --- /dev/null +++ b/src/main/java/com/redhat/devtools/intellij/quarkus/lsp4ij/console/explorer/LanguageServerProcessTreeNode.java @@ -0,0 +1,99 @@ +/******************************************************************************* + * Copyright (c) 2023 Red Hat Inc. and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 + * which is available at https://www.apache.org/licenses/LICENSE-2.0. + * + * SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 + * + * Contributors: + * Red Hat Inc. - initial API and implementation + *******************************************************************************/ +package com.redhat.devtools.intellij.quarkus.lsp4ij.console.explorer; + +import com.intellij.icons.AllIcons; +import com.intellij.openapi.util.text.StringUtil; +import com.intellij.ui.AnimatedIcon; +import com.redhat.devtools.intellij.quarkus.lsp4ij.LanguageServerWrapper; + +import javax.swing.*; +import javax.swing.tree.DefaultMutableTreeNode; +import javax.swing.tree.DefaultTreeModel; +import javax.swing.tree.MutableTreeNode; + +/** + * Language server process node. + */ +public class LanguageServerProcessTreeNode extends DefaultMutableTreeNode { + + private static final Icon RUNNING_ICON = new AnimatedIcon.Default(); + + private final LanguageServerWrapper languageServer; + + private final DefaultTreeModel treeModel; + + private ServerStatus serverStatus; + + private long startTime = -1; + + public LanguageServerProcessTreeNode(LanguageServerWrapper languageServer, DefaultTreeModel treeModel) { + this.languageServer = languageServer; + this.treeModel = treeModel; + } + + public void setServerStatus(ServerStatus serverStatus) { + this.serverStatus = serverStatus; + switch(serverStatus) { + case startingProcess: + startTime = System.currentTimeMillis(); + super.setUserObject("starting process..."); + break; + case startedProcess: + super.setUserObject("process started"); + break; + case starting: + super.setUserObject("starting..."); + break; + case started: + startTime = -1; + super.setUserObject("started"); + break; + case stopping: + startTime = System.currentTimeMillis(); + super.setUserObject("stopping..."); + break; + case stopped: + startTime = -1; + super.setUserObject("stopped"); + break; + } + treeModel.reload(this); + } + + public ServerStatus getServerStatus() { + return serverStatus; + } + + public Icon getIcon() { + switch(serverStatus) { + case started: + return AllIcons.Debugger.ThreadRunning; + case stopped: + return AllIcons.Debugger.ThreadSuspended; + default: + return RUNNING_ICON; + } + } + + public String getDisplayName() { + return (String) super.getUserObject(); + } + + public String getElapsedTime() { + long endTime = System.currentTimeMillis(); + long duration = endTime - startTime; + return StringUtil.formatDuration(duration, "\u2009"); + } +} diff --git a/src/main/java/com/redhat/devtools/intellij/quarkus/lsp4ij/console/explorer/LanguageServerTreeNode.java b/src/main/java/com/redhat/devtools/intellij/quarkus/lsp4ij/console/explorer/LanguageServerTreeNode.java new file mode 100644 index 000000000..331b0eea9 --- /dev/null +++ b/src/main/java/com/redhat/devtools/intellij/quarkus/lsp4ij/console/explorer/LanguageServerTreeNode.java @@ -0,0 +1,54 @@ +/******************************************************************************* + * Copyright (c) 2023 Red Hat Inc. and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 + * which is available at https://www.apache.org/licenses/LICENSE-2.0. + * + * SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 + * + * Contributors: + * Red Hat Inc. - initial API and implementation + *******************************************************************************/ +package com.redhat.devtools.intellij.quarkus.lsp4ij.console.explorer; + +import com.intellij.icons.AllIcons; +import com.redhat.devtools.intellij.quarkus.lsp4ij.LanguageServersRegistry; +import com.redhat.devtools.intellij.quarkus.lsp4ij.console.explorer.LanguageServerProcessTreeNode; + +import javax.swing.*; +import javax.swing.tree.DefaultMutableTreeNode; + +/** + * Language server node. + */ +public class LanguageServerTreeNode extends DefaultMutableTreeNode { + + private final LanguageServersRegistry.LanguageServerDefinition serverDefinition; + + public LanguageServerTreeNode(LanguageServersRegistry.LanguageServerDefinition serverDefinition) { + this.serverDefinition = serverDefinition; + } + + public LanguageServersRegistry.LanguageServerDefinition getServerDefinition() { + return serverDefinition; + } + + public LanguageServerProcessTreeNode getActiveProcessTreeNode() { + for (int i = 0; i < super.getChildCount(); i++) { + return (LanguageServerProcessTreeNode) super.getChildAt(i); + } + return null; + } + + public Icon getIcon() { + String serverId = getServerDefinition().id; + Icon icon = LanguageServersRegistry.getInstance().getServerIcon(serverId); + return icon != null ? icon : AllIcons.Webreferences.Server; + } + + public String getDisplayName() { + return serverDefinition.getDisplayName(); + } +} diff --git a/src/main/java/com/redhat/devtools/intellij/quarkus/lsp4ij/console/explorer/LanguageServerTreeRenderer.java b/src/main/java/com/redhat/devtools/intellij/quarkus/lsp4ij/console/explorer/LanguageServerTreeRenderer.java new file mode 100644 index 000000000..4595469e9 --- /dev/null +++ b/src/main/java/com/redhat/devtools/intellij/quarkus/lsp4ij/console/explorer/LanguageServerTreeRenderer.java @@ -0,0 +1,120 @@ +/******************************************************************************* + * Copyright (c) 2023 Red Hat Inc. and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 + * which is available at https://www.apache.org/licenses/LICENSE-2.0. + * + * SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 + * + * Contributors: + * Red Hat Inc. - initial API and implementation + *******************************************************************************/ +package com.redhat.devtools.intellij.quarkus.lsp4ij.console.explorer; + +import com.intellij.ide.ui.UISettings; +import com.intellij.ui.ColoredTreeCellRenderer; +import com.intellij.ui.RelativeFont; +import com.intellij.ui.SimpleTextAttributes; +import com.intellij.util.ui.UIUtil; +import org.jetbrains.annotations.NonNls; +import org.jetbrains.annotations.NotNull; + +import javax.swing.*; +import java.awt.*; + +/** + * Language Server tree nodes renderer. + *

+ * Some piece of code has been copied/pasted from https://github.com/JetBrains/intellij-community/blob/master/platform/smRunner/src/com/intellij/execution/testframework/sm/runner/ui/TestTreeRenderer.java and adapted for Language Server case. + */ +public class LanguageServerTreeRenderer extends ColoredTreeCellRenderer { + + @NonNls + private static final String SPACE_STRING = " "; + private String myDurationText; + private Color myDurationColor; + private int myDurationWidth; + private int myDurationOffset; + + @Override + public void customizeCellRenderer(@NotNull final JTree tree, + final Object value, + final boolean selected, + final boolean expanded, + final boolean leaf, + final int row, + final boolean hasFocus) { + myDurationText = null; + myDurationColor = null; + myDurationWidth = 0; + myDurationOffset = 0; + + if (value instanceof LanguageServerTreeNode) { + // Render of language server + LanguageServerTreeNode languageServerTreeNode = (LanguageServerTreeNode) value; + setIcon(languageServerTreeNode.getIcon()); + append(languageServerTreeNode.getDisplayName()); + return; + } + + if (value instanceof LanguageServerProcessTreeNode) { + // Render of language server process + LanguageServerProcessTreeNode languageProcessTreeNode = (LanguageServerProcessTreeNode) value; + setIcon(languageProcessTreeNode.getIcon()); + append(languageProcessTreeNode.getDisplayName()); + + if (languageProcessTreeNode.getServerStatus() != ServerStatus.started && languageProcessTreeNode.getServerStatus() != ServerStatus.stopped) { + // Display elapsed time when language server is starting/stopping + myDurationText = languageProcessTreeNode.getElapsedTime(); + if (myDurationText != null) { + FontMetrics metrics = getFontMetrics(RelativeFont.SMALL.derive(getFont())); + myDurationWidth = metrics.stringWidth(myDurationText); + myDurationOffset = metrics.getHeight() / 2; // an empty area before and after the text + myDurationColor = selected ? UIUtil.getTreeSelectionForeground(hasFocus) : SimpleTextAttributes.GRAYED_ATTRIBUTES.getFgColor(); + } + } + return; + } + //strange node + final String text = value.toString(); + //no icon + append(text != null ? text : SPACE_STRING, SimpleTextAttributes.GRAYED_ATTRIBUTES); + } + + @NotNull + @Override + public Dimension getPreferredSize() { + final Dimension preferredSize = super.getPreferredSize(); + if (myDurationWidth > 0) preferredSize.width += myDurationWidth + myDurationOffset; + return preferredSize; + } + + @Override + protected void paintComponent(Graphics g) { + UISettings.setupAntialiasing(g); + Shape clip = null; + int width = getWidth(); + int height = getHeight(); + if (isOpaque()) { + // paint background for expanded row + g.setColor(getBackground()); + g.fillRect(0, 0, width, height); + } + if (myDurationWidth > 0) { + width -= myDurationWidth + myDurationOffset; + if (width > 0 && height > 0) { + g.setColor(myDurationColor); + g.setFont(RelativeFont.SMALL.derive(getFont())); + g.drawString(myDurationText, width + myDurationOffset / 2, getTextBaseLine(g.getFontMetrics(), height)); + clip = g.getClip(); + g.clipRect(0, 0, width, height); + } + } + super.paintComponent(g); + // restore clip area if needed + if (clip != null) g.setClip(clip); + } +} + diff --git a/src/main/java/com/redhat/devtools/intellij/quarkus/lsp4ij/console/explorer/ServerStatus.java b/src/main/java/com/redhat/devtools/intellij/quarkus/lsp4ij/console/explorer/ServerStatus.java new file mode 100644 index 000000000..fa0d65ce1 --- /dev/null +++ b/src/main/java/com/redhat/devtools/intellij/quarkus/lsp4ij/console/explorer/ServerStatus.java @@ -0,0 +1,27 @@ +/******************************************************************************* + * Copyright (c) 2023 Red Hat Inc. and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 + * which is available at https://www.apache.org/licenses/LICENSE-2.0. + * + * SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 + * + * Contributors: + * Red Hat Inc. - initial API and implementation + *******************************************************************************/ +package com.redhat.devtools.intellij.quarkus.lsp4ij.console.explorer; + +/** + * Language server status. + */ +public enum ServerStatus { + + startingProcess, + startedProcess, + starting, + started, + stopping, + stopped; +} diff --git a/src/main/java/com/redhat/devtools/intellij/quarkus/lsp4ij/internal/SupportedFeatures.java b/src/main/java/com/redhat/devtools/intellij/quarkus/lsp4ij/internal/SupportedFeatures.java new file mode 100644 index 000000000..e05b1fcbb --- /dev/null +++ b/src/main/java/com/redhat/devtools/intellij/quarkus/lsp4ij/internal/SupportedFeatures.java @@ -0,0 +1,160 @@ +/******************************************************************************* + * Copyright (c) 2022-3 Cocotec Ltd 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: + * Ahmed Hussain (Cocotec Ltd) - initial implementation + * + *******************************************************************************/ +package com.redhat.devtools.intellij.quarkus.lsp4ij.internal; + +import java.util.Arrays; +import java.util.List; + +import org.eclipse.lsp4j.CodeActionCapabilities; +import org.eclipse.lsp4j.CodeActionKind; +import org.eclipse.lsp4j.CodeActionKindCapabilities; +import org.eclipse.lsp4j.CodeActionLiteralSupportCapabilities; +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.CompletionItemInsertTextModeSupportCapabilities; +import org.eclipse.lsp4j.CompletionItemResolveSupportCapabilities; +import org.eclipse.lsp4j.DefinitionCapabilities; +import org.eclipse.lsp4j.DocumentHighlightCapabilities; +import org.eclipse.lsp4j.DocumentLinkCapabilities; +import org.eclipse.lsp4j.DocumentSymbolCapabilities; +import org.eclipse.lsp4j.ExecuteCommandCapabilities; +import org.eclipse.lsp4j.FailureHandlingKind; +import org.eclipse.lsp4j.FoldingRangeCapabilities; +import org.eclipse.lsp4j.FormattingCapabilities; +import org.eclipse.lsp4j.HoverCapabilities; +import org.eclipse.lsp4j.InlayHintCapabilities; +import org.eclipse.lsp4j.InsertTextMode; +import org.eclipse.lsp4j.MarkupKind; +import org.eclipse.lsp4j.RangeFormattingCapabilities; +import org.eclipse.lsp4j.ReferencesCapabilities; +import org.eclipse.lsp4j.RenameCapabilities; +import org.eclipse.lsp4j.ResourceOperationKind; +import org.eclipse.lsp4j.SelectionRangeCapabilities; +import org.eclipse.lsp4j.ShowDocumentCapabilities; +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.TypeDefinitionCapabilities; +import org.eclipse.lsp4j.WindowClientCapabilities; +import org.eclipse.lsp4j.WindowShowMessageRequestCapabilities; +import org.eclipse.lsp4j.WorkspaceClientCapabilities; +import org.eclipse.lsp4j.WorkspaceEditCapabilities; + +import javax.annotation.Nonnull; + +/** + * + */ +public class SupportedFeatures { + + public static @Nonnull TextDocumentClientCapabilities getTextDocumentClientCapabilities() { + final var textDocumentClientCapabilities = new TextDocumentClientCapabilities(); + final var 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()); + // TODO : support textDocument/colorPresentation + // textDocumentClientCapabilities.setColorProvider(new ColorProviderCapabilities()); + final var completionItemCapabilities = new CompletionItemCapabilities(Boolean.TRUE); + completionItemCapabilities + .setDocumentationFormat(Arrays.asList(MarkupKind.MARKDOWN, MarkupKind.PLAINTEXT)); + completionItemCapabilities.setInsertTextModeSupport(new CompletionItemInsertTextModeSupportCapabilities(List.of(InsertTextMode.AsIs, InsertTextMode.AdjustIndentation))); + completionItemCapabilities.setResolveSupport(new CompletionItemResolveSupportCapabilities(List.of("documentation", "detail", "additionalTextEdits"))); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ + textDocumentClientCapabilities.setCompletion(new CompletionCapabilities(completionItemCapabilities)); + final var definitionCapabilities = new DefinitionCapabilities(); + definitionCapabilities.setLinkSupport(Boolean.TRUE); + textDocumentClientCapabilities.setDefinition(definitionCapabilities); + final var typeDefinitionCapabilities = new TypeDefinitionCapabilities(); + typeDefinitionCapabilities.setLinkSupport(Boolean.TRUE); + textDocumentClientCapabilities.setTypeDefinition(typeDefinitionCapabilities); + textDocumentClientCapabilities.setDocumentHighlight(new DocumentHighlightCapabilities()); + // TODO : support textDocument/documentLink + // textDocumentClientCapabilities.setDocumentLink(new DocumentLinkCapabilities()); + // TODO : support textDocument/documentSymbol + /** final var 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); + **/ + // TODO : support textDocument/foldingRange + // textDocumentClientCapabilities.setFoldingRange(new FoldingRangeCapabilities()); + // TODO : support textDocument/formatting + // textDocumentClientCapabilities.setFormatting(new FormattingCapabilities(Boolean.TRUE)); + final var hoverCapabilities = new HoverCapabilities(); + hoverCapabilities.setContentFormat(Arrays.asList(MarkupKind.MARKDOWN, MarkupKind.PLAINTEXT)); + textDocumentClientCapabilities.setHover(hoverCapabilities); + textDocumentClientCapabilities.setOnTypeFormatting(null); // TODO + // TODO : support textDocument/rangeFormatting + // textDocumentClientCapabilities.setRangeFormatting(new RangeFormattingCapabilities()); + textDocumentClientCapabilities.setReferences(new ReferencesCapabilities()); + final var renameCapabilities = new RenameCapabilities(); + renameCapabilities.setPrepareSupport(true); + textDocumentClientCapabilities.setRename(renameCapabilities); + // TODO + // textDocumentClientCapabilities.setSignatureHelp(new SignatureHelpCapabilities()); + textDocumentClientCapabilities + .setSynchronization(new SynchronizationCapabilities(Boolean.TRUE, Boolean.TRUE, Boolean.TRUE)); + // TODO + // SelectionRangeCapabilities selectionRange = new SelectionRangeCapabilities(); + // textDocumentClientCapabilities.setSelectionRange(selectionRange); + return textDocumentClientCapabilities; + } + + public static @Nonnull WorkspaceClientCapabilities getWorkspaceClientCapabilities() { + final var workspaceClientCapabilities = new WorkspaceClientCapabilities(); + workspaceClientCapabilities.setApplyEdit(Boolean.TRUE); + // TODO + // workspaceClientCapabilities.setConfiguration(Boolean.TRUE); + workspaceClientCapabilities.setExecuteCommand(new ExecuteCommandCapabilities(Boolean.TRUE)); + // TODO + // workspaceClientCapabilities.setSymbol(new SymbolCapabilities(Boolean.TRUE)); + workspaceClientCapabilities.setWorkspaceFolders(Boolean.TRUE); + WorkspaceEditCapabilities editCapabilities = new WorkspaceEditCapabilities(); + editCapabilities.setDocumentChanges(Boolean.TRUE); + // TODO + // editCapabilities.setResourceOperations(Arrays.asList(ResourceOperationKind.Create, + // ResourceOperationKind.Delete, ResourceOperationKind.Rename)); + // TODO + // editCapabilities.setFailureHandling(FailureHandlingKind.Undo); + workspaceClientCapabilities.setWorkspaceEdit(editCapabilities); + return workspaceClientCapabilities; + } + + public static WindowClientCapabilities getWindowClientCapabilities() { + final var windowClientCapabilities = new WindowClientCapabilities(); + windowClientCapabilities.setShowDocument(new ShowDocumentCapabilities(true)); + windowClientCapabilities.setWorkDoneProgress(true); + windowClientCapabilities.setShowMessage(new WindowShowMessageRequestCapabilities()); + return windowClientCapabilities; + } + +} \ No newline at end of file diff --git a/src/main/java/com/redhat/devtools/intellij/quarkus/lsp4ij/lifecycle/LanguageServerLifecycleListener.java b/src/main/java/com/redhat/devtools/intellij/quarkus/lsp4ij/lifecycle/LanguageServerLifecycleListener.java new file mode 100644 index 000000000..e6195851f --- /dev/null +++ b/src/main/java/com/redhat/devtools/intellij/quarkus/lsp4ij/lifecycle/LanguageServerLifecycleListener.java @@ -0,0 +1,53 @@ +/******************************************************************************* + * Copyright (c) 2023 Red Hat Inc. and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 + * which is available at https://www.apache.org/licenses/LICENSE-2.0. + * + * SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 + * + * Contributors: + * Red Hat Inc. - initial API and implementation + *******************************************************************************/ +/******************************************************************************* + * Copyright (c) 2023 Red Hat Inc. and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 + * which is available at https://www.apache.org/licenses/LICENSE-2.0. + * + * SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 + * + * Contributors: + * Red Hat Inc. - initial API and implementation + *******************************************************************************/ +package com.redhat.devtools.intellij.quarkus.lsp4ij.lifecycle; + +import com.redhat.devtools.intellij.quarkus.lsp4ij.LanguageServerWrapper; +import org.eclipse.lsp4j.jsonrpc.messages.Message; + +/** + * Language server lifecycle listener + * + * @author Angelo ZERR + */ +public interface LanguageServerLifecycleListener { + + void handleStartingProcess(LanguageServerWrapper languageServer); + + void handleStartedProcess(LanguageServerWrapper languageServer, Throwable exception); + + void handleStartedLanguageServer(LanguageServerWrapper languageServer, Throwable exception); + + void handleLSPMessage(Message message, LanguageServerWrapper languageServer); + + void handleStoppingLanguageServer(LanguageServerWrapper languageServer); + + void handleStoppedLanguageServer(LanguageServerWrapper languageServer, Throwable exception); + + void dispose(); + +} diff --git a/src/main/java/com/redhat/devtools/intellij/quarkus/lsp4ij/lifecycle/LanguageServerLifecycleManager.java b/src/main/java/com/redhat/devtools/intellij/quarkus/lsp4ij/lifecycle/LanguageServerLifecycleManager.java new file mode 100644 index 000000000..d32884c3d --- /dev/null +++ b/src/main/java/com/redhat/devtools/intellij/quarkus/lsp4ij/lifecycle/LanguageServerLifecycleManager.java @@ -0,0 +1,145 @@ +/******************************************************************************* + * Copyright (c) 2023 Red Hat Inc. and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 + * which is available at https://www.apache.org/licenses/LICENSE-2.0. + * + * SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 + * + * Contributors: + * Red Hat Inc. - initial API and implementation + *******************************************************************************/ +package com.redhat.devtools.intellij.quarkus.lsp4ij.lifecycle; + +import com.intellij.openapi.components.ServiceManager; +import com.intellij.openapi.project.Project; +import com.redhat.devtools.intellij.quarkus.lsp4ij.LanguageServerWrapper; +import org.eclipse.lsp4j.jsonrpc.messages.Message; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Collection; +import java.util.concurrent.ConcurrentLinkedQueue; + +/** + * Language server lifecycle manager + */ +public class LanguageServerLifecycleManager { + + public static LanguageServerLifecycleManager getInstance(Project project) { + return ServiceManager.getService(project, LanguageServerLifecycleManager.class); + } + + private static final Logger LOGGER = LoggerFactory.getLogger(LanguageServerLifecycleManager.class);//$NON-NLS-1$ + + private final Collection listeners; + + private boolean disposed; + + public LanguageServerLifecycleManager() { + this(new ConcurrentLinkedQueue<>()); + } + + public LanguageServerLifecycleManager(Collection listeners) { + this.listeners = listeners; + } + + public void addLanguageServerLifecycleListener(LanguageServerLifecycleListener listener) { + this.listeners.add(listener); + } + + public void removeLanguageServerLifecycleListener(LanguageServerLifecycleListener listener) { + this.listeners.remove(listener); + } + + public void onStartingProcess(LanguageServerWrapper languageServer) { + if (isDisposed()) { + return; + } + for (LanguageServerLifecycleListener listener : this.listeners) { + try { + listener.handleStartingProcess(languageServer); + } catch (Exception e) { + LOGGER.error("Error while handling starting process of the language server '" + languageServer.serverDefinition.id + "'", e); + } + } + } + + public void onStartedProcess(LanguageServerWrapper languageServer, Exception exception) { + if (isDisposed()) { + return; + } + for (LanguageServerLifecycleListener listener : this.listeners) { + try { + listener.handleStartedProcess(languageServer, exception); + } catch (Exception e) { + LOGGER.error("Error while handling started process of the language server '" + languageServer.serverDefinition.id + "'", e); + } + } + } + + public void onStartedLanguageServer(LanguageServerWrapper languageServer, Throwable exception) { + if (isDisposed()) { + return; + } + for (LanguageServerLifecycleListener listener : this.listeners) { + try { + listener.handleStartedLanguageServer(languageServer, exception); + } catch (Exception e) { + LOGGER.error("Error while handling started the language server '" + languageServer.serverDefinition.id + "'", e); + } + } + } + + public void logLSPMessage(Message message, LanguageServerWrapper languageServer) { + if (isDisposed()) { + return; + } + for (LanguageServerLifecycleListener listener : this.listeners) { + try { + listener.handleLSPMessage(message, languageServer); + } catch (Exception e) { + LOGGER.error("Error while handling LSP message of the language server '" + languageServer.serverDefinition.id + "'", e); + } + } + } + + public void onStoppingLanguageServer(LanguageServerWrapper languageServer) { + if (isDisposed()) { + return; + } + for (LanguageServerLifecycleListener listener : this.listeners) { + try { + listener.handleStoppingLanguageServer(languageServer); + } catch (Exception e) { + LOGGER.error("Error while handling stopping the language server '" + languageServer.serverDefinition.id + "'", e); + } + } + + } + + public void onStoppedLanguageServer(LanguageServerWrapper languageServer, Exception exception) { + if (isDisposed()) { + return; + } + for (LanguageServerLifecycleListener listener : this.listeners) { + try { + listener.handleStoppedLanguageServer(languageServer, exception); + } catch (Exception e) { + LOGGER.error("Error while handling stopped the language server '" + languageServer.serverDefinition.id + "'", e); + } + } + } + + public boolean isDisposed() { + return disposed; + } + + public void dispose() { + disposed = true; + listeners.stream().forEach(LanguageServerLifecycleListener::dispose); + listeners.clear(); + } +} diff --git a/src/main/java/com/redhat/devtools/intellij/quarkus/lsp4ij/lifecycle/NullLanguageServerLifecycleManager.java b/src/main/java/com/redhat/devtools/intellij/quarkus/lsp4ij/lifecycle/NullLanguageServerLifecycleManager.java new file mode 100644 index 000000000..562c73ade --- /dev/null +++ b/src/main/java/com/redhat/devtools/intellij/quarkus/lsp4ij/lifecycle/NullLanguageServerLifecycleManager.java @@ -0,0 +1,28 @@ +/******************************************************************************* + * Copyright (c) 2023 Red Hat Inc. and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 + * which is available at https://www.apache.org/licenses/LICENSE-2.0. + * + * SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 + * + * Contributors: + * Red Hat Inc. - initial API and implementation + *******************************************************************************/ +package com.redhat.devtools.intellij.quarkus.lsp4ij.lifecycle; + +import java.util.Collections; + +/** + * Language server lifecycle manager which does nothing. + */ +public class NullLanguageServerLifecycleManager extends LanguageServerLifecycleManager { + + public static final LanguageServerLifecycleManager INSTANCE = new NullLanguageServerLifecycleManager(); + + private NullLanguageServerLifecycleManager() { + super(Collections.emptyList()); + } +} diff --git a/src/main/java/com/redhat/devtools/intellij/quarkus/lsp4ij/operations/codelens/LSPCodelensInlayProvider.java b/src/main/java/com/redhat/devtools/intellij/quarkus/lsp4ij/operations/codelens/LSPCodelensInlayProvider.java index 06bc534e1..5af6ebd4c 100644 --- a/src/main/java/com/redhat/devtools/intellij/quarkus/lsp4ij/operations/codelens/LSPCodelensInlayProvider.java +++ b/src/main/java/com/redhat/devtools/intellij/quarkus/lsp4ij/operations/codelens/LSPCodelensInlayProvider.java @@ -62,12 +62,17 @@ public InlayHintsCollector getCollectorFor(@NotNull PsiFile psiFile, @Override public boolean collect(@NotNull PsiElement psiElement, @NotNull Editor editor, @NotNull InlayHintsSink inlayHintsSink) { try { + Project project = psiElement.getProject(); + if (project.isDisposed()) { + // The project has been closed, don't collect code lenses. + return false; + } URI docURI = LSPIJUtils.toUri(editor.getDocument()); if (docURI != null) { CodeLensParams param = new CodeLensParams(new TextDocumentIdentifier(docURI.toString())); BlockingDeque> pairs = new LinkedBlockingDeque<>(); List>> codelenses = new ArrayList<>(); - CompletableFuture future = LanguageServiceAccessor.getInstance(psiElement.getProject()) + CompletableFuture future = LanguageServiceAccessor.getInstance(project) .getLanguageServers(editor.getDocument(), capabilities -> capabilities.getCodeLensProvider() != null) .thenComposeAsync(languageServers -> CompletableFuture.allOf(languageServers.stream() .map(languageServer -> languageServer.getTextDocumentService().codeLens(param) diff --git a/src/main/java/com/redhat/devtools/intellij/quarkus/lsp4ij/operations/diagnostics/LSPDiagnosticsForServer.java b/src/main/java/com/redhat/devtools/intellij/quarkus/lsp4ij/operations/diagnostics/LSPDiagnosticsForServer.java index e67c7e2fc..5118685d1 100644 --- a/src/main/java/com/redhat/devtools/intellij/quarkus/lsp4ij/operations/diagnostics/LSPDiagnosticsForServer.java +++ b/src/main/java/com/redhat/devtools/intellij/quarkus/lsp4ij/operations/diagnostics/LSPDiagnosticsForServer.java @@ -66,7 +66,7 @@ public LSPDiagnosticsForServer(LanguageServerWrapper languageServerWrapper, Virt } private static boolean isCodeActionSupported(LanguageServerWrapper languageServerWrapper) { - if (!languageServerWrapper.isActive()) { + if (!languageServerWrapper.isActive() || languageServerWrapper.isStopping()) { // This use-case comes from when a diagnostics is published and the language server is stopped // We cannot use here languageServerWrapper.getServerCapabilities() otherwise it will restart the language server. return false; diff --git a/src/main/java/com/redhat/devtools/intellij/quarkus/lsp4ij/server/JavaProcessCommandBuilder.java b/src/main/java/com/redhat/devtools/intellij/quarkus/lsp4ij/server/JavaProcessCommandBuilder.java index 5b5891736..c86ebdd28 100644 --- a/src/main/java/com/redhat/devtools/intellij/quarkus/lsp4ij/server/JavaProcessCommandBuilder.java +++ b/src/main/java/com/redhat/devtools/intellij/quarkus/lsp4ij/server/JavaProcessCommandBuilder.java @@ -10,7 +10,7 @@ ******************************************************************************/ package com.redhat.devtools.intellij.quarkus.lsp4ij.server; -import com.redhat.devtools.intellij.quarkus.lsp4ij.settings.LanguageServerSettingsState; +import com.redhat.devtools.intellij.quarkus.lsp4ij.settings.UserDefinedLanguageServerSettings; import java.io.File; import java.nio.file.Files; @@ -39,7 +39,7 @@ public class JavaProcessCommandBuilder { public JavaProcessCommandBuilder(String languageId) { this.languageId = languageId; setJavaPath(computeJavaPath()); - LanguageServerSettingsState.LanguageServerDefinitionSettings settings = LanguageServerSettingsState.getInstance().getLanguageServerSettings(languageId); + UserDefinedLanguageServerSettings.LanguageServerDefinitionSettings settings = UserDefinedLanguageServerSettings.getInstance().getLanguageServerSettings(languageId); if (settings != null) { setDebugPort(settings.getDebugPort()); setDebugSuspend(settings.isDebugSuspend()); diff --git a/src/main/java/com/redhat/devtools/intellij/quarkus/lsp4ij/settings/LanguageServerConfigurable.java b/src/main/java/com/redhat/devtools/intellij/quarkus/lsp4ij/settings/LanguageServerConfigurable.java index c018e431f..93fe8558f 100644 --- a/src/main/java/com/redhat/devtools/intellij/quarkus/lsp4ij/settings/LanguageServerConfigurable.java +++ b/src/main/java/com/redhat/devtools/intellij/quarkus/lsp4ij/settings/LanguageServerConfigurable.java @@ -70,31 +70,38 @@ public JComponent createOptionsPanel() { @Override public boolean isModified() { - LanguageServerSettingsState.LanguageServerDefinitionSettings settings = LanguageServerSettingsState.getInstance() + UserDefinedLanguageServerSettings.LanguageServerDefinitionSettings settings = UserDefinedLanguageServerSettings.getInstance() .getLanguageServerSettings(languageServerDefinition.id); if (settings == null) { return true; } return !(myView.getDebugPort().equals(settings.getDebugPort()) - && myView.isDebugSuspend() == settings.isDebugSuspend()); + && myView.isDebugSuspend() == settings.isDebugSuspend() + && myView.getServerTrace() == settings.getServerTrace()); } @Override public void apply() throws ConfigurationException { - LanguageServerSettingsState.LanguageServerDefinitionSettings settings = new LanguageServerSettingsState.LanguageServerDefinitionSettings(); + UserDefinedLanguageServerSettings.LanguageServerDefinitionSettings settings = new UserDefinedLanguageServerSettings.LanguageServerDefinitionSettings(); settings.setDebugPort(myView.getDebugPort()); settings.setDebugSuspend(myView.isDebugSuspend()); - LanguageServerSettingsState.getInstance().setLanguageServerSettings(languageServerDefinition.id, settings); + settings.setServerTrace(myView.getServerTrace()); + UserDefinedLanguageServerSettings.getInstance().setLanguageServerSettings(languageServerDefinition.id, settings); } @Override public void reset() { - LanguageServerSettingsState.LanguageServerDefinitionSettings settings = LanguageServerSettingsState.getInstance() + ServerTrace serverTrace = ServerTrace.off; + UserDefinedLanguageServerSettings.LanguageServerDefinitionSettings settings = UserDefinedLanguageServerSettings.getInstance() .getLanguageServerSettings(languageServerDefinition.id); if (settings != null) { myView.setDebugPort(settings.getDebugPort()); myView.setDebugSuspend(settings.isDebugSuspend()); + if (settings.getServerTrace() != null) { + serverTrace = settings.getServerTrace(); + } } + myView.setServerTrace(serverTrace); } @Override diff --git a/src/main/java/com/redhat/devtools/intellij/quarkus/lsp4ij/settings/LanguageServerView.java b/src/main/java/com/redhat/devtools/intellij/quarkus/lsp4ij/settings/LanguageServerView.java index 1ea528deb..bc2ec82cc 100644 --- a/src/main/java/com/redhat/devtools/intellij/quarkus/lsp4ij/settings/LanguageServerView.java +++ b/src/main/java/com/redhat/devtools/intellij/quarkus/lsp4ij/settings/LanguageServerView.java @@ -14,17 +14,15 @@ package com.redhat.devtools.intellij.quarkus.lsp4ij.settings; import com.intellij.openapi.Disposable; +import com.intellij.openapi.ui.ComboBox; import com.intellij.ui.components.JBCheckBox; -import com.intellij.ui.components.JBLabel; import com.intellij.ui.components.JBTextField; import com.intellij.util.ui.FormBuilder; -import com.intellij.util.ui.JBUI; import com.intellij.util.ui.UI; import com.redhat.devtools.intellij.quarkus.lsp4ij.LanguageServerBundle; import com.redhat.devtools.intellij.quarkus.lsp4ij.LanguageServersRegistry; import javax.swing.*; -import java.awt.*; /** * UI settings view to configure a given language server: @@ -41,12 +39,15 @@ public class LanguageServerView implements Disposable { private JBCheckBox debugSuspendCheckBox = new JBCheckBox(LanguageServerBundle.message("language.server.debug.suspend")); + private ComboBox serverTraceComboBox = new ComboBox<>(new DefaultComboBoxModel<>(ServerTrace.values())); + public LanguageServerView(LanguageServersRegistry.LanguageServerDefinition languageServerDefinition) { this.myMainPanel = FormBuilder.createFormBuilder() .setFormLeftIndent(10) .addComponent(createTitleComponent(languageServerDefinition), 1) .addLabeledComponent(LanguageServerBundle.message("language.server.debug.port"), debugPortField, 1) .addComponent(debugSuspendCheckBox, 1) + .addLabeledComponent(LanguageServerBundle.message("language.server.trace"), serverTraceComboBox, 1) .addComponentFillVertically(new JPanel(), 0) .getPanel(); } @@ -83,6 +84,14 @@ public void setDebugSuspend(boolean debugSuspend) { debugSuspendCheckBox.setSelected(debugSuspend); } + public ServerTrace getServerTrace() { + return (ServerTrace) serverTraceComboBox.getSelectedItem(); + } + + public void setServerTrace(ServerTrace serverTrace) { + serverTraceComboBox.setSelectedItem(serverTrace); + } + @Override public void dispose() { diff --git a/src/main/java/com/redhat/devtools/intellij/quarkus/lsp4ij/settings/ServerTrace.java b/src/main/java/com/redhat/devtools/intellij/quarkus/lsp4ij/settings/ServerTrace.java new file mode 100644 index 000000000..e1d1d9f74 --- /dev/null +++ b/src/main/java/com/redhat/devtools/intellij/quarkus/lsp4ij/settings/ServerTrace.java @@ -0,0 +1,36 @@ +/******************************************************************************* + * Copyright (c) 2023 Red Hat Inc. and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 + * which is available at https://www.apache.org/licenses/LICENSE-2.0. + * + * SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 + * + * Contributors: + * Red Hat Inc. - initial API and implementation + *******************************************************************************/ +package com.redhat.devtools.intellij.quarkus.lsp4ij.settings; + +/** + * Language server trace level. + */ +public enum ServerTrace { + + off, + messages, + verbose; + + public static ServerTrace getServerTrace(String serverTrace) { + if (serverTrace == null || serverTrace.isEmpty()) { + return ServerTrace.off; + } + try { + return ServerTrace.valueOf(serverTrace); + } + catch(Exception e) { + return ServerTrace.off; + } + } +} diff --git a/src/main/java/com/redhat/devtools/intellij/quarkus/lsp4ij/settings/LanguageServerSettingsState.java b/src/main/java/com/redhat/devtools/intellij/quarkus/lsp4ij/settings/UserDefinedLanguageServerSettings.java similarity index 58% rename from src/main/java/com/redhat/devtools/intellij/quarkus/lsp4ij/settings/LanguageServerSettingsState.java rename to src/main/java/com/redhat/devtools/intellij/quarkus/lsp4ij/settings/UserDefinedLanguageServerSettings.java index b91f23cf9..eb49b6221 100644 --- a/src/main/java/com/redhat/devtools/intellij/quarkus/lsp4ij/settings/LanguageServerSettingsState.java +++ b/src/main/java/com/redhat/devtools/intellij/quarkus/lsp4ij/settings/UserDefinedLanguageServerSettings.java @@ -17,50 +17,52 @@ import com.intellij.openapi.components.ServiceManager; import com.intellij.openapi.components.State; import com.intellij.openapi.components.Storage; -import com.intellij.util.xmlb.XmlSerializerUtil; +import com.intellij.util.xmlb.annotations.Tag; +import com.intellij.util.xmlb.annotations.XCollection; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import java.util.HashMap; import java.util.Map; +import java.util.TreeMap; /** - * Settings for a given Language server definition + * User defined language server settings for a given Language server definition * *

    *
  • Debug port
  • *
  • Suspend and wait for a debugger
  • + *
  • Trace LSP requests/responses/notifications
  • *
*/ @State( - name = "com.redhat.devtools.intellij.lsp4ij.settings.LanguageServerSettingsState", + name = "LanguageServerSettingsState", storages = {@Storage("LanguageServersSettings.xml")} ) -public class LanguageServerSettingsState implements PersistentStateComponent { +public class UserDefinedLanguageServerSettings implements PersistentStateComponent { - private Map languageServers = new HashMap<>(); + public volatile MyState myState = new MyState(); - public static LanguageServerSettingsState getInstance() { - return ServiceManager.getService(LanguageServerSettingsState.class); + public static UserDefinedLanguageServerSettings getInstance() { + return ServiceManager.getService(UserDefinedLanguageServerSettings.class); } @Nullable @Override - public LanguageServerSettingsState getState() { - return this; + public MyState getState() { + return myState; } @Override - public void loadState(@NotNull LanguageServerSettingsState state) { - XmlSerializerUtil.copyBean(state, this); + public void loadState(@NotNull MyState state) { + myState = state; } public LanguageServerDefinitionSettings getLanguageServerSettings(String languageSeverId) { - return languageServers.get(languageSeverId); + return myState.myState.get(languageSeverId); } public void setLanguageServerSettings(String languageSeverId, LanguageServerDefinitionSettings settings) { - languageServers.put(languageSeverId, settings); + myState.myState.put(languageSeverId, settings); } public static class LanguageServerDefinitionSettings { @@ -69,6 +71,8 @@ public static class LanguageServerDefinitionSettings { private boolean debugSuspend; + private ServerTrace serverTrace; + public String getDebugPort() { return debugPort; } @@ -84,7 +88,24 @@ public boolean isDebugSuspend() { public void setDebugSuspend(boolean debugSuspend) { this.debugSuspend = debugSuspend; } + + public ServerTrace getServerTrace() { + return serverTrace; + } + + public void setServerTrace(ServerTrace serverTrace) { + this.serverTrace = serverTrace; + } } + static class MyState { + @Tag("state") + @XCollection + public Map myState = new TreeMap<>(); + + MyState() { + } + + } } diff --git a/src/main/java/com/redhat/devtools/intellij/qute/lsp/QuteLanguageClient.java b/src/main/java/com/redhat/devtools/intellij/qute/lsp/QuteLanguageClient.java index ab1386bb0..8816b26c1 100644 --- a/src/main/java/com/redhat/devtools/intellij/qute/lsp/QuteLanguageClient.java +++ b/src/main/java/com/redhat/devtools/intellij/qute/lsp/QuteLanguageClient.java @@ -69,6 +69,11 @@ public QuteLanguageClient(Project project) { QuarkusProjectService.getInstance(project); } + @Override + public void dispose() { + connection.disconnect(); + } + private void sendPropertiesChangeEvent(Set uris) { QuteLanguageServerAPI server = (QuteLanguageServerAPI) getLanguageServer(); if (server != null) { diff --git a/src/main/resources/META-INF/lsp.xml b/src/main/resources/META-INF/lsp.xml index 6b969fdb6..82909a057 100644 --- a/src/main/resources/META-INF/lsp.xml +++ b/src/main/resources/META-INF/lsp.xml @@ -2,14 +2,19 @@ + - + + @@ -31,6 +36,7 @@ + + + + + - @@ -88,10 +97,9 @@ implementationClass="com.redhat.devtools.intellij.quarkus.lsp4ij.operations.diagnostics.LSPDiagnosticAnnotator"/> + + + - - - com.redhat.devtools.intellij.quarkus.lsp4ij.ConnectDocumentToLanguageServerSetupParticipant - - diff --git a/src/main/resources/messages/LanguageServerBundle.properties b/src/main/resources/messages/LanguageServerBundle.properties index 1dc66547f..34ff30689 100644 --- a/src/main/resources/messages/LanguageServerBundle.properties +++ b/src/main/resources/messages/LanguageServerBundle.properties @@ -1,3 +1,21 @@ +############################################################################### +# Copyright (c) 2023 Red Hat Inc. and others. +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Eclipse Public License v2.0 +# which accompanies this distribution, and is available at +# http://www.eclipse.org/legal/epl-v20.html +# +# SPDX-License-Identifier: EPL-2.0 +# +# Contributors: +# Red Hat Inc. - initial API and implementation +############################################################################### + +## Language Servers UI settings page language.servers=Language Servers language.server.debug.port=Debug port: language.server.debug.suspend=Suspend and wait for a debugger? +language.server.trace=Trace: + +## LSP console +lsp.console.title=LSP Console diff --git a/src/main/resources/microprofile_icon_rgb_16px_default.png b/src/main/resources/microprofile_icon_rgb_16px_default.png new file mode 100644 index 0000000000000000000000000000000000000000..64a24ffc6cd526936648c69408945f698a1ddd92 GIT binary patch literal 460 zcmV;-0Wnp*{=2f?+)$%4;QQ66XapygUJbS zGx5ea^M^|bv4|n-LGePgGk+W-BO{nCBg&@esL$;HlbfC=HRM0OwsmW;q5)ZfM z3&`>nOvW&vC`c%~Cr!K!S!{KU%!)UwHD2CdsCpY&{OnYvWBCC>nf%;LJSJvY~1D2*7+R0ji}8vy|T0RR8w!gr}PH0*Q$0000