From 29eb813dfa8f8676a040fc3720af5e389c35609a Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Thu, 11 Jun 2026 17:18:20 +0200 Subject: [PATCH 01/22] Implement workspace file notifcation routing. --- .../routing/RoutingWorkspaceService.java | 25 +++++++++++++++++++ .../src/test/vscode-suite/dsl.test.ts | 3 +-- 2 files changed, 26 insertions(+), 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 cfe16492c..f23a8784b 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,13 +32,17 @@ import java.util.concurrent.ExecutorService; import java.util.stream.Collectors; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; +import org.eclipse.lsp4j.CreateFilesParams; +import org.eclipse.lsp4j.DeleteFilesParams; import org.eclipse.lsp4j.ExecuteCommandParams; +import org.eclipse.lsp4j.RenameFilesParams; 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 org.rascalmpl.vscode.lsp.util.locations.Locations; import io.usethesource.vallang.ISourceLocation; @@ -92,4 +96,25 @@ public CompletableFuture executeCommand(ExecuteCommandParams commandPara return CompletableFutureUtils.completedFuture(commandParams.getCommand() + " was ignored, since it is not a Rascal LSP command.", getExecutor()); } + @Override + public void didCreateFiles(CreateFilesParams params) { + params.getFiles().stream() + .collect(Collectors.groupingBy(f -> route(Locations.toLoc(f.getUri())))) + .forEach((r, creates) -> r.thenAccept(s -> s.didCreateFiles(new CreateFilesParams(creates)))); + } + + @Override + public void didDeleteFiles(DeleteFilesParams params) { + params.getFiles().stream() + .collect(Collectors.groupingBy(f -> route(Locations.toLoc(f.getUri())))) + .forEach((r, deletes) -> r.thenAccept(s -> s.didDeleteFiles(new DeleteFilesParams(deletes)))); + } + + @Override + public void didRenameFiles(RenameFilesParams params) { + params.getFiles().stream() + .collect(Collectors.groupingBy(f -> route(Locations.toLoc(f.getOldUri())))) + .forEach((r, renames) -> r.thenAccept(s -> s.didRenameFiles(new RenameFilesParams(renames)))); + } + } 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 15412f945..3c9d0e40b 100644 --- a/rascal-vscode-extension/src/test/vscode-suite/dsl.test.ts +++ b/rascal-vscode-extension/src/test/vscode-suite/dsl.test.ts @@ -260,8 +260,7 @@ end expect(editorText).to.contain("z := 2"); }); - // TODO Implement this test in a later PR - it.skip("renaming files works", async function() { + it("renaming files works", async function() { if (errorRecovery) { this.skip(); } const newDir = path.join(TestWorkspace.testProject, "src", "main", "pico", "rename-test"); await fs.rm(newDir, {recursive: true, force: true}); From 9c3424e746fab608ee2be38b736675cf688335b3 Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Thu, 11 Jun 2026 17:19:33 +0200 Subject: [PATCH 02/22] Enable completion tests. --- rascal-vscode-extension/src/test/vscode-suite/dsl.test.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 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 3c9d0e40b..c035f6b7f 100644 --- a/rascal-vscode-extension/src/test/vscode-suite/dsl.test.ts +++ b/rascal-vscode-extension/src/test/vscode-suite/dsl.test.ts @@ -311,8 +311,7 @@ end }, Delays.normal, "Call hierarchy should show `multiply` and its two outgoing calls."); }); - // TODO Implement this test in a later PR - it.skip("completion works", async function() { + it("completion works", async function() { const editor = await ide.openModule(TestWorkspace.picoFile); try { await editor.setTextAtLine(6, " aa : natural;"); @@ -326,8 +325,7 @@ end } }); - // TODO Implement this test in a later PR - it.skip("completion by trigger character works", async function() { + it("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 de39d64ee651b618f17e99817996b373a3b77878 Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Thu, 11 Jun 2026 17:19:55 +0200 Subject: [PATCH 03/22] Improve completion tests. --- .../src/test/vscode-suite/utils.ts | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/rascal-vscode-extension/src/test/vscode-suite/utils.ts b/rascal-vscode-extension/src/test/vscode-suite/utils.ts index 373c5c3a4..ca56cc7ff 100644 --- a/rascal-vscode-extension/src/test/vscode-suite/utils.ts +++ b/rascal-vscode-extension/src/test/vscode-suite/utils.ts @@ -571,14 +571,12 @@ export function isLanguageLoading(bench: Workbench, language: string): () => Pro } export async function expectCompletions(driver: WebDriver, editor: TextEditor, expectedLabels: string[]) { - const completions = await driver.wait(async () => { + await driver.wait(async () => { const completionMenu = new ContentAssist(editor); - return await ignoreFails(completionMenu.getItems()); - }, Delays.fast, "Completion items not found"); - - expect(completions).to.have.length(expectedLabels.length); - const labels: string[] = await Promise.all(completions!.map(c => c.getLabel())); - expect(labels).deep.equal(expectedLabels); + const completions = await ignoreFails(completionMenu.getItems()); + const labels: string[] = await Promise.all(completions!.map(c => c.getLabel())); + return labels === expectedLabels; + }, Delays.fast, `Completion items do not equal ${expectedLabels}`); } From c9e628b42c28cc6be5130edbacd770293d94dc5b Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Mon, 15 Jun 2026 16:33:01 +0200 Subject: [PATCH 04/22] Make completion check resilient again stale elements. --- .../src/test/vscode-suite/utils.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/rascal-vscode-extension/src/test/vscode-suite/utils.ts b/rascal-vscode-extension/src/test/vscode-suite/utils.ts index ca56cc7ff..333a539d5 100644 --- a/rascal-vscode-extension/src/test/vscode-suite/utils.ts +++ b/rascal-vscode-extension/src/test/vscode-suite/utils.ts @@ -572,10 +572,14 @@ export function isLanguageLoading(bench: Workbench, language: string): () => Pro export async function expectCompletions(driver: WebDriver, editor: TextEditor, expectedLabels: string[]) { await driver.wait(async () => { - const completionMenu = new ContentAssist(editor); - const completions = await ignoreFails(completionMenu.getItems()); - const labels: string[] = await Promise.all(completions!.map(c => c.getLabel())); - return labels === expectedLabels; + try { + const completionMenu = new ContentAssist(editor); + const completions = await completionMenu.getItems(); + const labels = await Promise.all(completions.map(c => c.getLabel())); + return labels === expectedLabels; + } catch (e) { + return false; + } }, Delays.fast, `Completion items do not equal ${expectedLabels}`); } From 420e89c8ced43083cabd631abf7d24c3d971bd4c Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Wed, 24 Jun 2026 17:09:41 +0200 Subject: [PATCH 05/22] Implement folding range refresh. --- .../vscode/lsp/parametric/routing/MultipleClientProxy.java | 5 +++++ 1 file changed, 5 insertions(+) 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 index be6d604bd..e5df7d407 100644 --- 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 @@ -170,6 +170,11 @@ public CompletableFuture refreshDiagnostics() { return availableClient().refreshDiagnostics(); } + @Override + public CompletableFuture refreshFoldingRanges() { + return availableClient().refreshFoldingRanges(); + } + @Override public CompletableFuture refreshInlayHints() { return availableClient().refreshInlayHints(); From 6b46f1cd1f544f2ae71756dad609c6920665f930 Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Thu, 25 Jun 2026 12:09:28 +0200 Subject: [PATCH 06/22] Do not disconnect development language server on unregister. --- .../routing/ActualRoutingLanguageServer.java | 12 +++--------- 1 file changed, 3 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 792239ba6..54279e602 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 @@ -471,21 +471,15 @@ 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 - logger.trace("unregisterLanguage({}) completely", lang.getName()); + // Do not remove the connection to the server. + // For the deployed scenario, this is handled by the process onExit hook. + // For the development scenario, we maintain the connection, since the remote server does not exit. for (var extension : lang.getExtensions()) { this.languagesByExtension.remove(extension); } - var removed = languageServers.remove(lang.getName()); - if (removed != null) { - work = work - .thenCompose(ignored -> removed) - .thenCompose(server -> server.shutdown().thenAccept(ignored -> server.exit())); - } } return work; From d13c1369d153b8642a8b5a24cb90e25efc52a473 Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Thu, 25 Jun 2026 12:09:45 +0200 Subject: [PATCH 07/22] Fix dynamic capability equals. --- .../lsp/parametric/capabilities/AbstractDynamicCapability.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/capabilities/AbstractDynamicCapability.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/capabilities/AbstractDynamicCapability.java index 594b74558..0a104af25 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/capabilities/AbstractDynamicCapability.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/capabilities/AbstractDynamicCapability.java @@ -150,7 +150,8 @@ public boolean equals(@Nullable Object obj) { } var other = (AbstractDynamicCapability) obj; return Objects.equals(id, other.id) - && Objects.equals(methodName, other.methodName); + && Objects.equals(methodName, other.methodName) + && Objects.equals(preferStaticRegistration, other.preferStaticRegistration); } @Override From a23a2f5022955d95a4dde6589417590adb2bdc71 Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Thu, 25 Jun 2026 13:26:40 +0200 Subject: [PATCH 08/22] Improve multiple client initialization. --- .../routing/ActualRoutingLanguageServer.java | 11 ++- .../routing/MultipleClientProxy.java | 69 ++++++++----------- 2 files changed, 38 insertions(+), 42 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 54279e602..1c4bbe5c0 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 @@ -113,7 +113,7 @@ public class ActualRoutingLanguageServer extends BaseLanguageServer.ActualLangua private final Map> languageServers = new ConcurrentHashMap<>(); private final Map languagesByExtension = new ConcurrentHashMap<>(); - private final MultipleClientProxy client = new MultipleClientProxy(); + private @MonotonicNonNull MultipleClientProxy remoteClient; private @MonotonicNonNull InitializeParams initializeParams; private final JsonWriter logForwarder; @@ -173,7 +173,7 @@ public CompletableFuture route(ISourceLocation lo @Override public void connect(LanguageClient client) { super.connect(client); // first let the super class proxy the client - this.client.connect(availableClient()); + this.remoteClient = new MultipleClientProxy(availableClient()); } private static String extension(ISourceLocation doc) { @@ -349,6 +349,11 @@ public void write(JsonWriter writer, ProxiedIValue proxiedValue) throws IOExcept } private @Nullable CompletableFuture startServer(LanguageParameter lang) { + if (remoteClient == null) { + // This should never happen, since it's initialized by `connect` before we are able to receive any `registerLanguage` requests. + throw new IllegalStateException("Remote client is not initialized"); + } + var serverParams = BaseLanguageServer.DEPLOY_MODE ? startServerProcess(lang) : connectToServer(lang) @@ -360,7 +365,7 @@ public void write(JsonWriter writer, ProxiedIValue proxiedValue) throws IOExcept var serverLauncher = new Launcher.Builder() .setRemoteInterface(IBaseLanguageServerExtensions.class) - .setLocalService(client) + .setLocalService(remoteClient) .setInput(serverParams.getLeft()) .setOutput(serverParams.getMiddle()) .configureGson(ActualRoutingLanguageServer::configureProxyGson) 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 index e5df7d407..e53f0713e 100644 --- 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 @@ -49,7 +49,6 @@ 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.uri.remote.jsonrpc.ISourceLocationChanged; import org.rascalmpl.vscode.lsp.IBaseLanguageClient; import org.rascalmpl.vscode.lsp.parametric.LanguageRegistry.LanguageParameter; @@ -60,161 +59,153 @@ /** * Client proxy implementation that aggregates results from multiple servers before forwarding to its own client. */ -public class MultipleClientProxy implements IBaseLanguageClient, LanguageClientAware { +public class MultipleClientProxy implements IBaseLanguageClient { private static final Logger logger = LogManager.getLogger(MultipleClientProxy.class); - private IBaseLanguageClient client; + private final IBaseLanguageClient client; - @Override - public void connect(LanguageClient client) { + protected MultipleClientProxy(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); + client.telemetryEvent(object); } @Override public void publishDiagnostics(PublishDiagnosticsParams diagnostics) { - availableClient().publishDiagnostics(diagnostics); + client.publishDiagnostics(diagnostics); } @Override public void showMessage(MessageParams messageParams) { - availableClient().showMessage(messageParams); + client.showMessage(messageParams); } @Override public CompletableFuture showMessageRequest(ShowMessageRequestParams requestParams) { - return availableClient().showMessageRequest(requestParams); + return client.showMessageRequest(requestParams); } @Override public void logMessage(MessageParams message) { - availableClient().logMessage(message); + client.logMessage(message); } @Override public void showContent(URI uri, IString title, IInteger viewColumn) { - availableClient().showContent(uri, title, viewColumn); + client.showContent(uri, title, viewColumn); } @Override public void receiveRegisterLanguage(LanguageParameter lang) { logger.debug("rascal/receiveRegisterLanguage({}, {})", lang.getName(), lang.getMainFunction()); - availableClient().receiveRegisterLanguage(lang); + client.receiveRegisterLanguage(lang); } @Override public void receiveUnregisterLanguage(LanguageParameter lang) { logger.debug("rascal/receiveUnregisterLanguage({}, {})", lang.getName(), lang.getMainFunction()); - availableClient().receiveUnregisterLanguage(lang); + client.receiveUnregisterLanguage(lang); } @Override public void editDocument(URI uri, @Nullable Range range, int viewColumn) { - availableClient().editDocument(uri, range, viewColumn); + client.editDocument(uri, range, viewColumn); } @Override public void startDebuggingSession(int serverPort) { - availableClient().startDebuggingSession(serverPort); + client.startDebuggingSession(serverPort); } @Override public void registerDebugServerPort(int processID, int serverPort) { - availableClient().registerDebugServerPort(processID, serverPort); + client.registerDebugServerPort(processID, serverPort); } @Override public CompletableFuture createProgress(WorkDoneProgressCreateParams params) { - return availableClient().createProgress(params); + return client.createProgress(params); } @Override public void notifyProgress(ProgressParams params) { - availableClient().notifyProgress(params); + client.notifyProgress(params); } @Override public CompletableFuture applyEdit(ApplyWorkspaceEditParams params) { - return availableClient().applyEdit(params); + return client.applyEdit(params); } @Override public CompletableFuture> configuration(ConfigurationParams configurationParams) { - return availableClient().configuration(configurationParams); + return client.configuration(configurationParams); } @Override public void logTrace(LogTraceParams params) { - availableClient().logTrace(params); + client.logTrace(params); } @Override public CompletableFuture refreshCodeLenses() { - return availableClient().refreshCodeLenses(); + return client.refreshCodeLenses(); } @Override public CompletableFuture refreshDiagnostics() { - return availableClient().refreshDiagnostics(); + return client.refreshDiagnostics(); } @Override public CompletableFuture refreshFoldingRanges() { - return availableClient().refreshFoldingRanges(); + return client.refreshFoldingRanges(); } @Override public CompletableFuture refreshInlayHints() { - return availableClient().refreshInlayHints(); + return client.refreshInlayHints(); } @Override public CompletableFuture refreshInlineValues() { - return availableClient().refreshInlineValues(); + return client.refreshInlineValues(); } @Override public CompletableFuture refreshSemanticTokens() { - return availableClient().refreshSemanticTokens(); + return client.refreshSemanticTokens(); } @Override public CompletableFuture showDocument(ShowDocumentParams params) { - return availableClient().showDocument(params); + return client.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); + return client.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); + return client.unregisterCapability(params); } @Override public CompletableFuture> workspaceFolders() { - return availableClient().workspaceFolders(); + return client.workspaceFolders(); } @Override public void sourceLocationChanged(ISourceLocationChanged changed) { - availableClient().sourceLocationChanged(changed); + client.sourceLocationChanged(changed); } } From 46ef19e0c4f7c5699d44ec4160f2861d83f82c0e Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Thu, 25 Jun 2026 13:33:51 +0200 Subject: [PATCH 09/22] Add text document content refresh. --- .../vscode/lsp/parametric/routing/MultipleClientProxy.java | 6 ++++++ 1 file changed, 6 insertions(+) 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 index e53f0713e..512a592e6 100644 --- 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 @@ -45,6 +45,7 @@ import org.eclipse.lsp4j.ShowDocumentParams; import org.eclipse.lsp4j.ShowDocumentResult; import org.eclipse.lsp4j.ShowMessageRequestParams; +import org.eclipse.lsp4j.TextDocumentContentRefreshParams; import org.eclipse.lsp4j.UnregistrationParams; import org.eclipse.lsp4j.WorkDoneProgressCreateParams; import org.eclipse.lsp4j.WorkspaceFolder; @@ -181,6 +182,11 @@ public CompletableFuture refreshSemanticTokens() { return client.refreshSemanticTokens(); } + @Override + public CompletableFuture refreshTextDocumentContent(TextDocumentContentRefreshParams params) { + return client.refreshTextDocumentContent(params); + } + @Override public CompletableFuture showDocument(ShowDocumentParams params) { return client.showDocument(params); From f4fcd5c904b12324370ff0f660c404e4f0b9c4a9 Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Thu, 25 Jun 2026 13:34:08 +0200 Subject: [PATCH 10/22] Document renamed file routing. --- .../vscode/lsp/parametric/routing/RoutingWorkspaceService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 f23a8784b..6a678e768 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 @@ -113,7 +113,7 @@ public void didDeleteFiles(DeleteFilesParams params) { @Override public void didRenameFiles(RenameFilesParams params) { params.getFiles().stream() - .collect(Collectors.groupingBy(f -> route(Locations.toLoc(f.getOldUri())))) + .collect(Collectors.groupingBy(f -> route(Locations.toLoc(f.getOldUri())))) // like VS Code, notify language associated with the old file name .forEach((r, renames) -> r.thenAccept(s -> s.didRenameFiles(new RenameFilesParams(renames)))); } From 0edb8821fee60bc5d7744a3550ea53617386ff3c Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Thu, 25 Jun 2026 14:18:44 +0200 Subject: [PATCH 11/22] Routing/logging wrapper. --- .../routing/RoutingTextDocumentService.java | 58 +++++++++++++------ 1 file changed, 39 insertions(+), 19 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 15cd072cb..01c49a51e 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,6 +30,7 @@ import java.util.List; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutorService; +import java.util.function.BiFunction; import java.util.stream.Collectors; import org.apache.logging.log4j.LogManager; @@ -85,6 +86,7 @@ import org.eclipse.lsp4j.SemanticTokensRangeParams; import org.eclipse.lsp4j.ServerCapabilities; import org.eclipse.lsp4j.SymbolInformation; +import org.eclipse.lsp4j.TextDocumentIdentifier; import org.eclipse.lsp4j.WorkspaceEdit; import org.eclipse.lsp4j.WorkspaceFolder; import org.eclipse.lsp4j.jsonrpc.messages.Either; @@ -157,6 +159,24 @@ public CompletableFuture route(String language) { return availableServerRouter().route(language).thenApply(LanguageServer::getTextDocumentService); } + private CompletableFuture route(TextDocumentIdentifier doc, BiFunction> endpoint, P params) { + return routeCompose(route(doc), endpoint, params); + } + + private CompletableFuture route(ISourceLocation loc, BiFunction> endpoint, P params) { + return routeCompose(route(loc), endpoint, params); + } + + private CompletableFuture routeCompose(CompletableFuture sf, BiFunction> endpoint, P params) { + return sf.thenCompose(s -> { + logger.trace("Calling endpoint with params: {}", params); + return endpoint.apply(s, params); + }).thenApply(r -> { + logger.trace("Return value: {}", r); + return r; + }); + } + @Override public Collection extensions() { throw new UnsupportedOperationException("extensions() should not be called on the routing server, but only on delegate servers."); @@ -280,103 +300,103 @@ public void didDeleteFiles(DeleteFilesParams params) { @Override public CompletableFuture> callHierarchyIncomingCalls( CallHierarchyIncomingCallsParams params) { - return route(Locations.toLoc(params.getItem().getUri())).thenCompose(s -> s.callHierarchyIncomingCalls(params)); + return route(Locations.toLoc(params.getItem().getUri()), TextDocumentService::callHierarchyIncomingCalls, params); } @Override public CompletableFuture> callHierarchyOutgoingCalls( CallHierarchyOutgoingCallsParams params) { - return route(Locations.toLoc(params.getItem().getUri())).thenCompose(s -> s.callHierarchyOutgoingCalls(params)); + return route(Locations.toLoc(params.getItem().getUri()), TextDocumentService::callHierarchyOutgoingCalls, params); } @Override public CompletableFuture, CompletionList>> completion(CompletionParams position) { - return route(position.getTextDocument()).thenCompose(s -> s.completion(position)); + return route(position.getTextDocument(), TextDocumentService::completion, position); } @Override public CompletableFuture, List>> definition( DefinitionParams params) { - return route(params.getTextDocument()).thenCompose(s -> s.definition(params)); + return route(params.getTextDocument(), TextDocumentService::definition, params); } @Override public CompletableFuture> prepareCallHierarchy(CallHierarchyPrepareParams params) { - return route(params.getTextDocument()).thenCompose(s -> s.prepareCallHierarchy(params)); + return route(params.getTextDocument(), TextDocumentService::prepareCallHierarchy, params); } @Override public CompletableFuture semanticTokensFull(SemanticTokensParams params) { - return route(params.getTextDocument()).thenCompose(s -> s.semanticTokensFull(params)); + return route(params.getTextDocument(), TextDocumentService::semanticTokensFull, params); } @Override public CompletableFuture> semanticTokensFullDelta( SemanticTokensDeltaParams params) { - return route(params.getTextDocument()).thenCompose(s -> s.semanticTokensFullDelta(params)); + return route(params.getTextDocument(), TextDocumentService::semanticTokensFullDelta, params); } @Override public CompletableFuture semanticTokensRange(SemanticTokensRangeParams params) { - return route(params.getTextDocument()).thenCompose(s -> s.semanticTokensRange(params)); + return route(params.getTextDocument(), TextDocumentService::semanticTokensRange, params); } @Override public CompletableFuture> codeLens(CodeLensParams params) { - return route(params.getTextDocument()).thenCompose(s -> s.codeLens(params)); + return route(params.getTextDocument(), TextDocumentService::codeLens, params); } @Override public CompletableFuture> prepareRename( PrepareRenameParams params) { - return route(params.getTextDocument()).thenCompose(s -> s.prepareRename(params)); + return route(params.getTextDocument(), TextDocumentService::prepareRename, params); } @Override public CompletableFuture rename(RenameParams params) { - return route(params.getTextDocument()).thenCompose(s -> s.rename(params)); + return route(params.getTextDocument(), TextDocumentService::rename, params); } @Override public CompletableFuture> inlayHint(InlayHintParams params) { - return route(params.getTextDocument()).thenCompose(s -> s.inlayHint(params)); + return route(params.getTextDocument(), TextDocumentService::inlayHint, params); } @Override public CompletableFuture>> codeAction(CodeActionParams params) { - return route(params.getTextDocument()).thenCompose(s -> s.codeAction(params)); + return route(params.getTextDocument(), TextDocumentService::codeAction, params); } @Override public CompletableFuture>> documentSymbol( DocumentSymbolParams params) { - return route(params.getTextDocument()).thenCompose(s -> s.documentSymbol(params)); + return route(params.getTextDocument(), TextDocumentService::documentSymbol, params); } @Override public CompletableFuture, List>> implementation( ImplementationParams params) { - return route(params.getTextDocument()).thenCompose(s -> s.implementation(params)); + return route(params.getTextDocument(), TextDocumentService::implementation, params); } @Override public CompletableFuture> references(ReferenceParams params) { - return route(params.getTextDocument()).thenCompose(s -> s.references(params)); + return route(params.getTextDocument(), TextDocumentService::references, params); } @Override public CompletableFuture> foldingRange(FoldingRangeRequestParams params) { - return route(params.getTextDocument()).thenCompose(s -> s.foldingRange(params)); + return route(params.getTextDocument(), TextDocumentService::foldingRange, params); } @Override public CompletableFuture<@Nullable Hover> hover(HoverParams params) { - return route(params.getTextDocument()).<@Nullable Hover>thenCompose(s -> s.hover(params)); + return this.route(params.getTextDocument(), TextDocumentService::hover, params); } @Override public CompletableFuture> selectionRange(SelectionRangeParams params) { - return route(params.getTextDocument()).thenCompose(s -> s.selectionRange(params)); + return route(params.getTextDocument(), TextDocumentService::selectionRange, params); } } From da7f26f405745a529f1a8f3353aaf0deccb1d4af Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Thu, 25 Jun 2026 14:19:40 +0200 Subject: [PATCH 12/22] Set log level per test suite. --- .../src/test/vscode-suite/utils.ts | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/rascal-vscode-extension/src/test/vscode-suite/utils.ts b/rascal-vscode-extension/src/test/vscode-suite/utils.ts index 333a539d5..abc218617 100644 --- a/rascal-vscode-extension/src/test/vscode-suite/utils.ts +++ b/rascal-vscode-extension/src/test/vscode-suite/utils.ts @@ -216,7 +216,7 @@ export class IDEOperations { this.driver = browser.driver; } - async load() { + async load(logLevel: LogLevel = "Debug") { await ignoreFails(this.browser.waitForWorkbench(Delays.slow)); for (let t = 0; t < 5; t++) { try { @@ -234,7 +234,7 @@ export class IDEOperations { const center = await ignoreFails(new Workbench().openNotificationsCenter()); await ignoreFails(center?.clearAllNotifications()); await ignoreFails(center?.close()); - await assureDebugLevelLoggingIsEnabled(); + await setLogLevel(logLevel); } async cleanup() { @@ -507,17 +507,13 @@ async function showRascalOutput(bbp: BottomBarPanel, channel: string) { return outputView; } -let alreadySetup = false; +type LogLevel = "Trace" | "Debug" | "Info" | "Warning" | "Error" | "Off"; -async function assureDebugLevelLoggingIsEnabled() { - if (alreadySetup) { - return; - } - alreadySetup = true; // to avoid doing this twice/parallel +async function setLogLevel(logLevel: LogLevel) { const prompt = await new Workbench().openCommandPrompt(); await prompt.setText(">workbench.action.setLogLevel"); await prompt.confirm(); - await prompt.setText("Debug"); + await prompt.setText(logLevel); await prompt.confirm(); } From d8d68064f471ad8dc54c23976e845aa32e880cd2 Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Thu, 25 Jun 2026 14:19:54 +0200 Subject: [PATCH 13/22] Set DSL log level to trace. --- rascal-vscode-extension/src/test/vscode-suite/dsl.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 c035f6b7f..dab2e2eab 100644 --- a/rascal-vscode-extension/src/test/vscode-suite/dsl.test.ts +++ b/rascal-vscode-extension/src/test/vscode-suite/dsl.test.ts @@ -81,7 +81,7 @@ parameterizedDescribe(function (errorRecovery: boolean) { bench = new Workbench(); await ignoreFails(browser.waitForWorkbench()); ide = new IDEOperations(browser); - await ide.load(); + await ide.load("Trace"); await loadPico(); protectedFiles = await ProtectedFiles.protect(TestWorkspace.picoFile); ide = new IDEOperations(browser); From 43e791c6044e6d066ee6d1f00da0321cb7a92aca Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Thu, 25 Jun 2026 14:23:04 +0200 Subject: [PATCH 14/22] Clear all notifications before loading language. --- rascal-vscode-extension/src/test/vscode-suite/dsl.test.ts | 3 +++ 1 file changed, 3 insertions(+) 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 dab2e2eab..2a66c77d1 100644 --- a/rascal-vscode-extension/src/test/vscode-suite/dsl.test.ts +++ b/rascal-vscode-extension/src/test/vscode-suite/dsl.test.ts @@ -55,6 +55,9 @@ parameterizedDescribe(function (errorRecovery: boolean) { } async function loadPico() { + const notifications = await bench.openNotificationsCenter(); + await notifications.clearAllNotifications(); + const repl = new RascalREPL(bench, driver); await repl.start(); await repl.execute("import testing::lang::pico::LanguageServer;", false, Delays.extremelySlow); From 3124f724baea23521bbf76365f0bcce547c3a783 Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Thu, 25 Jun 2026 15:49:08 +0200 Subject: [PATCH 15/22] Safe, reused unregistration. --- .../parametric/ParametricTextDocumentService.java | 6 +++++- .../routing/ActualRoutingLanguageServer.java | 14 ++++++++------ 2 files changed, 13 insertions(+), 7 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 4ea343dc4..bf14fb5b7 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 @@ -1013,10 +1013,14 @@ private static String buildContributionKey(LanguageParameter lang) { return lang.getMainFunction() + "::" + lang.getMainFunction(); } + public static boolean isLanguageCompletelyRemoved(LanguageParameter lang) { + return lang.getMainModule() == null || lang.getMainModule().isEmpty(); + } + @Override public synchronized void unregisterLanguage(LanguageParameter lang) { logger.info("unregisterLanguage({})", lang.getName()); - boolean removeAll = lang.getMainModule() == null || lang.getMainModule().isEmpty(); + boolean removeAll = isLanguageCompletelyRemoved(lang); if (!removeAll) { var contrib = contributions.get(lang.getName()); if (contrib != null && !contrib.removeContributor(buildContributionKey(lang))) { 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 1c4bbe5c0..670e577bb 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 @@ -473,11 +473,7 @@ public synchronized CompletableFuture sendRegisterLanguage(LanguageParamet 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) { + if (ParametricTextDocumentService.isLanguageCompletelyRemoved(lang)) { // Do not remove the connection to the server. // For the deployed scenario, this is handled by the process onExit hook. // For the development scenario, we maintain the connection, since the remote server does not exit. @@ -487,7 +483,13 @@ public synchronized CompletableFuture sendUnregisterLanguage(LanguageParam } } - return work; + return route(lang.getName()).handle((s, t) -> { + if (s == null) { + // Nothing to unregister + return CompletableFutureUtils.completedFuture(null, getExecutor()); + } + return s.sendUnregisterLanguage(lang); + }).thenCompose(Function.identity()); } @Override From 89befc34dfafe99684918b37edaa4c91ad16c6cd Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Thu, 25 Jun 2026 16:47:13 +0200 Subject: [PATCH 16/22] Do one capability update at a time. --- .../capabilities/CapabilityRegistration.java | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/capabilities/CapabilityRegistration.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/capabilities/CapabilityRegistration.java index f848ea052..452f9e154 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/capabilities/CapabilityRegistration.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/capabilities/CapabilityRegistration.java @@ -75,6 +75,8 @@ public class CapabilityRegistration { private final Set> staticCapabilities; private final AtomicReference> lastParams = new AtomicReference<>(Collections.emptyList()); + private final AtomicReference> currentUpdate; + // Map of method names with current registration values private final Map currentRegistrations = new ConcurrentHashMap<>(); @@ -87,6 +89,7 @@ public CapabilityRegistration(LanguageClient client, Executor exec, ClientCapabi this.client = client; this.exec = exec; this.noop = CompletableFutureUtils.completedFuture(null, exec); + this.currentUpdate = new AtomicReference<>(noop); // Check whether to register capabilities dynamically or statically var dynamicCaps = new HashSet>(); @@ -120,9 +123,7 @@ public void registerStaticCapabilities(ServerCapabilities result) { * @return A future that completes with a boolean that is false when any registration failed, and true otherwise. */ public CompletableFuture update(Collection languages) { - logger.debug("Updating {} dynamic capabilities for {} languages", dynamicCapabilities.size(), languages.size()); // Copy the contributions so we know we are looking at a stable collection of elements. - /* *VERY IMPORTANT* Setting the atomic reference from the thread that called us. We need to be sure that the `lastContributions` reference actually points to the most recently known contributions. @@ -130,6 +131,11 @@ public CompletableFuture update(Collection languages) { Additionally, this function should be called from a thread pool with predictable execution order. */ lastParams.set(List.copyOf(languages)); + return currentUpdate.updateAndGet(current -> current.thenComposeAsync(v -> doUpdate(languages), exec)); + } + + private CompletableFuture doUpdate(Collection languages) { + logger.debug("Updating {} dynamic capabilities for {} languages", dynamicCapabilities.size(), languages.size()); return CompletableFutureUtils.reduce(dynamicCapabilities.stream().map(this::updateRegistration), exec) .thenAccept(_v -> logger.debug("Done updating dynamic capabilities")); } @@ -200,6 +206,7 @@ private CompletableFuture updateRegistration(AbstractDynamicCapability result = register(registration, existingRegistration); } + // TODO Check/Fix that if one capability fails, the rest are still registered return result.handle((_v, t) -> { if (t != null) { // An error occurred. Inform the user and do not recurse. From 11296e800328c89b0d4d796e7b8d2238d7fa123e Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Fri, 26 Jun 2026 10:56:59 +0200 Subject: [PATCH 17/22] Isolate failing test/registration. --- .../DynamicServerCapabilities.java | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/capabilities/DynamicServerCapabilities.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/capabilities/DynamicServerCapabilities.java index f4247b989..e85e01abf 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/capabilities/DynamicServerCapabilities.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/capabilities/DynamicServerCapabilities.java @@ -27,20 +27,10 @@ package org.rascalmpl.vscode.lsp.parametric.capabilities; import java.util.concurrent.CompletableFuture; - -import org.eclipse.lsp4j.CallHierarchyRegistrationOptions; import org.eclipse.lsp4j.CodeActionRegistrationOptions; import org.eclipse.lsp4j.CodeLensOptions; import org.eclipse.lsp4j.CodeLensRegistrationOptions; -import org.eclipse.lsp4j.DefinitionRegistrationOptions; -import org.eclipse.lsp4j.DocumentSymbolRegistrationOptions; -import org.eclipse.lsp4j.FoldingRangeProviderOptions; -import org.eclipse.lsp4j.HoverRegistrationOptions; -import org.eclipse.lsp4j.ImplementationRegistrationOptions; import org.eclipse.lsp4j.InlayHintRegistrationOptions; -import org.eclipse.lsp4j.ReferenceRegistrationOptions; -import org.eclipse.lsp4j.RenameOptions; -import org.eclipse.lsp4j.SelectionRangeRegistrationOptions; import org.eclipse.lsp4j.TextDocumentClientCapabilities; import org.rascalmpl.vscode.lsp.parametric.ILanguageContributions; import org.rascalmpl.vscode.lsp.rascal.conversion.SemanticTokenizer; @@ -55,12 +45,14 @@ private DynamicServerCapabilities() { /* hide implicit constructor */ } /*package*/ public static AbstractDynamicCapability[] parametric(String rascalMetaCommandName) { return new AbstractDynamicCapability[] { // Text document capabilities + /* new TextDocumentCapabilityWithConstantOptions<>("prepareCallHierarchy", CallHierarchyRegistrationOptions::new, TextDocumentClientCapabilities::getCallHierarchy, ILanguageContributions::providesCallHierarchy, c -> c.setCallHierarchyProvider(true) ), + */ new TextDocumentCapabilityWithConstantOptions<>("codeAction", CodeActionRegistrationOptions::new, TextDocumentClientCapabilities::getCodeAction, @@ -74,6 +66,7 @@ private DynamicServerCapabilities() { /* hide implicit constructor */ } c -> c.setCodeLensProvider(new CodeLensOptions(false)) ), new CompletionCapability(), + /* new TextDocumentCapabilityWithConstantOptions<>("definition", DefinitionRegistrationOptions::new, TextDocumentClientCapabilities::getDefinition, @@ -104,12 +97,14 @@ private DynamicServerCapabilities() { /* hide implicit constructor */ } ILanguageContributions::providesImplementation, c -> c.setImplementationProvider(true) ), + */ new TextDocumentCapabilityWithConstantOptions<>("inlayHint", InlayHintRegistrationOptions::new, TextDocumentClientCapabilities::getInlayHint, ILanguageContributions::providesInlayHint, c -> c.setInlayHintProvider(true) ), + /* new TextDocumentCapabilityWithConstantOptions<>("references", ReferenceRegistrationOptions::new, TextDocumentClientCapabilities::getReferences, @@ -128,6 +123,7 @@ private DynamicServerCapabilities() { /* hide implicit constructor */ } ILanguageContributions::providesSelectionRange, c -> c.setSelectionRangeProvider(true) ), + */ new TextDocumentCapabilityWithConstantOptions<>("semanticTokens", SemanticTokenizer::options, TextDocumentClientCapabilities::getSemanticTokens, @@ -137,8 +133,10 @@ private DynamicServerCapabilities() { /* hide implicit constructor */ } // Workspace capabilities new ExecuteCommandCapability(rascalMetaCommandName), + /* new FileOperationCapability.DidRenameFiles(), new FileOperationCapability.DidDeleteFiles() + */ }; } From c7185079d22e0028f84b263c91ee3ab06420bac6 Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Fri, 26 Jun 2026 11:13:53 +0200 Subject: [PATCH 18/22] Isolate even more. --- rascal-vscode-extension/src/test/vscode-suite/dsl.test.ts | 2 ++ 1 file changed, 2 insertions(+) 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 2a66c77d1..28bb11894 100644 --- a/rascal-vscode-extension/src/test/vscode-suite/dsl.test.ts +++ b/rascal-vscode-extension/src/test/vscode-suite/dsl.test.ts @@ -35,7 +35,9 @@ import * as path from 'path/posix'; function parameterizedDescribe(body: (this: Suite, errorRecovery: boolean) => void) { describe('DSL', function() { body.apply(this, [false]); }); + /* describe('DSL+recovery', function() { body.apply(this, [true]); }); + */ } parameterizedDescribe(function (errorRecovery: boolean) { From d212f948583cbcc98d74fc9f6873065e2ff5c5d1 Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Fri, 26 Jun 2026 14:33:08 +0200 Subject: [PATCH 19/22] Revert "Isolate even more." This reverts commit c7185079d22e0028f84b263c91ee3ab06420bac6. --- rascal-vscode-extension/src/test/vscode-suite/dsl.test.ts | 2 -- 1 file changed, 2 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 28bb11894..2a66c77d1 100644 --- a/rascal-vscode-extension/src/test/vscode-suite/dsl.test.ts +++ b/rascal-vscode-extension/src/test/vscode-suite/dsl.test.ts @@ -35,9 +35,7 @@ import * as path from 'path/posix'; function parameterizedDescribe(body: (this: Suite, errorRecovery: boolean) => void) { describe('DSL', function() { body.apply(this, [false]); }); - /* describe('DSL+recovery', function() { body.apply(this, [true]); }); - */ } parameterizedDescribe(function (errorRecovery: boolean) { From 2e4a43b57bc8077d93182ab5ebf85bedb7067443 Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Fri, 26 Jun 2026 14:33:06 +0200 Subject: [PATCH 20/22] Revert "Isolate failing test/registration." This reverts commit 11296e800328c89b0d4d796e7b8d2238d7fa123e. --- .../DynamicServerCapabilities.java | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/capabilities/DynamicServerCapabilities.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/capabilities/DynamicServerCapabilities.java index e85e01abf..f4247b989 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/capabilities/DynamicServerCapabilities.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/capabilities/DynamicServerCapabilities.java @@ -27,10 +27,20 @@ package org.rascalmpl.vscode.lsp.parametric.capabilities; import java.util.concurrent.CompletableFuture; + +import org.eclipse.lsp4j.CallHierarchyRegistrationOptions; import org.eclipse.lsp4j.CodeActionRegistrationOptions; import org.eclipse.lsp4j.CodeLensOptions; import org.eclipse.lsp4j.CodeLensRegistrationOptions; +import org.eclipse.lsp4j.DefinitionRegistrationOptions; +import org.eclipse.lsp4j.DocumentSymbolRegistrationOptions; +import org.eclipse.lsp4j.FoldingRangeProviderOptions; +import org.eclipse.lsp4j.HoverRegistrationOptions; +import org.eclipse.lsp4j.ImplementationRegistrationOptions; import org.eclipse.lsp4j.InlayHintRegistrationOptions; +import org.eclipse.lsp4j.ReferenceRegistrationOptions; +import org.eclipse.lsp4j.RenameOptions; +import org.eclipse.lsp4j.SelectionRangeRegistrationOptions; import org.eclipse.lsp4j.TextDocumentClientCapabilities; import org.rascalmpl.vscode.lsp.parametric.ILanguageContributions; import org.rascalmpl.vscode.lsp.rascal.conversion.SemanticTokenizer; @@ -45,14 +55,12 @@ private DynamicServerCapabilities() { /* hide implicit constructor */ } /*package*/ public static AbstractDynamicCapability[] parametric(String rascalMetaCommandName) { return new AbstractDynamicCapability[] { // Text document capabilities - /* new TextDocumentCapabilityWithConstantOptions<>("prepareCallHierarchy", CallHierarchyRegistrationOptions::new, TextDocumentClientCapabilities::getCallHierarchy, ILanguageContributions::providesCallHierarchy, c -> c.setCallHierarchyProvider(true) ), - */ new TextDocumentCapabilityWithConstantOptions<>("codeAction", CodeActionRegistrationOptions::new, TextDocumentClientCapabilities::getCodeAction, @@ -66,7 +74,6 @@ private DynamicServerCapabilities() { /* hide implicit constructor */ } c -> c.setCodeLensProvider(new CodeLensOptions(false)) ), new CompletionCapability(), - /* new TextDocumentCapabilityWithConstantOptions<>("definition", DefinitionRegistrationOptions::new, TextDocumentClientCapabilities::getDefinition, @@ -97,14 +104,12 @@ private DynamicServerCapabilities() { /* hide implicit constructor */ } ILanguageContributions::providesImplementation, c -> c.setImplementationProvider(true) ), - */ new TextDocumentCapabilityWithConstantOptions<>("inlayHint", InlayHintRegistrationOptions::new, TextDocumentClientCapabilities::getInlayHint, ILanguageContributions::providesInlayHint, c -> c.setInlayHintProvider(true) ), - /* new TextDocumentCapabilityWithConstantOptions<>("references", ReferenceRegistrationOptions::new, TextDocumentClientCapabilities::getReferences, @@ -123,7 +128,6 @@ private DynamicServerCapabilities() { /* hide implicit constructor */ } ILanguageContributions::providesSelectionRange, c -> c.setSelectionRangeProvider(true) ), - */ new TextDocumentCapabilityWithConstantOptions<>("semanticTokens", SemanticTokenizer::options, TextDocumentClientCapabilities::getSemanticTokens, @@ -133,10 +137,8 @@ private DynamicServerCapabilities() { /* hide implicit constructor */ } // Workspace capabilities new ExecuteCommandCapability(rascalMetaCommandName), - /* new FileOperationCapability.DidRenameFiles(), new FileOperationCapability.DidDeleteFiles() - */ }; } From 00d85cc8cfc0f57b8451e53078e88e491443e55f Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Fri, 26 Jun 2026 16:12:36 +0200 Subject: [PATCH 21/22] Filter duplicate registrations from servers. --- .../routing/ActualRoutingLanguageServer.java | 2 +- .../routing/MultipleClientProxy.java | 88 +++++++++++++++++-- 2 files changed, 84 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 670e577bb..76e06405c 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 @@ -173,7 +173,7 @@ public CompletableFuture route(ISourceLocation lo @Override public void connect(LanguageClient client) { super.connect(client); // first let the super class proxy the client - this.remoteClient = new MultipleClientProxy(availableClient()); + this.remoteClient = new MultipleClientProxy(availableClient(), getExecutor()); } private static String extension(ISourceLocation doc) { 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 index 512a592e6..f18c00523 100644 --- 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 @@ -28,7 +28,15 @@ import java.net.URI; import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CopyOnWriteArraySet; +import java.util.concurrent.ExecutorService; +import java.util.function.Predicate; +import java.util.stream.Collectors; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.checkerframework.checker.nullness.qual.Nullable; @@ -41,11 +49,13 @@ import org.eclipse.lsp4j.ProgressParams; import org.eclipse.lsp4j.PublishDiagnosticsParams; import org.eclipse.lsp4j.Range; +import org.eclipse.lsp4j.Registration; import org.eclipse.lsp4j.RegistrationParams; import org.eclipse.lsp4j.ShowDocumentParams; import org.eclipse.lsp4j.ShowDocumentResult; import org.eclipse.lsp4j.ShowMessageRequestParams; import org.eclipse.lsp4j.TextDocumentContentRefreshParams; +import org.eclipse.lsp4j.Unregistration; import org.eclipse.lsp4j.UnregistrationParams; import org.eclipse.lsp4j.WorkDoneProgressCreateParams; import org.eclipse.lsp4j.WorkspaceFolder; @@ -53,6 +63,7 @@ import org.rascalmpl.uri.remote.jsonrpc.ISourceLocationChanged; import org.rascalmpl.vscode.lsp.IBaseLanguageClient; import org.rascalmpl.vscode.lsp.parametric.LanguageRegistry.LanguageParameter; +import org.rascalmpl.vscode.lsp.util.concurrent.CompletableFutureUtils; import io.usethesource.vallang.IInteger; import io.usethesource.vallang.IString; @@ -65,9 +76,16 @@ public class MultipleClientProxy implements IBaseLanguageClient { private static final Logger logger = LogManager.getLogger(MultipleClientProxy.class); private final IBaseLanguageClient client; + private final ExecutorService exec; + private final CompletableFuture noop; - protected MultipleClientProxy(LanguageClient client) { + private final Map> currentRegistrations = new ConcurrentHashMap<>(); + + + protected MultipleClientProxy(LanguageClient client, ExecutorService exec) { this.client = (IBaseLanguageClient) client; + this.exec = exec; + this.noop = CompletableFutureUtils.completedFuture(null, exec); } @Override @@ -194,14 +212,74 @@ public CompletableFuture showDocument(ShowDocumentParams par @Override public CompletableFuture registerCapability(RegistrationParams params) { - // TODO Collect/maintain capabilities of all delegate servers, combine, and unregister capabilities if necessary based on that. - return client.registerCapability(params); + return CompletableFutureUtils + .reduce(params + .getRegistrations() + .parallelStream() + .map(this::registerCapability), exec) + .thenAccept(v -> {}); // convert to Void + } + + private CompletableFuture registerCapability(Registration r) { + var c = currentRegistrations.computeIfAbsent(r.getMethod(), k -> new CopyOnWriteArraySet<>()); + // Lock on the registrations for this method for a moment + synchronized (c) { + var similarRegOpt = c.stream().filter(rr -> Objects.equals(rr.getRegisterOptions(), r.getRegisterOptions())).findAny(); + if (similarRegOpt.isPresent()) { + logger.trace("We already have a registration for {} with the same options; ignoring this one", r.getMethod()); + return noop; + } + + c.add(r); + return client.registerCapability(new RegistrationParams(List.of(r))) + .exceptionally(t -> { + logger.error("Exception while registering {}", r, t); + c.remove(r); + return null; + }); + } } @Override public CompletableFuture unregisterCapability(UnregistrationParams params) { - // TODO Collect/maintain capabilities of all delegate servers, combine, and unregister capabilities if necessary based on that. - return client.unregisterCapability(params); + return CompletableFutureUtils + .reduce(params + .getUnregisterations() + .parallelStream() + .map(this::unregisterCapability), exec) + .thenAccept(v -> {}); // convert to Void + } + + private boolean matches(Registration r, Unregistration u) { + return r.getId().equals(u.getId()) + && r.getMethod().equals(u.getMethod()); + } + + private Predicate matches(Unregistration u) { + return r -> matches(r, u); + } + + private CompletableFuture unregisterCapability(Unregistration u) { + var c = currentRegistrations.get(u.getMethod()); + if (c == null) { + return noop; + } + + synchronized (c) { + var cs = c.stream().filter(matches(u)).collect(Collectors.toSet()); + if (cs.isEmpty()) { + // No registrations => nothing to unregister + return noop; + } + + c.removeAll(cs); + return client.unregisterCapability(new UnregistrationParams(List.of(u))) + .exceptionally(t -> { + logger.error("Exception while unregistering {} ({})", u.getMethod(), u, t); + c.addAll(cs); + return null; + }); + } } @Override From 3244617cf01611957a41e517cf19c674ae59c3d5 Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Fri, 26 Jun 2026 17:07:21 +0200 Subject: [PATCH 22/22] Enforce trace log level. --- rascal-vscode-extension/src/test/vscode-suite/dsl-mix.test.ts | 3 +-- rascal-vscode-extension/src/test/vscode-suite/dsl.test.ts | 1 - 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/rascal-vscode-extension/src/test/vscode-suite/dsl-mix.test.ts b/rascal-vscode-extension/src/test/vscode-suite/dsl-mix.test.ts index 548155596..9c165d745 100644 --- a/rascal-vscode-extension/src/test/vscode-suite/dsl-mix.test.ts +++ b/rascal-vscode-extension/src/test/vscode-suite/dsl-mix.test.ts @@ -65,10 +65,9 @@ describe('DSL [multi-language]', function () { bench = new Workbench(); await ignoreFails(browser.waitForWorkbench()); ide = new IDEOperations(browser); - await ide.load(); + await ide.load("Trace"); await loadLanguages(); ide = new IDEOperations(browser); - await ide.load(); }); after(async () => { 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 2a66c77d1..9a4ea0249 100644 --- a/rascal-vscode-extension/src/test/vscode-suite/dsl.test.ts +++ b/rascal-vscode-extension/src/test/vscode-suite/dsl.test.ts @@ -88,7 +88,6 @@ parameterizedDescribe(function (errorRecovery: boolean) { await loadPico(); protectedFiles = await ProtectedFiles.protect(TestWorkspace.picoFile); ide = new IDEOperations(browser); - await ide.load(); }); after(async() => {