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 faa74e547..ac3a35a84 100644 --- a/src/main/java/com/redhat/devtools/intellij/lsp4mp4ij/psi/internal/core/ls/PsiUtilsLSImpl.java +++ b/src/main/java/com/redhat/devtools/intellij/lsp4mp4ij/psi/internal/core/ls/PsiUtilsLSImpl.java @@ -84,7 +84,7 @@ public IPsiUtils refine(Module module) { @Override public Module getModule(VirtualFile file) { - if (file != null) { + if (file != null && !project.isDisposed()) { return ProjectFileIndex.getInstance(project).getModuleForFile(file, false); } return null; @@ -93,7 +93,7 @@ public Module getModule(VirtualFile file) { @Override public Module getModule(String uri) throws IOException { VirtualFile file = findFile(uri); - return file!=null?getModule(file):null; + return file != null ? getModule(file) : null; } @Override @@ -173,7 +173,7 @@ public int toOffset(Document document, int line, int character) { @Override public int toOffset(PsiFile file, int line, int character) { Document document = PsiDocumentManager.getInstance(file.getProject()).getDocument(file); - return document!=null?toOffset(document, line, character):0; + return document != null ? toOffset(document, line, character) : 0; } @Override @@ -181,7 +181,11 @@ public PsiFile resolveCompilationUnit(String uri) { try { VirtualFile file = findFile(uri); if (file != null) { - return PsiManager.getInstance(getModule(file).getProject()).findFile(file); + Module module = getModule(file); + if (module == null) { + return null; + } + return PsiManager.getInstance(module.getProject()).findFile(file); } } catch (IOException e) { LOGGER.error(e.getLocalizedMessage(), e); @@ -204,7 +208,7 @@ public static ClasspathKind getClasspathKind(VirtualFile file, Module module) { } public static String getProjectURI(Module module) { - return module != null?module.getModuleFilePath():null; + return module != null ? module.getModuleFilePath() : null; } @Override @@ -216,4 +220,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/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..e83d58ca0 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 @@ -18,20 +18,13 @@ import com.intellij.openapi.project.Project; import com.intellij.openapi.vfs.VirtualFile; 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); - - private Project project; + private final Project project; public ConnectDocumentToLanguageServerSetupParticipant(Project project) { this.project = project; @@ -42,6 +35,11 @@ public void projectOpened() { project.getMessageBus().connect(project).subscribe(FileEditorManagerListener.FILE_EDITOR_MANAGER, this); } + @Override + public void projectClosed() { + LanguageServiceAccessor.getInstance(project).shutdownAllDispatchers(); + } + @Override public void fileOpened(@NotNull FileEditorManager source, @NotNull VirtualFile file) { Document document = FileDocumentManager.getInstance().getDocument(file); @@ -52,26 +50,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 13f4882dc..91d974217 100644 --- a/src/main/java/com/redhat/devtools/intellij/quarkus/lsp4ij/LSPIJUtils.java +++ b/src/main/java/com/redhat/devtools/intellij/quarkus/lsp4ij/LSPIJUtils.java @@ -65,7 +65,7 @@ private static T toTextDocumentPositionPa param.setPosition(start); TextDocumentIdentifier id = new TextDocumentIdentifier(); if (uri != null) { - id.setUri(uri.toString()); + id.setUri(uri.toASCIIString()); } param.setTextDocument(id); return param; 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/LanguageServerWrapper.java b/src/main/java/com/redhat/devtools/intellij/quarkus/lsp4ij/LanguageServerWrapper.java index 4934c42ca..428654d26 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.console.LSPConsoleToolWindow; +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.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,162 @@ 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(); + if (this.initializeFuture == null) { + final boolean shouldLog = LoggingStreamConnectionProviderProxy.shouldLog(serverDefinition.id); + if(shouldLog) { + LSPConsoleToolWindow.showLSPConsoleFor(initialProject.getProject()); } - 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(); + 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) { + LanguageServerLifecycleManager.getInstance() + .startingProcess(this); } - initParams.setRootUri(LSPIJUtils.toUri(projectDirectory).toString()); - } else { - initParams.setRootUri(LSPIJUtils.toUri(new File("/")).toString()); //$NON-NLS-1$ + lspStreamProvider.start(); + // End process with success + if (shouldLog) { + LanguageServerLifecycleManager.getInstance() + .startedProcess(this, null); + } + } catch (IOException e) { + // End process with error + if (shouldLog) { + LanguageServerLifecycleManager.getInstance() + .startedProcess(this, e); + } + throw new RuntimeException(e); } - } - Launcher launcher = Launcher.createLauncher(client, - serverDefinition.getServerInterface(), this.lspStreamProvider.getInputStream(), - this.lspStreamProvider.getOutputStream(), executorService, consumer -> (message -> { - try { - consumer.consume(message); - logMessage(message); - URI root = initParams.getRootUri() != null ? URI.create(initParams.getRootUri()) : null; - final StreamConnectionProvider currentConnectionProvider = this.lspStreamProvider; - if (currentConnectionProvider != null && isActive()) { - currentConnectionProvider.handleMessage(message, this.languageServer, root); - } - } catch (Exception e) { - LOGGER.warn(e.getLocalizedMessage(), e); - } - })); - - this.languageServer = launcher.getRemoteProxy(); - client.connect(languageServer, this); - this.launcherFuture = launcher.startListening(); - - WorkspaceClientCapabilities workspaceClientCapabilities = new WorkspaceClientCapabilities(); - workspaceClientCapabilities.setApplyEdit(Boolean.TRUE); - workspaceClientCapabilities.setExecuteCommand(new ExecuteCommandCapabilities(Boolean.TRUE)); - workspaceClientCapabilities.setSymbol(new SymbolCapabilities(Boolean.TRUE)); - workspaceClientCapabilities.setWorkspaceFolders(Boolean.TRUE); - WorkspaceEditCapabilities editCapabilities = new WorkspaceEditCapabilities(); - editCapabilities.setDocumentChanges(Boolean.TRUE); - editCapabilities.setResourceOperations(Arrays.asList(ResourceOperationKind.Create, - ResourceOperationKind.Delete, ResourceOperationKind.Rename)); - editCapabilities.setFailureHandling(FailureHandlingKind.Undo); - workspaceClientCapabilities.setWorkspaceEdit(editCapabilities); - - TextDocumentClientCapabilities textDocumentClientCapabilities = new TextDocumentClientCapabilities(); - CodeActionCapabilities codeAction = new CodeActionCapabilities(new CodeActionLiteralSupportCapabilities( - new CodeActionKindCapabilities(Arrays.asList(CodeActionKind.QuickFix, CodeActionKind.Refactor, - CodeActionKind.RefactorExtract, CodeActionKind.RefactorInline, - CodeActionKind.RefactorRewrite, CodeActionKind.Source, - CodeActionKind.SourceOrganizeImports))), - true); - codeAction.setDataSupport(true); - codeAction.setResolveSupport(new CodeActionResolveSupportCapabilities(List.of("edit"))); //$NON-NLS-1$ - textDocumentClientCapabilities.setCodeAction(codeAction); - - textDocumentClientCapabilities.setCodeLens(new CodeLensCapabilities()); - textDocumentClientCapabilities.setInlayHint(new InlayHintCapabilities()); - textDocumentClientCapabilities.setColorProvider(new ColorProviderCapabilities()); - CompletionItemCapabilities completionItemCapabilities = new CompletionItemCapabilities(Boolean.TRUE); - completionItemCapabilities.setDocumentationFormat(Arrays.asList(MarkupKind.MARKDOWN, MarkupKind.PLAINTEXT)); - textDocumentClientCapabilities - .setCompletion(new CompletionCapabilities(completionItemCapabilities)); - DefinitionCapabilities definitionCapabilities = new DefinitionCapabilities(); - definitionCapabilities.setLinkSupport(Boolean.TRUE); - textDocumentClientCapabilities.setDefinition(definitionCapabilities); - TypeDefinitionCapabilities typeDefinitionCapabilities = new TypeDefinitionCapabilities(); - typeDefinitionCapabilities.setLinkSupport(Boolean.TRUE); - textDocumentClientCapabilities.setTypeDefinition(typeDefinitionCapabilities); - textDocumentClientCapabilities.setDocumentHighlight(new DocumentHighlightCapabilities()); - textDocumentClientCapabilities.setDocumentLink(new DocumentLinkCapabilities()); - DocumentSymbolCapabilities documentSymbol = new DocumentSymbolCapabilities(); - documentSymbol.setHierarchicalDocumentSymbolSupport(true); - documentSymbol.setSymbolKind(new SymbolKindCapabilities(Arrays.asList(SymbolKind.Array, SymbolKind.Boolean, - SymbolKind.Class, SymbolKind.Constant, SymbolKind.Constructor, SymbolKind.Enum, - SymbolKind.EnumMember, SymbolKind.Event, SymbolKind.Field, SymbolKind.File, SymbolKind.Function, - SymbolKind.Interface, SymbolKind.Key, SymbolKind.Method, SymbolKind.Module, SymbolKind.Namespace, - SymbolKind.Null, SymbolKind.Number, SymbolKind.Object, SymbolKind.Operator, SymbolKind.Package, - SymbolKind.Property, SymbolKind.String, SymbolKind.Struct, SymbolKind.TypeParameter, - SymbolKind.Variable))); - textDocumentClientCapabilities.setDocumentSymbol(documentSymbol); - textDocumentClientCapabilities.setFormatting(new FormattingCapabilities(Boolean.TRUE)); - HoverCapabilities hoverCapabilities = new HoverCapabilities(); - hoverCapabilities.setContentFormat(Arrays.asList(MarkupKind.MARKDOWN, MarkupKind.PLAINTEXT)); - textDocumentClientCapabilities.setHover(hoverCapabilities); - textDocumentClientCapabilities.setOnTypeFormatting(null); // TODO - textDocumentClientCapabilities.setRangeFormatting(new RangeFormattingCapabilities()); - textDocumentClientCapabilities.setReferences(new ReferencesCapabilities()); - textDocumentClientCapabilities.setRename(new RenameCapabilities()); - textDocumentClientCapabilities.setSignatureHelp(new SignatureHelpCapabilities()); - textDocumentClientCapabilities - .setSynchronization(new SynchronizationCapabilities(Boolean.TRUE, Boolean.TRUE, Boolean.TRUE)); - initParams.setCapabilities( - new ClientCapabilities(workspaceClientCapabilities, textDocumentClientCapabilities, lspStreamProvider.getExperimentalFeaturesPOJO())); - initParams.setClientInfo(new ClientInfo(CLIENT_NAME)); - - initParams.setInitializationOptions(this.lspStreamProvider.getInitializationOptions(rootURI)); - initParams.setTrace(this.lspStreamProvider.getTrace(rootURI)); - - // no then...Async future here as we want this chain of operation to be sequential and - // "atomic"-ish - initializeFuture = languageServer.initialize(initParams).thenAccept(res -> { - serverCapabilities = res.getCapabilities(); - this.initiallySupportsWorkspaceFolders = supportsWorkspaceFolders(serverCapabilities); - }); - initializeFuture.thenRun(() -> { - // Here we call languageServer.initialized which will send to this IJ LSP client several 'client/registerCapability' - // which will call the private method registerCapability(RegistrationParams params) - // See https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#client_registerCapability - this.languageServer.initialized(new InitializedParams()); - }); + return null; + }).thenRun(() -> { + languageClient = serverDefinition.createLanguageClient(initialProject.getProject()); + + initParams.setProcessId((int) ProcessHandle.current().pid()); - final Map toReconnect = filesToReconnect; - initializeFuture.thenRunAsync(() -> { - if (this.initialProject != null) { - watchProject(this.initialProject, true); + if (rootURI != null) { + initParams.setRootUri(rootURI.toString()); + initParams.setRootPath(rootURI.getPath()); } - for (Map.Entry fileToReconnect : toReconnect.entrySet()) { - try { - connect(fileToReconnect.getKey(), fileToReconnect.getValue()); - } catch (IOException e) { - LOGGER.warn(e.getLocalizedMessage(), e); + + UnaryOperator wrapper = consumer -> (message -> { + if (shouldLog) { + logMessage(message); } - } - }); - EditorFactory.getInstance().getEventMulticaster().addDocumentListener(fileBufferListener); - messageBusConnection = ApplicationManager.getApplication().getMessageBus().connect(); - messageBusConnection.subscribe(AppTopics.FILE_DOCUMENT_SYNC, fileBufferListener); - } catch (Exception ex) { - LOGGER.warn(ex.getLocalizedMessage(), ex); - stop(); + 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); + } + 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); + }).exceptionally(e -> { + LOGGER.error("Error while starting language server '" + serverDefinition.id + "'", e); + initializeFuture.completeExceptionally(e); + stop(); + return null; + }); + } + } + + private CompletableFuture initServer(final URI rootURI) { + + final var workspaceClientCapabilities = SupportedFeatures.getWorkspaceClientCapabilities(); + final var textDocumentClientCapabilities = SupportedFeatures.getTextDocumentClientCapabilities(); + + WindowClientCapabilities windowClientCapabilities = SupportedFeatures.getWindowClientCapabilities(); + initParams.setCapabilities(new ClientCapabilities( + workspaceClientCapabilities, + textDocumentClientCapabilities, + windowClientCapabilities, + lspStreamProvider.getExperimentalFeaturesPOJO())); + initParams.setClientInfo(getClientInfo()); + initParams.setTrace(this.lspStreamProvider.getTrace(rootURI)); + + // no then...Async future here as we want this chain of operation to be sequential and "atomic"-ish + return languageServer.initialize(initParams); + } + + private ClientInfo getClientInfo() { + ApplicationInfo applicationInfo = ApplicationInfo.getInstance(); + String versionName = applicationInfo.getVersionName(); + String buildNumber = applicationInfo.getBuild().asString(); + + String intellijVersion = versionName + " (build " + buildNumber + ")"; + return new ClientInfo(CLIENT_NAME, intellijVersion); + } + + @Nullable + private URI getRootURI() { + final Module project = this.initialProject; + if (project != null && !project.isDisposed()) { + return LSPIJUtils.toUri(project); + } + + final URI path = this.initialPath; + if (path != null) { + File projectDirectory = new File(initialPath); + if (projectDirectory.isFile()) { + projectDirectory = projectDirectory.getParentFile(); + } + return LSPIJUtils.toUri(projectDirectory); } + return null; } private static boolean supportsWorkspaceFolders(ServerCapabilities serverCapabilities) { @@ -369,6 +376,9 @@ private Integer getCurrentProcessId() { } private void logMessage(Message message) { + LanguageServerLifecycleManager.getInstance().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 +386,27 @@ 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; } } + private void startStopTimer() { + timer = new Timer("Stop Language Server Timer"); //$NON-NLS-1$ + + timer.schedule(new TimerTask() { + @Override + public void run() { + stop(); + } + }, TimeUnit.SECONDS.toMillis(this.serverDefinition.lastDocumentDisconnectedTimeout)); + } + /** * @return whether the underlying connection to language server is still active */ @@ -387,6 +415,14 @@ public boolean isActive() { } synchronized void stop() { + final boolean alreadyStopping = this.stopping.getAndSet(true); + if (alreadyStopping) { + return; + } + removeStopTimer(); + if (this.languageClient != null) { + this.languageClient.dispose(); + } if (this.initializeFuture != null) { this.initializeFuture.cancel(true); this.initializeFuture = null; @@ -398,14 +434,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 +459,7 @@ synchronized void stop() { if (provider != null) { provider.stop(); } + this.stopping.set(false); }; CompletableFuture.runAsync(shutdownKillAndStopFutureAndProvider); @@ -431,6 +471,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 +480,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 +557,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 +596,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 +620,7 @@ private boolean supportsWorkspaceFolderCapability() { * @noreference internal so far */ private CompletableFuture connect(@Nonnull URI absolutePath, Document document) throws IOException { + removeStopTimer(); final URI thePath = absolutePath; // should be useless VirtualFile file = FileDocumentManager.getInstance().getFile(document); @@ -608,7 +672,10 @@ public void disconnect(URI path) { documentListener.getDocument().removeDocumentListener(documentListener); documentListener.documentClosed(); } - if (this.connectedDocuments.isEmpty()) { + if (this.serverDefinition.lastDocumentDisconnectedTimeout != 0) { + removeStopTimer(); + startStopTimer(); + } else { stop(); } } @@ -619,9 +686,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 +754,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 +792,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 +832,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 +861,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 +893,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 +950,4 @@ public boolean canOperate(@Nonnull Document document) { return serverDefinition.isSingleton || supportsWorkspaceFolderCapability(); } -} +} \ 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..6db7593e8 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; @@ -39,17 +40,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 +78,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; } 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 consoles; + private JBList languageServerDefinitions; + + public LSPConsoleTab(Project project) { + this.project = project; + this.listener = new LanguageServerLifecycleListener() { + + @Override + public void handleStartingProcess(LanguageServerWrapper languageServer) { + DefaultListModel list = (DefaultListModel) languageServerDefinitions.getModel(); + boolean empty = list.isEmpty(); + list.addElement(new LanguageServerDefinitionLabelAdapter(languageServer)); + languageServerDefinitions.validate(); + if (empty) { + onLanguageServerSelected(languageServer); + } + } + + @Override + public void handleStartedProcess(LanguageServerWrapper languageServer, Exception exception) { + + } + + @Override + public void handleLSPMessage(Message message, LanguageServerWrapper languageServer) { + StringBuilder formattedMessage = new StringBuilder("[Trace]"); + if (message instanceof RequestMessage) { + // [Trace - 12:27:33 AM] Sending request 'initialize - (0)'. + // Params: { + formattedMessage.append(" Sending request '" + ((RequestMessage) message).getMethod() + " - (" + ((RequestMessage) message).getId() + ")'."); + formattedMessage.append("\n"); + formattedMessage.append("Params: "); + } else if (message instanceof ResponseMessage) { + // [Trace - 12:27:35 AM] Received response 'initialize - (0)' in 1921ms. + formattedMessage.append(" Received response '" + "" + " - (" + ((ResponseMessage) message).getId() + ")'."); + formattedMessage.append("\n"); + formattedMessage.append("Result: "); + } else if (message instanceof NotificationMessage) { + // [Trace - 12:27:35 AM] Sending notification 'initialized'. + // Params: {} + formattedMessage.append(" Sending notification '" + ((NotificationMessage) message).getMethod() + "'."); + formattedMessage.append("\n"); + formattedMessage.append("Params: "); + } + formattedMessage.append(message.toString()); + formattedMessage.append("\n"); + formattedMessage.append("\n"); + showMessage(languageServer, formattedMessage.toString()); + } + }; + LanguageServerLifecycleManager.getInstance().addLanguageServerLifecycleListener(listener); + } + + public JComponent createComponent() { + languageServerDefinitions = createLanguageServerDefinitionList(ls -> onLanguageServerSelected(ls)); + var scrollPane = new JBScrollPane(languageServerDefinitions); + this.consoles = new ConsolesPanel(); + var splitPane = createSplitPanel(scrollPane, consoles); + return createToolWindowPanel(splitPane); + } + + private SimpleToolWindowPanel createToolWindowPanel(JComponent splitPane) { + var panel = new SimpleToolWindowPanel(false, true); + panel.setContent(splitPane); + panel.revalidate(); + panel.repaint(); + return panel; + } + + private void onLanguageServerSelected(LanguageServerWrapper languageServer) { + if (consoles == null) { + return; + } + consoles.select(languageServer, true); + } + + private 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; + } + + private JBList createLanguageServerDefinitionList(Consumer onSelected) { + JBList list = new JBList(new DefaultListModel()) { + + }; + list.addListSelectionListener(e -> { + onSelected.accept(list.getSelectedValue().languageServer); + }); + return list; + } + + public String getDisplayName() { + return project.getName(); + } + + @Override + public void dispose() { + if (consoles != null) { + consoles.dispose(); + } + LanguageServerLifecycleManager.getInstance().removeLanguageServerLifecycleListener(listener); + } + + /** + * A card-panel that displays panels for each container. + */ + private class ConsolesPanel extends CardLayoutPanel { + + @Override + protected LanguageServerWrapper prepare(LanguageServerWrapper key) { + return key; + } + + @Override + protected ConsoleOrErrorPanel create(LanguageServerWrapper key) { + return new ConsoleOrErrorPanel(); + } + + @Override + public void dispose() { + removeAll(); + } + + @Override + protected void dispose(LanguageServerWrapper key, ConsoleOrErrorPanel value) { + if (value != null) { + value.dispose(); + } + } + + } + + private class ConsoleOrErrorPanel extends SimpleCardLayoutPanel { + + private static final String NAME_VIEW_CONSOLE = "console"; + private static final String NAME_VIEW_ERROR = "error"; + + private final ConsoleView consoleView; + + public ConsoleOrErrorPanel() { + consoleView = createConsoleView(project); + if (consoleView != null) { + add(consoleView.getComponent(), NAME_VIEW_CONSOLE); + } + //add(errorView.component, NAME_VIEW_ERROR) + showConsole(); + } + + private void showConsole() { + show(NAME_VIEW_CONSOLE); + } + + public void showMessage(String message) { + consoleView.print(message, ConsoleViewContentType.SYSTEM_OUTPUT); + } + } + + private ConsoleView createConsoleView(Project project) { + var builder = TextConsoleBuilderFactory.getInstance().createBuilder(project); + builder.setViewer(true); + return builder.getConsole(); + } + + public void showMessage(LanguageServerWrapper container, String message) { + var consoleOrErrorPanel = consoles.getValue(container, false); + if (consoleOrErrorPanel != null) { + consoleOrErrorPanel.showMessage(message); + } + + } +} diff --git a/src/main/java/com/redhat/devtools/intellij/quarkus/lsp4ij/console/LSPConsoleToolWindow.java b/src/main/java/com/redhat/devtools/intellij/quarkus/lsp4ij/console/LSPConsoleToolWindow.java new file mode 100644 index 000000000..680e5a99d --- /dev/null +++ b/src/main/java/com/redhat/devtools/intellij/quarkus/lsp4ij/console/LSPConsoleToolWindow.java @@ -0,0 +1,96 @@ +/******************************************************************************* + * 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.Project; +import com.intellij.openapi.util.Disposer; +import com.intellij.openapi.wm.ToolWindow; +import com.intellij.openapi.wm.ToolWindowManager; +import com.intellij.ui.content.Content; +import com.intellij.ui.content.ContentManager; +import com.redhat.devtools.intellij.quarkus.lsp4ij.LanguageServerWrapper; +import com.redhat.devtools.intellij.quarkus.lsp4ij.lifecycle.LanguageServerLifecycleManager; +import com.redhat.devtools.intellij.quarkus.lsp4ij.utils.IDEAContentFactory; +import org.eclipse.lsp4j.jsonrpc.messages.Message; +import static com.redhat.devtools.intellij.quarkus.lsp4ij.utils.UIHelper.executeInUI; + +/** + * LSP console. + * + * @author Angelo ZERR + */ +public class LSPConsoleToolWindow { + + private static final String LSP_CONSOLE_FACTORY_ID = "lsp-console-factory"; + + public static boolean showLSPConsoleFor(Project project) { + return executeInUI(() -> { + var added = false; + var toolWindow = ToolWindowManager.getInstance(project).getToolWindow(LSP_CONSOLE_FACTORY_ID); + if (toolWindow != null) { + var displayName = project.getName(); // tab.getDisplayName() + var existing = toolWindow.getContentManager().findContent(displayName); + if (existing == null) { + LSPConsoleTab tab = new LSPConsoleTab(project); + var content = createContent(tab); + addContent(toolWindow, content); + added = true; + } + selectTab(displayName, toolWindow.getContentManager()); + ensureOpened(toolWindow); + } + return added; + }); + } + + private static Content createContent(LSPConsoleTab tab) { + var content = IDEAContentFactory.getInstance().createContent( + tab.createComponent(), + tab.getDisplayName(), + true + ); + Disposer.register(content, tab); + content.setCloseable(true); + return content; + } + + private static void addContent(ToolWindow toolWindow, Content content) { + executeInUI(() -> { + toolWindow.getContentManager().addContent(content); + }); + } + + private static void selectTab(String tabName, ContentManager manager) { + executeInUI(() -> { + var content = manager.findContent(tabName); + manager.setSelectedContent(content); + }); + } + + private static void ensureOpened(ToolWindow toolWindow) { + executeInUI(() -> { + if (toolWindow.isVisible() + && toolWindow.isActive() + && toolWindow.isAvailable() + ) { + return; //@executeInUI + } + toolWindow.setToHideOnEmptyContent(true); + toolWindow.setAvailable(true, null); + toolWindow.activate(null); + toolWindow.show(null); + }); + } + +} 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..ad39b0d45 --- /dev/null +++ b/src/main/java/com/redhat/devtools/intellij/quarkus/lsp4ij/console/LSPConsoleToolWindowFactory.java @@ -0,0 +1,33 @@ +/******************************************************************************* + * 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.Project; +import com.intellij.openapi.wm.ToolWindow; +import com.intellij.openapi.wm.ToolWindowFactory; +import com.redhat.devtools.intellij.quarkus.lsp4ij.LanguageServerBundle; +import org.jetbrains.annotations.NotNull; + +/** + * LSP console factory. + * + * @author Angelo ZERR + */ +public class LSPConsoleToolWindowFactory implements ToolWindowFactory { + + @Override + public void createToolWindowContent(@NotNull Project project, @NotNull ToolWindow toolWindow) { + toolWindow.setStripeTitle(LanguageServerBundle.message("lsp.console.title")); + } +} diff --git a/src/main/java/com/redhat/devtools/intellij/quarkus/lsp4ij/console/LanguageServerDefinitionLabelAdapter.java b/src/main/java/com/redhat/devtools/intellij/quarkus/lsp4ij/console/LanguageServerDefinitionLabelAdapter.java new file mode 100644 index 000000000..b599c60b1 --- /dev/null +++ b/src/main/java/com/redhat/devtools/intellij/quarkus/lsp4ij/console/LanguageServerDefinitionLabelAdapter.java @@ -0,0 +1,17 @@ +package com.redhat.devtools.intellij.quarkus.lsp4ij.console; + +import com.redhat.devtools.intellij.quarkus.lsp4ij.LanguageServerWrapper; + +public class LanguageServerDefinitionLabelAdapter { + + public final LanguageServerWrapper languageServer; + + public LanguageServerDefinitionLabelAdapter(LanguageServerWrapper languageServer) { + this.languageServer = languageServer; + } + + @Override + public String toString() { + return languageServer.serverDefinition.getDisplayName(); + } +} 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/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..3df93c0b7 --- /dev/null +++ b/src/main/java/com/redhat/devtools/intellij/quarkus/lsp4ij/lifecycle/LanguageServerLifecycleListener.java @@ -0,0 +1,14 @@ +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, Exception exception); + + void handleLSPMessage(Message message, LanguageServerWrapper languageServer); + +} 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..ccfd2bfd4 --- /dev/null +++ b/src/main/java/com/redhat/devtools/intellij/quarkus/lsp4ij/lifecycle/LanguageServerLifecycleManager.java @@ -0,0 +1,73 @@ +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 java.util.ArrayList; +import java.util.List; + +public class LanguageServerLifecycleManager { + + private final List listeners; + + public LanguageServerLifecycleManager() { + this.listeners = new ArrayList<>(); + } + + public static LanguageServerLifecycleManager getInstance() { + return ServiceManager.getService(LanguageServerLifecycleManager.class); + } + + public void addLanguageServerLifecycleListener(LanguageServerLifecycleListener listener) { + synchronized (this.listeners) { + this.listeners.add(listener); + } + } + + public void removeLanguageServerLifecycleListener(LanguageServerLifecycleListener listener) { + synchronized (this.listeners) { + this.listeners.remove(listener); + } + } + + public void startingProcess(LanguageServerWrapper languageServer) { + synchronized (this.listeners) { + for (LanguageServerLifecycleListener listener: this.listeners) { + try { + listener.handleStartingProcess(languageServer); + } + catch(Exception e) { + // Do nothing + } + } + } + } + + public void startedProcess(LanguageServerWrapper languageServer, Exception exception) { + synchronized (this.listeners) { + for (LanguageServerLifecycleListener listener: this.listeners) { + try { + listener.handleStartedProcess(languageServer, exception); + } + catch(Exception e) { + // Do nothing + } + } + } + } + + public void logLSPMessage(Message message, LanguageServerWrapper languageServer) { + synchronized (this.listeners) { + for (LanguageServerLifecycleListener listener: this.listeners) { + try { + listener.handleLSPMessage(message, languageServer); + } + catch(Exception e) { + // Do nothing + } + } + } + } +} 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..2a3d52932 --- /dev/null +++ b/src/main/java/com/redhat/devtools/intellij/quarkus/lsp4ij/utils/UIHelper.java @@ -0,0 +1,46 @@ +/******************************************************************************* + * 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().invokeAndWait(runnable); + } + } + + public static T executeInUI(Supplier supplier) { + 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..d71c2afa9 100644 --- a/src/main/resources/META-INF/lsp.xml +++ b/src/main/resources/META-INF/lsp.xml @@ -8,8 +8,12 @@ - + + @@ -52,9 +56,10 @@ + + - 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