From 4b30de26df3b9fa28f8855bba5c0222806cdaf90 Mon Sep 17 00:00:00 2001 From: azerr Date: Wed, 26 Jul 2023 12:47:07 +0200 Subject: [PATCH] feat: Support crash of language server starting (#1001) Fixes #1001 Signed-off-by: azerr --- .../lsp4ij/LanguageServerWrapper.java | 335 ++++++++++++------ .../lsp4ij/LanguageServersRegistry.java | 39 +- .../lsp4ij/LanguageServiceAccessor.java | 120 ++++--- .../{console/explorer => }/ServerStatus.java | 14 +- .../console/LSPConsoleToolWindowPanel.java | 21 +- .../console/actions/AutoFoldingAction.java | 3 +- .../actions/ClearThisConsoleAction.java | 10 +- .../explorer/LanguageServerExplorer.java | 28 ++ ...nguageServerExplorerLifecycleListener.java | 40 +-- .../LanguageServerProcessTreeNode.java | 71 ++-- .../explorer/LanguageServerTreeNode.java | 4 +- .../explorer/LanguageServerTreeRenderer.java | 9 +- .../explorer/TracingMessageConsumer.java | 4 +- .../actions/CopyStartServerCommandAction.java | 69 ++++ .../explorer/actions/RestartServerAction.java | 2 +- .../LanguageServerLifecycleListener.java | 10 +- .../LanguageServerLifecycleManager.java | 55 +-- .../server/CannotStartProcessException.java | 31 ++ .../server/CannotStartServerException.java | 24 ++ .../server/JavaProcessCommandBuilder.java | 8 +- .../server/LanguageServerException.java | 32 ++ .../devtools/intellij/lsp4ij/server/OS.java | 4 +- .../ProcessStreamConnectionProvider.java | 60 +++- .../server/StreamConnectionProvider.java | 108 +++--- src/main/resources/META-INF/lsp4ij.xml | 3 + .../messages/LanguageServerBundle.properties | 1 + 26 files changed, 729 insertions(+), 376 deletions(-) rename src/main/java/com/redhat/devtools/intellij/lsp4ij/{console/explorer => }/ServerStatus.java (67%) create mode 100644 src/main/java/com/redhat/devtools/intellij/lsp4ij/console/explorer/actions/CopyStartServerCommandAction.java create mode 100644 src/main/java/com/redhat/devtools/intellij/lsp4ij/server/CannotStartProcessException.java create mode 100644 src/main/java/com/redhat/devtools/intellij/lsp4ij/server/CannotStartServerException.java create mode 100644 src/main/java/com/redhat/devtools/intellij/lsp4ij/server/LanguageServerException.java diff --git a/src/main/java/com/redhat/devtools/intellij/lsp4ij/LanguageServerWrapper.java b/src/main/java/com/redhat/devtools/intellij/lsp4ij/LanguageServerWrapper.java index 2802b7873..f3236924c 100644 --- a/src/main/java/com/redhat/devtools/intellij/lsp4ij/LanguageServerWrapper.java +++ b/src/main/java/com/redhat/devtools/intellij/lsp4ij/LanguageServerWrapper.java @@ -1,3 +1,13 @@ +/******************************************************************************* + * 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 https://www.eclipse.org/legal/epl-v20.html + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + ******************************************************************************/ package com.redhat.devtools.intellij.lsp4ij; import com.google.common.util.concurrent.ThreadFactoryBuilder; @@ -22,14 +32,10 @@ import com.intellij.openapi.vfs.VirtualFile; import com.intellij.util.messages.MessageBusConnection; import com.redhat.devtools.intellij.lsp4ij.internal.SupportedFeatures; -import com.redhat.devtools.intellij.lsp4ij.server.ProcessStreamConnectionProvider; -import com.redhat.devtools.intellij.lsp4ij.server.StreamConnectionProvider; -import com.redhat.devtools.intellij.lsp4ij.settings.ServerTrace; -import com.redhat.devtools.intellij.lsp4ij.settings.UserDefinedLanguageServerSettings; import com.redhat.devtools.intellij.lsp4ij.lifecycle.LanguageServerLifecycleManager; import com.redhat.devtools.intellij.lsp4ij.lifecycle.NullLanguageServerLifecycleManager; +import com.redhat.devtools.intellij.lsp4ij.server.*; import org.eclipse.lsp4j.*; -import org.eclipse.lsp4j.jsonrpc.JsonRpcException; import org.eclipse.lsp4j.jsonrpc.Launcher; import org.eclipse.lsp4j.jsonrpc.MessageConsumer; import org.eclipse.lsp4j.jsonrpc.messages.Either; @@ -50,9 +56,13 @@ import java.util.function.Consumer; import java.util.function.UnaryOperator; +/** + * Language server wrapper. + */ public class LanguageServerWrapper { private static final Logger LOGGER = LoggerFactory.getLogger(LanguageServerWrapper.class);//$NON-NLS-1$ private static final String CLIENT_NAME = "IntelliJ"; + private static final int MAX_NUMBER_OF_RESTART_ATTEMPTS = 20; // TODO move this max value in settings class Listener implements DocumentListener, FileDocumentManagerListener, FileEditorManagerListener { @Override @@ -90,7 +100,6 @@ public void fileClosed(@NotNull FileEditorManager source, @NotNull VirtualFile f } } } - } private Listener fileBufferListener = new Listener(); @@ -110,6 +119,8 @@ public void fileClosed(@NotNull FileEditorManager source, @NotNull VirtualFile f protected StreamConnectionProvider lspStreamProvider; private Future launcherFuture; + + private int numberOfRestartAttempts; private CompletableFuture initializeFuture; private LanguageServer languageServer; private LanguageClientImpl languageClient; @@ -117,6 +128,14 @@ public void fileClosed(@NotNull FileEditorManager source, @NotNull VirtualFile f private Timer timer; private final AtomicBoolean stopping = new AtomicBoolean(false); + private ServerStatus serverStatus; + + private LanguageServerException serverError; + + private Long currentProcessId; + + private List currentProcessCommandLines; + private final ExecutorService dispatcher; private final ExecutorService listener; @@ -158,6 +177,7 @@ private LanguageServerWrapper(@Nullable Module project, @Nonnull LanguageServers String listenerThreadNameFormat = "LS-" + serverDefinition.id + projectName + "#listener-%d"; //$NON-NLS-1$ //$NON-NLS-2$ this.listener = Executors .newCachedThreadPool(new ThreadFactoryBuilder().setNameFormat(listenerThreadNameFormat).build()); + udateStatus(ServerStatus.none); } public Project getProject() { @@ -198,13 +218,40 @@ private List getRelevantWorkspaceFolders() { return folders; } + public synchronized void restart() throws IOException { + numberOfRestartAttempts = 0; + setEnabled(true); + stop(); + start(); + } + + private void setEnabled(boolean enabled) { + this.serverDefinition.setEnabled(enabled); + } + + public boolean isEnabled() { + return serverDefinition.isEnabled(); + } + /** * Starts a language server and triggers initialization. If language server is * started and active, does nothing. If language server is inactive, restart it. * - * @throws IOException + * @throws LanguageServerException thrown when the language server cannot be started */ - public synchronized void start() throws IOException { + public synchronized void start() throws LanguageServerException { + if (serverError != null) { + // Here the language server has been not possible + // we stop it and attempts a new restart if needed + stop(); + if (numberOfRestartAttempts > MAX_NUMBER_OF_RESTART_ATTEMPTS - 1) { + // Disable the language server + setEnabled(false); + return; + } else { + numberOfRestartAttempts++; + } + } final var filesToReconnect = new HashMap(); if (this.languageServer != null) { if (isActive()) { @@ -216,23 +263,33 @@ public synchronized void start() throws IOException { stop(); } } + if (this.initializeFuture == null) { final URI rootURI = getRootURI(); this.launcherFuture = new CompletableFuture<>(); this.initializeFuture = CompletableFuture.supplyAsync(() -> { this.lspStreamProvider = serverDefinition.createConnectionProvider(initialProject.getProject()); initParams.setInitializationOptions(this.lspStreamProvider.getInitializationOptions(rootURI)); - try { - // Starting process... - getLanguageServerLifecycleManager().onStartingProcess(this); - lspStreamProvider.start(); - // End process with success - getLanguageServerLifecycleManager().onStartedProcess(this, null); - } catch (IOException e) { - // End process with error - getLanguageServerLifecycleManager().onStartedProcess(this, e); - throw new RuntimeException(e); + + // Starting process... + udateStatus(ServerStatus.starting); + getLanguageServerLifecycleManager().onStatusChanged(this); + this.currentProcessId = null; + this.currentProcessCommandLines = null; + lspStreamProvider.start(); + + // As process can be stopped, we loose pid and command lines information + // when server is stopped, we store them here. + // to display them in the Language server explorer even if process is killed. + if (lspStreamProvider instanceof ProcessStreamConnectionProvider) { + ProcessStreamConnectionProvider provider = (ProcessStreamConnectionProvider) lspStreamProvider; + this.currentProcessId = provider.getPid(); + this.currentProcessCommandLines = provider.getCommands(); } + + // Throws the CannotStartProcessException exception if process is not alive. + // This usecase comes for instance when the start process command fails (not a valid start command) + lspStreamProvider.ensureIsAlive(); return null; }).thenRun(() -> { languageClient = serverDefinition.createLanguageClient(initialProject.getProject()); @@ -247,11 +304,10 @@ public synchronized void start() throws IOException { logMessage(message, consumer); try { consumer.consume(message); - } catch (JsonRpcException e) { - // When shutdown or exit is called, the pipe can be closed, in this case the exception must be ignored: - if (!isIgnoreException(e)) { - throw e; - } + } catch (Throwable e) { + // Log in the LSP console the error + getLanguageServerLifecycleManager().onError(this, e); + throw e; } final StreamConnectionProvider currentConnectionProvider = this.lspStreamProvider; if (currentConnectionProvider != null && isActive()) { @@ -273,6 +329,7 @@ public synchronized void start() throws IOException { }) .thenCompose(unused -> initServer(rootURI)) .thenAccept(res -> { + serverError = null; serverCapabilities = res.getCapabilities(); this.initiallySupportsWorkspaceFolders = supportsWorkspaceFolders(serverCapabilities); }).thenRun(() -> { @@ -295,28 +352,25 @@ public synchronized void start() throws IOException { messageBusConnection = ApplicationManager.getApplication().getMessageBus().connect(); messageBusConnection.subscribe(AppTopics.FILE_DOCUMENT_SYNC, fileBufferListener); messageBusConnection.subscribe(FileEditorManagerListener.FILE_EDITOR_MANAGER, fileBufferListener); - getLanguageServerLifecycleManager().onStartedLanguageServer(this, null); + udateStatus(ServerStatus.started); + getLanguageServerLifecycleManager().onStatusChanged(this); }).exceptionally(e -> { - LOGGER.error("Error while starting language server '" + serverDefinition.id + "'", e); - initializeFuture.completeExceptionally(e); - getLanguageServerLifecycleManager().onStartedLanguageServer(this, e); - stop(); + if (e instanceof CompletionException) { + e = e.getCause(); + } + if (e instanceof CannotStartProcessException) { + serverError = (CannotStartProcessException) e; + } else { + serverError = new CannotStartServerException("Error while starting language server '" + serverDefinition.id + "' (pid=" + getCurrentProcessId() + ")", e); + } + initializeFuture.completeExceptionally(serverError); + getLanguageServerLifecycleManager().onError(this, e); + stop(false); return null; }); } } - private boolean isIgnoreException(JsonRpcException e) { - if (!isStopping()) { - // The language server is not stopping, don't ignore the error - return false; - } - if (JsonRpcException.indicatesStreamClosed(e)) { - return true; - } - return e.getCause() != null && "The pipe is being closed".equals(e.getCause().getMessage()); - } - private CompletableFuture initServer(final URI rootURI) { final var workspaceClientCapabilities = SupportedFeatures.getWorkspaceClientCapabilities(); @@ -372,17 +426,25 @@ private void logMessage(Message message, MessageConsumer consumer) { getLanguageServerLifecycleManager().logLSPMessage(message, consumer, this); } - private void removeStopTimer() { + private void removeStopTimer(boolean stopping) { if (timer != null) { timer.cancel(); timer = null; - getLanguageServerLifecycleManager().onStartedLanguageServer(this, null); + if (!stopping) { + udateStatus(ServerStatus.started); + getLanguageServerLifecycleManager().onStatusChanged(this); + } } } + private void udateStatus(ServerStatus serverStatus) { + this.serverStatus = serverStatus; + } + private void startStopTimer() { timer = new Timer("Stop Language Server Timer"); //$NON-NLS-1$ - getLanguageServerLifecycleManager().onStoppingLanguageServer(this); + udateStatus(ServerStatus.stopping); + getLanguageServerLifecycleManager().onStatusChanged(this); timer.schedule(new TimerTask() { @Override public void run() { @@ -409,72 +471,113 @@ public boolean isStopping() { public synchronized void stop() { final boolean alreadyStopping = this.stopping.getAndSet(true); - if (alreadyStopping) { - return; - } - getLanguageServerLifecycleManager().onStoppingLanguageServer(this); - removeStopTimer(); - if (this.languageClient != null) { - this.languageClient.dispose(); - } - if (this.initializeFuture != null) { - this.initializeFuture.cancel(true); - this.initializeFuture = null; - } - - this.serverCapabilities = null; - this.dynamicRegistrations.clear(); - - final Future serverFuture = this.launcherFuture; - final StreamConnectionProvider provider = this.lspStreamProvider; - final LanguageServer languageServerInstance = this.languageServer; - // ResourcesPlugin.getWorkspace().removeResourceChangeListener(workspaceFolderUpdater); + stop(alreadyStopping); + } - Runnable shutdownKillAndStopFutureAndProvider = () -> { - if (languageServerInstance != null) { - CompletableFuture shutdown = languageServerInstance.shutdown(); - try { - shutdown.get(5, TimeUnit.SECONDS); - } catch (InterruptedException ex) { - Thread.currentThread().interrupt(); - } catch (TimeoutException ex) { - LOGGER.warn("Timeout error while shutdown the language server '" + serverDefinition.id + "'", ex); - } catch (Exception ex) { - LOGGER.error("Error while shutdown the language server '" + serverDefinition.id + "'", ex); - } + public synchronized void stop(boolean alreadyStopping) { + try { + if (alreadyStopping) { + return; } + udateStatus(ServerStatus.stopping); + getLanguageServerLifecycleManager().onStatusChanged(this); - // Consume language server exit() before cancelling launcher future (serverFuture.cancel()) - // to avoid having error like "The pipe is being closed". - if (languageServerInstance != null) { - languageServerInstance.exit(); + removeStopTimer(true); + if (this.languageClient != null) { + this.languageClient.dispose(); } - if (serverFuture != null) { - serverFuture.cancel(true); + if (this.initializeFuture != null) { + this.initializeFuture.cancel(true); + this.initializeFuture = null; } - if (provider != null) { - provider.stop(); - } - this.stopping.set(false); - getLanguageServerLifecycleManager().onStoppedLanguageServer(this, null); - }; + this.serverCapabilities = null; + this.dynamicRegistrations.clear(); + + // We need to shutdown, kill and stop the process in a thread to avoid for instance + // stopping the new process created with a new start. + final Future serverFuture = this.launcherFuture; + final StreamConnectionProvider provider = this.lspStreamProvider; + final LanguageServer languageServerInstance = this.languageServer; + + Runnable shutdownKillAndStopFutureAndProvider = () -> { + if (languageServerInstance != null && provider != null && provider.isAlive()) { + // The LSP language server instance and the process which starts the language server is alive. Process + // - shutdown + // - exit + + // shutdown the language server + try { + shutdownLanguageServerInstance(languageServerInstance); + } catch (Exception ex) { + getLanguageServerLifecycleManager().onError(this, ex); + } - CompletableFuture.runAsync(shutdownKillAndStopFutureAndProvider); + // exit the language server + // Consume language server exit() before cancelling launcher future (serverFuture.cancel()) + // to avoid having error like "The pipe is being closed". + try { + exitLanguageServerInstance(languageServerInstance); + } catch (Exception ex) { + getLanguageServerLifecycleManager().onError(this, ex); + } + } + + if (serverFuture != null) { + serverFuture.cancel(true); + } - this.launcherFuture = null; - this.lspStreamProvider = null; + if (provider != null) { + provider.stop(); + } + this.stopping.set(false); + udateStatus(ServerStatus.stopped); + getLanguageServerLifecycleManager().onStatusChanged(this); + }; + + CompletableFuture.runAsync(shutdownKillAndStopFutureAndProvider); + } finally { + this.launcherFuture = null; + this.lspStreamProvider = null; + + while (!this.connectedDocuments.isEmpty()) { + disconnect(this.connectedDocuments.keySet().iterator().next(), true); + } + this.languageServer = null; + this.languageClient = null; + + EditorFactory.getInstance().getEventMulticaster().removeDocumentListener(fileBufferListener); + if (messageBusConnection != null) { + messageBusConnection.disconnect(); + } + } + } - while (!this.connectedDocuments.isEmpty()) { - disconnect(this.connectedDocuments.keySet().iterator().next(), true); + private void shutdownLanguageServerInstance(LanguageServer languageServerInstance) throws Exception { + CompletableFuture shutdown = languageServerInstance.shutdown(); + try { + shutdown.get(5, TimeUnit.SECONDS); + } catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + } catch (TimeoutException ex) { + String message = "Timeout error while shutdown the language server '" + serverDefinition.id + "'"; + LOGGER.warn(message, ex); + throw new Exception(message, ex); + } catch (Exception ex) { + String message = "Error while shutdown the language server '" + serverDefinition.id + "'"; + LOGGER.warn(message, ex); + throw new Exception(message, ex); } - this.languageServer = null; - this.languageClient = null; + } - EditorFactory.getInstance().getEventMulticaster().removeDocumentListener(fileBufferListener); - if (messageBusConnection != null) { - messageBusConnection.disconnect(); + private void exitLanguageServerInstance(LanguageServer languageServerInstance) throws Exception { + try { + languageServerInstance.exit(); + } catch (Exception ex) { + String message = "Error while exit the language server '" + serverDefinition.id + "'"; + LOGGER.error(message, ex); + throw new Exception(message, ex); } } @@ -619,7 +722,7 @@ private boolean supportsWorkspaceFolderCapability() { * @noreference internal so far */ private CompletableFuture connect(@Nonnull URI absolutePath, Document document) throws IOException { - removeStopTimer(); + removeStopTimer(false); final URI thePath = absolutePath; // should be useless VirtualFile file = FileDocumentManager.getInstance().getFile(document); @@ -677,7 +780,7 @@ private void disconnect(URI path, boolean stopping) { } if (!stopping && this.connectedDocuments.isEmpty()) { if (this.serverDefinition.lastDocumentDisconnectedTimeout != 0 && !ApplicationManager.getApplication().isUnitTestMode()) { - removeStopTimer(); + removeStopTimer(true); startStopTimer(); } else { stop(); @@ -736,8 +839,9 @@ public LanguageServer getServer() { public CompletableFuture getInitializedServer() { try { start(); - } catch (IOException ex) { - LOGGER.warn(ex.getLocalizedMessage(), ex); + } catch (LanguageServerException ex) { + // The language server cannot be started, return a null language server + return CompletableFuture.completedFuture(null); } if (initializeFuture != null && !this.initializeFuture.isDone()) { /*if (ApplicationManager.getApplication().isDispatchThread()) { // UI Thread @@ -972,12 +1076,41 @@ private static int getParentProcessId() { return (int) ProcessHandle.current().pid(); } + // ------------------ Current Process information. + /** * Returns the current process id and null otherwise. * * @return the current process id and null otherwise. */ public Long getCurrentProcessId() { - return lspStreamProvider instanceof ProcessStreamConnectionProvider ? ((ProcessStreamConnectionProvider) lspStreamProvider).getPid() : null; + return currentProcessId; + } + + public List getCurrentProcessCommandLine() { + return currentProcessCommandLines; + } + + // ------------------ Server status information . + + /** + * Returns the server status. + * + * @return the server status. + */ + public ServerStatus getServerStatus() { + return serverStatus; + } + + public LanguageServerException getServerError() { + return serverError; + } + + public int getNumberOfRestartAttempts() { + return numberOfRestartAttempts; + } + + public int getMaxNumberOfRestartAttempts() { + return MAX_NUMBER_OF_RESTART_ATTEMPTS; } } \ No newline at end of file diff --git a/src/main/java/com/redhat/devtools/intellij/lsp4ij/LanguageServersRegistry.java b/src/main/java/com/redhat/devtools/intellij/lsp4ij/LanguageServersRegistry.java index 4e491bc56..e413d7551 100644 --- a/src/main/java/com/redhat/devtools/intellij/lsp4ij/LanguageServersRegistry.java +++ b/src/main/java/com/redhat/devtools/intellij/lsp4ij/LanguageServersRegistry.java @@ -39,12 +39,16 @@ 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 @Nonnull + String id; + public final @Nonnull + String label; public final boolean isSingleton; - public final @Nonnull Map languageIdMappings; + public final @Nonnull + Map languageIdMappings; public final String description; public final int lastDocumentDisconnectedTimeout; + private boolean enabled; public LanguageServerDefinition(@Nonnull String id, @Nonnull String label, String description, boolean isSingleton, Integer lastDocumentDisconnectedTimeout) { this.id = id; @@ -53,6 +57,26 @@ public LanguageServerDefinition(@Nonnull String id, @Nonnull String label, Strin this.isSingleton = isSingleton; this.lastDocumentDisconnectedTimeout = lastDocumentDisconnectedTimeout != null && lastDocumentDisconnectedTimeout > 0 ? lastDocumentDisconnectedTimeout : DEFAULT_LAST_DOCUMENTED_DISCONNECTED_TIMEOUT; this.languageIdMappings = new ConcurrentHashMap<>(); + setEnabled(true); + } + + + /** + * Returns true if the language server definition is enabled and false otherwise. + * + * @return true if the language server definition is enabled and false otherwise. + */ + public boolean isEnabled() { + return enabled; + } + + /** + * Set enabled the language server definition. + * + * @param enabled enabled the language server definition. + */ + public void setEnabled(boolean enabled) { + this.enabled = enabled; } public void registerAssociation(@Nonnull Language language, @Nonnull String languageId) { @@ -224,9 +248,12 @@ LanguageServerDefinition getDefinition(@NonNull String languageServerId) { */ private static class LanguageMapping { - @Nonnull public final String id; - @Nonnull public final Language language; - @Nullable public final String languageId; + @Nonnull + public final String id; + @Nonnull + public final Language language; + @Nullable + public final String languageId; public LanguageMapping(@Nonnull Language language, @Nonnull String id, @Nullable String languageId) { this.language = language; diff --git a/src/main/java/com/redhat/devtools/intellij/lsp4ij/LanguageServiceAccessor.java b/src/main/java/com/redhat/devtools/intellij/lsp4ij/LanguageServiceAccessor.java index 06282e435..9e31ccdee 100644 --- a/src/main/java/com/redhat/devtools/intellij/lsp4ij/LanguageServiceAccessor.java +++ b/src/main/java/com/redhat/devtools/intellij/lsp4ij/LanguageServiceAccessor.java @@ -1,3 +1,13 @@ +/******************************************************************************* + * 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 https://www.eclipse.org/legal/epl-v20.html + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + ******************************************************************************/ package com.redhat.devtools.intellij.lsp4ij; import com.intellij.lang.Language; @@ -21,23 +31,15 @@ import javax.annotation.Nullable; import java.io.IOException; import java.net.URI; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.LinkedHashSet; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.Queue; -import java.util.Set; +import java.util.*; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; import java.util.function.Predicate; import java.util.stream.Collectors; +/** + * Language server accessor. + */ public class LanguageServiceAccessor { private static final Logger LOGGER = LoggerFactory.getLogger(LanguageServiceAccessor.class); private final Project project; @@ -45,7 +47,7 @@ public class LanguageServiceAccessor { public static LanguageServiceAccessor getInstance(Project project) { return ServiceManager.getService(project, LanguageServiceAccessor.class); } - + private LanguageServiceAccessor(Project project) { this.project = project; } @@ -64,6 +66,15 @@ public void clearStartedServers() { } } + /** + * Return the started servers. + * + * @return the started servers. + */ + public Set getStartedServers() { + return startedServers; + } + void shutdownAllDispatchers() { startedServers.forEach(LanguageServerWrapper::stopDispatcher); } @@ -201,7 +212,7 @@ public void enableLanguageServerContentType( */ @Deprecated public LanguageServer getLanguageServer(@Nonnull VirtualFile file, @Nonnull LanguageServersRegistry.LanguageServerDefinition lsDefinition, - Predicate capabilitiesPredicate) + Predicate capabilitiesPredicate) throws IOException { LanguageServerWrapper wrapper = getLSWrapperForConnection(LSPIJUtils.getProject(file), lsDefinition, LSPIJUtils.toUri(file)); if (capabilitiesPredicate == null @@ -222,8 +233,8 @@ public LanguageServer getLanguageServer(@Nonnull VirtualFile file, @Nonnull Lang * @return a LanguageServer for the given file, which is defined with provided server ID and conforms to specified request */ public CompletableFuture getInitializedLanguageServer(@Nonnull VirtualFile file, - @Nonnull LanguageServersRegistry.LanguageServerDefinition lsDefinition, - Predicate capabilitiesPredicate) + @Nonnull LanguageServersRegistry.LanguageServerDefinition lsDefinition, + Predicate capabilitiesPredicate) throws IOException { LanguageServerWrapper wrapper = getLSWrapperForConnection(LSPIJUtils.getProject(file), lsDefinition, LSPIJUtils.toUri(file)); if (capabilitiesPredicate == null @@ -239,17 +250,16 @@ public CompletableFuture getInitializedLanguageServer(@Nonnull V * Get the requested language server instance for the given document. Starts the * language server if not already started. * - * @param document the document for which the initialized LanguageServer shall be returned - * @param serverId the ID of the LanguageServer to be returned - * @param capabilitesPredicate - * a predicate to check capabilities + * @param document the document for which the initialized LanguageServer shall be returned + * @param serverId the ID of the LanguageServer to be returned + * @param capabilitesPredicate a predicate to check capabilities * @return a LanguageServer for the given file, which is defined with provided - * server ID and conforms to specified request. If - * {@code capabilitesPredicate} does not test positive for the server's - * capabilities, {@code null} is returned. + * server ID and conforms to specified request. If + * {@code capabilitesPredicate} does not test positive for the server's + * capabilities, {@code null} is returned. */ public CompletableFuture getInitializedLanguageServer(Document document, - LanguageServersRegistry.LanguageServerDefinition lsDefinition, Predicate capabilitiesPredicate) + LanguageServersRegistry.LanguageServerDefinition lsDefinition, Predicate capabilitiesPredicate) throws IOException { URI initialPath = LSPIJUtils.toUri(document); LanguageServerWrapper wrapper = getLSWrapperForConnection(document, lsDefinition, initialPath); @@ -264,15 +274,13 @@ public CompletableFuture getInitializedLanguageServer(Document d * Checks if the given {@code wrapper}'s capabilities comply with the given * {@code capabilitiesPredicate}. * - * @param wrapper - * the server that's capabilities are tested with - * {@code capabilitiesPredicate} - * @param capabilitiesPredicate - * predicate testing the capabilities of {@code wrapper}. + * @param wrapper the server that's capabilities are tested with + * {@code capabilitiesPredicate} + * @param capabilitiesPredicate predicate testing the capabilities of {@code wrapper}. * @return The result of applying the capabilities of {@code wrapper} to - * {@code capabilitiesPredicate}, or {@code false} if - * {@code capabilitiesPredicate == null} or - * {@code wrapper.getServerCapabilities() == null} + * {@code capabilitiesPredicate}, or {@code false} if + * {@code capabilitiesPredicate == null} or + * {@code wrapper.getServerCapabilities() == null} */ private static boolean capabilitiesComply(LanguageServerWrapper wrapper, Predicate capabilitiesPredicate) { @@ -282,8 +290,6 @@ private static boolean capabilitiesComply(LanguageServerWrapper wrapper, } - - /** * TODO we need a similar method for generic IDocument (enabling non-IFiles) * @@ -296,7 +302,7 @@ private static boolean capabilitiesComply(LanguageServerWrapper wrapper, */ @Nonnull public Collection getLSWrappers(@Nonnull VirtualFile file, - @Nullable Predicate request) throws IOException { + @Nullable Predicate request) throws IOException { LinkedHashSet res = new LinkedHashSet<>(); Module project = LSPIJUtils.getProject(file); if (project == null) { @@ -350,7 +356,7 @@ private Collection getLSWrappers(@Nonnull Document docume res.addAll(startedServers.stream() .filter(wrapper -> { try { - return wrapper.isConnectedTo(path) || LanguageServersRegistry.getInstance().matches(document, wrapper.serverDefinition, project); + return wrapper.isEnabled() && (wrapper.isConnectedTo(path) || LanguageServersRegistry.getInstance().matches(document, wrapper.serverDefinition, project)); } catch (ProcessCanceledException cancellation) { throw cancellation; } catch (Exception e) { @@ -408,13 +414,17 @@ private Collection getLSWrappers(@Nonnull Document docume */ @Deprecated public LanguageServerWrapper getLSWrapperForConnection(@Nonnull Module project, - @Nonnull LanguageServersRegistry.LanguageServerDefinition serverDefinition) throws IOException { + @Nonnull LanguageServersRegistry.LanguageServerDefinition serverDefinition) throws IOException { return getLSWrapperForConnection(project, serverDefinition, null); } @Deprecated private LanguageServerWrapper getLSWrapperForConnection(@Nonnull Module project, - @Nonnull LanguageServersRegistry.LanguageServerDefinition serverDefinition, @Nullable URI initialPath) throws IOException { + @Nonnull LanguageServersRegistry.LanguageServerDefinition serverDefinition, @Nullable URI initialPath) throws IOException { + if (!serverDefinition.isEnabled()) { + // don't return a language server wrapper for the given server definition + return null; + } LanguageServerWrapper wrapper = null; synchronized (startedServers) { @@ -436,7 +446,11 @@ private LanguageServerWrapper getLSWrapperForConnection(@Nonnull Module project, } private LanguageServerWrapper getLSWrapperForConnection(Document document, - LanguageServersRegistry.LanguageServerDefinition serverDefinition, URI initialPath) throws IOException { + LanguageServersRegistry.LanguageServerDefinition serverDefinition, URI initialPath) throws IOException { + if (!serverDefinition.isEnabled()) { + // don't return a language server wrapper for the given server definition + return null; + } LanguageServerWrapper wrapper = null; synchronized (startedServers) { @@ -476,9 +490,8 @@ private List getStartedLSWrappers(Predicate getMatchingStartedWrappers(@Nonnull VirtualFile file, - @Nullable Predicate request) { + @Nullable Predicate request) { synchronized (startedServers) { return startedServers.stream().filter(wrapper -> wrapper.isConnectedTo(LSPIJUtils.toUri(file)) || (LanguageServersRegistry.getInstance().matches(file, wrapper.serverDefinition, project) @@ -509,7 +522,7 @@ public List getActiveLanguageServers(Predicate getLanguageServers(@Nonnull Module project, - Predicate request) { + Predicate request) { return getLanguageServers(project, request, false); } @@ -523,7 +536,7 @@ public List getLanguageServers(@Nonnull Module project, */ @Nonnull public List getLanguageServers(@Nullable Module project, - Predicate request, boolean onlyActiveLS) { + Predicate request, boolean onlyActiveLS) { List serverInfos = new ArrayList<>(); for (LanguageServerWrapper wrapper : startedServers) { if ((!onlyActiveLS || wrapper.isActive()) && (project == null || wrapper.canOperate(project))) { @@ -576,16 +589,17 @@ public CompletableFuture>> getL final List> res = Collections.synchronizedList(new ArrayList<>()); try { return CompletableFuture.allOf(getLSWrappers(document).stream().map(wrapper -> - wrapper.getInitializedServer().thenComposeAsync(server -> { - if (server != null && (filter == null || filter.test(wrapper.getServerCapabilities()))) { - try { - return wrapper.connect(document); - } catch (IOException ex) { - LOGGER.warn(ex.getLocalizedMessage(), ex); - } - } - return CompletableFuture.completedFuture(null); - }).thenAccept(server -> { + wrapper.getInitializedServer() + .thenComposeAsync(server -> { + if (server != null && wrapper.isEnabled() && (filter == null || filter.test(wrapper.getServerCapabilities()))) { + try { + return wrapper.connect(document); + } catch (IOException ex) { + LOGGER.warn(ex.getLocalizedMessage(), ex); + } + } + return CompletableFuture.completedFuture(null); + }).thenAccept(server -> { if (server != null) { res.add(new Pair(wrapper, server)); } diff --git a/src/main/java/com/redhat/devtools/intellij/lsp4ij/console/explorer/ServerStatus.java b/src/main/java/com/redhat/devtools/intellij/lsp4ij/ServerStatus.java similarity index 67% rename from src/main/java/com/redhat/devtools/intellij/lsp4ij/console/explorer/ServerStatus.java rename to src/main/java/com/redhat/devtools/intellij/lsp4ij/ServerStatus.java index 64e0eaa2e..6851f4f61 100644 --- a/src/main/java/com/redhat/devtools/intellij/lsp4ij/console/explorer/ServerStatus.java +++ b/src/main/java/com/redhat/devtools/intellij/lsp4ij/ServerStatus.java @@ -11,17 +11,17 @@ * Contributors: * Red Hat Inc. - initial API and implementation *******************************************************************************/ -package com.redhat.devtools.intellij.lsp4ij.console.explorer; +package com.redhat.devtools.intellij.lsp4ij; /** * Language server status. */ public enum ServerStatus { - startingProcess, - startedProcess, - starting, - started, - stopping, - stopped; + none, // initial status + starting, // The language server process is starting + started, // The language server is started without error + stopping, // The language server is stopping + stopped // The language server is stopped with or without error; + } diff --git a/src/main/java/com/redhat/devtools/intellij/lsp4ij/console/LSPConsoleToolWindowPanel.java b/src/main/java/com/redhat/devtools/intellij/lsp4ij/console/LSPConsoleToolWindowPanel.java index f6b2478b8..73083060f 100644 --- a/src/main/java/com/redhat/devtools/intellij/lsp4ij/console/LSPConsoleToolWindowPanel.java +++ b/src/main/java/com/redhat/devtools/intellij/lsp4ij/console/LSPConsoleToolWindowPanel.java @@ -37,6 +37,7 @@ import com.redhat.devtools.intellij.lsp4ij.console.explorer.LanguageServerTreeNode; import com.redhat.devtools.intellij.lsp4ij.settings.ServerTrace; import com.redhat.devtools.intellij.lsp4ij.settings.UserDefinedLanguageServerSettings; +import org.apache.commons.lang.exception.ExceptionUtils; import org.jetbrains.annotations.NotNull; import javax.swing.*; @@ -69,6 +70,7 @@ private void createUI() { super.setContent(splitPane); super.revalidate(); super.repaint(); + explorer.load(); } public Project getProject() { @@ -142,7 +144,7 @@ public ConsoleContentPanel(DefaultMutableTreeNode key) { add(createDetailPanel((LanguageServerTreeNode) key), NAME_VIEW_DETAIL); showDetail(); } else if (key instanceof LanguageServerProcessTreeNode) { - consoleView = createConsoleView(((LanguageServerProcessTreeNode)key).getLanguageServer().serverDefinition, project); + consoleView = createConsoleView(((LanguageServerProcessTreeNode) key).getLanguageServer().serverDefinition, project); JComponent consoleComponent = consoleView.getComponent(); Disposer.register(LSPConsoleToolWindowPanel.this, consoleView); add(consoleComponent, NAME_VIEW_CONSOLE); @@ -214,10 +216,15 @@ public void showMessage(String message) { consoleView.print(message, ConsoleViewContentType.SYSTEM_OUTPUT); } + public void showError(Throwable exception) { + String stacktrace = ExceptionUtils.getStackTrace(exception); + consoleView.print(stacktrace, ConsoleViewContentType.ERROR_OUTPUT); + } + @Override public void dispose() { super.dispose(); - if(consoleView != null) { + if (consoleView != null) { consoleView.dispose(); } } @@ -239,6 +246,16 @@ public void showMessage(LanguageServerProcessTreeNode processTreeNode, String me } } + public void showError(LanguageServerProcessTreeNode processTreeNode, Throwable exception) { + if (isDisposed()) { + return; + } + var consoleOrErrorPanel = consoles.getValue(processTreeNode, true); + if (consoleOrErrorPanel != null) { + consoleOrErrorPanel.showError(exception); + } + } + @Override public void dispose() { disposed = true; diff --git a/src/main/java/com/redhat/devtools/intellij/lsp4ij/console/actions/AutoFoldingAction.java b/src/main/java/com/redhat/devtools/intellij/lsp4ij/console/actions/AutoFoldingAction.java index 4b61148d5..f226e9eba 100644 --- a/src/main/java/com/redhat/devtools/intellij/lsp4ij/console/actions/AutoFoldingAction.java +++ b/src/main/java/com/redhat/devtools/intellij/lsp4ij/console/actions/AutoFoldingAction.java @@ -49,13 +49,12 @@ public boolean isSelected(@NotNull AnActionEvent e) { * Returns true if LSP traces from the console editor should be expanded and false otherwise. * * @param editor the console editor. - * * @return true if LSP traces from the console editor should be expanded and false otherwise. */ public static boolean shouldLSPTracesBeExpanded(Editor editor) { // Takes the last fold region and returns the expanded state FoldRegion[] allRegions = editor.getFoldingModel().getAllFoldRegions(); - FoldRegion lastRegion = allRegions.length > 0 ? allRegions[allRegions.length - 1] : null; + FoldRegion lastRegion = allRegions.length > 0 ? allRegions[allRegions.length - 1] : null; return lastRegion != null ? lastRegion.isExpanded() : false; } diff --git a/src/main/java/com/redhat/devtools/intellij/lsp4ij/console/actions/ClearThisConsoleAction.java b/src/main/java/com/redhat/devtools/intellij/lsp4ij/console/actions/ClearThisConsoleAction.java index eef14dd05..7740c90e2 100644 --- a/src/main/java/com/redhat/devtools/intellij/lsp4ij/console/actions/ClearThisConsoleAction.java +++ b/src/main/java/com/redhat/devtools/intellij/lsp4ij/console/actions/ClearThisConsoleAction.java @@ -27,17 +27,17 @@ public class ClearThisConsoleAction extends ClearConsoleAction { private final ConsoleView myConsoleView; public ClearThisConsoleAction(@NotNull ConsoleView consoleView) { - myConsoleView = consoleView; + myConsoleView = consoleView; } @Override public void update(@NotNull AnActionEvent e) { - boolean enabled = myConsoleView.getContentSize() > 0; - e.getPresentation().setEnabled(enabled); + boolean enabled = myConsoleView.getContentSize() > 0; + e.getPresentation().setEnabled(enabled); } @Override public void actionPerformed(@NotNull AnActionEvent e) { - myConsoleView.clear(); + myConsoleView.clear(); } - } \ No newline at end of file +} \ No newline at end of file diff --git a/src/main/java/com/redhat/devtools/intellij/lsp4ij/console/explorer/LanguageServerExplorer.java b/src/main/java/com/redhat/devtools/intellij/lsp4ij/console/explorer/LanguageServerExplorer.java index 45f98c554..45b6b1850 100644 --- a/src/main/java/com/redhat/devtools/intellij/lsp4ij/console/explorer/LanguageServerExplorer.java +++ b/src/main/java/com/redhat/devtools/intellij/lsp4ij/console/explorer/LanguageServerExplorer.java @@ -21,7 +21,9 @@ import com.intellij.ui.PopupHandler; import com.intellij.ui.treeStructure.Tree; import com.redhat.devtools.intellij.lsp4ij.LanguageServersRegistry; +import com.redhat.devtools.intellij.lsp4ij.LanguageServiceAccessor; import com.redhat.devtools.intellij.lsp4ij.console.LSPConsoleToolWindowPanel; +import com.redhat.devtools.intellij.lsp4ij.console.explorer.actions.CopyStartServerCommandAction; import com.redhat.devtools.intellij.lsp4ij.console.explorer.actions.RestartServerAction; import com.redhat.devtools.intellij.lsp4ij.console.explorer.actions.StopServerAction; import com.redhat.devtools.intellij.lsp4ij.lifecycle.LanguageServerLifecycleManager; @@ -102,6 +104,7 @@ private Tree buildTree() { .sorted(Comparator.comparing(LanguageServersRegistry.LanguageServerDefinition::getDisplayName)) .map(LanguageServerTreeNode::new) .forEach(top::add); + tree.setCellRenderer(new LanguageServerTreeRenderer()); tree.addTreeSelectionListener(treeSelectionListener); @@ -119,12 +122,14 @@ public void invokePopup(Component comp, int x, int y) { if (node instanceof LanguageServerProcessTreeNode) { LanguageServerProcessTreeNode processTreeNode = (LanguageServerProcessTreeNode) node; switch (processTreeNode.getServerStatus()) { + case starting: case started: // Stop language server action group = new DefaultActionGroup(); AnAction stopServerAction = ActionManager.getInstance().getAction(StopServerAction.ACTION_ID); group.add(stopServerAction); break; + case stopping: case stopped: // Restart language server action group = new DefaultActionGroup(); @@ -132,6 +137,11 @@ public void invokePopup(Component comp, int x, int y) { group.add(restartServerAction); break; } + if (group == null) { + group = new DefaultActionGroup(); + } + AnAction testStartServerAction = ActionManager.getInstance().getAction(CopyStartServerCommandAction.ACTION_ID); + group.add(testStartServerAction); } if (group != null) { @@ -167,6 +177,10 @@ public void showMessage(LanguageServerProcessTreeNode processTreeNode, String me panel.showMessage(processTreeNode, message); } + public void showError(LanguageServerProcessTreeNode processTreeNode, Throwable exception) { + panel.showError(processTreeNode, exception); + } + public DefaultTreeModel getTreeModel() { return (DefaultTreeModel) tree.getModel(); } @@ -182,4 +196,18 @@ public void selectAndExpand(DefaultMutableTreeNode treeNode) { public Project getProject() { return panel.getProject(); } + + /** + * Initialize language server process with the started language servers. + */ + public void load() { + LanguageServiceAccessor.getInstance(getProject()).getStartedServers() + .forEach(ls -> { + Throwable serverError = ls.getServerError(); + listener.handleStatusChanged(ls); + if (serverError != null) { + listener.handleError(ls, serverError); + } + }); + } } diff --git a/src/main/java/com/redhat/devtools/intellij/lsp4ij/console/explorer/LanguageServerExplorerLifecycleListener.java b/src/main/java/com/redhat/devtools/intellij/lsp4ij/console/explorer/LanguageServerExplorerLifecycleListener.java index a41715456..6426a4220 100644 --- a/src/main/java/com/redhat/devtools/intellij/lsp4ij/console/explorer/LanguageServerExplorerLifecycleListener.java +++ b/src/main/java/com/redhat/devtools/intellij/lsp4ij/console/explorer/LanguageServerExplorerLifecycleListener.java @@ -14,6 +14,7 @@ package com.redhat.devtools.intellij.lsp4ij.console.explorer; import com.intellij.openapi.application.ApplicationManager; +import com.redhat.devtools.intellij.lsp4ij.ServerStatus; import com.redhat.devtools.intellij.lsp4ij.settings.ServerTrace; import com.redhat.devtools.intellij.lsp4ij.LanguageServerWrapper; import com.redhat.devtools.intellij.lsp4ij.lifecycle.LanguageServerLifecycleListener; @@ -43,18 +44,10 @@ public LanguageServerExplorerLifecycleListener(LanguageServerExplorer explorer) } @Override - public void handleStartingProcess(LanguageServerWrapper languageServer) { - updateServerStatus(languageServer, ServerStatus.startingProcess, true); - } - - @Override - public void handleStartedProcess(LanguageServerWrapper languageServer, Throwable exception) { - updateServerStatus(languageServer, ServerStatus.startedProcess, false); - } - - @Override - public void handleStartedLanguageServer(LanguageServerWrapper languageServer, Throwable exception) { - updateServerStatus(languageServer, ServerStatus.started, false); + public void handleStatusChanged(LanguageServerWrapper languageServer) { + ServerStatus serverStatus = languageServer.getServerStatus(); + boolean selectProcess = serverStatus == ServerStatus.starting; + updateServerStatus(languageServer, serverStatus, selectProcess); } @Override @@ -73,15 +66,14 @@ public void handleLSPMessage(Message message, MessageConsumer messageConsumer, L invokeLater(() -> showMessage(processTreeNode, log)); } - @Override - public void handleStoppingLanguageServer(LanguageServerWrapper languageServer) { - updateServerStatus(languageServer, ServerStatus.stopping, false); - } + public void handleError(LanguageServerWrapper languageServer, Throwable exception) { + LanguageServerProcessTreeNode processTreeNode = updateServerStatus(languageServer, null, false); + if (exception == null) { + return; + } - @Override - public void handleStoppedLanguageServer(LanguageServerWrapper languageServer, Throwable exception) { - updateServerStatus(languageServer, ServerStatus.stopped, false); + invokeLater(() -> showError(processTreeNode, exception)); } private TracingMessageConsumer getLSPRequestCacheFor(LanguageServerWrapper languageServer) { @@ -134,12 +126,13 @@ private LanguageServerProcessTreeNode updateServerStatus(LanguageServerWrapper l processTreeNode = new LanguageServerProcessTreeNode(languageServer, treeModel); if (serverStatus == null) { // compute the server status - serverStatus = languageServer.isActive() ? ServerStatus.started : ServerStatus.stopped; + serverStatus = languageServer.getServerStatus(); } selectProcess = true; serverNode.add(processTreeNode); } boolean serverStatusChanged = serverStatus != null && serverStatus != processTreeNode.getServerStatus(); + processTreeNode.setServerStatus(serverStatus != null ? serverStatus : languageServer.getServerStatus()); boolean updateUI = serverStatusChanged || selectProcess; if (updateUI) { final var node = processTreeNode; @@ -167,6 +160,13 @@ private void showMessage(LanguageServerProcessTreeNode processTreeNode, String m explorer.showMessage(processTreeNode, message); } + private void showError(LanguageServerProcessTreeNode processTreeNode, Throwable exception) { + if (explorer.isDisposed()) { + return; + } + explorer.showError(processTreeNode, exception); + } + public boolean isDisposed() { return disposed; } diff --git a/src/main/java/com/redhat/devtools/intellij/lsp4ij/console/explorer/LanguageServerProcessTreeNode.java b/src/main/java/com/redhat/devtools/intellij/lsp4ij/console/explorer/LanguageServerProcessTreeNode.java index 27e60b861..5cf998133 100644 --- a/src/main/java/com/redhat/devtools/intellij/lsp4ij/console/explorer/LanguageServerProcessTreeNode.java +++ b/src/main/java/com/redhat/devtools/intellij/lsp4ij/console/explorer/LanguageServerProcessTreeNode.java @@ -17,6 +17,7 @@ import com.intellij.openapi.util.text.StringUtil; import com.intellij.ui.AnimatedIcon; import com.redhat.devtools.intellij.lsp4ij.LanguageServerWrapper; +import com.redhat.devtools.intellij.lsp4ij.ServerStatus; import javax.swing.*; import javax.swing.tree.DefaultMutableTreeNode; @@ -46,37 +47,45 @@ public LanguageServerProcessTreeNode(LanguageServerWrapper languageServer, Defau public void setServerStatus(ServerStatus serverStatus) { this.serverStatus = serverStatus; - switch(serverStatus) { - case startingProcess: - startTime = System.currentTimeMillis(); - displayName = "starting process..."; - break; - case startedProcess: - displayName = "process started"; - break; + displayName = getDisplayName(serverStatus); + switch (serverStatus) { case starting: - displayName = "starting..."; - break; - case started: - startTime = -1; - Long pid = languageServer.getCurrentProcessId(); - StringBuilder name = new StringBuilder("started"); - if (pid != null) { - name.append(" pid:"); - name.append(pid); - } - displayName = name.toString(); - break; case stopping: startTime = System.currentTimeMillis(); - displayName = "stopping..."; break; case stopped: + case started: startTime = -1; - displayName = "stopped"; break; } - treeModel.reload(this); + this.setUserObject(displayName); + treeModel.nodeChanged(this); + } + + private String getDisplayName(ServerStatus serverStatus) { + if (!languageServer.isEnabled()) { + return "disabled"; + } + Throwable serverError = languageServer.getServerError(); + StringBuilder name = new StringBuilder(); + if (serverError == null) { + name.append(serverStatus.name()); + } else { + name.append(serverStatus == ServerStatus.stopped ? "crashed" : serverStatus.name()); + int nbTryRestart = languageServer.getNumberOfRestartAttempts(); + int nbTryRestartMax = languageServer.getMaxNumberOfRestartAttempts(); + name.append(" ["); + name.append(nbTryRestart); + name.append("/"); + name.append(nbTryRestartMax); + name.append("]"); + } + Long pid = languageServer.getCurrentProcessId(); + if (pid != null) { + name.append(" pid:"); + name.append(pid); + } + return name.toString(); } public LanguageServerWrapper getLanguageServer() { @@ -88,11 +97,21 @@ public ServerStatus getServerStatus() { } public Icon getIcon() { - switch(serverStatus) { + if (!languageServer.isEnabled()) { + return AllIcons.RunConfigurations.TestFailed; + } + boolean hasError = languageServer.getServerError() != null; + switch (serverStatus) { case started: - return AllIcons.Debugger.ThreadRunning; + if (hasError) { + return AllIcons.RunConfigurations.TestFailed; + } + return AllIcons.Actions.Commit; case stopped: - return AllIcons.Debugger.ThreadSuspended; + if (hasError) { + return AllIcons.RunConfigurations.TestError; + } + return AllIcons.Actions.Suspend; default: return RUNNING_ICON; } diff --git a/src/main/java/com/redhat/devtools/intellij/lsp4ij/console/explorer/LanguageServerTreeNode.java b/src/main/java/com/redhat/devtools/intellij/lsp4ij/console/explorer/LanguageServerTreeNode.java index 0a2aa3264..ff49fff37 100644 --- a/src/main/java/com/redhat/devtools/intellij/lsp4ij/console/explorer/LanguageServerTreeNode.java +++ b/src/main/java/com/redhat/devtools/intellij/lsp4ij/console/explorer/LanguageServerTreeNode.java @@ -21,7 +21,7 @@ /** * Language server node. */ -public class LanguageServerTreeNode extends DefaultMutableTreeNode { +public class LanguageServerTreeNode extends DefaultMutableTreeNode { private final LanguageServersRegistry.LanguageServerDefinition serverDefinition; @@ -35,7 +35,7 @@ public LanguageServersRegistry.LanguageServerDefinition getServerDefinition() { public LanguageServerProcessTreeNode getActiveProcessTreeNode() { for (int i = 0; i < super.getChildCount(); i++) { - return (LanguageServerProcessTreeNode) super.getChildAt(i); + return (LanguageServerProcessTreeNode) super.getChildAt(i); } return null; } diff --git a/src/main/java/com/redhat/devtools/intellij/lsp4ij/console/explorer/LanguageServerTreeRenderer.java b/src/main/java/com/redhat/devtools/intellij/lsp4ij/console/explorer/LanguageServerTreeRenderer.java index bff5e0be9..4b3c31a06 100644 --- a/src/main/java/com/redhat/devtools/intellij/lsp4ij/console/explorer/LanguageServerTreeRenderer.java +++ b/src/main/java/com/redhat/devtools/intellij/lsp4ij/console/explorer/LanguageServerTreeRenderer.java @@ -18,6 +18,7 @@ import com.intellij.ui.RelativeFont; import com.intellij.ui.SimpleTextAttributes; import com.intellij.util.ui.UIUtil; +import com.redhat.devtools.intellij.lsp4ij.ServerStatus; import org.jetbrains.annotations.NonNls; import org.jetbrains.annotations.NotNull; @@ -65,12 +66,14 @@ public void customizeCellRenderer(@NotNull final JTree tree, setIcon(languageProcessTreeNode.getIcon()); append(languageProcessTreeNode.getDisplayName()); - if (languageProcessTreeNode.getServerStatus() != ServerStatus.started && languageProcessTreeNode.getServerStatus() != ServerStatus.stopped) { + if (languageProcessTreeNode.getServerStatus() == ServerStatus.starting + || languageProcessTreeNode.getServerStatus() == ServerStatus.stopping) { // Display elapsed time when language server is starting/stopping myDurationText = languageProcessTreeNode.getElapsedTime(); - if (myDurationText != null) { + final var durationText = myDurationText; + if (durationText != null) { FontMetrics metrics = getFontMetrics(RelativeFont.SMALL.derive(getFont())); - myDurationWidth = metrics.stringWidth(myDurationText); + myDurationWidth = metrics.stringWidth(durationText); myDurationOffset = metrics.getHeight() / 2; // an empty area before and after the text myDurationColor = selected ? UIUtil.getTreeSelectionForeground(hasFocus) : SimpleTextAttributes.GRAYED_ATTRIBUTES.getFgColor(); } diff --git a/src/main/java/com/redhat/devtools/intellij/lsp4ij/console/explorer/TracingMessageConsumer.java b/src/main/java/com/redhat/devtools/intellij/lsp4ij/console/explorer/TracingMessageConsumer.java index b131d7491..5b5bd7f13 100644 --- a/src/main/java/com/redhat/devtools/intellij/lsp4ij/console/explorer/TracingMessageConsumer.java +++ b/src/main/java/com/redhat/devtools/intellij/lsp4ij/console/explorer/TracingMessageConsumer.java @@ -33,7 +33,7 @@ /** * Outputs logs in a format that can be parsed by the LSP Inspector. * https://microsoft.github.io/language-server-protocol/inspector/ - * + *

* This class is a copy/paste of https://github.com/eclipse-lsp4j/lsp4j/blob/main/org.eclipse.lsp4j.jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/TracingMessageConsumer.java * adapted for IJ. */ @@ -171,7 +171,7 @@ private static String getResultTrace(String resultJson, String errorJson) { } else { result.append("No result returned."); } - if(errorJson != null && !"null".equals(errorJson)) { + if (errorJson != null && !"null".equals(errorJson)) { result.append("\nError: "); result.append(errorJson); } diff --git a/src/main/java/com/redhat/devtools/intellij/lsp4ij/console/explorer/actions/CopyStartServerCommandAction.java b/src/main/java/com/redhat/devtools/intellij/lsp4ij/console/explorer/actions/CopyStartServerCommandAction.java new file mode 100644 index 000000000..fc4024a50 --- /dev/null +++ b/src/main/java/com/redhat/devtools/intellij/lsp4ij/console/explorer/actions/CopyStartServerCommandAction.java @@ -0,0 +1,69 @@ +/******************************************************************************* + * 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.lsp4ij.console.explorer.actions; + +import com.intellij.openapi.actionSystem.AnActionEvent; +import com.intellij.openapi.project.DumbAware; +import com.intellij.ui.treeStructure.Tree; +import com.redhat.devtools.intellij.lsp4ij.LanguageServerBundle; +import com.redhat.devtools.intellij.lsp4ij.LanguageServerWrapper; +import org.jetbrains.annotations.NotNull; + +import java.awt.*; +import java.awt.datatransfer.Clipboard; +import java.awt.datatransfer.StringSelection; +import java.util.List; +import java.util.stream.Collectors; + +/** + * Copy in the clipboard the command which starts the selected language server process from the language explorer. + *

+ * This action can be helpful to understand some problem with start of the language server + */ +public class CopyStartServerCommandAction extends TreeAction implements DumbAware { + + public static final String ACTION_ID = "com.redhat.devtools.intellij.lsp4ij.console.explorer.actions.CopyStartServerCommandAction"; + + public CopyStartServerCommandAction() { + super(LanguageServerBundle.message("lsp.console.explorer.actions.copy.command")); + } + + @Override + protected void actionPerformed(@NotNull Tree tree, @NotNull AnActionEvent e) { + LanguageServerWrapper languageServer = getSelectedLanguageServer(tree); + if (languageServer != null) { + + List commands = languageServer.getCurrentProcessCommandLine(); + if (commands == null) { + return; + } + + String text = commands + .stream() + .map(param -> { + if (param.indexOf(' ') != -1) { + return "\"" + param + "\""; + } + return param; + }) + .collect(Collectors.joining(" ")); + + Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard(); + StringSelection selection = new StringSelection(text); + clipboard.setContents(selection, null); + + } + } + +} diff --git a/src/main/java/com/redhat/devtools/intellij/lsp4ij/console/explorer/actions/RestartServerAction.java b/src/main/java/com/redhat/devtools/intellij/lsp4ij/console/explorer/actions/RestartServerAction.java index 3912ccf8f..626ca611e 100644 --- a/src/main/java/com/redhat/devtools/intellij/lsp4ij/console/explorer/actions/RestartServerAction.java +++ b/src/main/java/com/redhat/devtools/intellij/lsp4ij/console/explorer/actions/RestartServerAction.java @@ -42,7 +42,7 @@ protected void actionPerformed(@NotNull Tree tree, @NotNull AnActionEvent e) { LanguageServerWrapper languageServer = getSelectedLanguageServer(tree); if (languageServer != null) { try { - languageServer.start(); + languageServer.restart(); } catch (IOException ex) { LOGGER.error("Failed restarting server", ex); } diff --git a/src/main/java/com/redhat/devtools/intellij/lsp4ij/lifecycle/LanguageServerLifecycleListener.java b/src/main/java/com/redhat/devtools/intellij/lsp4ij/lifecycle/LanguageServerLifecycleListener.java index a9814db30..391b545f4 100644 --- a/src/main/java/com/redhat/devtools/intellij/lsp4ij/lifecycle/LanguageServerLifecycleListener.java +++ b/src/main/java/com/redhat/devtools/intellij/lsp4ij/lifecycle/LanguageServerLifecycleListener.java @@ -37,17 +37,11 @@ */ public interface LanguageServerLifecycleListener { - void handleStartingProcess(LanguageServerWrapper languageServer); - - void handleStartedProcess(LanguageServerWrapper languageServer, Throwable exception); - - void handleStartedLanguageServer(LanguageServerWrapper languageServer, Throwable exception); + void handleStatusChanged(LanguageServerWrapper languageServer); void handleLSPMessage(Message message, MessageConsumer consumer, LanguageServerWrapper languageServer); - void handleStoppingLanguageServer(LanguageServerWrapper languageServer); - - void handleStoppedLanguageServer(LanguageServerWrapper languageServer, Throwable exception); + void handleError(LanguageServerWrapper languageServer, Throwable exception); void dispose(); diff --git a/src/main/java/com/redhat/devtools/intellij/lsp4ij/lifecycle/LanguageServerLifecycleManager.java b/src/main/java/com/redhat/devtools/intellij/lsp4ij/lifecycle/LanguageServerLifecycleManager.java index e09de4f64..32b532747 100644 --- a/src/main/java/com/redhat/devtools/intellij/lsp4ij/lifecycle/LanguageServerLifecycleManager.java +++ b/src/main/java/com/redhat/devtools/intellij/lsp4ij/lifecycle/LanguageServerLifecycleManager.java @@ -55,41 +55,15 @@ public void removeLanguageServerLifecycleListener(LanguageServerLifecycleListene this.listeners.remove(listener); } - public void onStartingProcess(LanguageServerWrapper languageServer) { + public void onStatusChanged(LanguageServerWrapper languageServer) { if (isDisposed()) { return; } for (LanguageServerLifecycleListener listener : this.listeners) { try { - listener.handleStartingProcess(languageServer); + listener.handleStatusChanged(languageServer); } catch (Exception e) { - LOGGER.error("Error while handling starting process of the language server '" + languageServer.serverDefinition.id + "'", e); - } - } - } - - public void onStartedProcess(LanguageServerWrapper languageServer, Exception exception) { - if (isDisposed()) { - return; - } - for (LanguageServerLifecycleListener listener : this.listeners) { - try { - listener.handleStartedProcess(languageServer, exception); - } catch (Exception e) { - LOGGER.error("Error while handling started process of the language server '" + languageServer.serverDefinition.id + "'", e); - } - } - } - - public void onStartedLanguageServer(LanguageServerWrapper languageServer, Throwable exception) { - if (isDisposed()) { - return; - } - for (LanguageServerLifecycleListener listener : this.listeners) { - try { - listener.handleStartedLanguageServer(languageServer, exception); - } catch (Exception e) { - LOGGER.error("Error while handling started the language server '" + languageServer.serverDefinition.id + "'", e); + LOGGER.error("Error while status changed of the language server '" + languageServer.serverDefinition.id + "'", e); } } } @@ -107,33 +81,18 @@ public void logLSPMessage(Message message, MessageConsumer consumer, LanguageSer } } - public void onStoppingLanguageServer(LanguageServerWrapper languageServer) { + public void onError(LanguageServerWrapper languageServer, Throwable exception) { if (isDisposed()) { return; } for (LanguageServerLifecycleListener listener : this.listeners) { try { - listener.handleStoppingLanguageServer(languageServer); + listener.handleError(languageServer, exception); } catch (Exception e) { - LOGGER.error("Error while handling stopping the language server '" + languageServer.serverDefinition.id + "'", e); + LOGGER.error("Error while handling error of the language server '" + languageServer.serverDefinition.id + "'", e); } } - } - - public void onStoppedLanguageServer(LanguageServerWrapper languageServer, Exception exception) { - if (isDisposed()) { - return; - } - for (LanguageServerLifecycleListener listener : this.listeners) { - try { - listener.handleStoppedLanguageServer(languageServer, exception); - } catch (Exception e) { - LOGGER.error("Error while handling stopped the language server '" + languageServer.serverDefinition.id + "'", e); - } - } - } - public boolean isDisposed() { return disposed; } @@ -143,4 +102,6 @@ public void dispose() { listeners.stream().forEach(LanguageServerLifecycleListener::dispose); listeners.clear(); } + + } diff --git a/src/main/java/com/redhat/devtools/intellij/lsp4ij/server/CannotStartProcessException.java b/src/main/java/com/redhat/devtools/intellij/lsp4ij/server/CannotStartProcessException.java new file mode 100644 index 000000000..d32adb420 --- /dev/null +++ b/src/main/java/com/redhat/devtools/intellij/lsp4ij/server/CannotStartProcessException.java @@ -0,0 +1,31 @@ +/******************************************************************************* + * 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.lsp4ij.server; + +import java.io.IOException; + +/** + * Language server exception when start process cannot be done. + */ +public class CannotStartProcessException extends LanguageServerException { + + public CannotStartProcessException(IOException e) { + super(e); + } + + public CannotStartProcessException(String message) { + super(message); + } + +} diff --git a/src/main/java/com/redhat/devtools/intellij/lsp4ij/server/CannotStartServerException.java b/src/main/java/com/redhat/devtools/intellij/lsp4ij/server/CannotStartServerException.java new file mode 100644 index 000000000..284f45f33 --- /dev/null +++ b/src/main/java/com/redhat/devtools/intellij/lsp4ij/server/CannotStartServerException.java @@ -0,0 +1,24 @@ +/******************************************************************************* + * 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.lsp4ij.server; + +/** + * Language server exception when language server cannot be done. + */ +public class CannotStartServerException extends LanguageServerException { + + public CannotStartServerException(String message, Throwable e) { + super(message, e); + } +} diff --git a/src/main/java/com/redhat/devtools/intellij/lsp4ij/server/JavaProcessCommandBuilder.java b/src/main/java/com/redhat/devtools/intellij/lsp4ij/server/JavaProcessCommandBuilder.java index f0602e849..014207d39 100644 --- a/src/main/java/com/redhat/devtools/intellij/lsp4ij/server/JavaProcessCommandBuilder.java +++ b/src/main/java/com/redhat/devtools/intellij/lsp4ij/server/JavaProcessCommandBuilder.java @@ -13,12 +13,8 @@ import com.redhat.devtools.intellij.lsp4ij.settings.UserDefinedLanguageServerSettings; import java.io.File; -import java.nio.file.Files; -import java.nio.file.Paths; import java.util.ArrayList; import java.util.List; -import java.util.regex.Pattern; -import java.util.stream.Stream; /** * A builder to create Java process command. @@ -78,11 +74,11 @@ public List create() { String suspend = debugSuspend ? "y" : "n"; commands.add("-agentlib:jdwp=transport=dt_socket,server=y,suspend=" + suspend + ",address=" + debugPort); } - if(jar != null) { + if (jar != null) { commands.add("-jar"); commands.add(jar); } - if(cp != null) { + if (cp != null) { commands.add("-cp"); commands.add(cp); } diff --git a/src/main/java/com/redhat/devtools/intellij/lsp4ij/server/LanguageServerException.java b/src/main/java/com/redhat/devtools/intellij/lsp4ij/server/LanguageServerException.java new file mode 100644 index 000000000..8ec1ca4a5 --- /dev/null +++ b/src/main/java/com/redhat/devtools/intellij/lsp4ij/server/LanguageServerException.java @@ -0,0 +1,32 @@ +/******************************************************************************* + * Copyright (c) 2023 Red Hat Inc. and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 + * which is available at https://www.apache.org/licenses/LICENSE-2.0. + * + * SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 + * + * Contributors: + * Red Hat Inc. - initial API and implementation + *******************************************************************************/ +package com.redhat.devtools.intellij.lsp4ij.server; + +/** + * Base class for language server exception. + */ +public class LanguageServerException extends RuntimeException { + + public LanguageServerException(String message, Throwable e) { + super(message, e); + } + + public LanguageServerException(Throwable e) { + super(e); + } + + public LanguageServerException(String message) { + super(message); + } +} diff --git a/src/main/java/com/redhat/devtools/intellij/lsp4ij/server/OS.java b/src/main/java/com/redhat/devtools/intellij/lsp4ij/server/OS.java index 465d85470..f106595cf 100644 --- a/src/main/java/com/redhat/devtools/intellij/lsp4ij/server/OS.java +++ b/src/main/java/com/redhat/devtools/intellij/lsp4ij/server/OS.java @@ -4,7 +4,7 @@ /** * Enumerated type for operating systems. - * + *

* Copied from https://github.com/smallrye/smallrye-common/blob/main/os/src/main/java/io/smallrye/common/os/OS.java */ public enum OS { @@ -68,7 +68,7 @@ static OS parse(String osName) { /** * @return {@code true} if this {@code OS} is known to be the - * operating system on which the current JVM is executing + * operating system on which the current JVM is executing */ public boolean isCurrent() { return this == CURRENT_OS; diff --git a/src/main/java/com/redhat/devtools/intellij/lsp4ij/server/ProcessStreamConnectionProvider.java b/src/main/java/com/redhat/devtools/intellij/lsp4ij/server/ProcessStreamConnectionProvider.java index 08a8a20d2..b344fbb60 100644 --- a/src/main/java/com/redhat/devtools/intellij/lsp4ij/server/ProcessStreamConnectionProvider.java +++ b/src/main/java/com/redhat/devtools/intellij/lsp4ij/server/ProcessStreamConnectionProvider.java @@ -8,10 +8,12 @@ import java.util.List; import java.util.Objects; -public abstract class ProcessStreamConnectionProvider implements StreamConnectionProvider{ - private @Nullable Process process; +public abstract class ProcessStreamConnectionProvider implements StreamConnectionProvider { + private @Nullable + Process process; private List commands; - private @Nullable String workingDir; + private @Nullable + String workingDir; public ProcessStreamConnectionProvider() { } @@ -26,16 +28,36 @@ public ProcessStreamConnectionProvider(List commands, String workingDir) } @Override - public void start() throws IOException { + public void start() throws CannotStartProcessException { if (this.commands == null || this.commands.isEmpty() || this.commands.stream().anyMatch(Objects::isNull)) { - throw new IOException("Unable to start language server: " + this.toString()); //$NON-NLS-1$ + throw new CannotStartProcessException("Unable to start language server: " + this.toString()); //$NON-NLS-1$ } - ProcessBuilder builder = createProcessBuilder(); - Process p = builder.start(); - this.process = p; - if (!p.isAlive()) { - throw new IOException("Unable to start language server: " + this.toString()); //$NON-NLS-1$ + try { + Process p = builder.start(); + this.process = p; + } catch (IOException e) { + throw new CannotStartProcessException(e); + } + } + + @Override + public boolean isAlive() { + return process != null ? process.isAlive() : false; + } + + @Override + public void ensureIsAlive() throws CannotStartProcessException { + // Wait few ms before checking the is alive flag. + synchronized (this.process) { + try { + this.process.wait(200); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + if (!isAlive()) { + throw new CannotStartProcessException("Unable to start language server: " + this.toString()); //$NON-NLS-1$ } } @@ -49,24 +71,28 @@ protected ProcessBuilder createProcessBuilder() { } @Override - public @Nullable InputStream getInputStream() { + public @Nullable + InputStream getInputStream() { Process p = process; return p == null ? null : p.getInputStream(); } @Override - public @Nullable InputStream getErrorStream() { + public @Nullable + InputStream getErrorStream() { Process p = process; return p == null ? null : p.getErrorStream(); } @Override - public @Nullable OutputStream getOutputStream() { + public @Nullable + OutputStream getOutputStream() { Process p = process; return p == null ? null : p.getOutputStream(); } - public @Nullable Long getPid() { + public @Nullable + Long getPid() { final Process p = process; return p == null ? null : p.pid(); } @@ -76,10 +102,11 @@ public void stop() { Process p = process; if (p != null) { p.destroy(); + process = null; } } - protected List getCommands() { + public List getCommands() { return commands; } @@ -87,7 +114,8 @@ public void setCommands(List commands) { this.commands = commands; } - protected @Nullable String getWorkingDirectory() { + protected @Nullable + String getWorkingDirectory() { return workingDir; } diff --git a/src/main/java/com/redhat/devtools/intellij/lsp4ij/server/StreamConnectionProvider.java b/src/main/java/com/redhat/devtools/intellij/lsp4ij/server/StreamConnectionProvider.java index 8b45ec6d4..c9889883c 100644 --- a/src/main/java/com/redhat/devtools/intellij/lsp4ij/server/StreamConnectionProvider.java +++ b/src/main/java/com/redhat/devtools/intellij/lsp4ij/server/StreamConnectionProvider.java @@ -4,18 +4,17 @@ import org.eclipse.lsp4j.services.LanguageServer; import javax.annotation.Nullable; -import java.io.FilterInputStream; -import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.URI; public interface StreamConnectionProvider { - public void start() throws IOException; - public InputStream getInputStream(); + void start() throws CannotStartProcessException; - public OutputStream getOutputStream(); + InputStream getInputStream(); + + OutputStream getOutputStream(); /** * Returns the {@link InputStream} connected to the error output of the process @@ -23,74 +22,30 @@ public interface StreamConnectionProvider { * output it returns null. * * @return the {@link InputStream} connected to the error output of the language - * server process or null if it's redirected or process not - * yet started. + * server process or null if it's redirected or process not + * yet started. */ - public @Nullable InputStream getErrorStream(); - - /** - * Forwards a copy of an {@link InputStream} to an {@link OutputStream}. - * - * @param input - * the {@link InputStream} that will be copied - * @param output - * the {@link OutputStream} to forward the copy to - * @return a newly created {@link InputStream} that copies all data to the - * provided {@link OutputStream} - */ - public default InputStream forwardCopyTo(InputStream input, OutputStream output) { - if (input == null) - return null; - if (output == null) - return input; - - FilterInputStream filterInput = new FilterInputStream(input) { - @Override - public int read() throws IOException { - int res = super.read(); - System.err.print((char) res); - return res; - } - - @Override - public int read(byte[] b, int off, int len) throws IOException { - int bytes = super.read(b, off, len); - byte[] payload = new byte[bytes]; - System.arraycopy(b, off, payload, 0, bytes); - output.write(payload, 0, payload.length); - return bytes; - } - - @Override - public int read(byte[] b) throws IOException { - int bytes = super.read(b); - byte[] payload = new byte[bytes]; - System.arraycopy(b, 0, payload, 0, bytes); - output.write(payload, 0, payload.length); - return bytes; - } - }; - - return filterInput; - } + @Nullable + InputStream getErrorStream(); /** * User provided initialization options. */ - public default Object getInitializationOptions(URI rootUri){ + default Object getInitializationOptions(URI rootUri) { return null; } /** * Returns an object that describes the experimental features supported * by the client. + * + * @return an object whose fields represent the different experimental features + * supported by the client. * @implNote The returned object gets serialized by LSP4J, which itself uses - * GSon, so a GSon object can work too. + * GSon, so a GSon object can work too. * @since 0.12 - * @return an object whose fields represent the different experimental features - * supported by the client. */ - public default Object getExperimentalFeaturesPOJO() { + default Object getExperimentalFeaturesPOJO() { return null; } @@ -98,25 +53,44 @@ public default Object getExperimentalFeaturesPOJO() { * Provides trace level to be set on language server initialization.
* Legal values: "off" | "messages" | "verbose". * - * @param rootUri - * the workspace root URI. - * + * @param rootUri the workspace root URI. * @return the initial trace level to set * @see "https://microsoft.github.io/language-server-protocol/specification#initialize" */ - public default String getTrace(URI rootUri) { + default String getTrace(URI rootUri) { return "off"; //$NON-NLS-1$ } - public void stop(); + void stop(); /** * Allows to hook custom behavior on messages. - * @param message a message + * + * @param message a message. * @param languageServer the language server receiving/sending the message. - * @param rootUri + * @param rootUri the root Uri. + */ + default void handleMessage(Message message, LanguageServer languageServer, URI rootUri) { + } + + /** + * Returns true if the connection provider is alive and false otherwise. + * + * @return true if the connection provider is alive and false otherwise. */ - public default void handleMessage(Message message, LanguageServer languageServer, URI rootURI) {} + default boolean isAlive() { + return true; + } + /** + * Ensure that process is alive. + * + * @throws CannotStartProcessException if process is not alive. + */ + default void ensureIsAlive() throws CannotStartProcessException { + if (!isAlive()) { + throw new CannotStartProcessException("Unable to start language server: " + this.toString()); //$NON-NLS-1$ + } + } } diff --git a/src/main/resources/META-INF/lsp4ij.xml b/src/main/resources/META-INF/lsp4ij.xml index be532ea08..abc3d87e8 100644 --- a/src/main/resources/META-INF/lsp4ij.xml +++ b/src/main/resources/META-INF/lsp4ij.xml @@ -33,6 +33,9 @@ + diff --git a/src/main/resources/messages/LanguageServerBundle.properties b/src/main/resources/messages/LanguageServerBundle.properties index 5bc6bf19d..69de6f5b8 100644 --- a/src/main/resources/messages/LanguageServerBundle.properties +++ b/src/main/resources/messages/LanguageServerBundle.properties @@ -21,6 +21,7 @@ language.server.trace=Trace: lsp.console.title=LSP Consoles lsp.console.explorer.actions.restart=Restart lsp.console.explorer.actions.stop=Stop +lsp.console.explorer.actions.copy.command=Copy Start Command lsp.console.actions.folding=Collapse/Expand All ## Dialog