From f1e1748377c1ef4696cc807b4373606650e409f5 Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Wed, 22 Apr 2026 10:11:44 +0200 Subject: [PATCH 001/107] Boilerplate for LSP router. --- .../lsp/parametric/LanguageServerRouter.java | 283 ++++++++++++++++++ 1 file changed, 283 insertions(+) create mode 100644 rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/LanguageServerRouter.java diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/LanguageServerRouter.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/LanguageServerRouter.java new file mode 100644 index 000000000..de620ac93 --- /dev/null +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/LanguageServerRouter.java @@ -0,0 +1,283 @@ +/* + * Copyright (c) 2018-2025, NWO-I CWI and Swat.engineering + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +package org.rascalmpl.vscode.lsp.parametric; + +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutorService; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.eclipse.lsp4j.ClientCapabilities; +import org.eclipse.lsp4j.CreateFilesParams; +import org.eclipse.lsp4j.DeleteFilesParams; +import org.eclipse.lsp4j.DidChangeTextDocumentParams; +import org.eclipse.lsp4j.DidCloseTextDocumentParams; +import org.eclipse.lsp4j.DidOpenTextDocumentParams; +import org.eclipse.lsp4j.DidSaveTextDocumentParams; +import org.eclipse.lsp4j.InitializeParams; +import org.eclipse.lsp4j.InitializeResult; +import org.eclipse.lsp4j.RenameFilesParams; +import org.eclipse.lsp4j.ServerCapabilities; +import org.eclipse.lsp4j.WorkspaceFolder; +import org.eclipse.lsp4j.services.LanguageClient; +import org.eclipse.lsp4j.services.LanguageClientAware; +import org.eclipse.lsp4j.services.TextDocumentService; +import org.eclipse.lsp4j.services.WorkspaceService; +import org.rascalmpl.util.locations.ColumnMaps; +import org.rascalmpl.util.locations.LineColumnOffsetMap; +import org.rascalmpl.vscode.lsp.BaseWorkspaceService; +import org.rascalmpl.vscode.lsp.IBaseLanguageServerExtensions; +import org.rascalmpl.vscode.lsp.IBaseTextDocumentService; +import org.rascalmpl.vscode.lsp.TextDocumentState; +import org.rascalmpl.vscode.lsp.parametric.LanguageRegistry.LanguageParameter; +import org.rascalmpl.vscode.lsp.uri.jsonrpc.messages.VFSRegister; + +import io.usethesource.vallang.ISourceLocation; +import io.usethesource.vallang.IValue; + +public class LanguageServerRouter implements IBaseLanguageServerExtensions, LanguageClientAware { + + private final Runnable onExit; + private final ExecutorService exec; + private final IBaseTextDocumentService docService; + private final BaseWorkspaceService wsService; + + public LanguageServerRouter(Runnable onExit, ExecutorService exec) { + this.onExit = onExit; + this.exec = exec; + this.docService = new RoutingTextDocumentService(); + this.wsService = new RoutingWorkspaceService(exec, docService); + docService.pair(wsService); + } + + @Override + public void connect(LanguageClient client) { + // TODO Auto-generated method stub + throw new UnsupportedOperationException("Unimplemented method 'connect'"); + } + + private class RoutingTextDocumentService implements IBaseTextDocumentService { + + @Override + public void didOpen(DidOpenTextDocumentParams params) { + // TODO Auto-generated method stub + throw new UnsupportedOperationException("Unimplemented method 'didOpen'"); + } + + @Override + public void didChange(DidChangeTextDocumentParams params) { + // TODO Auto-generated method stub + throw new UnsupportedOperationException("Unimplemented method 'didChange'"); + } + + @Override + public void didClose(DidCloseTextDocumentParams params) { + // TODO Auto-generated method stub + throw new UnsupportedOperationException("Unimplemented method 'didClose'"); + } + + @Override + public void didSave(DidSaveTextDocumentParams params) { + // TODO Auto-generated method stub + throw new UnsupportedOperationException("Unimplemented method 'didSave'"); + } + + @Override + public void initializeServerCapabilities(ClientCapabilities clientCapabilities, ServerCapabilities result) { + // TODO Auto-generated method stub + throw new UnsupportedOperationException("Unimplemented method 'initializeServerCapabilities'"); + } + + @Override + public void shutdown() { + // TODO Auto-generated method stub + throw new UnsupportedOperationException("Unimplemented method 'shutdown'"); + } + + @Override + public void connect(LanguageClient client) { + // TODO Auto-generated method stub + throw new UnsupportedOperationException("Unimplemented method 'connect'"); + } + + @Override + public void pair(BaseWorkspaceService workspaceService) { + // TODO Auto-generated method stub + throw new UnsupportedOperationException("Unimplemented method 'pair'"); + } + + @Override + public void initialized() { + // TODO Auto-generated method stub + throw new UnsupportedOperationException("Unimplemented method 'initialized'"); + } + + @Override + public void registerLanguage(LanguageParameter lang) { + // TODO Auto-generated method stub + throw new UnsupportedOperationException("Unimplemented method 'registerLanguage'"); + } + + @Override + public void unregisterLanguage(LanguageParameter lang) { + // TODO Auto-generated method stub + throw new UnsupportedOperationException("Unimplemented method 'unregisterLanguage'"); + } + + @Override + public void projectAdded(String name, ISourceLocation projectRoot) { + // TODO Auto-generated method stub + throw new UnsupportedOperationException("Unimplemented method 'projectAdded'"); + } + + @Override + public void projectRemoved(String name, ISourceLocation projectRoot) { + // TODO Auto-generated method stub + throw new UnsupportedOperationException("Unimplemented method 'projectRemoved'"); + } + + @Override + public CompletableFuture executeCommand(String languageName, String command) { + // TODO Auto-generated method stub + throw new UnsupportedOperationException("Unimplemented method 'executeCommand'"); + } + + @Override + public LineColumnOffsetMap getColumnMap(ISourceLocation file) { + // TODO Auto-generated method stub + throw new UnsupportedOperationException("Unimplemented method 'getColumnMap'"); + } + + @Override + public ColumnMaps getColumnMaps() { + // TODO Auto-generated method stub + throw new UnsupportedOperationException("Unimplemented method 'getColumnMaps'"); + } + + @Override + public @Nullable TextDocumentState getDocumentState(ISourceLocation file) { + // TODO Auto-generated method stub + throw new UnsupportedOperationException("Unimplemented method 'getDocumentState'"); + } + + @Override + public boolean isManagingFile(ISourceLocation file) { + // TODO Auto-generated method stub + throw new UnsupportedOperationException("Unimplemented method 'isManagingFile'"); + } + + @Override + public void didCreateFiles(CreateFilesParams params) { + // TODO Auto-generated method stub + throw new UnsupportedOperationException("Unimplemented method 'didCreateFiles'"); + } + + @Override + public void didRenameFiles(RenameFilesParams params, List workspaceFolders) { + // TODO Auto-generated method stub + throw new UnsupportedOperationException("Unimplemented method 'didRenameFiles'"); + } + + @Override + public void didDeleteFiles(DeleteFilesParams params) { + // TODO Auto-generated method stub + throw new UnsupportedOperationException("Unimplemented method 'didDeleteFiles'"); + } + + @Override + public void cancelProgress(String progressId) { + // TODO Auto-generated method stub + throw new UnsupportedOperationException("Unimplemented method 'cancelProgress'"); + } + + } + + private class RoutingWorkspaceService extends BaseWorkspaceService { + + protected RoutingWorkspaceService(ExecutorService exec, IBaseTextDocumentService documentService) { + super(exec, documentService); + } + + } + + @Override + public CompletableFuture initialize(InitializeParams params) { + // TODO Auto-generated method stub + throw new UnsupportedOperationException("Unimplemented method 'initialize'"); + } + + @Override + public CompletableFuture shutdown() { + // TODO Auto-generated method stub + throw new UnsupportedOperationException("Unimplemented method 'shutdown'"); + } + + @Override + public void exit() { + // TODO Auto-generated method stub + throw new UnsupportedOperationException("Unimplemented method 'exit'"); + } + + @Override + public TextDocumentService getTextDocumentService() { + // TODO Auto-generated method stub + throw new UnsupportedOperationException("Unimplemented method 'getTextDocumentService'"); + } + + @Override + public WorkspaceService getWorkspaceService() { + // TODO Auto-generated method stub + throw new UnsupportedOperationException("Unimplemented method 'getWorkspaceService'"); + } + + @Override + public void registerVFS(VFSRegister registration) { + // TODO Auto-generated method stub + throw new UnsupportedOperationException("Unimplemented method 'registerVFS'"); + } + + @Override + public void setMinimumLogLevel(String level) { + // TODO Auto-generated method stub + throw new UnsupportedOperationException("Unimplemented method 'setMinimumLogLevel'"); + } + // public static void main(String[] args) { + // LanguageParameter dedicatedLanguage; + // if (args.length > 0) { + // dedicatedLanguage = new GsonBuilder().create().fromJson(args[0], LanguageParameter.class); + // } + // else { + // dedicatedLanguage = null; + // } + + // startLanguageServer(NamedThreadPool.single("parametric-lsp") + // , NamedThreadPool.cached("parametric") + // , threadPool -> new ParametricTextDocumentService(threadPool, dedicatedLanguage) + // , ParametricWorkspaceService::new + // , 9999 + // ); + // } +} From d8387236b585102e4ef9bae811b3362240f473f3 Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Wed, 22 Apr 2026 11:03:20 +0200 Subject: [PATCH 002/107] Symmetric pairing of services. --- .../vscode/lsp/BaseLanguageServer.java | 9 +++--- .../vscode/lsp/BaseWorkspaceService.java | 30 ++++++++++++------- .../lsp/parametric/LanguageServerRouter.java | 7 +++-- .../ParametricWorkspaceService.java | 5 ++-- .../lsp/rascal/RascalWorkspaceService.java | 5 ++-- 5 files changed, 33 insertions(+), 23 deletions(-) diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/BaseLanguageServer.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/BaseLanguageServer.java index 60890c16f..4dfcd9afd 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/BaseLanguageServer.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/BaseLanguageServer.java @@ -41,7 +41,6 @@ import java.util.concurrent.CompletionException; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; -import java.util.function.BiFunction; import java.util.function.Function; import org.apache.logging.log4j.Level; import org.apache.logging.log4j.LogManager; @@ -131,14 +130,15 @@ private static void printClassPath() { } @SuppressWarnings({"java:S2189", "java:S106"}) - public static void startLanguageServer(ExecutorService requestPool, ExecutorService workerPool, Function docServiceProvider, BiFunction workspaceServiceProvider, int portNumber) { + public static void startLanguageServer(ExecutorService requestPool, ExecutorService workerPool, Function docServiceProvider, Function workspaceServiceProvider, int portNumber) { logger.info("Starting Rascal Language Server: {}", getVersion()); printClassPath(); if (DEPLOY_MODE) { var docService = docServiceProvider.apply(workerPool); - var wsService = workspaceServiceProvider.apply(workerPool, docService); + var wsService = workspaceServiceProvider.apply(workerPool); docService.pair(wsService); + wsService.pair(docService); startLSP(constructLSPClient(capturedIn, capturedOut, new ActualLanguageServer(() -> System.exit(0), workerPool, docService, wsService), requestPool)); } else { @@ -146,8 +146,9 @@ public static void startLanguageServer(ExecutorService requestPool, ExecutorServ logger.info("Rascal LSP server listens on port number: {}", portNumber); while (true) { var docService = docServiceProvider.apply(workerPool); - var wsService = workspaceServiceProvider.apply(workerPool, docService); + var wsService = workspaceServiceProvider.apply(workerPool); docService.pair(wsService); + wsService.pair(docService); startLSP(constructLSPClient(serverSocket.accept(), new ActualLanguageServer(() -> {}, workerPool, docService, wsService), requestPool)); } } catch (IOException e) { diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/BaseWorkspaceService.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/BaseWorkspaceService.java index fee26a870..a5ab5f480 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/BaseWorkspaceService.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/BaseWorkspaceService.java @@ -69,15 +69,25 @@ public abstract class BaseWorkspaceService implements WorkspaceService, Language private final ExecutorService exec; - private final IBaseTextDocumentService documentService; + private @MonotonicNonNull IBaseTextDocumentService documentService; private final CopyOnWriteArrayList workspaceFolders = new CopyOnWriteArrayList<>(); - protected BaseWorkspaceService(ExecutorService exec, IBaseTextDocumentService documentService) { - this.documentService = documentService; + protected BaseWorkspaceService(ExecutorService exec) { this.exec = exec; } + public void pair(IBaseTextDocumentService documentService) { + this.documentService = documentService; + } + + private IBaseTextDocumentService availableDocumentService() { + if (documentService == null) { + throw new IllegalStateException("Document Service has not been paired"); + } + return documentService; + } + public void initialize(ClientCapabilities clientCap, @Nullable List currentWorkspaceFolders, ServerCapabilities capabilities) { this.workspaceFolders.clear(); if (currentWorkspaceFolders != null) { @@ -126,7 +136,7 @@ public void didChangeWorkspaceFolders(DidChangeWorkspaceFoldersParams params) { if (removed != null) { workspaceFolders.removeAll(removed); for (WorkspaceFolder folder : removed) { - documentService.projectRemoved(folder.getName(), Locations.toLoc(folder.getUri())); + availableDocumentService().projectRemoved(folder.getName(), Locations.toLoc(folder.getUri())); } } @@ -134,7 +144,7 @@ public void didChangeWorkspaceFolders(DidChangeWorkspaceFoldersParams params) { if (added != null) { workspaceFolders.addAll(added); for (WorkspaceFolder folder : added) { - documentService.projectAdded(folder.getName(), Locations.toLoc(folder.getUri())); + availableDocumentService().projectAdded(folder.getName(), Locations.toLoc(folder.getUri())); } } } @@ -142,14 +152,14 @@ public void didChangeWorkspaceFolders(DidChangeWorkspaceFoldersParams params) { @Override public void didCreateFiles(CreateFilesParams params) { logger.debug("workspace/didCreateFiles: {}", params.getFiles()); - exec.submit(() -> documentService.didCreateFiles(params)); + exec.submit(() -> availableDocumentService().didCreateFiles(params)); } @Override public void didRenameFiles(RenameFilesParams params) { logger.debug("workspace/didRenameFiles: {}", params.getFiles()); - exec.submit(() -> documentService.didRenameFiles(params, workspaceFolders())); + exec.submit(() -> availableDocumentService().didRenameFiles(params, workspaceFolders())); exec.submit(() -> { // cleanup the old files (we do not get a `didDelete` event) @@ -157,14 +167,14 @@ public void didRenameFiles(RenameFilesParams params) { .map(f -> f.getOldUri()) .map(FileDelete::new) .collect(Collectors.toList()); - documentService.didDeleteFiles(new DeleteFilesParams(oldFiles)); + availableDocumentService().didDeleteFiles(new DeleteFilesParams(oldFiles)); }); } @Override public void didDeleteFiles(DeleteFilesParams params) { logger.debug("workspace/didDeleteFiles: {}", params.getFiles()); - exec.submit(() -> documentService.didDeleteFiles(params)); + exec.submit(() -> availableDocumentService().didDeleteFiles(params)); } @Override @@ -175,7 +185,7 @@ public CompletableFuture executeCommand(ExecuteCommandParams commandPara if (params.getCommand().startsWith(RASCAL_META_COMMAND) || params.getCommand().startsWith(RASCAL_COMMAND)) { String languageName = ((JsonPrimitive) params.getArguments().get(0)).getAsString(); String command = ((JsonPrimitive) params.getArguments().get(1)).getAsString(); - return documentService.executeCommand(languageName, command).thenApply(v -> v); + return availableDocumentService().executeCommand(languageName, command).thenApply(v -> v); } return CompletableFutureUtils.completedFuture(params.getCommand() + " was ignored.", exec); diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/LanguageServerRouter.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/LanguageServerRouter.java index de620ac93..0ddf4a2cf 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/LanguageServerRouter.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/LanguageServerRouter.java @@ -69,8 +69,9 @@ public LanguageServerRouter(Runnable onExit, ExecutorService exec) { this.onExit = onExit; this.exec = exec; this.docService = new RoutingTextDocumentService(); - this.wsService = new RoutingWorkspaceService(exec, docService); + this.wsService = new RoutingWorkspaceService(exec); docService.pair(wsService); + wsService.pair(docService); } @Override @@ -217,8 +218,8 @@ public void cancelProgress(String progressId) { private class RoutingWorkspaceService extends BaseWorkspaceService { - protected RoutingWorkspaceService(ExecutorService exec, IBaseTextDocumentService documentService) { - super(exec, documentService); + protected RoutingWorkspaceService(ExecutorService exec) { + super(exec); } } diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricWorkspaceService.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricWorkspaceService.java index 8b5d7a71c..f84770050 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricWorkspaceService.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricWorkspaceService.java @@ -28,10 +28,9 @@ import java.util.concurrent.ExecutorService; import org.rascalmpl.vscode.lsp.BaseWorkspaceService; -import org.rascalmpl.vscode.lsp.IBaseTextDocumentService; public class ParametricWorkspaceService extends BaseWorkspaceService { - ParametricWorkspaceService(ExecutorService exec, IBaseTextDocumentService docService) { - super(exec, docService); + ParametricWorkspaceService(ExecutorService exec) { + super(exec); } } diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/RascalWorkspaceService.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/RascalWorkspaceService.java index f485f4adc..58fd9d1f6 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/RascalWorkspaceService.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/RascalWorkspaceService.java @@ -43,13 +43,12 @@ import org.eclipse.lsp4j.WorkspaceFolder; import org.eclipse.lsp4j.WorkspaceServerCapabilities; import org.rascalmpl.vscode.lsp.BaseWorkspaceService; -import org.rascalmpl.vscode.lsp.IBaseTextDocumentService; import org.rascalmpl.vscode.lsp.util.Nullables; public class RascalWorkspaceService extends BaseWorkspaceService { - RascalWorkspaceService(ExecutorService exec, IBaseTextDocumentService documentService) { - super(exec, documentService); + RascalWorkspaceService(ExecutorService exec) { + super(exec); } @Override From 50c8fc06b799150b49c3d4a313073e837441ffd3 Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Wed, 22 Apr 2026 13:29:41 +0200 Subject: [PATCH 003/107] Routing language server as special case of parametric. --- .../vscode/lsp/BaseLanguageServer.java | 20 ++--- .../parametric/ParametricLanguageServer.java | 22 ++++-- .../lsp/parametric/RoutingLanguageServer.java | 73 +++++++++++++++++++ .../src/lsp/RascalLSPConnection.ts | 2 +- 4 files changed, 98 insertions(+), 19 deletions(-) create mode 100644 rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/RoutingLanguageServer.java diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/BaseLanguageServer.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/BaseLanguageServer.java index 4dfcd9afd..512e158b8 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/BaseLanguageServer.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/BaseLanguageServer.java @@ -76,9 +76,9 @@ */ @SuppressWarnings("java:S106") // we are using system.in/system.out correctly in this class public abstract class BaseLanguageServer { - private static final PrintStream capturedOut; - private static final InputStream capturedIn; - private static final boolean DEPLOY_MODE; + protected static final PrintStream capturedOut; + protected static final InputStream capturedIn; + protected static final boolean DEPLOY_MODE; private static final String LOG_CONFIGURATION_KEY = "log4j2.configurationFactory"; static { @@ -104,13 +104,13 @@ protected BaseLanguageServer() {} private static final Logger logger = LogManager.getLogger(BaseLanguageServer.class); - private static Launcher constructLSPClient(Socket client, ActualLanguageServer server, ExecutorService threadPool) + protected static Launcher constructLSPClient(Socket client, ActualLanguageServer server, ExecutorService threadPool) throws IOException { client.setTcpNoDelay(true); return constructLSPClient(client.getInputStream(), client.getOutputStream(), server, threadPool); } - private static Launcher constructLSPClient(InputStream in, OutputStream out, ActualLanguageServer server, ExecutorService threadPool) { + protected static Launcher constructLSPClient(InputStream in, OutputStream out, ActualLanguageServer server, ExecutorService threadPool) { Launcher clientLauncher = new Launcher.Builder() .setLocalService(server) .setRemoteInterface(IBaseLanguageClient.class) @@ -125,7 +125,7 @@ private static Launcher constructLSPClient(InputStream in, return clientLauncher; } - private static void printClassPath() { + protected static void printClassPath() { logger.trace("Started with classpath: {}", () -> System.getProperty("java.class.path")); } @@ -159,7 +159,7 @@ public static void startLanguageServer(ExecutorService requestPool, ExecutorServ private static final String DEFAULT_VERSION = "unknown"; - private static String getVersion() { + protected static String getVersion() { try (InputStream prop = ActualLanguageServer.class.getClassLoader().getResourceAsStream("project.properties")) { if (prop == null) { logger.error("Could not find project.properties file"); @@ -176,7 +176,7 @@ private static String getVersion() { } } - private static void startLSP(Launcher server) { + protected static void startLSP(Launcher server) { try { server.startListening().get(); } catch (InterruptedException e) { @@ -194,7 +194,7 @@ private static void startLSP(Launcher server) { } } } - private static class ActualLanguageServer implements IBaseLanguageServerExtensions, LanguageClientAware { + protected static class ActualLanguageServer implements IBaseLanguageServerExtensions, LanguageClientAware { static final Logger logger = LogManager.getLogger(ActualLanguageServer.class); private final IBaseTextDocumentService lspDocumentService; private final BaseWorkspaceService lspWorkspaceService; @@ -202,7 +202,7 @@ private static class ActualLanguageServer implements IBaseLanguageServerExtensio private final ExecutorService executor; private @MonotonicNonNull IDEServicesConfiguration remoteIDEServicesConfiguration; - private ActualLanguageServer(Runnable onExit, ExecutorService executor, IBaseTextDocumentService lspDocumentService, BaseWorkspaceService lspWorkspaceService) { + protected ActualLanguageServer(Runnable onExit, ExecutorService executor, IBaseTextDocumentService lspDocumentService, BaseWorkspaceService lspWorkspaceService) { this.onExit = onExit; this.executor = executor; this.lspDocumentService = lspDocumentService; diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricLanguageServer.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricLanguageServer.java index 5913f1a12..fc9cb7f36 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricLanguageServer.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricLanguageServer.java @@ -27,13 +27,24 @@ package org.rascalmpl.vscode.lsp.parametric; +import com.google.gson.GsonBuilder; import org.rascalmpl.vscode.lsp.BaseLanguageServer; import org.rascalmpl.vscode.lsp.parametric.LanguageRegistry.LanguageParameter; import org.rascalmpl.vscode.lsp.util.NamedThreadPool; -import com.google.gson.GsonBuilder; - public class ParametricLanguageServer extends BaseLanguageServer { + + protected static final int PORT_NUMBER = 9999; + + protected static void start(LanguageParameter dedicatedLanguage) { + startLanguageServer(NamedThreadPool.single("parametric-lsp") + , NamedThreadPool.cached("parametric") + , threadPool -> new ParametricTextDocumentService(threadPool, dedicatedLanguage) + , ParametricWorkspaceService::new + , PORT_NUMBER + ); + } + public static void main(String[] args) { LanguageParameter dedicatedLanguage; if (args.length > 0) { @@ -43,11 +54,6 @@ public static void main(String[] args) { dedicatedLanguage = null; } - startLanguageServer(NamedThreadPool.single("parametric-lsp") - , NamedThreadPool.cached("parametric") - , threadPool -> new ParametricTextDocumentService(threadPool, dedicatedLanguage) - , ParametricWorkspaceService::new - , 9999 - ); + start(dedicatedLanguage); } } diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/RoutingLanguageServer.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/RoutingLanguageServer.java new file mode 100644 index 000000000..a8e5fcad3 --- /dev/null +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/RoutingLanguageServer.java @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2018-2025, NWO-I CWI and Swat.engineering + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +package org.rascalmpl.vscode.lsp.parametric; + +import com.google.gson.GsonBuilder; +import java.io.IOException; +import java.net.InetAddress; +import java.net.ServerSocket; +import java.util.concurrent.ExecutorService; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.rascalmpl.vscode.lsp.parametric.LanguageRegistry.LanguageParameter; +import org.rascalmpl.vscode.lsp.util.NamedThreadPool; + +public class RoutingLanguageServer extends ParametricLanguageServer { + + private static final Logger logger = LogManager.getLogger(RoutingLanguageServer.class); + + public static void startLanguageServer(ExecutorService requestPool, ExecutorService workerPool, int portNumber) { + logger.info("Starting Rascal Language Server: {}", getVersion()); + printClassPath(); + + if (DEPLOY_MODE) { + startLSP(constructLSPClient(capturedIn, capturedOut, new LanguageServerRouter(() -> System.exit(0), workerPool), requestPool)); + } + else { + try (ServerSocket serverSocket = new ServerSocket(portNumber, 0, InetAddress.getByName("127.0.0.1"))) { + logger.info("Rascal LSP server listens on port number: {}", portNumber); + while (true) { + startLSP(constructLSPClient(serverSocket.accept(), new LanguageServerRouter(() -> {}, workerPool), requestPool)); + } + } catch (IOException e) { + logger.fatal("Failure to start TCP server on port {}", portNumber, e); + } + } + } + + public static void main(String[] args) { + if (args.length > 0) { + var dedicatedLanguage = new GsonBuilder().create().fromJson(args[0], LanguageParameter.class); + start(dedicatedLanguage); + } else { + startLanguageServer(NamedThreadPool.single("parametric-lsp-router") + , NamedThreadPool.cached("parametric-router") + , PORT_NUMBER + ); + } + } +} diff --git a/rascal-vscode-extension/src/lsp/RascalLSPConnection.ts b/rascal-vscode-extension/src/lsp/RascalLSPConnection.ts index 9ab434b37..0b269a907 100644 --- a/rascal-vscode-extension/src/lsp/RascalLSPConnection.ts +++ b/rascal-vscode-extension/src/lsp/RascalLSPConnection.ts @@ -168,7 +168,7 @@ async function buildRascalServerOptions(jarPath: string, isParametricServer: boo ]; let mainClass: string; if (isParametricServer) { - mainClass = 'org.rascalmpl.vscode.lsp.parametric.ParametricLanguageServer'; + mainClass = 'org.rascalmpl.vscode.lsp.parametric.RoutingLanguageServer'; commandArgs.push(calculateDSLMemoryReservation(dedicated)); } else { From f320f88f72fafe2a16b80880ee7e37200c3cb282 Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Wed, 22 Apr 2026 13:31:50 +0200 Subject: [PATCH 004/107] Extract routing services. --- .../vscode/lsp/ISingleLanguageServer.java | 32 ++ .../vscode/lsp/LanguageServerRouter.java | 98 ++++++ .../lsp/RoutingTextDocumentService.java | 199 ++++++++++++ .../vscode/lsp/RoutingWorkspaceService.java | 54 ++++ .../lsp/parametric/LanguageServerRouter.java | 284 ------------------ .../lsp/parametric/RoutingLanguageServer.java | 1 + 6 files changed, 384 insertions(+), 284 deletions(-) create mode 100644 rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/ISingleLanguageServer.java create mode 100644 rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/LanguageServerRouter.java create mode 100644 rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/RoutingTextDocumentService.java create mode 100644 rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/RoutingWorkspaceService.java delete mode 100644 rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/LanguageServerRouter.java diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/ISingleLanguageServer.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/ISingleLanguageServer.java new file mode 100644 index 000000000..3b58f6746 --- /dev/null +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/ISingleLanguageServer.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2018-2025, NWO-I CWI and Swat.engineering + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +package org.rascalmpl.vscode.lsp; + +public interface ISingleLanguageServer { + IBaseLanguageClient getClient(); + IBaseLanguageServerExtensions getServer(); +} diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/LanguageServerRouter.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/LanguageServerRouter.java new file mode 100644 index 000000000..da88ee0a6 --- /dev/null +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/LanguageServerRouter.java @@ -0,0 +1,98 @@ +/* + * Copyright (c) 2018-2025, NWO-I CWI and Swat.engineering + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +package org.rascalmpl.vscode.lsp; + +import java.util.HashSet; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutorService; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.eclipse.lsp4j.InitializeParams; +import org.eclipse.lsp4j.InitializeResult; +import org.rascalmpl.uri.URIUtil; + +import io.usethesource.vallang.ISourceLocation; + +public class LanguageServerRouter extends BaseLanguageServer.ActualLanguageServer { + + private static final Logger logger = LogManager.getLogger(LanguageServerRouter.class); + + private final Map languagesByExtension; + private final Map languageServers; + + public LanguageServerRouter(Runnable onExit, ExecutorService exec) { + super(onExit, exec, new RoutingTextDocumentService(), new RoutingWorkspaceService(exec)); + + this.languagesByExtension = new ConcurrentHashMap<>(); + this.languageServers = new ConcurrentHashMap<>(); + } + + private ISingleLanguageServer route(ISourceLocation uri) { + var lang = safeLanguage(uri).orElseThrow(() -> + new UnsupportedOperationException(String.format("Rascal Parametric LSP has no support for this file, since no language is registered for extension '%s': %s", extension(uri), uri)) + ); + return languageByName(lang); + } + + private ISingleLanguageServer languageByName(String lang) { + var service = languageServers.get(lang); + if (service == null) { + throw new UnsupportedOperationException(String.format("Rascal Parametric LSP has no support for this file, since no language is registered with name '%s'", lang)); + } + return service; + } + + private Optional safeLanguage(ISourceLocation loc) { + var ext = extension(loc); + if ("".equals(ext)) { + var languages = new HashSet<>(languagesByExtension.values()); + if (languages.size() == 1) { + logger.trace("File was opened without an extension; falling back to the single registered language for: {}", loc); + return languages.stream().findFirst(); + } else { + logger.error("File was opened without an extension and there are multiple languages registered, so we cannot pick a fallback for: {}", loc); + return Optional.empty(); + } + } + return Optional.ofNullable(languagesByExtension.get(ext)); + } + + private static String extension(ISourceLocation doc) { + return URIUtil.getExtension(doc); + } + + @Override + public CompletableFuture initialize(InitializeParams params) { + ((RoutingTextDocumentService) getTextDocumentService()).setRouter(this::route); + ((RoutingWorkspaceService) getWorkspaceService()).setRouter(this::route); + return super.initialize(params); + } + +} diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/RoutingTextDocumentService.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/RoutingTextDocumentService.java new file mode 100644 index 000000000..07ba3b8f9 --- /dev/null +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/RoutingTextDocumentService.java @@ -0,0 +1,199 @@ +/* + * Copyright (c) 2018-2025, NWO-I CWI and Swat.engineering + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +package org.rascalmpl.vscode.lsp; + +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.function.Function; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.eclipse.lsp4j.ClientCapabilities; +import org.eclipse.lsp4j.CreateFilesParams; +import org.eclipse.lsp4j.DeleteFilesParams; +import org.eclipse.lsp4j.DidChangeTextDocumentParams; +import org.eclipse.lsp4j.DidCloseTextDocumentParams; +import org.eclipse.lsp4j.DidOpenTextDocumentParams; +import org.eclipse.lsp4j.DidSaveTextDocumentParams; +import org.eclipse.lsp4j.RenameFilesParams; +import org.eclipse.lsp4j.ServerCapabilities; +import org.eclipse.lsp4j.WorkspaceFolder; +import org.eclipse.lsp4j.services.LanguageClient; +import org.rascalmpl.util.locations.ColumnMaps; +import org.rascalmpl.util.locations.LineColumnOffsetMap; +import org.rascalmpl.vscode.lsp.parametric.LanguageRegistry.LanguageParameter; + +import io.usethesource.vallang.ISourceLocation; +import io.usethesource.vallang.IValue; + +class RoutingTextDocumentService implements IBaseTextDocumentService { + + private @MonotonicNonNull Function route; + + void setRouter(Function routeFunc) { + this.route = routeFunc; + } + + private IBaseTextDocumentService route(ISourceLocation uri) { + if (this.route == null) { + throw new IllegalStateException("Router has not been initialized"); + } + return (IBaseTextDocumentService) this.route.apply(uri).getServer().getTextDocumentService(); + } + + @Override + public void didOpen(DidOpenTextDocumentParams params) { + // TODO Auto-generated method stub + throw new UnsupportedOperationException("Unimplemented method 'didOpen'"); + } + + @Override + public void didChange(DidChangeTextDocumentParams params) { + // TODO Auto-generated method stub + throw new UnsupportedOperationException("Unimplemented method 'didChange'"); + } + + @Override + public void didClose(DidCloseTextDocumentParams params) { + // TODO Auto-generated method stub + throw new UnsupportedOperationException("Unimplemented method 'didClose'"); + } + + @Override + public void didSave(DidSaveTextDocumentParams params) { + // TODO Auto-generated method stub + throw new UnsupportedOperationException("Unimplemented method 'didSave'"); + } + + @Override + public void initializeServerCapabilities(ClientCapabilities clientCapabilities, ServerCapabilities result) { + // TODO Auto-generated method stub + throw new UnsupportedOperationException("Unimplemented method 'initializeServerCapabilities'"); + } + + @Override + public void shutdown() { + // TODO Auto-generated method stub + throw new UnsupportedOperationException("Unimplemented method 'shutdown'"); + } + + @Override + public void connect(LanguageClient client) { + // TODO Auto-generated method stub + throw new UnsupportedOperationException("Unimplemented method 'connect'"); + } + + @Override + public void pair(BaseWorkspaceService workspaceService) { + // TODO Auto-generated method stub + throw new UnsupportedOperationException("Unimplemented method 'pair'"); + } + + @Override + public void initialized() { + // TODO Auto-generated method stub + throw new UnsupportedOperationException("Unimplemented method 'initialized'"); + } + + @Override + public void registerLanguage(LanguageParameter lang) { + // TODO Auto-generated method stub + throw new UnsupportedOperationException("Unimplemented method 'registerLanguage'"); + } + + @Override + public void unregisterLanguage(LanguageParameter lang) { + // TODO Auto-generated method stub + throw new UnsupportedOperationException("Unimplemented method 'unregisterLanguage'"); + } + + @Override + public void projectAdded(String name, ISourceLocation projectRoot) { + // TODO Auto-generated method stub + throw new UnsupportedOperationException("Unimplemented method 'projectAdded'"); + } + + @Override + public void projectRemoved(String name, ISourceLocation projectRoot) { + // TODO Auto-generated method stub + throw new UnsupportedOperationException("Unimplemented method 'projectRemoved'"); + } + + @Override + public CompletableFuture executeCommand(String languageName, String command) { + // TODO Auto-generated method stub + throw new UnsupportedOperationException("Unimplemented method 'executeCommand'"); + } + + @Override + public LineColumnOffsetMap getColumnMap(ISourceLocation file) { + // TODO Auto-generated method stub + throw new UnsupportedOperationException("Unimplemented method 'getColumnMap'"); + } + + @Override + public ColumnMaps getColumnMaps() { + // TODO Auto-generated method stub + throw new UnsupportedOperationException("Unimplemented method 'getColumnMaps'"); + } + + @Override + public @Nullable TextDocumentState getDocumentState(ISourceLocation file) { + // TODO Auto-generated method stub + throw new UnsupportedOperationException("Unimplemented method 'getDocumentState'"); + } + + @Override + public boolean isManagingFile(ISourceLocation file) { + // TODO Auto-generated method stub + throw new UnsupportedOperationException("Unimplemented method 'isManagingFile'"); + } + + @Override + public void didCreateFiles(CreateFilesParams params) { + // TODO Auto-generated method stub + throw new UnsupportedOperationException("Unimplemented method 'didCreateFiles'"); + } + + @Override + public void didRenameFiles(RenameFilesParams params, List workspaceFolders) { + // TODO Auto-generated method stub + throw new UnsupportedOperationException("Unimplemented method 'didRenameFiles'"); + } + + @Override + public void didDeleteFiles(DeleteFilesParams params) { + // TODO Auto-generated method stub + throw new UnsupportedOperationException("Unimplemented method 'didDeleteFiles'"); + } + + @Override + public void cancelProgress(String progressId) { + // TODO Auto-generated method stub + throw new UnsupportedOperationException("Unimplemented method 'cancelProgress'"); + } + +} diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/RoutingWorkspaceService.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/RoutingWorkspaceService.java new file mode 100644 index 000000000..aa6a42e05 --- /dev/null +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/RoutingWorkspaceService.java @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2018-2025, NWO-I CWI and Swat.engineering + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +package org.rascalmpl.vscode.lsp; + +import java.util.concurrent.ExecutorService; +import java.util.function.Function; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; + +import io.usethesource.vallang.ISourceLocation; + +class RoutingWorkspaceService extends BaseWorkspaceService { + + private @MonotonicNonNull Function route; + + protected RoutingWorkspaceService(ExecutorService exec) { + super(exec); + } + + void setRouter(Function routeFunc) { + this.route = routeFunc; + } + + private BaseWorkspaceService route(ISourceLocation uri) { + if (this.route == null) { + throw new IllegalStateException("Router has not been initialized"); + } + return (BaseWorkspaceService) this.route.apply(uri).getServer().getWorkspaceService(); + } + +} diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/LanguageServerRouter.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/LanguageServerRouter.java deleted file mode 100644 index 0ddf4a2cf..000000000 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/LanguageServerRouter.java +++ /dev/null @@ -1,284 +0,0 @@ -/* - * Copyright (c) 2018-2025, NWO-I CWI and Swat.engineering - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE - * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE - * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR - * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF - * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS - * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN - * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - * POSSIBILITY OF SUCH DAMAGE. - */ -package org.rascalmpl.vscode.lsp.parametric; - -import java.util.List; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ExecutorService; -import org.checkerframework.checker.nullness.qual.Nullable; -import org.eclipse.lsp4j.ClientCapabilities; -import org.eclipse.lsp4j.CreateFilesParams; -import org.eclipse.lsp4j.DeleteFilesParams; -import org.eclipse.lsp4j.DidChangeTextDocumentParams; -import org.eclipse.lsp4j.DidCloseTextDocumentParams; -import org.eclipse.lsp4j.DidOpenTextDocumentParams; -import org.eclipse.lsp4j.DidSaveTextDocumentParams; -import org.eclipse.lsp4j.InitializeParams; -import org.eclipse.lsp4j.InitializeResult; -import org.eclipse.lsp4j.RenameFilesParams; -import org.eclipse.lsp4j.ServerCapabilities; -import org.eclipse.lsp4j.WorkspaceFolder; -import org.eclipse.lsp4j.services.LanguageClient; -import org.eclipse.lsp4j.services.LanguageClientAware; -import org.eclipse.lsp4j.services.TextDocumentService; -import org.eclipse.lsp4j.services.WorkspaceService; -import org.rascalmpl.util.locations.ColumnMaps; -import org.rascalmpl.util.locations.LineColumnOffsetMap; -import org.rascalmpl.vscode.lsp.BaseWorkspaceService; -import org.rascalmpl.vscode.lsp.IBaseLanguageServerExtensions; -import org.rascalmpl.vscode.lsp.IBaseTextDocumentService; -import org.rascalmpl.vscode.lsp.TextDocumentState; -import org.rascalmpl.vscode.lsp.parametric.LanguageRegistry.LanguageParameter; -import org.rascalmpl.vscode.lsp.uri.jsonrpc.messages.VFSRegister; - -import io.usethesource.vallang.ISourceLocation; -import io.usethesource.vallang.IValue; - -public class LanguageServerRouter implements IBaseLanguageServerExtensions, LanguageClientAware { - - private final Runnable onExit; - private final ExecutorService exec; - private final IBaseTextDocumentService docService; - private final BaseWorkspaceService wsService; - - public LanguageServerRouter(Runnable onExit, ExecutorService exec) { - this.onExit = onExit; - this.exec = exec; - this.docService = new RoutingTextDocumentService(); - this.wsService = new RoutingWorkspaceService(exec); - docService.pair(wsService); - wsService.pair(docService); - } - - @Override - public void connect(LanguageClient client) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'connect'"); - } - - private class RoutingTextDocumentService implements IBaseTextDocumentService { - - @Override - public void didOpen(DidOpenTextDocumentParams params) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'didOpen'"); - } - - @Override - public void didChange(DidChangeTextDocumentParams params) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'didChange'"); - } - - @Override - public void didClose(DidCloseTextDocumentParams params) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'didClose'"); - } - - @Override - public void didSave(DidSaveTextDocumentParams params) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'didSave'"); - } - - @Override - public void initializeServerCapabilities(ClientCapabilities clientCapabilities, ServerCapabilities result) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'initializeServerCapabilities'"); - } - - @Override - public void shutdown() { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'shutdown'"); - } - - @Override - public void connect(LanguageClient client) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'connect'"); - } - - @Override - public void pair(BaseWorkspaceService workspaceService) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'pair'"); - } - - @Override - public void initialized() { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'initialized'"); - } - - @Override - public void registerLanguage(LanguageParameter lang) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'registerLanguage'"); - } - - @Override - public void unregisterLanguage(LanguageParameter lang) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'unregisterLanguage'"); - } - - @Override - public void projectAdded(String name, ISourceLocation projectRoot) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'projectAdded'"); - } - - @Override - public void projectRemoved(String name, ISourceLocation projectRoot) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'projectRemoved'"); - } - - @Override - public CompletableFuture executeCommand(String languageName, String command) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'executeCommand'"); - } - - @Override - public LineColumnOffsetMap getColumnMap(ISourceLocation file) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'getColumnMap'"); - } - - @Override - public ColumnMaps getColumnMaps() { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'getColumnMaps'"); - } - - @Override - public @Nullable TextDocumentState getDocumentState(ISourceLocation file) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'getDocumentState'"); - } - - @Override - public boolean isManagingFile(ISourceLocation file) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'isManagingFile'"); - } - - @Override - public void didCreateFiles(CreateFilesParams params) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'didCreateFiles'"); - } - - @Override - public void didRenameFiles(RenameFilesParams params, List workspaceFolders) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'didRenameFiles'"); - } - - @Override - public void didDeleteFiles(DeleteFilesParams params) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'didDeleteFiles'"); - } - - @Override - public void cancelProgress(String progressId) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'cancelProgress'"); - } - - } - - private class RoutingWorkspaceService extends BaseWorkspaceService { - - protected RoutingWorkspaceService(ExecutorService exec) { - super(exec); - } - - } - - @Override - public CompletableFuture initialize(InitializeParams params) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'initialize'"); - } - - @Override - public CompletableFuture shutdown() { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'shutdown'"); - } - - @Override - public void exit() { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'exit'"); - } - - @Override - public TextDocumentService getTextDocumentService() { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'getTextDocumentService'"); - } - - @Override - public WorkspaceService getWorkspaceService() { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'getWorkspaceService'"); - } - - @Override - public void registerVFS(VFSRegister registration) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'registerVFS'"); - } - - @Override - public void setMinimumLogLevel(String level) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'setMinimumLogLevel'"); - } - // public static void main(String[] args) { - // LanguageParameter dedicatedLanguage; - // if (args.length > 0) { - // dedicatedLanguage = new GsonBuilder().create().fromJson(args[0], LanguageParameter.class); - // } - // else { - // dedicatedLanguage = null; - // } - - // startLanguageServer(NamedThreadPool.single("parametric-lsp") - // , NamedThreadPool.cached("parametric") - // , threadPool -> new ParametricTextDocumentService(threadPool, dedicatedLanguage) - // , ParametricWorkspaceService::new - // , 9999 - // ); - // } -} diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/RoutingLanguageServer.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/RoutingLanguageServer.java index a8e5fcad3..f2c842ce0 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/RoutingLanguageServer.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/RoutingLanguageServer.java @@ -33,6 +33,7 @@ import java.util.concurrent.ExecutorService; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.rascalmpl.vscode.lsp.LanguageServerRouter; import org.rascalmpl.vscode.lsp.parametric.LanguageRegistry.LanguageParameter; import org.rascalmpl.vscode.lsp.util.NamedThreadPool; From fe01d948080cc7332b1e7d2badd1da6a73e19fb8 Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Thu, 23 Apr 2026 13:33:14 +0200 Subject: [PATCH 005/107] Basic delegate server. --- rascal-lsp/.vscode/launch.json | 27 +++ .../vscode/lsp/BaseLanguageServer.java | 2 + .../vscode/lsp/ISingleLanguageServer.java | 32 ---- .../vscode/lsp/LanguageServerRouter.java | 179 ++++++++++++++++-- .../lsp/RoutingTextDocumentService.java | 70 +++++-- .../vscode/lsp/RoutingWorkspaceService.java | 17 -- .../parametric/ParametricLanguageServer.java | 14 +- .../ParametricTextDocumentService.java | 14 +- .../lsp/parametric/RoutingLanguageServer.java | 8 +- .../lsp/rascal/RascalTextDocumentService.java | 2 +- .../rascal/conversion/SemanticTokenizer.java | 2 +- 11 files changed, 270 insertions(+), 97 deletions(-) delete mode 100644 rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/ISingleLanguageServer.java diff --git a/rascal-lsp/.vscode/launch.json b/rascal-lsp/.vscode/launch.json index 9dde3567f..673fc225e 100644 --- a/rascal-lsp/.vscode/launch.json +++ b/rascal-lsp/.vscode/launch.json @@ -26,6 +26,33 @@ "-Drascal.compilerClasspath=${workspaceFolder}/target/lib/rascal.jar", "-Drascal.fallbackResolver=org.rascalmpl.vscode.lsp.uri.FallbackResolver" ] + }, + { + "type": "java", + "name": "Parametric Server (routing delegate 1)", + "request": "launch", + "mainClass": "org.rascalmpl.vscode.lsp.parametric.ParametricLanguageServer", + "args": "9990", + "projectName": "rascal-lsp", + "console": "internalConsole", + "vmArgs": [ + "-Dlog4j2.level=TRACE", + "-Drascal.compilerClasspath=${workspaceFolder}/target/lib/rascal.jar", + "-Drascal.fallbackResolver=org.rascalmpl.vscode.lsp.uri.FallbackResolver" + ] + }, + { + "type": "java", + "name": "Parametric Routing Server", + "request": "launch", + "mainClass": "org.rascalmpl.vscode.lsp.parametric.RoutingLanguageServer", + "projectName": "rascal-lsp", + "console": "internalConsole", + "vmArgs": [ + "-Dlog4j2.level=TRACE", + "-Drascal.compilerClasspath=${workspaceFolder}/target/lib/rascal.jar", + "-Drascal.fallbackResolver=org.rascalmpl.vscode.lsp.uri.FallbackResolver" + ] } ] } diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/BaseLanguageServer.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/BaseLanguageServer.java index 512e158b8..e5a976d89 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/BaseLanguageServer.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/BaseLanguageServer.java @@ -207,6 +207,8 @@ protected ActualLanguageServer(Runnable onExit, ExecutorService executor, IBaseT this.executor = executor; this.lspDocumentService = lspDocumentService; this.lspWorkspaceService = lspWorkspaceService; + lspDocumentService.pair(lspWorkspaceService); + lspWorkspaceService.pair(lspDocumentService); } @Override diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/ISingleLanguageServer.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/ISingleLanguageServer.java deleted file mode 100644 index 3b58f6746..000000000 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/ISingleLanguageServer.java +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright (c) 2018-2025, NWO-I CWI and Swat.engineering - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE - * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE - * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR - * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF - * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS - * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN - * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - * POSSIBILITY OF SUCH DAMAGE. - */ -package org.rascalmpl.vscode.lsp; - -public interface ISingleLanguageServer { - IBaseLanguageClient getClient(); - IBaseLanguageServerExtensions getServer(); -} diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/LanguageServerRouter.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/LanguageServerRouter.java index da88ee0a6..e6a6b2538 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/LanguageServerRouter.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/LanguageServerRouter.java @@ -26,6 +26,14 @@ */ package org.rascalmpl.vscode.lsp; +import static org.rascalmpl.vscode.lsp.BaseLanguageServer.DEPLOY_MODE; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.InetAddress; +import java.net.Socket; +import java.net.URI; import java.util.HashSet; import java.util.Map; import java.util.Optional; @@ -34,34 +42,37 @@ import java.util.concurrent.ExecutorService; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.checkerframework.checker.nullness.qual.Nullable; import org.eclipse.lsp4j.InitializeParams; import org.eclipse.lsp4j.InitializeResult; +import org.eclipse.lsp4j.MessageActionItem; +import org.eclipse.lsp4j.MessageParams; +import org.eclipse.lsp4j.PublishDiagnosticsParams; +import org.eclipse.lsp4j.Range; +import org.eclipse.lsp4j.ShowMessageRequestParams; +import org.eclipse.lsp4j.jsonrpc.Launcher; import org.rascalmpl.uri.URIUtil; +import org.rascalmpl.vscode.lsp.parametric.LanguageRegistry.LanguageParameter; +import io.usethesource.vallang.IInteger; import io.usethesource.vallang.ISourceLocation; +import io.usethesource.vallang.IString; -public class LanguageServerRouter extends BaseLanguageServer.ActualLanguageServer { +public class LanguageServerRouter extends BaseLanguageServer.ActualLanguageServer implements IBaseLanguageClient { private static final Logger logger = LogManager.getLogger(LanguageServerRouter.class); private final Map languagesByExtension; - private final Map languageServers; + private final Map> languageServers; public LanguageServerRouter(Runnable onExit, ExecutorService exec) { - super(onExit, exec, new RoutingTextDocumentService(), new RoutingWorkspaceService(exec)); + super(onExit, exec, new RoutingTextDocumentService(exec), new RoutingWorkspaceService(exec)); this.languagesByExtension = new ConcurrentHashMap<>(); this.languageServers = new ConcurrentHashMap<>(); } - private ISingleLanguageServer route(ISourceLocation uri) { - var lang = safeLanguage(uri).orElseThrow(() -> - new UnsupportedOperationException(String.format("Rascal Parametric LSP has no support for this file, since no language is registered for extension '%s': %s", extension(uri), uri)) - ); - return languageByName(lang); - } - - private ISingleLanguageServer languageByName(String lang) { + /*package*/ CompletableFuture languageByName(String lang) { var service = languageServers.get(lang); if (service == null) { throw new UnsupportedOperationException(String.format("Rascal Parametric LSP has no support for this file, since no language is registered with name '%s'", lang)); @@ -88,11 +99,153 @@ private static String extension(ISourceLocation doc) { return URIUtil.getExtension(doc); } + private @Nullable CompletableFuture startServer(LanguageParameter lang) { + try { + var classPath = System.getProperty("java.class.path"); + InputStream in; + OutputStream out; + if (DEPLOY_MODE) { + var proc = new ProcessBuilder("java" + , "-Dlog4j2.configurationFactory=org.rascalmpl.vscode.lsp.log.LogJsonConfiguration" + , "-Dlog4j2.level=DEBUG" + , "-Drascal.fallbackResolver=org.rascalmpl.vscode.lsp.uri.FallbackResolver" + , "-Drascal.lsp.deploy=" + DEPLOY_MODE + , "-Drascal.compilerClasspath=" + classPath + , "-cp" + classPath + , "org.rascalmpl.vscode.lsp.parametric.ParametricLanguageServer" + ).start(); + in = proc.getInputStream(); + out = proc.getOutputStream(); + } else { + // TODO Predictably increment ports for subsequent languages + Socket socket = new Socket(InetAddress.getLoopbackAddress(), 9990); + socket.setTcpNoDelay(true); + in = socket.getInputStream(); + out = socket.getOutputStream(); + } + var client = new Launcher.Builder() + .setRemoteInterface(IBaseLanguageServerExtensions.class) + .setLocalService(this) + .setInput(in) + .setOutput(out) + .create(); + + client.startListening(); + var server = client.getRemoteProxy(); + var delegateServerCaps = server.initialize(delegateInitializationParams()); + + // When initialization is done, we can use the server + return delegateServerCaps.thenApply(_c -> server); + } catch (IOException e) { + logger.fatal(e); + return null; + } + } + + private InitializeParams delegateInitializationParams() { + var params = new InitializeParams(); + return params; + } + + public RoutingTextDocumentService getTextDocumentService() { + return (RoutingTextDocumentService) super.getTextDocumentService(); + } + + public RoutingWorkspaceService getWorkspaceService() { + return (RoutingWorkspaceService) super.getWorkspaceService(); + } + @Override public CompletableFuture initialize(InitializeParams params) { - ((RoutingTextDocumentService) getTextDocumentService()).setRouter(this::route); - ((RoutingWorkspaceService) getWorkspaceService()).setRouter(this::route); + getTextDocumentService().setServer(this); return super.initialize(params); } + @Override + public synchronized CompletableFuture sendRegisterLanguage(LanguageParameter lang) { + // If we do not have a parametric server running for this language, start and initialize it. + synchronized (this) { + var remote = languageServers.computeIfAbsent(lang.getName(), name -> { + for (var ext : lang.getExtensions()) { + languagesByExtension.put(ext, lang.getName()); + } + return startServer(lang); + }); + } + + return super.sendRegisterLanguage(lang); + } + + @Override + public synchronized CompletableFuture sendUnregisterLanguage(LanguageParameter lang) { + // TODO Handle shutting down the remote parametric server iff it is empty now. + return super.sendUnregisterLanguage(lang); + } + + @Override + public void telemetryEvent(Object object) { + // TODO Auto-generated method stub + throw new UnsupportedOperationException("Unimplemented method 'telemetryEvent'"); + } + + @Override + public void publishDiagnostics(PublishDiagnosticsParams diagnostics) { + // TODO Auto-generated method stub + throw new UnsupportedOperationException("Unimplemented method 'publishDiagnostics'"); + } + + @Override + public void showMessage(MessageParams messageParams) { + // TODO Auto-generated method stub + throw new UnsupportedOperationException("Unimplemented method 'showMessage'"); + } + + @Override + public CompletableFuture showMessageRequest(ShowMessageRequestParams requestParams) { + // TODO Auto-generated method stub + throw new UnsupportedOperationException("Unimplemented method 'showMessageRequest'"); + } + + @Override + public void logMessage(MessageParams message) { + // TODO Auto-generated method stub + throw new UnsupportedOperationException("Unimplemented method 'logMessage'"); + } + + @Override + public void showContent(URI uri, IString title, IInteger viewColumn) { + // TODO Auto-generated method stub + throw new UnsupportedOperationException("Unimplemented method 'showContent'"); + } + + @Override + public void receiveRegisterLanguage(LanguageParameter lang) { + // TODO Auto-generated method stub + throw new UnsupportedOperationException("Unimplemented method 'receiveRegisterLanguage'"); + } + + @Override + public void receiveUnregisterLanguage(LanguageParameter lang) { + // TODO Auto-generated method stub + throw new UnsupportedOperationException("Unimplemented method 'receiveUnregisterLanguage'"); + } + + @Override + public void editDocument(URI uri, @Nullable Range range, int viewColumn) { + // TODO Auto-generated method stub + throw new UnsupportedOperationException("Unimplemented method 'editDocument'"); + } + + @Override + public void startDebuggingSession(int serverPort) { + // TODO Auto-generated method stub + throw new UnsupportedOperationException("Unimplemented method 'startDebuggingSession'"); + } + + @Override + public void registerDebugServerPort(int processID, int serverPort) { + // TODO Auto-generated method stub + throw new UnsupportedOperationException("Unimplemented method 'registerDebugServerPort'"); + } + } diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/RoutingTextDocumentService.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/RoutingTextDocumentService.java index 07ba3b8f9..09dc5e252 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/RoutingTextDocumentService.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/RoutingTextDocumentService.java @@ -28,7 +28,9 @@ import java.util.List; import java.util.concurrent.CompletableFuture; -import java.util.function.Function; +import java.util.concurrent.ExecutorService; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.checkerframework.checker.nullness.qual.Nullable; import org.eclipse.lsp4j.ClientCapabilities; @@ -42,26 +44,59 @@ import org.eclipse.lsp4j.ServerCapabilities; import org.eclipse.lsp4j.WorkspaceFolder; import org.eclipse.lsp4j.services.LanguageClient; +import org.eclipse.lsp4j.services.TextDocumentService; +import org.eclipse.lsp4j.services.WorkspaceService; import org.rascalmpl.util.locations.ColumnMaps; import org.rascalmpl.util.locations.LineColumnOffsetMap; import org.rascalmpl.vscode.lsp.parametric.LanguageRegistry.LanguageParameter; +import org.rascalmpl.vscode.lsp.parametric.ParametricTextDocumentService; import io.usethesource.vallang.ISourceLocation; import io.usethesource.vallang.IValue; class RoutingTextDocumentService implements IBaseTextDocumentService { - private @MonotonicNonNull Function route; + private static final Logger logger = LogManager.getLogger(RoutingTextDocumentService.class); - void setRouter(Function routeFunc) { - this.route = routeFunc; + private @MonotonicNonNull LanguageClient client; + private @MonotonicNonNull BaseWorkspaceService wsService; + private @MonotonicNonNull LanguageServerRouter server; + + private final ExecutorService exec; + + public RoutingTextDocumentService(ExecutorService exec) { + this.exec = exec; + } + + public void setServer(LanguageServerRouter server) { + this.server = server; + } + + private CompletableFuture route(String langName) { + var server = availableServer(); + return server.languageByName(langName); + } + + private CompletableFuture routeDocService(String langName) { + return route(langName).thenApply(IBaseLanguageServerExtensions::getTextDocumentService); + } + + private CompletableFuture routeWsService(String langName) { + return route(langName).thenApply(IBaseLanguageServerExtensions::getWorkspaceService); } - private IBaseTextDocumentService route(ISourceLocation uri) { - if (this.route == null) { - throw new IllegalStateException("Router has not been initialized"); + private LanguageClient availableClient() { + if (client == null) { + throw new IllegalStateException("Client not connected yet."); } - return (IBaseTextDocumentService) this.route.apply(uri).getServer().getTextDocumentService(); + return client; + } + + private LanguageServerRouter availableServer() { + if (server == null) { + throw new IllegalStateException("Server not connected yet."); + } + return server; } @Override @@ -90,8 +125,7 @@ public void didSave(DidSaveTextDocumentParams params) { @Override public void initializeServerCapabilities(ClientCapabilities clientCapabilities, ServerCapabilities result) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'initializeServerCapabilities'"); + ParametricTextDocumentService.setStaticServerCapabilities(result); } @Override @@ -102,32 +136,28 @@ public void shutdown() { @Override public void connect(LanguageClient client) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'connect'"); + logger.debug("Connecting client {}", client); + this.client = client; } @Override public void pair(BaseWorkspaceService workspaceService) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'pair'"); + this.wsService = workspaceService; } @Override public void initialized() { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'initialized'"); + // reserved for future use } @Override public void registerLanguage(LanguageParameter lang) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'registerLanguage'"); + route(lang.getName()).thenApply(s -> s.sendRegisterLanguage(lang)); } @Override public void unregisterLanguage(LanguageParameter lang) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'unregisterLanguage'"); + route(lang.getName()).thenApply(s -> s.sendUnregisterLanguage(lang)); } @Override diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/RoutingWorkspaceService.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/RoutingWorkspaceService.java index aa6a42e05..44bef9b57 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/RoutingWorkspaceService.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/RoutingWorkspaceService.java @@ -27,28 +27,11 @@ package org.rascalmpl.vscode.lsp; import java.util.concurrent.ExecutorService; -import java.util.function.Function; -import org.checkerframework.checker.nullness.qual.MonotonicNonNull; - -import io.usethesource.vallang.ISourceLocation; class RoutingWorkspaceService extends BaseWorkspaceService { - private @MonotonicNonNull Function route; - protected RoutingWorkspaceService(ExecutorService exec) { super(exec); } - void setRouter(Function routeFunc) { - this.route = routeFunc; - } - - private BaseWorkspaceService route(ISourceLocation uri) { - if (this.route == null) { - throw new IllegalStateException("Router has not been initialized"); - } - return (BaseWorkspaceService) this.route.apply(uri).getServer().getWorkspaceService(); - } - } diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricLanguageServer.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricLanguageServer.java index fc9cb7f36..55a085bf1 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricLanguageServer.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricLanguageServer.java @@ -34,26 +34,30 @@ public class ParametricLanguageServer extends BaseLanguageServer { - protected static final int PORT_NUMBER = 9999; + protected static final int DEFAULT_PORT_NUMBER = 9999; - protected static void start(LanguageParameter dedicatedLanguage) { + protected static void start(int portNumber, LanguageParameter dedicatedLanguage) { startLanguageServer(NamedThreadPool.single("parametric-lsp") , NamedThreadPool.cached("parametric") , threadPool -> new ParametricTextDocumentService(threadPool, dedicatedLanguage) , ParametricWorkspaceService::new - , PORT_NUMBER + , portNumber ); } public static void main(String[] args) { + int portNumber = args.length == 0 + ? DEFAULT_PORT_NUMBER + : Integer.parseInt(args[0]); + LanguageParameter dedicatedLanguage; - if (args.length > 0) { + if (args.length > 1) { dedicatedLanguage = new GsonBuilder().create().fromJson(args[0], LanguageParameter.class); } else { dedicatedLanguage = null; } - start(dedicatedLanguage); + start(portNumber, dedicatedLanguage); } } diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricTextDocumentService.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricTextDocumentService.java index 97e85b6e6..f52116b32 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricTextDocumentService.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricTextDocumentService.java @@ -266,26 +266,32 @@ public void initializeServerCapabilities(ClientCapabilities clientCapabilities, , new CompletionCapability() , /* new FileOperationCapability.DidCreateFiles(exec), */ new FileOperationCapability.DidRenameFiles(exec), new FileOperationCapability.DidDeleteFiles(exec) ); - dynamicCapabilities.registerStaticCapabilities(result); + setStaticServerCapabilities(dedicatedLanguageName, result); + } + + public static void setStaticServerCapabilities(ServerCapabilities result) { + setStaticServerCapabilities("", result); + } + private static void setStaticServerCapabilities(String dedicatedLanguageName, ServerCapabilities result) { result.setDefinitionProvider(true); result.setTextDocumentSync(TextDocumentSyncKind.Full); result.setHoverProvider(true); result.setReferencesProvider(true); result.setDocumentSymbolProvider(true); result.setImplementationProvider(true); - result.setSemanticTokensProvider(tokenizer.options()); + result.setSemanticTokensProvider(SemanticTokenizer.options()); result.setCodeActionProvider(true); result.setCodeLensProvider(new CodeLensOptions(false)); result.setRenameProvider(new RenameOptions(true)); - result.setExecuteCommandProvider(new ExecuteCommandOptions(Collections.singletonList(getRascalMetaCommandName()))); + result.setExecuteCommandProvider(new ExecuteCommandOptions(Collections.singletonList(getRascalMetaCommandName(dedicatedLanguageName)))); result.setInlayHintProvider(true); result.setSelectionRangeProvider(true); result.setFoldingRangeProvider(true); result.setCallHierarchyProvider(true); } - private String getRascalMetaCommandName() { + public static String getRascalMetaCommandName(String dedicatedLanguageName) { // if we run in dedicated mode, we prefix the commands with our language name // to avoid ambiguity with other dedicated languages and the generic rascal plugin if (!dedicatedLanguageName.isEmpty()) { diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/RoutingLanguageServer.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/RoutingLanguageServer.java index f2c842ce0..7d5bd2f9f 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/RoutingLanguageServer.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/RoutingLanguageServer.java @@ -42,7 +42,7 @@ public class RoutingLanguageServer extends ParametricLanguageServer { private static final Logger logger = LogManager.getLogger(RoutingLanguageServer.class); public static void startLanguageServer(ExecutorService requestPool, ExecutorService workerPool, int portNumber) { - logger.info("Starting Rascal Language Server: {}", getVersion()); + logger.info("Starting Rascal Language Server Router: {}", getVersion()); printClassPath(); if (DEPLOY_MODE) { @@ -50,7 +50,7 @@ public static void startLanguageServer(ExecutorService requestPool, ExecutorServ } else { try (ServerSocket serverSocket = new ServerSocket(portNumber, 0, InetAddress.getByName("127.0.0.1"))) { - logger.info("Rascal LSP server listens on port number: {}", portNumber); + logger.info("Rascal LSP server router listens on port number: {}", portNumber); while (true) { startLSP(constructLSPClient(serverSocket.accept(), new LanguageServerRouter(() -> {}, workerPool), requestPool)); } @@ -63,11 +63,11 @@ public static void startLanguageServer(ExecutorService requestPool, ExecutorServ public static void main(String[] args) { if (args.length > 0) { var dedicatedLanguage = new GsonBuilder().create().fromJson(args[0], LanguageParameter.class); - start(dedicatedLanguage); + start(DEFAULT_PORT_NUMBER, dedicatedLanguage); } else { startLanguageServer(NamedThreadPool.single("parametric-lsp-router") , NamedThreadPool.cached("parametric-router") - , PORT_NUMBER + , DEFAULT_PORT_NUMBER ); } } diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/RascalTextDocumentService.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/RascalTextDocumentService.java index a13d62b58..267b11205 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/RascalTextDocumentService.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/RascalTextDocumentService.java @@ -235,7 +235,7 @@ public void initializeServerCapabilities(ClientCapabilities clientCapabilities, result.setTextDocumentSync(TextDocumentSyncKind.Full); result.setDocumentSymbolProvider(true); result.setHoverProvider(true); - result.setSemanticTokensProvider(tokenizer.options()); + result.setSemanticTokensProvider(SemanticTokenizer.options()); result.setCodeLensProvider(new CodeLensOptions(false)); result.setFoldingRangeProvider(true); result.setRenameProvider(new RenameOptions(true)); diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/conversion/SemanticTokenizer.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/conversion/SemanticTokenizer.java index 2fe9be185..153782d58 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/conversion/SemanticTokenizer.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/conversion/SemanticTokenizer.java @@ -76,7 +76,7 @@ public SemanticTokens semanticTokensFull(ITree tree, boolean specialCaseHighligh return new SemanticTokens(tokens.getTheList()); } - public SemanticTokensWithRegistrationOptions options() { + public static SemanticTokensWithRegistrationOptions options() { SemanticTokensWithRegistrationOptions result = new SemanticTokensWithRegistrationOptions(); SemanticTokensLegend legend = new SemanticTokensLegend(TokenTypes.getTokenTypes(), TokenTypes.getTokenModifiers()); From 2b6d9a4f108fddae38c6df3b619b75db11a2c11c Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Thu, 23 Apr 2026 14:56:45 +0200 Subject: [PATCH 006/107] Factor out initialization. --- .../vscode/lsp/LanguageServerRouter.java | 84 ++++++++++++++++--- 1 file changed, 73 insertions(+), 11 deletions(-) diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/LanguageServerRouter.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/LanguageServerRouter.java index e6a6b2538..fbda94be5 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/LanguageServerRouter.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/LanguageServerRouter.java @@ -37,11 +37,16 @@ import java.util.HashSet; import java.util.Map; import java.util.Optional; +import java.util.concurrent.CancellationException; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; +import java.util.concurrent.Future; +import java.util.concurrent.atomic.AtomicInteger; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.checkerframework.checker.nullness.qual.Nullable; import org.eclipse.lsp4j.InitializeParams; import org.eclipse.lsp4j.InitializeResult; @@ -65,6 +70,11 @@ public class LanguageServerRouter extends BaseLanguageServer.ActualLanguageServe private final Map languagesByExtension; private final Map> languageServers; + private @MonotonicNonNull InitializeParams initializeParams; + + private static final int REMOTE_BASE_PORT = 9990; + private AtomicInteger remotePortOffset = new AtomicInteger(0); + public LanguageServerRouter(Runnable onExit, ExecutorService exec) { super(onExit, exec, new RoutingTextDocumentService(exec), new RoutingWorkspaceService(exec)); @@ -104,7 +114,9 @@ private static String extension(ISourceLocation doc) { var classPath = System.getProperty("java.class.path"); InputStream in; OutputStream out; + Runnable onExit; if (DEPLOY_MODE) { + // In deployment, we start a process and connect to it via input/output streams var proc = new ProcessBuilder("java" , "-Dlog4j2.configurationFactory=org.rascalmpl.vscode.lsp.log.LogJsonConfiguration" , "-Dlog4j2.level=DEBUG" @@ -116,22 +128,32 @@ private static String extension(ISourceLocation doc) { ).start(); in = proc.getInputStream(); out = proc.getOutputStream(); + // TODO Do we need to close the process here? Or is this triggered after the process exits? + onExit = () -> {}; } else { - // TODO Predictably increment ports for subsequent languages - Socket socket = new Socket(InetAddress.getLoopbackAddress(), 9990); + // In development, we expect the server to have been launched on a pre-agreed port + int port = getNextPort(); + Socket socket = new Socket(InetAddress.getLoopbackAddress(), port); socket.setTcpNoDelay(true); in = socket.getInputStream(); out = socket.getOutputStream(); + onExit = () -> { + try { + socket.close(); + } catch (IOException e) { + logger.error("Closing socket for language {} on port {} failed", lang.getName(), port); + } + }; } - var client = new Launcher.Builder() + var serverLauncher = new Launcher.Builder() .setRemoteInterface(IBaseLanguageServerExtensions.class) .setLocalService(this) .setInput(in) .setOutput(out) .create(); - client.startListening(); - var server = client.getRemoteProxy(); + scheduleShutdown(serverLauncher.startListening(), lang, onExit); + var server = serverLauncher.getRemoteProxy(); var delegateServerCaps = server.initialize(delegateInitializationParams()); // When initialization is done, we can use the server @@ -142,8 +164,45 @@ private static String extension(ISourceLocation doc) { } } + private void scheduleShutdown(Future server, LanguageParameter lang, Runnable onExit) { + getExecutor().execute(() -> { + try { + server.get(); + } catch (CancellationException | ExecutionException | InterruptedException e) { + logger.error("Language server for {} crashed", lang.getName(), e); + } + try { + onExit.run(); + } catch (Throwable e) { + logger.error("Unexpected error while cleaning up connection to language server for {}", lang.getName(), e); + } + }); + } + + private int getNextPort() { + return REMOTE_BASE_PORT + remotePortOffset.getAndIncrement(); + } + + private InitializeParams availableInitializeParams() { + if (this.initializeParams == null) { + throw new IllegalStateException("Server not initialized yet"); + } + return initializeParams; + } + + // TODO If this function does not require any parameters, change to a constant private InitializeParams delegateInitializationParams() { var params = new InitializeParams(); + var clientParams = availableInitializeParams(); + params.setCapabilities(clientParams.getCapabilities()); // We support precisely the capabilities of VS Code + params.setClientInfo(clientParams.getClientInfo()); + params.setInitializationOptions(clientParams.getInitializationOptions()); + params.setLocale(clientParams.getLocale()); + try { + params.setProcessId((int) ProcessHandle.current().pid()); + } catch (UnsupportedOperationException | SecurityException e) { + logger.debug("Cannot set process ID", e); + } return params; } @@ -157,7 +216,12 @@ public RoutingWorkspaceService getWorkspaceService() { @Override public CompletableFuture initialize(InitializeParams params) { + // Capture the initialization params to re-use when initializing our delegates + this.initializeParams = params; + + // Our child needs us, but we cannot set this in the constructor, so we set it here. getTextDocumentService().setServer(this); + return super.initialize(params); } @@ -165,12 +229,10 @@ public CompletableFuture initialize(InitializeParams params) { public synchronized CompletableFuture sendRegisterLanguage(LanguageParameter lang) { // If we do not have a parametric server running for this language, start and initialize it. synchronized (this) { - var remote = languageServers.computeIfAbsent(lang.getName(), name -> { - for (var ext : lang.getExtensions()) { - languagesByExtension.put(ext, lang.getName()); - } - return startServer(lang); - }); + languageServers.computeIfAbsent(lang.getName(), _n -> startServer(lang)); + for (var ext : lang.getExtensions()) { + languagesByExtension.put(ext, lang.getName()); + } } return super.sendRegisterLanguage(lang); From 3134be6f53c92c57fe0d25da28c116350553aca2 Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Thu, 23 Apr 2026 15:02:44 +0200 Subject: [PATCH 007/107] Route all client calls back to the original client. --- .../vscode/lsp/BaseLanguageServer.java | 20 ++- .../vscode/lsp/LanguageServerRouter.java | 117 ++++++++++++++---- 2 files changed, 112 insertions(+), 25 deletions(-) diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/BaseLanguageServer.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/BaseLanguageServer.java index e5a976d89..d4660b1fa 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/BaseLanguageServer.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/BaseLanguageServer.java @@ -200,7 +200,9 @@ protected static class ActualLanguageServer implements IBaseLanguageServerExtens private final BaseWorkspaceService lspWorkspaceService; private final Runnable onExit; private final ExecutorService executor; + private @MonotonicNonNull IDEServicesConfiguration remoteIDEServicesConfiguration; + private @MonotonicNonNull IBaseLanguageClient client; protected ActualLanguageServer(Runnable onExit, ExecutorService executor, IBaseTextDocumentService lspDocumentService, BaseWorkspaceService lspWorkspaceService) { this.onExit = onExit; @@ -226,6 +228,13 @@ private static URI[] toURIArray(IList src) { .toArray(URI[]::new); } + protected IBaseLanguageClient availableClient() { + if (client == null) { + throw new IllegalStateException("Client not connected"); + } + return client; + } + @Override public CompletableFuture[]> supplyPathConfig(PathConfigParameter projectFolder) { return CompletableFuture.supplyAsync(() -> { @@ -307,9 +316,9 @@ public void setTrace(SetTraceParams params) { @Override public void connect(LanguageClient client) { - var actualClient = (IBaseLanguageClient) client; - lspDocumentService.connect(actualClient); - lspWorkspaceService.connect(actualClient); + this.client = (IBaseLanguageClient) client; + lspDocumentService.connect(this.client); + lspWorkspaceService.connect(this.client); remoteIDEServicesConfiguration = RemoteIDEServicesThread.startRemoteIDEServicesServer(client, lspDocumentService, executor); logger.debug("Remote IDE Services Port {}", remoteIDEServicesConfiguration); } @@ -329,5 +338,10 @@ public void setMinimumLogLevel(String level) { final var l = Level.toLevel(level, Level.DEBUG); // fall back to debug when the string cannot be mapped Configurator.setRootLevel(l); } + + protected ExecutorService getExecutor() { + return executor; + } + } } diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/LanguageServerRouter.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/LanguageServerRouter.java index fbda94be5..01e8dc714 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/LanguageServerRouter.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/LanguageServerRouter.java @@ -35,6 +35,7 @@ import java.net.Socket; import java.net.URI; import java.util.HashSet; +import java.util.List; import java.util.Map; import java.util.Optional; import java.util.concurrent.CancellationException; @@ -48,13 +49,24 @@ import org.apache.logging.log4j.Logger; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.checkerframework.checker.nullness.qual.Nullable; +import org.eclipse.lsp4j.ApplyWorkspaceEditParams; +import org.eclipse.lsp4j.ApplyWorkspaceEditResponse; +import org.eclipse.lsp4j.ConfigurationParams; import org.eclipse.lsp4j.InitializeParams; import org.eclipse.lsp4j.InitializeResult; +import org.eclipse.lsp4j.LogTraceParams; import org.eclipse.lsp4j.MessageActionItem; import org.eclipse.lsp4j.MessageParams; +import org.eclipse.lsp4j.ProgressParams; import org.eclipse.lsp4j.PublishDiagnosticsParams; import org.eclipse.lsp4j.Range; +import org.eclipse.lsp4j.RegistrationParams; +import org.eclipse.lsp4j.ShowDocumentParams; +import org.eclipse.lsp4j.ShowDocumentResult; import org.eclipse.lsp4j.ShowMessageRequestParams; +import org.eclipse.lsp4j.UnregistrationParams; +import org.eclipse.lsp4j.WorkDoneProgressCreateParams; +import org.eclipse.lsp4j.WorkspaceFolder; import org.eclipse.lsp4j.jsonrpc.Launcher; import org.rascalmpl.uri.URIUtil; import org.rascalmpl.vscode.lsp.parametric.LanguageRegistry.LanguageParameter; @@ -246,68 +258,129 @@ public synchronized CompletableFuture sendUnregisterLanguage(LanguageParam @Override public void telemetryEvent(Object object) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'telemetryEvent'"); + availableClient().telemetryEvent(object); } @Override public void publishDiagnostics(PublishDiagnosticsParams diagnostics) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'publishDiagnostics'"); + availableClient().publishDiagnostics(diagnostics); } @Override public void showMessage(MessageParams messageParams) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'showMessage'"); + availableClient().showMessage(messageParams); } @Override public CompletableFuture showMessageRequest(ShowMessageRequestParams requestParams) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'showMessageRequest'"); + return availableClient().showMessageRequest(requestParams); } @Override public void logMessage(MessageParams message) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'logMessage'"); + availableClient().logMessage(message); } @Override public void showContent(URI uri, IString title, IInteger viewColumn) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'showContent'"); + availableClient().showContent(uri, title, viewColumn); } @Override public void receiveRegisterLanguage(LanguageParameter lang) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'receiveRegisterLanguage'"); + availableClient().receiveRegisterLanguage(lang); } @Override public void receiveUnregisterLanguage(LanguageParameter lang) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'receiveUnregisterLanguage'"); + availableClient().receiveUnregisterLanguage(lang); } @Override public void editDocument(URI uri, @Nullable Range range, int viewColumn) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'editDocument'"); + availableClient().editDocument(uri, range, viewColumn); } @Override public void startDebuggingSession(int serverPort) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'startDebuggingSession'"); + availableClient().startDebuggingSession(serverPort); } @Override public void registerDebugServerPort(int processID, int serverPort) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'registerDebugServerPort'"); + availableClient().registerDebugServerPort(processID, serverPort); + } + + @Override + public CompletableFuture createProgress(WorkDoneProgressCreateParams params) { + return availableClient().createProgress(params); + } + + @Override + public void notifyProgress(ProgressParams params) { + availableClient().notifyProgress(params); + } + + @Override + public CompletableFuture applyEdit(ApplyWorkspaceEditParams params) { + return availableClient().applyEdit(params); + } + + @Override + public CompletableFuture> configuration(ConfigurationParams configurationParams) { + return availableClient().configuration(configurationParams); + } + + @Override + public void logTrace(LogTraceParams params) { + availableClient().logTrace(params); + } + + @Override + public CompletableFuture refreshCodeLenses() { + return availableClient().refreshCodeLenses(); + } + + @Override + public CompletableFuture refreshDiagnostics() { + return availableClient().refreshDiagnostics(); + } + + @Override + public CompletableFuture refreshInlayHints() { + return availableClient().refreshInlayHints(); + } + + @Override + public CompletableFuture refreshInlineValues() { + return availableClient().refreshInlineValues(); + } + + @Override + public CompletableFuture refreshSemanticTokens() { + return availableClient().refreshSemanticTokens(); + } + + @Override + public CompletableFuture showDocument(ShowDocumentParams params) { + return availableClient().showDocument(params); + } + + @Override + public CompletableFuture registerCapability(RegistrationParams params) { + // TODO Collect/maintain capabilities of all delegate servers, combine, and unregister capabilities if necessary based on that. + return availableClient().registerCapability(params); + } + + @Override + public CompletableFuture unregisterCapability(UnregistrationParams params) { + // TODO Collect/maintain capabilities of all delegate servers, combine, and unregister capabilities if necessary based on that. + return availableClient().unregisterCapability(params); + } + + @Override + public CompletableFuture> workspaceFolders() { + return availableClient().workspaceFolders(); } } From 4ed8701569bb2303d13a73c53a36602527771ca6 Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Thu, 23 Apr 2026 16:23:52 +0200 Subject: [PATCH 008/107] Simplify routing with utility interfaces. --- .../vscode/lsp/LanguageServerRouter.java | 10 ++++- .../lsp/RoutingTextDocumentService.java | 31 +++++-------- .../org/rascalmpl/vscode/lsp/util/Caller.java | 42 +++++++++++++++++ .../org/rascalmpl/vscode/lsp/util/Router.java | 45 +++++++++++++++++++ 4 files changed, 108 insertions(+), 20 deletions(-) create mode 100644 rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/util/Caller.java create mode 100644 rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/util/Router.java diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/LanguageServerRouter.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/LanguageServerRouter.java index 01e8dc714..c75e79734 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/LanguageServerRouter.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/LanguageServerRouter.java @@ -70,12 +70,13 @@ import org.eclipse.lsp4j.jsonrpc.Launcher; import org.rascalmpl.uri.URIUtil; import org.rascalmpl.vscode.lsp.parametric.LanguageRegistry.LanguageParameter; +import org.rascalmpl.vscode.lsp.util.Router; import io.usethesource.vallang.IInteger; import io.usethesource.vallang.ISourceLocation; import io.usethesource.vallang.IString; -public class LanguageServerRouter extends BaseLanguageServer.ActualLanguageServer implements IBaseLanguageClient { +public class LanguageServerRouter extends BaseLanguageServer.ActualLanguageServer implements IBaseLanguageClient, Router> { private static final Logger logger = LogManager.getLogger(LanguageServerRouter.class); @@ -102,6 +103,13 @@ public LanguageServerRouter(Runnable onExit, ExecutorService exec) { return service; } + @Override + public CompletableFuture route(ISourceLocation file) { + return safeLanguage(file).map(this::languageByName).orElseThrow(() -> { + throw new UnsupportedOperationException(String.format("Rascal Parametric LSP has no support for this file, since no language is registered with extension '%s': %s", extension(file), file)); + }); + } + private Optional safeLanguage(ISourceLocation loc) { var ext = extension(loc); if ("".equals(ext)) { diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/RoutingTextDocumentService.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/RoutingTextDocumentService.java index 09dc5e252..1428a588b 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/RoutingTextDocumentService.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/RoutingTextDocumentService.java @@ -45,16 +45,17 @@ import org.eclipse.lsp4j.WorkspaceFolder; import org.eclipse.lsp4j.services.LanguageClient; import org.eclipse.lsp4j.services.TextDocumentService; -import org.eclipse.lsp4j.services.WorkspaceService; import org.rascalmpl.util.locations.ColumnMaps; import org.rascalmpl.util.locations.LineColumnOffsetMap; import org.rascalmpl.vscode.lsp.parametric.LanguageRegistry.LanguageParameter; import org.rascalmpl.vscode.lsp.parametric.ParametricTextDocumentService; +import org.rascalmpl.vscode.lsp.util.Caller; +import org.rascalmpl.vscode.lsp.util.Router; import io.usethesource.vallang.ISourceLocation; import io.usethesource.vallang.IValue; -class RoutingTextDocumentService implements IBaseTextDocumentService { +class RoutingTextDocumentService implements IBaseTextDocumentService, Caller, Router> { private static final Logger logger = LogManager.getLogger(RoutingTextDocumentService.class); @@ -72,17 +73,11 @@ public void setServer(LanguageServerRouter server) { this.server = server; } - private CompletableFuture route(String langName) { - var server = availableServer(); - return server.languageByName(langName); - } - - private CompletableFuture routeDocService(String langName) { - return route(langName).thenApply(IBaseLanguageServerExtensions::getTextDocumentService); - } - - private CompletableFuture routeWsService(String langName) { - return route(langName).thenApply(IBaseLanguageServerExtensions::getWorkspaceService); + @Override + public CompletableFuture route(ISourceLocation loc) { + return availableServer() + .route(loc) + .thenApply(IBaseLanguageServerExtensions::getTextDocumentService); } private LanguageClient availableClient() { @@ -101,14 +96,12 @@ private LanguageServerRouter availableServer() { @Override public void didOpen(DidOpenTextDocumentParams params) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'didOpen'"); + call(route(params.getTextDocument()), TextDocumentService::didOpen, params); } @Override public void didChange(DidChangeTextDocumentParams params) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'didChange'"); + call(route(params.getTextDocument()), TextDocumentService::didChange, params); } @Override @@ -152,12 +145,12 @@ public void initialized() { @Override public void registerLanguage(LanguageParameter lang) { - route(lang.getName()).thenApply(s -> s.sendRegisterLanguage(lang)); + availableServer().languageByName(lang.getName()).thenApply(s -> s.sendRegisterLanguage(lang)); } @Override public void unregisterLanguage(LanguageParameter lang) { - route(lang.getName()).thenApply(s -> s.sendUnregisterLanguage(lang)); + availableServer().languageByName(lang.getName()).thenApply(s -> s.sendUnregisterLanguage(lang)); } @Override diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/util/Caller.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/util/Caller.java new file mode 100644 index 000000000..871eebbdf --- /dev/null +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/util/Caller.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2018-2025, NWO-I CWI and Swat.engineering + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +package org.rascalmpl.vscode.lsp.util; + +import java.util.concurrent.CompletableFuture; +import java.util.function.BiConsumer; +import java.util.function.BiFunction; + +public interface Caller { + + default CompletableFuture call(CompletableFuture t, BiFunction func, A arg) { + return t.thenApply(actualT -> func.apply(actualT, arg)); + } + + default CompletableFuture call(CompletableFuture t, BiConsumer func, A arg) { + return t.thenAccept(actualT -> func.accept(actualT, arg)); + } +} diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/util/Router.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/util/Router.java new file mode 100644 index 000000000..6e3e6d8e2 --- /dev/null +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/util/Router.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2018-2025, NWO-I CWI and Swat.engineering + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +package org.rascalmpl.vscode.lsp.util; + +import org.eclipse.lsp4j.TextDocumentItem; +import org.eclipse.lsp4j.VersionedTextDocumentIdentifier; +import org.rascalmpl.vscode.lsp.util.locations.Locations; + +import io.usethesource.vallang.ISourceLocation; + +public interface Router { + T route(ISourceLocation loc); + + default T route(TextDocumentItem doc) { + return route(Locations.toLoc(doc.getUri())); + } + + default T route(VersionedTextDocumentIdentifier id) { + return route(Locations.toLoc(id.getUri())); + } +} From 0e86a656fbde17271163482c53b329725eb0c2a1 Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Thu, 23 Apr 2026 16:30:42 +0200 Subject: [PATCH 009/107] Move classes to appropriate package. --- rascal-lsp/.vscode/launch.json | 2 +- .../org/rascalmpl/vscode/lsp/LanguageServerRouter.java | 4 +++- .../parametric/{ => routing}/RoutingLanguageServer.java | 3 ++- .../routing}/RoutingTextDocumentService.java | 9 +++++++-- .../routing}/RoutingWorkspaceService.java | 7 ++++--- rascal-vscode-extension/src/lsp/RascalLSPConnection.ts | 2 +- 6 files changed, 18 insertions(+), 9 deletions(-) rename rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/{ => routing}/RoutingLanguageServer.java (96%) rename rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/{ => parametric/routing}/RoutingTextDocumentService.java (94%) rename rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/{ => parametric/routing}/RoutingWorkspaceService.java (86%) diff --git a/rascal-lsp/.vscode/launch.json b/rascal-lsp/.vscode/launch.json index 673fc225e..07f179fc6 100644 --- a/rascal-lsp/.vscode/launch.json +++ b/rascal-lsp/.vscode/launch.json @@ -45,7 +45,7 @@ "type": "java", "name": "Parametric Routing Server", "request": "launch", - "mainClass": "org.rascalmpl.vscode.lsp.parametric.RoutingLanguageServer", + "mainClass": "org.rascalmpl.vscode.lsp.parametric.routing.RoutingLanguageServer", "projectName": "rascal-lsp", "console": "internalConsole", "vmArgs": [ diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/LanguageServerRouter.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/LanguageServerRouter.java index c75e79734..0b69b7f10 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/LanguageServerRouter.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/LanguageServerRouter.java @@ -70,6 +70,8 @@ import org.eclipse.lsp4j.jsonrpc.Launcher; import org.rascalmpl.uri.URIUtil; import org.rascalmpl.vscode.lsp.parametric.LanguageRegistry.LanguageParameter; +import org.rascalmpl.vscode.lsp.parametric.routing.RoutingTextDocumentService; +import org.rascalmpl.vscode.lsp.parametric.routing.RoutingWorkspaceService; import org.rascalmpl.vscode.lsp.util.Router; import io.usethesource.vallang.IInteger; @@ -95,7 +97,7 @@ public LanguageServerRouter(Runnable onExit, ExecutorService exec) { this.languageServers = new ConcurrentHashMap<>(); } - /*package*/ CompletableFuture languageByName(String lang) { + /*package*/ public CompletableFuture languageByName(String lang) { var service = languageServers.get(lang); if (service == null) { throw new UnsupportedOperationException(String.format("Rascal Parametric LSP has no support for this file, since no language is registered with name '%s'", lang)); diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/RoutingLanguageServer.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingLanguageServer.java similarity index 96% rename from rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/RoutingLanguageServer.java rename to rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingLanguageServer.java index 7d5bd2f9f..9a816025f 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/RoutingLanguageServer.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingLanguageServer.java @@ -24,7 +24,7 @@ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ -package org.rascalmpl.vscode.lsp.parametric; +package org.rascalmpl.vscode.lsp.parametric.routing; import com.google.gson.GsonBuilder; import java.io.IOException; @@ -35,6 +35,7 @@ import org.apache.logging.log4j.Logger; import org.rascalmpl.vscode.lsp.LanguageServerRouter; import org.rascalmpl.vscode.lsp.parametric.LanguageRegistry.LanguageParameter; +import org.rascalmpl.vscode.lsp.parametric.ParametricLanguageServer; import org.rascalmpl.vscode.lsp.util.NamedThreadPool; public class RoutingLanguageServer extends ParametricLanguageServer { diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/RoutingTextDocumentService.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingTextDocumentService.java similarity index 94% rename from rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/RoutingTextDocumentService.java rename to rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingTextDocumentService.java index 1428a588b..a35f4316a 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/RoutingTextDocumentService.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingTextDocumentService.java @@ -24,7 +24,7 @@ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ -package org.rascalmpl.vscode.lsp; +package org.rascalmpl.vscode.lsp.parametric.routing; import java.util.List; import java.util.concurrent.CompletableFuture; @@ -47,6 +47,11 @@ import org.eclipse.lsp4j.services.TextDocumentService; import org.rascalmpl.util.locations.ColumnMaps; import org.rascalmpl.util.locations.LineColumnOffsetMap; +import org.rascalmpl.vscode.lsp.BaseWorkspaceService; +import org.rascalmpl.vscode.lsp.IBaseLanguageServerExtensions; +import org.rascalmpl.vscode.lsp.IBaseTextDocumentService; +import org.rascalmpl.vscode.lsp.LanguageServerRouter; +import org.rascalmpl.vscode.lsp.TextDocumentState; import org.rascalmpl.vscode.lsp.parametric.LanguageRegistry.LanguageParameter; import org.rascalmpl.vscode.lsp.parametric.ParametricTextDocumentService; import org.rascalmpl.vscode.lsp.util.Caller; @@ -55,7 +60,7 @@ import io.usethesource.vallang.ISourceLocation; import io.usethesource.vallang.IValue; -class RoutingTextDocumentService implements IBaseTextDocumentService, Caller, Router> { +public class RoutingTextDocumentService implements IBaseTextDocumentService, Caller, Router> { private static final Logger logger = LogManager.getLogger(RoutingTextDocumentService.class); diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/RoutingWorkspaceService.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingWorkspaceService.java similarity index 86% rename from rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/RoutingWorkspaceService.java rename to rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingWorkspaceService.java index 44bef9b57..c0efe6b47 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/RoutingWorkspaceService.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingWorkspaceService.java @@ -24,13 +24,14 @@ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ -package org.rascalmpl.vscode.lsp; +package org.rascalmpl.vscode.lsp.parametric.routing; import java.util.concurrent.ExecutorService; +import org.rascalmpl.vscode.lsp.BaseWorkspaceService; -class RoutingWorkspaceService extends BaseWorkspaceService { +public class RoutingWorkspaceService extends BaseWorkspaceService { - protected RoutingWorkspaceService(ExecutorService exec) { + public RoutingWorkspaceService(ExecutorService exec) { super(exec); } diff --git a/rascal-vscode-extension/src/lsp/RascalLSPConnection.ts b/rascal-vscode-extension/src/lsp/RascalLSPConnection.ts index 0b269a907..d620edcb4 100644 --- a/rascal-vscode-extension/src/lsp/RascalLSPConnection.ts +++ b/rascal-vscode-extension/src/lsp/RascalLSPConnection.ts @@ -168,7 +168,7 @@ async function buildRascalServerOptions(jarPath: string, isParametricServer: boo ]; let mainClass: string; if (isParametricServer) { - mainClass = 'org.rascalmpl.vscode.lsp.parametric.RoutingLanguageServer'; + mainClass = 'org.rascalmpl.vscode.lsp.parametric.routing.RoutingLanguageServer'; commandArgs.push(calculateDSLMemoryReservation(dedicated)); } else { From 1fe0e436a13fa40fc25f51f15d6aecb62e12b419 Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Thu, 23 Apr 2026 16:56:28 +0200 Subject: [PATCH 010/107] Allow routing by extensions. --- .../vscode/lsp/LanguageServerRouter.java | 23 ++++++++++++------- .../routing/RoutingTextDocumentService.java | 20 +++++++++++----- 2 files changed, 29 insertions(+), 14 deletions(-) diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/LanguageServerRouter.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/LanguageServerRouter.java index 0b69b7f10..668ca26b7 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/LanguageServerRouter.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/LanguageServerRouter.java @@ -107,27 +107,34 @@ public LanguageServerRouter(Runnable onExit, ExecutorService exec) { @Override public CompletableFuture route(ISourceLocation file) { - return safeLanguage(file).map(this::languageByName).orElseThrow(() -> { - throw new UnsupportedOperationException(String.format("Rascal Parametric LSP has no support for this file, since no language is registered with extension '%s': %s", extension(file), file)); + return route(extension(file)); + } + + public CompletableFuture route(String extension) { + return safeLanguage(extension).map(this::languageByName).orElseThrow(() -> { + throw new UnsupportedOperationException(String.format("Rascal Parametric LSP has no support for this file, since no language is registered with extension '%s'", extension)); }); } private Optional safeLanguage(ISourceLocation loc) { - var ext = extension(loc); - if ("".equals(ext)) { + return safeLanguage(extension(loc)); + } + + private Optional safeLanguage(String extension) { + if ("".equals(extension)) { var languages = new HashSet<>(languagesByExtension.values()); if (languages.size() == 1) { - logger.trace("File was opened without an extension; falling back to the single registered language for: {}", loc); + logger.trace("File was opened without an extension; falling back to the single registered language for extension '{}'", extension); return languages.stream().findFirst(); } else { - logger.error("File was opened without an extension and there are multiple languages registered, so we cannot pick a fallback for: {}", loc); + logger.error("File was opened without an extension and there are multiple languages registered, so we cannot pick a fallback for extension '{}'", extension); return Optional.empty(); } } - return Optional.ofNullable(languagesByExtension.get(ext)); + return Optional.ofNullable(languagesByExtension.get(extension)); } - private static String extension(ISourceLocation doc) { + public static String extension(ISourceLocation doc) { return URIUtil.getExtension(doc); } diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingTextDocumentService.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingTextDocumentService.java index a35f4316a..aec3be544 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingTextDocumentService.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingTextDocumentService.java @@ -29,6 +29,7 @@ import java.util.List; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutorService; +import java.util.stream.Collectors; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; @@ -55,7 +56,9 @@ import org.rascalmpl.vscode.lsp.parametric.LanguageRegistry.LanguageParameter; import org.rascalmpl.vscode.lsp.parametric.ParametricTextDocumentService; import org.rascalmpl.vscode.lsp.util.Caller; +import org.rascalmpl.vscode.lsp.util.Lists; import org.rascalmpl.vscode.lsp.util.Router; +import org.rascalmpl.vscode.lsp.util.locations.Locations; import io.usethesource.vallang.ISourceLocation; import io.usethesource.vallang.IValue; @@ -202,20 +205,25 @@ public boolean isManagingFile(ISourceLocation file) { @Override public void didCreateFiles(CreateFilesParams params) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'didCreateFiles'"); + // TODO Mimick VS given certain file operation filters (capabilities) + var filesByExt = params.getFiles().stream() + .collect(Collectors.toMap(f -> LanguageServerRouter.extension(Locations.toLoc(f.getUri())), List::of, Lists::union)); + + for (var e : filesByExt.entrySet()) { + availableServer().route(e.getKey()).thenAccept(server -> { + server.getWorkspaceService().didCreateFiles(new CreateFilesParams(e.getValue())); + }); + } } @Override public void didRenameFiles(RenameFilesParams params, List workspaceFolders) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'didRenameFiles'"); + // TODO Mimick VS given certain file operation filters (capabilities) } @Override public void didDeleteFiles(DeleteFilesParams params) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'didDeleteFiles'"); + // TODO Mimick VS given certain file operation filters (capabilities) } @Override From d784529c00dbb1ef32cbae4d148b2ba6c03c194a Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Thu, 23 Apr 2026 21:28:34 +0200 Subject: [PATCH 011/107] Forward `textDocument/*` LSP calls to delegates. --- .../routing/RoutingTextDocumentService.java | 147 ++++++++++++++++++ .../org/rascalmpl/vscode/lsp/util/Caller.java | 4 + .../org/rascalmpl/vscode/lsp/util/Router.java | 5 + 3 files changed, 156 insertions(+) diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingTextDocumentService.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingTextDocumentService.java index aec3be544..8d0423f44 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingTextDocumentService.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingTextDocumentService.java @@ -34,16 +34,59 @@ import org.apache.logging.log4j.Logger; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.checkerframework.checker.nullness.qual.Nullable; +import org.eclipse.lsp4j.CallHierarchyIncomingCall; +import org.eclipse.lsp4j.CallHierarchyIncomingCallsParams; +import org.eclipse.lsp4j.CallHierarchyItem; +import org.eclipse.lsp4j.CallHierarchyOutgoingCall; +import org.eclipse.lsp4j.CallHierarchyOutgoingCallsParams; +import org.eclipse.lsp4j.CallHierarchyPrepareParams; import org.eclipse.lsp4j.ClientCapabilities; +import org.eclipse.lsp4j.CodeAction; +import org.eclipse.lsp4j.CodeActionParams; +import org.eclipse.lsp4j.CodeLens; +import org.eclipse.lsp4j.CodeLensParams; +import org.eclipse.lsp4j.Command; +import org.eclipse.lsp4j.CompletionItem; +import org.eclipse.lsp4j.CompletionList; +import org.eclipse.lsp4j.CompletionParams; import org.eclipse.lsp4j.CreateFilesParams; +import org.eclipse.lsp4j.DefinitionParams; import org.eclipse.lsp4j.DeleteFilesParams; import org.eclipse.lsp4j.DidChangeTextDocumentParams; import org.eclipse.lsp4j.DidCloseTextDocumentParams; import org.eclipse.lsp4j.DidOpenTextDocumentParams; import org.eclipse.lsp4j.DidSaveTextDocumentParams; +import org.eclipse.lsp4j.DocumentSymbol; +import org.eclipse.lsp4j.DocumentSymbolParams; +import org.eclipse.lsp4j.FoldingRange; +import org.eclipse.lsp4j.FoldingRangeRequestParams; +import org.eclipse.lsp4j.Hover; +import org.eclipse.lsp4j.HoverParams; +import org.eclipse.lsp4j.ImplementationParams; +import org.eclipse.lsp4j.InlayHint; +import org.eclipse.lsp4j.InlayHintParams; +import org.eclipse.lsp4j.Location; +import org.eclipse.lsp4j.LocationLink; +import org.eclipse.lsp4j.PrepareRenameDefaultBehavior; +import org.eclipse.lsp4j.PrepareRenameParams; +import org.eclipse.lsp4j.PrepareRenameResult; +import org.eclipse.lsp4j.Range; +import org.eclipse.lsp4j.ReferenceParams; import org.eclipse.lsp4j.RenameFilesParams; +import org.eclipse.lsp4j.RenameParams; +import org.eclipse.lsp4j.SelectionRange; +import org.eclipse.lsp4j.SelectionRangeParams; +import org.eclipse.lsp4j.SemanticTokens; +import org.eclipse.lsp4j.SemanticTokensDelta; +import org.eclipse.lsp4j.SemanticTokensDeltaParams; +import org.eclipse.lsp4j.SemanticTokensParams; +import org.eclipse.lsp4j.SemanticTokensRangeParams; import org.eclipse.lsp4j.ServerCapabilities; +import org.eclipse.lsp4j.SymbolInformation; +import org.eclipse.lsp4j.WorkspaceEdit; import org.eclipse.lsp4j.WorkspaceFolder; +import org.eclipse.lsp4j.jsonrpc.messages.Either; +import org.eclipse.lsp4j.jsonrpc.messages.Either3; import org.eclipse.lsp4j.services.LanguageClient; import org.eclipse.lsp4j.services.TextDocumentService; import org.rascalmpl.util.locations.ColumnMaps; @@ -232,4 +275,108 @@ public void cancelProgress(String progressId) { throw new UnsupportedOperationException("Unimplemented method 'cancelProgress'"); } + @Override + public CompletableFuture> callHierarchyIncomingCalls( + CallHierarchyIncomingCallsParams params) { + return callC(route(Locations.toLoc(params.getItem().getUri())), TextDocumentService::callHierarchyIncomingCalls, params); + } + + @Override + public CompletableFuture> callHierarchyOutgoingCalls( + CallHierarchyOutgoingCallsParams params) { + return callC(route(Locations.toLoc(params.getItem().getUri())), TextDocumentService::callHierarchyOutgoingCalls, params); + } + + @Override + public CompletableFuture, CompletionList>> completion(CompletionParams position) { + return callC(route(position.getTextDocument()), TextDocumentService::completion, position); + } + + @Override + public CompletableFuture, List>> definition( + DefinitionParams params) { + return callC(route(params.getTextDocument()), TextDocumentService::definition, params); + } + + @Override + public CompletableFuture> prepareCallHierarchy(CallHierarchyPrepareParams params) { + return callC(route(params.getTextDocument()), TextDocumentService::prepareCallHierarchy, params); + } + + @Override + public CompletableFuture semanticTokensFull(SemanticTokensParams params) { + return callC(route(params.getTextDocument()), TextDocumentService::semanticTokensFull, params); + } + + @Override + public CompletableFuture> semanticTokensFullDelta( + SemanticTokensDeltaParams params) { + return callC(route(params.getTextDocument()), TextDocumentService::semanticTokensFullDelta, params); + } + + @Override + public CompletableFuture semanticTokensRange(SemanticTokensRangeParams params) { + return callC(route(params.getTextDocument()), TextDocumentService::semanticTokensRange, params); + } + + @Override + public CompletableFuture> codeLens(CodeLensParams params) { + return callC(route(params.getTextDocument()), TextDocumentService::codeLens, params); + } + + @Override + public CompletableFuture> prepareRename( + PrepareRenameParams params) { + return callC(route(params.getTextDocument()), TextDocumentService::prepareRename, params); + } + + @Override + public CompletableFuture rename(RenameParams params) { + return callC(route(params.getTextDocument()), TextDocumentService::rename, params); + } + + @Override + public CompletableFuture> inlayHint(InlayHintParams params) { + return callC(route(params.getTextDocument()), TextDocumentService::inlayHint, params); + } + + @Override + public CompletableFuture>> codeAction(CodeActionParams params) { + return callC(route(params.getTextDocument()), TextDocumentService::codeAction, params); + } + + @Override + public CompletableFuture>> documentSymbol( + DocumentSymbolParams params) { + return callC(route(params.getTextDocument()), TextDocumentService::documentSymbol, params); + } + + @Override + public CompletableFuture, List>> implementation( + ImplementationParams params) { + return callC(route(params.getTextDocument()), TextDocumentService::implementation, params); + } + + @Override + public CompletableFuture> references(ReferenceParams params) { + return callC(route(params.getTextDocument()), TextDocumentService::references, params); + } + + @Override + public CompletableFuture> foldingRange(FoldingRangeRequestParams params) { + return callC(route(params.getTextDocument()), TextDocumentService::foldingRange, params); + } + + @Override + public CompletableFuture hover(HoverParams params) { + return callC(route(params.getTextDocument()), TextDocumentService::hover, params); + } + + @Override + public CompletableFuture> selectionRange(SelectionRangeParams params) { + return callC(route(params.getTextDocument()), TextDocumentService::selectionRange, params); + } + + + } diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/util/Caller.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/util/Caller.java index 871eebbdf..3685995cd 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/util/Caller.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/util/Caller.java @@ -32,6 +32,10 @@ public interface Caller { + default CompletableFuture callC(CompletableFuture t, BiFunction> func, A arg) { + return t.thenCompose(actualT -> func.apply(actualT, arg)); + } + default CompletableFuture call(CompletableFuture t, BiFunction func, A arg) { return t.thenApply(actualT -> func.apply(actualT, arg)); } diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/util/Router.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/util/Router.java index 6e3e6d8e2..81f0de44b 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/util/Router.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/util/Router.java @@ -26,6 +26,7 @@ */ package org.rascalmpl.vscode.lsp.util; +import org.eclipse.lsp4j.TextDocumentIdentifier; import org.eclipse.lsp4j.TextDocumentItem; import org.eclipse.lsp4j.VersionedTextDocumentIdentifier; import org.rascalmpl.vscode.lsp.util.locations.Locations; @@ -42,4 +43,8 @@ default T route(TextDocumentItem doc) { default T route(VersionedTextDocumentIdentifier id) { return route(Locations.toLoc(id.getUri())); } + + default T route(TextDocumentIdentifier id) { + return route(Locations.toLoc(id.getUri())); + } } From 3a4b347ba8ce42463ddb5cef34ea9f1ce083bb0c Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Thu, 23 Apr 2026 21:57:02 +0200 Subject: [PATCH 012/107] Start multiple delegate servers on predefined ports. --- rascal-lsp/.vscode/launch.json | 20 ++++++++++++++++--- .../parametric/ParametricLanguageServer.java | 20 ++++++++++--------- 2 files changed, 28 insertions(+), 12 deletions(-) diff --git a/rascal-lsp/.vscode/launch.json b/rascal-lsp/.vscode/launch.json index 07f179fc6..5d25815d2 100644 --- a/rascal-lsp/.vscode/launch.json +++ b/rascal-lsp/.vscode/launch.json @@ -27,12 +27,26 @@ "-Drascal.fallbackResolver=org.rascalmpl.vscode.lsp.uri.FallbackResolver" ] }, - { + { + "type": "java", + "name": "Delegate Parametric Server [1]", + "request": "launch", + "mainClass": "org.rascalmpl.vscode.lsp.parametric.ParametricLanguageServer", + "args": ["--port", "9990"], + "projectName": "rascal-lsp", + "console": "internalConsole", + "vmArgs": [ + "-Dlog4j2.level=TRACE", + "-Drascal.compilerClasspath=${workspaceFolder}/target/lib/rascal.jar", + "-Drascal.fallbackResolver=org.rascalmpl.vscode.lsp.uri.FallbackResolver" + ] + }, + { "type": "java", - "name": "Parametric Server (routing delegate 1)", + "name": "Delegate Parametric Server [2]", "request": "launch", "mainClass": "org.rascalmpl.vscode.lsp.parametric.ParametricLanguageServer", - "args": "9990", + "args": ["--port", "9991"], "projectName": "rascal-lsp", "console": "internalConsole", "vmArgs": [ diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricLanguageServer.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricLanguageServer.java index 55a085bf1..98a54f2f4 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricLanguageServer.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricLanguageServer.java @@ -46,16 +46,18 @@ protected static void start(int portNumber, LanguageParameter dedicatedLanguage) } public static void main(String[] args) { - int portNumber = args.length == 0 - ? DEFAULT_PORT_NUMBER - : Integer.parseInt(args[0]); + int portNumber = DEFAULT_PORT_NUMBER; + LanguageParameter dedicatedLanguage = null; - LanguageParameter dedicatedLanguage; - if (args.length > 1) { - dedicatedLanguage = new GsonBuilder().create().fromJson(args[0], LanguageParameter.class); - } - else { - dedicatedLanguage = null; + for (int i = 0; i < args.length; i++) { + switch (args[i]) { + case "--port": + portNumber = Integer.parseInt(args[++i]); + break; + default: + dedicatedLanguage = new GsonBuilder().create().fromJson(args[0], LanguageParameter.class); + break; + } } start(portNumber, dedicatedLanguage); From 381b8470f5cf498c43509370c36440f3512dc07a Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Fri, 24 Apr 2026 15:15:31 +0200 Subject: [PATCH 013/107] Add deployed launch config. --- rascal-vscode-extension/.vscode/launch.json | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/rascal-vscode-extension/.vscode/launch.json b/rascal-vscode-extension/.vscode/launch.json index 1dc41c2a9..bb764573b 100644 --- a/rascal-vscode-extension/.vscode/launch.json +++ b/rascal-vscode-extension/.vscode/launch.json @@ -37,6 +37,22 @@ }, "preLaunchTask": "${defaultBuildTask}" }, + { + "name": "Run Extension (deployed)", + "type": "extensionHost", + "request": "launch", + "args": [ + "--disable-extensions", + "--extensionDevelopmentPath=${workspaceFolder}" + ], + "outFiles": [ + "${workspaceFolder}/out/**/*.js" + ], + "env": { + "RASCAL_LSP_DEV": "false" + }, + "preLaunchTask": "${defaultBuildTask}" + }, { "name": "Extension Tests", "type": "extensionHost", From 8b3fbc123222aca6d39c52aaa3a79ba7155d186f Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Fri, 24 Apr 2026 15:18:52 +0200 Subject: [PATCH 014/107] Fix deployed running and cleanup. --- .../vscode/lsp/LanguageServerRouter.java | 29 ++++++++++++++----- .../routing/RoutingLanguageServer.java | 2 +- .../routing/RoutingTextDocumentService.java | 3 +- 3 files changed, 23 insertions(+), 11 deletions(-) diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/LanguageServerRouter.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/LanguageServerRouter.java index 668ca26b7..c09dd6285 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/LanguageServerRouter.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/LanguageServerRouter.java @@ -31,6 +31,7 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.lang.ProcessBuilder.Redirect; import java.net.InetAddress; import java.net.Socket; import java.net.URI; @@ -68,10 +69,12 @@ import org.eclipse.lsp4j.WorkDoneProgressCreateParams; import org.eclipse.lsp4j.WorkspaceFolder; import org.eclipse.lsp4j.jsonrpc.Launcher; +import org.rascalmpl.ideservices.GsonUtils; import org.rascalmpl.uri.URIUtil; import org.rascalmpl.vscode.lsp.parametric.LanguageRegistry.LanguageParameter; import org.rascalmpl.vscode.lsp.parametric.routing.RoutingTextDocumentService; import org.rascalmpl.vscode.lsp.parametric.routing.RoutingWorkspaceService; +import org.rascalmpl.vscode.lsp.util.NamedThreadPool; import org.rascalmpl.vscode.lsp.util.Router; import io.usethesource.vallang.IInteger; @@ -140,24 +143,29 @@ public static String extension(ISourceLocation doc) { private @Nullable CompletableFuture startServer(LanguageParameter lang) { try { - var classPath = System.getProperty("java.class.path"); InputStream in; OutputStream out; Runnable onExit; if (DEPLOY_MODE) { + // TODO Figure out Rascal/Rascal-LSP versions/class path + logger.info("Starting LSP process for {}", lang.getName()); + logger.debug("{} runs with Rascal {} and Rascal-LSP {}", lang.getName(), "x.xx.xx", "y.yy.yy"); // In deployment, we start a process and connect to it via input/output streams - var proc = new ProcessBuilder("java" + var classPath = System.getProperty("java.class.path"); + var proc = new ProcessBuilder(ProcessHandle.current().info().command().get() , "-Dlog4j2.configurationFactory=org.rascalmpl.vscode.lsp.log.LogJsonConfiguration" , "-Dlog4j2.level=DEBUG" , "-Drascal.fallbackResolver=org.rascalmpl.vscode.lsp.uri.FallbackResolver" - , "-Drascal.lsp.deploy=" + DEPLOY_MODE + , "-Drascal.lsp.deploy=true" , "-Drascal.compilerClasspath=" + classPath - , "-cp" + classPath + , "-Xmx2048M" + , "-cp", classPath , "org.rascalmpl.vscode.lsp.parametric.ParametricLanguageServer" - ).start(); + ).redirectError(Redirect.INHERIT).start(); + + logger.debug("Launched language server on process {}", proc.pid()); in = proc.getInputStream(); out = proc.getOutputStream(); - // TODO Do we need to close the process here? Or is this triggered after the process exits? onExit = () -> {}; } else { // In development, we expect the server to have been launched on a pre-agreed port @@ -168,9 +176,10 @@ public static String extension(ISourceLocation doc) { out = socket.getOutputStream(); onExit = () -> { try { + logger.debug("Closing socket for language {} on port", lang.getName(), port); socket.close(); } catch (IOException e) { - logger.error("Closing socket for language {} on port {} failed", lang.getName(), port); + logger.error("Closing socket failed", lang.getName(), port); } }; } @@ -179,9 +188,12 @@ public static String extension(ISourceLocation doc) { .setLocalService(this) .setInput(in) .setOutput(out) + .configureGson(GsonUtils.complexAsJsonObject()) // Only needed if we want to communicate IValues + .setExecutorService(NamedThreadPool.single("parametric-lsp-router-out")) .create(); - scheduleShutdown(serverLauncher.startListening(), lang, onExit); + var runner = serverLauncher.startListening(); + scheduleShutdown(runner, lang, onExit); var server = serverLauncher.getRemoteProxy(); var delegateServerCaps = server.initialize(delegateInitializationParams()); @@ -197,6 +209,7 @@ private void scheduleShutdown(Future server, LanguageParameter lang, Runna getExecutor().execute(() -> { try { server.get(); + logger.info("Language server for {} terminated gracefully", lang.getName()); } catch (CancellationException | ExecutionException | InterruptedException e) { logger.error("Language server for {} crashed", lang.getName(), e); } diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingLanguageServer.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingLanguageServer.java index 9a816025f..839300dc5 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingLanguageServer.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingLanguageServer.java @@ -66,7 +66,7 @@ public static void main(String[] args) { var dedicatedLanguage = new GsonBuilder().create().fromJson(args[0], LanguageParameter.class); start(DEFAULT_PORT_NUMBER, dedicatedLanguage); } else { - startLanguageServer(NamedThreadPool.single("parametric-lsp-router") + startLanguageServer(NamedThreadPool.single("parametric-lsp-router-in") , NamedThreadPool.cached("parametric-router") , DEFAULT_PORT_NUMBER ); diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingTextDocumentService.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingTextDocumentService.java index 8d0423f44..3563f64d5 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingTextDocumentService.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingTextDocumentService.java @@ -174,8 +174,7 @@ public void initializeServerCapabilities(ClientCapabilities clientCapabilities, @Override public void shutdown() { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'shutdown'"); + availableServer().shutdown(); } @Override From 8d5bae3775ac63fd8983c455d9cd741a34c42fc0 Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Fri, 24 Apr 2026 15:44:55 +0200 Subject: [PATCH 015/107] Fix lint errors and nulls. --- .../vscode/lsp/LanguageServerRouter.java | 129 +++++++++--------- .../parametric/ParametricLanguageServer.java | 3 +- .../routing/RoutingTextDocumentService.java | 2 +- 3 files changed, 70 insertions(+), 64 deletions(-) diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/LanguageServerRouter.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/LanguageServerRouter.java index c09dd6285..bdcea0c2d 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/LanguageServerRouter.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/LanguageServerRouter.java @@ -141,68 +141,64 @@ public static String extension(ISourceLocation doc) { return URIUtil.getExtension(doc); } - private @Nullable CompletableFuture startServer(LanguageParameter lang) { - try { - InputStream in; - OutputStream out; - Runnable onExit; - if (DEPLOY_MODE) { - // TODO Figure out Rascal/Rascal-LSP versions/class path - logger.info("Starting LSP process for {}", lang.getName()); - logger.debug("{} runs with Rascal {} and Rascal-LSP {}", lang.getName(), "x.xx.xx", "y.yy.yy"); - // In deployment, we start a process and connect to it via input/output streams - var classPath = System.getProperty("java.class.path"); - var proc = new ProcessBuilder(ProcessHandle.current().info().command().get() - , "-Dlog4j2.configurationFactory=org.rascalmpl.vscode.lsp.log.LogJsonConfiguration" - , "-Dlog4j2.level=DEBUG" - , "-Drascal.fallbackResolver=org.rascalmpl.vscode.lsp.uri.FallbackResolver" - , "-Drascal.lsp.deploy=true" - , "-Drascal.compilerClasspath=" + classPath - , "-Xmx2048M" - , "-cp", classPath - , "org.rascalmpl.vscode.lsp.parametric.ParametricLanguageServer" - ).redirectError(Redirect.INHERIT).start(); - - logger.debug("Launched language server on process {}", proc.pid()); - in = proc.getInputStream(); - out = proc.getOutputStream(); - onExit = () -> {}; - } else { - // In development, we expect the server to have been launched on a pre-agreed port - int port = getNextPort(); - Socket socket = new Socket(InetAddress.getLoopbackAddress(), port); - socket.setTcpNoDelay(true); - in = socket.getInputStream(); - out = socket.getOutputStream(); - onExit = () -> { - try { - logger.debug("Closing socket for language {} on port", lang.getName(), port); - socket.close(); - } catch (IOException e) { - logger.error("Closing socket failed", lang.getName(), port); - } - }; - } - var serverLauncher = new Launcher.Builder() - .setRemoteInterface(IBaseLanguageServerExtensions.class) - .setLocalService(this) - .setInput(in) - .setOutput(out) - .configureGson(GsonUtils.complexAsJsonObject()) // Only needed if we want to communicate IValues - .setExecutorService(NamedThreadPool.single("parametric-lsp-router-out")) - .create(); - - var runner = serverLauncher.startListening(); - scheduleShutdown(runner, lang, onExit); - var server = serverLauncher.getRemoteProxy(); - var delegateServerCaps = server.initialize(delegateInitializationParams()); - - // When initialization is done, we can use the server - return delegateServerCaps.thenApply(_c -> server); - } catch (IOException e) { - logger.fatal(e); - return null; + private CompletableFuture startServer(LanguageParameter lang) throws IOException { + InputStream in; + OutputStream out; + Runnable onExit; + if (DEPLOY_MODE) { + // TODO Figure out Rascal/Rascal-LSP versions/class path + logger.info("Starting LSP process for {}", lang.getName()); + logger.debug("{} runs with Rascal {} and Rascal-LSP {}", lang.getName(), "x.xx.xx", "y.yy.yy"); + // In deployment, we start a process and connect to it via input/output streams + var classPath = System.getProperty("java.class.path"); + var javaCmd = ProcessHandle.current().info().command().orElse("java"); + var proc = new ProcessBuilder(javaCmd + , "-Dlog4j2.configurationFactory=org.rascalmpl.vscode.lsp.log.LogJsonConfiguration" + , "-Dlog4j2.level=DEBUG" + , "-Drascal.fallbackResolver=org.rascalmpl.vscode.lsp.uri.FallbackResolver" + , "-Drascal.lsp.deploy=true" + , "-Drascal.compilerClasspath=" + classPath + , "-Xmx2048M" + , "-cp", classPath + , "org.rascalmpl.vscode.lsp.parametric.ParametricLanguageServer" + ).redirectError(Redirect.INHERIT).start(); + + logger.debug("Launched language server on process {}", proc.pid()); + in = proc.getInputStream(); + out = proc.getOutputStream(); + onExit = () -> {}; + } else { + // In development, we expect the server to have been launched on a pre-agreed port + int port = getNextPort(); + Socket socket = new Socket(InetAddress.getLoopbackAddress(), port); + socket.setTcpNoDelay(true); + in = socket.getInputStream(); + out = socket.getOutputStream(); + onExit = () -> { + try { + logger.debug("Closing socket for language {} on port", lang.getName(), port); + socket.close(); + } catch (IOException e) { + logger.error("Closing socket failed", lang.getName(), port); + } + }; } + var serverLauncher = new Launcher.Builder() + .setRemoteInterface(IBaseLanguageServerExtensions.class) + .setLocalService(this) + .setInput(in) + .setOutput(out) + .configureGson(GsonUtils.complexAsJsonObject()) // Only needed if we want to communicate IValues + .setExecutorService(NamedThreadPool.single("parametric-lsp-router-out")) + .create(); + + var runner = serverLauncher.startListening(); + scheduleShutdown(runner, lang, onExit); + var server = serverLauncher.getRemoteProxy(); + var delegateServerCaps = server.initialize(delegateInitializationParams()); + + // When initialization is done, we can use the server + return delegateServerCaps.thenApply(_c -> server); } private void scheduleShutdown(Future server, LanguageParameter lang, Runnable onExit) { @@ -212,6 +208,8 @@ private void scheduleShutdown(Future server, LanguageParameter lang, Runna logger.info("Language server for {} terminated gracefully", lang.getName()); } catch (CancellationException | ExecutionException | InterruptedException e) { logger.error("Language server for {} crashed", lang.getName(), e); + // TODO Remove from the map? Attempt a restart with the same parameters? + Thread.currentThread().interrupt(); } try { onExit.run(); @@ -271,7 +269,14 @@ public CompletableFuture initialize(InitializeParams params) { public synchronized CompletableFuture sendRegisterLanguage(LanguageParameter lang) { // If we do not have a parametric server running for this language, start and initialize it. synchronized (this) { - languageServers.computeIfAbsent(lang.getName(), _n -> startServer(lang)); + languageServers.computeIfAbsent(lang.getName(), _n -> { + try { + return startServer(lang); + } catch (IOException e) { + logger.error("Unexpected error while starting language server for {}", lang.getName(), e); + return null; + } + }); for (var ext : lang.getExtensions()) { languagesByExtension.put(ext, lang.getName()); } diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricLanguageServer.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricLanguageServer.java index 98a54f2f4..aaa880258 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricLanguageServer.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricLanguageServer.java @@ -28,6 +28,7 @@ import com.google.gson.GsonBuilder; +import org.checkerframework.checker.nullness.qual.Nullable; import org.rascalmpl.vscode.lsp.BaseLanguageServer; import org.rascalmpl.vscode.lsp.parametric.LanguageRegistry.LanguageParameter; import org.rascalmpl.vscode.lsp.util.NamedThreadPool; @@ -36,7 +37,7 @@ public class ParametricLanguageServer extends BaseLanguageServer { protected static final int DEFAULT_PORT_NUMBER = 9999; - protected static void start(int portNumber, LanguageParameter dedicatedLanguage) { + protected static void start(int portNumber, @Nullable LanguageParameter dedicatedLanguage) { startLanguageServer(NamedThreadPool.single("parametric-lsp") , NamedThreadPool.cached("parametric") , threadPool -> new ParametricTextDocumentService(threadPool, dedicatedLanguage) diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingTextDocumentService.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingTextDocumentService.java index 3563f64d5..6f2654486 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingTextDocumentService.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingTextDocumentService.java @@ -367,7 +367,7 @@ public CompletableFuture> foldingRange(FoldingRangeRequestPar } @Override - public CompletableFuture hover(HoverParams params) { + public CompletableFuture<@Nullable Hover> hover(HoverParams params) { return callC(route(params.getTextDocument()), TextDocumentService::hover, params); } From 0f434343a2edc629d6e1aea49a3d47c70559c198 Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Fri, 24 Apr 2026 16:10:22 +0200 Subject: [PATCH 016/107] Only interrupt on interruption exception. --- .../java/org/rascalmpl/vscode/lsp/LanguageServerRouter.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/LanguageServerRouter.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/LanguageServerRouter.java index bdcea0c2d..3b916745b 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/LanguageServerRouter.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/LanguageServerRouter.java @@ -206,9 +206,10 @@ private void scheduleShutdown(Future server, LanguageParameter lang, Runna try { server.get(); logger.info("Language server for {} terminated gracefully", lang.getName()); - } catch (CancellationException | ExecutionException | InterruptedException e) { + } catch (CancellationException | ExecutionException e) { logger.error("Language server for {} crashed", lang.getName(), e); // TODO Remove from the map? Attempt a restart with the same parameters? + } catch (InterruptedException e) { Thread.currentThread().interrupt(); } try { From 40e0dc9332082c6cbbe86ca42dfe78bc1314651b Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Fri, 24 Apr 2026 16:15:38 +0200 Subject: [PATCH 017/107] Shut down child processes on exit. --- .../java/org/rascalmpl/vscode/lsp/LanguageServerRouter.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/LanguageServerRouter.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/LanguageServerRouter.java index 3b916745b..f8061febd 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/LanguageServerRouter.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/LanguageServerRouter.java @@ -35,6 +35,7 @@ import java.net.InetAddress; import java.net.Socket; import java.net.URI; +import java.util.Collection; import java.util.HashSet; import java.util.List; import java.util.Map; @@ -42,6 +43,7 @@ import java.util.concurrent.CancellationException; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Future; @@ -87,6 +89,7 @@ public class LanguageServerRouter extends BaseLanguageServer.ActualLanguageServe private final Map languagesByExtension; private final Map> languageServers; + private final Collection delegateProcesses = new CopyOnWriteArrayList<>(); private @MonotonicNonNull InitializeParams initializeParams; @@ -98,6 +101,9 @@ public LanguageServerRouter(Runnable onExit, ExecutorService exec) { this.languagesByExtension = new ConcurrentHashMap<>(); this.languageServers = new ConcurrentHashMap<>(); + + // Shutdown child processes when we exit + Runtime.getRuntime().addShutdownHook(new Thread(() -> delegateProcesses.forEach(Process::destroy))); } /*package*/ public CompletableFuture languageByName(String lang) { From bd14c052e91130e0ae5640d65d9470d9227ffaf8 Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Tue, 28 Apr 2026 10:14:48 +0200 Subject: [PATCH 018/107] Implement command execution. --- .../org/rascalmpl/vscode/lsp/BaseLanguageServer.java | 5 +++++ .../vscode/lsp/IBaseLanguageServerExtensions.java | 4 ++++ .../rascalmpl/vscode/lsp/IBaseTextDocumentService.java | 2 ++ .../parametric/routing/RoutingTextDocumentService.java | 10 +++++----- 4 files changed, 16 insertions(+), 5 deletions(-) diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/BaseLanguageServer.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/BaseLanguageServer.java index d4660b1fa..ed66c535b 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/BaseLanguageServer.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/BaseLanguageServer.java @@ -303,6 +303,11 @@ public IBaseTextDocumentService getTextDocumentService() { return lspDocumentService; } + @Override + public IBaseTextDocumentService getIBaseTextDocumentService() { + return lspDocumentService; + } + @Override public BaseWorkspaceService getWorkspaceService() { return lspWorkspaceService; diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/IBaseLanguageServerExtensions.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/IBaseLanguageServerExtensions.java index 47693efef..4bbef2ffb 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/IBaseLanguageServerExtensions.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/IBaseLanguageServerExtensions.java @@ -30,6 +30,7 @@ import java.util.concurrent.CompletableFuture; import org.eclipse.lsp4j.jsonrpc.messages.Tuple.Two; +import org.eclipse.lsp4j.jsonrpc.services.JsonDelegate; import org.eclipse.lsp4j.jsonrpc.services.JsonNotification; import org.eclipse.lsp4j.jsonrpc.services.JsonRequest; import org.eclipse.lsp4j.services.LanguageServer; @@ -63,4 +64,7 @@ default CompletableFuture[]> supplyPathConfig(PathConfigParam @JsonNotification("rascal/logLevel") void setMinimumLogLevel(String level); + + @JsonDelegate + IBaseTextDocumentService getIBaseTextDocumentService(); } diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/IBaseTextDocumentService.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/IBaseTextDocumentService.java index d7fb14096..aa55f5ba2 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/IBaseTextDocumentService.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/IBaseTextDocumentService.java @@ -36,6 +36,7 @@ import org.eclipse.lsp4j.RenameFilesParams; import org.eclipse.lsp4j.ServerCapabilities; import org.eclipse.lsp4j.WorkspaceFolder; +import org.eclipse.lsp4j.jsonrpc.services.JsonRequest; import org.eclipse.lsp4j.services.LanguageClient; import org.eclipse.lsp4j.services.TextDocumentService; import org.rascalmpl.util.locations.ColumnMaps; @@ -60,6 +61,7 @@ public interface IBaseTextDocumentService extends TextDocumentService { void projectAdded(String name, ISourceLocation projectRoot); void projectRemoved(String name, ISourceLocation projectRoot); + @JsonRequest("executeRascalCommand") CompletableFuture executeCommand(String languageName, String command); LineColumnOffsetMap getColumnMap(ISourceLocation file); ColumnMaps getColumnMaps(); diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingTextDocumentService.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingTextDocumentService.java index 6f2654486..5cad09a42 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingTextDocumentService.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingTextDocumentService.java @@ -106,7 +106,7 @@ import io.usethesource.vallang.ISourceLocation; import io.usethesource.vallang.IValue; -public class RoutingTextDocumentService implements IBaseTextDocumentService, Caller, Router> { +public class RoutingTextDocumentService implements IBaseTextDocumentService, Caller, Router> { private static final Logger logger = LogManager.getLogger(RoutingTextDocumentService.class); @@ -125,10 +125,10 @@ public void setServer(LanguageServerRouter server) { } @Override - public CompletableFuture route(ISourceLocation loc) { + public CompletableFuture route(ISourceLocation loc) { return availableServer() .route(loc) - .thenApply(IBaseLanguageServerExtensions::getTextDocumentService); + .thenApply(IBaseLanguageServerExtensions::getIBaseTextDocumentService); } private LanguageClient availableClient() { @@ -217,8 +217,8 @@ public void projectRemoved(String name, ISourceLocation projectRoot) { @Override public CompletableFuture executeCommand(String languageName, String command) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'executeCommand'"); + return availableServer().languageByName(languageName) + .thenCompose(s -> s.getIBaseTextDocumentService().executeCommand(languageName, command)); } @Override From d5d3e1b29c91078991cf8516c48f5e20c7703c7e Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Tue, 28 Apr 2026 13:46:48 +0200 Subject: [PATCH 019/107] Print port number in log message. --- .../main/java/org/rascalmpl/vscode/lsp/BaseLanguageServer.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/BaseLanguageServer.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/BaseLanguageServer.java index ed66c535b..2de326cf2 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/BaseLanguageServer.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/BaseLanguageServer.java @@ -325,7 +325,7 @@ public void connect(LanguageClient client) { lspDocumentService.connect(this.client); lspWorkspaceService.connect(this.client); remoteIDEServicesConfiguration = RemoteIDEServicesThread.startRemoteIDEServicesServer(client, lspDocumentService, executor); - logger.debug("Remote IDE Services Port {}", remoteIDEServicesConfiguration); + logger.debug("Remote IDE Services Port {}", remoteIDEServicesConfiguration.getPort()); } @Override From 60a350950570dda6ccc54f7f1c79aa5383a4ca4a Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Tue, 28 Apr 2026 13:50:38 +0200 Subject: [PATCH 020/107] Prepare for class path based on POM. --- .../vscode/lsp/LanguageServerRouter.java | 53 +++++++++++++++++-- .../library/demo/lang/pico/LanguageServer.rsc | 6 ++- 2 files changed, 53 insertions(+), 6 deletions(-) diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/LanguageServerRouter.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/LanguageServerRouter.java index f8061febd..3637f9ba6 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/LanguageServerRouter.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/LanguageServerRouter.java @@ -73,6 +73,7 @@ import org.eclipse.lsp4j.jsonrpc.Launcher; import org.rascalmpl.ideservices.GsonUtils; import org.rascalmpl.uri.URIUtil; +import org.rascalmpl.util.maven.Artifact; import org.rascalmpl.vscode.lsp.parametric.LanguageRegistry.LanguageParameter; import org.rascalmpl.vscode.lsp.parametric.routing.RoutingTextDocumentService; import org.rascalmpl.vscode.lsp.parametric.routing.RoutingWorkspaceService; @@ -147,6 +148,50 @@ public static String extension(ISourceLocation doc) { return URIUtil.getExtension(doc); } + private static boolean isRascalLspProject(Artifact art) { + var c = art.getCoordinate(); + if (!c.getGroupId().equals("org.rascalmpl")) { + return false; + } + var id = c.getArtifactId(); + return "rascal-lsp".equals(id); + } + + private String classPath(LanguageParameter lang) { + // TODO Build class path based on POM + /* + try { + var pcfg = PathConfig.parse(lang.getPathConfig()); + var pom = Locations.toPhysicalIfPossible(URIUtil.getChildLocation(pcfg.getProjectRoot(), "pom.xml")); + var p = new MavenParser(Path.of(pom.getURI())); + var rootProject = p.parseProject(); + + // Check if we are in Rascal-LSP + var classPath = new StringBuilder(); + if (isRascalLspProject(rootProject)) { + classPath.append(';'); + classPath.append(Path.of(Locations.toUri(pcfg.getBin()))); + } + + // Check the project dependencies + var deps = rootProject.resolveDependencies(Scope.COMPILE, p); + for (var d : deps) { + if (d.getResolved() != null) { + classPath.append(';'); + classPath.append(d.getResolved()); + } + } + // strip of the initial separator ';' + return classPath.substring(1).toString(); + } catch (IOException e) { + logger.error("Error while parsing path config {}", lang.getPathConfig(), e); + } catch (ModelResolutionError e) { + logger.error("Error while parsing Maven project {}", e); + } + */ + return System.getProperty("java.class.path"); + } + private CompletableFuture startServer(LanguageParameter lang) throws IOException { InputStream in; OutputStream out; @@ -154,11 +199,11 @@ private CompletableFuture startServer(LanguagePar if (DEPLOY_MODE) { // TODO Figure out Rascal/Rascal-LSP versions/class path logger.info("Starting LSP process for {}", lang.getName()); - logger.debug("{} runs with Rascal {} and Rascal-LSP {}", lang.getName(), "x.xx.xx", "y.yy.yy"); + + var classPath = classPath(lang); + logger.debug("{} runs with class path {}", lang.getName(), classPath); // In deployment, we start a process and connect to it via input/output streams - var classPath = System.getProperty("java.class.path"); - var javaCmd = ProcessHandle.current().info().command().orElse("java"); - var proc = new ProcessBuilder(javaCmd + var proc = new ProcessBuilder(ProcessHandle.current().info().command().orElse("java") , "-Dlog4j2.configurationFactory=org.rascalmpl.vscode.lsp.log.LogJsonConfiguration" , "-Dlog4j2.level=DEBUG" , "-Drascal.fallbackResolver=org.rascalmpl.vscode.lsp.uri.FallbackResolver" diff --git a/rascal-lsp/src/main/rascal/library/demo/lang/pico/LanguageServer.rsc b/rascal-lsp/src/main/rascal/library/demo/lang/pico/LanguageServer.rsc index f53840d72..9d290953a 100644 --- a/rascal-lsp/src/main/rascal/library/demo/lang/pico/LanguageServer.rsc +++ b/rascal-lsp/src/main/rascal/library/demo/lang/pico/LanguageServer.rsc @@ -361,9 +361,11 @@ in the presence of error trees. See ((util::LanguageServer)) for more details. Any feedback (errors and exceptions) is faster and more clearly printed in the terminal. } void main(bool errorRecovery=false) { + root = resolveLocation(|project://rascal-lsp|); + pcfg = pathConfig(projectRoot = root, bin = root + "target/classes"); registerLanguage( language( - pathConfig(), + pcfg, "Pico", {"pico", "pico-new"}, "demo::lang::pico::LanguageServer", @@ -372,7 +374,7 @@ void main(bool errorRecovery=false) { ); registerLanguage( language( - pathConfig(), + pcfg, "Pico", {"pico", "pico-new"}, "demo::lang::pico::LanguageServer", From 410c2cb955e3d9d85a30631f736230cc4d58d195 Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Tue, 28 Apr 2026 16:01:33 +0200 Subject: [PATCH 021/107] Log language registration calls. --- .../java/org/rascalmpl/vscode/lsp/BaseLanguageServer.java | 2 ++ .../java/org/rascalmpl/vscode/lsp/LanguageServerRouter.java | 4 ++++ .../lsp/parametric/routing/RoutingTextDocumentService.java | 2 ++ 3 files changed, 8 insertions(+) diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/BaseLanguageServer.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/BaseLanguageServer.java index 2de326cf2..8f3263a18 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/BaseLanguageServer.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/BaseLanguageServer.java @@ -255,10 +255,12 @@ public CompletableFuture[]> supplyPathConfig(PathConfigParame @Override public CompletableFuture sendRegisterLanguage(LanguageParameter lang) { + logger.debug("rascal/sendRegisterLanguage({}, {})", lang.getName(), lang.getMainFunction()); return CompletableFuture.runAsync(() -> lspDocumentService.registerLanguage(lang), executor); } @Override public CompletableFuture sendUnregisterLanguage(LanguageParameter lang) { + logger.debug("rascal/sendUnregisterLanguage({})", lang.getName()); return CompletableFuture.runAsync(() -> lspDocumentService.unregisterLanguage(lang), executor); } diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/LanguageServerRouter.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/LanguageServerRouter.java index 3637f9ba6..eb101b2f9 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/LanguageServerRouter.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/LanguageServerRouter.java @@ -319,6 +319,7 @@ public CompletableFuture initialize(InitializeParams params) { @Override public synchronized CompletableFuture sendRegisterLanguage(LanguageParameter lang) { + logger.debug("rascal/sendRegisterLanguage({}, {})", lang.getName(), lang.getMainFunction()); // If we do not have a parametric server running for this language, start and initialize it. synchronized (this) { languageServers.computeIfAbsent(lang.getName(), _n -> { @@ -339,6 +340,7 @@ public synchronized CompletableFuture sendRegisterLanguage(LanguageParamet @Override public synchronized CompletableFuture sendUnregisterLanguage(LanguageParameter lang) { + logger.debug("rascal/sendUnregisterLanguage({})", lang.getName()); // TODO Handle shutting down the remote parametric server iff it is empty now. return super.sendUnregisterLanguage(lang); } @@ -375,11 +377,13 @@ public void showContent(URI uri, IString title, IInteger viewColumn) { @Override public void receiveRegisterLanguage(LanguageParameter lang) { + logger.debug("rascal/receiveRegisterLanguage({}, {})", lang.getName(), lang.getMainFunction()); availableClient().receiveRegisterLanguage(lang); } @Override public void receiveUnregisterLanguage(LanguageParameter lang) { + logger.debug("rascal/receiveUnregisterLanguage({}, {})", lang.getName(), lang.getMainFunction()); availableClient().receiveUnregisterLanguage(lang); } diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingTextDocumentService.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingTextDocumentService.java index 5cad09a42..984ba7a13 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingTextDocumentService.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingTextDocumentService.java @@ -195,11 +195,13 @@ public void initialized() { @Override public void registerLanguage(LanguageParameter lang) { + logger.debug("textDocument/registerLanguage({}, {})", lang.getName(), lang.getMainFunction()); availableServer().languageByName(lang.getName()).thenApply(s -> s.sendRegisterLanguage(lang)); } @Override public void unregisterLanguage(LanguageParameter lang) { + logger.debug("textDocument/unregisterLanguage({})", lang.getName()); availableServer().languageByName(lang.getName()).thenApply(s -> s.sendUnregisterLanguage(lang)); } From 096ad1ab572b8ad686d8eb328b1c26652b17f9ab Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Tue, 28 Apr 2026 16:28:28 +0200 Subject: [PATCH 022/107] Do not resolve project location. --- .../src/main/rascal/library/demo/lang/pico/LanguageServer.rsc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rascal-lsp/src/main/rascal/library/demo/lang/pico/LanguageServer.rsc b/rascal-lsp/src/main/rascal/library/demo/lang/pico/LanguageServer.rsc index 9d290953a..b551e46f4 100644 --- a/rascal-lsp/src/main/rascal/library/demo/lang/pico/LanguageServer.rsc +++ b/rascal-lsp/src/main/rascal/library/demo/lang/pico/LanguageServer.rsc @@ -361,7 +361,7 @@ in the presence of error trees. See ((util::LanguageServer)) for more details. Any feedback (errors and exceptions) is faster and more clearly printed in the terminal. } void main(bool errorRecovery=false) { - root = resolveLocation(|project://rascal-lsp|); + root = |project://rascal-lsp/|; pcfg = pathConfig(projectRoot = root, bin = root + "target/classes"); registerLanguage( language( From aa08777fe985f5d47325a84ee30c1c2a3bb11279 Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Tue, 28 Apr 2026 18:04:35 +0200 Subject: [PATCH 023/107] Fix nullability. --- .../org/rascalmpl/vscode/lsp/LanguageServerRouter.java | 9 ++++++--- .../parametric/routing/RoutingTextDocumentService.java | 2 +- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/LanguageServerRouter.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/LanguageServerRouter.java index eb101b2f9..1dfb2e363 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/LanguageServerRouter.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/LanguageServerRouter.java @@ -48,6 +48,7 @@ import java.util.concurrent.ExecutorService; import java.util.concurrent.Future; import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Function; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; @@ -322,7 +323,7 @@ public synchronized CompletableFuture sendRegisterLanguage(LanguageParamet logger.debug("rascal/sendRegisterLanguage({}, {})", lang.getName(), lang.getMainFunction()); // If we do not have a parametric server running for this language, start and initialize it. synchronized (this) { - languageServers.computeIfAbsent(lang.getName(), _n -> { + var server = languageServers.computeIfAbsent(lang.getName(), (Function>) _n -> { try { return startServer(lang); } catch (IOException e) { @@ -330,8 +331,10 @@ public synchronized CompletableFuture sendRegisterLanguage(LanguageParamet return null; } }); - for (var ext : lang.getExtensions()) { - languagesByExtension.put(ext, lang.getName()); + if (server != null) { + for (var ext : lang.getExtensions()) { + languagesByExtension.put(ext, lang.getName()); + } } } diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingTextDocumentService.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingTextDocumentService.java index 984ba7a13..f0d8ad184 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingTextDocumentService.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingTextDocumentService.java @@ -370,7 +370,7 @@ public CompletableFuture> foldingRange(FoldingRangeRequestPar @Override public CompletableFuture<@Nullable Hover> hover(HoverParams params) { - return callC(route(params.getTextDocument()), TextDocumentService::hover, params); + return this.callC(route(params.getTextDocument()), TextDocumentService::hover, params); } @Override From 12742a48bff1b2dae2455ee55c2f625158f61dbe Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Tue, 28 Apr 2026 18:32:57 +0200 Subject: [PATCH 024/107] Rename caller interface name & methods. --- .../routing/RoutingTextDocumentService.java | 46 +++++++++---------- .../util/{Caller.java => FutureCaller.java} | 8 ++-- 2 files changed, 27 insertions(+), 27 deletions(-) rename rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/util/{Caller.java => FutureCaller.java} (82%) diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingTextDocumentService.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingTextDocumentService.java index f0d8ad184..a9e5d30d0 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingTextDocumentService.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingTextDocumentService.java @@ -98,7 +98,7 @@ import org.rascalmpl.vscode.lsp.TextDocumentState; import org.rascalmpl.vscode.lsp.parametric.LanguageRegistry.LanguageParameter; import org.rascalmpl.vscode.lsp.parametric.ParametricTextDocumentService; -import org.rascalmpl.vscode.lsp.util.Caller; +import org.rascalmpl.vscode.lsp.util.FutureCaller; import org.rascalmpl.vscode.lsp.util.Lists; import org.rascalmpl.vscode.lsp.util.Router; import org.rascalmpl.vscode.lsp.util.locations.Locations; @@ -106,7 +106,7 @@ import io.usethesource.vallang.ISourceLocation; import io.usethesource.vallang.IValue; -public class RoutingTextDocumentService implements IBaseTextDocumentService, Caller, Router> { +public class RoutingTextDocumentService implements IBaseTextDocumentService, FutureCaller, Router> { private static final Logger logger = LogManager.getLogger(RoutingTextDocumentService.class); @@ -147,12 +147,12 @@ private LanguageServerRouter availableServer() { @Override public void didOpen(DidOpenTextDocumentParams params) { - call(route(params.getTextDocument()), TextDocumentService::didOpen, params); + callAccept(route(params.getTextDocument()), TextDocumentService::didOpen, params); } @Override public void didChange(DidChangeTextDocumentParams params) { - call(route(params.getTextDocument()), TextDocumentService::didChange, params); + callAccept(route(params.getTextDocument()), TextDocumentService::didChange, params); } @Override @@ -279,103 +279,103 @@ public void cancelProgress(String progressId) { @Override public CompletableFuture> callHierarchyIncomingCalls( CallHierarchyIncomingCallsParams params) { - return callC(route(Locations.toLoc(params.getItem().getUri())), TextDocumentService::callHierarchyIncomingCalls, params); + return callCompose(route(Locations.toLoc(params.getItem().getUri())), TextDocumentService::callHierarchyIncomingCalls, params); } @Override public CompletableFuture> callHierarchyOutgoingCalls( CallHierarchyOutgoingCallsParams params) { - return callC(route(Locations.toLoc(params.getItem().getUri())), TextDocumentService::callHierarchyOutgoingCalls, params); + return callCompose(route(Locations.toLoc(params.getItem().getUri())), TextDocumentService::callHierarchyOutgoingCalls, params); } @Override public CompletableFuture, CompletionList>> completion(CompletionParams position) { - return callC(route(position.getTextDocument()), TextDocumentService::completion, position); + return callCompose(route(position.getTextDocument()), TextDocumentService::completion, position); } @Override public CompletableFuture, List>> definition( DefinitionParams params) { - return callC(route(params.getTextDocument()), TextDocumentService::definition, params); + return callCompose(route(params.getTextDocument()), TextDocumentService::definition, params); } @Override public CompletableFuture> prepareCallHierarchy(CallHierarchyPrepareParams params) { - return callC(route(params.getTextDocument()), TextDocumentService::prepareCallHierarchy, params); + return callCompose(route(params.getTextDocument()), TextDocumentService::prepareCallHierarchy, params); } @Override public CompletableFuture semanticTokensFull(SemanticTokensParams params) { - return callC(route(params.getTextDocument()), TextDocumentService::semanticTokensFull, params); + return callCompose(route(params.getTextDocument()), TextDocumentService::semanticTokensFull, params); } @Override public CompletableFuture> semanticTokensFullDelta( SemanticTokensDeltaParams params) { - return callC(route(params.getTextDocument()), TextDocumentService::semanticTokensFullDelta, params); + return callCompose(route(params.getTextDocument()), TextDocumentService::semanticTokensFullDelta, params); } @Override public CompletableFuture semanticTokensRange(SemanticTokensRangeParams params) { - return callC(route(params.getTextDocument()), TextDocumentService::semanticTokensRange, params); + return callCompose(route(params.getTextDocument()), TextDocumentService::semanticTokensRange, params); } @Override public CompletableFuture> codeLens(CodeLensParams params) { - return callC(route(params.getTextDocument()), TextDocumentService::codeLens, params); + return callCompose(route(params.getTextDocument()), TextDocumentService::codeLens, params); } @Override public CompletableFuture> prepareRename( PrepareRenameParams params) { - return callC(route(params.getTextDocument()), TextDocumentService::prepareRename, params); + return callCompose(route(params.getTextDocument()), TextDocumentService::prepareRename, params); } @Override public CompletableFuture rename(RenameParams params) { - return callC(route(params.getTextDocument()), TextDocumentService::rename, params); + return callCompose(route(params.getTextDocument()), TextDocumentService::rename, params); } @Override public CompletableFuture> inlayHint(InlayHintParams params) { - return callC(route(params.getTextDocument()), TextDocumentService::inlayHint, params); + return callCompose(route(params.getTextDocument()), TextDocumentService::inlayHint, params); } @Override public CompletableFuture>> codeAction(CodeActionParams params) { - return callC(route(params.getTextDocument()), TextDocumentService::codeAction, params); + return callCompose(route(params.getTextDocument()), TextDocumentService::codeAction, params); } @Override public CompletableFuture>> documentSymbol( DocumentSymbolParams params) { - return callC(route(params.getTextDocument()), TextDocumentService::documentSymbol, params); + return callCompose(route(params.getTextDocument()), TextDocumentService::documentSymbol, params); } @Override public CompletableFuture, List>> implementation( ImplementationParams params) { - return callC(route(params.getTextDocument()), TextDocumentService::implementation, params); + return callCompose(route(params.getTextDocument()), TextDocumentService::implementation, params); } @Override public CompletableFuture> references(ReferenceParams params) { - return callC(route(params.getTextDocument()), TextDocumentService::references, params); + return callCompose(route(params.getTextDocument()), TextDocumentService::references, params); } @Override public CompletableFuture> foldingRange(FoldingRangeRequestParams params) { - return callC(route(params.getTextDocument()), TextDocumentService::foldingRange, params); + return callCompose(route(params.getTextDocument()), TextDocumentService::foldingRange, params); } @Override public CompletableFuture<@Nullable Hover> hover(HoverParams params) { - return this.callC(route(params.getTextDocument()), TextDocumentService::hover, params); + return this.callCompose(route(params.getTextDocument()), TextDocumentService::hover, params); } @Override public CompletableFuture> selectionRange(SelectionRangeParams params) { - return callC(route(params.getTextDocument()), TextDocumentService::selectionRange, params); + return callCompose(route(params.getTextDocument()), TextDocumentService::selectionRange, params); } diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/util/Caller.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/util/FutureCaller.java similarity index 82% rename from rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/util/Caller.java rename to rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/util/FutureCaller.java index 3685995cd..c5d1d7db5 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/util/Caller.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/util/FutureCaller.java @@ -30,17 +30,17 @@ import java.util.function.BiConsumer; import java.util.function.BiFunction; -public interface Caller { +public interface FutureCaller { - default CompletableFuture callC(CompletableFuture t, BiFunction> func, A arg) { + default CompletableFuture callCompose(CompletableFuture t, BiFunction> func, A arg) { return t.thenCompose(actualT -> func.apply(actualT, arg)); } - default CompletableFuture call(CompletableFuture t, BiFunction func, A arg) { + default CompletableFuture callApply(CompletableFuture t, BiFunction func, A arg) { return t.thenApply(actualT -> func.apply(actualT, arg)); } - default CompletableFuture call(CompletableFuture t, BiConsumer func, A arg) { + default CompletableFuture callAccept(CompletableFuture t, BiConsumer func, A arg) { return t.thenAccept(actualT -> func.accept(actualT, arg)); } } From c9991f324dcfe1f2ee1fd9be2d568de98a5a6bc7 Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Tue, 28 Apr 2026 18:37:02 +0200 Subject: [PATCH 025/107] Rename router interface. --- .../java/org/rascalmpl/vscode/lsp/LanguageServerRouter.java | 4 ++-- .../lsp/parametric/routing/RoutingTextDocumentService.java | 4 ++-- .../vscode/lsp/util/{Router.java => DocumentRouter.java} | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) rename rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/util/{Router.java => DocumentRouter.java} (98%) diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/LanguageServerRouter.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/LanguageServerRouter.java index 1dfb2e363..1ce6982f7 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/LanguageServerRouter.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/LanguageServerRouter.java @@ -78,14 +78,14 @@ import org.rascalmpl.vscode.lsp.parametric.LanguageRegistry.LanguageParameter; import org.rascalmpl.vscode.lsp.parametric.routing.RoutingTextDocumentService; import org.rascalmpl.vscode.lsp.parametric.routing.RoutingWorkspaceService; +import org.rascalmpl.vscode.lsp.util.DocumentRouter; import org.rascalmpl.vscode.lsp.util.NamedThreadPool; -import org.rascalmpl.vscode.lsp.util.Router; import io.usethesource.vallang.IInteger; import io.usethesource.vallang.ISourceLocation; import io.usethesource.vallang.IString; -public class LanguageServerRouter extends BaseLanguageServer.ActualLanguageServer implements IBaseLanguageClient, Router> { +public class LanguageServerRouter extends BaseLanguageServer.ActualLanguageServer implements IBaseLanguageClient, DocumentRouter> { private static final Logger logger = LogManager.getLogger(LanguageServerRouter.class); diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingTextDocumentService.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingTextDocumentService.java index a9e5d30d0..f7b3dd02a 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingTextDocumentService.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingTextDocumentService.java @@ -98,15 +98,15 @@ import org.rascalmpl.vscode.lsp.TextDocumentState; import org.rascalmpl.vscode.lsp.parametric.LanguageRegistry.LanguageParameter; import org.rascalmpl.vscode.lsp.parametric.ParametricTextDocumentService; +import org.rascalmpl.vscode.lsp.util.DocumentRouter; import org.rascalmpl.vscode.lsp.util.FutureCaller; import org.rascalmpl.vscode.lsp.util.Lists; -import org.rascalmpl.vscode.lsp.util.Router; import org.rascalmpl.vscode.lsp.util.locations.Locations; import io.usethesource.vallang.ISourceLocation; import io.usethesource.vallang.IValue; -public class RoutingTextDocumentService implements IBaseTextDocumentService, FutureCaller, Router> { +public class RoutingTextDocumentService implements IBaseTextDocumentService, FutureCaller, DocumentRouter> { private static final Logger logger = LogManager.getLogger(RoutingTextDocumentService.class); diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/util/Router.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/util/DocumentRouter.java similarity index 98% rename from rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/util/Router.java rename to rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/util/DocumentRouter.java index 81f0de44b..4d8a04c4d 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/util/Router.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/util/DocumentRouter.java @@ -33,7 +33,7 @@ import io.usethesource.vallang.ISourceLocation; -public interface Router { +public interface DocumentRouter { T route(ISourceLocation loc); default T route(TextDocumentItem doc) { From 34f3daace2b4b1d5430b9eb452eb34b6169b447c Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Wed, 29 Apr 2026 10:09:08 +0200 Subject: [PATCH 026/107] Fix dedicated language arg. --- .../vscode/lsp/parametric/ParametricLanguageServer.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricLanguageServer.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricLanguageServer.java index aaa880258..cc08fc103 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricLanguageServer.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricLanguageServer.java @@ -56,7 +56,7 @@ public static void main(String[] args) { portNumber = Integer.parseInt(args[++i]); break; default: - dedicatedLanguage = new GsonBuilder().create().fromJson(args[0], LanguageParameter.class); + dedicatedLanguage = new GsonBuilder().create().fromJson(args[i], LanguageParameter.class); break; } } From 7ebc693351e8a03cae13677be689429e0fc9a4e5 Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Wed, 29 Apr 2026 10:56:14 +0200 Subject: [PATCH 027/107] Fix & suppress warnings. --- .../rascalmpl/vscode/lsp/BaseLanguageServer.java | 2 +- .../rascalmpl/vscode/lsp/LanguageServerRouter.java | 14 +++++++------- .../parametric/routing/RoutingLanguageServer.java | 1 + .../routing/RoutingTextDocumentService.java | 2 ++ 4 files changed, 11 insertions(+), 8 deletions(-) diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/BaseLanguageServer.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/BaseLanguageServer.java index 8f3263a18..9e5139bd3 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/BaseLanguageServer.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/BaseLanguageServer.java @@ -307,7 +307,7 @@ public IBaseTextDocumentService getTextDocumentService() { @Override public IBaseTextDocumentService getIBaseTextDocumentService() { - return lspDocumentService; + return getTextDocumentService(); } @Override diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/LanguageServerRouter.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/LanguageServerRouter.java index 1ce6982f7..dcfb4ae7a 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/LanguageServerRouter.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/LanguageServerRouter.java @@ -74,7 +74,6 @@ import org.eclipse.lsp4j.jsonrpc.Launcher; import org.rascalmpl.ideservices.GsonUtils; import org.rascalmpl.uri.URIUtil; -import org.rascalmpl.util.maven.Artifact; import org.rascalmpl.vscode.lsp.parametric.LanguageRegistry.LanguageParameter; import org.rascalmpl.vscode.lsp.parametric.routing.RoutingTextDocumentService; import org.rascalmpl.vscode.lsp.parametric.routing.RoutingWorkspaceService; @@ -127,10 +126,6 @@ public CompletableFuture route(String extension) }); } - private Optional safeLanguage(ISourceLocation loc) { - return safeLanguage(extension(loc)); - } - private Optional safeLanguage(String extension) { if ("".equals(extension)) { var languages = new HashSet<>(languagesByExtension.values()); @@ -149,6 +144,7 @@ public static String extension(ISourceLocation doc) { return URIUtil.getExtension(doc); } + /* private static boolean isRascalLspProject(Artifact art) { var c = art.getCoordinate(); if (!c.getGroupId().equals("org.rascalmpl")) { @@ -157,6 +153,7 @@ private static boolean isRascalLspProject(Artifact art) { var id = c.getArtifactId(); return "rascal-lsp".equals(id); } + */ private String classPath(LanguageParameter lang) { // TODO Build class path based on POM @@ -222,16 +219,17 @@ private CompletableFuture startServer(LanguagePar } else { // In development, we expect the server to have been launched on a pre-agreed port int port = getNextPort(); + @SuppressWarnings("java:S2095") // no need to close the socket here - we close it on server shutdown Socket socket = new Socket(InetAddress.getLoopbackAddress(), port); socket.setTcpNoDelay(true); in = socket.getInputStream(); out = socket.getOutputStream(); onExit = () -> { try { - logger.debug("Closing socket for language {} on port", lang.getName(), port); + logger.debug("Closing socket for language {} on port {}", lang.getName(), port); socket.close(); } catch (IOException e) { - logger.error("Closing socket failed", lang.getName(), port); + logger.error("Closing socket for {} on port {} failed", lang.getName(), port); } }; } @@ -299,10 +297,12 @@ private InitializeParams delegateInitializationParams() { return params; } + @Override public RoutingTextDocumentService getTextDocumentService() { return (RoutingTextDocumentService) super.getTextDocumentService(); } + @Override public RoutingWorkspaceService getWorkspaceService() { return (RoutingWorkspaceService) super.getWorkspaceService(); } diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingLanguageServer.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingLanguageServer.java index 839300dc5..345f7b13d 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingLanguageServer.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingLanguageServer.java @@ -42,6 +42,7 @@ public class RoutingLanguageServer extends ParametricLanguageServer { private static final Logger logger = LogManager.getLogger(RoutingLanguageServer.class); + @SuppressWarnings("java:S2189") // endless loop is fine for the development server public static void startLanguageServer(ExecutorService requestPool, ExecutorService workerPool, int portNumber) { logger.info("Starting Rascal Language Server Router: {}", getVersion()); printClassPath(); diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingTextDocumentService.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingTextDocumentService.java index f7b3dd02a..e8beb0438 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingTextDocumentService.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingTextDocumentService.java @@ -114,6 +114,7 @@ public class RoutingTextDocumentService implements IBaseTextDocumentService, Fut private @MonotonicNonNull BaseWorkspaceService wsService; private @MonotonicNonNull LanguageServerRouter server; + @SuppressWarnings("unused") private final ExecutorService exec; public RoutingTextDocumentService(ExecutorService exec) { @@ -131,6 +132,7 @@ public CompletableFuture route(ISourceLocation loc) { .thenApply(IBaseLanguageServerExtensions::getIBaseTextDocumentService); } + @SuppressWarnings("unused") private LanguageClient availableClient() { if (client == null) { throw new IllegalStateException("Client not connected yet."); From dbaa57b78127673d7ec3d03630844dcfc2009f99 Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Wed, 29 Apr 2026 11:30:57 +0200 Subject: [PATCH 028/107] Implement basic argument parsing. --- .../parametric/ParametricLanguageServer.java | 45 ++++++++++++++----- .../routing/RoutingLanguageServer.java | 11 +++-- 2 files changed, 39 insertions(+), 17 deletions(-) diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricLanguageServer.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricLanguageServer.java index cc08fc103..11d703f22 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricLanguageServer.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricLanguageServer.java @@ -35,32 +35,55 @@ public class ParametricLanguageServer extends BaseLanguageServer { - protected static final int DEFAULT_PORT_NUMBER = 9999; - - protected static void start(int portNumber, @Nullable LanguageParameter dedicatedLanguage) { + protected static void startParametric(ServerArgs args) { startLanguageServer(NamedThreadPool.single("parametric-lsp") , NamedThreadPool.cached("parametric") - , threadPool -> new ParametricTextDocumentService(threadPool, dedicatedLanguage) + , threadPool -> new ParametricTextDocumentService(threadPool, args.getDedicatedLanguage()) , ParametricWorkspaceService::new - , portNumber + , args.getPort() ); } public static void main(String[] args) { - int portNumber = DEFAULT_PORT_NUMBER; - LanguageParameter dedicatedLanguage = null; + startParametric(parseArgs(args)); + } + + public static class ServerArgs { + private int port = 9999; + private @Nullable LanguageParameter dedicatedLanguage = null; + + public int getPort() { + return port; + } + + public void setPort(int port) { + this.port = port; + } + public @Nullable LanguageParameter getDedicatedLanguage() { + return dedicatedLanguage; + } + + public void setDedicatedLanguage(LanguageParameter dedicatedLanguage) { + this.dedicatedLanguage = dedicatedLanguage; + } + + } + + protected static ServerArgs parseArgs(String[] args) { + var serverArgs = new ServerArgs(); for (int i = 0; i < args.length; i++) { switch (args[i]) { case "--port": - portNumber = Integer.parseInt(args[++i]); + serverArgs.setPort(Integer.parseInt(args[++i])); break; default: - dedicatedLanguage = new GsonBuilder().create().fromJson(args[i], LanguageParameter.class); + if (serverArgs.getDedicatedLanguage() == null) { + serverArgs.setDedicatedLanguage(new GsonBuilder().create().fromJson(args[i], LanguageParameter.class)); + } break; } } - - start(portNumber, dedicatedLanguage); + return serverArgs; } } diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingLanguageServer.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingLanguageServer.java index 345f7b13d..9c0f41637 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingLanguageServer.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingLanguageServer.java @@ -26,7 +26,6 @@ */ package org.rascalmpl.vscode.lsp.parametric.routing; -import com.google.gson.GsonBuilder; import java.io.IOException; import java.net.InetAddress; import java.net.ServerSocket; @@ -34,7 +33,6 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.rascalmpl.vscode.lsp.LanguageServerRouter; -import org.rascalmpl.vscode.lsp.parametric.LanguageRegistry.LanguageParameter; import org.rascalmpl.vscode.lsp.parametric.ParametricLanguageServer; import org.rascalmpl.vscode.lsp.util.NamedThreadPool; @@ -63,13 +61,14 @@ public static void startLanguageServer(ExecutorService requestPool, ExecutorServ } public static void main(String[] args) { - if (args.length > 0) { - var dedicatedLanguage = new GsonBuilder().create().fromJson(args[0], LanguageParameter.class); - start(DEFAULT_PORT_NUMBER, dedicatedLanguage); + var serverArgs = parseArgs(args); + if (serverArgs.getDedicatedLanguage() != null) { + // If we get a dedicated language argument, we just start a single parametric server + startParametric(serverArgs); } else { startLanguageServer(NamedThreadPool.single("parametric-lsp-router-in") , NamedThreadPool.cached("parametric-router") - , DEFAULT_PORT_NUMBER + , serverArgs.getPort() ); } } From a76ec75c4c6072c34c474aef8bab58ccadf60a56 Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Wed, 29 Apr 2026 11:32:05 +0200 Subject: [PATCH 029/107] Document new classes and interfaces. --- .../rascalmpl/vscode/lsp/LanguageServerRouter.java | 4 ++++ .../lsp/parametric/routing/RoutingLanguageServer.java | 4 ++++ .../routing/RoutingTextDocumentService.java | 3 +++ .../parametric/routing/RoutingWorkspaceService.java | 3 +++ .../org/rascalmpl/vscode/lsp/util/DocumentRouter.java | 11 +++++++++++ .../org/rascalmpl/vscode/lsp/util/FutureCaller.java | 4 ++++ 6 files changed, 29 insertions(+) diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/LanguageServerRouter.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/LanguageServerRouter.java index dcfb4ae7a..03ee64b78 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/LanguageServerRouter.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/LanguageServerRouter.java @@ -84,11 +84,15 @@ import io.usethesource.vallang.ISourceLocation; import io.usethesource.vallang.IString; +/** + * A language server implementation that routes LSP requests to dedicated remote language servers. + */ public class LanguageServerRouter extends BaseLanguageServer.ActualLanguageServer implements IBaseLanguageClient, DocumentRouter> { private static final Logger logger = LogManager.getLogger(LanguageServerRouter.class); private final Map languagesByExtension; + // TODO To be able to route to arbitrary third-party language servers, remote servers should implement `LanguageServer` (instead of `IBaseLanguageServerExtensions`) private final Map> languageServers; private final Collection delegateProcesses = new CopyOnWriteArrayList<>(); diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingLanguageServer.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingLanguageServer.java index 9c0f41637..8b0c34271 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingLanguageServer.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingLanguageServer.java @@ -36,6 +36,10 @@ import org.rascalmpl.vscode.lsp.parametric.ParametricLanguageServer; import org.rascalmpl.vscode.lsp.util.NamedThreadPool; +/** + * A language-parametric server that assigns a dedicated server to each language. + * This server routes LSP requests to the appropriate language server. + */ public class RoutingLanguageServer extends ParametricLanguageServer { private static final Logger logger = LogManager.getLogger(RoutingLanguageServer.class); diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingTextDocumentService.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingTextDocumentService.java index e8beb0438..551b63767 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingTextDocumentService.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingTextDocumentService.java @@ -106,6 +106,9 @@ import io.usethesource.vallang.ISourceLocation; import io.usethesource.vallang.IValue; +/** + * A language-parametric text document service that routes incoming requests to remote dedicated language servers. + */ public class RoutingTextDocumentService implements IBaseTextDocumentService, FutureCaller, DocumentRouter> { private static final Logger logger = LogManager.getLogger(RoutingTextDocumentService.class); diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingWorkspaceService.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingWorkspaceService.java index c0efe6b47..beb3a3dd7 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingWorkspaceService.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingWorkspaceService.java @@ -29,6 +29,9 @@ import java.util.concurrent.ExecutorService; import org.rascalmpl.vscode.lsp.BaseWorkspaceService; +/** + * A language-parametric workspace service that routes incoming requests to remote dedicated language servers. + */ public class RoutingWorkspaceService extends BaseWorkspaceService { public RoutingWorkspaceService(ExecutorService exec) { diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/util/DocumentRouter.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/util/DocumentRouter.java index 4d8a04c4d..72d6eac1b 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/util/DocumentRouter.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/util/DocumentRouter.java @@ -33,7 +33,17 @@ import io.usethesource.vallang.ISourceLocation; +/** + * A router of document-like inputs to outputs of {{@link T}}. + * @param The type of the mapped value. + */ public interface DocumentRouter { + + /** + * Map an {{@link ISourceLocation}} to a {{@link T}}. + * @param loc The input location. + * @return The mapped value. + */ T route(ISourceLocation loc); default T route(TextDocumentItem doc) { @@ -47,4 +57,5 @@ default T route(VersionedTextDocumentIdentifier id) { default T route(TextDocumentIdentifier id) { return route(Locations.toLoc(id.getUri())); } + } diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/util/FutureCaller.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/util/FutureCaller.java index c5d1d7db5..a063d6cdb 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/util/FutureCaller.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/util/FutureCaller.java @@ -30,6 +30,10 @@ import java.util.function.BiConsumer; import java.util.function.BiFunction; +/** + * A helper interface to support calling functions on values in futures. + * @param The type of the value in the futures. + */ public interface FutureCaller { default CompletableFuture callCompose(CompletableFuture t, BiFunction> func, A arg) { From 53cc8b37b599891c3c58ae147bfb64f02f773bb3 Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Wed, 29 Apr 2026 15:24:27 +0200 Subject: [PATCH 030/107] Remove redundant pairing. --- .../java/org/rascalmpl/vscode/lsp/BaseLanguageServer.java | 4 ---- 1 file changed, 4 deletions(-) diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/BaseLanguageServer.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/BaseLanguageServer.java index 9e5139bd3..eb6668d26 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/BaseLanguageServer.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/BaseLanguageServer.java @@ -137,8 +137,6 @@ public static void startLanguageServer(ExecutorService requestPool, ExecutorServ if (DEPLOY_MODE) { var docService = docServiceProvider.apply(workerPool); var wsService = workspaceServiceProvider.apply(workerPool); - docService.pair(wsService); - wsService.pair(docService); startLSP(constructLSPClient(capturedIn, capturedOut, new ActualLanguageServer(() -> System.exit(0), workerPool, docService, wsService), requestPool)); } else { @@ -147,8 +145,6 @@ public static void startLanguageServer(ExecutorService requestPool, ExecutorServ while (true) { var docService = docServiceProvider.apply(workerPool); var wsService = workspaceServiceProvider.apply(workerPool); - docService.pair(wsService); - wsService.pair(docService); startLSP(constructLSPClient(serverSocket.accept(), new ActualLanguageServer(() -> {}, workerPool, docService, wsService), requestPool)); } } catch (IOException e) { From 701b483d76a4ca3e7ead5e1a87054d978e4a669b Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Wed, 29 Apr 2026 16:47:05 +0200 Subject: [PATCH 031/107] Refactor starting/connecting language servers. --- .../vscode/lsp/LanguageServerRouter.java | 112 +++++++++--------- 1 file changed, 55 insertions(+), 57 deletions(-) diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/LanguageServerRouter.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/LanguageServerRouter.java index 03ee64b78..e0fc0a070 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/LanguageServerRouter.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/LanguageServerRouter.java @@ -46,9 +46,9 @@ import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; -import java.util.concurrent.Future; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Function; +import org.apache.commons.lang3.tuple.Triple; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; @@ -159,7 +159,7 @@ private static boolean isRascalLspProject(Artifact art) { } */ - private String classPath(LanguageParameter lang) { + private static String classPath(LanguageParameter lang) { // TODO Build class path based on POM /* try { @@ -194,84 +194,82 @@ private String classPath(LanguageParameter lang) { return System.getProperty("java.class.path"); } + private static Triple startServerProcess(LanguageParameter lang) throws IOException { + // TODO Figure out Rascal/Rascal-LSP versions/class path + logger.info("Starting LSP process for {}", lang.getName()); + + var classPath = classPath(lang); + logger.debug("{} runs with class path {}", lang.getName(), classPath); + // In deployment, we start a process and connect to it via input/output streams + var proc = new ProcessBuilder(ProcessHandle.current().info().command().orElse("java") + , "-Dlog4j2.configurationFactory=org.rascalmpl.vscode.lsp.log.LogJsonConfiguration" + , "-Dlog4j2.level=DEBUG" + , "-Drascal.fallbackResolver=org.rascalmpl.vscode.lsp.uri.FallbackResolver" + , "-Drascal.lsp.deploy=true" + , "-Drascal.compilerClasspath=" + classPath + , "-Xmx2048M" + , "-cp", classPath + , "org.rascalmpl.vscode.lsp.parametric.ParametricLanguageServer" + ).redirectError(Redirect.INHERIT).start(); + + logger.debug("Launched language server on process {}", proc.pid()); + return Triple.of(proc.getInputStream(), proc.getOutputStream(), () -> {}); + } + + private Triple connectToServer(LanguageParameter lang) throws IOException { + // In development, we expect the server to have been launched on a pre-agreed port + int port = getNextPort(); + @SuppressWarnings("java:S2095") // no need to close the socket here - we close it on server shutdown + Socket socket = new Socket(InetAddress.getLoopbackAddress(), port); + socket.setTcpNoDelay(true); + return Triple.of(socket.getInputStream(), socket.getOutputStream(), () -> { + try { + logger.debug("Closing socket for language {} on port {}", lang.getName(), port); + socket.close(); + } catch (IOException e) { + logger.error("Closing socket for {} on port {} failed", lang.getName(), port); + } + }); + } + private CompletableFuture startServer(LanguageParameter lang) throws IOException { - InputStream in; - OutputStream out; - Runnable onExit; - if (DEPLOY_MODE) { - // TODO Figure out Rascal/Rascal-LSP versions/class path - logger.info("Starting LSP process for {}", lang.getName()); - - var classPath = classPath(lang); - logger.debug("{} runs with class path {}", lang.getName(), classPath); - // In deployment, we start a process and connect to it via input/output streams - var proc = new ProcessBuilder(ProcessHandle.current().info().command().orElse("java") - , "-Dlog4j2.configurationFactory=org.rascalmpl.vscode.lsp.log.LogJsonConfiguration" - , "-Dlog4j2.level=DEBUG" - , "-Drascal.fallbackResolver=org.rascalmpl.vscode.lsp.uri.FallbackResolver" - , "-Drascal.lsp.deploy=true" - , "-Drascal.compilerClasspath=" + classPath - , "-Xmx2048M" - , "-cp", classPath - , "org.rascalmpl.vscode.lsp.parametric.ParametricLanguageServer" - ).redirectError(Redirect.INHERIT).start(); - - logger.debug("Launched language server on process {}", proc.pid()); - in = proc.getInputStream(); - out = proc.getOutputStream(); - onExit = () -> {}; - } else { - // In development, we expect the server to have been launched on a pre-agreed port - int port = getNextPort(); - @SuppressWarnings("java:S2095") // no need to close the socket here - we close it on server shutdown - Socket socket = new Socket(InetAddress.getLoopbackAddress(), port); - socket.setTcpNoDelay(true); - in = socket.getInputStream(); - out = socket.getOutputStream(); - onExit = () -> { - try { - logger.debug("Closing socket for language {} on port {}", lang.getName(), port); - socket.close(); - } catch (IOException e) { - logger.error("Closing socket for {} on port {} failed", lang.getName(), port); - } - }; - } + var serverParams = DEPLOY_MODE + ? startServerProcess(lang) + : connectToServer(lang) + ; + var serverLauncher = new Launcher.Builder() .setRemoteInterface(IBaseLanguageServerExtensions.class) .setLocalService(this) - .setInput(in) - .setOutput(out) + .setInput(serverParams.getLeft()) + .setOutput(serverParams.getMiddle()) .configureGson(GsonUtils.complexAsJsonObject()) // Only needed if we want to communicate IValues .setExecutorService(NamedThreadPool.single("parametric-lsp-router-out")) .create(); var runner = serverLauncher.startListening(); - scheduleShutdown(runner, lang, onExit); var server = serverLauncher.getRemoteProxy(); - var delegateServerCaps = server.initialize(delegateInitializationParams()); - // When initialization is done, we can use the server - return delegateServerCaps.thenApply(_c -> server); - } - - private void scheduleShutdown(Future server, LanguageParameter lang, Runnable onExit) { getExecutor().execute(() -> { try { - server.get(); + runner.get(); logger.info("Language server for {} terminated gracefully", lang.getName()); } catch (CancellationException | ExecutionException e) { - logger.error("Language server for {} crashed", lang.getName(), e); - // TODO Remove from the map? Attempt a restart with the same parameters? + logger.error("Language server for {} terminated", lang.getName(), e); + languageServers.remove(lang.getName(), server); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } try { - onExit.run(); + serverParams.getRight().run(); } catch (Throwable e) { logger.error("Unexpected error while cleaning up connection to language server for {}", lang.getName(), e); } }); + + // When initialization is done, we can use the server + return server.initialize(delegateInitializationParams()) + .thenApply(_c -> server); } private int getNextPort() { From 800e992220fc059e3f85c188b67f82896f10408d Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Wed, 29 Apr 2026 16:51:24 +0200 Subject: [PATCH 032/107] Improve readability & comment. --- .../java/org/rascalmpl/vscode/lsp/LanguageServerRouter.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/LanguageServerRouter.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/LanguageServerRouter.java index e0fc0a070..be4c77e08 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/LanguageServerRouter.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/LanguageServerRouter.java @@ -210,7 +210,9 @@ private static Triple startServerProcess(La , "-Xmx2048M" , "-cp", classPath , "org.rascalmpl.vscode.lsp.parametric.ParametricLanguageServer" - ).redirectError(Redirect.INHERIT).start(); + ) + .redirectError(Redirect.INHERIT) // Show logs in current process + .start(); logger.debug("Launched language server on process {}", proc.pid()); return Triple.of(proc.getInputStream(), proc.getOutputStream(), () -> {}); From ee69b059d6a8ebf8fa873842174efc5d910dd8b1 Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Wed, 29 Apr 2026 16:51:41 +0200 Subject: [PATCH 033/107] Start server with dedicated language. --- .../java/org/rascalmpl/vscode/lsp/LanguageServerRouter.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/LanguageServerRouter.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/LanguageServerRouter.java index be4c77e08..8f5c1ee9d 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/LanguageServerRouter.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/LanguageServerRouter.java @@ -28,6 +28,7 @@ import static org.rascalmpl.vscode.lsp.BaseLanguageServer.DEPLOY_MODE; +import com.google.gson.GsonBuilder; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; @@ -210,6 +211,7 @@ private static Triple startServerProcess(La , "-Xmx2048M" , "-cp", classPath , "org.rascalmpl.vscode.lsp.parametric.ParametricLanguageServer" + , new GsonBuilder().create().toJson(lang, LanguageParameter.class).replace("\"", "\\\"") ) .redirectError(Redirect.INHERIT) // Show logs in current process .start(); From 295f674e85beff6c09a9fa5b1f0ece964b9590b8 Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Wed, 29 Apr 2026 17:04:09 +0200 Subject: [PATCH 034/107] Exit dedicated remote server when it has no more contributions. --- .../rascalmpl/vscode/lsp/LanguageServerRouter.java | 1 + .../lsp/parametric/ParametricLanguageServer.java | 14 +++++++++++++- .../parametric/ParametricTextDocumentService.java | 12 +++++++++++- 3 files changed, 25 insertions(+), 2 deletions(-) diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/LanguageServerRouter.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/LanguageServerRouter.java index 8f5c1ee9d..ba1fee98e 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/LanguageServerRouter.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/LanguageServerRouter.java @@ -211,6 +211,7 @@ private static Triple startServerProcess(La , "-Xmx2048M" , "-cp", classPath , "org.rascalmpl.vscode.lsp.parametric.ParametricLanguageServer" + , "--exitWhenEmpty" , new GsonBuilder().create().toJson(lang, LanguageParameter.class).replace("\"", "\\\"") ) .redirectError(Redirect.INHERIT) // Show logs in current process diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricLanguageServer.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricLanguageServer.java index 11d703f22..f044f8003 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricLanguageServer.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricLanguageServer.java @@ -38,7 +38,7 @@ public class ParametricLanguageServer extends BaseLanguageServer { protected static void startParametric(ServerArgs args) { startLanguageServer(NamedThreadPool.single("parametric-lsp") , NamedThreadPool.cached("parametric") - , threadPool -> new ParametricTextDocumentService(threadPool, args.getDedicatedLanguage()) + , threadPool -> new ParametricTextDocumentService(threadPool, args.getDedicatedLanguage(), args.isExitWhenEmpty()) , ParametricWorkspaceService::new , args.getPort() ); @@ -51,6 +51,7 @@ public static void main(String[] args) { public static class ServerArgs { private int port = 9999; private @Nullable LanguageParameter dedicatedLanguage = null; + private boolean exitWhenEmpty = false; public int getPort() { return port; @@ -68,6 +69,14 @@ public void setDedicatedLanguage(LanguageParameter dedicatedLanguage) { this.dedicatedLanguage = dedicatedLanguage; } + public boolean isExitWhenEmpty() { + return exitWhenEmpty; + } + + public void setExitWhenEmpty(boolean exitWhenEmpty) { + this.exitWhenEmpty = exitWhenEmpty; + } + } protected static ServerArgs parseArgs(String[] args) { @@ -77,6 +86,9 @@ protected static ServerArgs parseArgs(String[] args) { case "--port": serverArgs.setPort(Integer.parseInt(args[++i])); break; + case "--exitWhenEmpty": + serverArgs.setExitWhenEmpty(true); + break; default: if (serverArgs.getDedicatedLanguage() == null) { serverArgs.setDedicatedLanguage(new GsonBuilder().create().fromJson(args[i], LanguageParameter.class)); diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricTextDocumentService.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricTextDocumentService.java index f52116b32..a5fe0cc78 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricTextDocumentService.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricTextDocumentService.java @@ -185,6 +185,8 @@ public class ParametricTextDocumentService implements IBaseTextDocumentService, private final String dedicatedLanguageName; private final SemanticTokenizer tokenizer = new SemanticTokenizer(); + private final boolean exitWhenEmpty; + private @MonotonicNonNull LanguageClient client; private @MonotonicNonNull BaseWorkspaceService workspaceService; private @MonotonicNonNull CapabilityRegistration dynamicCapabilities; @@ -209,13 +211,15 @@ public class ParametricTextDocumentService implements IBaseTextDocumentService, tf.sourceLocationType(), "to"); @SuppressWarnings({"initialization", "methodref.receiver.bound"}) // this::getContents - public ParametricTextDocumentService(ExecutorService exec, @Nullable LanguageParameter dedicatedLanguage) { + public ParametricTextDocumentService(ExecutorService exec, @Nullable LanguageParameter dedicatedLanguage, boolean exitWhenEmpty) { // The following call ensures that URIResolverRegistry is initialized before FallbackResolver is accessed URIResolverRegistry.getInstance(); this.exec = exec; this.files = new ConcurrentHashMap<>(); this.columns = new ColumnMaps(this::getContents); + this.exitWhenEmpty = exitWhenEmpty; + if (dedicatedLanguage == null) { this.dedicatedLanguageName = ""; this.dedicatedLanguage = null; @@ -1093,6 +1097,12 @@ public synchronized void unregisterLanguage(LanguageParameter lang) { contributions.remove(lang.getName()); } + if (exitWhenEmpty && contributions.isEmpty()) { + logger.debug("Shutting down; no more registered languages"); + shutdown(); + System.exit(0); + } + availableCapabilities().update(buildLanguageParams()); } From 01a643e483de430b15ade8a390ac78c7b9721627 Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Thu, 30 Apr 2026 16:34:21 +0200 Subject: [PATCH 035/107] Fix indentation. --- .../vscode/lsp/LanguageServerRouter.java | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/LanguageServerRouter.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/LanguageServerRouter.java index ba1fee98e..122647a0d 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/LanguageServerRouter.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/LanguageServerRouter.java @@ -203,19 +203,19 @@ private static Triple startServerProcess(La logger.debug("{} runs with class path {}", lang.getName(), classPath); // In deployment, we start a process and connect to it via input/output streams var proc = new ProcessBuilder(ProcessHandle.current().info().command().orElse("java") - , "-Dlog4j2.configurationFactory=org.rascalmpl.vscode.lsp.log.LogJsonConfiguration" - , "-Dlog4j2.level=DEBUG" - , "-Drascal.fallbackResolver=org.rascalmpl.vscode.lsp.uri.FallbackResolver" - , "-Drascal.lsp.deploy=true" - , "-Drascal.compilerClasspath=" + classPath - , "-Xmx2048M" - , "-cp", classPath - , "org.rascalmpl.vscode.lsp.parametric.ParametricLanguageServer" - , "--exitWhenEmpty" - , new GsonBuilder().create().toJson(lang, LanguageParameter.class).replace("\"", "\\\"") - ) - .redirectError(Redirect.INHERIT) // Show logs in current process - .start(); + , "-Dlog4j2.configurationFactory=org.rascalmpl.vscode.lsp.log.LogJsonConfiguration" + , "-Dlog4j2.level=DEBUG" + , "-Drascal.fallbackResolver=org.rascalmpl.vscode.lsp.uri.FallbackResolver" + , "-Drascal.lsp.deploy=true" + , "-Drascal.compilerClasspath=" + classPath + , "-Xmx2048M" + , "-cp", classPath + , "org.rascalmpl.vscode.lsp.parametric.ParametricLanguageServer" + , "--exitWhenEmpty" + , new GsonBuilder().create().toJson(lang, LanguageParameter.class).replace("\"", "\\\"") + ) + .redirectError(Redirect.INHERIT) // Show logs in current process + .start(); logger.debug("Launched language server on process {}", proc.pid()); return Triple.of(proc.getInputStream(), proc.getOutputStream(), () -> {}); From c66a12bd2ec168fec719de0e874bba29d2cccac2 Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Thu, 30 Apr 2026 17:07:06 +0200 Subject: [PATCH 036/107] Fix warnings. --- .../java/org/rascalmpl/vscode/lsp/LanguageServerRouter.java | 4 ++-- .../vscode/lsp/parametric/ParametricLanguageServer.java | 1 + .../lsp/parametric/routing/RoutingTextDocumentService.java | 6 +++--- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/LanguageServerRouter.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/LanguageServerRouter.java index 122647a0d..d2def8d2e 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/LanguageServerRouter.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/LanguageServerRouter.java @@ -267,14 +267,14 @@ private CompletableFuture startServer(LanguagePar } try { serverParams.getRight().run(); - } catch (Throwable e) { + } catch (Exception e) { logger.error("Unexpected error while cleaning up connection to language server for {}", lang.getName(), e); } }); // When initialization is done, we can use the server return server.initialize(delegateInitializationParams()) - .thenApply(_c -> server); + .thenApply(ignored -> server); // ignore initialized static server capabilities, since ours are the same } private int getNextPort() { diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricLanguageServer.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricLanguageServer.java index f044f8003..94f93ffae 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricLanguageServer.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricLanguageServer.java @@ -79,6 +79,7 @@ public void setExitWhenEmpty(boolean exitWhenEmpty) { } + @SuppressWarnings("java:S127") // skipping next argument from loop protected static ServerArgs parseArgs(String[] args) { var serverArgs = new ServerArgs(); for (int i = 0; i < args.length; i++) { diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingTextDocumentService.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingTextDocumentService.java index 551b63767..276373f41 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingTextDocumentService.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingTextDocumentService.java @@ -259,9 +259,9 @@ public void didCreateFiles(CreateFilesParams params) { .collect(Collectors.toMap(f -> LanguageServerRouter.extension(Locations.toLoc(f.getUri())), List::of, Lists::union)); for (var e : filesByExt.entrySet()) { - availableServer().route(e.getKey()).thenAccept(server -> { - server.getWorkspaceService().didCreateFiles(new CreateFilesParams(e.getValue())); - }); + availableServer().route(e.getKey()).thenAccept(s -> + s.getWorkspaceService().didCreateFiles(new CreateFilesParams(e.getValue())) + ); } } From 0790e7a1e4c86007946ae29d22fad3ce21800892 Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Thu, 30 Apr 2026 17:16:13 +0200 Subject: [PATCH 037/107] Run dedicated servers with POM-based class path. --- .../vscode/lsp/LanguageServerRouter.java | 15 ++++++++++----- .../library/demo/lang/pico/LanguageServer.rsc | 2 +- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/LanguageServerRouter.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/LanguageServerRouter.java index d2def8d2e..146b78f41 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/LanguageServerRouter.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/LanguageServerRouter.java @@ -36,6 +36,7 @@ import java.net.InetAddress; import java.net.Socket; import java.net.URI; +import java.nio.file.Path; import java.util.Collection; import java.util.HashSet; import java.util.List; @@ -74,12 +75,18 @@ import org.eclipse.lsp4j.WorkspaceFolder; import org.eclipse.lsp4j.jsonrpc.Launcher; import org.rascalmpl.ideservices.GsonUtils; +import org.rascalmpl.library.util.PathConfig; import org.rascalmpl.uri.URIUtil; +import org.rascalmpl.util.maven.Artifact; +import org.rascalmpl.util.maven.MavenParser; +import org.rascalmpl.util.maven.ModelResolutionError; +import org.rascalmpl.util.maven.Scope; import org.rascalmpl.vscode.lsp.parametric.LanguageRegistry.LanguageParameter; import org.rascalmpl.vscode.lsp.parametric.routing.RoutingTextDocumentService; import org.rascalmpl.vscode.lsp.parametric.routing.RoutingWorkspaceService; import org.rascalmpl.vscode.lsp.util.DocumentRouter; import org.rascalmpl.vscode.lsp.util.NamedThreadPool; +import org.rascalmpl.vscode.lsp.util.locations.Locations; import io.usethesource.vallang.IInteger; import io.usethesource.vallang.ISourceLocation; @@ -149,7 +156,6 @@ public static String extension(ISourceLocation doc) { return URIUtil.getExtension(doc); } - /* private static boolean isRascalLspProject(Artifact art) { var c = art.getCoordinate(); if (!c.getGroupId().equals("org.rascalmpl")) { @@ -158,11 +164,8 @@ private static boolean isRascalLspProject(Artifact art) { var id = c.getArtifactId(); return "rascal-lsp".equals(id); } - */ private static String classPath(LanguageParameter lang) { - // TODO Build class path based on POM - /* try { var pcfg = PathConfig.parse(lang.getPathConfig()); var pom = Locations.toPhysicalIfPossible(URIUtil.getChildLocation(pcfg.getProjectRoot(), "pom.xml")); @@ -191,7 +194,9 @@ private static String classPath(LanguageParameter lang) { } catch (ModelResolutionError e) { logger.error("Error while parsing Maven project {}", e); } - */ + + // If all else fails, just use the same class path that we have + logger.warn("Could not compute class path for {}; falling back to class path of routing server", lang.getName()); return System.getProperty("java.class.path"); } diff --git a/rascal-lsp/src/main/rascal/library/demo/lang/pico/LanguageServer.rsc b/rascal-lsp/src/main/rascal/library/demo/lang/pico/LanguageServer.rsc index b551e46f4..7748d04e2 100644 --- a/rascal-lsp/src/main/rascal/library/demo/lang/pico/LanguageServer.rsc +++ b/rascal-lsp/src/main/rascal/library/demo/lang/pico/LanguageServer.rsc @@ -361,7 +361,7 @@ in the presence of error trees. See ((util::LanguageServer)) for more details. Any feedback (errors and exceptions) is faster and more clearly printed in the terminal. } void main(bool errorRecovery=false) { - root = |project://rascal-lsp/|; + root = resolveLocation(|project://rascal-lsp/|); pcfg = pathConfig(projectRoot = root, bin = root + "target/classes"); registerLanguage( language( From 81be30768a2b01a8d7d4f95a839fa8b23bbef453 Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Fri, 1 May 2026 11:48:16 +0200 Subject: [PATCH 038/107] Handle registration from different locations. --- .../main/rascal/library/demo/lang/pico/LanguageServer.rsc | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/rascal-lsp/src/main/rascal/library/demo/lang/pico/LanguageServer.rsc b/rascal-lsp/src/main/rascal/library/demo/lang/pico/LanguageServer.rsc index 7748d04e2..56cf4b599 100644 --- a/rascal-lsp/src/main/rascal/library/demo/lang/pico/LanguageServer.rsc +++ b/rascal-lsp/src/main/rascal/library/demo/lang/pico/LanguageServer.rsc @@ -361,7 +361,12 @@ in the presence of error trees. See ((util::LanguageServer)) for more details. Any feedback (errors and exceptions) is faster and more clearly printed in the terminal. } void main(bool errorRecovery=false) { - root = resolveLocation(|project://rascal-lsp/|); + loc root; + try { + root = resolveLocation(|project://rascal-lsp|); + } catch SchemeNotSupported(_): { + root = resolveLocation(|cwd:///../../../rascal-lsp|); + } pcfg = pathConfig(projectRoot = root, bin = root + "target/classes"); registerLanguage( language( From 00504be6ece783c4e5782a56c5ad4c1adff454d5 Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Fri, 1 May 2026 13:39:37 +0200 Subject: [PATCH 039/107] Clean up terminated servers. --- .../vscode/lsp/LanguageServerRouter.java | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/LanguageServerRouter.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/LanguageServerRouter.java index 146b78f41..829653386 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/LanguageServerRouter.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/LanguageServerRouter.java @@ -260,15 +260,22 @@ private CompletableFuture startServer(LanguagePar var runner = serverLauncher.startListening(); var server = serverLauncher.getRemoteProxy(); + // When initialization is done, we can use the server + var initializedServer = server.initialize(delegateInitializationParams()) + .thenApply(ignored -> server); // ignore initialized static server capabilities, since ours are the same + getExecutor().execute(() -> { try { runner.get(); logger.info("Language server for {} terminated gracefully", lang.getName()); } catch (CancellationException | ExecutionException e) { - logger.error("Language server for {} terminated", lang.getName(), e); - languageServers.remove(lang.getName(), server); + logger.error("Language server for {} terminated with an exception", lang.getName(), e); } catch (InterruptedException e) { Thread.currentThread().interrupt(); + } finally { + if (languageServers.remove(lang.getName(), initializedServer)) { + logger.error("Could not remove LSP routing for {}; restart the Rascal extension", lang.getName()); + } } try { serverParams.getRight().run(); @@ -277,9 +284,7 @@ private CompletableFuture startServer(LanguagePar } }); - // When initialization is done, we can use the server - return server.initialize(delegateInitializationParams()) - .thenApply(ignored -> server); // ignore initialized static server capabilities, since ours are the same + return initializedServer; } private int getNextPort() { From 933e697cce5d291314ceff48a2329f4b52f3be5e Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Fri, 1 May 2026 15:20:42 +0200 Subject: [PATCH 040/107] Ignore unregistration of unknown languages. --- .../org/rascalmpl/vscode/lsp/LanguageServerRouter.java | 2 +- .../parametric/routing/RoutingTextDocumentService.java | 9 ++++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/LanguageServerRouter.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/LanguageServerRouter.java index 829653386..f9862d7c6 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/LanguageServerRouter.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/LanguageServerRouter.java @@ -119,7 +119,7 @@ public LanguageServerRouter(Runnable onExit, ExecutorService exec) { Runtime.getRuntime().addShutdownHook(new Thread(() -> delegateProcesses.forEach(Process::destroy))); } - /*package*/ public CompletableFuture languageByName(String lang) { + public CompletableFuture languageByName(String lang) { var service = languageServers.get(lang); if (service == null) { throw new UnsupportedOperationException(String.format("Rascal Parametric LSP has no support for this file, since no language is registered with name '%s'", lang)); diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingTextDocumentService.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingTextDocumentService.java index 276373f41..113ef59fb 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingTextDocumentService.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingTextDocumentService.java @@ -207,7 +207,14 @@ public void registerLanguage(LanguageParameter lang) { @Override public void unregisterLanguage(LanguageParameter lang) { logger.debug("textDocument/unregisterLanguage({})", lang.getName()); - availableServer().languageByName(lang.getName()).thenApply(s -> s.sendUnregisterLanguage(lang)); + CompletableFuture server; + try { + server = availableServer().languageByName(lang.getName()); + } catch (UnsupportedOperationException e) { + logger.debug("Ignored language unregistration for unknown language {}", lang.getName()); + return; + } + server.thenApply(s -> s.sendUnregisterLanguage(lang)); } @Override From 1f3379573ac684ffc15a7c4d6a46f3d8479e0443 Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Mon, 4 May 2026 10:05:49 +0200 Subject: [PATCH 041/107] Push down error handling to server connect methods. --- .../vscode/lsp/LanguageServerRouter.java | 117 ++++++++++-------- 1 file changed, 65 insertions(+), 52 deletions(-) diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/LanguageServerRouter.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/LanguageServerRouter.java index f9862d7c6..f38cd4b38 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/LanguageServerRouter.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/LanguageServerRouter.java @@ -49,7 +49,6 @@ import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.atomic.AtomicInteger; -import java.util.function.Function; import org.apache.commons.lang3.tuple.Triple; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -74,6 +73,9 @@ import org.eclipse.lsp4j.WorkDoneProgressCreateParams; import org.eclipse.lsp4j.WorkspaceFolder; import org.eclipse.lsp4j.jsonrpc.Launcher; +import org.eclipse.lsp4j.jsonrpc.ResponseErrorException; +import org.eclipse.lsp4j.jsonrpc.messages.ResponseError; +import org.eclipse.lsp4j.jsonrpc.messages.ResponseErrorCode; import org.rascalmpl.ideservices.GsonUtils; import org.rascalmpl.library.util.PathConfig; import org.rascalmpl.uri.URIUtil; @@ -85,7 +87,7 @@ import org.rascalmpl.vscode.lsp.parametric.routing.RoutingTextDocumentService; import org.rascalmpl.vscode.lsp.parametric.routing.RoutingWorkspaceService; import org.rascalmpl.vscode.lsp.util.DocumentRouter; -import org.rascalmpl.vscode.lsp.util.NamedThreadPool; +import org.rascalmpl.vscode.lsp.util.concurrent.CompletableFutureUtils; import org.rascalmpl.vscode.lsp.util.locations.Locations; import io.usethesource.vallang.IInteger; @@ -200,68 +202,84 @@ private static String classPath(LanguageParameter lang) { return System.getProperty("java.class.path"); } - private static Triple startServerProcess(LanguageParameter lang) throws IOException { - // TODO Figure out Rascal/Rascal-LSP versions/class path + private @Nullable Triple startServerProcess(LanguageParameter lang) { logger.info("Starting LSP process for {}", lang.getName()); var classPath = classPath(lang); logger.debug("{} runs with class path {}", lang.getName(), classPath); // In deployment, we start a process and connect to it via input/output streams - var proc = new ProcessBuilder(ProcessHandle.current().info().command().orElse("java") - , "-Dlog4j2.configurationFactory=org.rascalmpl.vscode.lsp.log.LogJsonConfiguration" - , "-Dlog4j2.level=DEBUG" - , "-Drascal.fallbackResolver=org.rascalmpl.vscode.lsp.uri.FallbackResolver" - , "-Drascal.lsp.deploy=true" - , "-Drascal.compilerClasspath=" + classPath - , "-Xmx2048M" - , "-cp", classPath - , "org.rascalmpl.vscode.lsp.parametric.ParametricLanguageServer" - , "--exitWhenEmpty" - , new GsonBuilder().create().toJson(lang, LanguageParameter.class).replace("\"", "\\\"") - ) - .redirectError(Redirect.INHERIT) // Show logs in current process - .start(); - - logger.debug("Launched language server on process {}", proc.pid()); - return Triple.of(proc.getInputStream(), proc.getOutputStream(), () -> {}); - } - - private Triple connectToServer(LanguageParameter lang) throws IOException { + try { + var proc = new ProcessBuilder(ProcessHandle.current().info().command().orElse("java") + , "-Dlog4j2.configurationFactory=org.rascalmpl.vscode.lsp.log.LogJsonConfiguration" + , "-Dlog4j2.level=DEBUG" + , "-Drascal.fallbackResolver=org.rascalmpl.vscode.lsp.uri.FallbackResolver" + , "-Drascal.lsp.deploy=true" + , "-Drascal.compilerClasspath=" + classPath + , "-Xmx2048M" + , "-cp", classPath + , "org.rascalmpl.vscode.lsp.parametric.ParametricLanguageServer" + , "--exitWhenEmpty" + , new GsonBuilder().create().toJson(lang, LanguageParameter.class).replace("\"", "\\\"") // escape JSON string on command line + ) + .redirectError(Redirect.INHERIT) // Show logs in current process + .start(); + + // Make sure we can clean this process up when we exit + delegateProcesses.add(proc); + + logger.debug("Launched language server on process {}", proc.pid()); + return Triple.of(proc.getInputStream(), proc.getOutputStream(), () -> {}); + } catch (IOException e) { + logger.error("Starting language server process for {} failed", lang.getName(), e); + return null; + } + } + + private @Nullable Triple connectToServer(LanguageParameter lang) { // In development, we expect the server to have been launched on a pre-agreed port int port = getNextPort(); - @SuppressWarnings("java:S2095") // no need to close the socket here - we close it on server shutdown - Socket socket = new Socket(InetAddress.getLoopbackAddress(), port); - socket.setTcpNoDelay(true); - return Triple.of(socket.getInputStream(), socket.getOutputStream(), () -> { - try { - logger.debug("Closing socket for language {} on port {}", lang.getName(), port); - socket.close(); - } catch (IOException e) { - logger.error("Closing socket for {} on port {} failed", lang.getName(), port); - } - }); + try { + @SuppressWarnings("java:S2095") // no need to close the socket here - we close it on server shutdown + Socket socket = new Socket(InetAddress.getLoopbackAddress(), port); + socket.setTcpNoDelay(true); + return Triple.of(socket.getInputStream(), socket.getOutputStream(), () -> { + try { + logger.debug("Closing socket for language {} on port {}", lang.getName(), port); + socket.close(); + } catch (IOException e) { + logger.error("Closing socket for {} on port {} failed", lang.getName(), port); + } + }); + } catch (IOException e) { + logger.error("Connecting to socket at port {} failed", port, e); + return null; + } } - private CompletableFuture startServer(LanguageParameter lang) throws IOException { + private @Nullable CompletableFuture startServer(LanguageParameter lang) { var serverParams = DEPLOY_MODE ? startServerProcess(lang) : connectToServer(lang) ; + if (serverParams == null) { + return null; + } + var serverLauncher = new Launcher.Builder() .setRemoteInterface(IBaseLanguageServerExtensions.class) .setLocalService(this) .setInput(serverParams.getLeft()) .setOutput(serverParams.getMiddle()) .configureGson(GsonUtils.complexAsJsonObject()) // Only needed if we want to communicate IValues - .setExecutorService(NamedThreadPool.single("parametric-lsp-router-out")) + .setExecutorService(getExecutor()) .create(); var runner = serverLauncher.startListening(); var server = serverLauncher.getRemoteProxy(); - // When initialization is done, we can use the server - var initializedServer = server.initialize(delegateInitializationParams()) + var initializedServer = CompletableFutureUtils.completedFuture(delegateInitializationParams(), getExecutor()) + .thenCompose(server::initialize) .thenApply(ignored -> server); // ignore initialized static server capabilities, since ours are the same getExecutor().execute(() -> { @@ -274,6 +292,7 @@ private CompletableFuture startServer(LanguagePar Thread.currentThread().interrupt(); } finally { if (languageServers.remove(lang.getName(), initializedServer)) { + // TODO Remove extension mapping as well logger.error("Could not remove LSP routing for {}; restart the Rascal extension", lang.getName()); } } @@ -284,7 +303,7 @@ private CompletableFuture startServer(LanguagePar } }); - return initializedServer; + return initializedServer; // When initialization is done, we can use the server } private int getNextPort() { @@ -339,23 +358,17 @@ public CompletableFuture initialize(InitializeParams params) { public synchronized CompletableFuture sendRegisterLanguage(LanguageParameter lang) { logger.debug("rascal/sendRegisterLanguage({}, {})", lang.getName(), lang.getMainFunction()); // If we do not have a parametric server running for this language, start and initialize it. - synchronized (this) { - var server = languageServers.computeIfAbsent(lang.getName(), (Function>) _n -> { - try { - return startServer(lang); - } catch (IOException e) { - logger.error("Unexpected error while starting language server for {}", lang.getName(), e); - return null; + return CompletableFuture.runAsync(() -> { + synchronized (this) { + var server = languageServers.computeIfAbsent(lang.getName(), _n -> startServer(lang)); + if (server == null) { + throw new ResponseErrorException(new ResponseError(ResponseErrorCode.RequestFailed, String.format("Connecting to LSP server for %s failed", lang.getName()), null)); } - }); - if (server != null) { for (var ext : lang.getExtensions()) { languagesByExtension.put(ext, lang.getName()); } } - } - - return super.sendRegisterLanguage(lang); + }, getExecutor()).thenCompose(v -> super.sendRegisterLanguage(lang)); } @Override From 6821900b55da9e1ad1180e44f83302e6e259a1f7 Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Mon, 4 May 2026 10:15:41 +0200 Subject: [PATCH 042/107] Rename thread pool. --- .../vscode/lsp/parametric/routing/RoutingLanguageServer.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingLanguageServer.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingLanguageServer.java index 8b0c34271..80e783794 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingLanguageServer.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingLanguageServer.java @@ -70,7 +70,7 @@ public static void main(String[] args) { // If we get a dedicated language argument, we just start a single parametric server startParametric(serverArgs); } else { - startLanguageServer(NamedThreadPool.single("parametric-lsp-router-in") + startLanguageServer(NamedThreadPool.single("parametric-lsp-router") , NamedThreadPool.cached("parametric-router") , serverArgs.getPort() ); From 80f54bf3cc93056a18d5bdcb1e01dc3c52b825d5 Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Mon, 4 May 2026 10:15:22 +0200 Subject: [PATCH 043/107] Clean up on language unregistration. --- .../vscode/lsp/LanguageServerRouter.java | 31 ++++++++++++++++--- .../routing/RoutingTextDocumentService.java | 2 +- 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/LanguageServerRouter.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/LanguageServerRouter.java index f38cd4b38..8edd06ff7 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/LanguageServerRouter.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/LanguageServerRouter.java @@ -291,9 +291,14 @@ private static String classPath(LanguageParameter lang) { } catch (InterruptedException e) { Thread.currentThread().interrupt(); } finally { - if (languageServers.remove(lang.getName(), initializedServer)) { - // TODO Remove extension mapping as well - logger.error("Could not remove LSP routing for {}; restart the Rascal extension", lang.getName()); + synchronized (this) { + if (languageServers.remove(lang.getName(), initializedServer)) { + for (var ext : lang.getExtensions()) { + languagesByExtension.remove(ext, lang.getName()); + } + } else { + logger.error("Could not remove LSP routing for {}; restart the Rascal extension", lang.getName()); + } } } try { @@ -368,13 +373,29 @@ public synchronized CompletableFuture sendRegisterLanguage(LanguageParamet languagesByExtension.put(ext, lang.getName()); } } - }, getExecutor()).thenCompose(v -> super.sendRegisterLanguage(lang)); + }, getExecutor()) + .thenCompose(v -> super.sendRegisterLanguage(lang)); } @Override public synchronized CompletableFuture sendUnregisterLanguage(LanguageParameter lang) { logger.debug("rascal/sendUnregisterLanguage({})", lang.getName()); - // TODO Handle shutting down the remote parametric server iff it is empty now. + + boolean removeAll = lang.getMainModule() == null || lang.getMainModule().isEmpty(); + if (removeAll) { + // clear the whole language + logger.trace("unregisterLanguage({}) completely", lang.getName()); + + for (var extension : lang.getExtensions()) { + this.languagesByExtension.remove(extension); + } + var removed = languageServers.remove(lang.getName()); + if (removed != null) { + removed.thenCompose(server -> server.shutdown().thenApply(o -> server)) + .thenAccept(server -> server.exit()); + } + } + return super.sendUnregisterLanguage(lang); } diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingTextDocumentService.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingTextDocumentService.java index 113ef59fb..22dd336f4 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingTextDocumentService.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingTextDocumentService.java @@ -201,7 +201,7 @@ public void initialized() { @Override public void registerLanguage(LanguageParameter lang) { logger.debug("textDocument/registerLanguage({}, {})", lang.getName(), lang.getMainFunction()); - availableServer().languageByName(lang.getName()).thenApply(s -> s.sendRegisterLanguage(lang)); + availableServer().languageByName(lang.getName()).thenCompose(s -> s.sendRegisterLanguage(lang)); } @Override From f1dfff79d4c37d46e5766a85a50d62d2378f2ad8 Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Mon, 4 May 2026 12:48:43 +0200 Subject: [PATCH 044/107] Do not use dedicated language mode. --- .../java/org/rascalmpl/vscode/lsp/LanguageServerRouter.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/LanguageServerRouter.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/LanguageServerRouter.java index 8edd06ff7..bb134079f 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/LanguageServerRouter.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/LanguageServerRouter.java @@ -28,7 +28,6 @@ import static org.rascalmpl.vscode.lsp.BaseLanguageServer.DEPLOY_MODE; -import com.google.gson.GsonBuilder; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; @@ -219,7 +218,7 @@ private static String classPath(LanguageParameter lang) { , "-cp", classPath , "org.rascalmpl.vscode.lsp.parametric.ParametricLanguageServer" , "--exitWhenEmpty" - , new GsonBuilder().create().toJson(lang, LanguageParameter.class).replace("\"", "\\\"") // escape JSON string on command line + // , new GsonBuilder().create().toJson(lang, LanguageParameter.class).replace("\"", "\\\"") // escape JSON string on command line ) .redirectError(Redirect.INHERIT) // Show logs in current process .start(); From 110ccbba99a8766ab66d1d616fadbe7222918a9c Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Mon, 4 May 2026 15:14:53 +0200 Subject: [PATCH 045/107] Await language registration before returning. --- .../routing/RoutingTextDocumentService.java | 29 ++++++++++++++++--- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingTextDocumentService.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingTextDocumentService.java index 22dd336f4..f28d42091 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingTextDocumentService.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingTextDocumentService.java @@ -28,7 +28,10 @@ import java.util.List; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; import java.util.stream.Collectors; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -201,20 +204,38 @@ public void initialized() { @Override public void registerLanguage(LanguageParameter lang) { logger.debug("textDocument/registerLanguage({}, {})", lang.getName(), lang.getMainFunction()); - availableServer().languageByName(lang.getName()).thenCompose(s -> s.sendRegisterLanguage(lang)); + try { + availableServer().languageByName(lang.getName()) + .thenAccept(server -> server.sendRegisterLanguage(lang)).get(1, TimeUnit.MINUTES); + } catch (UnsupportedOperationException e) { + // Strange, since we just registered this language and should have a server for it + logger.error("Language registration for unknown language {}", lang.getName()); + return; + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } catch (ExecutionException e ) { + logger.error("Registration of language {} failed due to unexpected error", e); + } catch (TimeoutException e) { + logger.error("Registration of language {} timed out", lang.getName(), e); + } } @Override public void unregisterLanguage(LanguageParameter lang) { logger.debug("textDocument/unregisterLanguage({})", lang.getName()); - CompletableFuture server; try { - server = availableServer().languageByName(lang.getName()); + availableServer().languageByName(lang.getName()) + .thenApply(s -> s.sendUnregisterLanguage(lang)).get(1, TimeUnit.MINUTES); } catch (UnsupportedOperationException e) { logger.debug("Ignored language unregistration for unknown language {}", lang.getName()); return; + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } catch (ExecutionException e ) { + logger.error("Unregistration of language {} failed due to unexpected error", e); + } catch (TimeoutException e) { + logger.error("Unregistration of language {} timed out", lang.getName(), e); } - server.thenApply(s -> s.sendUnregisterLanguage(lang)); } @Override From 7637abea42747f1bdc33ca4b9b86ccb1aee36781 Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Mon, 4 May 2026 15:18:42 +0200 Subject: [PATCH 046/107] Forward close/save notifications. --- .../parametric/routing/RoutingTextDocumentService.java | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingTextDocumentService.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingTextDocumentService.java index f28d42091..65aa28399 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingTextDocumentService.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingTextDocumentService.java @@ -155,24 +155,26 @@ private LanguageServerRouter availableServer() { @Override public void didOpen(DidOpenTextDocumentParams params) { + // Note: floating future callAccept(route(params.getTextDocument()), TextDocumentService::didOpen, params); } @Override public void didChange(DidChangeTextDocumentParams params) { + // Note: floating future callAccept(route(params.getTextDocument()), TextDocumentService::didChange, params); } @Override public void didClose(DidCloseTextDocumentParams params) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'didClose'"); + // Note: floating future + callAccept(route(params.getTextDocument()), TextDocumentService::didClose, params); } @Override public void didSave(DidSaveTextDocumentParams params) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'didSave'"); + // Note: floating future + callAccept(route(params.getTextDocument()), TextDocumentService::didSave, params); } @Override From 8713e45539ef9cff72969855396a5e647cd1fec7 Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Mon, 4 May 2026 15:19:03 +0200 Subject: [PATCH 047/107] Distribute some notifications over all remote servers. --- .../vscode/lsp/LanguageServerRouter.java | 6 ++++++ .../routing/RoutingTextDocumentService.java | 18 ++++++++++++------ .../vscode/lsp/util/DocumentRouter.java | 3 +++ .../vscode/lsp/util/FutureCaller.java | 14 ++++++++++++++ 4 files changed, 35 insertions(+), 6 deletions(-) diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/LanguageServerRouter.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/LanguageServerRouter.java index bb134079f..96b2cc334 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/LanguageServerRouter.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/LanguageServerRouter.java @@ -48,6 +48,7 @@ import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Stream; import org.apache.commons.lang3.tuple.Triple; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -128,6 +129,11 @@ public CompletableFuture languageByName(String la return service; } + @Override + public Stream> allRoutes() { + return languageServers.values().stream(); + } + @Override public CompletableFuture route(ISourceLocation file) { return route(extension(file)); diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingTextDocumentService.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingTextDocumentService.java index 65aa28399..75b830bcd 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingTextDocumentService.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingTextDocumentService.java @@ -33,6 +33,7 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.stream.Collectors; +import java.util.stream.Stream; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; @@ -131,6 +132,13 @@ public void setServer(LanguageServerRouter server) { this.server = server; } + @Override + public Stream> allRoutes() { + return availableServer() + .allRoutes() + .map(server -> server.thenApply(IBaseLanguageServerExtensions::getIBaseTextDocumentService)); + } + @Override public CompletableFuture route(ISourceLocation loc) { return availableServer() @@ -242,14 +250,12 @@ public void unregisterLanguage(LanguageParameter lang) { @Override public void projectAdded(String name, ISourceLocation projectRoot) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'projectAdded'"); + callAccept(allRoutes(), IBaseTextDocumentService::projectAdded, name, projectRoot); } @Override public void projectRemoved(String name, ISourceLocation projectRoot) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'projectRemoved'"); + callAccept(allRoutes(), IBaseTextDocumentService::projectRemoved, name, projectRoot); } @Override @@ -307,8 +313,8 @@ public void didDeleteFiles(DeleteFilesParams params) { @Override public void cancelProgress(String progressId) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'cancelProgress'"); + // Note: floating futures + callAccept(allRoutes(), IBaseTextDocumentService::cancelProgress, progressId); } @Override diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/util/DocumentRouter.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/util/DocumentRouter.java index 72d6eac1b..47b477df1 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/util/DocumentRouter.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/util/DocumentRouter.java @@ -26,6 +26,7 @@ */ package org.rascalmpl.vscode.lsp.util; +import java.util.stream.Stream; import org.eclipse.lsp4j.TextDocumentIdentifier; import org.eclipse.lsp4j.TextDocumentItem; import org.eclipse.lsp4j.VersionedTextDocumentIdentifier; @@ -58,4 +59,6 @@ default T route(TextDocumentIdentifier id) { return route(Locations.toLoc(id.getUri())); } + Stream allRoutes(); + } diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/util/FutureCaller.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/util/FutureCaller.java index a063d6cdb..3a9888921 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/util/FutureCaller.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/util/FutureCaller.java @@ -29,6 +29,8 @@ import java.util.concurrent.CompletableFuture; import java.util.function.BiConsumer; import java.util.function.BiFunction; +import java.util.stream.Stream; +import org.apache.commons.lang3.function.TriConsumer; /** * A helper interface to support calling functions on values in futures. @@ -47,4 +49,16 @@ default CompletableFuture callApply(CompletableFuture t, BiFunction default CompletableFuture callAccept(CompletableFuture t, BiConsumer func, A arg) { return t.thenAccept(actualT -> func.accept(actualT, arg)); } + + default CompletableFuture callAccept(CompletableFuture t, TriConsumer func, A arg1, B arg2) { + return t.thenAccept(actualT -> func.accept(actualT, arg1, arg2)); + } + + default Stream> callAccept(Stream> ts, BiConsumer func, A arg) { + return ts.map(t -> callAccept(t, func, arg)); + } + + default Stream> callAccept(Stream> ts, TriConsumer func, A arg1, B arg2) { + return ts.map(t -> callAccept(t, func, arg1, arg2)); + } } From b5b4a8e8e692ffc3cfc93325d80ea34c61fc5afa Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Mon, 4 May 2026 17:39:53 +0200 Subject: [PATCH 048/107] Attempt to make CF happy. --- .../org/rascalmpl/vscode/lsp/LanguageServerRouter.java | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/LanguageServerRouter.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/LanguageServerRouter.java index 96b2cc334..c2db7316f 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/LanguageServerRouter.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/LanguageServerRouter.java @@ -39,7 +39,6 @@ import java.util.Collection; import java.util.HashSet; import java.util.List; -import java.util.Map; import java.util.Optional; import java.util.concurrent.CancellationException; import java.util.concurrent.CompletableFuture; @@ -101,9 +100,9 @@ public class LanguageServerRouter extends BaseLanguageServer.ActualLanguageServe private static final Logger logger = LogManager.getLogger(LanguageServerRouter.class); - private final Map languagesByExtension; + private final ConcurrentHashMap languagesByExtension = new ConcurrentHashMap<>(); // TODO To be able to route to arbitrary third-party language servers, remote servers should implement `LanguageServer` (instead of `IBaseLanguageServerExtensions`) - private final Map> languageServers; + private final ConcurrentHashMap> languageServers = new ConcurrentHashMap<>(); private final Collection delegateProcesses = new CopyOnWriteArrayList<>(); private @MonotonicNonNull InitializeParams initializeParams; @@ -114,9 +113,6 @@ public class LanguageServerRouter extends BaseLanguageServer.ActualLanguageServe public LanguageServerRouter(Runnable onExit, ExecutorService exec) { super(onExit, exec, new RoutingTextDocumentService(exec), new RoutingWorkspaceService(exec)); - this.languagesByExtension = new ConcurrentHashMap<>(); - this.languageServers = new ConcurrentHashMap<>(); - // Shutdown child processes when we exit Runtime.getRuntime().addShutdownHook(new Thread(() -> delegateProcesses.forEach(Process::destroy))); } From ce9fd1539f3e902340d77dfad4b68e8a11282a9f Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Mon, 4 May 2026 17:51:16 +0200 Subject: [PATCH 049/107] Fix SQ warnings. --- .../vscode/lsp/LanguageServerRouter.java | 27 ++++++++++++------- .../routing/RoutingTextDocumentService.java | 16 +++++------ 2 files changed, 24 insertions(+), 19 deletions(-) diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/LanguageServerRouter.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/LanguageServerRouter.java index c2db7316f..212cf0e81 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/LanguageServerRouter.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/LanguageServerRouter.java @@ -75,6 +75,7 @@ import org.eclipse.lsp4j.jsonrpc.ResponseErrorException; import org.eclipse.lsp4j.jsonrpc.messages.ResponseError; import org.eclipse.lsp4j.jsonrpc.messages.ResponseErrorCode; +import org.eclipse.lsp4j.services.LanguageServer; import org.rascalmpl.ideservices.GsonUtils; import org.rascalmpl.library.util.PathConfig; import org.rascalmpl.uri.URIUtil; @@ -169,10 +170,18 @@ private static boolean isRascalLspProject(Artifact art) { } private static String classPath(LanguageParameter lang) { + PathConfig pcfg; + try { + pcfg = PathConfig.parse(lang.getPathConfig()); + } catch (IOException e) { + logger.error("Error while parsing path config {}", lang.getPathConfig(), e); + logger.warn("Could not compute class path for {}; falling back to class path of routing server", lang.getName()); + return System.getProperty("java.class.path"); + } + + var pom = Locations.toPhysicalIfPossible(URIUtil.getChildLocation(pcfg.getProjectRoot(), "pom.xml")); + var p = new MavenParser(Path.of(pom.getURI())); try { - var pcfg = PathConfig.parse(lang.getPathConfig()); - var pom = Locations.toPhysicalIfPossible(URIUtil.getChildLocation(pcfg.getProjectRoot(), "pom.xml")); - var p = new MavenParser(Path.of(pom.getURI())); var rootProject = p.parseProject(); // Check if we are in Rascal-LSP @@ -191,11 +200,9 @@ private static String classPath(LanguageParameter lang) { } } // strip of the initial separator ';' - return classPath.substring(1).toString(); - } catch (IOException e) { - logger.error("Error while parsing path config {}", lang.getPathConfig(), e); + return classPath.substring(1); } catch (ModelResolutionError e) { - logger.error("Error while parsing Maven project {}", e); + logger.error("Error while parsing POM at {}", pom, e); } // If all else fails, just use the same class path that we have @@ -355,7 +362,7 @@ public CompletableFuture initialize(InitializeParams params) { this.initializeParams = params; // Our child needs us, but we cannot set this in the constructor, so we set it here. - getTextDocumentService().setServer(this); + getTextDocumentService().setParentServer(this); return super.initialize(params); } @@ -366,7 +373,7 @@ public synchronized CompletableFuture sendRegisterLanguage(LanguageParamet // If we do not have a parametric server running for this language, start and initialize it. return CompletableFuture.runAsync(() -> { synchronized (this) { - var server = languageServers.computeIfAbsent(lang.getName(), _n -> startServer(lang)); + var server = languageServers.computeIfAbsent(lang.getName(), ignored -> startServer(lang)); if (server == null) { throw new ResponseErrorException(new ResponseError(ResponseErrorCode.RequestFailed, String.format("Connecting to LSP server for %s failed", lang.getName()), null)); } @@ -393,7 +400,7 @@ public synchronized CompletableFuture sendUnregisterLanguage(LanguageParam var removed = languageServers.remove(lang.getName()); if (removed != null) { removed.thenCompose(server -> server.shutdown().thenApply(o -> server)) - .thenAccept(server -> server.exit()); + .thenAccept(LanguageServer::exit); } } diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingTextDocumentService.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingTextDocumentService.java index ad615ce08..80b19d128 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingTextDocumentService.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingTextDocumentService.java @@ -119,7 +119,7 @@ public class RoutingTextDocumentService implements IBaseTextDocumentService, Fut private @MonotonicNonNull LanguageClient client; private @MonotonicNonNull BaseWorkspaceService wsService; - private @MonotonicNonNull LanguageServerRouter server; + private @MonotonicNonNull LanguageServerRouter parentServer; @SuppressWarnings("unused") private final ExecutorService exec; @@ -128,8 +128,8 @@ public RoutingTextDocumentService(ExecutorService exec) { this.exec = exec; } - public void setServer(LanguageServerRouter server) { - this.server = server; + public void setParentServer(LanguageServerRouter server) { + this.parentServer = server; } @Override @@ -155,10 +155,10 @@ private LanguageClient availableClient() { } private LanguageServerRouter availableServer() { - if (server == null) { + if (parentServer == null) { throw new IllegalStateException("Server not connected yet."); } - return server; + return parentServer; } @Override @@ -220,11 +220,10 @@ public void registerLanguage(LanguageParameter lang) { } catch (UnsupportedOperationException e) { // Strange, since we just registered this language and should have a server for it logger.error("Language registration for unknown language {}", lang.getName()); - return; } catch (InterruptedException e) { Thread.currentThread().interrupt(); } catch (ExecutionException e ) { - logger.error("Registration of language {} failed due to unexpected error", e); + logger.error("Registration of language {} failed due to unexpected error", lang.getName(), e); } catch (TimeoutException e) { logger.error("Registration of language {} timed out", lang.getName(), e); } @@ -238,11 +237,10 @@ public void unregisterLanguage(LanguageParameter lang) { .thenApply(s -> s.sendUnregisterLanguage(lang)).get(1, TimeUnit.MINUTES); } catch (UnsupportedOperationException e) { logger.debug("Ignored language unregistration for unknown language {}", lang.getName()); - return; } catch (InterruptedException e) { Thread.currentThread().interrupt(); } catch (ExecutionException e ) { - logger.error("Unregistration of language {} failed due to unexpected error", e); + logger.error("Unregistration of language {} failed due to unexpected error", lang.getName(), e); } catch (TimeoutException e) { logger.error("Unregistration of language {} timed out", lang.getName(), e); } From 5bc39e0f042b390cf68d4ba9fe4cdd6d58ef64fb Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Tue, 5 May 2026 11:20:15 +0200 Subject: [PATCH 050/107] Upload logs as artifact. --- .github/workflows/build.yaml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index aa2ca6f45..37403fcce 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -129,6 +129,15 @@ jobs: retention-days: 5 if-no-files-found: error + - name: Upload logs + uses: actions/upload-artifact@v7 + if: failure() + with: + name: logs-${{ matrix.os }} + path: ./rascal-vscode-extension/uitests/settings/logs/**/usethesource.rascalmpl/*.log + retention-days: 5 + if-no-files-found: error + - name: "cleanup before cache" shell: bash if: always() From 67950a37240c9369cd7bb20b58af5a629dee8a81 Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Tue, 5 May 2026 16:49:17 +0200 Subject: [PATCH 051/107] Properly compose and await language registration. --- .../vscode/lsp/LanguageServerRouter.java | 20 ++++++++----------- .../routing/RoutingTextDocumentService.java | 4 ++-- 2 files changed, 10 insertions(+), 14 deletions(-) diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/LanguageServerRouter.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/LanguageServerRouter.java index 212cf0e81..a59f8dc82 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/LanguageServerRouter.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/LanguageServerRouter.java @@ -371,18 +371,14 @@ public CompletableFuture initialize(InitializeParams params) { public synchronized CompletableFuture sendRegisterLanguage(LanguageParameter lang) { logger.debug("rascal/sendRegisterLanguage({}, {})", lang.getName(), lang.getMainFunction()); // If we do not have a parametric server running for this language, start and initialize it. - return CompletableFuture.runAsync(() -> { - synchronized (this) { - var server = languageServers.computeIfAbsent(lang.getName(), ignored -> startServer(lang)); - if (server == null) { - throw new ResponseErrorException(new ResponseError(ResponseErrorCode.RequestFailed, String.format("Connecting to LSP server for %s failed", lang.getName()), null)); - } - for (var ext : lang.getExtensions()) { - languagesByExtension.put(ext, lang.getName()); - } - } - }, getExecutor()) - .thenCompose(v -> super.sendRegisterLanguage(lang)); + var server = languageServers.computeIfAbsent(lang.getName(), ignored -> startServer(lang)); + if (server == null) { + throw new ResponseErrorException(new ResponseError(ResponseErrorCode.RequestFailed, String.format("Connecting to LSP server for %s failed", lang.getName()), null)); + } + for (var ext : lang.getExtensions()) { + languagesByExtension.put(ext, lang.getName()); + } + return super.sendRegisterLanguage(lang); } @Override diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingTextDocumentService.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingTextDocumentService.java index 80b19d128..459b3b877 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingTextDocumentService.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingTextDocumentService.java @@ -216,7 +216,7 @@ public void registerLanguage(LanguageParameter lang) { logger.debug("textDocument/registerLanguage({}, {})", lang.getName(), lang.getMainFunction()); try { availableServer().languageByName(lang.getName()) - .thenAccept(server -> server.sendRegisterLanguage(lang)).get(1, TimeUnit.MINUTES); + .thenCompose(server -> server.sendRegisterLanguage(lang)).get(1, TimeUnit.MINUTES); } catch (UnsupportedOperationException e) { // Strange, since we just registered this language and should have a server for it logger.error("Language registration for unknown language {}", lang.getName()); @@ -234,7 +234,7 @@ public void unregisterLanguage(LanguageParameter lang) { logger.debug("textDocument/unregisterLanguage({})", lang.getName()); try { availableServer().languageByName(lang.getName()) - .thenApply(s -> s.sendUnregisterLanguage(lang)).get(1, TimeUnit.MINUTES); + .thenCompose(s -> s.sendUnregisterLanguage(lang)).get(1, TimeUnit.MINUTES); } catch (UnsupportedOperationException e) { logger.debug("Ignored language unregistration for unknown language {}", lang.getName()); } catch (InterruptedException e) { From 3feac15343694600c908d7df2ce0a2d15c2a0d48 Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Tue, 5 May 2026 16:55:20 +0200 Subject: [PATCH 052/107] Re-do cleanup of child processes without hooks or bookkeeping. --- .../vscode/lsp/LanguageServerRouter.java | 35 +++++++++++++------ 1 file changed, 24 insertions(+), 11 deletions(-) diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/LanguageServerRouter.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/LanguageServerRouter.java index a59f8dc82..531f71a55 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/LanguageServerRouter.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/LanguageServerRouter.java @@ -36,16 +36,16 @@ import java.net.Socket; import java.net.URI; import java.nio.file.Path; -import java.util.Collection; import java.util.HashSet; import java.util.List; import java.util.Optional; import java.util.concurrent.CancellationException; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.Stream; import org.apache.commons.lang3.tuple.Triple; @@ -103,8 +103,10 @@ public class LanguageServerRouter extends BaseLanguageServer.ActualLanguageServe private final ConcurrentHashMap languagesByExtension = new ConcurrentHashMap<>(); // TODO To be able to route to arbitrary third-party language servers, remote servers should implement `LanguageServer` (instead of `IBaseLanguageServerExtensions`) + // Note: + // 1. This map should only contains running server processes. + // 2. Upon removal from this map, the process should be killed to avoid resource leaks. private final ConcurrentHashMap> languageServers = new ConcurrentHashMap<>(); - private final Collection delegateProcesses = new CopyOnWriteArrayList<>(); private @MonotonicNonNull InitializeParams initializeParams; @@ -113,9 +115,6 @@ public class LanguageServerRouter extends BaseLanguageServer.ActualLanguageServe public LanguageServerRouter(Runnable onExit, ExecutorService exec) { super(onExit, exec, new RoutingTextDocumentService(exec), new RoutingWorkspaceService(exec)); - - // Shutdown child processes when we exit - Runtime.getRuntime().addShutdownHook(new Thread(() -> delegateProcesses.forEach(Process::destroy))); } public CompletableFuture languageByName(String lang) { @@ -232,9 +231,6 @@ private static String classPath(LanguageParameter lang) { .redirectError(Redirect.INHERIT) // Show logs in current process .start(); - // Make sure we can clean this process up when we exit - delegateProcesses.add(proc); - logger.debug("Launched language server on process {}", proc.pid()); return Triple.of(proc.getInputStream(), proc.getOutputStream(), () -> {}); } catch (IOException e) { @@ -304,8 +300,6 @@ private static String classPath(LanguageParameter lang) { for (var ext : lang.getExtensions()) { languagesByExtension.remove(ext, lang.getName()); } - } else { - logger.error("Could not remove LSP routing for {}; restart the Rascal extension", lang.getName()); } } } @@ -532,4 +526,23 @@ public CompletableFuture> workspaceFolders() { return availableClient().workspaceFolders(); } + @Override + public CompletableFuture shutdown() { + return CompletableFutureUtils.reduce(allRoutes().map(serverFut -> serverFut.thenCompose(IBaseLanguageServerExtensions::shutdown)), getExecutor()) + .thenCompose(ignored -> super.shutdown()); + } + + @Override + public void exit() { + try { + CompletableFutureUtils.reduce(allRoutes().map(serverFut -> serverFut.thenAccept(IBaseLanguageServerExtensions::exit)), getExecutor()).get(10, TimeUnit.SECONDS); + } catch (ExecutionException | TimeoutException e) { + logger.error("Error while exiting child processes", e); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } finally { + super.exit(); + } + } + } From 9054f9b61dafc46764519a5175d46db3b255e9be Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Tue, 5 May 2026 17:20:24 +0200 Subject: [PATCH 053/107] Make class path separator OS-dependent. --- .../org/rascalmpl/vscode/lsp/LanguageServerRouter.java | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/LanguageServerRouter.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/LanguageServerRouter.java index 531f71a55..f73bbbf26 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/LanguageServerRouter.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/LanguageServerRouter.java @@ -169,6 +169,7 @@ private static boolean isRascalLspProject(Artifact art) { } private static String classPath(LanguageParameter lang) { + var separator = System.getProperty("path.separator"); PathConfig pcfg; try { pcfg = PathConfig.parse(lang.getPathConfig()); @@ -186,7 +187,7 @@ private static String classPath(LanguageParameter lang) { // Check if we are in Rascal-LSP var classPath = new StringBuilder(); if (isRascalLspProject(rootProject)) { - classPath.append(';'); + classPath.append(separator); classPath.append(Path.of(Locations.toUri(pcfg.getBin()))); } @@ -194,12 +195,12 @@ private static String classPath(LanguageParameter lang) { var deps = rootProject.resolveDependencies(Scope.COMPILE, p); for (var d : deps) { if (d.getResolved() != null) { - classPath.append(';'); + classPath.append(separator); classPath.append(d.getResolved()); } } - // strip of the initial separator ';' - return classPath.substring(1); + // strip of the initial separator + return classPath.substring(separator.length()); } catch (ModelResolutionError e) { logger.error("Error while parsing POM at {}", pom, e); } From 6ee7eca38e602de20646542d92b749d28df9acbf Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Tue, 5 May 2026 17:47:31 +0200 Subject: [PATCH 054/107] Fix CF by casting function. --- .../src/main/checkerframework/jdk.astub | 20 +++++++++++++++++++ .../vscode/lsp/LanguageServerRouter.java | 8 +++++--- 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/rascal-lsp/src/main/checkerframework/jdk.astub b/rascal-lsp/src/main/checkerframework/jdk.astub index ff8f76718..3c3ffded2 100644 --- a/rascal-lsp/src/main/checkerframework/jdk.astub +++ b/rascal-lsp/src/main/checkerframework/jdk.astub @@ -61,3 +61,23 @@ public interface Map { K key, BiFunction remappingFunction) {} } + +package java.util.concurrent; + +import org.checkerframework.checker.nullness.qual.*; + +public interface ConcurrentHashMap { + // Since ConcurrentHashMap does not permit null values, compute functions can only return `null` if the compute function returns null + + public @PolyNull V compute( + K key, + BiFunction remappingFunction) {} + + public @PolyNull V computeIfAbsent( + K key, + Function computeFunction) {} + + public @PolyNull V computeIfPresent( + K key, + BiFunction remappingFunction) {} +} diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/LanguageServerRouter.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/LanguageServerRouter.java index f73bbbf26..44e509168 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/LanguageServerRouter.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/LanguageServerRouter.java @@ -38,6 +38,7 @@ import java.nio.file.Path; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Optional; import java.util.concurrent.CancellationException; import java.util.concurrent.CompletableFuture; @@ -47,6 +48,7 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Function; import java.util.stream.Stream; import org.apache.commons.lang3.tuple.Triple; import org.apache.logging.log4j.LogManager; @@ -101,12 +103,12 @@ public class LanguageServerRouter extends BaseLanguageServer.ActualLanguageServe private static final Logger logger = LogManager.getLogger(LanguageServerRouter.class); - private final ConcurrentHashMap languagesByExtension = new ConcurrentHashMap<>(); + private final Map languagesByExtension = new ConcurrentHashMap<>(); // TODO To be able to route to arbitrary third-party language servers, remote servers should implement `LanguageServer` (instead of `IBaseLanguageServerExtensions`) // Note: // 1. This map should only contains running server processes. // 2. Upon removal from this map, the process should be killed to avoid resource leaks. - private final ConcurrentHashMap> languageServers = new ConcurrentHashMap<>(); + private final Map> languageServers = new ConcurrentHashMap<>(); private @MonotonicNonNull InitializeParams initializeParams; @@ -366,7 +368,7 @@ public CompletableFuture initialize(InitializeParams params) { public synchronized CompletableFuture sendRegisterLanguage(LanguageParameter lang) { logger.debug("rascal/sendRegisterLanguage({}, {})", lang.getName(), lang.getMainFunction()); // If we do not have a parametric server running for this language, start and initialize it. - var server = languageServers.computeIfAbsent(lang.getName(), ignored -> startServer(lang)); + var server = languageServers.computeIfAbsent(lang.getName(), (Function>) ignored -> startServer(lang)); if (server == null) { throw new ResponseErrorException(new ResponseError(ResponseErrorCode.RequestFailed, String.format("Connecting to LSP server for %s failed", lang.getName()), null)); } From 1fd3c42c4589171b903931de133a664569342231 Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Fri, 8 May 2026 12:21:32 +0200 Subject: [PATCH 055/107] Encode as base64 when decoding is needed. --- rascal-lsp/.vscode/launch.json | 10 +++++-- .../vscode/lsp/BaseLanguageServer.java | 16 +++++----- .../vscode/lsp/LanguageServerRouter.java | 4 ++- .../parametric/ParametricLanguageServer.java | 29 ++++++++++++++++++- .../routing/RoutingLanguageServer.java | 8 +++-- .../lsp/rascal/RascalLanguageServer.java | 3 +- 6 files changed, 55 insertions(+), 15 deletions(-) diff --git a/rascal-lsp/.vscode/launch.json b/rascal-lsp/.vscode/launch.json index 5d25815d2..714fb6183 100644 --- a/rascal-lsp/.vscode/launch.json +++ b/rascal-lsp/.vscode/launch.json @@ -32,7 +32,10 @@ "name": "Delegate Parametric Server [1]", "request": "launch", "mainClass": "org.rascalmpl.vscode.lsp.parametric.ParametricLanguageServer", - "args": ["--port", "9990"], + "args": [ + "--port", "9990", + "--complexTypeMode", "base64", + ], "projectName": "rascal-lsp", "console": "internalConsole", "vmArgs": [ @@ -46,7 +49,10 @@ "name": "Delegate Parametric Server [2]", "request": "launch", "mainClass": "org.rascalmpl.vscode.lsp.parametric.ParametricLanguageServer", - "args": ["--port", "9991"], + "args": [ + "--port", "9991", + "--complexTypeMode", "base64", + ], "projectName": "rascal-lsp", "console": "internalConsole", "vmArgs": [ diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/BaseLanguageServer.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/BaseLanguageServer.java index eb6668d26..c35fb76fc 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/BaseLanguageServer.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/BaseLanguageServer.java @@ -58,7 +58,9 @@ import org.eclipse.lsp4j.services.LanguageClient; import org.eclipse.lsp4j.services.LanguageClientAware; import org.rascalmpl.ideservices.GsonUtils; +import org.rascalmpl.ideservices.GsonUtils.ComplexTypeMode; import org.rascalmpl.library.util.PathConfig; +import org.rascalmpl.values.RascalValueFactory; import org.rascalmpl.vscode.lsp.log.LogRedirectConfiguration; import org.rascalmpl.vscode.lsp.parametric.LanguageRegistry.LanguageParameter; import org.rascalmpl.vscode.lsp.terminal.RemoteIDEServicesThread; @@ -104,19 +106,19 @@ protected BaseLanguageServer() {} private static final Logger logger = LogManager.getLogger(BaseLanguageServer.class); - protected static Launcher constructLSPClient(Socket client, ActualLanguageServer server, ExecutorService threadPool) + protected static Launcher constructLSPClient(Socket client, ActualLanguageServer server, ExecutorService threadPool, ComplexTypeMode complexTypeMode) throws IOException { client.setTcpNoDelay(true); - return constructLSPClient(client.getInputStream(), client.getOutputStream(), server, threadPool); + return constructLSPClient(client.getInputStream(), client.getOutputStream(), server, threadPool, complexTypeMode); } - protected static Launcher constructLSPClient(InputStream in, OutputStream out, ActualLanguageServer server, ExecutorService threadPool) { + protected static Launcher constructLSPClient(InputStream in, OutputStream out, ActualLanguageServer server, ExecutorService threadPool, ComplexTypeMode complexTypeMode) { Launcher clientLauncher = new Launcher.Builder() .setLocalService(server) .setRemoteInterface(IBaseLanguageClient.class) .setInput(in) .setOutput(out) - .configureGson(GsonUtils.complexAsJsonObject()) + .configureGson(builder -> GsonUtils.configureGson(builder, complexTypeMode, RascalValueFactory.getStore())) .setExecutorService(threadPool) .create(); @@ -130,14 +132,14 @@ protected static void printClassPath() { } @SuppressWarnings({"java:S2189", "java:S106"}) - public static void startLanguageServer(ExecutorService requestPool, ExecutorService workerPool, Function docServiceProvider, Function workspaceServiceProvider, int portNumber) { + public static void startLanguageServer(ExecutorService requestPool, ExecutorService workerPool, Function docServiceProvider, Function workspaceServiceProvider, int portNumber, ComplexTypeMode complexTypeMode) { logger.info("Starting Rascal Language Server: {}", getVersion()); printClassPath(); if (DEPLOY_MODE) { var docService = docServiceProvider.apply(workerPool); var wsService = workspaceServiceProvider.apply(workerPool); - startLSP(constructLSPClient(capturedIn, capturedOut, new ActualLanguageServer(() -> System.exit(0), workerPool, docService, wsService), requestPool)); + startLSP(constructLSPClient(capturedIn, capturedOut, new ActualLanguageServer(() -> System.exit(0), workerPool, docService, wsService), requestPool, complexTypeMode)); } else { try (ServerSocket serverSocket = new ServerSocket(portNumber, 0, InetAddress.getByName("127.0.0.1"))) { @@ -145,7 +147,7 @@ public static void startLanguageServer(ExecutorService requestPool, ExecutorServ while (true) { var docService = docServiceProvider.apply(workerPool); var wsService = workspaceServiceProvider.apply(workerPool); - startLSP(constructLSPClient(serverSocket.accept(), new ActualLanguageServer(() -> {}, workerPool, docService, wsService), requestPool)); + startLSP(constructLSPClient(serverSocket.accept(), new ActualLanguageServer(() -> {}, workerPool, docService, wsService), requestPool, complexTypeMode)); } } catch (IOException e) { logger.fatal("Failure to start TCP server on port {}", portNumber, e); diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/LanguageServerRouter.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/LanguageServerRouter.java index 44e509168..a601d63f5 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/LanguageServerRouter.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/LanguageServerRouter.java @@ -85,6 +85,7 @@ import org.rascalmpl.util.maven.MavenParser; import org.rascalmpl.util.maven.ModelResolutionError; import org.rascalmpl.util.maven.Scope; +import org.rascalmpl.values.RascalValueFactory; import org.rascalmpl.vscode.lsp.parametric.LanguageRegistry.LanguageParameter; import org.rascalmpl.vscode.lsp.parametric.routing.RoutingTextDocumentService; import org.rascalmpl.vscode.lsp.parametric.routing.RoutingWorkspaceService; @@ -229,6 +230,7 @@ private static String classPath(LanguageParameter lang) { , "-cp", classPath , "org.rascalmpl.vscode.lsp.parametric.ParametricLanguageServer" , "--exitWhenEmpty" + , "--complexTypeMode", "base64" // , new GsonBuilder().create().toJson(lang, LanguageParameter.class).replace("\"", "\\\"") // escape JSON string on command line ) .redirectError(Redirect.INHERIT) // Show logs in current process @@ -278,7 +280,7 @@ private static String classPath(LanguageParameter lang) { .setLocalService(this) .setInput(serverParams.getLeft()) .setOutput(serverParams.getMiddle()) - .configureGson(GsonUtils.complexAsJsonObject()) // Only needed if we want to communicate IValues + .configureGson(GsonUtils.complexAsBase64String(RascalValueFactory.getStore())) .setExecutorService(getExecutor()) .create(); diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricLanguageServer.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricLanguageServer.java index 94f93ffae..3d42643fd 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricLanguageServer.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricLanguageServer.java @@ -29,6 +29,7 @@ import com.google.gson.GsonBuilder; import org.checkerframework.checker.nullness.qual.Nullable; +import org.rascalmpl.ideservices.GsonUtils.ComplexTypeMode; import org.rascalmpl.vscode.lsp.BaseLanguageServer; import org.rascalmpl.vscode.lsp.parametric.LanguageRegistry.LanguageParameter; import org.rascalmpl.vscode.lsp.util.NamedThreadPool; @@ -41,6 +42,7 @@ protected static void startParametric(ServerArgs args) { , threadPool -> new ParametricTextDocumentService(threadPool, args.getDedicatedLanguage(), args.isExitWhenEmpty()) , ParametricWorkspaceService::new , args.getPort() + , args.getComplexTypeMode() ); } @@ -52,6 +54,7 @@ public static class ServerArgs { private int port = 9999; private @Nullable LanguageParameter dedicatedLanguage = null; private boolean exitWhenEmpty = false; + private ComplexTypeMode complexTypeMode = ComplexTypeMode.ENCODE_AS_JSON_OBJECT; public int getPort() { return port; @@ -77,9 +80,17 @@ public void setExitWhenEmpty(boolean exitWhenEmpty) { this.exitWhenEmpty = exitWhenEmpty; } + public ComplexTypeMode getComplexTypeMode() { + return complexTypeMode; + } + + public void setComplexTypeMode(ComplexTypeMode complexTypeMode) { + this.complexTypeMode = complexTypeMode; + } + } - @SuppressWarnings("java:S127") // skipping next argument from loop + @SuppressWarnings("java:S127") // skipping next argument in loop protected static ServerArgs parseArgs(String[] args) { var serverArgs = new ServerArgs(); for (int i = 0; i < args.length; i++) { @@ -90,7 +101,23 @@ protected static ServerArgs parseArgs(String[] args) { case "--exitWhenEmpty": serverArgs.setExitWhenEmpty(true); break; + case "--complexTypeMode": { + switch (args[++i].toLowerCase()) { + case "json": + serverArgs.setComplexTypeMode(ComplexTypeMode.ENCODE_AS_JSON_OBJECT); + break; + case "base64": + serverArgs.setComplexTypeMode(ComplexTypeMode.ENCODE_AS_BASE64_STRING); + break; + case "string": + serverArgs.setComplexTypeMode(ComplexTypeMode.ENCODE_AS_STRING); + break; + default: // do nothing - we already have a default value + } + break; + } default: + // 'Positional' argument (for backwards compatibility) if (serverArgs.getDedicatedLanguage() == null) { serverArgs.setDedicatedLanguage(new GsonBuilder().create().fromJson(args[i], LanguageParameter.class)); } diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingLanguageServer.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingLanguageServer.java index 80e783794..1f415f9ec 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingLanguageServer.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingLanguageServer.java @@ -32,6 +32,7 @@ import java.util.concurrent.ExecutorService; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.rascalmpl.ideservices.GsonUtils.ComplexTypeMode; import org.rascalmpl.vscode.lsp.LanguageServerRouter; import org.rascalmpl.vscode.lsp.parametric.ParametricLanguageServer; import org.rascalmpl.vscode.lsp.util.NamedThreadPool; @@ -45,18 +46,18 @@ public class RoutingLanguageServer extends ParametricLanguageServer { private static final Logger logger = LogManager.getLogger(RoutingLanguageServer.class); @SuppressWarnings("java:S2189") // endless loop is fine for the development server - public static void startLanguageServer(ExecutorService requestPool, ExecutorService workerPool, int portNumber) { + public static void startLanguageServer(ExecutorService requestPool, ExecutorService workerPool, int portNumber, ComplexTypeMode complexTypeMode) { logger.info("Starting Rascal Language Server Router: {}", getVersion()); printClassPath(); if (DEPLOY_MODE) { - startLSP(constructLSPClient(capturedIn, capturedOut, new LanguageServerRouter(() -> System.exit(0), workerPool), requestPool)); + startLSP(constructLSPClient(capturedIn, capturedOut, new LanguageServerRouter(() -> System.exit(0), workerPool), requestPool, complexTypeMode)); } else { try (ServerSocket serverSocket = new ServerSocket(portNumber, 0, InetAddress.getByName("127.0.0.1"))) { logger.info("Rascal LSP server router listens on port number: {}", portNumber); while (true) { - startLSP(constructLSPClient(serverSocket.accept(), new LanguageServerRouter(() -> {}, workerPool), requestPool)); + startLSP(constructLSPClient(serverSocket.accept(), new LanguageServerRouter(() -> {}, workerPool), requestPool, complexTypeMode)); } } catch (IOException e) { logger.fatal("Failure to start TCP server on port {}", portNumber, e); @@ -73,6 +74,7 @@ public static void main(String[] args) { startLanguageServer(NamedThreadPool.single("parametric-lsp-router") , NamedThreadPool.cached("parametric-router") , serverArgs.getPort() + , serverArgs.getComplexTypeMode() ); } } diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/RascalLanguageServer.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/RascalLanguageServer.java index e36353df8..ffa620961 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/RascalLanguageServer.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/RascalLanguageServer.java @@ -29,13 +29,14 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.rascalmpl.ideservices.GsonUtils.ComplexTypeMode; import org.rascalmpl.vscode.lsp.BaseLanguageServer; import org.rascalmpl.vscode.lsp.util.NamedThreadPool; public class RascalLanguageServer extends BaseLanguageServer { public static void main(String[] args) { try { - startLanguageServer(NamedThreadPool.single("rascal-lsp"), NamedThreadPool.cached("rascal"), RascalTextDocumentService::new, RascalWorkspaceService::new, 8888); + startLanguageServer(NamedThreadPool.single("rascal-lsp"), NamedThreadPool.cached("rascal"), RascalTextDocumentService::new, RascalWorkspaceService::new, 8888, ComplexTypeMode.ENCODE_AS_JSON_OBJECT); } catch (Throwable e) { final Logger logger = LogManager.getLogger(RascalLanguageServer.class); From 03e601198dfcb05c59ddf274b6b96e06af0803ea Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Fri, 8 May 2026 12:21:49 +0200 Subject: [PATCH 056/107] Add TODO for later. --- .../lsp/parametric/routing/RoutingTextDocumentService.java | 1 + 1 file changed, 1 insertion(+) diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingTextDocumentService.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingTextDocumentService.java index 459b3b877..b8482cee8 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingTextDocumentService.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingTextDocumentService.java @@ -259,6 +259,7 @@ public void projectRemoved(String name, ISourceLocation projectRoot) { @Override public CompletableFuture executeCommand(String languageName, String command) { return availableServer().languageByName(languageName) + // TODO Go via workspace service instead .thenCompose(s -> s.getIBaseTextDocumentService().executeCommand(languageName, command)); } From b51e4977e7a8a44b87e26ed8e4efea52de88de0c Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Fri, 8 May 2026 12:27:12 +0200 Subject: [PATCH 057/107] Route commands via workspace service like other servers. --- .../vscode/lsp/LanguageServerRouter.java | 1 + .../routing/RoutingTextDocumentService.java | 4 +-- .../routing/RoutingWorkspaceService.java | 31 +++++++++++++++++++ 3 files changed, 33 insertions(+), 3 deletions(-) diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/LanguageServerRouter.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/LanguageServerRouter.java index a601d63f5..305e94d4b 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/LanguageServerRouter.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/LanguageServerRouter.java @@ -362,6 +362,7 @@ public CompletableFuture initialize(InitializeParams params) { // Our child needs us, but we cannot set this in the constructor, so we set it here. getTextDocumentService().setParentServer(this); + getWorkspaceService().setParentServer(this); return super.initialize(params); } diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingTextDocumentService.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingTextDocumentService.java index b8482cee8..32fb0e0e3 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingTextDocumentService.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingTextDocumentService.java @@ -258,9 +258,7 @@ public void projectRemoved(String name, ISourceLocation projectRoot) { @Override public CompletableFuture executeCommand(String languageName, String command) { - return availableServer().languageByName(languageName) - // TODO Go via workspace service instead - .thenCompose(s -> s.getIBaseTextDocumentService().executeCommand(languageName, command)); + throw new UnsupportedOperationException("Call RoutingWorkspaceService::executeCommand instead"); } @Override diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingWorkspaceService.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingWorkspaceService.java index beb3a3dd7..146e71475 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingWorkspaceService.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingWorkspaceService.java @@ -26,16 +26,47 @@ */ package org.rascalmpl.vscode.lsp.parametric.routing; +import com.google.gson.JsonPrimitive; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutorService; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; +import org.eclipse.lsp4j.ExecuteCommandParams; import org.rascalmpl.vscode.lsp.BaseWorkspaceService; +import org.rascalmpl.vscode.lsp.LanguageServerRouter; +import org.rascalmpl.vscode.lsp.util.concurrent.CompletableFutureUtils; /** * A language-parametric workspace service that routes incoming requests to remote dedicated language servers. */ public class RoutingWorkspaceService extends BaseWorkspaceService { + private @MonotonicNonNull LanguageServerRouter parentServer; + public RoutingWorkspaceService(ExecutorService exec) { super(exec); } + public void setParentServer(LanguageServerRouter server) { + this.parentServer = server; + } + + private LanguageServerRouter availableServer() { + if (parentServer == null) { + throw new IllegalStateException("Server not connected yet."); + } + return parentServer; + } + + @Override + public CompletableFuture executeCommand(ExecuteCommandParams commandParams) { + if (commandParams.getCommand().startsWith(RASCAL_META_COMMAND) || commandParams.getCommand().startsWith(RASCAL_COMMAND)) { + var languageName = ((JsonPrimitive) commandParams.getArguments().get(0)).getAsString(); + return availableServer().languageByName(languageName) + .thenCompose(s -> s.getWorkspaceService().executeCommand(commandParams)); + } + + return CompletableFutureUtils.completedFuture(commandParams.getCommand() + " was ignored.", getExecutor()); + } + + } From 3acc0ce9835ba477549a18d576fcd29855757416 Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Mon, 11 May 2026 10:33:42 +0200 Subject: [PATCH 058/107] Move routing server to dedicated package. --- .../org/rascalmpl/vscode/lsp/BaseLanguageServer.java | 4 ++-- .../routing}/LanguageServerRouter.java | 11 +++++------ .../lsp/parametric/routing/RoutingLanguageServer.java | 1 - .../routing/RoutingTextDocumentService.java | 1 - .../parametric/routing/RoutingWorkspaceService.java | 1 - 5 files changed, 7 insertions(+), 11 deletions(-) rename rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/{ => parametric/routing}/LanguageServerRouter.java (98%) diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/BaseLanguageServer.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/BaseLanguageServer.java index c35fb76fc..6decb6bd2 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/BaseLanguageServer.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/BaseLanguageServer.java @@ -80,7 +80,7 @@ public abstract class BaseLanguageServer { protected static final PrintStream capturedOut; protected static final InputStream capturedIn; - protected static final boolean DEPLOY_MODE; + public static final boolean DEPLOY_MODE; private static final String LOG_CONFIGURATION_KEY = "log4j2.configurationFactory"; static { @@ -192,7 +192,7 @@ protected static void startLSP(Launcher server) { } } } - protected static class ActualLanguageServer implements IBaseLanguageServerExtensions, LanguageClientAware { + public static class ActualLanguageServer implements IBaseLanguageServerExtensions, LanguageClientAware { static final Logger logger = LogManager.getLogger(ActualLanguageServer.class); private final IBaseTextDocumentService lspDocumentService; private final BaseWorkspaceService lspWorkspaceService; diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/LanguageServerRouter.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/LanguageServerRouter.java similarity index 98% rename from rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/LanguageServerRouter.java rename to rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/LanguageServerRouter.java index 305e94d4b..f7f8626ac 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/LanguageServerRouter.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/LanguageServerRouter.java @@ -24,9 +24,7 @@ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ -package org.rascalmpl.vscode.lsp; - -import static org.rascalmpl.vscode.lsp.BaseLanguageServer.DEPLOY_MODE; +package org.rascalmpl.vscode.lsp.parametric.routing; import java.io.IOException; import java.io.InputStream; @@ -86,9 +84,10 @@ import org.rascalmpl.util.maven.ModelResolutionError; import org.rascalmpl.util.maven.Scope; import org.rascalmpl.values.RascalValueFactory; +import org.rascalmpl.vscode.lsp.BaseLanguageServer; +import org.rascalmpl.vscode.lsp.IBaseLanguageClient; +import org.rascalmpl.vscode.lsp.IBaseLanguageServerExtensions; import org.rascalmpl.vscode.lsp.parametric.LanguageRegistry.LanguageParameter; -import org.rascalmpl.vscode.lsp.parametric.routing.RoutingTextDocumentService; -import org.rascalmpl.vscode.lsp.parametric.routing.RoutingWorkspaceService; import org.rascalmpl.vscode.lsp.util.DocumentRouter; import org.rascalmpl.vscode.lsp.util.concurrent.CompletableFutureUtils; import org.rascalmpl.vscode.lsp.util.locations.Locations; @@ -266,7 +265,7 @@ private static String classPath(LanguageParameter lang) { } private @Nullable CompletableFuture startServer(LanguageParameter lang) { - var serverParams = DEPLOY_MODE + var serverParams = BaseLanguageServer.DEPLOY_MODE ? startServerProcess(lang) : connectToServer(lang) ; diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingLanguageServer.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingLanguageServer.java index 1f415f9ec..a4d3052f5 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingLanguageServer.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingLanguageServer.java @@ -33,7 +33,6 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.rascalmpl.ideservices.GsonUtils.ComplexTypeMode; -import org.rascalmpl.vscode.lsp.LanguageServerRouter; import org.rascalmpl.vscode.lsp.parametric.ParametricLanguageServer; import org.rascalmpl.vscode.lsp.util.NamedThreadPool; diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingTextDocumentService.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingTextDocumentService.java index 32fb0e0e3..e93665a8e 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingTextDocumentService.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingTextDocumentService.java @@ -98,7 +98,6 @@ import org.rascalmpl.vscode.lsp.BaseWorkspaceService; import org.rascalmpl.vscode.lsp.IBaseLanguageServerExtensions; import org.rascalmpl.vscode.lsp.IBaseTextDocumentService; -import org.rascalmpl.vscode.lsp.LanguageServerRouter; import org.rascalmpl.vscode.lsp.TextDocumentState; import org.rascalmpl.vscode.lsp.parametric.LanguageRegistry.LanguageParameter; import org.rascalmpl.vscode.lsp.parametric.ParametricTextDocumentService; diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingWorkspaceService.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingWorkspaceService.java index 146e71475..9d62cb379 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingWorkspaceService.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingWorkspaceService.java @@ -32,7 +32,6 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.eclipse.lsp4j.ExecuteCommandParams; import org.rascalmpl.vscode.lsp.BaseWorkspaceService; -import org.rascalmpl.vscode.lsp.LanguageServerRouter; import org.rascalmpl.vscode.lsp.util.concurrent.CompletableFutureUtils; /** From e648018baf9e39b587023071d2b9b13d7f9039af Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Mon, 11 May 2026 13:46:48 +0200 Subject: [PATCH 059/107] Proxy IValues by wrapping the wrap JSON. --- rascal-lsp/.vscode/launch.json | 10 +- .../vscode/lsp/BaseLanguageServer.java | 16 +- .../vscode/lsp/IBaseTextDocumentService.java | 2 - .../parametric/ParametricLanguageServer.java | 29 +-- .../routing/LanguageServerRouter.java | 37 +++- .../lsp/parametric/routing/ProxiedIValue.java | 185 ++++++++++++++++++ .../routing/RoutingLanguageServer.java | 8 +- .../lsp/rascal/RascalLanguageServer.java | 3 +- 8 files changed, 232 insertions(+), 58 deletions(-) create mode 100644 rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/ProxiedIValue.java diff --git a/rascal-lsp/.vscode/launch.json b/rascal-lsp/.vscode/launch.json index 714fb6183..5d25815d2 100644 --- a/rascal-lsp/.vscode/launch.json +++ b/rascal-lsp/.vscode/launch.json @@ -32,10 +32,7 @@ "name": "Delegate Parametric Server [1]", "request": "launch", "mainClass": "org.rascalmpl.vscode.lsp.parametric.ParametricLanguageServer", - "args": [ - "--port", "9990", - "--complexTypeMode", "base64", - ], + "args": ["--port", "9990"], "projectName": "rascal-lsp", "console": "internalConsole", "vmArgs": [ @@ -49,10 +46,7 @@ "name": "Delegate Parametric Server [2]", "request": "launch", "mainClass": "org.rascalmpl.vscode.lsp.parametric.ParametricLanguageServer", - "args": [ - "--port", "9991", - "--complexTypeMode", "base64", - ], + "args": ["--port", "9991"], "projectName": "rascal-lsp", "console": "internalConsole", "vmArgs": [ diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/BaseLanguageServer.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/BaseLanguageServer.java index 6decb6bd2..0ad631d5e 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/BaseLanguageServer.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/BaseLanguageServer.java @@ -58,9 +58,7 @@ import org.eclipse.lsp4j.services.LanguageClient; import org.eclipse.lsp4j.services.LanguageClientAware; import org.rascalmpl.ideservices.GsonUtils; -import org.rascalmpl.ideservices.GsonUtils.ComplexTypeMode; import org.rascalmpl.library.util.PathConfig; -import org.rascalmpl.values.RascalValueFactory; import org.rascalmpl.vscode.lsp.log.LogRedirectConfiguration; import org.rascalmpl.vscode.lsp.parametric.LanguageRegistry.LanguageParameter; import org.rascalmpl.vscode.lsp.terminal.RemoteIDEServicesThread; @@ -106,19 +104,19 @@ protected BaseLanguageServer() {} private static final Logger logger = LogManager.getLogger(BaseLanguageServer.class); - protected static Launcher constructLSPClient(Socket client, ActualLanguageServer server, ExecutorService threadPool, ComplexTypeMode complexTypeMode) + protected static Launcher constructLSPClient(Socket client, ActualLanguageServer server, ExecutorService threadPool) throws IOException { client.setTcpNoDelay(true); - return constructLSPClient(client.getInputStream(), client.getOutputStream(), server, threadPool, complexTypeMode); + return constructLSPClient(client.getInputStream(), client.getOutputStream(), server, threadPool); } - protected static Launcher constructLSPClient(InputStream in, OutputStream out, ActualLanguageServer server, ExecutorService threadPool, ComplexTypeMode complexTypeMode) { + protected static Launcher constructLSPClient(InputStream in, OutputStream out, ActualLanguageServer server, ExecutorService threadPool) { Launcher clientLauncher = new Launcher.Builder() .setLocalService(server) .setRemoteInterface(IBaseLanguageClient.class) .setInput(in) .setOutput(out) - .configureGson(builder -> GsonUtils.configureGson(builder, complexTypeMode, RascalValueFactory.getStore())) + .configureGson(GsonUtils.complexAsJsonObject()) .setExecutorService(threadPool) .create(); @@ -132,14 +130,14 @@ protected static void printClassPath() { } @SuppressWarnings({"java:S2189", "java:S106"}) - public static void startLanguageServer(ExecutorService requestPool, ExecutorService workerPool, Function docServiceProvider, Function workspaceServiceProvider, int portNumber, ComplexTypeMode complexTypeMode) { + public static void startLanguageServer(ExecutorService requestPool, ExecutorService workerPool, Function docServiceProvider, Function workspaceServiceProvider, int portNumber) { logger.info("Starting Rascal Language Server: {}", getVersion()); printClassPath(); if (DEPLOY_MODE) { var docService = docServiceProvider.apply(workerPool); var wsService = workspaceServiceProvider.apply(workerPool); - startLSP(constructLSPClient(capturedIn, capturedOut, new ActualLanguageServer(() -> System.exit(0), workerPool, docService, wsService), requestPool, complexTypeMode)); + startLSP(constructLSPClient(capturedIn, capturedOut, new ActualLanguageServer(() -> System.exit(0), workerPool, docService, wsService), requestPool)); } else { try (ServerSocket serverSocket = new ServerSocket(portNumber, 0, InetAddress.getByName("127.0.0.1"))) { @@ -147,7 +145,7 @@ public static void startLanguageServer(ExecutorService requestPool, ExecutorServ while (true) { var docService = docServiceProvider.apply(workerPool); var wsService = workspaceServiceProvider.apply(workerPool); - startLSP(constructLSPClient(serverSocket.accept(), new ActualLanguageServer(() -> {}, workerPool, docService, wsService), requestPool, complexTypeMode)); + startLSP(constructLSPClient(serverSocket.accept(), new ActualLanguageServer(() -> {}, workerPool, docService, wsService), requestPool)); } } catch (IOException e) { logger.fatal("Failure to start TCP server on port {}", portNumber, e); diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/IBaseTextDocumentService.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/IBaseTextDocumentService.java index d885e6aa9..e733873d9 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/IBaseTextDocumentService.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/IBaseTextDocumentService.java @@ -35,7 +35,6 @@ import org.eclipse.lsp4j.RenameFilesParams; import org.eclipse.lsp4j.ServerCapabilities; import org.eclipse.lsp4j.WorkspaceFolder; -import org.eclipse.lsp4j.jsonrpc.services.JsonRequest; import org.eclipse.lsp4j.services.LanguageClient; import org.eclipse.lsp4j.services.TextDocumentService; import org.rascalmpl.vscode.lsp.parametric.LanguageRegistry.LanguageParameter; @@ -58,7 +57,6 @@ public interface IBaseTextDocumentService extends TextDocumentService, ITextDocu void projectAdded(String name, ISourceLocation projectRoot); void projectRemoved(String name, ISourceLocation projectRoot); - @JsonRequest("executeRascalCommand") CompletableFuture executeCommand(String languageName, String command); void didCreateFiles(CreateFilesParams params); diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricLanguageServer.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricLanguageServer.java index 3d42643fd..94f93ffae 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricLanguageServer.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricLanguageServer.java @@ -29,7 +29,6 @@ import com.google.gson.GsonBuilder; import org.checkerframework.checker.nullness.qual.Nullable; -import org.rascalmpl.ideservices.GsonUtils.ComplexTypeMode; import org.rascalmpl.vscode.lsp.BaseLanguageServer; import org.rascalmpl.vscode.lsp.parametric.LanguageRegistry.LanguageParameter; import org.rascalmpl.vscode.lsp.util.NamedThreadPool; @@ -42,7 +41,6 @@ protected static void startParametric(ServerArgs args) { , threadPool -> new ParametricTextDocumentService(threadPool, args.getDedicatedLanguage(), args.isExitWhenEmpty()) , ParametricWorkspaceService::new , args.getPort() - , args.getComplexTypeMode() ); } @@ -54,7 +52,6 @@ public static class ServerArgs { private int port = 9999; private @Nullable LanguageParameter dedicatedLanguage = null; private boolean exitWhenEmpty = false; - private ComplexTypeMode complexTypeMode = ComplexTypeMode.ENCODE_AS_JSON_OBJECT; public int getPort() { return port; @@ -80,17 +77,9 @@ public void setExitWhenEmpty(boolean exitWhenEmpty) { this.exitWhenEmpty = exitWhenEmpty; } - public ComplexTypeMode getComplexTypeMode() { - return complexTypeMode; - } - - public void setComplexTypeMode(ComplexTypeMode complexTypeMode) { - this.complexTypeMode = complexTypeMode; - } - } - @SuppressWarnings("java:S127") // skipping next argument in loop + @SuppressWarnings("java:S127") // skipping next argument from loop protected static ServerArgs parseArgs(String[] args) { var serverArgs = new ServerArgs(); for (int i = 0; i < args.length; i++) { @@ -101,23 +90,7 @@ protected static ServerArgs parseArgs(String[] args) { case "--exitWhenEmpty": serverArgs.setExitWhenEmpty(true); break; - case "--complexTypeMode": { - switch (args[++i].toLowerCase()) { - case "json": - serverArgs.setComplexTypeMode(ComplexTypeMode.ENCODE_AS_JSON_OBJECT); - break; - case "base64": - serverArgs.setComplexTypeMode(ComplexTypeMode.ENCODE_AS_BASE64_STRING); - break; - case "string": - serverArgs.setComplexTypeMode(ComplexTypeMode.ENCODE_AS_STRING); - break; - default: // do nothing - we already have a default value - } - break; - } default: - // 'Positional' argument (for backwards compatibility) if (serverArgs.getDedicatedLanguage() == null) { serverArgs.setDedicatedLanguage(new GsonBuilder().create().fromJson(args[i], LanguageParameter.class)); } diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/LanguageServerRouter.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/LanguageServerRouter.java index f7f8626ac..b50fe5e54 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/LanguageServerRouter.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/LanguageServerRouter.java @@ -26,6 +26,10 @@ */ package org.rascalmpl.vscode.lsp.parametric.routing; +import com.google.gson.GsonBuilder; +import com.google.gson.TypeAdapter; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonWriter; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; @@ -76,14 +80,12 @@ import org.eclipse.lsp4j.jsonrpc.messages.ResponseError; import org.eclipse.lsp4j.jsonrpc.messages.ResponseErrorCode; import org.eclipse.lsp4j.services.LanguageServer; -import org.rascalmpl.ideservices.GsonUtils; import org.rascalmpl.library.util.PathConfig; import org.rascalmpl.uri.URIUtil; import org.rascalmpl.util.maven.Artifact; import org.rascalmpl.util.maven.MavenParser; import org.rascalmpl.util.maven.ModelResolutionError; import org.rascalmpl.util.maven.Scope; -import org.rascalmpl.values.RascalValueFactory; import org.rascalmpl.vscode.lsp.BaseLanguageServer; import org.rascalmpl.vscode.lsp.IBaseLanguageClient; import org.rascalmpl.vscode.lsp.IBaseLanguageServerExtensions; @@ -229,7 +231,6 @@ private static String classPath(LanguageParameter lang) { , "-cp", classPath , "org.rascalmpl.vscode.lsp.parametric.ParametricLanguageServer" , "--exitWhenEmpty" - , "--complexTypeMode", "base64" // , new GsonBuilder().create().toJson(lang, LanguageParameter.class).replace("\"", "\\\"") // escape JSON string on command line ) .redirectError(Redirect.INHERIT) // Show logs in current process @@ -264,6 +265,34 @@ private static String classPath(LanguageParameter lang) { } } + /** + * Special GSON configuration that (un)wraps IValues as-is. + * + * Encoding and decoding an IValues loses type information, we cannot properly encode a decoded value again. + * `encode(decode(encode(v))) != v` + * Since the router should just proxy values passed from remote servers, without changing them, it uses a special encoder/decoder. + * + */ + private static void configureProxyGson(GsonBuilder builder) { + // Special encoding/decoding of IValues to/from a string + builder.registerTypeAdapter(ProxiedIValue.class, new TypeAdapter() { + + @Override + public ProxiedIValue read(JsonReader reader) throws IOException { + return ProxiedIValue.fromJson(reader); + } + + @Override + public void write(JsonWriter writer, ProxiedIValue proxiedValue) throws IOException { + ProxiedIValue.toJson(writer, proxiedValue); + } + + }); + + // TODO Support creating 'regular' IValues in the routing server + // For non-proxy values, register JSON encoding (but not decoding) + } + private @Nullable CompletableFuture startServer(LanguageParameter lang) { var serverParams = BaseLanguageServer.DEPLOY_MODE ? startServerProcess(lang) @@ -279,7 +308,7 @@ private static String classPath(LanguageParameter lang) { .setLocalService(this) .setInput(serverParams.getLeft()) .setOutput(serverParams.getMiddle()) - .configureGson(GsonUtils.complexAsBase64String(RascalValueFactory.getStore())) + .configureGson(LanguageServerRouter::configureProxyGson) .setExecutorService(getExecutor()) .create(); diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/ProxiedIValue.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/ProxiedIValue.java new file mode 100644 index 000000000..3c41a84e8 --- /dev/null +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/ProxiedIValue.java @@ -0,0 +1,185 @@ +/* + * Copyright (c) 2018-2025, NWO-I CWI and Swat.engineering + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +package org.rascalmpl.vscode.lsp.parametric.routing; + +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonWriter; +import java.io.IOException; + +import io.usethesource.vallang.IExternalValue; +import io.usethesource.vallang.IValue; +import io.usethesource.vallang.type.Type; + +/** + * Wraps a JSON string representing an IValue as an IValue. + * + * This class allows passing IValues through JSON-RPC-enabled servers without requiring to decode/encode them. + */ +public class ProxiedIValue implements IExternalValue { + + private String contents; + + /*package*/ ProxiedIValue(String json) { + this.contents = json; + } + + /*package*/ String getContents() { + return this.contents; + } + + @Override + public int getMatchFingerprint() { + throw new UnsupportedOperationException("ProxiedIValue::getMatchFingerprint"); + } + + @Override + public Type getType() { + throw new UnsupportedOperationException("ProxiedIValue::getType"); + } + + /** + * Unwrap the {@link IValue}'s JSON representation. + * @param writer the writer to write the unwrapped JSON to + * @param value the value to proxy + * @throws IOException if an unexpected input occurs + */ + /*package*/ static void toJson(JsonWriter writer, ProxiedIValue value) throws IOException { + writer.jsonValue(value.getContents()); + } + + /** + * Wrap the {@link IValue}'s JSON representation. + * @param reader the reader to read the JSON from + * @return the JSON as an {@link IValue} + * @throws IOException if an unexpected input occurs + */ + /*package*/ static ProxiedIValue fromJson(JsonReader reader) throws IOException { + var sb = new StringBuilder(); + readValue(reader, sb); + return new ProxiedIValue(sb.toString()); + } + + /** + * Recursively build a JSON string given a tokenized input. + * @param reader a tokenized JSON reader + * @param sb a builder to which to append the result string + * @throws IOException if an unexpected input occurs + */ + private static void readValue(JsonReader reader, StringBuilder sb) throws IOException { + switch (reader.peek()) { + case BEGIN_ARRAY: + readArray(reader, sb); + break; + case BEGIN_OBJECT: + readObject(reader, sb); + break; + case BOOLEAN: + readBoolean(reader, sb); + break; + case NULL: + readNull(reader, sb); + break; + case NUMBER: + readNumber(reader, sb); + break; + case STRING: + readString(reader, sb); + break; + case NAME: // fall-through + case END_ARRAY: // fall-through + case END_DOCUMENT: // fall-through + case END_OBJECT: // fall-through + throw new IOException("Malformed JSON"); + default: + throw new IOException("Unknown JSON token"); + } + } + + private static void readString(JsonReader reader, StringBuilder sb) throws IOException { + sb.append('"'); + sb.append(reader.nextString()); + sb.append('"'); + } + + private static void readNumber(JsonReader reader, StringBuilder sb) throws IOException { + try { + sb.append(reader.nextInt()); + } catch (NumberFormatException e) {} + try { + sb.append(reader.nextLong()); + } catch (NumberFormatException e) {} + try { + sb.append(reader.nextDouble()); + } catch (NumberFormatException e) {} + } + + private static void readNull(JsonReader reader, StringBuilder sb) throws IOException { + reader.nextNull(); + sb.append("null"); + } + + private static void readBoolean(JsonReader reader, StringBuilder sb) throws IOException { + sb.append(reader.nextBoolean()); + } + + private static void readArray(JsonReader reader, StringBuilder sb) throws IOException { + reader.beginArray(); + sb.append('['); + boolean hasNext = reader.hasNext(); + while (hasNext) { + readValue(reader, sb); + hasNext = reader.hasNext(); + if (hasNext) { + sb.append(','); + } + } + reader.endArray(); + sb.append(']'); + } + + private static void readObject(JsonReader reader, StringBuilder sb) throws IOException { + reader.beginObject(); + sb.append('{'); + + boolean hasNext = reader.hasNext(); + while (hasNext) { + sb.append('"'); + sb.append(reader.nextName()); + sb.append('"'); + sb.append(':'); + readValue(reader, sb); + + hasNext = reader.hasNext(); + if (hasNext) { + sb.append(','); + } + } + reader.endObject(); + sb.append('}'); + } + +} diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingLanguageServer.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingLanguageServer.java index a4d3052f5..14fd32421 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingLanguageServer.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingLanguageServer.java @@ -32,7 +32,6 @@ import java.util.concurrent.ExecutorService; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import org.rascalmpl.ideservices.GsonUtils.ComplexTypeMode; import org.rascalmpl.vscode.lsp.parametric.ParametricLanguageServer; import org.rascalmpl.vscode.lsp.util.NamedThreadPool; @@ -45,18 +44,18 @@ public class RoutingLanguageServer extends ParametricLanguageServer { private static final Logger logger = LogManager.getLogger(RoutingLanguageServer.class); @SuppressWarnings("java:S2189") // endless loop is fine for the development server - public static void startLanguageServer(ExecutorService requestPool, ExecutorService workerPool, int portNumber, ComplexTypeMode complexTypeMode) { + public static void startLanguageServer(ExecutorService requestPool, ExecutorService workerPool, int portNumber) { logger.info("Starting Rascal Language Server Router: {}", getVersion()); printClassPath(); if (DEPLOY_MODE) { - startLSP(constructLSPClient(capturedIn, capturedOut, new LanguageServerRouter(() -> System.exit(0), workerPool), requestPool, complexTypeMode)); + startLSP(constructLSPClient(capturedIn, capturedOut, new LanguageServerRouter(() -> System.exit(0), workerPool), requestPool)); } else { try (ServerSocket serverSocket = new ServerSocket(portNumber, 0, InetAddress.getByName("127.0.0.1"))) { logger.info("Rascal LSP server router listens on port number: {}", portNumber); while (true) { - startLSP(constructLSPClient(serverSocket.accept(), new LanguageServerRouter(() -> {}, workerPool), requestPool, complexTypeMode)); + startLSP(constructLSPClient(serverSocket.accept(), new LanguageServerRouter(() -> {}, workerPool), requestPool)); } } catch (IOException e) { logger.fatal("Failure to start TCP server on port {}", portNumber, e); @@ -73,7 +72,6 @@ public static void main(String[] args) { startLanguageServer(NamedThreadPool.single("parametric-lsp-router") , NamedThreadPool.cached("parametric-router") , serverArgs.getPort() - , serverArgs.getComplexTypeMode() ); } } diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/RascalLanguageServer.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/RascalLanguageServer.java index ffa620961..e36353df8 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/RascalLanguageServer.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/RascalLanguageServer.java @@ -29,14 +29,13 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import org.rascalmpl.ideservices.GsonUtils.ComplexTypeMode; import org.rascalmpl.vscode.lsp.BaseLanguageServer; import org.rascalmpl.vscode.lsp.util.NamedThreadPool; public class RascalLanguageServer extends BaseLanguageServer { public static void main(String[] args) { try { - startLanguageServer(NamedThreadPool.single("rascal-lsp"), NamedThreadPool.cached("rascal"), RascalTextDocumentService::new, RascalWorkspaceService::new, 8888, ComplexTypeMode.ENCODE_AS_JSON_OBJECT); + startLanguageServer(NamedThreadPool.single("rascal-lsp"), NamedThreadPool.cached("rascal"), RascalTextDocumentService::new, RascalWorkspaceService::new, 8888); } catch (Throwable e) { final Logger logger = LogManager.getLogger(RascalLanguageServer.class); From f026e3cbe0781a5c2e37233de4dbbb650e36ff48 Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Mon, 11 May 2026 15:00:17 +0200 Subject: [PATCH 060/107] Skip some tests that we implement later. --- .../src/test/vscode-suite/dsl.test.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/rascal-vscode-extension/src/test/vscode-suite/dsl.test.ts b/rascal-vscode-extension/src/test/vscode-suite/dsl.test.ts index c455803ea..242d9b7c3 100644 --- a/rascal-vscode-extension/src/test/vscode-suite/dsl.test.ts +++ b/rascal-vscode-extension/src/test/vscode-suite/dsl.test.ts @@ -256,7 +256,8 @@ end expect(editorText).to.contain("z := 2"); }); - it("renaming files works", async function() { + // TODO Implement this test in a later PR + it.skip("renaming files works", async function() { if (errorRecovery) { this.skip(); } const newDir = path.join(TestWorkspace.testProject, "src", "main", "pico", "rename-test"); const fromFile = path.join(newDir, "testing.pico"); @@ -306,7 +307,8 @@ end }, Delays.normal, "Call hierarchy should show `multiply` and its two outgoing calls."); }); - it("completion works", async function() { + // TODO Implement this test in a later PR + it.skip("completion works", async function() { const editor = await ide.openModule(TestWorkspace.picoFile); await editor.setTextAtLine(6, " aa : natural;"); @@ -315,7 +317,8 @@ end await expectCompletions(driver, editor, ["a", "aa"]); }); - it("completion by trigger character works", async function() { + // TODO Implement this test in a later PR + it.skip("completion by trigger character works", async function() { // We will be typing and introducing parse errors, so this only works with error recovery if (!errorRecovery) { this.skip(); } From 37a0ebec5ad21758ad23d64443da769e81ee30b4 Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Mon, 11 May 2026 16:12:14 +0200 Subject: [PATCH 061/107] Rename `LanguageServerRouter` => `ActualRoutingLanguageServer` --- ...ServerRouter.java => ActualRoutingLanguageServer.java} | 8 ++++---- .../lsp/parametric/routing/RoutingLanguageServer.java | 4 ++-- .../parametric/routing/RoutingTextDocumentService.java | 8 ++++---- .../lsp/parametric/routing/RoutingWorkspaceService.java | 6 +++--- 4 files changed, 13 insertions(+), 13 deletions(-) rename rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/{LanguageServerRouter.java => ActualRoutingLanguageServer.java} (98%) diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/LanguageServerRouter.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/ActualRoutingLanguageServer.java similarity index 98% rename from rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/LanguageServerRouter.java rename to rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/ActualRoutingLanguageServer.java index b50fe5e54..fff369c30 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/LanguageServerRouter.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/ActualRoutingLanguageServer.java @@ -101,9 +101,9 @@ /** * A language server implementation that routes LSP requests to dedicated remote language servers. */ -public class LanguageServerRouter extends BaseLanguageServer.ActualLanguageServer implements IBaseLanguageClient, DocumentRouter> { +public class ActualRoutingLanguageServer extends BaseLanguageServer.ActualLanguageServer implements IBaseLanguageClient, DocumentRouter> { - private static final Logger logger = LogManager.getLogger(LanguageServerRouter.class); + private static final Logger logger = LogManager.getLogger(ActualRoutingLanguageServer.class); private final Map languagesByExtension = new ConcurrentHashMap<>(); // TODO To be able to route to arbitrary third-party language servers, remote servers should implement `LanguageServer` (instead of `IBaseLanguageServerExtensions`) @@ -117,7 +117,7 @@ public class LanguageServerRouter extends BaseLanguageServer.ActualLanguageServe private static final int REMOTE_BASE_PORT = 9990; private AtomicInteger remotePortOffset = new AtomicInteger(0); - public LanguageServerRouter(Runnable onExit, ExecutorService exec) { + public ActualRoutingLanguageServer(Runnable onExit, ExecutorService exec) { super(onExit, exec, new RoutingTextDocumentService(exec), new RoutingWorkspaceService(exec)); } @@ -308,7 +308,7 @@ public void write(JsonWriter writer, ProxiedIValue proxiedValue) throws IOExcept .setLocalService(this) .setInput(serverParams.getLeft()) .setOutput(serverParams.getMiddle()) - .configureGson(LanguageServerRouter::configureProxyGson) + .configureGson(ActualRoutingLanguageServer::configureProxyGson) .setExecutorService(getExecutor()) .create(); diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingLanguageServer.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingLanguageServer.java index 14fd32421..2ec43889d 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingLanguageServer.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingLanguageServer.java @@ -49,13 +49,13 @@ public static void startLanguageServer(ExecutorService requestPool, ExecutorServ printClassPath(); if (DEPLOY_MODE) { - startLSP(constructLSPClient(capturedIn, capturedOut, new LanguageServerRouter(() -> System.exit(0), workerPool), requestPool)); + startLSP(constructLSPClient(capturedIn, capturedOut, new ActualRoutingLanguageServer(() -> System.exit(0), workerPool), requestPool)); } else { try (ServerSocket serverSocket = new ServerSocket(portNumber, 0, InetAddress.getByName("127.0.0.1"))) { logger.info("Rascal LSP server router listens on port number: {}", portNumber); while (true) { - startLSP(constructLSPClient(serverSocket.accept(), new LanguageServerRouter(() -> {}, workerPool), requestPool)); + startLSP(constructLSPClient(serverSocket.accept(), new ActualRoutingLanguageServer(() -> {}, workerPool), requestPool)); } } catch (IOException e) { logger.fatal("Failure to start TCP server on port {}", portNumber, e); diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingTextDocumentService.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingTextDocumentService.java index e93665a8e..be5c77095 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingTextDocumentService.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingTextDocumentService.java @@ -118,7 +118,7 @@ public class RoutingTextDocumentService implements IBaseTextDocumentService, Fut private @MonotonicNonNull LanguageClient client; private @MonotonicNonNull BaseWorkspaceService wsService; - private @MonotonicNonNull LanguageServerRouter parentServer; + private @MonotonicNonNull ActualRoutingLanguageServer parentServer; @SuppressWarnings("unused") private final ExecutorService exec; @@ -127,7 +127,7 @@ public RoutingTextDocumentService(ExecutorService exec) { this.exec = exec; } - public void setParentServer(LanguageServerRouter server) { + public void setParentServer(ActualRoutingLanguageServer server) { this.parentServer = server; } @@ -153,7 +153,7 @@ private LanguageClient availableClient() { return client; } - private LanguageServerRouter availableServer() { + private ActualRoutingLanguageServer availableServer() { if (parentServer == null) { throw new IllegalStateException("Server not connected yet."); } @@ -288,7 +288,7 @@ public boolean isManagingFile(ISourceLocation file) { public void didCreateFiles(CreateFilesParams params) { // TODO Mimick VS given certain file operation filters (capabilities) var filesByExt = params.getFiles().stream() - .collect(Collectors.toMap(f -> LanguageServerRouter.extension(Locations.toLoc(f.getUri())), List::of, Lists::union)); + .collect(Collectors.toMap(f -> ActualRoutingLanguageServer.extension(Locations.toLoc(f.getUri())), List::of, Lists::union)); for (var e : filesByExt.entrySet()) { availableServer().route(e.getKey()).thenAccept(s -> diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingWorkspaceService.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingWorkspaceService.java index 9d62cb379..7d27342c0 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingWorkspaceService.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingWorkspaceService.java @@ -39,17 +39,17 @@ */ public class RoutingWorkspaceService extends BaseWorkspaceService { - private @MonotonicNonNull LanguageServerRouter parentServer; + private @MonotonicNonNull ActualRoutingLanguageServer parentServer; public RoutingWorkspaceService(ExecutorService exec) { super(exec); } - public void setParentServer(LanguageServerRouter server) { + public void setParentServer(ActualRoutingLanguageServer server) { this.parentServer = server; } - private LanguageServerRouter availableServer() { + private ActualRoutingLanguageServer availableServer() { if (parentServer == null) { throw new IllegalStateException("Server not connected yet."); } From 97905d9115cf73cc15e117cc16f677c24178df30 Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Mon, 11 May 2026 16:22:00 +0200 Subject: [PATCH 062/107] Various small fixes and reverts. --- .../rascalmpl/vscode/lsp/BaseLanguageServer.java | 14 +++----------- .../routing/ActualRoutingLanguageServer.java | 15 +++++++++++++++ .../lsp/parametric/routing/ProxiedIValue.java | 2 +- .../rascalmpl/vscode/lsp/util/DocumentRouter.java | 4 ++-- 4 files changed, 21 insertions(+), 14 deletions(-) diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/BaseLanguageServer.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/BaseLanguageServer.java index 0ad631d5e..9536d4f33 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/BaseLanguageServer.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/BaseLanguageServer.java @@ -198,7 +198,6 @@ public static class ActualLanguageServer implements IBaseLanguageServerExtension private final ExecutorService executor; private @MonotonicNonNull IDEServicesConfiguration remoteIDEServicesConfiguration; - private @MonotonicNonNull IBaseLanguageClient client; protected ActualLanguageServer(Runnable onExit, ExecutorService executor, IBaseTextDocumentService lspDocumentService, BaseWorkspaceService lspWorkspaceService) { this.onExit = onExit; @@ -224,13 +223,6 @@ private static URI[] toURIArray(IList src) { .toArray(URI[]::new); } - protected IBaseLanguageClient availableClient() { - if (client == null) { - throw new IllegalStateException("Client not connected"); - } - return client; - } - @Override public CompletableFuture[]> supplyPathConfig(PathConfigParameter projectFolder) { return CompletableFuture.supplyAsync(() -> { @@ -319,9 +311,9 @@ public void setTrace(SetTraceParams params) { @Override public void connect(LanguageClient client) { - this.client = (IBaseLanguageClient) client; - lspDocumentService.connect(this.client); - lspWorkspaceService.connect(this.client); + var actualClient = (IBaseLanguageClient) client; + lspDocumentService.connect(actualClient); + lspWorkspaceService.connect(actualClient); remoteIDEServicesConfiguration = RemoteIDEServicesThread.startRemoteIDEServicesServer(client, lspDocumentService, executor); logger.debug("Remote IDE Services Port {}", remoteIDEServicesConfiguration.getPort()); } diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/ActualRoutingLanguageServer.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/ActualRoutingLanguageServer.java index fff369c30..7a740cdc2 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/ActualRoutingLanguageServer.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/ActualRoutingLanguageServer.java @@ -79,6 +79,7 @@ import org.eclipse.lsp4j.jsonrpc.ResponseErrorException; import org.eclipse.lsp4j.jsonrpc.messages.ResponseError; import org.eclipse.lsp4j.jsonrpc.messages.ResponseErrorCode; +import org.eclipse.lsp4j.services.LanguageClient; import org.eclipse.lsp4j.services.LanguageServer; import org.rascalmpl.library.util.PathConfig; import org.rascalmpl.uri.URIUtil; @@ -112,6 +113,7 @@ public class ActualRoutingLanguageServer extends BaseLanguageServer.ActualLangua // 2. Upon removal from this map, the process should be killed to avoid resource leaks. private final Map> languageServers = new ConcurrentHashMap<>(); + private @MonotonicNonNull IBaseLanguageClient client; private @MonotonicNonNull InitializeParams initializeParams; private static final int REMOTE_BASE_PORT = 9990; @@ -139,6 +141,12 @@ public CompletableFuture route(ISourceLocation fi return route(extension(file)); } + @Override + public void connect(LanguageClient client) { + this.client = (IBaseLanguageClient) client; + super.connect(client); + } + public CompletableFuture route(String extension) { return safeLanguage(extension).map(this::languageByName).orElseThrow(() -> { throw new UnsupportedOperationException(String.format("Rascal Parametric LSP has no support for this file, since no language is registered with extension '%s'", extension)); @@ -431,6 +439,13 @@ public synchronized CompletableFuture sendUnregisterLanguage(LanguageParam return super.sendUnregisterLanguage(lang); } + private IBaseLanguageClient availableClient() { + if (client == null) { + throw new IllegalStateException("Client not connected"); + } + return client; + } + @Override public void telemetryEvent(Object object) { availableClient().telemetryEvent(object); diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/ProxiedIValue.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/ProxiedIValue.java index 3c41a84e8..37294d5b0 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/ProxiedIValue.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/ProxiedIValue.java @@ -39,7 +39,7 @@ * * This class allows passing IValues through JSON-RPC-enabled servers without requiring to decode/encode them. */ -public class ProxiedIValue implements IExternalValue { +class ProxiedIValue implements IExternalValue { private String contents; diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/util/DocumentRouter.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/util/DocumentRouter.java index 47b477df1..15f5eb1a5 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/util/DocumentRouter.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/util/DocumentRouter.java @@ -35,13 +35,13 @@ import io.usethesource.vallang.ISourceLocation; /** - * A router of document-like inputs to outputs of {{@link T}}. + * A router of document-like inputs to outputs of {@link T}. * @param The type of the mapped value. */ public interface DocumentRouter { /** - * Map an {{@link ISourceLocation}} to a {{@link T}}. + * Map an {@link ISourceLocation} to a {@link T}. * @param loc The input location. * @return The mapped value. */ From 6f475fe794fddec844f27c71453cc7fc95b447c9 Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Mon, 11 May 2026 16:57:14 +0200 Subject: [PATCH 063/107] Used proxied language client in routing server. --- .../rascalmpl/vscode/lsp/BaseLanguageServer.java | 16 ++++++++++++---- .../routing/ActualRoutingLanguageServer.java | 7 ------- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/BaseLanguageServer.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/BaseLanguageServer.java index 7a555db0c..b845e46d2 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/BaseLanguageServer.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/BaseLanguageServer.java @@ -224,6 +224,7 @@ public static class ActualLanguageServer implements IBaseLanguageServerExtension private final ExecutorService executor; private @MonotonicNonNull IDEServicesConfiguration remoteIDEServicesConfiguration; + private @MonotonicNonNull IBaseLanguageClient client; protected ActualLanguageServer(Runnable onExit, ExecutorService executor, IBaseTextDocumentService lspDocumentService, BaseWorkspaceService lspWorkspaceService) { this.onExit = onExit; @@ -337,13 +338,20 @@ public void setTrace(SetTraceParams params) { @Override public void connect(LanguageClient client) { - var proxy = addShutdownDetectionTo(client); - lspDocumentService.connect(proxy); - lspWorkspaceService.connect(proxy); - remoteIDEServicesConfiguration = RemoteIDEServicesThread.startRemoteIDEServicesServer(proxy, lspDocumentService, executor); + this.client = addShutdownDetectionTo(client); + lspDocumentService.connect(this.client); + lspWorkspaceService.connect(this.client); + remoteIDEServicesConfiguration = RemoteIDEServicesThread.startRemoteIDEServicesServer(this.client, lspDocumentService, executor); logger.debug("Remote IDE Services Port {}", remoteIDEServicesConfiguration); } + protected IBaseLanguageClient availableClient() { + if (client == null) { + throw new IllegalStateException("Language Client has not been connected yet"); + } + return client; + } + /** * Creates a proxy instance that forwards method calls to the provided * language client only when (the thread pool of) this language server diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/ActualRoutingLanguageServer.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/ActualRoutingLanguageServer.java index 7a740cdc2..f692e390b 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/ActualRoutingLanguageServer.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/ActualRoutingLanguageServer.java @@ -439,13 +439,6 @@ public synchronized CompletableFuture sendUnregisterLanguage(LanguageParam return super.sendUnregisterLanguage(lang); } - private IBaseLanguageClient availableClient() { - if (client == null) { - throw new IllegalStateException("Client not connected"); - } - return client; - } - @Override public void telemetryEvent(Object object) { availableClient().telemetryEvent(object); From 842de262460418672991077fdf267cc749d37447 Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Mon, 11 May 2026 17:42:01 +0200 Subject: [PATCH 064/107] Improved JSON number parser fallbacks. --- .../vscode/lsp/parametric/routing/ProxiedIValue.java | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/ProxiedIValue.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/ProxiedIValue.java index 37294d5b0..f65377e04 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/ProxiedIValue.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/ProxiedIValue.java @@ -128,13 +128,17 @@ private static void readString(JsonReader reader, StringBuilder sb) throws IOExc private static void readNumber(JsonReader reader, StringBuilder sb) throws IOException { try { sb.append(reader.nextInt()); - } catch (NumberFormatException e) {} + return; + } catch (NumberFormatException e) {/* try the next number type */} try { sb.append(reader.nextLong()); - } catch (NumberFormatException e) {} + return; + } catch (NumberFormatException e) {/* try the next number type */} try { sb.append(reader.nextDouble()); - } catch (NumberFormatException e) {} + } catch (NumberFormatException e) { + throw new IOException("Could not parse number in JSON object: " + reader.getPath(), e); + } } private static void readNull(JsonReader reader, StringBuilder sb) throws IOException { From ea42bd31da245aa92a3950fd58b886f2a5c9a255 Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Tue, 12 May 2026 13:06:24 +0200 Subject: [PATCH 065/107] Remove/change some TODOs. --- .../routing/ActualRoutingLanguageServer.java | 12 +++++++----- .../routing/RoutingTextDocumentService.java | 8 ++++---- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/ActualRoutingLanguageServer.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/ActualRoutingLanguageServer.java index f692e390b..801639fe6 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/ActualRoutingLanguageServer.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/ActualRoutingLanguageServer.java @@ -107,10 +107,10 @@ public class ActualRoutingLanguageServer extends BaseLanguageServer.ActualLangua private static final Logger logger = LogManager.getLogger(ActualRoutingLanguageServer.class); private final Map languagesByExtension = new ConcurrentHashMap<>(); - // TODO To be able to route to arbitrary third-party language servers, remote servers should implement `LanguageServer` (instead of `IBaseLanguageServerExtensions`) - // Note: + // NOTE // 1. This map should only contains running server processes. // 2. Upon removal from this map, the process should be killed to avoid resource leaks. + // NOTE To be able to route to arbitrary third-party language servers, remote servers should implement `LanguageServer` (instead of `IBaseLanguageServerExtensions`) private final Map> languageServers = new ConcurrentHashMap<>(); private @MonotonicNonNull IBaseLanguageClient client; @@ -297,8 +297,8 @@ public void write(JsonWriter writer, ProxiedIValue proxiedValue) throws IOExcept }); - // TODO Support creating 'regular' IValues in the routing server - // For non-proxy values, register JSON encoding (but not decoding) + // If support for creating (instead of forwaring) IValues in the routing server is required, + // register JSON encoding (but not decoding) for regular (non-rpoxy) IValues here } private @Nullable CompletableFuture startServer(LanguageParameter lang) { @@ -373,10 +373,12 @@ private InitializeParams delegateInitializationParams() { params.setClientInfo(clientParams.getClientInfo()); params.setInitializationOptions(clientParams.getInitializationOptions()); params.setLocale(clientParams.getLocale()); + params.setTrace(clientParams.getTrace()); + // TODO Set open workspace folders at the time of starting the server try { params.setProcessId((int) ProcessHandle.current().pid()); } catch (UnsupportedOperationException | SecurityException e) { - logger.debug("Cannot set process ID", e); + logger.debug("Cannot set delegate server parent process ID", e); } return params; } diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingTextDocumentService.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingTextDocumentService.java index be5c77095..d98046241 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingTextDocumentService.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingTextDocumentService.java @@ -262,25 +262,25 @@ public CompletableFuture executeCommand(String languageName, String comm @Override public LineColumnOffsetMap getColumnMap(ISourceLocation file) { - // TODO Auto-generated method stub + // TODO Implement in a follow-up PR throw new UnsupportedOperationException("Unimplemented method 'getColumnMap'"); } @Override public ColumnMaps getColumnMaps() { - // TODO Auto-generated method stub + // TODO Implement in a follow-up PR throw new UnsupportedOperationException("Unimplemented method 'getColumnMaps'"); } @Override public TextDocumentState getEditorState(ISourceLocation file) { - // TODO Auto-generated method stub + // TODO Implement in a follow-up PR throw new UnsupportedOperationException("Unimplemented method 'getDocumentState'"); } @Override public boolean isManagingFile(ISourceLocation file) { - // TODO Auto-generated method stub + // TODO Implement in a follow-up PR throw new UnsupportedOperationException("Unimplemented method 'isManagingFile'"); } From 2b0197945a2feb5ebf5198d6caa490a9bd563cb5 Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Tue, 12 May 2026 13:12:07 +0200 Subject: [PATCH 066/107] Inline future caller utils. --- .../routing/ActualRoutingLanguageServer.java | 10 +-- .../routing/RoutingTextDocumentService.java | 72 ++++++++++--------- .../vscode/lsp/util/DocumentRouter.java | 4 +- .../vscode/lsp/util/FutureCaller.java | 64 ----------------- 4 files changed, 45 insertions(+), 105 deletions(-) delete mode 100644 rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/util/FutureCaller.java diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/ActualRoutingLanguageServer.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/ActualRoutingLanguageServer.java index 801639fe6..4361480bc 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/ActualRoutingLanguageServer.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/ActualRoutingLanguageServer.java @@ -38,6 +38,7 @@ import java.net.Socket; import java.net.URI; import java.nio.file.Path; +import java.util.Collection; import java.util.HashSet; import java.util.List; import java.util.Map; @@ -51,7 +52,6 @@ import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Function; -import java.util.stream.Stream; import org.apache.commons.lang3.tuple.Triple; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -132,8 +132,8 @@ public CompletableFuture languageByName(String la } @Override - public Stream> allRoutes() { - return languageServers.values().stream(); + public Collection> allRoutes() { + return languageServers.values(); } @Override @@ -572,14 +572,14 @@ public CompletableFuture> workspaceFolders() { @Override public CompletableFuture shutdown() { - return CompletableFutureUtils.reduce(allRoutes().map(serverFut -> serverFut.thenCompose(IBaseLanguageServerExtensions::shutdown)), getExecutor()) + return CompletableFutureUtils.reduce(allRoutes().stream().map(serverFut -> serverFut.thenCompose(IBaseLanguageServerExtensions::shutdown)), getExecutor()) .thenCompose(ignored -> super.shutdown()); } @Override public void exit() { try { - CompletableFutureUtils.reduce(allRoutes().map(serverFut -> serverFut.thenAccept(IBaseLanguageServerExtensions::exit)), getExecutor()).get(10, TimeUnit.SECONDS); + CompletableFutureUtils.reduce(allRoutes().stream().map(serverFut -> serverFut.thenAccept(IBaseLanguageServerExtensions::exit)), getExecutor()).get(10, TimeUnit.SECONDS); } catch (ExecutionException | TimeoutException e) { logger.error("Error while exiting child processes", e); } catch (InterruptedException e) { diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingTextDocumentService.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingTextDocumentService.java index d98046241..477be9d08 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingTextDocumentService.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingTextDocumentService.java @@ -26,6 +26,7 @@ */ package org.rascalmpl.vscode.lsp.parametric.routing; +import java.util.Collection; import java.util.List; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; @@ -33,7 +34,6 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.stream.Collectors; -import java.util.stream.Stream; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; @@ -92,7 +92,6 @@ import org.eclipse.lsp4j.jsonrpc.messages.Either; import org.eclipse.lsp4j.jsonrpc.messages.Either3; import org.eclipse.lsp4j.services.LanguageClient; -import org.eclipse.lsp4j.services.TextDocumentService; import org.rascalmpl.util.locations.ColumnMaps; import org.rascalmpl.util.locations.LineColumnOffsetMap; import org.rascalmpl.vscode.lsp.BaseWorkspaceService; @@ -102,7 +101,6 @@ import org.rascalmpl.vscode.lsp.parametric.LanguageRegistry.LanguageParameter; import org.rascalmpl.vscode.lsp.parametric.ParametricTextDocumentService; import org.rascalmpl.vscode.lsp.util.DocumentRouter; -import org.rascalmpl.vscode.lsp.util.FutureCaller; import org.rascalmpl.vscode.lsp.util.Lists; import org.rascalmpl.vscode.lsp.util.locations.Locations; @@ -112,7 +110,7 @@ /** * A language-parametric text document service that routes incoming requests to remote dedicated language servers. */ -public class RoutingTextDocumentService implements IBaseTextDocumentService, FutureCaller, DocumentRouter> { +public class RoutingTextDocumentService implements IBaseTextDocumentService, DocumentRouter> { private static final Logger logger = LogManager.getLogger(RoutingTextDocumentService.class); @@ -132,10 +130,12 @@ public void setParentServer(ActualRoutingLanguageServer server) { } @Override - public Stream> allRoutes() { + public Collection> allRoutes() { return availableServer() .allRoutes() - .map(server -> server.thenApply(IBaseLanguageServerExtensions::getIBaseTextDocumentService)); + .stream() + .map(server -> server.thenApply(IBaseLanguageServerExtensions::getIBaseTextDocumentService)) + .collect(Collectors.toList()); } @Override @@ -163,25 +163,25 @@ private ActualRoutingLanguageServer availableServer() { @Override public void didOpen(DidOpenTextDocumentParams params) { // Note: floating future - callAccept(route(params.getTextDocument()), TextDocumentService::didOpen, params); + route(params.getTextDocument()).thenAccept(s -> s.didOpen(params)); } @Override public void didChange(DidChangeTextDocumentParams params) { // Note: floating future - callAccept(route(params.getTextDocument()), TextDocumentService::didChange, params); + route(params.getTextDocument()).thenAccept(s -> s.didChange(params)); } @Override public void didClose(DidCloseTextDocumentParams params) { // Note: floating future - callAccept(route(params.getTextDocument()), TextDocumentService::didClose, params); + route(params.getTextDocument()).thenAccept(s -> s.didClose(params)); } @Override public void didSave(DidSaveTextDocumentParams params) { // Note: floating future - callAccept(route(params.getTextDocument()), TextDocumentService::didSave, params); + route(params.getTextDocument()).thenAccept(s -> s.didSave(params)); } @Override @@ -247,12 +247,16 @@ public void unregisterLanguage(LanguageParameter lang) { @Override public void projectAdded(String name, ISourceLocation projectRoot) { - callAccept(allRoutes(), IBaseTextDocumentService::projectAdded, name, projectRoot); + for (var r : allRoutes()) { + r.thenAccept(s -> s.projectAdded(name, projectRoot)); + } } @Override public void projectRemoved(String name, ISourceLocation projectRoot) { - callAccept(allRoutes(), IBaseTextDocumentService::projectRemoved, name, projectRoot); + for (var r : allRoutes()) { + r.thenAccept(s -> s.projectRemoved(name, projectRoot)); + } } @Override @@ -310,111 +314,111 @@ public void didDeleteFiles(DeleteFilesParams params) { @Override public void cancelProgress(String progressId) { // Note: floating futures - callAccept(allRoutes(), IBaseTextDocumentService::cancelProgress, progressId); + for (var r : allRoutes()) { + r.thenAccept(s -> s.cancelProgress(progressId)); + } } @Override public CompletableFuture> callHierarchyIncomingCalls( CallHierarchyIncomingCallsParams params) { - return callCompose(route(Locations.toLoc(params.getItem().getUri())), TextDocumentService::callHierarchyIncomingCalls, params); + return route(Locations.toLoc(params.getItem().getUri())).thenCompose(s -> s.callHierarchyIncomingCalls(params)); } @Override public CompletableFuture> callHierarchyOutgoingCalls( CallHierarchyOutgoingCallsParams params) { - return callCompose(route(Locations.toLoc(params.getItem().getUri())), TextDocumentService::callHierarchyOutgoingCalls, params); + return route(Locations.toLoc(params.getItem().getUri())).thenCompose(s -> s.callHierarchyOutgoingCalls(params)); } @Override public CompletableFuture, CompletionList>> completion(CompletionParams position) { - return callCompose(route(position.getTextDocument()), TextDocumentService::completion, position); + return route(position.getTextDocument()).thenCompose(s -> s.completion(position)); } @Override public CompletableFuture, List>> definition( DefinitionParams params) { - return callCompose(route(params.getTextDocument()), TextDocumentService::definition, params); + return route(params.getTextDocument()).thenCompose(s -> s.definition(params)); } @Override public CompletableFuture> prepareCallHierarchy(CallHierarchyPrepareParams params) { - return callCompose(route(params.getTextDocument()), TextDocumentService::prepareCallHierarchy, params); + return route(params.getTextDocument()).thenCompose(s -> s.prepareCallHierarchy(params)); } @Override public CompletableFuture semanticTokensFull(SemanticTokensParams params) { - return callCompose(route(params.getTextDocument()), TextDocumentService::semanticTokensFull, params); + return route(params.getTextDocument()).thenCompose(s -> s.semanticTokensFull(params)); } @Override public CompletableFuture> semanticTokensFullDelta( SemanticTokensDeltaParams params) { - return callCompose(route(params.getTextDocument()), TextDocumentService::semanticTokensFullDelta, params); + return route(params.getTextDocument()).thenCompose(s -> s.semanticTokensFullDelta(params)); } @Override public CompletableFuture semanticTokensRange(SemanticTokensRangeParams params) { - return callCompose(route(params.getTextDocument()), TextDocumentService::semanticTokensRange, params); + return route(params.getTextDocument()).thenCompose(s -> s.semanticTokensRange(params)); } @Override public CompletableFuture> codeLens(CodeLensParams params) { - return callCompose(route(params.getTextDocument()), TextDocumentService::codeLens, params); + return route(params.getTextDocument()).thenCompose(s -> s.codeLens(params)); } @Override public CompletableFuture> prepareRename( PrepareRenameParams params) { - return callCompose(route(params.getTextDocument()), TextDocumentService::prepareRename, params); + return route(params.getTextDocument()).thenCompose(s -> s.prepareRename(params)); } @Override public CompletableFuture rename(RenameParams params) { - return callCompose(route(params.getTextDocument()), TextDocumentService::rename, params); + return route(params.getTextDocument()).thenCompose(s -> s.rename(params)); } @Override public CompletableFuture> inlayHint(InlayHintParams params) { - return callCompose(route(params.getTextDocument()), TextDocumentService::inlayHint, params); + return route(params.getTextDocument()).thenCompose(s -> s.inlayHint(params)); } @Override public CompletableFuture>> codeAction(CodeActionParams params) { - return callCompose(route(params.getTextDocument()), TextDocumentService::codeAction, params); + return route(params.getTextDocument()).thenCompose(s -> s.codeAction(params)); } @Override public CompletableFuture>> documentSymbol( DocumentSymbolParams params) { - return callCompose(route(params.getTextDocument()), TextDocumentService::documentSymbol, params); + return route(params.getTextDocument()).thenCompose(s -> s.documentSymbol(params)); } @Override public CompletableFuture, List>> implementation( ImplementationParams params) { - return callCompose(route(params.getTextDocument()), TextDocumentService::implementation, params); + return route(params.getTextDocument()).thenCompose(s -> s.implementation(params)); } @Override public CompletableFuture> references(ReferenceParams params) { - return callCompose(route(params.getTextDocument()), TextDocumentService::references, params); + return route(params.getTextDocument()).thenCompose(s -> s.references(params)); } @Override public CompletableFuture> foldingRange(FoldingRangeRequestParams params) { - return callCompose(route(params.getTextDocument()), TextDocumentService::foldingRange, params); + return route(params.getTextDocument()).thenCompose(s -> s.foldingRange(params)); } @Override public CompletableFuture<@Nullable Hover> hover(HoverParams params) { - return this.callCompose(route(params.getTextDocument()), TextDocumentService::hover, params); + return route(params.getTextDocument()).<@Nullable Hover>thenCompose(s -> s.hover(params)); } @Override public CompletableFuture> selectionRange(SelectionRangeParams params) { - return callCompose(route(params.getTextDocument()), TextDocumentService::selectionRange, params); + return route(params.getTextDocument()).thenCompose(s -> s.selectionRange(params)); } - - } diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/util/DocumentRouter.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/util/DocumentRouter.java index 15f5eb1a5..546e8bb75 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/util/DocumentRouter.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/util/DocumentRouter.java @@ -26,7 +26,7 @@ */ package org.rascalmpl.vscode.lsp.util; -import java.util.stream.Stream; +import java.util.Collection; import org.eclipse.lsp4j.TextDocumentIdentifier; import org.eclipse.lsp4j.TextDocumentItem; import org.eclipse.lsp4j.VersionedTextDocumentIdentifier; @@ -59,6 +59,6 @@ default T route(TextDocumentIdentifier id) { return route(Locations.toLoc(id.getUri())); } - Stream allRoutes(); + Collection allRoutes(); } diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/util/FutureCaller.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/util/FutureCaller.java deleted file mode 100644 index 3a9888921..000000000 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/util/FutureCaller.java +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Copyright (c) 2018-2025, NWO-I CWI and Swat.engineering - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE - * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE - * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR - * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF - * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS - * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN - * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - * POSSIBILITY OF SUCH DAMAGE. - */ -package org.rascalmpl.vscode.lsp.util; - -import java.util.concurrent.CompletableFuture; -import java.util.function.BiConsumer; -import java.util.function.BiFunction; -import java.util.stream.Stream; -import org.apache.commons.lang3.function.TriConsumer; - -/** - * A helper interface to support calling functions on values in futures. - * @param The type of the value in the futures. - */ -public interface FutureCaller { - - default CompletableFuture callCompose(CompletableFuture t, BiFunction> func, A arg) { - return t.thenCompose(actualT -> func.apply(actualT, arg)); - } - - default CompletableFuture callApply(CompletableFuture t, BiFunction func, A arg) { - return t.thenApply(actualT -> func.apply(actualT, arg)); - } - - default CompletableFuture callAccept(CompletableFuture t, BiConsumer func, A arg) { - return t.thenAccept(actualT -> func.accept(actualT, arg)); - } - - default CompletableFuture callAccept(CompletableFuture t, TriConsumer func, A arg1, B arg2) { - return t.thenAccept(actualT -> func.accept(actualT, arg1, arg2)); - } - - default Stream> callAccept(Stream> ts, BiConsumer func, A arg) { - return ts.map(t -> callAccept(t, func, arg)); - } - - default Stream> callAccept(Stream> ts, TriConsumer func, A arg1, B arg2) { - return ts.map(t -> callAccept(t, func, arg1, arg2)); - } -} From 3c19a6df6471fc429dc803d29d619fbccf732676 Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Wed, 13 May 2026 11:59:55 +0200 Subject: [PATCH 067/107] Remove unused executor. --- .../parametric/routing/ActualRoutingLanguageServer.java | 2 +- .../lsp/parametric/routing/RoutingTextDocumentService.java | 7 +------ 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/ActualRoutingLanguageServer.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/ActualRoutingLanguageServer.java index 4361480bc..b42f99f87 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/ActualRoutingLanguageServer.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/ActualRoutingLanguageServer.java @@ -120,7 +120,7 @@ public class ActualRoutingLanguageServer extends BaseLanguageServer.ActualLangua private AtomicInteger remotePortOffset = new AtomicInteger(0); public ActualRoutingLanguageServer(Runnable onExit, ExecutorService exec) { - super(onExit, exec, new RoutingTextDocumentService(exec), new RoutingWorkspaceService(exec)); + super(onExit, exec, new RoutingTextDocumentService(), new RoutingWorkspaceService(exec)); } public CompletableFuture languageByName(String lang) { diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingTextDocumentService.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingTextDocumentService.java index 477be9d08..1119ce3d2 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingTextDocumentService.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingTextDocumentService.java @@ -30,7 +30,6 @@ import java.util.List; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; -import java.util.concurrent.ExecutorService; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.stream.Collectors; @@ -118,11 +117,7 @@ public class RoutingTextDocumentService implements IBaseTextDocumentService, Doc private @MonotonicNonNull BaseWorkspaceService wsService; private @MonotonicNonNull ActualRoutingLanguageServer parentServer; - @SuppressWarnings("unused") - private final ExecutorService exec; - - public RoutingTextDocumentService(ExecutorService exec) { - this.exec = exec; + public RoutingTextDocumentService() { } public void setParentServer(ActualRoutingLanguageServer server) { From 8ec2677665abb9132d3fdd818b5b0cb9ec35cbf8 Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Wed, 13 May 2026 12:00:16 +0200 Subject: [PATCH 068/107] Do not suppress warning on function that we need later. --- .../lsp/parametric/routing/RoutingTextDocumentService.java | 1 - 1 file changed, 1 deletion(-) diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingTextDocumentService.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingTextDocumentService.java index 1119ce3d2..ddf43332b 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingTextDocumentService.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingTextDocumentService.java @@ -140,7 +140,6 @@ public CompletableFuture route(ISourceLocation loc) { .thenApply(IBaseLanguageServerExtensions::getIBaseTextDocumentService); } - @SuppressWarnings("unused") private LanguageClient availableClient() { if (client == null) { throw new IllegalStateException("Client not connected yet."); From 485921cbca8643112f4d25a54fe24813559c3fb6 Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Wed, 13 May 2026 14:23:25 +0200 Subject: [PATCH 069/107] Route doc->doc, workspace->workspace. --- .../lsp/IBaseLanguageServerExtensions.java | 1 + .../routing/ActualRoutingLanguageServer.java | 29 ++++--- .../routing/RoutingTextDocumentService.java | 79 +++++-------------- .../routing/RoutingWorkspaceService.java | 45 ++++++++--- .../vscode/lsp/util/DocumentRouter.java | 7 ++ 5 files changed, 79 insertions(+), 82 deletions(-) diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/IBaseLanguageServerExtensions.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/IBaseLanguageServerExtensions.java index 4bbef2ffb..70bb5d744 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/IBaseLanguageServerExtensions.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/IBaseLanguageServerExtensions.java @@ -67,4 +67,5 @@ default CompletableFuture[]> supplyPathConfig(PathConfigParam @JsonDelegate IBaseTextDocumentService getIBaseTextDocumentService(); + } diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/ActualRoutingLanguageServer.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/ActualRoutingLanguageServer.java index b42f99f87..d01f89d49 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/ActualRoutingLanguageServer.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/ActualRoutingLanguageServer.java @@ -123,7 +123,8 @@ public ActualRoutingLanguageServer(Runnable onExit, ExecutorService exec) { super(onExit, exec, new RoutingTextDocumentService(), new RoutingWorkspaceService(exec)); } - public CompletableFuture languageByName(String lang) { + @Override + public CompletableFuture route(String lang) { var service = languageServers.get(lang); if (service == null) { throw new UnsupportedOperationException(String.format("Rascal Parametric LSP has no support for this file, since no language is registered with name '%s'", lang)); @@ -138,7 +139,10 @@ public Collection> allRoutes() @Override public CompletableFuture route(ISourceLocation file) { - return route(extension(file)); + var ext = extension(file); + return safeLanguage(ext).map(this::route).orElseThrow(() -> { + throw new UnsupportedOperationException(String.format("Rascal Parametric LSP has no support for this file, since no language is registered with extension '%s'", ext)); + }); } @Override @@ -147,12 +151,6 @@ public void connect(LanguageClient client) { super.connect(client); } - public CompletableFuture route(String extension) { - return safeLanguage(extension).map(this::languageByName).orElseThrow(() -> { - throw new UnsupportedOperationException(String.format("Rascal Parametric LSP has no support for this file, since no language is registered with extension '%s'", extension)); - }); - } - private Optional safeLanguage(String extension) { if ("".equals(extension)) { var languages = new HashSet<>(languagesByExtension.values()); @@ -399,8 +397,8 @@ public CompletableFuture initialize(InitializeParams params) { this.initializeParams = params; // Our child needs us, but we cannot set this in the constructor, so we set it here. - getTextDocumentService().setParentServer(this); - getWorkspaceService().setParentServer(this); + getTextDocumentService().setServerRouter(this); + getWorkspaceService().setServerRouter(this); return super.initialize(params); } @@ -416,13 +414,16 @@ public synchronized CompletableFuture sendRegisterLanguage(LanguageParamet for (var ext : lang.getExtensions()) { languagesByExtension.put(ext, lang.getName()); } - return super.sendRegisterLanguage(lang); + return server.thenCompose(s -> s.sendRegisterLanguage(lang)); } @Override public synchronized CompletableFuture sendUnregisterLanguage(LanguageParameter lang) { logger.debug("rascal/sendUnregisterLanguage({})", lang.getName()); + var work = route(lang.getName()) + .thenCompose(s -> s.sendUnregisterLanguage(lang)); + boolean removeAll = lang.getMainModule() == null || lang.getMainModule().isEmpty(); if (removeAll) { // clear the whole language @@ -433,12 +434,14 @@ public synchronized CompletableFuture sendUnregisterLanguage(LanguageParam } var removed = languageServers.remove(lang.getName()); if (removed != null) { - removed.thenCompose(server -> server.shutdown().thenApply(o -> server)) + work = work + .thenCompose(ignored -> removed) + .thenCompose(server -> server.shutdown().thenApply(o -> server)) .thenAccept(LanguageServer::exit); } } - return super.sendUnregisterLanguage(lang); + return work; } @Override diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingTextDocumentService.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingTextDocumentService.java index ddf43332b..70dfc7383 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingTextDocumentService.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingTextDocumentService.java @@ -29,9 +29,6 @@ import java.util.Collection; import java.util.List; import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; import java.util.stream.Collectors; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -100,7 +97,6 @@ import org.rascalmpl.vscode.lsp.parametric.LanguageRegistry.LanguageParameter; import org.rascalmpl.vscode.lsp.parametric.ParametricTextDocumentService; import org.rascalmpl.vscode.lsp.util.DocumentRouter; -import org.rascalmpl.vscode.lsp.util.Lists; import org.rascalmpl.vscode.lsp.util.locations.Locations; import io.usethesource.vallang.ISourceLocation; @@ -114,30 +110,35 @@ public class RoutingTextDocumentService implements IBaseTextDocumentService, Doc private static final Logger logger = LogManager.getLogger(RoutingTextDocumentService.class); private @MonotonicNonNull LanguageClient client; - private @MonotonicNonNull BaseWorkspaceService wsService; - private @MonotonicNonNull ActualRoutingLanguageServer parentServer; + private @MonotonicNonNull DocumentRouter> serverRouter; - public RoutingTextDocumentService() { + /*package*/ void setServerRouter(DocumentRouter> serverRouter) { + this.serverRouter = serverRouter; } - public void setParentServer(ActualRoutingLanguageServer server) { - this.parentServer = server; + private DocumentRouter> availableServerRouter() { + if (serverRouter == null) { + // This should only happen if we forgot to call `setServerRouter` before finishing initialization + throw new IllegalStateException("No server router available"); + } + return serverRouter; } @Override public Collection> allRoutes() { - return availableServer() - .allRoutes() - .stream() + return availableServerRouter().allRoutes().stream() .map(server -> server.thenApply(IBaseLanguageServerExtensions::getIBaseTextDocumentService)) .collect(Collectors.toList()); } @Override public CompletableFuture route(ISourceLocation loc) { - return availableServer() - .route(loc) - .thenApply(IBaseLanguageServerExtensions::getIBaseTextDocumentService); + return availableServerRouter().route(loc).thenApply(IBaseLanguageServerExtensions::getIBaseTextDocumentService); + } + + @Override + public CompletableFuture route(String language) { + return availableServerRouter().route(language).thenApply(IBaseLanguageServerExtensions::getIBaseTextDocumentService); } private LanguageClient availableClient() { @@ -147,13 +148,6 @@ private LanguageClient availableClient() { return client; } - private ActualRoutingLanguageServer availableServer() { - if (parentServer == null) { - throw new IllegalStateException("Server not connected yet."); - } - return parentServer; - } - @Override public void didOpen(DidOpenTextDocumentParams params) { // Note: floating future @@ -185,7 +179,7 @@ public void initializeServerCapabilities(ClientCapabilities clientCapabilities, @Override public void shutdown() { - availableServer().shutdown(); + // reserved for future use } @Override @@ -196,7 +190,7 @@ public void connect(LanguageClient client) { @Override public void pair(BaseWorkspaceService workspaceService) { - this.wsService = workspaceService; + // reserved for future use } @Override @@ -206,37 +200,12 @@ public void initialized() { @Override public void registerLanguage(LanguageParameter lang) { - logger.debug("textDocument/registerLanguage({}, {})", lang.getName(), lang.getMainFunction()); - try { - availableServer().languageByName(lang.getName()) - .thenCompose(server -> server.sendRegisterLanguage(lang)).get(1, TimeUnit.MINUTES); - } catch (UnsupportedOperationException e) { - // Strange, since we just registered this language and should have a server for it - logger.error("Language registration for unknown language {}", lang.getName()); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } catch (ExecutionException e ) { - logger.error("Registration of language {} failed due to unexpected error", lang.getName(), e); - } catch (TimeoutException e) { - logger.error("Registration of language {} timed out", lang.getName(), e); - } + // Nothing to do here } @Override public void unregisterLanguage(LanguageParameter lang) { - logger.debug("textDocument/unregisterLanguage({})", lang.getName()); - try { - availableServer().languageByName(lang.getName()) - .thenCompose(s -> s.sendUnregisterLanguage(lang)).get(1, TimeUnit.MINUTES); - } catch (UnsupportedOperationException e) { - logger.debug("Ignored language unregistration for unknown language {}", lang.getName()); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } catch (ExecutionException e ) { - logger.error("Unregistration of language {} failed due to unexpected error", lang.getName(), e); - } catch (TimeoutException e) { - logger.error("Unregistration of language {} timed out", lang.getName(), e); - } + // Nothing to do here } @Override @@ -285,14 +254,6 @@ public boolean isManagingFile(ISourceLocation file) { @Override public void didCreateFiles(CreateFilesParams params) { // TODO Mimick VS given certain file operation filters (capabilities) - var filesByExt = params.getFiles().stream() - .collect(Collectors.toMap(f -> ActualRoutingLanguageServer.extension(Locations.toLoc(f.getUri())), List::of, Lists::union)); - - for (var e : filesByExt.entrySet()) { - availableServer().route(e.getKey()).thenAccept(s -> - s.getWorkspaceService().didCreateFiles(new CreateFilesParams(e.getValue())) - ); - } } @Override diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingWorkspaceService.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingWorkspaceService.java index 7d27342c0..be7641732 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingWorkspaceService.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingWorkspaceService.java @@ -27,41 +27,66 @@ package org.rascalmpl.vscode.lsp.parametric.routing; import com.google.gson.JsonPrimitive; +import java.util.Collection; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutorService; +import java.util.stream.Collectors; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.eclipse.lsp4j.ExecuteCommandParams; +import org.eclipse.lsp4j.services.LanguageServer; +import org.eclipse.lsp4j.services.WorkspaceService; import org.rascalmpl.vscode.lsp.BaseWorkspaceService; +import org.rascalmpl.vscode.lsp.IBaseLanguageServerExtensions; +import org.rascalmpl.vscode.lsp.util.DocumentRouter; import org.rascalmpl.vscode.lsp.util.concurrent.CompletableFutureUtils; +import io.usethesource.vallang.ISourceLocation; + /** * A language-parametric workspace service that routes incoming requests to remote dedicated language servers. */ -public class RoutingWorkspaceService extends BaseWorkspaceService { +public class RoutingWorkspaceService extends BaseWorkspaceService implements DocumentRouter> { - private @MonotonicNonNull ActualRoutingLanguageServer parentServer; + private @MonotonicNonNull DocumentRouter> serverRouter; public RoutingWorkspaceService(ExecutorService exec) { super(exec); } - public void setParentServer(ActualRoutingLanguageServer server) { - this.parentServer = server; + /*package*/ void setServerRouter(DocumentRouter> server) { + this.serverRouter = server; } - private ActualRoutingLanguageServer availableServer() { - if (parentServer == null) { - throw new IllegalStateException("Server not connected yet."); + private DocumentRouter> availableServerRouter() { + if (serverRouter == null) { + // This should only happen if we forgot to call `setServerRouter` before finishing initialization + throw new IllegalStateException("No server router available"); } - return parentServer; + return serverRouter; + } + + @Override + public Collection> allRoutes() { + return availableServerRouter().allRoutes().stream() + .map(server -> server.thenApply(LanguageServer::getWorkspaceService)) + .collect(Collectors.toList()); + } + + @Override + public CompletableFuture route(ISourceLocation loc) { + return availableServerRouter().route(loc).thenApply(LanguageServer::getWorkspaceService); + } + + @Override + public CompletableFuture route(String languageName) { + return availableServerRouter().route(languageName).thenApply(LanguageServer::getWorkspaceService); } @Override public CompletableFuture executeCommand(ExecuteCommandParams commandParams) { if (commandParams.getCommand().startsWith(RASCAL_META_COMMAND) || commandParams.getCommand().startsWith(RASCAL_COMMAND)) { var languageName = ((JsonPrimitive) commandParams.getArguments().get(0)).getAsString(); - return availableServer().languageByName(languageName) - .thenCompose(s -> s.getWorkspaceService().executeCommand(commandParams)); + return route(languageName).thenCompose(s -> s.executeCommand(commandParams)); } return CompletableFutureUtils.completedFuture(commandParams.getCommand() + " was ignored.", getExecutor()); diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/util/DocumentRouter.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/util/DocumentRouter.java index 546e8bb75..06d994666 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/util/DocumentRouter.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/util/DocumentRouter.java @@ -47,6 +47,13 @@ public interface DocumentRouter { */ T route(ISourceLocation loc); + /** + * Map a {@link String} name to a {@link T}. + * @param doc The name key. + * @return The mapped value. + */ + T route(String name); + default T route(TextDocumentItem doc) { return route(Locations.toLoc(doc.getUri())); } From ff932569a51365321d5193d951a4a064e55fdab2 Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Wed, 13 May 2026 14:23:53 +0200 Subject: [PATCH 070/107] More precise command ignore message. --- .../vscode/lsp/parametric/routing/RoutingWorkspaceService.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingWorkspaceService.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingWorkspaceService.java index be7641732..cfe16492c 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingWorkspaceService.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingWorkspaceService.java @@ -89,8 +89,7 @@ public CompletableFuture executeCommand(ExecuteCommandParams commandPara return route(languageName).thenCompose(s -> s.executeCommand(commandParams)); } - return CompletableFutureUtils.completedFuture(commandParams.getCommand() + " was ignored.", getExecutor()); + return CompletableFutureUtils.completedFuture(commandParams.getCommand() + " was ignored, since it is not a Rascal LSP command.", getExecutor()); } - } From 949aab0e9d61dfcd22608b4b8b3fff36ed36a319 Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Wed, 13 May 2026 14:31:23 +0200 Subject: [PATCH 071/107] Deserialize routed IValue as wrapped JSON element. --- .../lsp/parametric/routing/ProxiedIValue.java | 129 ++---------------- 1 file changed, 12 insertions(+), 117 deletions(-) diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/ProxiedIValue.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/ProxiedIValue.java index f65377e04..5f8192bf1 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/ProxiedIValue.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/ProxiedIValue.java @@ -26,6 +26,9 @@ */ package org.rascalmpl.vscode.lsp.parametric.routing; +import com.google.gson.Gson; +import com.google.gson.JsonElement; +import com.google.gson.JsonParser; import com.google.gson.stream.JsonReader; import com.google.gson.stream.JsonWriter; import java.io.IOException; @@ -35,20 +38,18 @@ import io.usethesource.vallang.type.Type; /** - * Wraps a JSON string representing an IValue as an IValue. + * Wraps a JSON element representing an IValue as an IValue. * * This class allows passing IValues through JSON-RPC-enabled servers without requiring to decode/encode them. */ class ProxiedIValue implements IExternalValue { - private String contents; + private static final Gson gson = new Gson(); - /*package*/ ProxiedIValue(String json) { - this.contents = json; - } + private final JsonElement element; - /*package*/ String getContents() { - return this.contents; + private ProxiedIValue(JsonElement element) { + this.element = element; } @Override @@ -67,123 +68,17 @@ public Type getType() { * @param value the value to proxy * @throws IOException if an unexpected input occurs */ - /*package*/ static void toJson(JsonWriter writer, ProxiedIValue value) throws IOException { - writer.jsonValue(value.getContents()); + /*package*/ static void toJson(JsonWriter writer, ProxiedIValue value) { + gson.toJson(value.element, writer); } /** * Wrap the {@link IValue}'s JSON representation. * @param reader the reader to read the JSON from * @return the JSON as an {@link IValue} - * @throws IOException if an unexpected input occurs */ - /*package*/ static ProxiedIValue fromJson(JsonReader reader) throws IOException { - var sb = new StringBuilder(); - readValue(reader, sb); - return new ProxiedIValue(sb.toString()); - } - - /** - * Recursively build a JSON string given a tokenized input. - * @param reader a tokenized JSON reader - * @param sb a builder to which to append the result string - * @throws IOException if an unexpected input occurs - */ - private static void readValue(JsonReader reader, StringBuilder sb) throws IOException { - switch (reader.peek()) { - case BEGIN_ARRAY: - readArray(reader, sb); - break; - case BEGIN_OBJECT: - readObject(reader, sb); - break; - case BOOLEAN: - readBoolean(reader, sb); - break; - case NULL: - readNull(reader, sb); - break; - case NUMBER: - readNumber(reader, sb); - break; - case STRING: - readString(reader, sb); - break; - case NAME: // fall-through - case END_ARRAY: // fall-through - case END_DOCUMENT: // fall-through - case END_OBJECT: // fall-through - throw new IOException("Malformed JSON"); - default: - throw new IOException("Unknown JSON token"); - } - } - - private static void readString(JsonReader reader, StringBuilder sb) throws IOException { - sb.append('"'); - sb.append(reader.nextString()); - sb.append('"'); - } - - private static void readNumber(JsonReader reader, StringBuilder sb) throws IOException { - try { - sb.append(reader.nextInt()); - return; - } catch (NumberFormatException e) {/* try the next number type */} - try { - sb.append(reader.nextLong()); - return; - } catch (NumberFormatException e) {/* try the next number type */} - try { - sb.append(reader.nextDouble()); - } catch (NumberFormatException e) { - throw new IOException("Could not parse number in JSON object: " + reader.getPath(), e); - } - } - - private static void readNull(JsonReader reader, StringBuilder sb) throws IOException { - reader.nextNull(); - sb.append("null"); - } - - private static void readBoolean(JsonReader reader, StringBuilder sb) throws IOException { - sb.append(reader.nextBoolean()); - } - - private static void readArray(JsonReader reader, StringBuilder sb) throws IOException { - reader.beginArray(); - sb.append('['); - boolean hasNext = reader.hasNext(); - while (hasNext) { - readValue(reader, sb); - hasNext = reader.hasNext(); - if (hasNext) { - sb.append(','); - } - } - reader.endArray(); - sb.append(']'); - } - - private static void readObject(JsonReader reader, StringBuilder sb) throws IOException { - reader.beginObject(); - sb.append('{'); - - boolean hasNext = reader.hasNext(); - while (hasNext) { - sb.append('"'); - sb.append(reader.nextName()); - sb.append('"'); - sb.append(':'); - readValue(reader, sb); - - hasNext = reader.hasNext(); - if (hasNext) { - sb.append(','); - } - } - reader.endObject(); - sb.append('}'); + /*package*/ static ProxiedIValue fromJson(JsonReader reader) { + return new ProxiedIValue(JsonParser.parseReader(reader)); } } From ac870fc71f30133403dea9c284234aaad14b60b8 Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Wed, 13 May 2026 15:02:36 +0200 Subject: [PATCH 072/107] Construct class path based on path config. --- .../routing/ActualRoutingLanguageServer.java | 93 +++++++++---------- .../library/demo/lang/pico/LanguageServer.rsc | 4 +- 2 files changed, 48 insertions(+), 49 deletions(-) diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/ActualRoutingLanguageServer.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/ActualRoutingLanguageServer.java index d01f89d49..2497d637d 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/ActualRoutingLanguageServer.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/ActualRoutingLanguageServer.java @@ -30,6 +30,7 @@ import com.google.gson.TypeAdapter; import com.google.gson.stream.JsonReader; import com.google.gson.stream.JsonWriter; +import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; @@ -52,6 +53,8 @@ import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; import org.apache.commons.lang3.tuple.Triple; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -83,10 +86,7 @@ import org.eclipse.lsp4j.services.LanguageServer; import org.rascalmpl.library.util.PathConfig; import org.rascalmpl.uri.URIUtil; -import org.rascalmpl.util.maven.Artifact; -import org.rascalmpl.util.maven.MavenParser; -import org.rascalmpl.util.maven.ModelResolutionError; -import org.rascalmpl.util.maven.Scope; +import org.rascalmpl.util.maven.MavenSettings; import org.rascalmpl.vscode.lsp.BaseLanguageServer; import org.rascalmpl.vscode.lsp.IBaseLanguageClient; import org.rascalmpl.vscode.lsp.IBaseLanguageServerExtensions; @@ -169,64 +169,61 @@ public static String extension(ISourceLocation doc) { return URIUtil.getExtension(doc); } - private static boolean isRascalLspProject(Artifact art) { - var c = art.getCoordinate(); - if (!c.getGroupId().equals("org.rascalmpl")) { - return false; + private static Path resolveUri(URI uri) { + if (!uri.getScheme().equals("mvn")) { + return Path.of(uri); } - var id = c.getArtifactId(); - return "rascal-lsp".equals(id); - } - private static String classPath(LanguageParameter lang) { - var separator = System.getProperty("path.separator"); - PathConfig pcfg; - try { - pcfg = PathConfig.parse(lang.getPathConfig()); - } catch (IOException e) { - logger.error("Error while parsing path config {}", lang.getPathConfig(), e); - logger.warn("Could not compute class path for {}; falling back to class path of routing server", lang.getName()); - return System.getProperty("java.class.path"); + if (uri.getAuthority().isEmpty()) { + throw new IllegalArgumentException("Invalid Maven URI (no authority): " + uri.toString()); } - var pom = Locations.toPhysicalIfPossible(URIUtil.getChildLocation(pcfg.getProjectRoot(), "pom.xml")); - var p = new MavenParser(Path.of(pom.getURI())); - try { - var rootProject = p.parseProject(); + var parts = uri.getAuthority().split("--"); + if (parts.length != 3) { + throw new IllegalArgumentException("Invalid Maven URI (expected 2 '--' separators): " + uri.toString()); + } - // Check if we are in Rascal-LSP - var classPath = new StringBuilder(); - if (isRascalLspProject(rootProject)) { - classPath.append(separator); - classPath.append(Path.of(Locations.toUri(pcfg.getBin()))); - } + var group = parts[0]; + var name = parts[1]; + var version = parts[2]; + var jarPath = group.replaceAll("\\.", "/") + + "/" + + name + + "/" + + version + + "/" + + name + + "-" + + version + + ".jar" + ; - // Check the project dependencies - var deps = rootProject.resolveDependencies(Scope.COMPILE, p); - for (var d : deps) { - if (d.getResolved() != null) { - classPath.append(separator); - classPath.append(d.getResolved()); - } - } - // strip of the initial separator - return classPath.substring(separator.length()); - } catch (ModelResolutionError e) { - logger.error("Error while parsing POM at {}", pom, e); - } + return MavenSettings.mavenRepository().resolve(jarPath); + } - // If all else fails, just use the same class path that we have - logger.warn("Could not compute class path for {}; falling back to class path of routing server", lang.getName()); - return System.getProperty("java.class.path"); + private static String classPath(LanguageParameter lang) throws IOException { + var pcfg = PathConfig.parse(lang.getPathConfig()); + var libs = pcfg.getLibs().stream().map(ISourceLocation.class::cast); + var bins = Stream.of(pcfg.getBin()); + + var classPath = Stream.concat(bins, libs) // project target folder first + .map(Locations::toPhysicalIfPossible) + .map(Locations::toUri) + .map(ActualRoutingLanguageServer::resolveUri) + .map(Path::toString) + .collect(Collectors.toList()); + + return String.join(File.pathSeparator, classPath); } private @Nullable Triple startServerProcess(LanguageParameter lang) { logger.info("Starting LSP process for {}", lang.getName()); - var classPath = classPath(lang); - logger.debug("{} runs with class path {}", lang.getName(), classPath); // In deployment, we start a process and connect to it via input/output streams try { + var classPath = classPath(lang); + logger.debug("{} runs with class path {}", lang.getName(), classPath); + var proc = new ProcessBuilder(ProcessHandle.current().info().command().orElse("java") , "-Dlog4j2.configurationFactory=org.rascalmpl.vscode.lsp.log.LogJsonConfiguration" , "-Dlog4j2.level=DEBUG" diff --git a/rascal-lsp/src/main/rascal/library/demo/lang/pico/LanguageServer.rsc b/rascal-lsp/src/main/rascal/library/demo/lang/pico/LanguageServer.rsc index 56cf4b599..d2d8688f7 100644 --- a/rascal-lsp/src/main/rascal/library/demo/lang/pico/LanguageServer.rsc +++ b/rascal-lsp/src/main/rascal/library/demo/lang/pico/LanguageServer.rsc @@ -363,11 +363,13 @@ Any feedback (errors and exceptions) is faster and more clearly printed in the t void main(bool errorRecovery=false) { loc root; try { + // Try to resolve the LSP project. root = resolveLocation(|project://rascal-lsp|); } catch SchemeNotSupported(_): { + // Otherwise, we are in the nested pico workspace. Resolve the LSP project from there. root = resolveLocation(|cwd:///../../../rascal-lsp|); } - pcfg = pathConfig(projectRoot = root, bin = root + "target/classes"); + pcfg = getProjectPathConfig(root, mode=interpreter()); registerLanguage( language( pcfg, From 1e642c226aac5ed5db042a117d655e15b53bb585 Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Thu, 14 May 2026 11:39:31 +0200 Subject: [PATCH 073/107] Swap launch config order. --- rascal-lsp/.vscode/launch.json | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/rascal-lsp/.vscode/launch.json b/rascal-lsp/.vscode/launch.json index 5d25815d2..ddd58f276 100644 --- a/rascal-lsp/.vscode/launch.json +++ b/rascal-lsp/.vscode/launch.json @@ -29,10 +29,9 @@ }, { "type": "java", - "name": "Delegate Parametric Server [1]", + "name": "Parametric Routing Server", "request": "launch", - "mainClass": "org.rascalmpl.vscode.lsp.parametric.ParametricLanguageServer", - "args": ["--port", "9990"], + "mainClass": "org.rascalmpl.vscode.lsp.parametric.routing.RoutingLanguageServer", "projectName": "rascal-lsp", "console": "internalConsole", "vmArgs": [ @@ -43,10 +42,10 @@ }, { "type": "java", - "name": "Delegate Parametric Server [2]", + "name": "Delegate Parametric Server [1]", "request": "launch", "mainClass": "org.rascalmpl.vscode.lsp.parametric.ParametricLanguageServer", - "args": ["--port", "9991"], + "args": ["--port", "9990"], "projectName": "rascal-lsp", "console": "internalConsole", "vmArgs": [ @@ -57,9 +56,10 @@ }, { "type": "java", - "name": "Parametric Routing Server", + "name": "Delegate Parametric Server [2]", "request": "launch", - "mainClass": "org.rascalmpl.vscode.lsp.parametric.routing.RoutingLanguageServer", + "mainClass": "org.rascalmpl.vscode.lsp.parametric.ParametricLanguageServer", + "args": ["--port", "9991"], "projectName": "rascal-lsp", "console": "internalConsole", "vmArgs": [ From 754297e28d48f6f4a33f8e848173c05347f3523b Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Thu, 14 May 2026 13:52:36 +0200 Subject: [PATCH 074/107] Extract client proxy to separate class. --- .../routing/ActualRoutingLanguageServer.java | 160 +------------- .../routing/MultipleClientProxy.java | 209 ++++++++++++++++++ 2 files changed, 215 insertions(+), 154 deletions(-) create mode 100644 rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/MultipleClientProxy.java diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/ActualRoutingLanguageServer.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/ActualRoutingLanguageServer.java index 2497d637d..0a774e817 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/ActualRoutingLanguageServer.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/ActualRoutingLanguageServer.java @@ -41,7 +41,6 @@ import java.nio.file.Path; import java.util.Collection; import java.util.HashSet; -import java.util.List; import java.util.Map; import java.util.Optional; import java.util.concurrent.CancellationException; @@ -60,24 +59,8 @@ import org.apache.logging.log4j.Logger; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.checkerframework.checker.nullness.qual.Nullable; -import org.eclipse.lsp4j.ApplyWorkspaceEditParams; -import org.eclipse.lsp4j.ApplyWorkspaceEditResponse; -import org.eclipse.lsp4j.ConfigurationParams; import org.eclipse.lsp4j.InitializeParams; import org.eclipse.lsp4j.InitializeResult; -import org.eclipse.lsp4j.LogTraceParams; -import org.eclipse.lsp4j.MessageActionItem; -import org.eclipse.lsp4j.MessageParams; -import org.eclipse.lsp4j.ProgressParams; -import org.eclipse.lsp4j.PublishDiagnosticsParams; -import org.eclipse.lsp4j.Range; -import org.eclipse.lsp4j.RegistrationParams; -import org.eclipse.lsp4j.ShowDocumentParams; -import org.eclipse.lsp4j.ShowDocumentResult; -import org.eclipse.lsp4j.ShowMessageRequestParams; -import org.eclipse.lsp4j.UnregistrationParams; -import org.eclipse.lsp4j.WorkDoneProgressCreateParams; -import org.eclipse.lsp4j.WorkspaceFolder; import org.eclipse.lsp4j.jsonrpc.Launcher; import org.eclipse.lsp4j.jsonrpc.ResponseErrorException; import org.eclipse.lsp4j.jsonrpc.messages.ResponseError; @@ -88,21 +71,19 @@ import org.rascalmpl.uri.URIUtil; import org.rascalmpl.util.maven.MavenSettings; import org.rascalmpl.vscode.lsp.BaseLanguageServer; -import org.rascalmpl.vscode.lsp.IBaseLanguageClient; import org.rascalmpl.vscode.lsp.IBaseLanguageServerExtensions; import org.rascalmpl.vscode.lsp.parametric.LanguageRegistry.LanguageParameter; import org.rascalmpl.vscode.lsp.util.DocumentRouter; import org.rascalmpl.vscode.lsp.util.concurrent.CompletableFutureUtils; import org.rascalmpl.vscode.lsp.util.locations.Locations; -import io.usethesource.vallang.IInteger; import io.usethesource.vallang.ISourceLocation; -import io.usethesource.vallang.IString; +import io.usethesource.vallang.IValue; /** * A language server implementation that routes LSP requests to dedicated remote language servers. */ -public class ActualRoutingLanguageServer extends BaseLanguageServer.ActualLanguageServer implements IBaseLanguageClient, DocumentRouter> { +public class ActualRoutingLanguageServer extends BaseLanguageServer.ActualLanguageServer implements DocumentRouter> { private static final Logger logger = LogManager.getLogger(ActualRoutingLanguageServer.class); @@ -113,7 +94,7 @@ public class ActualRoutingLanguageServer extends BaseLanguageServer.ActualLangua // NOTE To be able to route to arbitrary third-party language servers, remote servers should implement `LanguageServer` (instead of `IBaseLanguageServerExtensions`) private final Map> languageServers = new ConcurrentHashMap<>(); - private @MonotonicNonNull IBaseLanguageClient client; + private final MultipleClientProxy client = new MultipleClientProxy(); private @MonotonicNonNull InitializeParams initializeParams; private static final int REMOTE_BASE_PORT = 9990; @@ -147,8 +128,8 @@ public CompletableFuture route(ISourceLocation fi @Override public void connect(LanguageClient client) { - this.client = (IBaseLanguageClient) client; - super.connect(client); + super.connect(client); // first let the super class proxy the client + this.client.connect(availableClient()); } private Optional safeLanguage(String extension) { @@ -308,7 +289,7 @@ public void write(JsonWriter writer, ProxiedIValue proxiedValue) throws IOExcept var serverLauncher = new Launcher.Builder() .setRemoteInterface(IBaseLanguageServerExtensions.class) - .setLocalService(this) + .setLocalService(client) .setInput(serverParams.getLeft()) .setOutput(serverParams.getMiddle()) .configureGson(ActualRoutingLanguageServer::configureProxyGson) @@ -441,135 +422,6 @@ public synchronized CompletableFuture sendUnregisterLanguage(LanguageParam return work; } - @Override - public void telemetryEvent(Object object) { - availableClient().telemetryEvent(object); - } - - @Override - public void publishDiagnostics(PublishDiagnosticsParams diagnostics) { - availableClient().publishDiagnostics(diagnostics); - } - - @Override - public void showMessage(MessageParams messageParams) { - availableClient().showMessage(messageParams); - } - - @Override - public CompletableFuture showMessageRequest(ShowMessageRequestParams requestParams) { - return availableClient().showMessageRequest(requestParams); - } - - @Override - public void logMessage(MessageParams message) { - availableClient().logMessage(message); - } - - @Override - public void showContent(URI uri, IString title, IInteger viewColumn) { - availableClient().showContent(uri, title, viewColumn); - } - - @Override - public void receiveRegisterLanguage(LanguageParameter lang) { - logger.debug("rascal/receiveRegisterLanguage({}, {})", lang.getName(), lang.getMainFunction()); - availableClient().receiveRegisterLanguage(lang); - } - - @Override - public void receiveUnregisterLanguage(LanguageParameter lang) { - logger.debug("rascal/receiveUnregisterLanguage({}, {})", lang.getName(), lang.getMainFunction()); - availableClient().receiveUnregisterLanguage(lang); - } - - @Override - public void editDocument(URI uri, @Nullable Range range, int viewColumn) { - availableClient().editDocument(uri, range, viewColumn); - } - - @Override - public void startDebuggingSession(int serverPort) { - availableClient().startDebuggingSession(serverPort); - } - - @Override - public void registerDebugServerPort(int processID, int serverPort) { - availableClient().registerDebugServerPort(processID, serverPort); - } - - @Override - public CompletableFuture createProgress(WorkDoneProgressCreateParams params) { - return availableClient().createProgress(params); - } - - @Override - public void notifyProgress(ProgressParams params) { - availableClient().notifyProgress(params); - } - - @Override - public CompletableFuture applyEdit(ApplyWorkspaceEditParams params) { - return availableClient().applyEdit(params); - } - - @Override - public CompletableFuture> configuration(ConfigurationParams configurationParams) { - return availableClient().configuration(configurationParams); - } - - @Override - public void logTrace(LogTraceParams params) { - availableClient().logTrace(params); - } - - @Override - public CompletableFuture refreshCodeLenses() { - return availableClient().refreshCodeLenses(); - } - - @Override - public CompletableFuture refreshDiagnostics() { - return availableClient().refreshDiagnostics(); - } - - @Override - public CompletableFuture refreshInlayHints() { - return availableClient().refreshInlayHints(); - } - - @Override - public CompletableFuture refreshInlineValues() { - return availableClient().refreshInlineValues(); - } - - @Override - public CompletableFuture refreshSemanticTokens() { - return availableClient().refreshSemanticTokens(); - } - - @Override - public CompletableFuture showDocument(ShowDocumentParams params) { - return availableClient().showDocument(params); - } - - @Override - public CompletableFuture registerCapability(RegistrationParams params) { - // TODO Collect/maintain capabilities of all delegate servers, combine, and unregister capabilities if necessary based on that. - return availableClient().registerCapability(params); - } - - @Override - public CompletableFuture unregisterCapability(UnregistrationParams params) { - // TODO Collect/maintain capabilities of all delegate servers, combine, and unregister capabilities if necessary based on that. - return availableClient().unregisterCapability(params); - } - - @Override - public CompletableFuture> workspaceFolders() { - return availableClient().workspaceFolders(); - } - @Override public CompletableFuture shutdown() { return CompletableFutureUtils.reduce(allRoutes().stream().map(serverFut -> serverFut.thenCompose(IBaseLanguageServerExtensions::shutdown)), getExecutor()) diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/MultipleClientProxy.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/MultipleClientProxy.java new file mode 100644 index 000000000..a59f04ade --- /dev/null +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/MultipleClientProxy.java @@ -0,0 +1,209 @@ +/* + * Copyright (c) 2018-2025, NWO-I CWI and Swat.engineering + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +package org.rascalmpl.vscode.lsp.parametric.routing; + +import java.net.URI; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.eclipse.lsp4j.ApplyWorkspaceEditParams; +import org.eclipse.lsp4j.ApplyWorkspaceEditResponse; +import org.eclipse.lsp4j.ConfigurationParams; +import org.eclipse.lsp4j.LogTraceParams; +import org.eclipse.lsp4j.MessageActionItem; +import org.eclipse.lsp4j.MessageParams; +import org.eclipse.lsp4j.ProgressParams; +import org.eclipse.lsp4j.PublishDiagnosticsParams; +import org.eclipse.lsp4j.Range; +import org.eclipse.lsp4j.RegistrationParams; +import org.eclipse.lsp4j.ShowDocumentParams; +import org.eclipse.lsp4j.ShowDocumentResult; +import org.eclipse.lsp4j.ShowMessageRequestParams; +import org.eclipse.lsp4j.UnregistrationParams; +import org.eclipse.lsp4j.WorkDoneProgressCreateParams; +import org.eclipse.lsp4j.WorkspaceFolder; +import org.eclipse.lsp4j.services.LanguageClient; +import org.eclipse.lsp4j.services.LanguageClientAware; +import org.rascalmpl.vscode.lsp.IBaseLanguageClient; +import org.rascalmpl.vscode.lsp.parametric.LanguageRegistry.LanguageParameter; + +import io.usethesource.vallang.IInteger; +import io.usethesource.vallang.IString; + +/** + * Client proxy implementation that aggregates results from multiple servers before forwarding to its own client. + */ +public class MultipleClientProxy implements IBaseLanguageClient, LanguageClientAware { + + private static final Logger logger = LogManager.getLogger(MultipleClientProxy.class); + + private IBaseLanguageClient client; + + @Override + public void connect(LanguageClient client) { + this.client = (IBaseLanguageClient) client; + } + + protected IBaseLanguageClient availableClient() { + if (client == null) { + throw new IllegalStateException("Language Client has not been connected yet"); + } + return client; + } + + @Override + public void telemetryEvent(Object object) { + availableClient().telemetryEvent(object); + } + + @Override + public void publishDiagnostics(PublishDiagnosticsParams diagnostics) { + availableClient().publishDiagnostics(diagnostics); + } + + @Override + public void showMessage(MessageParams messageParams) { + availableClient().showMessage(messageParams); + } + + @Override + public CompletableFuture showMessageRequest(ShowMessageRequestParams requestParams) { + return availableClient().showMessageRequest(requestParams); + } + + @Override + public void logMessage(MessageParams message) { + availableClient().logMessage(message); + } + + @Override + public void showContent(URI uri, IString title, IInteger viewColumn) { + availableClient().showContent(uri, title, viewColumn); + } + + @Override + public void receiveRegisterLanguage(LanguageParameter lang) { + logger.debug("rascal/receiveRegisterLanguage({}, {})", lang.getName(), lang.getMainFunction()); + availableClient().receiveRegisterLanguage(lang); + } + + @Override + public void receiveUnregisterLanguage(LanguageParameter lang) { + logger.debug("rascal/receiveUnregisterLanguage({}, {})", lang.getName(), lang.getMainFunction()); + availableClient().receiveUnregisterLanguage(lang); + } + + @Override + public void editDocument(URI uri, @Nullable Range range, int viewColumn) { + availableClient().editDocument(uri, range, viewColumn); + } + + @Override + public void startDebuggingSession(int serverPort) { + availableClient().startDebuggingSession(serverPort); + } + + @Override + public void registerDebugServerPort(int processID, int serverPort) { + availableClient().registerDebugServerPort(processID, serverPort); + } + + @Override + public CompletableFuture createProgress(WorkDoneProgressCreateParams params) { + return availableClient().createProgress(params); + } + + @Override + public void notifyProgress(ProgressParams params) { + availableClient().notifyProgress(params); + } + + @Override + public CompletableFuture applyEdit(ApplyWorkspaceEditParams params) { + return availableClient().applyEdit(params); + } + + @Override + public CompletableFuture> configuration(ConfigurationParams configurationParams) { + return availableClient().configuration(configurationParams); + } + + @Override + public void logTrace(LogTraceParams params) { + availableClient().logTrace(params); + } + + @Override + public CompletableFuture refreshCodeLenses() { + return availableClient().refreshCodeLenses(); + } + + @Override + public CompletableFuture refreshDiagnostics() { + return availableClient().refreshDiagnostics(); + } + + @Override + public CompletableFuture refreshInlayHints() { + return availableClient().refreshInlayHints(); + } + + @Override + public CompletableFuture refreshInlineValues() { + return availableClient().refreshInlineValues(); + } + + @Override + public CompletableFuture refreshSemanticTokens() { + return availableClient().refreshSemanticTokens(); + } + + @Override + public CompletableFuture showDocument(ShowDocumentParams params) { + return availableClient().showDocument(params); + } + + @Override + public CompletableFuture registerCapability(RegistrationParams params) { + // TODO Collect/maintain capabilities of all delegate servers, combine, and unregister capabilities if necessary based on that. + return availableClient().registerCapability(params); + } + + @Override + public CompletableFuture unregisterCapability(UnregistrationParams params) { + // TODO Collect/maintain capabilities of all delegate servers, combine, and unregister capabilities if necessary based on that. + return availableClient().unregisterCapability(params); + } + + @Override + public CompletableFuture> workspaceFolders() { + return availableClient().workspaceFolders(); + } + +} From 367a8648425b919a1a0dd521ebbb3a517aecd927 Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Thu, 14 May 2026 14:38:42 +0200 Subject: [PATCH 075/107] Implement leaner doc service interface after refactoring routing. --- .../vscode/lsp/BaseLanguageServer.java | 5 --- .../lsp/IBaseLanguageServerExtensions.java | 5 --- .../routing/ActualRoutingLanguageServer.java | 7 ++++ .../routing/RoutingTextDocumentService.java | 37 ++++++++----------- 4 files changed, 23 insertions(+), 31 deletions(-) diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/BaseLanguageServer.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/BaseLanguageServer.java index b845e46d2..a5c679177 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/BaseLanguageServer.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/BaseLanguageServer.java @@ -320,11 +320,6 @@ public IBaseTextDocumentService getTextDocumentService() { return lspDocumentService; } - @Override - public IBaseTextDocumentService getIBaseTextDocumentService() { - return getTextDocumentService(); - } - @Override public BaseWorkspaceService getWorkspaceService() { return lspWorkspaceService; diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/IBaseLanguageServerExtensions.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/IBaseLanguageServerExtensions.java index 70bb5d744..50a254cdd 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/IBaseLanguageServerExtensions.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/IBaseLanguageServerExtensions.java @@ -28,9 +28,7 @@ import java.net.URI; import java.util.concurrent.CompletableFuture; - import org.eclipse.lsp4j.jsonrpc.messages.Tuple.Two; -import org.eclipse.lsp4j.jsonrpc.services.JsonDelegate; import org.eclipse.lsp4j.jsonrpc.services.JsonNotification; import org.eclipse.lsp4j.jsonrpc.services.JsonRequest; import org.eclipse.lsp4j.services.LanguageServer; @@ -65,7 +63,4 @@ default CompletableFuture[]> supplyPathConfig(PathConfigParam @JsonNotification("rascal/logLevel") void setMinimumLogLevel(String level); - @JsonDelegate - IBaseTextDocumentService getIBaseTextDocumentService(); - } diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/ActualRoutingLanguageServer.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/ActualRoutingLanguageServer.java index 0a774e817..14a006433 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/ActualRoutingLanguageServer.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/ActualRoutingLanguageServer.java @@ -61,6 +61,7 @@ import org.checkerframework.checker.nullness.qual.Nullable; import org.eclipse.lsp4j.InitializeParams; import org.eclipse.lsp4j.InitializeResult; +import org.eclipse.lsp4j.WorkDoneProgressCancelParams; import org.eclipse.lsp4j.jsonrpc.Launcher; import org.eclipse.lsp4j.jsonrpc.ResponseErrorException; import org.eclipse.lsp4j.jsonrpc.messages.ResponseError; @@ -441,4 +442,10 @@ public void exit() { } } + @Override + public void cancelProgress(WorkDoneProgressCancelParams params) { + // Forward to everyone + allRoutes().forEach(r -> r.thenAccept(s -> s.cancelProgress(params))); + } + } diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingTextDocumentService.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingTextDocumentService.java index 70dfc7383..428490d8f 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingTextDocumentService.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingTextDocumentService.java @@ -88,6 +88,8 @@ import org.eclipse.lsp4j.jsonrpc.messages.Either; import org.eclipse.lsp4j.jsonrpc.messages.Either3; import org.eclipse.lsp4j.services.LanguageClient; +import org.eclipse.lsp4j.services.LanguageServer; +import org.eclipse.lsp4j.services.TextDocumentService; import org.rascalmpl.util.locations.ColumnMaps; import org.rascalmpl.util.locations.LineColumnOffsetMap; import org.rascalmpl.vscode.lsp.BaseWorkspaceService; @@ -105,7 +107,7 @@ /** * A language-parametric text document service that routes incoming requests to remote dedicated language servers. */ -public class RoutingTextDocumentService implements IBaseTextDocumentService, DocumentRouter> { +public class RoutingTextDocumentService implements IBaseTextDocumentService, DocumentRouter> { private static final Logger logger = LogManager.getLogger(RoutingTextDocumentService.class); @@ -125,20 +127,20 @@ private DocumentRouter> availab } @Override - public Collection> allRoutes() { + public Collection> allRoutes() { return availableServerRouter().allRoutes().stream() - .map(server -> server.thenApply(IBaseLanguageServerExtensions::getIBaseTextDocumentService)) + .map(server -> server.thenApply(LanguageServer::getTextDocumentService)) .collect(Collectors.toList()); } @Override - public CompletableFuture route(ISourceLocation loc) { - return availableServerRouter().route(loc).thenApply(IBaseLanguageServerExtensions::getIBaseTextDocumentService); + public CompletableFuture route(ISourceLocation loc) { + return availableServerRouter().route(loc).thenApply(LanguageServer::getTextDocumentService); } @Override - public CompletableFuture route(String language) { - return availableServerRouter().route(language).thenApply(IBaseLanguageServerExtensions::getIBaseTextDocumentService); + public CompletableFuture route(String language) { + return availableServerRouter().route(language).thenApply(LanguageServer::getTextDocumentService); } private LanguageClient availableClient() { @@ -210,16 +212,17 @@ public void unregisterLanguage(LanguageParameter lang) { @Override public void projectAdded(String name, ISourceLocation projectRoot) { - for (var r : allRoutes()) { - r.thenAccept(s -> s.projectAdded(name, projectRoot)); - } + // Nothing to do here } @Override public void projectRemoved(String name, ISourceLocation projectRoot) { - for (var r : allRoutes()) { - r.thenAccept(s -> s.projectRemoved(name, projectRoot)); - } + // Nothing to do here + } + + @Override + public void cancelProgress(String progressId) { + // Nothing to do here } @Override @@ -266,14 +269,6 @@ public void didDeleteFiles(DeleteFilesParams params) { // TODO Mimick VS given certain file operation filters (capabilities) } - @Override - public void cancelProgress(String progressId) { - // Note: floating futures - for (var r : allRoutes()) { - r.thenAccept(s -> s.cancelProgress(progressId)); - } - } - @Override public CompletableFuture> callHierarchyIncomingCalls( CallHierarchyIncomingCallsParams params) { From 9bef253a72028d93c803b02b4a2e873b025ee4f2 Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Thu, 14 May 2026 15:43:58 +0200 Subject: [PATCH 076/107] Restore static registration of dynamic capabilities. --- .../vscode/lsp/parametric/ParametricTextDocumentService.java | 1 + 1 file changed, 1 insertion(+) diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricTextDocumentService.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricTextDocumentService.java index 986840aea..6df0cdbd1 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricTextDocumentService.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricTextDocumentService.java @@ -234,6 +234,7 @@ public void initializeServerCapabilities(ClientCapabilities clientCapabilities, , /* new FileOperationCapability.DidCreateFiles(exec), */ new FileOperationCapability.DidRenameFiles(exec), new FileOperationCapability.DidDeleteFiles(exec) ); setStaticServerCapabilities(dedicatedLanguageName, result); + dynamicCapabilities.registerStaticCapabilities(result); } public static void setStaticServerCapabilities(ServerCapabilities result) { From 99cc70a727572cf8f3abb71c4c8886ea71e66c4a Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Thu, 14 May 2026 16:09:39 +0200 Subject: [PATCH 077/107] Wrap missing language exceptions in futures. --- .../routing/ActualRoutingLanguageServer.java | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/ActualRoutingLanguageServer.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/ActualRoutingLanguageServer.java index 14a006433..7e0611a63 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/ActualRoutingLanguageServer.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/ActualRoutingLanguageServer.java @@ -107,11 +107,14 @@ public ActualRoutingLanguageServer(Runnable onExit, ExecutorService exec) { @Override public CompletableFuture route(String lang) { - var service = languageServers.get(lang); - if (service == null) { - throw new UnsupportedOperationException(String.format("Rascal Parametric LSP has no support for this file, since no language is registered with name '%s'", lang)); - } - return service; + return CompletableFutureUtils.completedFuture(languageServers.get(lang), getExecutor()) + .thenApply(service -> { + if (service == null) { + throw new UnsupportedOperationException(String.format("Rascal Parametric LSP has no support for this file, since no language is registered with name '%s'", lang)); + } + return service; + }) + .thenCompose(Function.identity()); } @Override @@ -122,9 +125,10 @@ public Collection> allRoutes() @Override public CompletableFuture route(ISourceLocation file) { var ext = extension(file); - return safeLanguage(ext).map(this::route).orElseThrow(() -> { - throw new UnsupportedOperationException(String.format("Rascal Parametric LSP has no support for this file, since no language is registered with extension '%s'", ext)); - }); + return CompletableFutureUtils.completedFuture(ext, getExecutor()) + .thenApply(this::safeLanguage) + .thenApply(lang -> lang.orElseThrow(() -> new UnsupportedOperationException(String.format("Rascal Parametric LSP has no support for this file, since no language is registered with extension '%s'", ext)))) + .thenCompose(this::route); } @Override From d7041277fe612a1495ff4da461148c3dd396cd6e Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Thu, 14 May 2026 16:16:56 +0200 Subject: [PATCH 078/107] Clean up child processes on exit. --- .../routing/ActualRoutingLanguageServer.java | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/ActualRoutingLanguageServer.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/ActualRoutingLanguageServer.java index 7e0611a63..dc114eaad 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/ActualRoutingLanguageServer.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/ActualRoutingLanguageServer.java @@ -46,6 +46,7 @@ import java.util.concurrent.CancellationException; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.TimeUnit; @@ -88,12 +89,14 @@ public class ActualRoutingLanguageServer extends BaseLanguageServer.ActualLangua private static final Logger logger = LogManager.getLogger(ActualRoutingLanguageServer.class); - private final Map languagesByExtension = new ConcurrentHashMap<>(); + // NOTE // 1. This map should only contains running server processes. // 2. Upon removal from this map, the process should be killed to avoid resource leaks. // NOTE To be able to route to arbitrary third-party language servers, remote servers should implement `LanguageServer` (instead of `IBaseLanguageServerExtensions`) private final Map> languageServers = new ConcurrentHashMap<>(); + private final Map languagesByExtension = new ConcurrentHashMap<>(); + private final Collection childProcesses = new CopyOnWriteArrayList<>(); private final MultipleClientProxy client = new MultipleClientProxy(); private @MonotonicNonNull InitializeParams initializeParams; @@ -103,6 +106,7 @@ public class ActualRoutingLanguageServer extends BaseLanguageServer.ActualLangua public ActualRoutingLanguageServer(Runnable onExit, ExecutorService exec) { super(onExit, exec, new RoutingTextDocumentService(), new RoutingWorkspaceService(exec)); + Runtime.getRuntime().addShutdownHook(new Thread(() -> childProcesses.forEach(Process::destroy))); } @Override @@ -225,6 +229,9 @@ private static String classPath(LanguageParameter lang) throws IOException { .redirectError(Redirect.INHERIT) // Show logs in current process .start(); + childProcesses.add(proc); + proc.onExit().thenAcceptAsync(p -> childProcesses.remove(p), getExecutor()); + logger.debug("Launched language server on process {}", proc.pid()); return Triple.of(proc.getInputStream(), proc.getOutputStream(), () -> {}); } catch (IOException e) { @@ -311,7 +318,7 @@ public void write(JsonWriter writer, ProxiedIValue proxiedValue) throws IOExcept getExecutor().execute(() -> { try { runner.get(); - logger.info("Language server for {} terminated gracefully", lang.getName()); + logger.info("Language server for {} terminated", lang.getName()); } catch (CancellationException | ExecutionException e) { logger.error("Language server for {} terminated with an exception", lang.getName(), e); } catch (InterruptedException e) { From 94332082e570714bd1d8f6930343c7cea6de873c Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Thu, 14 May 2026 16:17:17 +0200 Subject: [PATCH 079/107] Notes & docs. --- .../routing/ActualRoutingLanguageServer.java | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/ActualRoutingLanguageServer.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/ActualRoutingLanguageServer.java index dc114eaad..137873a25 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/ActualRoutingLanguageServer.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/ActualRoutingLanguageServer.java @@ -264,13 +264,12 @@ private static String classPath(LanguageParameter lang) throws IOException { /** * Special GSON configuration that (un)wraps IValues as-is. * - * Encoding and decoding an IValues loses type information, we cannot properly encode a decoded value again. - * `encode(decode(encode(v))) != v` + * Encoding and decoding an {@link IValue} loses dynamic type information, hance a decoded value can not be encoded properly again. + * `encode(decode(encode(v))) != encode(v)` * Since the router should just proxy values passed from remote servers, without changing them, it uses a special encoder/decoder. * */ private static void configureProxyGson(GsonBuilder builder) { - // Special encoding/decoding of IValues to/from a string builder.registerTypeAdapter(ProxiedIValue.class, new TypeAdapter() { @Override @@ -286,7 +285,7 @@ public void write(JsonWriter writer, ProxiedIValue proxiedValue) throws IOExcept }); // If support for creating (instead of forwaring) IValues in the routing server is required, - // register JSON encoding (but not decoding) for regular (non-rpoxy) IValues here + // register JSON encoding (but not decoding) for regular (non-proxy) IValues here. } private @Nullable CompletableFuture startServer(LanguageParameter lang) { @@ -313,7 +312,8 @@ public void write(JsonWriter writer, ProxiedIValue proxiedValue) throws IOExcept var initializedServer = CompletableFutureUtils.completedFuture(delegateInitializationParams(), getExecutor()) .thenCompose(server::initialize) - .thenApply(ignored -> server); // ignore initialized static server capabilities, since ours are the same + // TODO Handle static server capabilities that are different than ours (because the remote has a different Rascal-LSP version) + .thenApply(ignored -> server); getExecutor().execute(() -> { try { @@ -414,6 +414,7 @@ public synchronized CompletableFuture sendUnregisterLanguage(LanguageParam var work = route(lang.getName()) .thenCompose(s -> s.sendUnregisterLanguage(lang)); + // Note: this should be handled for the deployed scenario by the process onExit hook. boolean removeAll = lang.getMainModule() == null || lang.getMainModule().isEmpty(); if (removeAll) { // clear the whole language From 9ce67cd4d1bb0348e073df6e4a3b5620a53ab6b3 Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Fri, 15 May 2026 11:43:35 +0200 Subject: [PATCH 080/107] Re-use language server start logic. --- .../vscode/lsp/BaseLanguageServer.java | 19 +++++---- .../routing/ActualRoutingLanguageServer.java | 6 ++- .../routing/RoutingLanguageServer.java | 39 ++++--------------- .../routing/RoutingTextDocumentService.java | 4 ++ .../lsp/rascal/RascalTextDocumentService.java | 1 - 5 files changed, 27 insertions(+), 42 deletions(-) diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/BaseLanguageServer.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/BaseLanguageServer.java index a5c679177..22ea406d1 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/BaseLanguageServer.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/BaseLanguageServer.java @@ -133,8 +133,17 @@ protected static void printClassPath() { logger.trace("Started with classpath: {}", () -> System.getProperty("java.class.path")); } + @FunctionalInterface + protected interface ServerConstructor { + E apply(A a, B b, C c, D d); + } + + protected static void startLanguageServer(String requestPoolName, String workerPoolName, Function docServiceProvider, Function workspaceServiceProvider, int portNumber) { + startLanguageServer(ActualLanguageServer::new, requestPoolName, workerPoolName, docServiceProvider, workspaceServiceProvider, portNumber); + } + @SuppressWarnings({"java:S2189", "java:S106"}) - public static void startLanguageServer(String requestPoolName, String workerPoolName, Function docServiceProvider, Function workspaceServiceProvider, int portNumber) { + protected static void startLanguageServer(ServerConstructor serverBuilder, String requestPoolName, String workerPoolName, Function docServiceProvider, Function workspaceServiceProvider, int portNumber) { logger.info("Starting Rascal Language Server: {}", getVersion()); printClassPath(); @@ -145,9 +154,7 @@ public static void startLanguageServer(String requestPoolName, String workerPool try { var docService = docServiceProvider.apply(workerPool); var wsService = workspaceServiceProvider.apply(workerPool); - docService.pair(wsService); - wsService.pair(docService); - startLSP(constructLSPClient(capturedIn, capturedOut, new ActualLanguageServer(() -> System.exit(0), workerPool, docService, wsService), requestPool)); + startLSP(constructLSPClient(capturedIn, capturedOut, serverBuilder.apply(() -> System.exit(0), workerPool, docService, wsService), requestPool)); } finally { requestPool.shutdown(); workerPool.shutdown(); @@ -164,9 +171,7 @@ public static void startLanguageServer(String requestPoolName, String workerPool logger.info("New client connected to Rascal LSP server (listening on port number: {})", portNumber); var docService = docServiceProvider.apply(workerPool); var wsService = workspaceServiceProvider.apply(workerPool); - docService.pair(wsService); - wsService.pair(docService); - startLSP(constructLSPClient(clientSocket, new ActualLanguageServer(() -> {}, workerPool, docService, wsService), requestPool)); + startLSP(constructLSPClient(clientSocket, serverBuilder.apply(() -> {}, workerPool, docService, wsService), requestPool)); } finally { requestPool.shutdown(); diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/ActualRoutingLanguageServer.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/ActualRoutingLanguageServer.java index 137873a25..8cac1179b 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/ActualRoutingLanguageServer.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/ActualRoutingLanguageServer.java @@ -73,7 +73,9 @@ import org.rascalmpl.uri.URIUtil; import org.rascalmpl.util.maven.MavenSettings; import org.rascalmpl.vscode.lsp.BaseLanguageServer; +import org.rascalmpl.vscode.lsp.BaseWorkspaceService; import org.rascalmpl.vscode.lsp.IBaseLanguageServerExtensions; +import org.rascalmpl.vscode.lsp.IBaseTextDocumentService; import org.rascalmpl.vscode.lsp.parametric.LanguageRegistry.LanguageParameter; import org.rascalmpl.vscode.lsp.util.DocumentRouter; import org.rascalmpl.vscode.lsp.util.concurrent.CompletableFutureUtils; @@ -104,8 +106,8 @@ public class ActualRoutingLanguageServer extends BaseLanguageServer.ActualLangua private static final int REMOTE_BASE_PORT = 9990; private AtomicInteger remotePortOffset = new AtomicInteger(0); - public ActualRoutingLanguageServer(Runnable onExit, ExecutorService exec) { - super(onExit, exec, new RoutingTextDocumentService(), new RoutingWorkspaceService(exec)); + public ActualRoutingLanguageServer(Runnable onExit, ExecutorService exec, IBaseTextDocumentService lspDocumentService, BaseWorkspaceService lspWorkspaceService) { + super(onExit, exec, lspDocumentService, lspWorkspaceService); Runtime.getRuntime().addShutdownHook(new Thread(() -> childProcesses.forEach(Process::destroy))); } diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingLanguageServer.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingLanguageServer.java index 2ec43889d..6bf46cfc5 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingLanguageServer.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingLanguageServer.java @@ -26,14 +26,7 @@ */ package org.rascalmpl.vscode.lsp.parametric.routing; -import java.io.IOException; -import java.net.InetAddress; -import java.net.ServerSocket; -import java.util.concurrent.ExecutorService; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; import org.rascalmpl.vscode.lsp.parametric.ParametricLanguageServer; -import org.rascalmpl.vscode.lsp.util.NamedThreadPool; /** * A language-parametric server that assigns a dedicated server to each language. @@ -41,37 +34,19 @@ */ public class RoutingLanguageServer extends ParametricLanguageServer { - private static final Logger logger = LogManager.getLogger(RoutingLanguageServer.class); - - @SuppressWarnings("java:S2189") // endless loop is fine for the development server - public static void startLanguageServer(ExecutorService requestPool, ExecutorService workerPool, int portNumber) { - logger.info("Starting Rascal Language Server Router: {}", getVersion()); - printClassPath(); - - if (DEPLOY_MODE) { - startLSP(constructLSPClient(capturedIn, capturedOut, new ActualRoutingLanguageServer(() -> System.exit(0), workerPool), requestPool)); - } - else { - try (ServerSocket serverSocket = new ServerSocket(portNumber, 0, InetAddress.getByName("127.0.0.1"))) { - logger.info("Rascal LSP server router listens on port number: {}", portNumber); - while (true) { - startLSP(constructLSPClient(serverSocket.accept(), new ActualRoutingLanguageServer(() -> {}, workerPool), requestPool)); - } - } catch (IOException e) { - logger.fatal("Failure to start TCP server on port {}", portNumber, e); - } - } - } - public static void main(String[] args) { var serverArgs = parseArgs(args); if (serverArgs.getDedicatedLanguage() != null) { // If we get a dedicated language argument, we just start a single parametric server startParametric(serverArgs); } else { - startLanguageServer(NamedThreadPool.single("parametric-lsp-router") - , NamedThreadPool.cached("parametric-router") - , serverArgs.getPort() + startLanguageServer( + ActualRoutingLanguageServer::new + , "parametric-lsp-router" + , "parametric-router" + , RoutingTextDocumentService::new + , RoutingWorkspaceService::new + , serverArgs.getPort() ); } } diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingTextDocumentService.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingTextDocumentService.java index 428490d8f..6da026ba2 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingTextDocumentService.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingTextDocumentService.java @@ -29,6 +29,7 @@ import java.util.Collection; import java.util.List; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutorService; import java.util.stream.Collectors; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -114,6 +115,9 @@ public class RoutingTextDocumentService implements IBaseTextDocumentService, Doc private @MonotonicNonNull LanguageClient client; private @MonotonicNonNull DocumentRouter> serverRouter; + @SuppressWarnings("unused") + /*package*/ RoutingTextDocumentService(ExecutorService exec) {} + /*package*/ void setServerRouter(DocumentRouter> serverRouter) { this.serverRouter = serverRouter; } diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/RascalTextDocumentService.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/RascalTextDocumentService.java index 03f332aa6..cb692868e 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/RascalTextDocumentService.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/RascalTextDocumentService.java @@ -157,7 +157,6 @@ public RascalTextDocumentService(ExecutorService exec) { FallbackResolver.getInstance().registerTextDocumentService(this); } - private LanguageClient availableClient() { if (client == null) { throw new IllegalStateException("Client has not been connected yet"); From 6968b4ae85c953995d39bc8481e209f6d069d97f Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Fri, 15 May 2026 17:10:54 +0200 Subject: [PATCH 081/107] Forwards logs properly. --- .../vscode/lsp/BaseLanguageServer.java | 4 +- .../routing/ActualRoutingLanguageServer.java | 51 +++++++++++++++++-- 2 files changed, 50 insertions(+), 5 deletions(-) diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/BaseLanguageServer.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/BaseLanguageServer.java index 22ea406d1..2f32acaff 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/BaseLanguageServer.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/BaseLanguageServer.java @@ -80,8 +80,8 @@ */ @SuppressWarnings("java:S106") // we are using system.in/system.out correctly in this class public abstract class BaseLanguageServer { - protected static final PrintStream capturedOut; - protected static final InputStream capturedIn; + private static final PrintStream capturedOut; + private static final InputStream capturedIn; public static final boolean DEPLOY_MODE; private static final String LOG_CONFIGURATION_KEY = "log4j2.configurationFactory"; diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/ActualRoutingLanguageServer.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/ActualRoutingLanguageServer.java index 8cac1179b..75e4af9a8 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/ActualRoutingLanguageServer.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/ActualRoutingLanguageServer.java @@ -26,15 +26,21 @@ */ package org.rascalmpl.vscode.lsp.parametric.routing; +import com.google.gson.Gson; import com.google.gson.GsonBuilder; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import com.google.gson.JsonSyntaxException; import com.google.gson.TypeAdapter; import com.google.gson.stream.JsonReader; import com.google.gson.stream.JsonWriter; +import java.io.BufferedReader; import java.io.File; import java.io.IOException; import java.io.InputStream; +import java.io.InputStreamReader; import java.io.OutputStream; -import java.lang.ProcessBuilder.Redirect; +import java.io.PrintWriter; import java.net.InetAddress; import java.net.Socket; import java.net.URI; @@ -91,6 +97,7 @@ public class ActualRoutingLanguageServer extends BaseLanguageServer.ActualLangua private static final Logger logger = LogManager.getLogger(ActualRoutingLanguageServer.class); + private final Gson gson = new Gson(); // NOTE // 1. This map should only contains running server processes. @@ -102,12 +109,15 @@ public class ActualRoutingLanguageServer extends BaseLanguageServer.ActualLangua private final MultipleClientProxy client = new MultipleClientProxy(); private @MonotonicNonNull InitializeParams initializeParams; + private final JsonWriter logForwarder; private static final int REMOTE_BASE_PORT = 9990; private AtomicInteger remotePortOffset = new AtomicInteger(0); public ActualRoutingLanguageServer(Runnable onExit, ExecutorService exec, IBaseTextDocumentService lspDocumentService, BaseWorkspaceService lspWorkspaceService) { super(onExit, exec, lspDocumentService, lspWorkspaceService); + logForwarder = new JsonWriter(new PrintWriter(System.out, false)); + Runtime.getRuntime().addShutdownHook(new Thread(() -> childProcesses.forEach(Process::destroy))); } @@ -228,11 +238,46 @@ private static String classPath(LanguageParameter lang) throws IOException { , "--exitWhenEmpty" // , new GsonBuilder().create().toJson(lang, LanguageParameter.class).replace("\"", "\\\"") // escape JSON string on command line ) - .redirectError(Redirect.INHERIT) // Show logs in current process .start(); childProcesses.add(proc); - proc.onExit().thenAcceptAsync(p -> childProcesses.remove(p), getExecutor()); + + // Pipe logs from error stream + getExecutor().execute(() -> { + var reader = new BufferedReader(new InputStreamReader(proc.getErrorStream())); + var line = ""; + var threadNamePrefix = lang.getName() + ": "; + while (proc.isAlive()) { + synchronized (System.out) { // Read/write a block of JSON objects, so messages from distinct servers are not interleaved. + while (true) { + try { + line = reader.readLine(); + if (line == null) { + break; + } + var json = JsonParser.parseString(line); + try { + json = json.getAsJsonObject(); + var threadName = ((JsonObject) json).getAsJsonPrimitive("threadName").getAsString(); + ((JsonObject) json).addProperty("threadName", threadNamePrefix + threadName); + } catch (Exception e) { /* ignored */ } + gson.toJson(json, logForwarder); + logForwarder.flush(); + // One object per line; this is what log4j does as well. + System.out.println(); + } catch (JsonSyntaxException e) { + // Sometimes the child process logs non-JSON (e.g. logs while setting up the JSON logger). + // In this case, just forward the raw line. + if (!(line == null || line.isBlank())) { + System.out.println(line); + } + } catch (IOException e) { + logger.catching(e); + } + } + } + } + }); logger.debug("Launched language server on process {}", proc.pid()); return Triple.of(proc.getInputStream(), proc.getOutputStream(), () -> {}); From df575bb1042e3314ea3e9c15fa9b67bdf3317b8e Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Fri, 15 May 2026 17:11:19 +0200 Subject: [PATCH 082/107] Child process cleanup. --- .../vscode/lsp/BaseLanguageServer.java | 8 +++---- .../vscode/lsp/IBaseTextDocumentService.java | 1 - .../ParametricTextDocumentService.java | 5 ----- .../routing/ActualRoutingLanguageServer.java | 22 +++++++++++-------- .../routing/RoutingTextDocumentService.java | 5 ----- .../lsp/rascal/RascalTextDocumentService.java | 4 ---- 6 files changed, 17 insertions(+), 28 deletions(-) diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/BaseLanguageServer.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/BaseLanguageServer.java index 2f32acaff..189b84499 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/BaseLanguageServer.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/BaseLanguageServer.java @@ -286,6 +286,9 @@ public CompletableFuture sendUnregisterLanguage(LanguageParameter lang) { @Override public CompletableFuture initialize(InitializeParams params) { + // Exit when our parent process exits + executor.submit(() -> ProcessHandle.of(params.getProcessId()).ifPresent(p -> p.onExit().thenAccept(ignored -> this.exit()))); + return CompletableFuture.supplyAsync(() -> { logger.info("LSP connection started (connected to {} version {})", params.getClientInfo().getName(), params.getClientInfo().getVersion()); logger.debug("LSP client capabilities: {}", params.getCapabilities()); @@ -309,10 +312,7 @@ public void initialized(InitializedParams params) { @Override public CompletableFuture shutdown() { - return CompletableFuture.supplyAsync(() -> { - lspDocumentService.shutdown(); - return true; - }, executor); + return CompletableFuture.completedFuture(new Object()); } @Override diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/IBaseTextDocumentService.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/IBaseTextDocumentService.java index e733873d9..144b932b0 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/IBaseTextDocumentService.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/IBaseTextDocumentService.java @@ -47,7 +47,6 @@ public interface IBaseTextDocumentService extends TextDocumentService, ITextDocu static final Duration NORMAL_DEBOUNCE = Duration.ofMillis(800); void initializeServerCapabilities(ClientCapabilities clientCapabilities, ServerCapabilities result); - void shutdown(); void connect(LanguageClient client); void pair(BaseWorkspaceService workspaceService); void initialized(); diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricTextDocumentService.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricTextDocumentService.java index 6df0cdbd1..0b80f49b1 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricTextDocumentService.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricTextDocumentService.java @@ -683,10 +683,6 @@ private TextDocumentState open(TextDocumentItem doc, long timestamp) { return openFile(doc, l -> contributions(l)::parsing, timestamp, exec); } - public void shutdown() { - // Don't shutdown the thread pool; its lifecycle is managed outside this object. - } - private CompletableFuture getSemanticTokens(TextDocumentIdentifier doc) { var loc = Locations.toLoc(doc); var specialCaseHighlighting = contributions(loc).specialCaseHighlighting(); @@ -1047,7 +1043,6 @@ public synchronized void unregisterLanguage(LanguageParameter lang) { if (exitWhenEmpty && contributions.isEmpty()) { logger.debug("Shutting down; no more registered languages"); - shutdown(); System.exit(0); } diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/ActualRoutingLanguageServer.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/ActualRoutingLanguageServer.java index 75e4af9a8..6d976b346 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/ActualRoutingLanguageServer.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/ActualRoutingLanguageServer.java @@ -52,7 +52,6 @@ import java.util.concurrent.CancellationException; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.TimeUnit; @@ -105,7 +104,6 @@ public class ActualRoutingLanguageServer extends BaseLanguageServer.ActualLangua // NOTE To be able to route to arbitrary third-party language servers, remote servers should implement `LanguageServer` (instead of `IBaseLanguageServerExtensions`) private final Map> languageServers = new ConcurrentHashMap<>(); private final Map languagesByExtension = new ConcurrentHashMap<>(); - private final Collection childProcesses = new CopyOnWriteArrayList<>(); private final MultipleClientProxy client = new MultipleClientProxy(); private @MonotonicNonNull InitializeParams initializeParams; @@ -118,7 +116,15 @@ public ActualRoutingLanguageServer(Runnable onExit, ExecutorService exec, IBaseT super(onExit, exec, lspDocumentService, lspWorkspaceService); logForwarder = new JsonWriter(new PrintWriter(System.out, false)); - Runtime.getRuntime().addShutdownHook(new Thread(() -> childProcesses.forEach(Process::destroy))); + Runtime.getRuntime().addShutdownHook(new Thread(() -> destroyChildProcesses())); + } + + private static void destroyChildProcesses() { + ProcessHandle.current().children().forEach(p -> { + if (p.isAlive() && !p.destroy()) { + p.destroyForcibly(); + } + }); } @Override @@ -240,8 +246,6 @@ private static String classPath(LanguageParameter lang) throws IOException { ) .start(); - childProcesses.add(proc); - // Pipe logs from error stream getExecutor().execute(() -> { var reader = new BufferedReader(new InputStreamReader(proc.getErrorStream())); @@ -474,8 +478,7 @@ public synchronized CompletableFuture sendUnregisterLanguage(LanguageParam if (removed != null) { work = work .thenCompose(ignored -> removed) - .thenCompose(server -> server.shutdown().thenApply(o -> server)) - .thenAccept(LanguageServer::exit); + .thenCompose(server -> server.shutdown().thenAccept(ignored -> server.exit())); } } @@ -484,19 +487,20 @@ public synchronized CompletableFuture sendUnregisterLanguage(LanguageParam @Override public CompletableFuture shutdown() { - return CompletableFutureUtils.reduce(allRoutes().stream().map(serverFut -> serverFut.thenCompose(IBaseLanguageServerExtensions::shutdown)), getExecutor()) + return CompletableFutureUtils.reduce(allRoutes().stream().map(serverFut -> serverFut.thenCompose(LanguageServer::shutdown)), getExecutor()) .thenCompose(ignored -> super.shutdown()); } @Override public void exit() { try { - CompletableFutureUtils.reduce(allRoutes().stream().map(serverFut -> serverFut.thenAccept(IBaseLanguageServerExtensions::exit)), getExecutor()).get(10, TimeUnit.SECONDS); + CompletableFutureUtils.reduce(allRoutes().stream().map(serverFut -> serverFut.thenAccept(LanguageServer::exit)), getExecutor()).get(10, TimeUnit.SECONDS); } catch (ExecutionException | TimeoutException e) { logger.error("Error while exiting child processes", e); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } finally { + destroyChildProcesses(); super.exit(); } } diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingTextDocumentService.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingTextDocumentService.java index 6da026ba2..654d1487a 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingTextDocumentService.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingTextDocumentService.java @@ -183,11 +183,6 @@ public void initializeServerCapabilities(ClientCapabilities clientCapabilities, ParametricTextDocumentService.setStaticServerCapabilities(result); } - @Override - public void shutdown() { - // reserved for future use - } - @Override public void connect(LanguageClient client) { logger.debug("Connecting client {}", client); diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/RascalTextDocumentService.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/RascalTextDocumentService.java index cb692868e..89e420c7a 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/RascalTextDocumentService.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/RascalTextDocumentService.java @@ -500,10 +500,6 @@ private TextDocumentState getFile(TextDocumentIdentifier doc) { return getFile(Locations.toLoc(doc)); } - public void shutdown() { - // Don't shutdown the thread pool; its lifecycle is managed outside this object. - } - private CompletableFuture getSemanticTokens(TextDocumentIdentifier doc) { return recoverExceptions(getFile(doc).getCurrentTreeAsync(true) .thenApply(Versioned::get) From 4af6cb8e86e094ae7c33784f4696c4007f1498cb Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Mon, 18 May 2026 16:49:17 +0200 Subject: [PATCH 083/107] Remove commented code; do not use dedicated mode. --- .../lsp/parametric/routing/ActualRoutingLanguageServer.java | 1 - 1 file changed, 1 deletion(-) diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/ActualRoutingLanguageServer.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/ActualRoutingLanguageServer.java index 6d976b346..ba52c4de7 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/ActualRoutingLanguageServer.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/ActualRoutingLanguageServer.java @@ -242,7 +242,6 @@ private static String classPath(LanguageParameter lang) throws IOException { , "-cp", classPath , "org.rascalmpl.vscode.lsp.parametric.ParametricLanguageServer" , "--exitWhenEmpty" - // , new GsonBuilder().create().toJson(lang, LanguageParameter.class).replace("\"", "\\\"") // escape JSON string on command line ) .start(); From e39f31b47e59a6489959d64d8f2f3f2306da5a55 Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Tue, 19 May 2026 09:22:49 +0200 Subject: [PATCH 084/107] Fix indentation. --- .../parametric/routing/RoutingLanguageServer.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingLanguageServer.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingLanguageServer.java index 6bf46cfc5..94069a6b6 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingLanguageServer.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingLanguageServer.java @@ -41,12 +41,12 @@ public static void main(String[] args) { startParametric(serverArgs); } else { startLanguageServer( - ActualRoutingLanguageServer::new - , "parametric-lsp-router" - , "parametric-router" - , RoutingTextDocumentService::new - , RoutingWorkspaceService::new - , serverArgs.getPort() + ActualRoutingLanguageServer::new, + "parametric-lsp-router", + "parametric-router", + RoutingTextDocumentService::new, + RoutingWorkspaceService::new, + serverArgs.getPort() ); } } From e5ca190c835de321b513b374f8710fd6f81f981c Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Tue, 19 May 2026 15:15:41 +0200 Subject: [PATCH 085/107] Maven-based class path. --- .../routing/ActualRoutingLanguageServer.java | 84 +++++++++---------- 1 file changed, 40 insertions(+), 44 deletions(-) diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/ActualRoutingLanguageServer.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/ActualRoutingLanguageServer.java index ba52c4de7..6c5761815 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/ActualRoutingLanguageServer.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/ActualRoutingLanguageServer.java @@ -43,10 +43,10 @@ import java.io.PrintWriter; import java.net.InetAddress; import java.net.Socket; -import java.net.URI; import java.nio.file.Path; import java.util.Collection; import java.util.HashSet; +import java.util.LinkedList; import java.util.Map; import java.util.Optional; import java.util.concurrent.CancellationException; @@ -59,7 +59,6 @@ import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Function; import java.util.stream.Collectors; -import java.util.stream.Stream; import org.apache.commons.lang3.tuple.Triple; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -76,7 +75,10 @@ import org.eclipse.lsp4j.services.LanguageServer; import org.rascalmpl.library.util.PathConfig; import org.rascalmpl.uri.URIUtil; -import org.rascalmpl.util.maven.MavenSettings; +import org.rascalmpl.util.maven.Artifact; +import org.rascalmpl.util.maven.MavenParser; +import org.rascalmpl.util.maven.ModelResolutionError; +import org.rascalmpl.util.maven.Scope; import org.rascalmpl.vscode.lsp.BaseLanguageServer; import org.rascalmpl.vscode.lsp.BaseWorkspaceService; import org.rascalmpl.vscode.lsp.IBaseLanguageServerExtensions; @@ -177,51 +179,45 @@ public static String extension(ISourceLocation doc) { return URIUtil.getExtension(doc); } - private static Path resolveUri(URI uri) { - if (!uri.getScheme().equals("mvn")) { - return Path.of(uri); - } - - if (uri.getAuthority().isEmpty()) { - throw new IllegalArgumentException("Invalid Maven URI (no authority): " + uri.toString()); - } - - var parts = uri.getAuthority().split("--"); - if (parts.length != 3) { - throw new IllegalArgumentException("Invalid Maven URI (expected 2 '--' separators): " + uri.toString()); - } - - var group = parts[0]; - var name = parts[1]; - var version = parts[2]; - var jarPath = group.replaceAll("\\.", "/") - + "/" - + name - + "/" - + version - + "/" - + name - + "-" - + version - + ".jar" - ; + private static boolean isRascal(Artifact art) { + return "org.rascalmpl".equals(art.getCoordinate().getGroupId()) && "rascal".equals(art.getCoordinate().getArtifactId()); + } - return MavenSettings.mavenRepository().resolve(jarPath); + private static boolean isRascalLsp(Artifact art) { + return "org.rascalmpl".equals(art.getCoordinate().getGroupId()) && "rascal-lsp".equals(art.getCoordinate().getArtifactId()); } - private static String classPath(LanguageParameter lang) throws IOException { + private static String classPath(LanguageParameter lang) throws IOException, ModelResolutionError { var pcfg = PathConfig.parse(lang.getPathConfig()); - var libs = pcfg.getLibs().stream().map(ISourceLocation.class::cast); - var bins = Stream.of(pcfg.getBin()); - - var classPath = Stream.concat(bins, libs) // project target folder first - .map(Locations::toPhysicalIfPossible) - .map(Locations::toUri) - .map(ActualRoutingLanguageServer::resolveUri) - .map(Path::toString) - .collect(Collectors.toList()); + var pom = Locations.toPhysicalIfPossible(URIUtil.getChildLocation(pcfg.getProjectRoot(), "pom.xml")); + var maven = new MavenParser(Path.of(pom.getURI())); + + var project = maven.parseProject(); + var deps = project.resolveDependencies(Scope.COMPILE, maven); + + var classPath = new LinkedList(); + if (isRascalLsp(project)) { + // We add our bin directory and all of our dependencies + classPath.add(Path.of(Locations.toUri(Locations.toPhysicalIfPossible(pcfg.getBin())))); + for (var dep : deps) { + var res = dep.getResolved(); + if (res != null) { + classPath.add(res); + } + } + } else { + // Add Rascal/LSP JARs + for (var dep : deps) { + if (isRascal(dep) || isRascalLsp(dep)) { + var res = dep.getResolved(); + if (res != null) { + classPath.add(res); + } + } + } + } - return String.join(File.pathSeparator, classPath); + return String.join(File.pathSeparator, classPath.stream().map(Path::toString).collect(Collectors.toList())); } private @Nullable Triple startServerProcess(LanguageParameter lang) { @@ -284,7 +280,7 @@ private static String classPath(LanguageParameter lang) throws IOException { logger.debug("Launched language server on process {}", proc.pid()); return Triple.of(proc.getInputStream(), proc.getOutputStream(), () -> {}); - } catch (IOException e) { + } catch (IOException | ModelResolutionError e) { logger.error("Starting language server process for {} failed", lang.getName(), e); return null; } From cfee654725184f9e498a8f5bc15bad193fce1bfb Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Tue, 19 May 2026 15:16:48 +0200 Subject: [PATCH 086/107] Suppress and fix warnings. --- .../routing/ActualRoutingLanguageServer.java | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/ActualRoutingLanguageServer.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/ActualRoutingLanguageServer.java index 6c5761815..c223e0217 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/ActualRoutingLanguageServer.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/ActualRoutingLanguageServer.java @@ -28,7 +28,7 @@ import com.google.gson.Gson; import com.google.gson.GsonBuilder; -import com.google.gson.JsonObject; +import com.google.gson.JsonElement; import com.google.gson.JsonParser; import com.google.gson.JsonSyntaxException; import com.google.gson.TypeAdapter; @@ -114,6 +114,7 @@ public class ActualRoutingLanguageServer extends BaseLanguageServer.ActualLangua private static final int REMOTE_BASE_PORT = 9990; private AtomicInteger remotePortOffset = new AtomicInteger(0); + @SuppressWarnings("java:S106") // System.out public ActualRoutingLanguageServer(Runnable onExit, ExecutorService exec, IBaseTextDocumentService lspDocumentService, BaseWorkspaceService lspWorkspaceService) { super(onExit, exec, lspDocumentService, lspWorkspaceService); logForwarder = new JsonWriter(new PrintWriter(System.out, false)); @@ -220,6 +221,15 @@ private static String classPath(LanguageParameter lang) throws IOException, Mode return String.join(File.pathSeparator, classPath.stream().map(Path::toString).collect(Collectors.toList())); } + private static JsonElement prependThreadName(String threadNamePrefix, JsonElement json) { + try { + var obj = json.getAsJsonObject(); + var threadName = obj.getAsJsonPrimitive("threadName").getAsString(); + obj.addProperty("threadName", threadNamePrefix + threadName); + } catch (Exception e) { /* ignored */ } + return json; + } + private @Nullable Triple startServerProcess(LanguageParameter lang) { logger.info("Starting LSP process for {}", lang.getName()); @@ -245,7 +255,6 @@ private static String classPath(LanguageParameter lang) throws IOException, Mode getExecutor().execute(() -> { var reader = new BufferedReader(new InputStreamReader(proc.getErrorStream())); var line = ""; - var threadNamePrefix = lang.getName() + ": "; while (proc.isAlive()) { synchronized (System.out) { // Read/write a block of JSON objects, so messages from distinct servers are not interleaved. while (true) { @@ -255,12 +264,7 @@ private static String classPath(LanguageParameter lang) throws IOException, Mode break; } var json = JsonParser.parseString(line); - try { - json = json.getAsJsonObject(); - var threadName = ((JsonObject) json).getAsJsonPrimitive("threadName").getAsString(); - ((JsonObject) json).addProperty("threadName", threadNamePrefix + threadName); - } catch (Exception e) { /* ignored */ } - gson.toJson(json, logForwarder); + gson.toJson(prependThreadName(lang.getName() + " ", json), logForwarder); logForwarder.flush(); // One object per line; this is what log4j does as well. System.out.println(); From 453739cc79f777a50a0708c9ac0e50c1ccc0937f Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Tue, 19 May 2026 15:31:05 +0200 Subject: [PATCH 087/107] Reduce visibility of function. --- .../lsp/parametric/routing/ActualRoutingLanguageServer.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/ActualRoutingLanguageServer.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/ActualRoutingLanguageServer.java index c223e0217..4540b531f 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/ActualRoutingLanguageServer.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/ActualRoutingLanguageServer.java @@ -176,7 +176,7 @@ private Optional safeLanguage(String extension) { return Optional.ofNullable(languagesByExtension.get(extension)); } - public static String extension(ISourceLocation doc) { + private static String extension(ISourceLocation doc) { return URIUtil.getExtension(doc); } From c378259e9d513b2ccee15dcaeb0f0f92d791cde1 Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Wed, 20 May 2026 10:30:51 +0200 Subject: [PATCH 088/107] Wait longer for diagnostics to clear. --- rascal-vscode-extension/src/test/vscode-suite/utils.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/rascal-vscode-extension/src/test/vscode-suite/utils.ts b/rascal-vscode-extension/src/test/vscode-suite/utils.ts index 044f1dfb9..da1aa9089 100644 --- a/rascal-vscode-extension/src/test/vscode-suite/utils.ts +++ b/rascal-vscode-extension/src/test/vscode-suite/utils.ts @@ -241,8 +241,10 @@ export class IDEOperations { // There should be no more error diagnostics const bottomBar = new Workbench().getBottomBar(); const problemsView = await bottomBar.openProblemsView(); - const allVisibleMarkers = await problemsView.getAllVisibleMarkers(MarkerType.Error); - expect(allVisibleMarkers.length, "Not all error diagnostics have been cleared").to.equal(0); + await this.driver.wait(async () => { + const allVisibleMarkers = await problemsView.getAllVisibleMarkers(MarkerType.Error); + return allVisibleMarkers.length === 0; + }, Delays.normal, "Not all error diagnostics have been cleared"); } assertLineBecomes(editor: TextEditor, lineNumber: number, lineContents: string, msg: string, wait = Delays.verySlow) : Promise { From 6e8a341a1faea713d136fc4650ea21e11ea97de5 Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Wed, 20 May 2026 15:15:47 +0200 Subject: [PATCH 089/107] Ignore failures while setting debug level. --- .../src/test/vscode-suite/utils.ts | 36 +++++++++++-------- 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/rascal-vscode-extension/src/test/vscode-suite/utils.ts b/rascal-vscode-extension/src/test/vscode-suite/utils.ts index da1aa9089..e89d2d780 100644 --- a/rascal-vscode-extension/src/test/vscode-suite/utils.ts +++ b/rascal-vscode-extension/src/test/vscode-suite/utils.ts @@ -228,7 +228,27 @@ export class IDEOperations { const center = await ignoreFails(new Workbench().openNotificationsCenter()); await ignoreFails(center?.clearAllNotifications()); await ignoreFails(center?.close()); - await assureDebugLevelLoggingIsEnabled(); + await this.assureDebugLevelLoggingIsEnabled(); + } + + private async assureDebugLevelLoggingIsEnabled() { + if (alreadySetup) { + return; + } + alreadySetup = true; // to avoid doing this twice/parallel + await this.driver.wait(async () => { + try { + const prompt = await new Workbench().openCommandPrompt(); + await prompt.setText(">workbench.action.setLogLevel"); + await prompt.confirm(); + await prompt.setText("Debug"); + await prompt.confirm(); + return true; + } catch (e) { + console.error("Setting log level failed: ", e); + return false; + } + }); } async cleanup() { @@ -290,7 +310,7 @@ export class IDEOperations { try { await new Workbench().executeCommand("workbench.action.revertAndCloseActiveEditor"); } catch (ex) { - const title = ignoreFails(new TextEditor().getTitle()) ?? 'unknown'; + const title = await ignoreFails(new TextEditor().getTitle()) ?? 'unknown'; await this.screenshot(`revert of ${title} failed ` + tryCount); console.log(`Revert of ${title} failed, but we ignore it`, ex); } @@ -494,18 +514,6 @@ async function showRascalOutput(bbp: BottomBarPanel, channel: string) { let alreadySetup = false; -async function assureDebugLevelLoggingIsEnabled() { - if (alreadySetup) { - return; - } - alreadySetup = true; // to avoid doing this twice/parallel - const prompt = await new Workbench().openCommandPrompt(); - await prompt.setText(">workbench.action.setLogLevel"); - await prompt.confirm(); - await prompt.setText("Debug"); - await prompt.confirm(); -} - export function printRascalOutputOnFailure(channel: 'Language Parametric Rascal' | 'Rascal MPL') { const ZOOM_OUT_FACTOR = 5; From 2902727525e70819313d5d18767120f97aca2a97 Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Wed, 20 May 2026 15:55:48 +0200 Subject: [PATCH 090/107] Await file notifications. --- .../routing/RoutingTextDocumentService.java | 59 ++++++++++++++++--- 1 file changed, 51 insertions(+), 8 deletions(-) diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingTextDocumentService.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingTextDocumentService.java index 654d1487a..c8dec143d 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingTextDocumentService.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingTextDocumentService.java @@ -29,7 +29,10 @@ import java.util.Collection; import java.util.List; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; import java.util.stream.Collectors; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -156,26 +159,66 @@ private LanguageClient availableClient() { @Override public void didOpen(DidOpenTextDocumentParams params) { - // Note: floating future - route(params.getTextDocument()).thenAccept(s -> s.didOpen(params)); + try { + route(params.getTextDocument()).thenAccept(s -> s.didOpen(params)).get(10, TimeUnit.SECONDS); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } catch (ExecutionException e) { + logger.catching(e); + if (e.getCause() != null && e.getCause() instanceof RuntimeException) { + throw (RuntimeException) e.getCause(); + } + } catch (TimeoutException e) { + logger.error("didOpen timed out", e); + } } @Override public void didChange(DidChangeTextDocumentParams params) { - // Note: floating future - route(params.getTextDocument()).thenAccept(s -> s.didChange(params)); + try { + route(params.getTextDocument()).thenAccept(s -> s.didChange(params)).get(10, TimeUnit.SECONDS); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } catch (ExecutionException e) { + logger.catching(e); + if (e.getCause() != null && e.getCause() instanceof RuntimeException) { + throw (RuntimeException) e.getCause(); + } + } catch (TimeoutException e) { + logger.error("didChange timed out", e); + } } @Override public void didClose(DidCloseTextDocumentParams params) { - // Note: floating future - route(params.getTextDocument()).thenAccept(s -> s.didClose(params)); + try { + route(params.getTextDocument()).thenAccept(s -> s.didClose(params)).get(10, TimeUnit.SECONDS); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } catch (ExecutionException e) { + logger.catching(e); + if (e.getCause() != null && e.getCause() instanceof RuntimeException) { + throw (RuntimeException) e.getCause(); + } + } catch (TimeoutException e) { + logger.error("didClose timed out", e); + } } @Override public void didSave(DidSaveTextDocumentParams params) { - // Note: floating future - route(params.getTextDocument()).thenAccept(s -> s.didSave(params)); + try { + route(params.getTextDocument()).thenAccept(s -> s.didSave(params)).get(10, TimeUnit.SECONDS); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } catch (ExecutionException e) { + logger.catching(e); + if (e.getCause() != null && e.getCause() instanceof RuntimeException) { + throw (RuntimeException) e.getCause(); + } + } catch (TimeoutException e) { + logger.error("didSave timed out", e); + } } @Override From 33bbd3b0357d94d74829fdd75837e4349c00664f Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Wed, 20 May 2026 17:03:36 +0200 Subject: [PATCH 091/107] Fix warnings. --- .../routing/ActualRoutingLanguageServer.java | 101 +++++++++--------- 1 file changed, 52 insertions(+), 49 deletions(-) diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/ActualRoutingLanguageServer.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/ActualRoutingLanguageServer.java index 4540b531f..69217e9ba 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/ActualRoutingLanguageServer.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/ActualRoutingLanguageServer.java @@ -46,7 +46,7 @@ import java.nio.file.Path; import java.util.Collection; import java.util.HashSet; -import java.util.LinkedList; +import java.util.List; import java.util.Map; import java.util.Optional; import java.util.concurrent.CancellationException; @@ -59,6 +59,7 @@ import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Function; import java.util.stream.Collectors; +import java.util.stream.Stream; import org.apache.commons.lang3.tuple.Triple; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -85,6 +86,7 @@ import org.rascalmpl.vscode.lsp.IBaseTextDocumentService; import org.rascalmpl.vscode.lsp.parametric.LanguageRegistry.LanguageParameter; import org.rascalmpl.vscode.lsp.util.DocumentRouter; +import org.rascalmpl.vscode.lsp.util.Lists; import org.rascalmpl.vscode.lsp.util.concurrent.CompletableFutureUtils; import org.rascalmpl.vscode.lsp.util.locations.Locations; @@ -188,7 +190,7 @@ private static boolean isRascalLsp(Artifact art) { return "org.rascalmpl".equals(art.getCoordinate().getGroupId()) && "rascal-lsp".equals(art.getCoordinate().getArtifactId()); } - private static String classPath(LanguageParameter lang) throws IOException, ModelResolutionError { + private static List classPath(LanguageParameter lang) throws IOException, ModelResolutionError { var pcfg = PathConfig.parse(lang.getPathConfig()); var pom = Locations.toPhysicalIfPossible(URIUtil.getChildLocation(pcfg.getProjectRoot(), "pom.xml")); var maven = new MavenParser(Path.of(pom.getURI())); @@ -196,38 +198,61 @@ private static String classPath(LanguageParameter lang) throws IOException, Mode var project = maven.parseProject(); var deps = project.resolveDependencies(Scope.COMPILE, maven); - var classPath = new LinkedList(); if (isRascalLsp(project)) { // We add our bin directory and all of our dependencies - classPath.add(Path.of(Locations.toUri(Locations.toPhysicalIfPossible(pcfg.getBin())))); - for (var dep : deps) { - var res = dep.getResolved(); - if (res != null) { - classPath.add(res); - } - } - } else { - // Add Rascal/LSP JARs - for (var dep : deps) { - if (isRascal(dep) || isRascalLsp(dep)) { - var res = dep.getResolved(); - if (res != null) { - classPath.add(res); - } - } - } + var target = Path.of(Locations.toUri(Locations.toPhysicalIfPossible(pcfg.getBin()))); + var depPaths = deps.stream() + .flatMap(a -> { + var r = a.getResolved(); + return r == null ? Stream.of() : Stream.of(r); + }).collect(Collectors.toList()); + return Lists.union(List.of(target), depPaths); } - return String.join(File.pathSeparator, classPath.stream().map(Path::toString).collect(Collectors.toList())); + return deps.stream() + .filter(d -> isRascal(d) || isRascalLsp(d)) + .flatMap(a -> { + var r = a.getResolved(); + return r == null ? Stream.of() : Stream.of(r); + }) + .collect(Collectors.toList()); } - private static JsonElement prependThreadName(String threadNamePrefix, JsonElement json) { + private static void prependThreadName(String prefix, JsonElement json) { try { var obj = json.getAsJsonObject(); var threadName = obj.getAsJsonPrimitive("threadName").getAsString(); - obj.addProperty("threadName", threadNamePrefix + threadName); + obj.addProperty("threadName", prefix + threadName); } catch (Exception e) { /* ignored */ } - return json; + } + + @SuppressWarnings("java:S106") // System.out + private void forwardLogs(BufferedReader reader, String threadPrefix) { + synchronized (System.out) { // Read/write a block of JSON objects, so messages from distinct servers are not interleaved. + var line = ""; + while (true) { + try { + line = reader.readLine(); + if (line == null) { + break; + } + var json = JsonParser.parseString(line); + prependThreadName(threadPrefix, json); + gson.toJson(json, logForwarder); + logForwarder.flush(); + // One object per line; this is what log4j does as well. + System.out.println(); + } catch (JsonSyntaxException e) { + // Sometimes the child process logs non-JSON (e.g. logs while setting up the JSON logger). + // In this case, just forward the raw line. + if (!(line == null || line.isBlank())) { + System.out.println(line); + } + } catch (IOException e) { + logger.catching(e); + } + } + } } private @Nullable Triple startServerProcess(LanguageParameter lang) { @@ -235,7 +260,7 @@ private static JsonElement prependThreadName(String threadNamePrefix, JsonElemen // In deployment, we start a process and connect to it via input/output streams try { - var classPath = classPath(lang); + var classPath = String.join(File.pathSeparator, classPath(lang).stream().map(Path::toString).collect(Collectors.toList())); logger.debug("{} runs with class path {}", lang.getName(), classPath); var proc = new ProcessBuilder(ProcessHandle.current().info().command().orElse("java") @@ -254,31 +279,9 @@ private static JsonElement prependThreadName(String threadNamePrefix, JsonElemen // Pipe logs from error stream getExecutor().execute(() -> { var reader = new BufferedReader(new InputStreamReader(proc.getErrorStream())); - var line = ""; + var threadPrefix = lang.getName() + " "; while (proc.isAlive()) { - synchronized (System.out) { // Read/write a block of JSON objects, so messages from distinct servers are not interleaved. - while (true) { - try { - line = reader.readLine(); - if (line == null) { - break; - } - var json = JsonParser.parseString(line); - gson.toJson(prependThreadName(lang.getName() + " ", json), logForwarder); - logForwarder.flush(); - // One object per line; this is what log4j does as well. - System.out.println(); - } catch (JsonSyntaxException e) { - // Sometimes the child process logs non-JSON (e.g. logs while setting up the JSON logger). - // In this case, just forward the raw line. - if (!(line == null || line.isBlank())) { - System.out.println(line); - } - } catch (IOException e) { - logger.catching(e); - } - } - } + forwardLogs(reader, threadPrefix); } }); From 4cb1941a11c7dfe4264d8c439132f2d7a610400f Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Wed, 20 May 2026 17:32:45 +0200 Subject: [PATCH 092/107] Revert "Ignore failures while setting debug level." This reverts commit 6e8a341a1faea713d136fc4650ea21e11ea97de5. --- .../src/test/vscode-suite/utils.ts | 36 ++++++++----------- 1 file changed, 14 insertions(+), 22 deletions(-) diff --git a/rascal-vscode-extension/src/test/vscode-suite/utils.ts b/rascal-vscode-extension/src/test/vscode-suite/utils.ts index e89d2d780..da1aa9089 100644 --- a/rascal-vscode-extension/src/test/vscode-suite/utils.ts +++ b/rascal-vscode-extension/src/test/vscode-suite/utils.ts @@ -228,27 +228,7 @@ export class IDEOperations { const center = await ignoreFails(new Workbench().openNotificationsCenter()); await ignoreFails(center?.clearAllNotifications()); await ignoreFails(center?.close()); - await this.assureDebugLevelLoggingIsEnabled(); - } - - private async assureDebugLevelLoggingIsEnabled() { - if (alreadySetup) { - return; - } - alreadySetup = true; // to avoid doing this twice/parallel - await this.driver.wait(async () => { - try { - const prompt = await new Workbench().openCommandPrompt(); - await prompt.setText(">workbench.action.setLogLevel"); - await prompt.confirm(); - await prompt.setText("Debug"); - await prompt.confirm(); - return true; - } catch (e) { - console.error("Setting log level failed: ", e); - return false; - } - }); + await assureDebugLevelLoggingIsEnabled(); } async cleanup() { @@ -310,7 +290,7 @@ export class IDEOperations { try { await new Workbench().executeCommand("workbench.action.revertAndCloseActiveEditor"); } catch (ex) { - const title = await ignoreFails(new TextEditor().getTitle()) ?? 'unknown'; + const title = ignoreFails(new TextEditor().getTitle()) ?? 'unknown'; await this.screenshot(`revert of ${title} failed ` + tryCount); console.log(`Revert of ${title} failed, but we ignore it`, ex); } @@ -514,6 +494,18 @@ async function showRascalOutput(bbp: BottomBarPanel, channel: string) { let alreadySetup = false; +async function assureDebugLevelLoggingIsEnabled() { + if (alreadySetup) { + return; + } + alreadySetup = true; // to avoid doing this twice/parallel + const prompt = await new Workbench().openCommandPrompt(); + await prompt.setText(">workbench.action.setLogLevel"); + await prompt.confirm(); + await prompt.setText("Debug"); + await prompt.confirm(); +} + export function printRascalOutputOnFailure(channel: 'Language Parametric Rascal' | 'Rascal MPL') { const ZOOM_OUT_FACTOR = 5; From 59b6ac70f0b83e23438382161c90a8860ee51456 Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Wed, 20 May 2026 17:41:27 +0200 Subject: [PATCH 093/107] Revert "Child process cleanup." This reverts commit df575bb1042e3314ea3e9c15fa9b67bdf3317b8e. --- .../java/org/rascalmpl/vscode/lsp/BaseLanguageServer.java | 5 ++++- .../org/rascalmpl/vscode/lsp/IBaseTextDocumentService.java | 1 + .../vscode/lsp/parametric/ParametricTextDocumentService.java | 5 +++++ .../lsp/parametric/routing/RoutingTextDocumentService.java | 5 +++++ .../vscode/lsp/rascal/RascalTextDocumentService.java | 4 ++++ 5 files changed, 19 insertions(+), 1 deletion(-) diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/BaseLanguageServer.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/BaseLanguageServer.java index 189b84499..f6e2a30b3 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/BaseLanguageServer.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/BaseLanguageServer.java @@ -312,7 +312,10 @@ public void initialized(InitializedParams params) { @Override public CompletableFuture shutdown() { - return CompletableFuture.completedFuture(new Object()); + return CompletableFuture.supplyAsync(() -> { + lspDocumentService.shutdown(); + return true; + }, executor); } @Override diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/IBaseTextDocumentService.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/IBaseTextDocumentService.java index 144b932b0..e733873d9 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/IBaseTextDocumentService.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/IBaseTextDocumentService.java @@ -47,6 +47,7 @@ public interface IBaseTextDocumentService extends TextDocumentService, ITextDocu static final Duration NORMAL_DEBOUNCE = Duration.ofMillis(800); void initializeServerCapabilities(ClientCapabilities clientCapabilities, ServerCapabilities result); + void shutdown(); void connect(LanguageClient client); void pair(BaseWorkspaceService workspaceService); void initialized(); diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricTextDocumentService.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricTextDocumentService.java index 0b80f49b1..6df0cdbd1 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricTextDocumentService.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricTextDocumentService.java @@ -683,6 +683,10 @@ private TextDocumentState open(TextDocumentItem doc, long timestamp) { return openFile(doc, l -> contributions(l)::parsing, timestamp, exec); } + public void shutdown() { + // Don't shutdown the thread pool; its lifecycle is managed outside this object. + } + private CompletableFuture getSemanticTokens(TextDocumentIdentifier doc) { var loc = Locations.toLoc(doc); var specialCaseHighlighting = contributions(loc).specialCaseHighlighting(); @@ -1043,6 +1047,7 @@ public synchronized void unregisterLanguage(LanguageParameter lang) { if (exitWhenEmpty && contributions.isEmpty()) { logger.debug("Shutting down; no more registered languages"); + shutdown(); System.exit(0); } diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingTextDocumentService.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingTextDocumentService.java index c8dec143d..4fe2d8d1c 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingTextDocumentService.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingTextDocumentService.java @@ -226,6 +226,11 @@ public void initializeServerCapabilities(ClientCapabilities clientCapabilities, ParametricTextDocumentService.setStaticServerCapabilities(result); } + @Override + public void shutdown() { + // reserved for future use + } + @Override public void connect(LanguageClient client) { logger.debug("Connecting client {}", client); diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/RascalTextDocumentService.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/RascalTextDocumentService.java index 89e420c7a..cb692868e 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/RascalTextDocumentService.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/RascalTextDocumentService.java @@ -500,6 +500,10 @@ private TextDocumentState getFile(TextDocumentIdentifier doc) { return getFile(Locations.toLoc(doc)); } + public void shutdown() { + // Don't shutdown the thread pool; its lifecycle is managed outside this object. + } + private CompletableFuture getSemanticTokens(TextDocumentIdentifier doc) { return recoverExceptions(getFile(doc).getCurrentTreeAsync(true) .thenApply(Versioned::get) From 76436439b4947a220229c4c2e2cc51291c5fc86d Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Thu, 21 May 2026 09:39:41 +0200 Subject: [PATCH 094/107] Simplify & fix log forwarding. --- .../routing/ActualRoutingLanguageServer.java | 54 +++++++++---------- 1 file changed, 25 insertions(+), 29 deletions(-) diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/ActualRoutingLanguageServer.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/ActualRoutingLanguageServer.java index 69217e9ba..46d315f04 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/ActualRoutingLanguageServer.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/ActualRoutingLanguageServer.java @@ -227,32 +227,34 @@ private static void prependThreadName(String prefix, JsonElement json) { } @SuppressWarnings("java:S106") // System.out - private void forwardLogs(BufferedReader reader, String threadPrefix) { - synchronized (System.out) { // Read/write a block of JSON objects, so messages from distinct servers are not interleaved. + private void forwardLogs(InputStream logStream, String langName) { + getExecutor().execute(() -> { var line = ""; - while (true) { - try { - line = reader.readLine(); - if (line == null) { - break; - } - var json = JsonParser.parseString(line); - prependThreadName(threadPrefix, json); - gson.toJson(json, logForwarder); - logForwarder.flush(); - // One object per line; this is what log4j does as well. - System.out.println(); - } catch (JsonSyntaxException e) { - // Sometimes the child process logs non-JSON (e.g. logs while setting up the JSON logger). - // In this case, just forward the raw line. - if (!(line == null || line.isBlank())) { - System.out.println(line); + var threadPrefix = langName + " | "; + var reader = new BufferedReader(new InputStreamReader(logStream)); + try { + while ((line = reader.readLine()) != null) { + try { + var json = JsonParser.parseString(line); + prependThreadName(threadPrefix, json); + synchronized (System.out) { // Lock, so we can make sure our JSON is followed by a newline. + gson.toJson(json, logForwarder); + logForwarder.flush(); + // One object per line; this is what log4j does as well. + System.out.println(); + } + } catch (JsonSyntaxException e) { + // Sometimes the child process logs non-JSON (e.g. logs while setting up the JSON logger). + // In this case, just forward the raw line. + if (!(line == null || line.isBlank())) { + System.out.println(line); + } } - } catch (IOException e) { - logger.catching(e); } + } catch (IOException e) { + logger.error("Error while reading logs for {}", langName, e); } - } + }); } private @Nullable Triple startServerProcess(LanguageParameter lang) { @@ -277,13 +279,7 @@ private void forwardLogs(BufferedReader reader, String threadPrefix) { .start(); // Pipe logs from error stream - getExecutor().execute(() -> { - var reader = new BufferedReader(new InputStreamReader(proc.getErrorStream())); - var threadPrefix = lang.getName() + " "; - while (proc.isAlive()) { - forwardLogs(reader, threadPrefix); - } - }); + forwardLogs(proc.getErrorStream(), lang.getName()); logger.debug("Launched language server on process {}", proc.pid()); return Triple.of(proc.getInputStream(), proc.getOutputStream(), () -> {}); From bebb23e12b7bdacc40b15eb48a82a421c590ed91 Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Thu, 21 May 2026 11:35:20 +0200 Subject: [PATCH 095/107] Clean up logging. --- .../routing/ActualRoutingLanguageServer.java | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/ActualRoutingLanguageServer.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/ActualRoutingLanguageServer.java index 46d315f04..6e6a05d4b 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/ActualRoutingLanguageServer.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/ActualRoutingLanguageServer.java @@ -35,12 +35,13 @@ import com.google.gson.stream.JsonReader; import com.google.gson.stream.JsonWriter; import java.io.BufferedReader; +import java.io.BufferedWriter; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; -import java.io.PrintWriter; +import java.io.OutputStreamWriter; import java.net.InetAddress; import java.net.Socket; import java.nio.file.Path; @@ -119,7 +120,7 @@ public class ActualRoutingLanguageServer extends BaseLanguageServer.ActualLangua @SuppressWarnings("java:S106") // System.out public ActualRoutingLanguageServer(Runnable onExit, ExecutorService exec, IBaseTextDocumentService lspDocumentService, BaseWorkspaceService lspWorkspaceService) { super(onExit, exec, lspDocumentService, lspWorkspaceService); - logForwarder = new JsonWriter(new PrintWriter(System.out, false)); + logForwarder = new JsonWriter(new BufferedWriter(new OutputStreamWriter(System.out))); Runtime.getRuntime().addShutdownHook(new Thread(() -> destroyChildProcesses())); } @@ -222,7 +223,9 @@ private static void prependThreadName(String prefix, JsonElement json) { try { var obj = json.getAsJsonObject(); var threadName = obj.getAsJsonPrimitive("threadName").getAsString(); - obj.addProperty("threadName", prefix + threadName); + if (threadName != null) { + obj.addProperty("threadName", prefix + threadName); + } } catch (Exception e) { /* ignored */ } } @@ -246,7 +249,7 @@ private void forwardLogs(InputStream logStream, String langName) { } catch (JsonSyntaxException e) { // Sometimes the child process logs non-JSON (e.g. logs while setting up the JSON logger). // In this case, just forward the raw line. - if (!(line == null || line.isBlank())) { + if (!line.isBlank()) { System.out.println(line); } } From 91f14d7634f0ea985503c60bd9f51c67a8ba1d7b Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Thu, 21 May 2026 13:43:27 +0200 Subject: [PATCH 096/107] Log to stderr directly instead of stdout. --- .../routing/ActualRoutingLanguageServer.java | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/ActualRoutingLanguageServer.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/ActualRoutingLanguageServer.java index 6e6a05d4b..deb4cf2cb 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/ActualRoutingLanguageServer.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/ActualRoutingLanguageServer.java @@ -117,10 +117,12 @@ public class ActualRoutingLanguageServer extends BaseLanguageServer.ActualLangua private static final int REMOTE_BASE_PORT = 9990; private AtomicInteger remotePortOffset = new AtomicInteger(0); - @SuppressWarnings("java:S106") // System.out + @SuppressWarnings("java:S106") // System.err public ActualRoutingLanguageServer(Runnable onExit, ExecutorService exec, IBaseTextDocumentService lspDocumentService, BaseWorkspaceService lspWorkspaceService) { super(onExit, exec, lspDocumentService, lspWorkspaceService); - logForwarder = new JsonWriter(new BufferedWriter(new OutputStreamWriter(System.out))); + + // log4j loggers write to stderr. We wrap the same stream, so we can directly pipe log messages from our child processes to it. + logForwarder = new JsonWriter(new BufferedWriter(new OutputStreamWriter(System.err))); Runtime.getRuntime().addShutdownHook(new Thread(() -> destroyChildProcesses())); } @@ -229,7 +231,7 @@ private static void prependThreadName(String prefix, JsonElement json) { } catch (Exception e) { /* ignored */ } } - @SuppressWarnings("java:S106") // System.out + @SuppressWarnings("java:S106") // System.err private void forwardLogs(InputStream logStream, String langName) { getExecutor().execute(() -> { var line = ""; @@ -240,17 +242,19 @@ private void forwardLogs(InputStream logStream, String langName) { try { var json = JsonParser.parseString(line); prependThreadName(threadPrefix, json); - synchronized (System.out) { // Lock, so we can make sure our JSON is followed by a newline. + // Lock, so we can make sure our JSON is followed by a newline. + synchronized (System.err) { gson.toJson(json, logForwarder); logForwarder.flush(); // One object per line; this is what log4j does as well. - System.out.println(); + System.err.println(); } } catch (JsonSyntaxException e) { // Sometimes the child process logs non-JSON (e.g. logs while setting up the JSON logger). // In this case, just forward the raw line. if (!line.isBlank()) { - System.out.println(line); + // No need to lock, since `println` takes care of that. + System.err.println(line); } } } From fea7a106c2bb3b449c1393cea6793d289a53c8c4 Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Thu, 21 May 2026 13:44:11 +0200 Subject: [PATCH 097/107] Start servers with the same log level. --- .../vscode/lsp/log/LogJsonConfiguration.java | 11 ++++++++--- .../vscode/lsp/log/LogRedirectConfiguration.java | 5 +---- .../routing/ActualRoutingLanguageServer.java | 3 ++- 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/log/LogJsonConfiguration.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/log/LogJsonConfiguration.java index d25a1eeb6..7e5d4a9c1 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/log/LogJsonConfiguration.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/log/LogJsonConfiguration.java @@ -63,11 +63,16 @@ public Configuration getConfiguration(LoggerContext loggerContext, Configuration return buildConfiguration(); } - private Configuration buildConfiguration() { - Level targetLevel = Level.getLevel(System.getProperty("log4j2.level", "INFO")); + public static Level getLogLevel() { + var targetLevel = Level.getLevel(System.getProperty("log4j2.level", "INFO")); if (targetLevel == null) { - targetLevel = Level.INFO; + return Level.INFO; } + return targetLevel; + } + + private Configuration buildConfiguration() { + Level targetLevel = getLogLevel(); ConfigurationBuilder builder = ConfigurationBuilderFactory.newConfigurationBuilder(); builder.setConfigurationName("JsonLogger"); diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/log/LogRedirectConfiguration.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/log/LogRedirectConfiguration.java index 0cbd80ce1..ff46c5b75 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/log/LogRedirectConfiguration.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/log/LogRedirectConfiguration.java @@ -63,10 +63,7 @@ public Configuration getConfiguration(LoggerContext loggerContext, Configuration } private static Configuration buildRedirectConfig() { - Level targetLevel = Level.getLevel(System.getProperty("log4j2.level", "INFO")); - if (targetLevel == null) { - targetLevel = Level.INFO; - } + Level targetLevel = LogJsonConfiguration.getLogLevel(); ConfigurationBuilder builder = ConfigurationBuilderFactory.newConfigurationBuilder(); builder.setConfigurationName("DefaultLogger"); diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/ActualRoutingLanguageServer.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/ActualRoutingLanguageServer.java index deb4cf2cb..7c7bd1799 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/ActualRoutingLanguageServer.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/ActualRoutingLanguageServer.java @@ -85,6 +85,7 @@ import org.rascalmpl.vscode.lsp.BaseWorkspaceService; import org.rascalmpl.vscode.lsp.IBaseLanguageServerExtensions; import org.rascalmpl.vscode.lsp.IBaseTextDocumentService; +import org.rascalmpl.vscode.lsp.log.LogJsonConfiguration; import org.rascalmpl.vscode.lsp.parametric.LanguageRegistry.LanguageParameter; import org.rascalmpl.vscode.lsp.util.DocumentRouter; import org.rascalmpl.vscode.lsp.util.Lists; @@ -274,7 +275,7 @@ private void forwardLogs(InputStream logStream, String langName) { var proc = new ProcessBuilder(ProcessHandle.current().info().command().orElse("java") , "-Dlog4j2.configurationFactory=org.rascalmpl.vscode.lsp.log.LogJsonConfiguration" - , "-Dlog4j2.level=DEBUG" + , "-Dlog4j2.level=" + LogJsonConfiguration.getLogLevel() , "-Drascal.fallbackResolver=org.rascalmpl.vscode.lsp.uri.FallbackResolver" , "-Drascal.lsp.deploy=true" , "-Drascal.compilerClasspath=" + classPath From a28792eb1540e6b2c960b8820fbedda313b23308 Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Thu, 21 May 2026 13:45:00 +0200 Subject: [PATCH 098/107] Rewrite filtering of dependencies. --- .../routing/ActualRoutingLanguageServer.java | 32 ++++++++----------- 1 file changed, 14 insertions(+), 18 deletions(-) diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/ActualRoutingLanguageServer.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/ActualRoutingLanguageServer.java index 7c7bd1799..95638c9a5 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/ActualRoutingLanguageServer.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/ActualRoutingLanguageServer.java @@ -49,6 +49,7 @@ import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Optional; import java.util.concurrent.CancellationException; import java.util.concurrent.CompletableFuture; @@ -60,11 +61,11 @@ import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Function; import java.util.stream.Collectors; -import java.util.stream.Stream; import org.apache.commons.lang3.tuple.Triple; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; +import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; import org.eclipse.lsp4j.InitializeParams; import org.eclipse.lsp4j.InitializeResult; @@ -138,14 +139,11 @@ private static void destroyChildProcesses() { @Override public CompletableFuture route(String lang) { - return CompletableFutureUtils.completedFuture(languageServers.get(lang), getExecutor()) - .thenApply(service -> { - if (service == null) { - throw new UnsupportedOperationException(String.format("Rascal Parametric LSP has no support for this file, since no language is registered with name '%s'", lang)); - } - return service; - }) - .thenCompose(Function.identity()); + var service = languageServers.get(lang); + if (service == null) { + return CompletableFuture.failedFuture(new UnsupportedOperationException(String.format("Rascal Parametric LSP has no support for this file, since no language is registered with name '%s'", lang))); + } + return service; } @Override @@ -203,22 +201,20 @@ private static List classPath(LanguageParameter lang) throws IOException, var deps = project.resolveDependencies(Scope.COMPILE, maven); if (isRascalLsp(project)) { - // We add our bin directory and all of our dependencies + // When loading a language server within the Rasal LSP project (e.g. in tests), we do not have a dependency on/JAR of LSP. + // Instead, we use its compiled classes and the JARs of all its dependencies. var target = Path.of(Locations.toUri(Locations.toPhysicalIfPossible(pcfg.getBin()))); var depPaths = deps.stream() - .flatMap(a -> { - var r = a.getResolved(); - return r == null ? Stream.of() : Stream.of(r); - }).collect(Collectors.toList()); + .map((Function) Artifact::getResolved) + .filter(Objects::nonNull) + .collect(Collectors.<@NonNull Path>toList()); return Lists.union(List.of(target), depPaths); } return deps.stream() .filter(d -> isRascal(d) || isRascalLsp(d)) - .flatMap(a -> { - var r = a.getResolved(); - return r == null ? Stream.of() : Stream.of(r); - }) + .map((Function) Artifact::getResolved) + .filter(Objects::nonNull) .collect(Collectors.toList()); } From 391e67b41dfa8dd8d857dd8bc8248f63268ce7a2 Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Thu, 21 May 2026 13:46:16 +0200 Subject: [PATCH 099/107] Cleanup of resources. --- .../routing/ActualRoutingLanguageServer.java | 43 +++++++++++++------ 1 file changed, 31 insertions(+), 12 deletions(-) diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/ActualRoutingLanguageServer.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/ActualRoutingLanguageServer.java index 95638c9a5..da07213a2 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/ActualRoutingLanguageServer.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/ActualRoutingLanguageServer.java @@ -131,8 +131,12 @@ public ActualRoutingLanguageServer(Runnable onExit, ExecutorService exec, IBaseT private static void destroyChildProcesses() { ProcessHandle.current().children().forEach(p -> { - if (p.isAlive() && !p.destroy()) { - p.destroyForcibly(); + try { + if (p.isAlive() && !p.destroy()) { + p.destroyForcibly(); + } + } catch (Exception e) { + logger.error("Error while destroying process {}", p.pid(), e); } }); } @@ -231,10 +235,9 @@ private static void prependThreadName(String prefix, JsonElement json) { @SuppressWarnings("java:S106") // System.err private void forwardLogs(InputStream logStream, String langName) { getExecutor().execute(() -> { - var line = ""; var threadPrefix = langName + " | "; - var reader = new BufferedReader(new InputStreamReader(logStream)); - try { + try (var reader = new BufferedReader(new InputStreamReader(logStream))) { + String line; while ((line = reader.readLine()) != null) { try { var json = JsonParser.parseString(line); @@ -384,11 +387,12 @@ public void write(JsonWriter writer, ProxiedIValue proxiedValue) throws IOExcept } } } - } - try { - serverParams.getRight().run(); - } catch (Exception e) { - logger.error("Unexpected error while cleaning up connection to language server for {}", lang.getName(), e); + try { + // Run exit hook + serverParams.getRight().run(); + } catch (Exception e) { + logger.error("Unexpected error while cleaning up connection to language server for {}", lang.getName(), e); + } } }); @@ -490,13 +494,28 @@ public synchronized CompletableFuture sendUnregisterLanguage(LanguageParam @Override public CompletableFuture shutdown() { return CompletableFutureUtils.reduce(allRoutes().stream().map(serverFut -> serverFut.thenCompose(LanguageServer::shutdown)), getExecutor()) - .thenCompose(ignored -> super.shutdown()); + .thenCompose(ignored -> super.shutdown()) + .whenComplete((v, t) -> { + try { + logForwarder.flush(); + } catch (IOException e) { + logger.catching(e); + } + }); } @Override public void exit() { try { - CompletableFutureUtils.reduce(allRoutes().stream().map(serverFut -> serverFut.thenAccept(LanguageServer::exit)), getExecutor()).get(10, TimeUnit.SECONDS); + CompletableFutureUtils.reduce(allRoutes().stream().map(serverFut -> serverFut.thenAccept(LanguageServer::exit)), getExecutor()) + .whenComplete((v, t) -> { + try { + logForwarder.close(); + } catch (IOException e) { + logger.catching(e); + } + }) + .get(10, TimeUnit.SECONDS); } catch (ExecutionException | TimeoutException e) { logger.error("Error while exiting child processes", e); } catch (InterruptedException e) { From 8438484a5b5fb885653b0454b44c61758643394a Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Thu, 21 May 2026 13:57:09 +0200 Subject: [PATCH 100/107] Re-use safe language implementation. --- .../ParametricTextDocumentService.java | 11 +++++-- .../routing/ActualRoutingLanguageServer.java | 29 +++++-------------- 2 files changed, 15 insertions(+), 25 deletions(-) diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricTextDocumentService.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricTextDocumentService.java index 0b80f49b1..c82d83f5a 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricTextDocumentService.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricTextDocumentService.java @@ -638,17 +638,22 @@ private static T last(List l) { } private Optional safeLanguage(ISourceLocation loc) { + return languageByExtension(loc, registeredExtensions); + } + + public static Optional languageByExtension(ISourceLocation loc, Map languagesByExtension) { var ext = extension(loc); + var languages = languagesByExtension.values().stream().collect(Collectors.toSet()); if ("".equals(ext)) { - if (contributions.size() == 1) { + if (languages.size() == 1) { logger.trace("file was opened without an extension; falling back to the single registered language for: {}", loc); - return contributions.keySet().stream().findFirst(); + return languages.stream().findFirst(); } else { logger.error("file was opened without an extension and there are multiple languages registered, so we cannot pick a fallback for: {}", loc); return Optional.empty(); } } - return Optional.ofNullable(registeredExtensions.get(ext)); + return Optional.ofNullable(languagesByExtension.get(ext)); } private String language(ISourceLocation loc) { diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/ActualRoutingLanguageServer.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/ActualRoutingLanguageServer.java index da07213a2..0625903f8 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/ActualRoutingLanguageServer.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/ActualRoutingLanguageServer.java @@ -46,11 +46,9 @@ import java.net.Socket; import java.nio.file.Path; import java.util.Collection; -import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Objects; -import java.util.Optional; import java.util.concurrent.CancellationException; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; @@ -88,6 +86,7 @@ import org.rascalmpl.vscode.lsp.IBaseTextDocumentService; import org.rascalmpl.vscode.lsp.log.LogJsonConfiguration; import org.rascalmpl.vscode.lsp.parametric.LanguageRegistry.LanguageParameter; +import org.rascalmpl.vscode.lsp.parametric.ParametricTextDocumentService; import org.rascalmpl.vscode.lsp.util.DocumentRouter; import org.rascalmpl.vscode.lsp.util.Lists; import org.rascalmpl.vscode.lsp.util.concurrent.CompletableFutureUtils; @@ -156,12 +155,12 @@ public Collection> allRoutes() } @Override - public CompletableFuture route(ISourceLocation file) { - var ext = extension(file); - return CompletableFutureUtils.completedFuture(ext, getExecutor()) - .thenApply(this::safeLanguage) - .thenApply(lang -> lang.orElseThrow(() -> new UnsupportedOperationException(String.format("Rascal Parametric LSP has no support for this file, since no language is registered with extension '%s'", ext)))) - .thenCompose(this::route); + public CompletableFuture route(ISourceLocation loc) { + var lang = ParametricTextDocumentService.languageByExtension(loc, languagesByExtension); + if (lang.isEmpty()) { + return CompletableFuture.failedFuture(new UnsupportedOperationException(String.format("Rascal Parametric LSP has no support for this file, since no language is registered with extension '%s'", extension(loc)))); + } + return route(lang.get()); } @Override @@ -170,20 +169,6 @@ public void connect(LanguageClient client) { this.client.connect(availableClient()); } - private Optional safeLanguage(String extension) { - if ("".equals(extension)) { - var languages = new HashSet<>(languagesByExtension.values()); - if (languages.size() == 1) { - logger.trace("File was opened without an extension; falling back to the single registered language for extension '{}'", extension); - return languages.stream().findFirst(); - } else { - logger.error("File was opened without an extension and there are multiple languages registered, so we cannot pick a fallback for extension '{}'", extension); - return Optional.empty(); - } - } - return Optional.ofNullable(languagesByExtension.get(extension)); - } - private static String extension(ISourceLocation doc) { return URIUtil.getExtension(doc); } From 766bbe5658123caf1e54639c15035b9dba818e6d Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Thu, 21 May 2026 14:04:12 +0200 Subject: [PATCH 101/107] Make single-use interface more specific. --- .../java/org/rascalmpl/vscode/lsp/BaseLanguageServer.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/BaseLanguageServer.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/BaseLanguageServer.java index 189b84499..6e1d9086c 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/BaseLanguageServer.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/BaseLanguageServer.java @@ -134,8 +134,8 @@ protected static void printClassPath() { } @FunctionalInterface - protected interface ServerConstructor { - E apply(A a, B b, C c, D d); + protected interface ServerBuilder { + ActualLanguageServer apply(Runnable a, ExecutorService b, IBaseTextDocumentService c, BaseWorkspaceService d); } protected static void startLanguageServer(String requestPoolName, String workerPoolName, Function docServiceProvider, Function workspaceServiceProvider, int portNumber) { @@ -143,7 +143,7 @@ protected static void startLanguageServer(String requestPoolName, String workerP } @SuppressWarnings({"java:S2189", "java:S106"}) - protected static void startLanguageServer(ServerConstructor serverBuilder, String requestPoolName, String workerPoolName, Function docServiceProvider, Function workspaceServiceProvider, int portNumber) { + protected static void startLanguageServer(ServerBuilder serverBuilder, String requestPoolName, String workerPoolName, Function docServiceProvider, Function workspaceServiceProvider, int portNumber) { logger.info("Starting Rascal Language Server: {}", getVersion()); printClassPath(); From 33e042ec2345263de17f6e77c68ceb2b73fd074f Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Thu, 21 May 2026 16:32:46 +0200 Subject: [PATCH 102/107] Use port pool for dev servers. --- .../routing/ActualRoutingLanguageServer.java | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/ActualRoutingLanguageServer.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/ActualRoutingLanguageServer.java index 0625903f8..f51880ffd 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/ActualRoutingLanguageServer.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/ActualRoutingLanguageServer.java @@ -48,15 +48,16 @@ import java.util.Collection; import java.util.List; import java.util.Map; +import java.util.NavigableSet; import java.util.Objects; import java.util.concurrent.CancellationException; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentSkipListSet; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; -import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Function; import java.util.stream.Collectors; import org.apache.commons.lang3.tuple.Triple; @@ -116,7 +117,8 @@ public class ActualRoutingLanguageServer extends BaseLanguageServer.ActualLangua private final JsonWriter logForwarder; private static final int REMOTE_BASE_PORT = 9990; - private AtomicInteger remotePortOffset = new AtomicInteger(0); + private static final int PORT_POOL_SIZE = 9; + private NavigableSet portPool = new ConcurrentSkipListSet<>(); @SuppressWarnings("java:S106") // System.err public ActualRoutingLanguageServer(Runnable onExit, ExecutorService exec, IBaseTextDocumentService lspDocumentService, BaseWorkspaceService lspWorkspaceService) { @@ -125,6 +127,10 @@ public ActualRoutingLanguageServer(Runnable onExit, ExecutorService exec, IBaseT // log4j loggers write to stderr. We wrap the same stream, so we can directly pipe log messages from our child processes to it. logForwarder = new JsonWriter(new BufferedWriter(new OutputStreamWriter(System.err))); + for (int i = 0; i < PORT_POOL_SIZE; i++) { + portPool.add(REMOTE_BASE_PORT + i); + } + Runtime.getRuntime().addShutdownHook(new Thread(() -> destroyChildProcesses())); } @@ -283,7 +289,7 @@ private void forwardLogs(InputStream logStream, String langName) { private @Nullable Triple connectToServer(LanguageParameter lang) { // In development, we expect the server to have been launched on a pre-agreed port - int port = getNextPort(); + int port = portPool.pollFirst(); try { @SuppressWarnings("java:S2095") // no need to close the socket here - we close it on server shutdown Socket socket = new Socket(InetAddress.getLoopbackAddress(), port); @@ -294,6 +300,8 @@ private void forwardLogs(InputStream logStream, String langName) { socket.close(); } catch (IOException e) { logger.error("Closing socket for {} on port {} failed", lang.getName(), port); + } finally { + portPool.add(port); // Re-use the port } }); } catch (IOException e) { @@ -384,10 +392,6 @@ public void write(JsonWriter writer, ProxiedIValue proxiedValue) throws IOExcept return initializedServer; // When initialization is done, we can use the server } - private int getNextPort() { - return REMOTE_BASE_PORT + remotePortOffset.getAndIncrement(); - } - private InitializeParams availableInitializeParams() { if (this.initializeParams == null) { throw new IllegalStateException("Server not initialized yet"); From 191610e9d6306e61e2b209c0ee7bc6ce0c777929 Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Thu, 21 May 2026 16:39:56 +0200 Subject: [PATCH 103/107] Remove redundant checks. --- .../parametric/routing/RoutingTextDocumentService.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingTextDocumentService.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingTextDocumentService.java index c8dec143d..28f62dc5d 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingTextDocumentService.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingTextDocumentService.java @@ -165,7 +165,7 @@ public void didOpen(DidOpenTextDocumentParams params) { Thread.currentThread().interrupt(); } catch (ExecutionException e) { logger.catching(e); - if (e.getCause() != null && e.getCause() instanceof RuntimeException) { + if (e.getCause() instanceof RuntimeException) { throw (RuntimeException) e.getCause(); } } catch (TimeoutException e) { @@ -181,7 +181,7 @@ public void didChange(DidChangeTextDocumentParams params) { Thread.currentThread().interrupt(); } catch (ExecutionException e) { logger.catching(e); - if (e.getCause() != null && e.getCause() instanceof RuntimeException) { + if (e.getCause() instanceof RuntimeException) { throw (RuntimeException) e.getCause(); } } catch (TimeoutException e) { @@ -197,7 +197,7 @@ public void didClose(DidCloseTextDocumentParams params) { Thread.currentThread().interrupt(); } catch (ExecutionException e) { logger.catching(e); - if (e.getCause() != null && e.getCause() instanceof RuntimeException) { + if (e.getCause() instanceof RuntimeException) { throw (RuntimeException) e.getCause(); } } catch (TimeoutException e) { @@ -213,7 +213,7 @@ public void didSave(DidSaveTextDocumentParams params) { Thread.currentThread().interrupt(); } catch (ExecutionException e) { logger.catching(e); - if (e.getCause() != null && e.getCause() instanceof RuntimeException) { + if (e.getCause() instanceof RuntimeException) { throw (RuntimeException) e.getCause(); } } catch (TimeoutException e) { From 01eb3853e6817a86725b3d26d978d23412f740af Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Thu, 21 May 2026 17:30:05 +0200 Subject: [PATCH 104/107] Revert "Await file notifications." This reverts commit 2902727525e70819313d5d18767120f97aca2a97. --- .../routing/RoutingTextDocumentService.java | 59 +++---------------- 1 file changed, 8 insertions(+), 51 deletions(-) diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingTextDocumentService.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingTextDocumentService.java index 28f62dc5d..654d1487a 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingTextDocumentService.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingTextDocumentService.java @@ -29,10 +29,7 @@ import java.util.Collection; import java.util.List; import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; import java.util.stream.Collectors; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -159,66 +156,26 @@ private LanguageClient availableClient() { @Override public void didOpen(DidOpenTextDocumentParams params) { - try { - route(params.getTextDocument()).thenAccept(s -> s.didOpen(params)).get(10, TimeUnit.SECONDS); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } catch (ExecutionException e) { - logger.catching(e); - if (e.getCause() instanceof RuntimeException) { - throw (RuntimeException) e.getCause(); - } - } catch (TimeoutException e) { - logger.error("didOpen timed out", e); - } + // Note: floating future + route(params.getTextDocument()).thenAccept(s -> s.didOpen(params)); } @Override public void didChange(DidChangeTextDocumentParams params) { - try { - route(params.getTextDocument()).thenAccept(s -> s.didChange(params)).get(10, TimeUnit.SECONDS); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } catch (ExecutionException e) { - logger.catching(e); - if (e.getCause() instanceof RuntimeException) { - throw (RuntimeException) e.getCause(); - } - } catch (TimeoutException e) { - logger.error("didChange timed out", e); - } + // Note: floating future + route(params.getTextDocument()).thenAccept(s -> s.didChange(params)); } @Override public void didClose(DidCloseTextDocumentParams params) { - try { - route(params.getTextDocument()).thenAccept(s -> s.didClose(params)).get(10, TimeUnit.SECONDS); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } catch (ExecutionException e) { - logger.catching(e); - if (e.getCause() instanceof RuntimeException) { - throw (RuntimeException) e.getCause(); - } - } catch (TimeoutException e) { - logger.error("didClose timed out", e); - } + // Note: floating future + route(params.getTextDocument()).thenAccept(s -> s.didClose(params)); } @Override public void didSave(DidSaveTextDocumentParams params) { - try { - route(params.getTextDocument()).thenAccept(s -> s.didSave(params)).get(10, TimeUnit.SECONDS); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } catch (ExecutionException e) { - logger.catching(e); - if (e.getCause() instanceof RuntimeException) { - throw (RuntimeException) e.getCause(); - } - } catch (TimeoutException e) { - logger.error("didSave timed out", e); - } + // Note: floating future + route(params.getTextDocument()).thenAccept(s -> s.didSave(params)); } @Override From a298003f2be83f732f733c68ba25865c45157f79 Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Tue, 26 May 2026 09:27:05 +0200 Subject: [PATCH 105/107] Null check. --- .../lsp/parametric/routing/ActualRoutingLanguageServer.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/ActualRoutingLanguageServer.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/ActualRoutingLanguageServer.java index f51880ffd..eeabea82b 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/ActualRoutingLanguageServer.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/ActualRoutingLanguageServer.java @@ -289,7 +289,10 @@ private void forwardLogs(InputStream logStream, String langName) { private @Nullable Triple connectToServer(LanguageParameter lang) { // In development, we expect the server to have been launched on a pre-agreed port - int port = portPool.pollFirst(); + var port = portPool.pollFirst(); + if (port == null) { + throw new IllegalStateException("Pool of dev ports is exhausted. Stop some unused servers or increase the size of the pool."); + } try { @SuppressWarnings("java:S2095") // no need to close the socket here - we close it on server shutdown Socket socket = new Socket(InetAddress.getLoopbackAddress(), port); From 4e5038e6bd2d92a56cd73ec6b5e1a7c710c78195 Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Tue, 26 May 2026 16:46:24 +0200 Subject: [PATCH 106/107] Always set thread name. --- .../routing/ActualRoutingLanguageServer.java | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/ActualRoutingLanguageServer.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/ActualRoutingLanguageServer.java index eeabea82b..a3e3302c7 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/ActualRoutingLanguageServer.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/ActualRoutingLanguageServer.java @@ -213,26 +213,22 @@ private static List classPath(LanguageParameter lang) throws IOException, .collect(Collectors.toList()); } - private static void prependThreadName(String prefix, JsonElement json) { + private static void prependThreadName(String langName, JsonElement json) { try { var obj = json.getAsJsonObject(); - var threadName = obj.getAsJsonPrimitive("threadName").getAsString(); - if (threadName != null) { - obj.addProperty("threadName", prefix + threadName); - } + obj.addProperty("threadName", langName + (obj.has("threadName") ? " | " + obj.getAsJsonPrimitive("threadName").getAsString() : "")); } catch (Exception e) { /* ignored */ } } @SuppressWarnings("java:S106") // System.err private void forwardLogs(InputStream logStream, String langName) { getExecutor().execute(() -> { - var threadPrefix = langName + " | "; try (var reader = new BufferedReader(new InputStreamReader(logStream))) { String line; while ((line = reader.readLine()) != null) { try { var json = JsonParser.parseString(line); - prependThreadName(threadPrefix, json); + prependThreadName(langName, json); // Lock, so we can make sure our JSON is followed by a newline. synchronized (System.err) { gson.toJson(json, logForwarder); From 091223b0fba0b8edb55426646423fe559e8163d1 Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Wed, 27 May 2026 10:27:13 +0200 Subject: [PATCH 107/107] Document server exit hooks. --- .../parametric/routing/ActualRoutingLanguageServer.java | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/ActualRoutingLanguageServer.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/ActualRoutingLanguageServer.java index a3e3302c7..7889599de 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/ActualRoutingLanguageServer.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/ActualRoutingLanguageServer.java @@ -251,6 +251,10 @@ private void forwardLogs(InputStream logStream, String langName) { }); } + /** + * Starts a language server (dedicated to a single language) in a child process. + * Returns an pair of streams of bi-directional communication, and a runnable to clean up after the server terminates. + */ private @Nullable Triple startServerProcess(LanguageParameter lang) { logger.info("Starting LSP process for {}", lang.getName()); @@ -283,6 +287,10 @@ private void forwardLogs(InputStream logStream, String langName) { } } + /** + * Connects to a language server in a separate process, for debugging. + * Returns an pair of streams of bi-directional communication, and a runnable to clean up after the server terminates. + */ private @Nullable Triple connectToServer(LanguageParameter lang) { // In development, we expect the server to have been launched on a pre-agreed port var port = portPool.pollFirst(); @@ -295,6 +303,7 @@ private void forwardLogs(InputStream logStream, String langName) { socket.setTcpNoDelay(true); return Triple.of(socket.getInputStream(), socket.getOutputStream(), () -> { try { + // After the JSON-RPC connection terminates, close the socket logger.debug("Closing socket for language {} on port {}", lang.getName(), port); socket.close(); } catch (IOException e) {