From 571c668e810b5da637a217d1c49ce0a7c1bcae4c 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 | 10 + .../lang/MicroProfileServerIconProvider.java | 13 + .../quarkus/lang/QuarkusIconProvider.java | 2 +- .../lang/QuarkusServerIconProvider.java | 14 + .../quarkus/lsp/QuarkusLanguageClient.java | 5 + ...umentToLanguageServerSetupParticipant.java | 45 +- .../lsp4ij/DocumentContentSynchronizer.java | 3 +- .../intellij/quarkus/lsp4ij/LSPIJUtils.java | 2 +- .../quarkus/lsp4ij/LanguageClientImpl.java | 3 + .../LanguageServerIconProviderDefinition.java | 16 + .../quarkus/lsp4ij/LanguageServerWrapper.java | 587 ++++++++++-------- .../lsp4ij/LanguageServersRegistry.java | 28 +- .../lsp4ij/LanguageServiceAccessor.java | 4 + .../LoggingStreamConnectionProviderProxy.java | 14 +- .../lsp4ij/ServerExtensionPointBean.java | 3 + .../quarkus/lsp4ij/ServerIconProvider.java | 8 + .../ServerIconProviderExtensionPointBean.java | 25 + .../console/LSPConsoleToolWindowFactory.java | 42 ++ .../console/LSPConsoleToolWindowPanel.java | 154 +++++ .../lsp4ij/console/SimpleCardLayoutPanel.java | 88 +++ .../explorer/LanguageServerExplorer.java | 131 ++++ ...nguageServerExplorerLifecycleListener.java | 209 +++++++ .../LanguageServerProcessTreeNode.java | 83 +++ .../explorer/LanguageServerTreeNode.java | 38 ++ .../explorer/LanguageServerTreeRenderer.java | 121 ++++ .../lsp4ij/console/explorer/ServerStatus.java | 11 + .../lsp4ij/internal/SupportedFeatures.java | 160 +++++ .../LanguageServerLifecycleListener.java | 22 + .../LanguageServerLifecycleManager.java | 129 ++++ .../NullLanguageServerLifecycleManager.java | 12 + .../codelens/LSPCodelensInlayProvider.java | 7 +- .../server/JavaProcessCommandBuilder.java | 4 +- .../settings/LanguageServerConfigurable.java | 17 +- .../lsp4ij/settings/LanguageServerView.java | 15 +- .../quarkus/lsp4ij/settings/ServerTrace.java | 36 ++ ...=> UserDefinedLanguageServerSettings.java} | 49 +- .../lsp4ij/utils/IDEAContentFactory.java | 28 + .../quarkus/lsp4ij/utils/UIHelper.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 44 files changed, 1918 insertions(+), 333 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 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/java/com/redhat/devtools/intellij/quarkus/lsp4ij/utils/IDEAContentFactory.java create mode 100644 src/main/java/com/redhat/devtools/intellij/quarkus/lsp4ij/utils/UIHelper.java 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..2b594410c --- /dev/null +++ b/src/main/java/com/redhat/devtools/intellij/microprofile/lang/MicroProfileIcons.java @@ -0,0 +1,10 @@ +package com.redhat.devtools.intellij.microprofile.lang; + +import com.intellij.openapi.util.IconLoader; + +import javax.swing.*; + +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..c6f542a5a --- /dev/null +++ b/src/main/java/com/redhat/devtools/intellij/microprofile/lang/MicroProfileServerIconProvider.java @@ -0,0 +1,13 @@ +package com.redhat.devtools.intellij.microprofile.lang; + +import com.redhat.devtools.intellij.quarkus.lsp4ij.ServerIconProvider; + +import javax.swing.*; + +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..712f5f9c3 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 @@ -21,7 +21,7 @@ import javax.swing.Icon; 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..1dd893409 --- /dev/null +++ b/src/main/java/com/redhat/devtools/intellij/quarkus/lang/QuarkusServerIconProvider.java @@ -0,0 +1,14 @@ +package com.redhat.devtools.intellij.quarkus.lang; + +import com.redhat.devtools.intellij.microprofile.lang.MicroProfileIcons; +import com.redhat.devtools.intellij.quarkus.lsp4ij.ServerIconProvider; + +import javax.swing.*; + +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..b0fc28ce0 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 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..1264159c7 --- /dev/null +++ b/src/main/java/com/redhat/devtools/intellij/quarkus/lsp4ij/LanguageServerIconProviderDefinition.java @@ -0,0 +1,16 @@ +package com.redhat.devtools.intellij.quarkus.lsp4ij; + +import javax.swing.*; + +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..7771d00cf 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,22 @@ 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 org.eclipse.lsp4j.*; 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 +41,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 +71,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 +103,26 @@ public void beforeDocumentSaving(@NotNull Document document) { protected Map connectedDocuments; @Nullable protected final URI initialPath; + protected final InitializeParams initParams = new InitializeParams(); protected StreamConnectionProvider lspStreamProvider; private Future launcherFuture; private CompletableFuture initializeFuture; private LanguageServer languageServer; + private LanguageClientImpl languageClient; private ServerCapabilities serverCapabilities; + private Timer timer; + private AtomicBoolean stopping = new AtomicBoolean(false); + + private final ExecutorService dispatcher; + + private final ExecutorService listener; /** * Map containing unregistration handlers for dynamic capability registrations. */ - private @Nonnull Map dynamicRegistrations = new HashMap<>(); + private @Nonnull + Map dynamicRegistrations = new HashMap<>(); private boolean initiallySupportsWorkspaceFolders = false; /* Backwards compatible constructor */ @@ -163,7 +134,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 +144,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 +202,163 @@ 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 = LoggingStreamConnectionProviderProxy.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); + } + } catch (IOException e) { + // End process with error + if (shouldLog) { + getLanguageServerLifecycleManager().onStartedProcess(this, e); } - initParams.setRootUri(LSPIJUtils.toUri(projectDirectory).toString()); - } else { - initParams.setRootUri(LSPIJUtils.toUri(new File("/")).toString()); //$NON-NLS-1$ + 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); + 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()); + } + + UnaryOperator wrapper = consumer -> (message -> { + if (shouldLog) { + logMessage(message); + } + consumer.consume(message); + 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); } - } catch (Exception e) { - LOGGER.warn(e.getLocalizedMessage(), e); + 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); } - })); - - 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); - } - for (Map.Entry fileToReconnect : toReconnect.entrySet()) { - try { - connect(fileToReconnect.getKey(), fileToReconnect.getValue()); - } catch (IOException e) { - LOGGER.warn(e.getLocalizedMessage(), e); - } - } - }); - 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(); + }).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 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 +377,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 +387,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 */ @@ -387,6 +420,15 @@ public boolean isActive() { } 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 +440,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 +465,10 @@ synchronized void stop() { if (provider != null) { provider.stop(); } + this.stopping.set(false); + + getLanguageServerLifecycleManager().onStoppedLanguageServer(this, null); + }; CompletableFuture.runAsync(shutdownKillAndStopFutureAndProvider); @@ -431,6 +480,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 +489,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 +566,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 +605,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 +629,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 +682,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 +697,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 +765,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 +803,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 +843,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 +872,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 +904,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 +961,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..5bf1ee9fa 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 : 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; 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; } @@ -131,6 +141,8 @@ public static LanguageServersRegistry getInstance() { private 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 index d0299b4bf..48d914551 100644 --- a/src/main/java/com/redhat/devtools/intellij/quarkus/lsp4ij/LoggingStreamConnectionProviderProxy.java +++ b/src/main/java/com/redhat/devtools/intellij/quarkus/lsp4ij/LoggingStreamConnectionProviderProxy.java @@ -1,6 +1,8 @@ package com.redhat.devtools.intellij.quarkus.lsp4ij; import com.redhat.devtools.intellij.quarkus.lsp4ij.server.StreamConnectionProvider; +import com.redhat.devtools.intellij.quarkus.lsp4ij.settings.UserDefinedLanguageServerSettings; +import com.redhat.devtools.intellij.quarkus.lsp4ij.settings.ServerTrace; import org.eclipse.lsp4j.jsonrpc.messages.Message; import org.eclipse.lsp4j.services.LanguageServer; import org.slf4j.Logger; @@ -27,8 +29,8 @@ public class LoggingStreamConnectionProviderProxy implements StreamConnectionPro private InputStream errorStream; private final String id; private File logFile; - private boolean logToFile = true; - private boolean logToConsole = false; + private boolean logToFile = false; + private boolean logToConsole = true; /** @@ -38,7 +40,12 @@ public class LoggingStreamConnectionProviderProxy implements StreamConnectionPro * @return If connections should be logged */ public static boolean shouldLog(String serverId) { - return Boolean.getBoolean("com.redhat.devtools.intellij.quarkus.trace"); + UserDefinedLanguageServerSettings.LanguageServerDefinitionSettings settings = UserDefinedLanguageServerSettings.getInstance().getLanguageServerSettings(serverId); + if (settings == null) { + return false; + } + ServerTrace serverTrace = settings.getServerTrace(); + return serverTrace != null && serverTrace != ServerTrace.off; } public LoggingStreamConnectionProviderProxy(StreamConnectionProvider provider, String serverId) { @@ -165,6 +172,7 @@ public void handleMessage(Message message, LanguageServer languageServer, URI ro } private void logToConsole(String string) { + // LSPConsoleToolWindow System.out.println(string); } 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..ca0ae0d66 --- /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..f27ff1db8 --- /dev/null +++ b/src/main/java/com/redhat/devtools/intellij/quarkus/lsp4ij/console/LSPConsoleToolWindowPanel.java @@ -0,0 +1,154 @@ +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.*; + +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); + } + + public void showEmptyConsole() { + consoles.showEmptyContent(); + } + + /** + * 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(); + } + } + + public void showEmptyContent() { + } + } + + private class ConsoleOrErrorPanel extends SimpleCardLayoutPanel { + + private static final String NAME_VIEW_CONSOLE = "console"; + private static final String NAME_VIEW_EMPTY = "empty"; + + private final ConsoleView consoleView; + + public ConsoleOrErrorPanel() { + consoleView = createConsoleView(project); + if (consoleView != null) { + add(consoleView.getComponent(), NAME_VIEW_CONSOLE); + } + add(new JPanel(), NAME_VIEW_EMPTY); + showConsole(); + } + + private void showConsole() { + show(NAME_VIEW_CONSOLE); + } + + public void showEmptyContent() { + show(NAME_VIEW_EMPTY); + } + + 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..5cc86ea09 --- /dev/null +++ b/src/main/java/com/redhat/devtools/intellij/quarkus/lsp4ij/console/SimpleCardLayoutPanel.java @@ -0,0 +1,88 @@ +/******************************************************************************* + * Copyright (c) 2022 Red Hat, Inc. + * Distributed under license by Red Hat, Inc. All rights reserved. + * This program is 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 + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + * based on com.intellij.ui.CardLayoutPanel + ******************************************************************************/ + +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..185e72ba0 --- /dev/null +++ b/src/main/java/com/redhat/devtools/intellij/quarkus/lsp4ij/console/explorer/LanguageServerExplorer.java @@ -0,0 +1,131 @@ +/****************************************************************************** + * Copyright (c) 2023 Red Hat, Inc. + * Distributed under license by Red Hat, Inc. All rights reserved. + * This program is 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 + * + * 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 LanguageServerLifecycleListener 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); + } + + + private void showEmptyConsole() { + panel.showEmptyConsole(); + } + + /** + * 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); + } else { + showEmptyConsole(); + } + }); + + 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(); + } + + 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..be030cf0e --- /dev/null +++ b/src/main/java/com/redhat/devtools/intellij/quarkus/lsp4ij/console/explorer/LanguageServerExplorerLifecycleListener.java @@ -0,0 +1,209 @@ +package com.redhat.devtools.intellij.quarkus.lsp4ij.console.explorer; + +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 javax.swing.tree.TreePath; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import static com.redhat.devtools.intellij.quarkus.lsp4ij.utils.UIHelper.executeInUI; + +public class LanguageServerExplorerLifecycleListener implements LanguageServerLifecycleListener { + + 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 methods = new ConcurrentHashMap<>(10); + + private final LanguageServerExplorer explorer; + + public LanguageServerExplorerLifecycleListener(LanguageServerExplorer explorer) { + this.explorer = explorer; + } + + @Override + public void handleStartingProcess(LanguageServerWrapper languageServer) { + executeInUI(() -> { + if (isDisposed()) { + return; + } + LanguageServerProcessTreeNode processTreeNode = findLanguageServerProcessTreeNode(languageServer); + processTreeNode.setServerStatus(ServerStatus.startingProcess); + explorer.selectAndExpand(processTreeNode); + }); + } + + @Override + public void handleStartedProcess(LanguageServerWrapper languageServer, Throwable exception) { + executeInUI(() -> { + if (isDisposed()) { + return; + } + LanguageServerProcessTreeNode processTreeNode = findLanguageServerProcessTreeNode(languageServer); + processTreeNode.setServerStatus(ServerStatus.startedProcess); + }); + } + + @Override + public void handleStartedLanguageServer(LanguageServerWrapper languageServer, Throwable exception) { + executeInUI(() -> { + if (isDisposed()) { + return; + } + LanguageServerProcessTreeNode processTreeNode = findLanguageServerProcessTreeNode(languageServer); + processTreeNode.setServerStatus(ServerStatus.started); + }); + } + + @Override + public void handleLSPMessage(Message message, LanguageServerWrapper languageServer) { + if (isDisposed()) { + return; + } + ServerTrace serverTrace = getServerTrace(languageServer.serverDefinition.id); + if (serverTrace == ServerTrace.off) { + return; + } + + StringBuilder formattedMessage = new StringBuilder("[Trace]"); + if (message instanceof RequestMessage) { + // [Trace - 12:27:33 AM] Sending request 'initialize - (0)'. + // Params: { + String id = ((RequestMessage) message).getId(); + String method = ((RequestMessage) message).getMethod(); + methods.put(id, new LSPRequestInfo(method, System.currentTimeMillis())); + 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 = methods.remove(id); + 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'. + // Params: {} + formattedMessage.append(" Sending notification '" + ((NotificationMessage) message).getMethod() + "'."); + } + if (serverTrace == ServerTrace.verbose) { + formattedMessage.append("\n"); + formattedMessage.append(message.toString()); + } + formattedMessage.append("\n"); + formattedMessage.append("\n"); + + executeInUI(() -> { + showMessage(languageServer, formattedMessage.toString()); + }); + } + + @Override + public void handleStoppingLanguageServer(LanguageServerWrapper languageServer) { + executeInUI(() -> { + if (isDisposed()) { + return; + } + LanguageServerProcessTreeNode processTreeNode = findLanguageServerProcessTreeNode(languageServer); + processTreeNode.setServerStatus(ServerStatus.stopping); + }); + } + + @Override + public void handleStoppedLanguageServer(LanguageServerWrapper languageServer, Throwable exception) { + executeInUI(() -> { + if (isDisposed()) { + return; + } + LanguageServerProcessTreeNode processTreeNode = findLanguageServerProcessTreeNode(languageServer); + 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); + 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 (isDisposed()) { + return; + } + LanguageServerProcessTreeNode processTreeNode = findLanguageServerProcessTreeNode(languageServer); + if (processTreeNode.getServerStatus() == null) { + processTreeNode.setServerStatus(ServerStatus.started); + explorer.selectAndExpand(processTreeNode); + } + explorer.showMessage(processTreeNode, message); + } + + private boolean isDisposed() { + return disposed || explorer.isDisposed(); + } + + @Override + public void dispose() { + disposed = true; + methods.clear(); + } +} 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..cf30cabde --- /dev/null +++ b/src/main/java/com/redhat/devtools/intellij/quarkus/lsp4ij/console/explorer/LanguageServerProcessTreeNode.java @@ -0,0 +1,83 @@ +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; + +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..d896c4611 --- /dev/null +++ b/src/main/java/com/redhat/devtools/intellij/quarkus/lsp4ij/console/explorer/LanguageServerTreeNode.java @@ -0,0 +1,38 @@ +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; + +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..fb8ae7fa6 --- /dev/null +++ b/src/main/java/com/redhat/devtools/intellij/quarkus/lsp4ij/console/explorer/LanguageServerTreeRenderer.java @@ -0,0 +1,121 @@ +package com.redhat.devtools.intellij.quarkus.lsp4ij.console.explorer; + + +// Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. + +import com.intellij.icons.AllIcons; +import com.intellij.ide.ui.UISettings; +import com.intellij.ide.util.treeView.NodeDescriptor; +import com.intellij.ui.*; +import com.intellij.ui.render.RenderingUtil; +import com.intellij.ui.scale.JBUIScale; +import com.intellij.util.ui.EmptyIcon; +import com.intellij.util.ui.UIUtil; +import com.redhat.devtools.intellij.quarkus.lsp4ij.LanguageServersRegistry; +import com.redhat.devtools.intellij.quarkus.lsp4ij.console.explorer.LanguageServerProcessTreeNode; +import com.redhat.devtools.intellij.quarkus.lsp4ij.console.explorer.LanguageServerTreeNode; +import org.jetbrains.annotations.NonNls; +import org.jetbrains.annotations.NotNull; + +import javax.swing.*; +import javax.swing.tree.DefaultMutableTreeNode; +import java.awt.*; + +public class LanguageServerTreeRenderer extends ColoredTreeCellRenderer { + + private static final Icon LOADING_NODE_ICON = JBUIScale.scaleIcon(EmptyIcon.create(8, 16)); + + @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) { + LanguageServerTreeNode languageServerTreeNode = (LanguageServerTreeNode) value; + setIcon(languageServerTreeNode.getIcon()); + append(languageServerTreeNode.getDisplayName()); + return; + } + + if (value instanceof LanguageServerProcessTreeNode) { + LanguageServerProcessTreeNode languageProcessTreeNode = (LanguageServerProcessTreeNode) value; + setIcon(languageProcessTreeNode.getIcon()); + append(languageProcessTreeNode.getDisplayName()); + + if (languageProcessTreeNode.getServerStatus() != ServerStatus.started && languageProcessTreeNode.getServerStatus() != ServerStatus.stopped) { + + // if (value instanceof LoadingNode) { + + /*} + else { + setForeground(RenderingUtil.getForeground(tree)); + setIcon(null); + }*/ + + + myDurationText = languageProcessTreeNode.getElapsedTime(); //testProxy.getDurationString(myConsoleProperties); + 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..690ef80eb --- /dev/null +++ b/src/main/java/com/redhat/devtools/intellij/quarkus/lsp4ij/console/explorer/ServerStatus.java @@ -0,0 +1,11 @@ +package com.redhat.devtools.intellij.quarkus.lsp4ij.console.explorer; + +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..59729690a --- /dev/null +++ b/src/main/java/com/redhat/devtools/intellij/quarkus/lsp4ij/lifecycle/LanguageServerLifecycleListener.java @@ -0,0 +1,22 @@ +package com.redhat.devtools.intellij.quarkus.lsp4ij.lifecycle; + +import com.redhat.devtools.intellij.quarkus.lsp4ij.LanguageServerWrapper; +import org.eclipse.lsp4j.jsonrpc.messages.Message; + +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..61b482fd0 --- /dev/null +++ b/src/main/java/com/redhat/devtools/intellij/quarkus/lsp4ij/lifecycle/LanguageServerLifecycleManager.java @@ -0,0 +1,129 @@ +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; + +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..437813c79 --- /dev/null +++ b/src/main/java/com/redhat/devtools/intellij/quarkus/lsp4ij/lifecycle/NullLanguageServerLifecycleManager.java @@ -0,0 +1,12 @@ +package com.redhat.devtools.intellij.quarkus.lsp4ij.lifecycle; + +import java.util.Collections; + +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/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/quarkus/lsp4ij/utils/IDEAContentFactory.java b/src/main/java/com/redhat/devtools/intellij/quarkus/lsp4ij/utils/IDEAContentFactory.java new file mode 100644 index 000000000..3adbac992 --- /dev/null +++ b/src/main/java/com/redhat/devtools/intellij/quarkus/lsp4ij/utils/IDEAContentFactory.java @@ -0,0 +1,28 @@ +/******************************************************************************* + * Copyright (c) 2023 Red Hat, Inc. + * Distributed under license by Red Hat, Inc. All rights reserved. + * This program is 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 + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + ******************************************************************************/ +package com.redhat.devtools.intellij.quarkus.lsp4ij.utils; + +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.ui.content.ContentFactory; + +/** + * This class is a copy/paste of https://github.com/redhat-developer/intellij-common/blob/main/src/main/java/com/redhat/devtools/intellij/common/utils/IDEAContentFactory.java + */ +public class IDEAContentFactory { + + public static final ContentFactory getInstance() { + // IC-2022.1 introduced ContentFactory.getInstance(), deprecated ContentFactory.SERVICE.getInstance() + // this helper can be removed once we support IC-2022.1 as the lowest version + // and replaced by ContentFactory.getInstance() + return ApplicationManager.getApplication().getService(ContentFactory.class); + } + +} \ No newline at end of file diff --git a/src/main/java/com/redhat/devtools/intellij/quarkus/lsp4ij/utils/UIHelper.java b/src/main/java/com/redhat/devtools/intellij/quarkus/lsp4ij/utils/UIHelper.java new file mode 100644 index 000000000..c6492a26d --- /dev/null +++ b/src/main/java/com/redhat/devtools/intellij/quarkus/lsp4ij/utils/UIHelper.java @@ -0,0 +1,49 @@ +/******************************************************************************* + * Copyright (c) 2019 Red Hat, Inc. + * Distributed under license by Red Hat, Inc. All rights reserved. + * This program is 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 + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + ******************************************************************************/ +package com.redhat.devtools.intellij.quarkus.lsp4ij.utils; + +import com.intellij.ide.ui.LafManager; +import com.intellij.ide.ui.laf.darcula.DarculaLaf; +import com.intellij.openapi.application.ApplicationManager; + +import javax.swing.UIManager; +import java.util.function.Supplier; + +/** + * This class is a copy/paste of https://github.com/redhat-developer/intellij-common/blob/main/src/main/java/com/redhat/devtools/intellij/common/utils/UIHelper.java + */ +public class UIHelper { + public static void executeInUI(Runnable runnable) { + if (ApplicationManager.getApplication().isDispatchThread()) { + runnable.run(); + } else { + ApplicationManager.getApplication().invokeLater(runnable); + } + } + + public static T executeInUI(Supplier supplier) { + if (!ApplicationManager.getApplication().isActive()) { + return null; + } + if (ApplicationManager.getApplication().isDispatchThread()) { + return supplier.get(); + } else { + final Object[] val = new Object[1]; + ApplicationManager.getApplication().invokeAndWait(() -> val[0] = supplier.get()); + return (T) val[0]; + } + } + + public static boolean isDarkMode() { + UIManager.LookAndFeelInfo lafInfo = LafManager.getInstance().getCurrentLookAndFeel(); + return lafInfo != null && lafInfo.getClassName().equals(DarculaLaf.class.getName()); + } +} \ No newline at end of file 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