From 3007e547e6a2145cbad4af1fd3a1acb342496e21 Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Wed, 15 Apr 2026 16:10:15 +0200 Subject: [PATCH 01/23] Parametric router boilerplate. --- .../vscode/lsp/BaseLanguageServer.java | 9 +- .../vscode/lsp/BaseWorkspaceService.java | 31 +++- .../parametric/ParametricLanguageRouter.java | 171 ++++++++++++++++++ .../ParametricWorkspaceService.java | 5 +- .../lsp/rascal/RascalWorkspaceService.java | 5 +- 5 files changed, 201 insertions(+), 20 deletions(-) create mode 100644 rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricLanguageRouter.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 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..cadaf27fc 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,18 @@ 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; } + void pair(IBaseTextDocumentService documentService) { + this.documentService = documentService; + } + public void initialize(ClientCapabilities clientCap, @Nullable List currentWorkspaceFolders, ServerCapabilities capabilities) { this.workspaceFolders.clear(); if (currentWorkspaceFolders != null) { @@ -93,6 +96,14 @@ public void initialize(ClientCapabilities clientCap, @Nullable List workspaceFolders() { return Collections.unmodifiableList(workspaceFolders); } @@ -126,7 +137,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 +145,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 +153,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 +168,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 +186,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/ParametricLanguageRouter.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricLanguageRouter.java new file mode 100644 index 000000000..12abc101b --- /dev/null +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricLanguageRouter.java @@ -0,0 +1,171 @@ +/* + * 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.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.rascalmpl.util.locations.ColumnMaps; +import org.rascalmpl.util.locations.LineColumnOffsetMap; +import org.rascalmpl.vscode.lsp.BaseWorkspaceService; +import org.rascalmpl.vscode.lsp.IBaseTextDocumentService; +import org.rascalmpl.vscode.lsp.TextDocumentState; +import org.rascalmpl.vscode.lsp.parametric.LanguageRegistry.LanguageParameter; + +import io.usethesource.vallang.ISourceLocation; +import io.usethesource.vallang.IValue; + +public class ParametricLanguageRouter extends BaseWorkspaceService implements IBaseTextDocumentService { + + protected ParametricLanguageRouter(ExecutorService exec) { + super(exec); + } + + @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 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 didRenameFiles(RenameFilesParams params, List workspaceFolders) { + // TODO Auto-generated method stub + throw new UnsupportedOperationException("Unimplemented method 'didRenameFiles'"); + } + + @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/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 1345af6fa3b46b8c20cee66255ee9dc798cf5adf Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Wed, 15 Apr 2026 17:05:21 +0200 Subject: [PATCH 02/23] Add parametric router tests. --- .../ParametricLanguageRouterTest.java | 145 ++++++++++++++++++ 1 file changed, 145 insertions(+) create mode 100644 rascal-lsp/src/test/java/org/rascalmpl/vscode/lsp/parametric/ParametricLanguageRouterTest.java diff --git a/rascal-lsp/src/test/java/org/rascalmpl/vscode/lsp/parametric/ParametricLanguageRouterTest.java b/rascal-lsp/src/test/java/org/rascalmpl/vscode/lsp/parametric/ParametricLanguageRouterTest.java new file mode 100644 index 000000000..4926d957b --- /dev/null +++ b/rascal-lsp/src/test/java/org/rascalmpl/vscode/lsp/parametric/ParametricLanguageRouterTest.java @@ -0,0 +1,145 @@ +/* + * 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 static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.io.IOException; +import java.net.URISyntaxException; +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import org.eclipse.lsp4j.DidCloseTextDocumentParams; +import org.eclipse.lsp4j.DidOpenTextDocumentParams; +import org.eclipse.lsp4j.SemanticTokensParams; +import org.eclipse.lsp4j.TextDocumentIdentifier; +import org.eclipse.lsp4j.TextDocumentItem; +import org.junit.Before; +import org.junit.Test; +import org.rascalmpl.library.util.PathConfig; +import org.rascalmpl.uri.URIUtil; +import org.rascalmpl.values.IRascalValueFactory; +import org.rascalmpl.vscode.lsp.parametric.LanguageRegistry.LanguageParameter; +import org.rascalmpl.vscode.lsp.util.EvaluatorUtil; +import org.rascalmpl.vscode.lsp.util.NamedThreadPool; +import org.rascalmpl.vscode.lsp.util.locations.Locations; + +import io.usethesource.vallang.ISourceLocation; + +public class ParametricLanguageRouterTest { + private static final IRascalValueFactory VF = IRascalValueFactory.getInstance(); + private final ExecutorService exec = NamedThreadPool.cached("parametric-language-router-test"); + private ParametricLanguageRouter router; + // private PathConfigs pathConfigs; + // @Mock private PathConfigDiagnostics pathConfigDiagnostics; + + @Before + public void setUp() { + router = new ParametricLanguageRouter(exec); + // pathConfigs = new PathConfigs(exec, pathConfigDiagnostics); + } + + private void registerPicoLanguage(boolean errorRecovery) { + var contribs = List.of( + errorRecovery ? "picoLanguageServerWithRecovery" : "picoLanguageServer", + errorRecovery ? "picoLanguageServerSlowSummaryWithRecovery" : "picoLanguageServerSlowSummary" + ); + + for (var contrib : contribs) { + router.registerLanguage(new LanguageParameter( + "pathConfig()", + "Pico", + new String[] {"pico"}, + "demo::lang::pico::LanguageServer", + contrib, + null)); + } + } + + private void registerSmallPicoLanguage() { + try { + var root = VF.sourceLocation("cwd", "", "../examples/pico-dsl-lsp"); + var pcfg = new PathConfig(root); + pcfg.addSourceLoc(URIUtil.getChildLocation(root, "src/main/rascal")); + pcfg = EvaluatorUtil.addLSPSources(pcfg, false); + router.registerLanguage(new LanguageParameter( + pcfg.asConstructor().toString(), + "SmallPico", + new String[] {"smallpico"}, + "lang::pico::LanguageServer", + "picoContributions", + null)); + } catch (IOException e) { + fail("Failed to add source location to path config: " + e); + } catch (URISyntaxException e) { + fail("Failed to build project root location: " + e); + } + } + + private void picoSanityCheck() { + ISourceLocation sourceFile; + try { + sourceFile = VF.sourceLocation("cwd", "", "src/main/rascal/library/demo/lang/pico/examples/fac.pico"); + } catch (URISyntaxException e) { + fail("Failed to build source location: " + e); + return; + } + + var uri = Locations.toUri(sourceFile).toString(); + var doc = new TextDocumentItem(uri, "parametric-rascalmpl", 1, ""); + var docId = new TextDocumentIdentifier(uri); + + router.didOpen(new DidOpenTextDocumentParams(doc)); + try { + var tokens = router.semanticTokensFull(new SemanticTokensParams(docId)).get(30, TimeUnit.SECONDS); + assertTrue(tokens.getData().size() > 0); + } catch (InterruptedException | ExecutionException | TimeoutException e) { + fail("Sanity check failed: " + e); + } + router.didClose(new DidCloseTextDocumentParams(docId)); + } + + @Test + public void registerPico() { + registerPicoLanguage(false); + picoSanityCheck(); + } + + @Test + public void registerSmallPico() { + registerSmallPicoLanguage(); + } + + @Test + public void registerTwoPicos() { + registerPicoLanguage(false); + registerSmallPicoLanguage(); + } +} From d08579bb25bfdbf0521c1796443817d73cb85c5b Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Wed, 15 Apr 2026 18:11:05 +0200 Subject: [PATCH 03/23] First implementation of single-language server and some router methods. --- .../vscode/lsp/BaseWorkspaceService.java | 11 +- .../parametric/ISingleLanguageService.java | 182 +++++++ .../parametric/ParametricLanguageRouter.java | 133 ++++-- .../lsp/parametric/SingleLanguageServer.java | 443 ++++++++++++++++++ 4 files changed, 729 insertions(+), 40 deletions(-) create mode 100644 rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ISingleLanguageService.java create mode 100644 rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/SingleLanguageServer.java 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 cadaf27fc..e0521fb10 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 @@ -67,7 +67,7 @@ public abstract class BaseWorkspaceService implements WorkspaceService, Language public static final String RASCAL_META_COMMAND = "rascal-meta-command"; public static final String RASCAL_COMMAND = "rascal-command"; - private final ExecutorService exec; + protected final ExecutorService exec; private @MonotonicNonNull IBaseTextDocumentService documentService; private final CopyOnWriteArrayList workspaceFolders = new CopyOnWriteArrayList<>(); @@ -77,7 +77,7 @@ protected BaseWorkspaceService(ExecutorService exec) { this.exec = exec; } - void pair(IBaseTextDocumentService documentService) { + public void pair(IBaseTextDocumentService documentService) { this.documentService = documentService; } @@ -104,6 +104,13 @@ IBaseTextDocumentService availableDocumentService() { return this.documentService; } + protected LanguageClient availableClient() { + if (this.client == null) { + throw new IllegalStateException("Language client not initialized"); + } + return this.client; + } + public List workspaceFolders() { return Collections.unmodifiableList(workspaceFolders); } diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ISingleLanguageService.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ISingleLanguageService.java new file mode 100644 index 000000000..459683e76 --- /dev/null +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ISingleLanguageService.java @@ -0,0 +1,182 @@ +/* + * 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 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.CodeAction; +import org.eclipse.lsp4j.CodeActionParams; +import org.eclipse.lsp4j.CodeLens; +import org.eclipse.lsp4j.CodeLensParams; +import org.eclipse.lsp4j.ColorInformation; +import org.eclipse.lsp4j.ColorPresentation; +import org.eclipse.lsp4j.ColorPresentationParams; +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.DeclarationParams; +import org.eclipse.lsp4j.DefinitionParams; +import org.eclipse.lsp4j.DeleteFilesParams; +import org.eclipse.lsp4j.DidChangeConfigurationParams; +import org.eclipse.lsp4j.DidChangeTextDocumentParams; +import org.eclipse.lsp4j.DidChangeWatchedFilesParams; +import org.eclipse.lsp4j.DidChangeWorkspaceFoldersParams; +import org.eclipse.lsp4j.DidCloseTextDocumentParams; +import org.eclipse.lsp4j.DidOpenTextDocumentParams; +import org.eclipse.lsp4j.DidSaveTextDocumentParams; +import org.eclipse.lsp4j.DocumentColorParams; +import org.eclipse.lsp4j.DocumentDiagnosticParams; +import org.eclipse.lsp4j.DocumentDiagnosticReport; +import org.eclipse.lsp4j.DocumentFormattingParams; +import org.eclipse.lsp4j.DocumentHighlight; +import org.eclipse.lsp4j.DocumentHighlightParams; +import org.eclipse.lsp4j.DocumentLink; +import org.eclipse.lsp4j.DocumentLinkParams; +import org.eclipse.lsp4j.DocumentOnTypeFormattingParams; +import org.eclipse.lsp4j.DocumentRangeFormattingParams; +import org.eclipse.lsp4j.DocumentSymbol; +import org.eclipse.lsp4j.DocumentSymbolParams; +import org.eclipse.lsp4j.ExecuteCommandParams; +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.InlineValue; +import org.eclipse.lsp4j.InlineValueParams; +import org.eclipse.lsp4j.LinkedEditingRangeParams; +import org.eclipse.lsp4j.LinkedEditingRanges; +import org.eclipse.lsp4j.Location; +import org.eclipse.lsp4j.LocationLink; +import org.eclipse.lsp4j.Moniker; +import org.eclipse.lsp4j.MonikerParams; +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.SignatureHelp; +import org.eclipse.lsp4j.SignatureHelpParams; +import org.eclipse.lsp4j.SymbolInformation; +import org.eclipse.lsp4j.TextEdit; +import org.eclipse.lsp4j.TypeDefinitionParams; +import org.eclipse.lsp4j.TypeHierarchyItem; +import org.eclipse.lsp4j.TypeHierarchyPrepareParams; +import org.eclipse.lsp4j.TypeHierarchySubtypesParams; +import org.eclipse.lsp4j.TypeHierarchySupertypesParams; +import org.eclipse.lsp4j.WillSaveTextDocumentParams; +import org.eclipse.lsp4j.WorkspaceDiagnosticParams; +import org.eclipse.lsp4j.WorkspaceDiagnosticReport; +import org.eclipse.lsp4j.WorkspaceEdit; +import org.eclipse.lsp4j.WorkspaceSymbol; +import org.eclipse.lsp4j.WorkspaceSymbolParams; +import org.eclipse.lsp4j.jsonrpc.messages.Either; +import org.eclipse.lsp4j.jsonrpc.messages.Either3; +import org.eclipse.lsp4j.services.TextDocumentService; +import org.eclipse.lsp4j.services.WorkspaceService; + +public interface ISingleLanguageService extends TextDocumentService, WorkspaceService { + @Override CompletableFuture> callHierarchyIncomingCalls(CallHierarchyIncomingCallsParams params); + @Override CompletableFuture> callHierarchyOutgoingCalls(CallHierarchyOutgoingCallsParams params); + @Override CompletableFuture>> codeAction(CodeActionParams params); + @Override CompletableFuture> codeLens(CodeLensParams params); + @Override CompletableFuture> colorPresentation(ColorPresentationParams params); + @Override CompletableFuture, CompletionList>> completion(CompletionParams position); + @Override CompletableFuture, List>> declaration(DeclarationParams params); + @Override CompletableFuture, List>> definition(DefinitionParams params); + @Override CompletableFuture diagnostic(DocumentDiagnosticParams params); + @Override void didChange(DidChangeTextDocumentParams params); + @Override void didClose(DidCloseTextDocumentParams params); + @Override void didOpen(DidOpenTextDocumentParams params); + @Override void didSave(DidSaveTextDocumentParams params); + @Override CompletableFuture> documentColor(DocumentColorParams params); + @Override CompletableFuture> documentHighlight(DocumentHighlightParams params); + @Override CompletableFuture> documentLink(DocumentLinkParams params); + @Override CompletableFuture documentLinkResolve(DocumentLink params); + @Override CompletableFuture>> documentSymbol(DocumentSymbolParams params); + @Override CompletableFuture> foldingRange(FoldingRangeRequestParams params); + @Override CompletableFuture> formatting(DocumentFormattingParams params); + @Override CompletableFuture hover(HoverParams params); + @Override CompletableFuture, List>> implementation(ImplementationParams params); + @Override CompletableFuture> inlayHint(InlayHintParams params); + @Override CompletableFuture> inlineValue(InlineValueParams params); + @Override CompletableFuture linkedEditingRange(LinkedEditingRangeParams params); + @Override CompletableFuture> moniker(MonikerParams params); + @Override CompletableFuture> onTypeFormatting(DocumentOnTypeFormattingParams params); + @Override CompletableFuture> prepareCallHierarchy(CallHierarchyPrepareParams params); + @Override CompletableFuture> prepareRename(PrepareRenameParams params); + @Override CompletableFuture> prepareTypeHierarchy(TypeHierarchyPrepareParams params); + @Override CompletableFuture> rangeFormatting(DocumentRangeFormattingParams params); + @Override CompletableFuture> references(ReferenceParams params); + @Override CompletableFuture rename(RenameParams params); + @Override CompletableFuture resolveCodeAction(CodeAction unresolved); + @Override CompletableFuture resolveCodeLens(CodeLens unresolved); + @Override CompletableFuture resolveCompletionItem(CompletionItem unresolved); + @Override CompletableFuture resolveInlayHint(InlayHint unresolved); + @Override CompletableFuture> selectionRange(SelectionRangeParams params); + @Override CompletableFuture semanticTokensFull(SemanticTokensParams params); + @Override CompletableFuture> semanticTokensFullDelta(SemanticTokensDeltaParams params); + @Override CompletableFuture semanticTokensRange(SemanticTokensRangeParams params); + @Override CompletableFuture signatureHelp(SignatureHelpParams params); + @Override CompletableFuture, List>> typeDefinition(TypeDefinitionParams params); + @Override CompletableFuture> typeHierarchySubtypes(TypeHierarchySubtypesParams params); + @Override CompletableFuture> typeHierarchySupertypes(TypeHierarchySupertypesParams params); + @Override void willSave(WillSaveTextDocumentParams params); + @Override CompletableFuture> willSaveWaitUntil(WillSaveTextDocumentParams params); + @Override CompletableFuture diagnostic(WorkspaceDiagnosticParams params); + @Override void didChangeConfiguration(DidChangeConfigurationParams params); + @Override void didChangeWatchedFiles(DidChangeWatchedFilesParams params); + @Override void didChangeWorkspaceFolders(DidChangeWorkspaceFoldersParams params); + @Override void didCreateFiles(CreateFilesParams params); + @Override void didDeleteFiles(DeleteFilesParams params); + @Override void didRenameFiles(RenameFilesParams params); + @Override CompletableFuture executeCommand(ExecuteCommandParams params); + @Override CompletableFuture resolveWorkspaceSymbol(WorkspaceSymbol workspaceSymbol); + @Override CompletableFuture, List>> symbol(WorkspaceSymbolParams params); + @Override CompletableFuture willCreateFiles(CreateFilesParams params); + @Override CompletableFuture willDeleteFiles(DeleteFilesParams params); + @Override CompletableFuture willRenameFiles(RenameFilesParams params); +} diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricLanguageRouter.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricLanguageRouter.java index 12abc101b..c4200a427 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricLanguageRouter.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricLanguageRouter.java @@ -26,146 +26,203 @@ */ package org.rascalmpl.vscode.lsp.parametric; +import java.util.Collections; import java.util.List; +import java.util.Map; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutorService; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.checkerframework.checker.nullness.qual.Nullable; import org.eclipse.lsp4j.ClientCapabilities; +import org.eclipse.lsp4j.CodeLensOptions; import org.eclipse.lsp4j.DidChangeTextDocumentParams; import org.eclipse.lsp4j.DidCloseTextDocumentParams; import org.eclipse.lsp4j.DidOpenTextDocumentParams; import org.eclipse.lsp4j.DidSaveTextDocumentParams; +import org.eclipse.lsp4j.ExecuteCommandOptions; import org.eclipse.lsp4j.RenameFilesParams; +import org.eclipse.lsp4j.RenameOptions; import org.eclipse.lsp4j.ServerCapabilities; +import org.eclipse.lsp4j.TextDocumentIdentifier; +import org.eclipse.lsp4j.TextDocumentItem; +import org.eclipse.lsp4j.TextDocumentSyncKind; +import org.eclipse.lsp4j.VersionedTextDocumentIdentifier; import org.eclipse.lsp4j.WorkspaceFolder; +import org.eclipse.lsp4j.services.TextDocumentService; +import org.rascalmpl.uri.URIUtil; import org.rascalmpl.util.locations.ColumnMaps; import org.rascalmpl.util.locations.LineColumnOffsetMap; import org.rascalmpl.vscode.lsp.BaseWorkspaceService; 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.parametric.capabilities.CapabilityRegistration; +import org.rascalmpl.vscode.lsp.parametric.capabilities.CompletionCapability; +import org.rascalmpl.vscode.lsp.parametric.capabilities.FileOperationCapability; +import org.rascalmpl.vscode.lsp.rascal.conversion.SemanticTokenizer; +import org.rascalmpl.vscode.lsp.util.locations.Locations; import io.usethesource.vallang.ISourceLocation; import io.usethesource.vallang.IValue; public class ParametricLanguageRouter extends BaseWorkspaceService implements IBaseTextDocumentService { + private final Map languageServices = new ConcurrentHashMap<>(); + private final Map languagesByExtension = new ConcurrentHashMap<>(); + + private @MonotonicNonNull CapabilityRegistration dynamicCapabilities; + protected ParametricLanguageRouter(ExecutorService exec) { super(exec); } + private ISingleLanguageService language(TextDocumentItem textDocument) { + return language(textDocument.getUri()); + } + + private TextDocumentService language(VersionedTextDocumentIdentifier textDocument) { + return language(textDocument.getUri()); + } + + private TextDocumentService language(TextDocumentIdentifier textDocument) { + return language(textDocument.getUri()); + } + + private ISingleLanguageService language(String uri) { + var ext = URIUtil.getExtension(URIUtil.assumeCorrectLocation(uri)); + var lang = languagesByExtension.get(ext); + if (lang == null) { + throw new IllegalStateException("No language exists for extension:" + ext); + } + var service = languageServices.get(ext); + if (service == null) { + throw new IllegalStateException("No language service exists for language: " + lang); + } + return service; + } + @Override public void didOpen(DidOpenTextDocumentParams params) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'didOpen'"); + language(params.getTextDocument()).didOpen(params); } @Override public void didChange(DidChangeTextDocumentParams params) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'didChange'"); + language(params.getTextDocument()).didChange(params); } @Override public void didClose(DidCloseTextDocumentParams params) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'didClose'"); + language(params.getTextDocument()).didClose(params); } @Override public void didSave(DidSaveTextDocumentParams params) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'didSave'"); + language(params.getTextDocument()).didSave(params); } @Override public void initializeServerCapabilities(ClientCapabilities clientCapabilities, ServerCapabilities result) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'initializeServerCapabilities'"); + // Since the initialize request is the very first request after connecting, we can initialize the capabilities here + // https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#initialize + dynamicCapabilities = new CapabilityRegistration(availableClient(), exec, clientCapabilities + , new CompletionCapability() + , /* new FileOperationCapability.DidCreateFiles(exec), */ new FileOperationCapability.DidRenameFiles(exec), new FileOperationCapability.DidDeleteFiles(exec) + ); + dynamicCapabilities.registerStaticCapabilities(result); + + var tokenizer = new SemanticTokenizer(); + + result.setDefinitionProvider(true); + result.setTextDocumentSync(TextDocumentSyncKind.Full); + result.setHoverProvider(true); + result.setReferencesProvider(true); + result.setDocumentSymbolProvider(true); + result.setImplementationProvider(true); + result.setSemanticTokensProvider(tokenizer.options()); + result.setCodeActionProvider(true); + result.setCodeLensProvider(new CodeLensOptions(false)); + result.setRenameProvider(new RenameOptions(true)); + result.setExecuteCommandProvider(new ExecuteCommandOptions(Collections.singletonList(getRascalMetaCommandName()))); + result.setInlayHintProvider(true); + result.setSelectionRangeProvider(true); + result.setFoldingRangeProvider(true); + result.setCallHierarchyProvider(true); } @Override public void shutdown() { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'shutdown'"); + // TODO Kill all delegate processes } @Override public void pair(BaseWorkspaceService workspaceService) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'pair'"); + pair(workspaceService); } @Override public void initialized() { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'initialized'"); + initialized(); } @Override public void registerLanguage(LanguageParameter lang) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'registerLanguage'"); + // Main workhorse + // TODO Start a delegate process with the right versions on the classpath for this language } @Override public void unregisterLanguage(LanguageParameter lang) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'unregisterLanguage'"); + // TODO Kill the delegate process for this language and clean up maps } @Override public void projectAdded(String name, ISourceLocation projectRoot) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'projectAdded'"); + // No need to do anything } @Override public void projectRemoved(String name, ISourceLocation projectRoot) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'projectRemoved'"); + // No need to do anything } @Override public CompletableFuture executeCommand(String languageName, String command) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'executeCommand'"); + // TODO Figure out what to do + return language(params).executeCommand(params); } @Override public LineColumnOffsetMap getColumnMap(ISourceLocation file) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'getColumnMap'"); + return language(Locations.toUri(file).toString()).getColumnMap(file); } @Override public ColumnMaps getColumnMaps() { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'getColumnMaps'"); + return language(params).getColumnMaps(params); } @Override public @Nullable TextDocumentState getDocumentState(ISourceLocation file) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'getDocumentState'"); + return language(params).getDocumentState(params); } @Override public boolean isManagingFile(ISourceLocation file) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'isManagingFile'"); + return language(params).isManagingFile(params); } @Override public void didRenameFiles(RenameFilesParams params, List workspaceFolders) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'didRenameFiles'"); + // TODO Split by language/extension and inform each delegate of their own renamed files + return language(params).didRenameFiles(params); } @Override public void cancelProgress(String progressId) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'cancelProgress'"); + // TODO Since we don't know from which language this progress came, probably inform everyone + return language(params).cancelProgress(params); } } diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/SingleLanguageServer.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/SingleLanguageServer.java new file mode 100644 index 000000000..33661e1ca --- /dev/null +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/SingleLanguageServer.java @@ -0,0 +1,443 @@ +/* + * 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 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.CodeAction; +import org.eclipse.lsp4j.CodeActionParams; +import org.eclipse.lsp4j.CodeLens; +import org.eclipse.lsp4j.CodeLensParams; +import org.eclipse.lsp4j.ColorInformation; +import org.eclipse.lsp4j.ColorPresentation; +import org.eclipse.lsp4j.ColorPresentationParams; +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.DeclarationParams; +import org.eclipse.lsp4j.DefinitionParams; +import org.eclipse.lsp4j.DeleteFilesParams; +import org.eclipse.lsp4j.DidChangeConfigurationParams; +import org.eclipse.lsp4j.DidChangeTextDocumentParams; +import org.eclipse.lsp4j.DidChangeWatchedFilesParams; +import org.eclipse.lsp4j.DidChangeWorkspaceFoldersParams; +import org.eclipse.lsp4j.DidCloseTextDocumentParams; +import org.eclipse.lsp4j.DidOpenTextDocumentParams; +import org.eclipse.lsp4j.DidSaveTextDocumentParams; +import org.eclipse.lsp4j.DocumentColorParams; +import org.eclipse.lsp4j.DocumentDiagnosticParams; +import org.eclipse.lsp4j.DocumentDiagnosticReport; +import org.eclipse.lsp4j.DocumentFormattingParams; +import org.eclipse.lsp4j.DocumentHighlight; +import org.eclipse.lsp4j.DocumentHighlightParams; +import org.eclipse.lsp4j.DocumentLink; +import org.eclipse.lsp4j.DocumentLinkParams; +import org.eclipse.lsp4j.DocumentOnTypeFormattingParams; +import org.eclipse.lsp4j.DocumentRangeFormattingParams; +import org.eclipse.lsp4j.DocumentSymbol; +import org.eclipse.lsp4j.DocumentSymbolParams; +import org.eclipse.lsp4j.ExecuteCommandParams; +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.InlineValue; +import org.eclipse.lsp4j.InlineValueParams; +import org.eclipse.lsp4j.LinkedEditingRangeParams; +import org.eclipse.lsp4j.LinkedEditingRanges; +import org.eclipse.lsp4j.Location; +import org.eclipse.lsp4j.LocationLink; +import org.eclipse.lsp4j.Moniker; +import org.eclipse.lsp4j.MonikerParams; +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.SignatureHelp; +import org.eclipse.lsp4j.SignatureHelpParams; +import org.eclipse.lsp4j.SymbolInformation; +import org.eclipse.lsp4j.TextEdit; +import org.eclipse.lsp4j.TypeDefinitionParams; +import org.eclipse.lsp4j.TypeHierarchyItem; +import org.eclipse.lsp4j.TypeHierarchyPrepareParams; +import org.eclipse.lsp4j.TypeHierarchySubtypesParams; +import org.eclipse.lsp4j.TypeHierarchySupertypesParams; +import org.eclipse.lsp4j.WillSaveTextDocumentParams; +import org.eclipse.lsp4j.WorkspaceDiagnosticParams; +import org.eclipse.lsp4j.WorkspaceDiagnosticReport; +import org.eclipse.lsp4j.WorkspaceEdit; +import org.eclipse.lsp4j.WorkspaceSymbol; +import org.eclipse.lsp4j.WorkspaceSymbolParams; +import org.eclipse.lsp4j.jsonrpc.messages.Either; +import org.eclipse.lsp4j.jsonrpc.messages.Either3; +import org.rascalmpl.vscode.lsp.util.NamedThreadPool; + +public class SingleLanguageServer implements ISingleLanguageService { + + private final ParametricTextDocumentService docService; + private final ParametricWorkspaceService wsService; + + private SingleLanguageServer(String langName) { + var exec = NamedThreadPool.cached(langName); + this.docService = new ParametricTextDocumentService(exec, null); + this.wsService = new ParametricWorkspaceService(exec); + this.docService.pair(wsService); + this.wsService.pair(docService); + } + + @Override + public void didOpen(DidOpenTextDocumentParams params) { + docService.didOpen(params); + } + + @Override + public void didChange(DidChangeTextDocumentParams params) { + docService.didChange(params); + } + + @Override + public void didClose(DidCloseTextDocumentParams params) { + docService.didClose(params); + } + + @Override + public void didSave(DidSaveTextDocumentParams params) { + docService.didSave(params); + } + + @Override + public void didChangeConfiguration(DidChangeConfigurationParams params) { + wsService.didChangeConfiguration(params); + } + + @Override + public void didChangeWatchedFiles(DidChangeWatchedFilesParams params) { + wsService.didChangeWatchedFiles(params); + } + + @Override + public CompletableFuture> callHierarchyIncomingCalls( + CallHierarchyIncomingCallsParams params) { + return docService.callHierarchyIncomingCalls(params); + } + + @Override + public CompletableFuture> callHierarchyOutgoingCalls( + CallHierarchyOutgoingCallsParams params) { + return docService.callHierarchyOutgoingCalls(params); + } + + @Override + public CompletableFuture>> codeAction(CodeActionParams params) { + return docService.codeAction(params); + } + + @Override + public CompletableFuture> codeLens(CodeLensParams params) { + return docService.codeLens(params); + } + + @Override + public CompletableFuture> colorPresentation(ColorPresentationParams params) { + return docService.colorPresentation(params); + } + + @Override + public CompletableFuture, CompletionList>> completion(CompletionParams position) { + return docService.completion(position); + } + + @Override + public CompletableFuture, List>> declaration( + DeclarationParams params) { + return docService.declaration(params); + } + + @Override + public CompletableFuture, List>> definition( + DefinitionParams params) { + return docService.definition(params); + } + + @Override + public CompletableFuture diagnostic(DocumentDiagnosticParams params) { + return docService.diagnostic(params); + } + + @Override + public CompletableFuture> documentColor(DocumentColorParams params) { + return docService.documentColor(params); + } + + @Override + public CompletableFuture> documentHighlight(DocumentHighlightParams params) { + return docService.documentHighlight(params); + } + + @Override + public CompletableFuture> documentLink(DocumentLinkParams params) { + return docService.documentLink(params); + } + + @Override + public CompletableFuture documentLinkResolve(DocumentLink params) { + return docService.documentLinkResolve(params); + } + + @Override + public CompletableFuture>> documentSymbol( + DocumentSymbolParams params) { + return docService.documentSymbol(params); + } + + @Override + public CompletableFuture> foldingRange(FoldingRangeRequestParams params) { + return docService.foldingRange(params); + } + + @Override + public CompletableFuture> formatting(DocumentFormattingParams params) { + return docService.formatting(params); + } + + @Override + public CompletableFuture hover(HoverParams params) { + return docService.hover(params); + } + + @Override + public CompletableFuture, List>> implementation( + ImplementationParams params) { + return docService.implementation(params); + } + + @Override + public CompletableFuture> inlayHint(InlayHintParams params) { + return docService.inlayHint(params); + } + + @Override + public CompletableFuture> inlineValue(InlineValueParams params) { + return docService.inlineValue(params); + } + + @Override + public CompletableFuture linkedEditingRange(LinkedEditingRangeParams params) { + return docService.linkedEditingRange(params); + } + + @Override + public CompletableFuture> moniker(MonikerParams params) { + return docService.moniker(params); + } + + @Override + public CompletableFuture> onTypeFormatting(DocumentOnTypeFormattingParams params) { + return docService.onTypeFormatting(params); + } + + @Override + public CompletableFuture> prepareCallHierarchy(CallHierarchyPrepareParams params) { + return docService.prepareCallHierarchy(params); + } + + @Override + public CompletableFuture> prepareRename( + PrepareRenameParams params) { + return docService.prepareRename(params); + } + + @Override + public CompletableFuture> prepareTypeHierarchy(TypeHierarchyPrepareParams params) { + return docService.prepareTypeHierarchy(params); + } + + @Override + public CompletableFuture> rangeFormatting(DocumentRangeFormattingParams params) { + return docService.rangeFormatting(params); + } + + @Override + public CompletableFuture> references(ReferenceParams params) { + return docService.references(params); + } + + @Override + public CompletableFuture rename(RenameParams params) { + return docService.rename(params); + } + + @Override + public CompletableFuture resolveCodeAction(CodeAction unresolved) { + return docService.resolveCodeAction(unresolved); + } + + @Override + public CompletableFuture resolveCodeLens(CodeLens unresolved) { + return docService.resolveCodeLens(unresolved); + } + + @Override + public CompletableFuture resolveCompletionItem(CompletionItem unresolved) { + return docService.resolveCompletionItem(unresolved); + } + + @Override + public CompletableFuture resolveInlayHint(InlayHint unresolved) { + return docService.resolveInlayHint(unresolved); + } + + @Override + public CompletableFuture> selectionRange(SelectionRangeParams params) { + return docService.selectionRange(params); + } + + @Override + public CompletableFuture semanticTokensFull(SemanticTokensParams params) { + return docService.semanticTokensFull(params); + } + + @Override + public CompletableFuture> semanticTokensFullDelta( + SemanticTokensDeltaParams params) { + return docService.semanticTokensFullDelta(params); + } + + @Override + public CompletableFuture semanticTokensRange(SemanticTokensRangeParams params) { + return docService.semanticTokensRange(params); + } + + @Override + public CompletableFuture signatureHelp(SignatureHelpParams params) { + return docService.signatureHelp(params); + } + + @Override + public CompletableFuture, List>> typeDefinition( + TypeDefinitionParams params) { + return docService.typeDefinition(params); + } + + @Override + public CompletableFuture> typeHierarchySubtypes(TypeHierarchySubtypesParams params) { + return docService.typeHierarchySubtypes(params); + } + + @Override + public CompletableFuture> typeHierarchySupertypes(TypeHierarchySupertypesParams params) { + return docService.typeHierarchySupertypes(params); + } + + @Override + public void willSave(WillSaveTextDocumentParams params) { + docService.willSave(params); + } + + @Override + public CompletableFuture> willSaveWaitUntil(WillSaveTextDocumentParams params) { + return docService.willSaveWaitUntil(params); + } + + @Override + public CompletableFuture diagnostic(WorkspaceDiagnosticParams params) { + return wsService.diagnostic(params); + } + + @Override + public void didChangeWorkspaceFolders(DidChangeWorkspaceFoldersParams params) { + wsService.didChangeWorkspaceFolders(params); + } + + @Override + public void didCreateFiles(CreateFilesParams params) { + docService.didCreateFiles(params); + } + + @Override + public void didDeleteFiles(DeleteFilesParams params) { + docService.didDeleteFiles(params); + } + + @Override + public void didRenameFiles(RenameFilesParams params) { + wsService.didRenameFiles(params); + } + + @Override + public CompletableFuture executeCommand(ExecuteCommandParams params) { + return wsService.executeCommand(params); + } + + @Override + public CompletableFuture resolveWorkspaceSymbol(WorkspaceSymbol workspaceSymbol) { + return wsService.resolveWorkspaceSymbol(workspaceSymbol); + } + + @Override + public CompletableFuture, List>> symbol( + WorkspaceSymbolParams params) { + return wsService.symbol(params); + } + + @Override + public CompletableFuture willCreateFiles(CreateFilesParams params) { + return wsService.willCreateFiles(params); + } + + @Override + public CompletableFuture willDeleteFiles(DeleteFilesParams params) { + return wsService.willDeleteFiles(params); + } + + @Override + public CompletableFuture willRenameFiles(RenameFilesParams params) { + return wsService.willRenameFiles(params); + } + +} From eea545ea3f9b2c60968594fe4ea5b3e50090120a Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Thu, 16 Apr 2026 11:15:19 +0200 Subject: [PATCH 04/23] Complete parametric router boilerplate. --- .../vscode/lsp/BaseWorkspaceService.java | 7 +- .../vscode/lsp/IBaseTextDocumentService.java | 3 - .../parametric/ISingleLanguageService.java | 2 + .../parametric/ParametricLanguageRouter.java | 168 ++++++++++++++---- .../parametric/ParametricLanguageServer.java | 15 +- .../ParametricTextDocumentService.java | 78 ++------ .../lsp/parametric/SingleLanguageServer.java | 9 +- .../lsp/rascal/RascalTextDocumentService.java | 2 + .../vscode/lsp/uri/FallbackResolver.java | 2 +- .../org/rascalmpl/vscode/lsp/util/Lists.java | 4 + .../ParametricLanguageRouterTest.java | 2 +- 11 files changed, 181 insertions(+), 111 deletions(-) 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 e0521fb10..a4352ae5f 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 @@ -26,13 +26,13 @@ */ package org.rascalmpl.vscode.lsp; -import com.google.gson.JsonPrimitive; import java.util.Collections; import java.util.List; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.ExecutorService; import java.util.stream.Collectors; +import org.apache.commons.lang3.NotImplementedException; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; @@ -55,7 +55,6 @@ import org.eclipse.lsp4j.services.LanguageClientAware; import org.eclipse.lsp4j.services.WorkspaceService; import org.rascalmpl.vscode.lsp.util.Nullables; -import org.rascalmpl.vscode.lsp.util.concurrent.CompletableFutureUtils; import org.rascalmpl.vscode.lsp.util.locations.Locations; public abstract class BaseWorkspaceService implements WorkspaceService, LanguageClientAware { @@ -188,6 +187,9 @@ public void didDeleteFiles(DeleteFilesParams params) { @Override public CompletableFuture executeCommand(ExecuteCommandParams commandParams) { logger.debug("workspace/executeCommand: {}", commandParams); + // TODO Split for Rascal and parametric + throw new NotImplementedException("BaseWorkspaceService::executeCommand"); + /* return CompletableFutureUtils.completedFuture(commandParams, exec) .thenCompose(params -> { if (params.getCommand().startsWith(RASCAL_META_COMMAND) || params.getCommand().startsWith(RASCAL_COMMAND)) { @@ -198,6 +200,7 @@ public CompletableFuture executeCommand(ExecuteCommandParams commandPara return CompletableFutureUtils.completedFuture(params.getCommand() + " was ignored.", exec); }); + */ } protected final ExecutorService getExecutor() { 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..b9e229103 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 @@ -28,7 +28,6 @@ import java.time.Duration; import java.util.List; -import java.util.concurrent.CompletableFuture; import org.checkerframework.checker.nullness.qual.Nullable; import org.eclipse.lsp4j.ClientCapabilities; import org.eclipse.lsp4j.CreateFilesParams; @@ -43,7 +42,6 @@ import org.rascalmpl.vscode.lsp.parametric.LanguageRegistry.LanguageParameter; import io.usethesource.vallang.ISourceLocation; -import io.usethesource.vallang.IValue; public interface IBaseTextDocumentService extends TextDocumentService { static final Duration NO_DEBOUNCE = Duration.ZERO; @@ -60,7 +58,6 @@ public interface IBaseTextDocumentService extends TextDocumentService { void projectAdded(String name, ISourceLocation projectRoot); void projectRemoved(String name, ISourceLocation projectRoot); - CompletableFuture executeCommand(String languageName, String command); LineColumnOffsetMap getColumnMap(ISourceLocation file); ColumnMaps getColumnMaps(); @Nullable TextDocumentState getDocumentState(ISourceLocation file); diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ISingleLanguageService.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ISingleLanguageService.java index 459683e76..aeecfa378 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ISingleLanguageService.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ISingleLanguageService.java @@ -119,6 +119,8 @@ import org.eclipse.lsp4j.services.WorkspaceService; public interface ISingleLanguageService extends TextDocumentService, WorkspaceService { + void cancelProgress(String progressId); + @Override CompletableFuture> callHierarchyIncomingCalls(CallHierarchyIncomingCallsParams params); @Override CompletableFuture> callHierarchyOutgoingCalls(CallHierarchyOutgoingCallsParams params); @Override CompletableFuture>> codeAction(CodeActionParams params); diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricLanguageRouter.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricLanguageRouter.java index c4200a427..a6429ea83 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricLanguageRouter.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricLanguageRouter.java @@ -29,13 +29,18 @@ import java.util.Collections; import java.util.List; import java.util.Map; -import java.util.concurrent.CompletableFuture; +import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutorService; +import java.util.stream.Collectors; +import org.apache.commons.lang3.NotImplementedException; +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; import org.eclipse.lsp4j.CodeLensOptions; +import org.eclipse.lsp4j.DeleteFilesParams; import org.eclipse.lsp4j.DidChangeTextDocumentParams; import org.eclipse.lsp4j.DidCloseTextDocumentParams; import org.eclipse.lsp4j.DidOpenTextDocumentParams; @@ -61,47 +66,83 @@ import org.rascalmpl.vscode.lsp.parametric.capabilities.CompletionCapability; import org.rascalmpl.vscode.lsp.parametric.capabilities.FileOperationCapability; import org.rascalmpl.vscode.lsp.rascal.conversion.SemanticTokenizer; -import org.rascalmpl.vscode.lsp.util.locations.Locations; +import org.rascalmpl.vscode.lsp.util.Lists; import io.usethesource.vallang.ISourceLocation; -import io.usethesource.vallang.IValue; public class ParametricLanguageRouter extends BaseWorkspaceService implements IBaseTextDocumentService { + private static final Logger logger = LogManager.getLogger(ParametricLanguageRouter.class); + + //// ROUTING + + // Map language name to remote service private final Map languageServices = new ConcurrentHashMap<>(); + // Map file extension to language name private final Map languagesByExtension = new ConcurrentHashMap<>(); + + // Server stuff + private final @Nullable LanguageParameter dedicatedLanguage; + private final String dedicatedLanguageName; + private @MonotonicNonNull CapabilityRegistration dynamicCapabilities; - protected ParametricLanguageRouter(ExecutorService exec) { + protected ParametricLanguageRouter(ExecutorService exec, @Nullable LanguageParameter dedicatedLanguage) { super(exec); + + this.dedicatedLanguage = dedicatedLanguage; + if (dedicatedLanguage == null) { + this.dedicatedLanguageName = ""; + } + else { + this.dedicatedLanguageName = dedicatedLanguage.getName(); + } } + //// LANGUAGE MANAGEMENT + private ISingleLanguageService language(TextDocumentItem textDocument) { - return language(textDocument.getUri()); + return language(URIUtil.assumeCorrectLocation(textDocument.getUri())); } private TextDocumentService language(VersionedTextDocumentIdentifier textDocument) { - return language(textDocument.getUri()); + return language(URIUtil.assumeCorrectLocation(textDocument.getUri())); } private TextDocumentService language(TextDocumentIdentifier textDocument) { - return language(textDocument.getUri()); + return language(URIUtil.assumeCorrectLocation(textDocument.getUri())); } - private ISingleLanguageService language(String uri) { - var ext = URIUtil.getExtension(URIUtil.assumeCorrectLocation(uri)); + private ISingleLanguageService language(ISourceLocation uri) { + var ext = extension(uri); var lang = languagesByExtension.get(ext); if (lang == null) { throw new IllegalStateException("No language exists for extension:" + ext); } - var service = languageServices.get(ext); + return language(lang); + } + + private ISingleLanguageService language(String langName) { + var service = languageServices.get(langName); if (service == null) { - throw new IllegalStateException("No language service exists for language: " + lang); + throw new IllegalStateException("No language service exists for language: " + langName); } return service; } + private static String extension(ISourceLocation doc) { + return URIUtil.getExtension(doc); + } + + private Optional safeLanguage(ISourceLocation uri) { + var ext = URIUtil.getExtension(uri); + var lang = languagesByExtension.get(ext); + return Optional.ofNullable(lang).map(this::language); + } + + //// WORKSPACE REQUESTS + @Override public void didOpen(DidOpenTextDocumentParams params) { language(params.getTextDocument()).didOpen(params); @@ -112,19 +153,59 @@ public void didChange(DidChangeTextDocumentParams params) { language(params.getTextDocument()).didChange(params); } + @Override + public void didSave(DidSaveTextDocumentParams params) { + language(params.getTextDocument()).didSave(params); + } + @Override public void didClose(DidCloseTextDocumentParams params) { language(params.getTextDocument()).didClose(params); } @Override - public void didSave(DidSaveTextDocumentParams params) { - language(params.getTextDocument()).didSave(params); + public void didDeleteFiles(DeleteFilesParams params) { + // Parameters contain a list of files. + // Since these files can belong to different languages, we map them to their respective language and + // delegate to several services. + params.getFiles().stream() + .collect(Collectors.toMap(f -> language(URIUtil.assumeCorrectLocation(f.getUri())), List::of, Lists::union)) + .entrySet() + .forEach(e -> e.getKey().didDeleteFiles(new DeleteFilesParams(e.getValue()))); + } + + @Override + public void didRenameFiles(RenameFilesParams params, List _workspaceFolders) { + // Parameters contain a list of files. + // Since these files can belong to different languages, we map them to their respective language and + // delegate to several services. + params.getFiles().stream() + .collect(Collectors.toMap(f -> language(URIUtil.assumeCorrectLocation(f.getOldUri())), List::of, Lists::union)) + .entrySet() + .forEach(e -> e.getKey().didRenameFiles(new RenameFilesParams(e.getValue()))); + } + + //// GLOBAL SERVER STUFF + + private String getRascalMetaCommandName() { + // 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()) { + return BaseWorkspaceService.RASCAL_META_COMMAND + "-" + dedicatedLanguageName; + } + return BaseWorkspaceService.RASCAL_META_COMMAND; + } + + private CapabilityRegistration availableCapabilities() { + if (dynamicCapabilities == null) { + throw new IllegalStateException("Dynamic capabilities are `null` - the document service did not yet connect to a client."); + } + return dynamicCapabilities; } @Override public void initializeServerCapabilities(ClientCapabilities clientCapabilities, ServerCapabilities result) { - // Since the initialize request is the very first request after connecting, we can initialize the capabilities here + // Since the initialize request is the very first request after connecting, we can initialize the capabilities here // https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#initialize dynamicCapabilities = new CapabilityRegistration(availableClient(), exec, clientCapabilities , new CompletionCapability() @@ -154,6 +235,7 @@ public void initializeServerCapabilities(ClientCapabilities clientCapabilities, @Override public void shutdown() { // TODO Kill all delegate processes + exec.shutdown(); } @Override @@ -163,13 +245,46 @@ public void pair(BaseWorkspaceService workspaceService) { @Override public void initialized() { - initialized(); + if (dedicatedLanguage != null) { + // if there was one scheduled, we now start it up, since the connection has been made + // and the client and capabilities are initialized + this.registerLanguage(dedicatedLanguage); + } + } + + private ISingleLanguageService getOrBuildLanguageService(LanguageParameter lang) { + return languageServices.computeIfAbsent(lang.getName(), l -> new SingleLanguageServer(l)); } @Override public void registerLanguage(LanguageParameter lang) { // Main workhorse // TODO Start a delegate process with the right versions on the classpath for this language + + logger.info("registerLanguage({})", lang.getName()); + + var langService = getOrBuildLanguageService(lang); + + for (var extension: lang.getExtensions()) { + this.languagesByExtension.put(extension, lang.getName()); + } + + // `CapabilityRegistration::update` should never be called asynchronously, since that might re-order incoming updates. + // Since `registerLanguage` is called from a single-threaded pool, calling it here is safe. + // Note: `CapabilityRegistration::update` returns a void future, which we do not have to wait on. + // TODO Dynamic registration of capabilities + // availableCapabilities().update(buildLanguageParams()); + + // If we opened any files with this extension before, now associate them with contributions + // TODO How to manage/update open files? + /* + var extensions = Arrays.asList(lang.getExtensions()); + for (var f : files.keySet()) { + if (extensions.contains(extension(f))) { + updateFileState(lang, f); + } + } + */ } @Override @@ -187,42 +302,29 @@ public void projectRemoved(String name, ISourceLocation projectRoot) { // No need to do anything } - @Override - public CompletableFuture executeCommand(String languageName, String command) { - // TODO Figure out what to do - return language(params).executeCommand(params); - } - @Override public LineColumnOffsetMap getColumnMap(ISourceLocation file) { - return language(Locations.toUri(file).toString()).getColumnMap(file); + throw new NotImplementedException("ParametricLanguageRouter::getColumnMap"); } @Override public ColumnMaps getColumnMaps() { - return language(params).getColumnMaps(params); + throw new NotImplementedException("ParametricLanguageRouter::getColumnMaps"); } @Override public @Nullable TextDocumentState getDocumentState(ISourceLocation file) { - return language(params).getDocumentState(params); + throw new NotImplementedException("ParametricLanguageRouter::getDocumentState"); } @Override public boolean isManagingFile(ISourceLocation file) { - return language(params).isManagingFile(params); - } - - @Override - public void didRenameFiles(RenameFilesParams params, List workspaceFolders) { - // TODO Split by language/extension and inform each delegate of their own renamed files - return language(params).didRenameFiles(params); + throw new NotImplementedException("ParametricLanguageRouter::isManagingFile"); } @Override public void cancelProgress(String progressId) { - // TODO Since we don't know from which language this progress came, probably inform everyone - return language(params).cancelProgress(params); + languageServices.values().stream().forEach(l -> l.cancelProgress(progressId)); } } 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..abd422e7a 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,12 +27,15 @@ package org.rascalmpl.vscode.lsp.parametric; +import com.google.gson.GsonBuilder; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Function; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; 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 { public static void main(String[] args) { LanguageParameter dedicatedLanguage; @@ -43,10 +46,14 @@ public static void main(String[] args) { dedicatedLanguage = null; } + AtomicReference<@MonotonicNonNull ParametricLanguageRouter> router = new AtomicReference<>(); + Function supplyService = exec -> + router.updateAndGet(v -> v != null ? v : new ParametricLanguageRouter(exec, dedicatedLanguage)); + startLanguageServer(NamedThreadPool.single("parametric-lsp") , NamedThreadPool.cached("parametric") - , threadPool -> new ParametricTextDocumentService(threadPool, dedicatedLanguage) - , ParametricWorkspaceService::new + , supplyService.andThen(Function.identity()) + , supplyService.andThen(Function.identity()) , 9999 ); } 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..75cdd21db 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 @@ -64,7 +64,6 @@ import org.eclipse.lsp4j.CodeAction; import org.eclipse.lsp4j.CodeActionParams; import org.eclipse.lsp4j.CodeLens; -import org.eclipse.lsp4j.CodeLensOptions; import org.eclipse.lsp4j.CodeLensParams; import org.eclipse.lsp4j.Command; import org.eclipse.lsp4j.CompletionItem; @@ -80,7 +79,6 @@ import org.eclipse.lsp4j.DidSaveTextDocumentParams; import org.eclipse.lsp4j.DocumentSymbol; import org.eclipse.lsp4j.DocumentSymbolParams; -import org.eclipse.lsp4j.ExecuteCommandOptions; import org.eclipse.lsp4j.FileDelete; import org.eclipse.lsp4j.FileRename; import org.eclipse.lsp4j.FoldingRange; @@ -103,7 +101,6 @@ import org.eclipse.lsp4j.Range; import org.eclipse.lsp4j.ReferenceParams; import org.eclipse.lsp4j.RenameFilesParams; -import org.eclipse.lsp4j.RenameOptions; import org.eclipse.lsp4j.RenameParams; import org.eclipse.lsp4j.SelectionRange; import org.eclipse.lsp4j.SelectionRangeParams; @@ -116,7 +113,6 @@ import org.eclipse.lsp4j.SymbolInformation; import org.eclipse.lsp4j.TextDocumentIdentifier; import org.eclipse.lsp4j.TextDocumentItem; -import org.eclipse.lsp4j.TextDocumentSyncKind; import org.eclipse.lsp4j.VersionedTextDocumentIdentifier; import org.eclipse.lsp4j.WorkspaceEdit; import org.eclipse.lsp4j.WorkspaceFolder; @@ -140,8 +136,6 @@ import org.rascalmpl.vscode.lsp.TextDocumentState; import org.rascalmpl.vscode.lsp.parametric.LanguageRegistry.LanguageParameter; import org.rascalmpl.vscode.lsp.parametric.capabilities.CapabilityRegistration; -import org.rascalmpl.vscode.lsp.parametric.capabilities.CompletionCapability; -import org.rascalmpl.vscode.lsp.parametric.capabilities.FileOperationCapability; import org.rascalmpl.vscode.lsp.parametric.capabilities.ICapabilityParams; import org.rascalmpl.vscode.lsp.parametric.model.ParametricFileFacts; import org.rascalmpl.vscode.lsp.parametric.model.ParametricSummary; @@ -183,7 +177,7 @@ public class ParametricTextDocumentService implements IBaseTextDocumentService, private final ExecutorService exec; - private final String dedicatedLanguageName; + private final String dedicatedLanguageName = ""; private final SemanticTokenizer tokenizer = new SemanticTokenizer(); private @MonotonicNonNull LanguageClient client; private @MonotonicNonNull BaseWorkspaceService workspaceService; @@ -199,8 +193,6 @@ public class ParametricTextDocumentService implements IBaseTextDocumentService, /** language to contribution */ private final Map contributions = new ConcurrentHashMap<>(); - private final @Nullable LanguageParameter dedicatedLanguage; - // Create "renamed" constructor of "FileSystemChange" so we can build a list of DocumentEdit objects for didRenameFiles private final TypeStore typeStore = new TypeStore(); private final TypeFactory tf = TypeFactory.getInstance(); @@ -209,21 +201,14 @@ 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) { // 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); - if (dedicatedLanguage == null) { - this.dedicatedLanguageName = ""; - this.dedicatedLanguage = null; - } - else { - this.dedicatedLanguageName = dedicatedLanguage.getName(); - this.dedicatedLanguage = dedicatedLanguage; - } + FallbackResolver.getInstance().registerTextDocumentService(this); } @@ -252,46 +237,9 @@ public String getContents(ISourceLocation file) { } } - private CapabilityRegistration availableCapabilities() { - if (dynamicCapabilities == null) { - throw new IllegalStateException("Dynamic capabilities are `null` - the document service did not yet connect to a client."); - } - return dynamicCapabilities; - } - - public void initializeServerCapabilities(ClientCapabilities clientCapabilities, final ServerCapabilities result) { - // Since the initialize request is the very first request after connecting, we can initialize the capabilities here - // https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#initialize - dynamicCapabilities = new CapabilityRegistration(availableClient(), exec, clientCapabilities - , new CompletionCapability() - , /* new FileOperationCapability.DidCreateFiles(exec), */ new FileOperationCapability.DidRenameFiles(exec), new FileOperationCapability.DidDeleteFiles(exec) - ); - dynamicCapabilities.registerStaticCapabilities(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.setCodeActionProvider(true); - result.setCodeLensProvider(new CodeLensOptions(false)); - result.setRenameProvider(new RenameOptions(true)); - result.setExecuteCommandProvider(new ExecuteCommandOptions(Collections.singletonList(getRascalMetaCommandName()))); - result.setInlayHintProvider(true); - result.setSelectionRangeProvider(true); - result.setFoldingRangeProvider(true); - result.setCallHierarchyProvider(true); - } - - private String getRascalMetaCommandName() { - // 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()) { - return BaseWorkspaceService.RASCAL_META_COMMAND + "-" + dedicatedLanguageName; - } - return BaseWorkspaceService.RASCAL_META_COMMAND; + @Override + public void initializeServerCapabilities(ClientCapabilities clientCapabilities, ServerCapabilities result) { + // Nothing to do } private BaseWorkspaceService availableWorkspaceService() { @@ -321,11 +269,7 @@ public void connect(LanguageClient client) { @Override public void initialized() { - if (dedicatedLanguage != null) { - // if there was one scheduled, we now start it up, since the connection has been made - // and the client and capabilities are initialized - this.registerLanguage(dedicatedLanguage); - } + // Nothing to do } // LSP interface methods @@ -1010,7 +954,8 @@ public synchronized void registerLanguage(LanguageParameter lang) { // `CapabilityRegistration::update` should never be called asynchronously, since that might re-order incoming updates. // Since `registerLanguage` is called from a single-threaded pool, calling it here is safe. // Note: `CapabilityRegistration::update` returns a void future, which we do not have to wait on. - availableCapabilities().update(buildLanguageParams()); + // TODO Update capabilities dynamically + // availableCapabilities().update(buildLanguageParams()); // If we opened any files with this extension before, now associate them with contributions var extensions = Arrays.asList(lang.getExtensions()); @@ -1087,7 +1032,8 @@ public synchronized void unregisterLanguage(LanguageParameter lang) { contributions.remove(lang.getName()); } - availableCapabilities().update(buildLanguageParams()); + // TODO Update capabilities dynamically + // availableCapabilities().update(buildLanguageParams()); } @Override @@ -1100,6 +1046,7 @@ public void projectRemoved(String name, ISourceLocation projectRoot) { // No need to do anything } + /* @Override public CompletableFuture executeCommand(String languageName, String command) { ILanguageContributions contribs = contributions.get(languageName); @@ -1112,6 +1059,7 @@ public CompletableFuture executeCommand(String languageName, String comm return CompletableFutureUtils.completedFuture(IRascalValueFactory.getInstance().string("No contributions configured for the language: " + languageName), exec); } } + */ @Override public boolean isManagingFile(ISourceLocation file) { diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/SingleLanguageServer.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/SingleLanguageServer.java index 33661e1ca..abed4d56b 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/SingleLanguageServer.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/SingleLanguageServer.java @@ -122,9 +122,9 @@ public class SingleLanguageServer implements ISingleLanguageService { private final ParametricTextDocumentService docService; private final ParametricWorkspaceService wsService; - private SingleLanguageServer(String langName) { + /*package*/ SingleLanguageServer(String langName) { var exec = NamedThreadPool.cached(langName); - this.docService = new ParametricTextDocumentService(exec, null); + this.docService = new ParametricTextDocumentService(exec); this.wsService = new ParametricWorkspaceService(exec); this.docService.pair(wsService); this.wsService.pair(docService); @@ -440,4 +440,9 @@ public CompletableFuture willRenameFiles(RenameFilesParams params return wsService.willRenameFiles(params); } + @Override + public void cancelProgress(String progressId) { + docService.cancelProgress(progressId); + } + } 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..544508107 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 @@ -719,10 +719,12 @@ private CodeLens makeRunCodeLens(CodeLensSuggestion detected) { ); } + /* @Override public CompletableFuture executeCommand(String extension, String command) { return availableRascalServices().executeCommand(command).get(); } + */ private static CompletableFuture recoverExceptions(CompletableFuture future, Supplier defaultValue) { return future diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/uri/FallbackResolver.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/uri/FallbackResolver.java index a722f327c..9e38e4a70 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/uri/FallbackResolver.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/uri/FallbackResolver.java @@ -79,7 +79,7 @@ public class FallbackResolver implements ISourceLocationInputOutput, ISourceLoca // making it avaible through this method, we allow the IBaseTextDocumentService implementations to interact with it. public static FallbackResolver getInstance() { if (instance == null) { - throw new IllegalStateException("FallbackResolver accessed before initialization"); + instance = new FallbackResolver(); } return instance; } diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/util/Lists.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/util/Lists.java index 4edaadb87..bee8c4e9e 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/util/Lists.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/util/Lists.java @@ -67,4 +67,8 @@ public static List union(List a, List b, List c) { result.addAll(c); return result; } + + public static T last(List l) { + return l.get(l.size() - 1); + } } diff --git a/rascal-lsp/src/test/java/org/rascalmpl/vscode/lsp/parametric/ParametricLanguageRouterTest.java b/rascal-lsp/src/test/java/org/rascalmpl/vscode/lsp/parametric/ParametricLanguageRouterTest.java index 4926d957b..71de9bc83 100644 --- a/rascal-lsp/src/test/java/org/rascalmpl/vscode/lsp/parametric/ParametricLanguageRouterTest.java +++ b/rascal-lsp/src/test/java/org/rascalmpl/vscode/lsp/parametric/ParametricLanguageRouterTest.java @@ -62,7 +62,7 @@ public class ParametricLanguageRouterTest { @Before public void setUp() { - router = new ParametricLanguageRouter(exec); + router = new ParametricLanguageRouter(exec, null); // pathConfigs = new PathConfigs(exec, pathConfigDiagnostics); } From e516b8798ba3ac51c15af95741eaa23151f371de Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Thu, 16 Apr 2026 13:08:51 +0200 Subject: [PATCH 05/23] Remove copies and unused endpoints. --- .../parametric/ISingleLanguageService.java | 150 ----------------- .../lsp/parametric/SingleLanguageServer.java | 158 ------------------ 2 files changed, 308 deletions(-) diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ISingleLanguageService.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ISingleLanguageService.java index aeecfa378..b745d3fab 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ISingleLanguageService.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ISingleLanguageService.java @@ -26,159 +26,9 @@ */ package org.rascalmpl.vscode.lsp.parametric; -import java.util.List; -import java.util.concurrent.CompletableFuture; -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.CodeAction; -import org.eclipse.lsp4j.CodeActionParams; -import org.eclipse.lsp4j.CodeLens; -import org.eclipse.lsp4j.CodeLensParams; -import org.eclipse.lsp4j.ColorInformation; -import org.eclipse.lsp4j.ColorPresentation; -import org.eclipse.lsp4j.ColorPresentationParams; -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.DeclarationParams; -import org.eclipse.lsp4j.DefinitionParams; -import org.eclipse.lsp4j.DeleteFilesParams; -import org.eclipse.lsp4j.DidChangeConfigurationParams; -import org.eclipse.lsp4j.DidChangeTextDocumentParams; -import org.eclipse.lsp4j.DidChangeWatchedFilesParams; -import org.eclipse.lsp4j.DidChangeWorkspaceFoldersParams; -import org.eclipse.lsp4j.DidCloseTextDocumentParams; -import org.eclipse.lsp4j.DidOpenTextDocumentParams; -import org.eclipse.lsp4j.DidSaveTextDocumentParams; -import org.eclipse.lsp4j.DocumentColorParams; -import org.eclipse.lsp4j.DocumentDiagnosticParams; -import org.eclipse.lsp4j.DocumentDiagnosticReport; -import org.eclipse.lsp4j.DocumentFormattingParams; -import org.eclipse.lsp4j.DocumentHighlight; -import org.eclipse.lsp4j.DocumentHighlightParams; -import org.eclipse.lsp4j.DocumentLink; -import org.eclipse.lsp4j.DocumentLinkParams; -import org.eclipse.lsp4j.DocumentOnTypeFormattingParams; -import org.eclipse.lsp4j.DocumentRangeFormattingParams; -import org.eclipse.lsp4j.DocumentSymbol; -import org.eclipse.lsp4j.DocumentSymbolParams; -import org.eclipse.lsp4j.ExecuteCommandParams; -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.InlineValue; -import org.eclipse.lsp4j.InlineValueParams; -import org.eclipse.lsp4j.LinkedEditingRangeParams; -import org.eclipse.lsp4j.LinkedEditingRanges; -import org.eclipse.lsp4j.Location; -import org.eclipse.lsp4j.LocationLink; -import org.eclipse.lsp4j.Moniker; -import org.eclipse.lsp4j.MonikerParams; -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.SignatureHelp; -import org.eclipse.lsp4j.SignatureHelpParams; -import org.eclipse.lsp4j.SymbolInformation; -import org.eclipse.lsp4j.TextEdit; -import org.eclipse.lsp4j.TypeDefinitionParams; -import org.eclipse.lsp4j.TypeHierarchyItem; -import org.eclipse.lsp4j.TypeHierarchyPrepareParams; -import org.eclipse.lsp4j.TypeHierarchySubtypesParams; -import org.eclipse.lsp4j.TypeHierarchySupertypesParams; -import org.eclipse.lsp4j.WillSaveTextDocumentParams; -import org.eclipse.lsp4j.WorkspaceDiagnosticParams; -import org.eclipse.lsp4j.WorkspaceDiagnosticReport; -import org.eclipse.lsp4j.WorkspaceEdit; -import org.eclipse.lsp4j.WorkspaceSymbol; -import org.eclipse.lsp4j.WorkspaceSymbolParams; -import org.eclipse.lsp4j.jsonrpc.messages.Either; -import org.eclipse.lsp4j.jsonrpc.messages.Either3; import org.eclipse.lsp4j.services.TextDocumentService; import org.eclipse.lsp4j.services.WorkspaceService; public interface ISingleLanguageService extends TextDocumentService, WorkspaceService { void cancelProgress(String progressId); - - @Override CompletableFuture> callHierarchyIncomingCalls(CallHierarchyIncomingCallsParams params); - @Override CompletableFuture> callHierarchyOutgoingCalls(CallHierarchyOutgoingCallsParams params); - @Override CompletableFuture>> codeAction(CodeActionParams params); - @Override CompletableFuture> codeLens(CodeLensParams params); - @Override CompletableFuture> colorPresentation(ColorPresentationParams params); - @Override CompletableFuture, CompletionList>> completion(CompletionParams position); - @Override CompletableFuture, List>> declaration(DeclarationParams params); - @Override CompletableFuture, List>> definition(DefinitionParams params); - @Override CompletableFuture diagnostic(DocumentDiagnosticParams params); - @Override void didChange(DidChangeTextDocumentParams params); - @Override void didClose(DidCloseTextDocumentParams params); - @Override void didOpen(DidOpenTextDocumentParams params); - @Override void didSave(DidSaveTextDocumentParams params); - @Override CompletableFuture> documentColor(DocumentColorParams params); - @Override CompletableFuture> documentHighlight(DocumentHighlightParams params); - @Override CompletableFuture> documentLink(DocumentLinkParams params); - @Override CompletableFuture documentLinkResolve(DocumentLink params); - @Override CompletableFuture>> documentSymbol(DocumentSymbolParams params); - @Override CompletableFuture> foldingRange(FoldingRangeRequestParams params); - @Override CompletableFuture> formatting(DocumentFormattingParams params); - @Override CompletableFuture hover(HoverParams params); - @Override CompletableFuture, List>> implementation(ImplementationParams params); - @Override CompletableFuture> inlayHint(InlayHintParams params); - @Override CompletableFuture> inlineValue(InlineValueParams params); - @Override CompletableFuture linkedEditingRange(LinkedEditingRangeParams params); - @Override CompletableFuture> moniker(MonikerParams params); - @Override CompletableFuture> onTypeFormatting(DocumentOnTypeFormattingParams params); - @Override CompletableFuture> prepareCallHierarchy(CallHierarchyPrepareParams params); - @Override CompletableFuture> prepareRename(PrepareRenameParams params); - @Override CompletableFuture> prepareTypeHierarchy(TypeHierarchyPrepareParams params); - @Override CompletableFuture> rangeFormatting(DocumentRangeFormattingParams params); - @Override CompletableFuture> references(ReferenceParams params); - @Override CompletableFuture rename(RenameParams params); - @Override CompletableFuture resolveCodeAction(CodeAction unresolved); - @Override CompletableFuture resolveCodeLens(CodeLens unresolved); - @Override CompletableFuture resolveCompletionItem(CompletionItem unresolved); - @Override CompletableFuture resolveInlayHint(InlayHint unresolved); - @Override CompletableFuture> selectionRange(SelectionRangeParams params); - @Override CompletableFuture semanticTokensFull(SemanticTokensParams params); - @Override CompletableFuture> semanticTokensFullDelta(SemanticTokensDeltaParams params); - @Override CompletableFuture semanticTokensRange(SemanticTokensRangeParams params); - @Override CompletableFuture signatureHelp(SignatureHelpParams params); - @Override CompletableFuture, List>> typeDefinition(TypeDefinitionParams params); - @Override CompletableFuture> typeHierarchySubtypes(TypeHierarchySubtypesParams params); - @Override CompletableFuture> typeHierarchySupertypes(TypeHierarchySupertypesParams params); - @Override void willSave(WillSaveTextDocumentParams params); - @Override CompletableFuture> willSaveWaitUntil(WillSaveTextDocumentParams params); - @Override CompletableFuture diagnostic(WorkspaceDiagnosticParams params); - @Override void didChangeConfiguration(DidChangeConfigurationParams params); - @Override void didChangeWatchedFiles(DidChangeWatchedFilesParams params); - @Override void didChangeWorkspaceFolders(DidChangeWorkspaceFoldersParams params); - @Override void didCreateFiles(CreateFilesParams params); - @Override void didDeleteFiles(DeleteFilesParams params); - @Override void didRenameFiles(RenameFilesParams params); - @Override CompletableFuture executeCommand(ExecuteCommandParams params); - @Override CompletableFuture resolveWorkspaceSymbol(WorkspaceSymbol workspaceSymbol); - @Override CompletableFuture, List>> symbol(WorkspaceSymbolParams params); - @Override CompletableFuture willCreateFiles(CreateFilesParams params); - @Override CompletableFuture willDeleteFiles(DeleteFilesParams params); - @Override CompletableFuture willRenameFiles(RenameFilesParams params); } diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/SingleLanguageServer.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/SingleLanguageServer.java index abed4d56b..2506438f6 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/SingleLanguageServer.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/SingleLanguageServer.java @@ -38,15 +38,11 @@ import org.eclipse.lsp4j.CodeActionParams; import org.eclipse.lsp4j.CodeLens; import org.eclipse.lsp4j.CodeLensParams; -import org.eclipse.lsp4j.ColorInformation; -import org.eclipse.lsp4j.ColorPresentation; -import org.eclipse.lsp4j.ColorPresentationParams; 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.DeclarationParams; import org.eclipse.lsp4j.DefinitionParams; import org.eclipse.lsp4j.DeleteFilesParams; import org.eclipse.lsp4j.DidChangeConfigurationParams; @@ -56,15 +52,7 @@ import org.eclipse.lsp4j.DidCloseTextDocumentParams; import org.eclipse.lsp4j.DidOpenTextDocumentParams; import org.eclipse.lsp4j.DidSaveTextDocumentParams; -import org.eclipse.lsp4j.DocumentColorParams; -import org.eclipse.lsp4j.DocumentDiagnosticParams; -import org.eclipse.lsp4j.DocumentDiagnosticReport; import org.eclipse.lsp4j.DocumentFormattingParams; -import org.eclipse.lsp4j.DocumentHighlight; -import org.eclipse.lsp4j.DocumentHighlightParams; -import org.eclipse.lsp4j.DocumentLink; -import org.eclipse.lsp4j.DocumentLinkParams; -import org.eclipse.lsp4j.DocumentOnTypeFormattingParams; import org.eclipse.lsp4j.DocumentRangeFormattingParams; import org.eclipse.lsp4j.DocumentSymbol; import org.eclipse.lsp4j.DocumentSymbolParams; @@ -76,14 +64,8 @@ import org.eclipse.lsp4j.ImplementationParams; import org.eclipse.lsp4j.InlayHint; import org.eclipse.lsp4j.InlayHintParams; -import org.eclipse.lsp4j.InlineValue; -import org.eclipse.lsp4j.InlineValueParams; -import org.eclipse.lsp4j.LinkedEditingRangeParams; -import org.eclipse.lsp4j.LinkedEditingRanges; import org.eclipse.lsp4j.Location; import org.eclipse.lsp4j.LocationLink; -import org.eclipse.lsp4j.Moniker; -import org.eclipse.lsp4j.MonikerParams; import org.eclipse.lsp4j.PrepareRenameDefaultBehavior; import org.eclipse.lsp4j.PrepareRenameParams; import org.eclipse.lsp4j.PrepareRenameResult; @@ -98,21 +80,9 @@ import org.eclipse.lsp4j.SemanticTokensDeltaParams; import org.eclipse.lsp4j.SemanticTokensParams; import org.eclipse.lsp4j.SemanticTokensRangeParams; -import org.eclipse.lsp4j.SignatureHelp; -import org.eclipse.lsp4j.SignatureHelpParams; import org.eclipse.lsp4j.SymbolInformation; import org.eclipse.lsp4j.TextEdit; -import org.eclipse.lsp4j.TypeDefinitionParams; -import org.eclipse.lsp4j.TypeHierarchyItem; -import org.eclipse.lsp4j.TypeHierarchyPrepareParams; -import org.eclipse.lsp4j.TypeHierarchySubtypesParams; -import org.eclipse.lsp4j.TypeHierarchySupertypesParams; -import org.eclipse.lsp4j.WillSaveTextDocumentParams; -import org.eclipse.lsp4j.WorkspaceDiagnosticParams; -import org.eclipse.lsp4j.WorkspaceDiagnosticReport; import org.eclipse.lsp4j.WorkspaceEdit; -import org.eclipse.lsp4j.WorkspaceSymbol; -import org.eclipse.lsp4j.WorkspaceSymbolParams; import org.eclipse.lsp4j.jsonrpc.messages.Either; import org.eclipse.lsp4j.jsonrpc.messages.Either3; import org.rascalmpl.vscode.lsp.util.NamedThreadPool; @@ -182,53 +152,17 @@ public CompletableFuture> codeLens(CodeLensParams param return docService.codeLens(params); } - @Override - public CompletableFuture> colorPresentation(ColorPresentationParams params) { - return docService.colorPresentation(params); - } - @Override public CompletableFuture, CompletionList>> completion(CompletionParams position) { return docService.completion(position); } - @Override - public CompletableFuture, List>> declaration( - DeclarationParams params) { - return docService.declaration(params); - } - @Override public CompletableFuture, List>> definition( DefinitionParams params) { return docService.definition(params); } - @Override - public CompletableFuture diagnostic(DocumentDiagnosticParams params) { - return docService.diagnostic(params); - } - - @Override - public CompletableFuture> documentColor(DocumentColorParams params) { - return docService.documentColor(params); - } - - @Override - public CompletableFuture> documentHighlight(DocumentHighlightParams params) { - return docService.documentHighlight(params); - } - - @Override - public CompletableFuture> documentLink(DocumentLinkParams params) { - return docService.documentLink(params); - } - - @Override - public CompletableFuture documentLinkResolve(DocumentLink params) { - return docService.documentLinkResolve(params); - } - @Override public CompletableFuture>> documentSymbol( DocumentSymbolParams params) { @@ -261,26 +195,6 @@ public CompletableFuture> inlayHint(InlayHintParams params) { return docService.inlayHint(params); } - @Override - public CompletableFuture> inlineValue(InlineValueParams params) { - return docService.inlineValue(params); - } - - @Override - public CompletableFuture linkedEditingRange(LinkedEditingRangeParams params) { - return docService.linkedEditingRange(params); - } - - @Override - public CompletableFuture> moniker(MonikerParams params) { - return docService.moniker(params); - } - - @Override - public CompletableFuture> onTypeFormatting(DocumentOnTypeFormattingParams params) { - return docService.onTypeFormatting(params); - } - @Override public CompletableFuture> prepareCallHierarchy(CallHierarchyPrepareParams params) { return docService.prepareCallHierarchy(params); @@ -292,11 +206,6 @@ public CompletableFuture> prepareTypeHierarchy(TypeHierarchyPrepareParams params) { - return docService.prepareTypeHierarchy(params); - } - @Override public CompletableFuture> rangeFormatting(DocumentRangeFormattingParams params) { return docService.rangeFormatting(params); @@ -312,26 +221,6 @@ public CompletableFuture rename(RenameParams params) { return docService.rename(params); } - @Override - public CompletableFuture resolveCodeAction(CodeAction unresolved) { - return docService.resolveCodeAction(unresolved); - } - - @Override - public CompletableFuture resolveCodeLens(CodeLens unresolved) { - return docService.resolveCodeLens(unresolved); - } - - @Override - public CompletableFuture resolveCompletionItem(CompletionItem unresolved) { - return docService.resolveCompletionItem(unresolved); - } - - @Override - public CompletableFuture resolveInlayHint(InlayHint unresolved) { - return docService.resolveInlayHint(unresolved); - } - @Override public CompletableFuture> selectionRange(SelectionRangeParams params) { return docService.selectionRange(params); @@ -353,42 +242,6 @@ public CompletableFuture semanticTokensRange(SemanticTokensRange return docService.semanticTokensRange(params); } - @Override - public CompletableFuture signatureHelp(SignatureHelpParams params) { - return docService.signatureHelp(params); - } - - @Override - public CompletableFuture, List>> typeDefinition( - TypeDefinitionParams params) { - return docService.typeDefinition(params); - } - - @Override - public CompletableFuture> typeHierarchySubtypes(TypeHierarchySubtypesParams params) { - return docService.typeHierarchySubtypes(params); - } - - @Override - public CompletableFuture> typeHierarchySupertypes(TypeHierarchySupertypesParams params) { - return docService.typeHierarchySupertypes(params); - } - - @Override - public void willSave(WillSaveTextDocumentParams params) { - docService.willSave(params); - } - - @Override - public CompletableFuture> willSaveWaitUntil(WillSaveTextDocumentParams params) { - return docService.willSaveWaitUntil(params); - } - - @Override - public CompletableFuture diagnostic(WorkspaceDiagnosticParams params) { - return wsService.diagnostic(params); - } - @Override public void didChangeWorkspaceFolders(DidChangeWorkspaceFoldersParams params) { wsService.didChangeWorkspaceFolders(params); @@ -414,17 +267,6 @@ public CompletableFuture executeCommand(ExecuteCommandParams params) { return wsService.executeCommand(params); } - @Override - public CompletableFuture resolveWorkspaceSymbol(WorkspaceSymbol workspaceSymbol) { - return wsService.resolveWorkspaceSymbol(workspaceSymbol); - } - - @Override - public CompletableFuture, List>> symbol( - WorkspaceSymbolParams params) { - return wsService.symbol(params); - } - @Override public CompletableFuture willCreateFiles(CreateFilesParams params) { return wsService.willCreateFiles(params); From 4baf07e47ce8814171e908b06248fb7a57271e54 Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Thu, 16 Apr 2026 13:09:27 +0200 Subject: [PATCH 06/23] Route LSP calls by language. --- .../parametric/ParametricLanguageRouter.java | 172 +++++++++++++++++- 1 file changed, 167 insertions(+), 5 deletions(-) diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricLanguageRouter.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricLanguageRouter.java index a6429ea83..76a5ae8e5 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricLanguageRouter.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricLanguageRouter.java @@ -29,31 +29,78 @@ import java.util.Collections; import java.util.List; import java.util.Map; -import java.util.Optional; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutorService; +import java.util.function.BiFunction; import java.util.stream.Collectors; import org.apache.commons.lang3.NotImplementedException; 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.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.CodeLensOptions; +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.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.DocumentFormattingParams; +import org.eclipse.lsp4j.DocumentRangeFormattingParams; +import org.eclipse.lsp4j.DocumentSymbol; +import org.eclipse.lsp4j.DocumentSymbolParams; import org.eclipse.lsp4j.ExecuteCommandOptions; +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.RenameOptions; +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.TextDocumentIdentifier; import org.eclipse.lsp4j.TextDocumentItem; import org.eclipse.lsp4j.TextDocumentSyncKind; +import org.eclipse.lsp4j.TextEdit; import org.eclipse.lsp4j.VersionedTextDocumentIdentifier; +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.TextDocumentService; import org.rascalmpl.uri.URIUtil; import org.rascalmpl.util.locations.ColumnMaps; @@ -135,10 +182,13 @@ private static String extension(ISourceLocation doc) { return URIUtil.getExtension(doc); } - private Optional safeLanguage(ISourceLocation uri) { - var ext = URIUtil.getExtension(uri); - var lang = languagesByExtension.get(ext); - return Optional.ofNullable(lang).map(this::language); + private R route(TextDocumentIdentifier textDocument, BiFunction func, P param) { + return route(textDocument, func, param); + } + + private R route(String uri, BiFunction func, P param) { + TextDocumentService lang = language(uri); + return func.apply(lang, param); } //// WORKSPACE REQUESTS @@ -327,4 +377,116 @@ public void cancelProgress(String progressId) { languageServices.values().stream().forEach(l -> l.cancelProgress(progressId)); } + @Override + public CompletableFuture> callHierarchyIncomingCalls( + CallHierarchyIncomingCallsParams params) { + return route(params.getItem().getUri(), TextDocumentService::callHierarchyIncomingCalls, params); + } + + @Override + public CompletableFuture> callHierarchyOutgoingCalls( + CallHierarchyOutgoingCallsParams params) { + return route(params.getItem().getUri(), TextDocumentService::callHierarchyOutgoingCalls, params); + } + + @Override + public CompletableFuture>> codeAction(CodeActionParams params) { + return route(params.getTextDocument(), TextDocumentService::codeAction, params); + } + + @Override + public CompletableFuture> codeLens(CodeLensParams params) { + return route(params.getTextDocument(), TextDocumentService::codeLens, params); + } + + @Override + public CompletableFuture, CompletionList>> completion(CompletionParams position) { + return route(position.getTextDocument(), TextDocumentService::completion, position); + } + + @Override + public CompletableFuture, List>> definition( + DefinitionParams params) { + return route(params.getTextDocument(), TextDocumentService::definition, params); + } + + @Override + public CompletableFuture>> documentSymbol( + DocumentSymbolParams params) { + return route(params.getTextDocument(), TextDocumentService::documentSymbol, params); + } + + @Override + public CompletableFuture> foldingRange(FoldingRangeRequestParams params) { + return route(params.getTextDocument(), TextDocumentService::foldingRange, params); + } + + @Override + public CompletableFuture> formatting(DocumentFormattingParams params) { + return route(params.getTextDocument(), TextDocumentService::formatting, params); + } + + @Override + public CompletableFuture hover(HoverParams params) { + return route(params.getTextDocument(), TextDocumentService::hover, params); + } + + @Override + public CompletableFuture, List>> implementation( + ImplementationParams params) { + return route(params.getTextDocument(), TextDocumentService::implementation, params); + } + + @Override + public CompletableFuture> inlayHint(InlayHintParams params) { + return route(params.getTextDocument(), TextDocumentService::inlayHint, params); + } + + @Override + public CompletableFuture> prepareCallHierarchy(CallHierarchyPrepareParams params) { + return route(params.getTextDocument(), TextDocumentService::prepareCallHierarchy, params); + } + + @Override + public CompletableFuture> prepareRename( + PrepareRenameParams params) { + return route(params.getTextDocument(), TextDocumentService::prepareRename, params); + } + + @Override + public CompletableFuture> rangeFormatting(DocumentRangeFormattingParams params) { + return route(params.getTextDocument(), TextDocumentService::rangeFormatting, params); + } + + @Override + public CompletableFuture> references(ReferenceParams params) { + return route(params.getTextDocument(), TextDocumentService::references, params); + } + + @Override + public CompletableFuture rename(RenameParams params) { + return route(params.getTextDocument(), TextDocumentService::rename, params); + } + + @Override + public CompletableFuture> selectionRange(SelectionRangeParams params) { + return route(params.getTextDocument(), TextDocumentService::selectionRange, params); + } + + @Override + public CompletableFuture semanticTokensFull(SemanticTokensParams params) { + return route(params.getTextDocument().getUri(), TextDocumentService::semanticTokensFull, params); + } + + @Override + public CompletableFuture> semanticTokensFullDelta( + SemanticTokensDeltaParams params) { + return route(params.getTextDocument().getUri(), TextDocumentService::semanticTokensFullDelta, params); + } + + @Override + public CompletableFuture semanticTokensRange(SemanticTokensRangeParams params) { + return route(params.getTextDocument(), TextDocumentService::semanticTokensRange, params); + } + } From a80ec90d5716aab4c5026434b9ac57834d281251 Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Thu, 16 Apr 2026 15:31:37 +0200 Subject: [PATCH 07/23] Extract open file content management. --- .../parametric/ParametricLanguageRouter.java | 32 ++--- .../ParametricTextDocumentService.java | 115 ++++----------- .../parametric/TextDocumentStateManager.java | 135 ++++++++++++++++++ 3 files changed, 177 insertions(+), 105 deletions(-) create mode 100644 rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/TextDocumentStateManager.java diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricLanguageRouter.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricLanguageRouter.java index 76a5ae8e5..852149590 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricLanguageRouter.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricLanguageRouter.java @@ -34,7 +34,6 @@ import java.util.concurrent.ExecutorService; import java.util.function.BiFunction; import java.util.stream.Collectors; -import org.apache.commons.lang3.NotImplementedException; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; @@ -114,6 +113,7 @@ import org.rascalmpl.vscode.lsp.parametric.capabilities.FileOperationCapability; import org.rascalmpl.vscode.lsp.rascal.conversion.SemanticTokenizer; import org.rascalmpl.vscode.lsp.util.Lists; +import org.rascalmpl.vscode.lsp.util.locations.Locations; import io.usethesource.vallang.ISourceLocation; @@ -135,6 +135,8 @@ public class ParametricLanguageRouter extends BaseWorkspaceService implements IB private @MonotonicNonNull CapabilityRegistration dynamicCapabilities; + private final TextDocumentStateManager files = new TextDocumentStateManager(); + protected ParametricLanguageRouter(ExecutorService exec, @Nullable LanguageParameter dedicatedLanguage) { super(exec); @@ -200,6 +202,7 @@ public void didOpen(DidOpenTextDocumentParams params) { @Override public void didChange(DidChangeTextDocumentParams params) { + files.updateFile(Locations.toLoc(params.getTextDocument())); language(params.getTextDocument()).didChange(params); } @@ -210,7 +213,9 @@ public void didSave(DidSaveTextDocumentParams params) { @Override public void didClose(DidCloseTextDocumentParams params) { - language(params.getTextDocument()).didClose(params); + var loc = Locations.toLoc(params.getTextDocument()); + files.removeFile(loc); + language(loc).didClose(params); } @Override @@ -222,6 +227,8 @@ public void didDeleteFiles(DeleteFilesParams params) { .collect(Collectors.toMap(f -> language(URIUtil.assumeCorrectLocation(f.getUri())), List::of, Lists::union)) .entrySet() .forEach(e -> e.getKey().didDeleteFiles(new DeleteFilesParams(e.getValue()))); + + // TODO Clear column maps for these files? (Parametric did not do this) } @Override @@ -233,6 +240,8 @@ public void didRenameFiles(RenameFilesParams params, List _work .collect(Collectors.toMap(f -> language(URIUtil.assumeCorrectLocation(f.getOldUri())), List::of, Lists::union)) .entrySet() .forEach(e -> e.getKey().didRenameFiles(new RenameFilesParams(e.getValue()))); + + // TODO Move column maps for these files? (Parametric did not do this) } //// GLOBAL SERVER STUFF @@ -324,17 +333,6 @@ public void registerLanguage(LanguageParameter lang) { // Note: `CapabilityRegistration::update` returns a void future, which we do not have to wait on. // TODO Dynamic registration of capabilities // availableCapabilities().update(buildLanguageParams()); - - // If we opened any files with this extension before, now associate them with contributions - // TODO How to manage/update open files? - /* - var extensions = Arrays.asList(lang.getExtensions()); - for (var f : files.keySet()) { - if (extensions.contains(extension(f))) { - updateFileState(lang, f); - } - } - */ } @Override @@ -354,22 +352,22 @@ public void projectRemoved(String name, ISourceLocation projectRoot) { @Override public LineColumnOffsetMap getColumnMap(ISourceLocation file) { - throw new NotImplementedException("ParametricLanguageRouter::getColumnMap"); + return files.getColumnMap(file); } @Override public ColumnMaps getColumnMaps() { - throw new NotImplementedException("ParametricLanguageRouter::getColumnMaps"); + return files.getColumnMaps(); } @Override public @Nullable TextDocumentState getDocumentState(ISourceLocation file) { - throw new NotImplementedException("ParametricLanguageRouter::getDocumentState"); + return files.getDocumentState(file); } @Override public boolean isManagingFile(ISourceLocation file) { - throw new NotImplementedException("ParametricLanguageRouter::isManagingFile"); + return files.isManagingFile(file); } @Override 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 75cdd21db..f335e838b 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 @@ -26,8 +26,6 @@ */ package org.rascalmpl.vscode.lsp.parametric; -import java.io.IOException; -import java.io.Reader; import java.time.Duration; import java.util.ArrayList; import java.util.Arrays; @@ -50,7 +48,6 @@ import org.apache.logging.log4j.Level; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import org.apache.logging.log4j.core.util.IOUtils; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.checkerframework.checker.nullness.qual.Nullable; import org.eclipse.lsp4j.ApplyWorkspaceEditParams; @@ -125,8 +122,6 @@ import org.eclipse.lsp4j.services.LanguageClientAware; import org.rascalmpl.uri.URIResolverRegistry; import org.rascalmpl.uri.URIUtil; -import org.rascalmpl.util.locations.ColumnMaps; -import org.rascalmpl.util.locations.LineColumnOffsetMap; import org.rascalmpl.values.IRascalValueFactory; import org.rascalmpl.values.parsetrees.ITree; import org.rascalmpl.values.parsetrees.TreeAdapter; @@ -171,7 +166,7 @@ import io.usethesource.vallang.type.TypeFactory; import io.usethesource.vallang.type.TypeStore; -public class ParametricTextDocumentService implements IBaseTextDocumentService, LanguageClientAware { +public class ParametricTextDocumentService extends TextDocumentStateManager implements IBaseTextDocumentService, LanguageClientAware { private static final IValueFactory VF = IRascalValueFactory.getInstance(); private static final Logger logger = LogManager.getLogger(ParametricTextDocumentService.class); @@ -183,9 +178,6 @@ public class ParametricTextDocumentService implements IBaseTextDocumentService, private @MonotonicNonNull BaseWorkspaceService workspaceService; private @MonotonicNonNull CapabilityRegistration dynamicCapabilities; - private final Map files; - private final ColumnMaps columns; - /** extension to language */ private final Map registeredExtensions = new ConcurrentHashMap<>(); /** language to facts */ @@ -203,40 +195,11 @@ public class ParametricTextDocumentService implements IBaseTextDocumentService, @SuppressWarnings({"initialization", "methodref.receiver.bound"}) // this::getContents public ParametricTextDocumentService(ExecutorService exec) { // 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); - + URIResolverRegistry.getInstance(); FallbackResolver.getInstance().registerTextDocumentService(this); } - @Override - public ColumnMaps getColumnMaps() { - return columns; - } - - @Override - public LineColumnOffsetMap getColumnMap(ISourceLocation file) { - return columns.get(file); - } - - public String getContents(ISourceLocation file) { - file = file.top(); - TextDocumentState ideState = files.get(file); - if (ideState != null) { - return ideState.getCurrentContent().get(); - } - try (Reader src = URIResolverRegistry.getInstance().getCharacterReader(file)) { - return IOUtils.toString(src); - } - catch (IOException e) { - logger.error("Error opening file {} to get contents", file, e); - return ""; - } - } - @Override public void initializeServerCapabilities(ClientCapabilities clientCapabilities, ServerCapabilities result) { // Nothing to do @@ -303,7 +266,7 @@ public void didSave(DidSaveTextDocumentParams params) { public void didClose(DidCloseTextDocumentParams params) { logger.debug("Did Close file: {}", params.getTextDocument()); var loc = Locations.toLoc(params.getTextDocument()); - if (files.remove(loc) == null) { + if (removeFile(loc)) { throw new ResponseErrorException(unknownFileError(loc, params)); } facts(loc).close(loc); @@ -355,14 +318,14 @@ private void triggerBuilder(TextDocumentIdentifier doc) { private void updateContents(VersionedTextDocumentIdentifier doc, String newContents, long timestamp) { logger.trace("New contents for {}", doc); TextDocumentState file = getFile(Locations.toLoc(doc)); - columns.clear(file.getLocation()); + updateFile(file.getLocation()); handleParsingErrors(file, file.update(doc.getVersion(), newContents, timestamp)); } private void handleParsingErrors(TextDocumentState file, CompletableFuture>> diagnosticsAsync) { diagnosticsAsync.thenAccept(diagnostics -> { List parseErrors = diagnostics.get().stream() - .map(diagnostic -> diagnostic.instantiate(columns)) + .map(diagnostic -> diagnostic.instantiate(getColumnMaps())) .collect(Collectors.toList()); logger.trace("Finished parsing tree, reporting new parse errors: {} for: {}", parseErrors, file.getLocation()); @@ -403,7 +366,7 @@ public CompletableFuture computeRenameRange(final ILanguageCon @Override public CompletableFuture rename(RenameParams params) { logger.trace("rename for: {}, new name: {}", params.getTextDocument().getUri(), params.getNewName()); - ISourceLocation loc = Locations.setPosition(Locations.toLoc(params.getTextDocument()), params.getPosition(), columns); + ISourceLocation loc = Locations.setPosition(Locations.toLoc(params.getTextDocument()), params.getPosition(), getColumnMaps()); ILanguageContributions contribs = contributions(loc); return getFile(loc) .getCurrentTreeAsync(true) @@ -440,7 +403,7 @@ private CompletableFuture computeRename(final ILanguageContributi .thenApply(tuple -> { IList documentEdits = (IList) tuple.get(0); showMessages(availableClient(), (ISet) tuple.get(1)); - return DocumentChanges.translateDocumentChanges(documentEdits, columns); + return DocumentChanges.translateDocumentChanges(documentEdits, getColumnMaps()); }) .get(); } @@ -511,7 +474,7 @@ public void didRenameFiles(RenameFilesParams params, List works return; } - WorkspaceEdit changes = DocumentChanges.translateDocumentChanges(edits, columns); + WorkspaceEdit changes = DocumentChanges.translateDocumentChanges(edits, getColumnMaps()); client.applyEdit(new ApplyWorkspaceEditParams(changes, "Rename files")).thenAccept(editResponse -> { if (!editResponse.isApplied()) { throw new RuntimeException("didRenameFiles resulted in a list of edits but applying them failed" @@ -591,7 +554,7 @@ private InlayHint rowToInlayHint(IValue v) { var atEnd = KeywordParameter.get("atEnd", tKW, false); // translate to lsp - var result = new InlayHint(Locations.toPosition(loc, columns, atEnd), Either.forLeft(label.trim())); + var result = new InlayHint(Locations.toPosition(loc, getColumnMaps(), atEnd), Either.forLeft(label.trim())); result.setKind(kind.getName().equals("type") ? InlayHintKind.Type : InlayHintKind.Parameter); result.setPaddingLeft(label.startsWith(" ")); result.setPaddingRight(label.endsWith(" ")); @@ -606,7 +569,7 @@ private CodeLens locCommandTupleToCodeLense(String languageName, IValue v) { ISourceLocation loc = (ISourceLocation) t.get(0); IConstructor command = (IConstructor) t.get(1); - return new CodeLens(Locations.toRange(loc, columns), CodeActions.constructorToCommand(dedicatedLanguageName, languageName, command), null); + return new CodeLens(Locations.toRange(loc, getColumnMaps()), CodeActions.constructorToCommand(dedicatedLanguageName, languageName, command), null); } private static T last(List l) { @@ -656,17 +619,7 @@ private ParametricFileFacts facts(ISourceLocation doc) { } private TextDocumentState open(TextDocumentItem doc, long timestamp) { - return files.computeIfAbsent(Locations.toLoc(doc), - l -> new TextDocumentState(contributions(l)::parsing, l, doc.getVersion(), doc.getText(), timestamp, exec)); - } - - private TextDocumentState getFile(ISourceLocation loc) { - loc = loc.top(); - TextDocumentState file = files.get(loc); - if (file == null) { - throw new ResponseErrorException(unknownFileError(loc, loc)); - } - return file; + return openFile(doc, contributions(Locations.toLoc(doc))::parsing, timestamp, exec); } public void shutdown() { @@ -715,7 +668,7 @@ public CompletableFuture>> docume .thenApply(Versioned::get) .thenApply(contrib::documentSymbol) .thenCompose(InterruptibleFuture::get) - .thenApply(documentSymbols -> DocumentSymbols.toLSP(documentSymbols, columns.get(file.getLocation()))) + .thenApply(documentSymbols -> DocumentSymbols.toLSP(documentSymbols, getColumnMap(file.getLocation()))) , Collections::emptyList); } @@ -723,7 +676,7 @@ public CompletableFuture>> docume public CompletableFuture>> codeAction(CodeActionParams params) { logger.debug("codeAction: {}", params); - var location = Locations.setPosition(Locations.toLoc(params.getTextDocument()), params.getRange().getStart(), columns); + var location = Locations.setPosition(Locations.toLoc(params.getTextDocument()), params.getRange().getStart(), getColumnMaps()); final ILanguageContributions contribs = contributions(location); // first we make a future stream for filtering out the "fixes" that were optionally sent along with earlier diagnostics @@ -834,17 +787,17 @@ public CompletableFuture> selectionRange(SelectionRangePara return recoverExceptions(file.getCurrentTreeAsync(true) .thenApply(Versioned::get) .thenCompose(t -> CompletableFutureUtils.reduce(params.getPositions().stream() - .map(p -> Locations.setPosition(loc, p, columns)) + .map(p -> Locations.setPosition(loc, p, getColumnMaps())) .map(p -> computeSelection .thenCompose(compute -> compute.apply(TreeSearch.computeFocusList(t, p.getBeginLine(), p.getBeginColumn()))) - .thenApply(selection -> SelectionRanges.toSelectionRange(p, selection, columns))) + .thenApply(selection -> SelectionRanges.toSelectionRange(p, selection, getColumnMaps()))) .collect(Collectors.toUnmodifiableList()), exec)), Collections::emptyList); } @Override public CompletableFuture> prepareCallHierarchy(CallHierarchyPrepareParams params) { - final var loc = Locations.setPosition(Locations.toLoc(params.getTextDocument()), params.getPosition(), columns); + final var loc = Locations.setPosition(Locations.toLoc(params.getTextDocument()), params.getPosition(), getColumnMaps()); final var contrib = contributions(loc); final var file = getFile(loc); @@ -857,7 +810,7 @@ public CompletableFuture> prepareCallHierarchy(CallHiera var ch = new CallHierarchy(exec); return items.stream() .map(IConstructor.class::cast) - .map(ci -> ch.toLSP(ci, columns)) + .map(ci -> ch.toLSP(ci, getColumnMaps())) .collect(Collectors.toList()); })), Collections::emptyList); } @@ -865,7 +818,7 @@ public CompletableFuture> prepareCallHierarchy(CallHiera private CompletableFuture> incomingOutgoingCalls(BiFunction, T> constructor, CallHierarchyItem source, CallHierarchy.Direction direction) { final var contrib = contributions(Locations.toLoc(source.getUri())); var ch = new CallHierarchy(exec); - return ch.toRascal(source, contrib::parseCallHierarchyData, columns) + return ch.toRascal(source, contrib::parseCallHierarchyData, getColumnMaps()) .thenCompose(sourceItem -> contrib.incomingOutgoingCalls(sourceItem, ch.direction(direction)).get()) .thenApply(callRel -> { // we need to maintain the order @@ -874,10 +827,10 @@ private CompletableFuture> incomingOutgoingCalls(BiFunction new ArrayList<>()); var callSite = (ISourceLocation)((ITuple)entry).get(1); - sites.add(Locations.toRange(callSite, columns)); + sites.add(Locations.toRange(callSite, getColumnMaps())); } return orderedEdges.entrySet().stream() - .map(entry -> constructor.apply(ch.toLSP(entry.getKey(), columns), entry.getValue())) + .map(entry -> constructor.apply(ch.toLSP(entry.getKey(), getColumnMaps()), entry.getValue())) .collect(Collectors.toList()); }); } @@ -896,7 +849,7 @@ public CompletableFuture> callHierarchyOutgoingC public CompletableFuture, CompletionList>> completion(CompletionParams params) { logger.debug("Completion: {} at {} with {}", params.getTextDocument(), params.getPosition(), params.getContext()); - var loc = Locations.setPosition(Locations.toLoc(params.getTextDocument()), params.getPosition(), columns); + var loc = Locations.setPosition(Locations.toLoc(params.getTextDocument()), params.getPosition(), getColumnMaps()); var contrib = contributions(loc); var file = getFile(loc); @@ -907,7 +860,7 @@ public CompletableFuture, CompletionList>> completio var focus = TreeSearch.computeFocusList(t, loc.getBeginLine(), loc.getBeginColumn()); var cursorOffset = loc.getBeginColumn() - TreeAdapter.getLocation((ITree) focus.get(0)).getBeginColumn(); return contrib.completion(focus, VF.integer(cursorOffset), completion.triggerKindToRascal(params.getContext())).get() - .thenApply(ci -> completion.toLSP(this, ci, dedicatedLanguageName, contrib.getName(), loc.getBeginLine(), columns.get(loc))); + .thenApply(ci -> completion.toLSP(this, ci, dedicatedLanguageName, contrib.getName(), loc.getBeginLine(), getColumnMap(loc))); }) .thenApply(Either::forLeft), () -> Either.forLeft(Collections.emptyList())); } @@ -920,7 +873,7 @@ public synchronized void registerLanguage(LanguageParameter lang) { t -> new LanguageContributionsMultiplexer(lang.getName(), exec) ); var fact = facts.computeIfAbsent(lang.getName(), t -> - new ParametricFileFacts(exec, columns, multiplexer) + new ParametricFileFacts(exec, getColumnMaps(), multiplexer) ); var parserConfig = lang.getPrecompiledParser(); @@ -959,7 +912,7 @@ public synchronized void registerLanguage(LanguageParameter lang) { // If we opened any files with this extension before, now associate them with contributions var extensions = Arrays.asList(lang.getExtensions()); - for (var f : files.keySet()) { + for (var f : getOpenFiles()) { if (extensions.contains(extension(f))) { updateFileState(lang, f); } @@ -988,9 +941,7 @@ public Set fileExtensions() { private void updateFileState(LanguageParameter lang, ISourceLocation f) { f = f.top(); logger.trace("File of language {} - updating state: {}", lang.getName(), f); - // Since we cannot know what happened to this file before we were called, we need to be careful about races. - // It might have been closed in the meantime, so we compute the new value if the key still exists, based on the current value. - var state = files.computeIfPresent(f, (loc, currentState) -> currentState.changeParser(contributions(loc)::parsing)); + var state = super.updateFileState(f, contributions(f)::parsing); if (state == null) { logger.debug("Updating the parser of {} failed, since it was closed.", f); return; @@ -1061,23 +1012,11 @@ public CompletableFuture executeCommand(String languageName, String comm } */ - @Override - public boolean isManagingFile(ISourceLocation file) { - return files.containsKey(file.top()); - } - - @Override - public @Nullable TextDocumentState getDocumentState(ISourceLocation file) { - return files.get(file.top()); - } - @Override public void cancelProgress(String progressId) { contributions.values().forEach(plex -> plex.cancelProgress(progressId)); } - private ResponseError unknownFileError(ISourceLocation loc, Object data) { - return new ResponseError(ResponseErrorCode.RequestFailed, "Unknown file: " + loc, data); - } + } diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/TextDocumentStateManager.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/TextDocumentStateManager.java new file mode 100644 index 000000000..fc4a7f9d2 --- /dev/null +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/TextDocumentStateManager.java @@ -0,0 +1,135 @@ +/* + * 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.io.IOException; +import java.io.Reader; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutorService; +import java.util.function.BiFunction; +import org.apache.commons.io.IOUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.eclipse.lsp4j.TextDocumentItem; +import org.eclipse.lsp4j.jsonrpc.ResponseErrorException; +import org.eclipse.lsp4j.jsonrpc.messages.ResponseError; +import org.eclipse.lsp4j.jsonrpc.messages.ResponseErrorCode; +import org.rascalmpl.uri.URIResolverRegistry; +import org.rascalmpl.util.locations.ColumnMaps; +import org.rascalmpl.util.locations.LineColumnOffsetMap; +import org.rascalmpl.values.parsetrees.ITree; +import org.rascalmpl.vscode.lsp.TextDocumentState; +import org.rascalmpl.vscode.lsp.util.locations.Locations; + +import io.usethesource.vallang.ISourceLocation; + +public class TextDocumentStateManager { + + private static final Logger logger = LogManager.getLogger(TextDocumentStateManager.class); + + private final Map files = new ConcurrentHashMap<>(); + private final ColumnMaps columns = new ColumnMaps(this::getContents); + + public String getContents(ISourceLocation file) { + file = file.top(); + TextDocumentState ideState = getFile(file); + if (ideState != null) { + return ideState.getCurrentContent().get(); + } + try (Reader src = URIResolverRegistry.getInstance().getCharacterReader(file)) { + return IOUtils.toString(src); + } + catch (IOException e) { + logger.error("Error opening file {} to get contents", file, e); + return ""; + } + } + + public ColumnMaps getColumnMaps() { + return columns; + } + + public boolean isManagingFile(ISourceLocation loc) { + return files.containsKey(loc.top()); + } + + public @Nullable TextDocumentState getDocumentState(ISourceLocation file) { + return files.get(file.top()); + } + + public LineColumnOffsetMap getColumnMap(ISourceLocation loc) { + return columns.get(loc.top()); + } + + TextDocumentState getFile(ISourceLocation loc) { + loc = loc.top(); + TextDocumentState file = files.get(loc); + if (file == null) { + throw new ResponseErrorException(unknownFileError(loc, loc)); + } + return file; + } + + TextDocumentState openFile(TextDocumentItem doc, BiFunction> parser, long timestamp, ExecutorService exec) { + return files.computeIfAbsent(Locations.toLoc(doc), + l -> new TextDocumentState(parser, l, doc.getVersion(), doc.getText(), timestamp, exec)); + } + + void updateFile(ISourceLocation loc) { + columns.clear(loc.top()); + } + + boolean removeFile(ISourceLocation loc) { + updateFile(loc); + return files.remove(loc.top()) == null; + } + + @Nullable TextDocumentState updateFileState(ISourceLocation f, BiFunction> parser) { + f = f.top(); + logger.trace("Updating state: {}", f); + + // Since we cannot know what happened to this file before we were called, we need to be careful about races. + // It might have been closed in the meantime, so we compute the new value if the key still exists, based on the current value. + var state = files.computeIfPresent(f, (loc, currentState) -> currentState.changeParser(parser)); + if (state == null) { + logger.debug("Updating the parser of {} failed, since it was closed.", f); + } + return state; + } + + Set getOpenFiles() { + return files.keySet(); + } + + static ResponseError unknownFileError(ISourceLocation loc, Object data) { + return new ResponseError(ResponseErrorCode.RequestFailed, "Unknown file: " + loc, data); + } +} From 3d06f3ff872f4d3fc854c2e4ba32e304d688a4db Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Thu, 16 Apr 2026 15:46:40 +0200 Subject: [PATCH 08/23] Remove language on unregister. --- .../vscode/lsp/parametric/ParametricLanguageRouter.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricLanguageRouter.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricLanguageRouter.java index 852149590..1cb3e3052 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricLanguageRouter.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricLanguageRouter.java @@ -337,6 +337,8 @@ public void registerLanguage(LanguageParameter lang) { @Override public void unregisterLanguage(LanguageParameter lang) { + logger.info("unregisterLanguage({})", lang.getName()); + var removedLang = languageServices.remove(lang.getName()); // TODO Kill the delegate process for this language and clean up maps } From 58001106674a0203f3bfe57a16c7a69466b65a14 Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Thu, 16 Apr 2026 16:01:25 +0200 Subject: [PATCH 09/23] Fix accidental endless recursions. --- .../vscode/lsp/parametric/ParametricLanguageRouter.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricLanguageRouter.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricLanguageRouter.java index 1cb3e3052..c7c21f720 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricLanguageRouter.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricLanguageRouter.java @@ -185,7 +185,7 @@ private static String extension(ISourceLocation doc) { } private R route(TextDocumentIdentifier textDocument, BiFunction func, P param) { - return route(textDocument, func, param); + return route(textDocument.getUri(), func, param); } private R route(String uri, BiFunction func, P param) { @@ -299,7 +299,7 @@ public void shutdown() { @Override public void pair(BaseWorkspaceService workspaceService) { - pair(workspaceService); + // Nothing to do; no need to pair with ourselves } @Override From 9c336eeb63472def07fcee1a124461fa4a1d11a2 Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Thu, 16 Apr 2026 16:10:52 +0200 Subject: [PATCH 10/23] Fix string ambiguities. --- .../parametric/ParametricLanguageRouter.java | 24 ++++++++----------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricLanguageRouter.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricLanguageRouter.java index c7c21f720..37e07f654 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricLanguageRouter.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricLanguageRouter.java @@ -169,13 +169,9 @@ private ISingleLanguageService language(ISourceLocation uri) { if (lang == null) { throw new IllegalStateException("No language exists for extension:" + ext); } - return language(lang); - } - - private ISingleLanguageService language(String langName) { - var service = languageServices.get(langName); + var service = languageServices.get(lang); if (service == null) { - throw new IllegalStateException("No language service exists for language: " + langName); + throw new IllegalStateException("No language service exists for language: " + lang); } return service; } @@ -184,12 +180,12 @@ private static String extension(ISourceLocation doc) { return URIUtil.getExtension(doc); } - private R route(TextDocumentIdentifier textDocument, BiFunction func, P param) { - return route(textDocument.getUri(), func, param); + private R route(TextDocumentIdentifier file, BiFunction func, P param) { + return route(Locations.toLoc(file.getUri()), func, param); } - private R route(String uri, BiFunction func, P param) { - TextDocumentService lang = language(uri); + private R route(ISourceLocation file, BiFunction func, P param) { + TextDocumentService lang = language(file); return func.apply(lang, param); } @@ -380,13 +376,13 @@ public void cancelProgress(String progressId) { @Override public CompletableFuture> callHierarchyIncomingCalls( CallHierarchyIncomingCallsParams params) { - return route(params.getItem().getUri(), TextDocumentService::callHierarchyIncomingCalls, params); + return route(Locations.toLoc(params.getItem().getUri()), TextDocumentService::callHierarchyIncomingCalls, params); } @Override public CompletableFuture> callHierarchyOutgoingCalls( CallHierarchyOutgoingCallsParams params) { - return route(params.getItem().getUri(), TextDocumentService::callHierarchyOutgoingCalls, params); + return route(Locations.toLoc(params.getItem().getUri()), TextDocumentService::callHierarchyOutgoingCalls, params); } @Override @@ -475,13 +471,13 @@ public CompletableFuture> selectionRange(SelectionRangePara @Override public CompletableFuture semanticTokensFull(SemanticTokensParams params) { - return route(params.getTextDocument().getUri(), TextDocumentService::semanticTokensFull, params); + return route(params.getTextDocument(), TextDocumentService::semanticTokensFull, params); } @Override public CompletableFuture> semanticTokensFullDelta( SemanticTokensDeltaParams params) { - return route(params.getTextDocument().getUri(), TextDocumentService::semanticTokensFullDelta, params); + return route(params.getTextDocument(), TextDocumentService::semanticTokensFullDelta, params); } @Override From 44b2aeb96af0ed52836381cf75c74e2f96d1c759 Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Thu, 16 Apr 2026 16:11:27 +0200 Subject: [PATCH 11/23] Add TODO note for later. --- .../java/org/rascalmpl/vscode/lsp/IBaseTextDocumentService.java | 1 + 1 file changed, 1 insertion(+) 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 b9e229103..031f574fb 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 @@ -60,6 +60,7 @@ public interface IBaseTextDocumentService extends TextDocumentService { LineColumnOffsetMap getColumnMap(ISourceLocation file); ColumnMaps getColumnMaps(); + // Simplify return type to something that can be serialized @Nullable TextDocumentState getDocumentState(ISourceLocation file); boolean isManagingFile(ISourceLocation file); From 69ef3d5529a6bfae3cc9445b1f8d5f9b2f3b9bbb Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Thu, 16 Apr 2026 16:14:28 +0200 Subject: [PATCH 12/23] Propagate language registrations. --- .../vscode/lsp/IBaseTextDocumentService.java | 2 +- .../vscode/lsp/parametric/ISingleLanguageService.java | 3 +++ .../lsp/parametric/ParametricLanguageRouter.java | 2 ++ .../vscode/lsp/parametric/SingleLanguageServer.java | 11 +++++++++++ 4 files changed, 17 insertions(+), 1 deletion(-) 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 031f574fb..b1a980c83 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 @@ -60,7 +60,7 @@ public interface IBaseTextDocumentService extends TextDocumentService { LineColumnOffsetMap getColumnMap(ISourceLocation file); ColumnMaps getColumnMaps(); - // Simplify return type to something that can be serialized + // TODO Simplify return type to something that can be serialized over JSON-RPC @Nullable TextDocumentState getDocumentState(ISourceLocation file); boolean isManagingFile(ISourceLocation file); diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ISingleLanguageService.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ISingleLanguageService.java index b745d3fab..3c1bc68a4 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ISingleLanguageService.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ISingleLanguageService.java @@ -28,7 +28,10 @@ import org.eclipse.lsp4j.services.TextDocumentService; import org.eclipse.lsp4j.services.WorkspaceService; +import org.rascalmpl.vscode.lsp.parametric.LanguageRegistry.LanguageParameter; public interface ISingleLanguageService extends TextDocumentService, WorkspaceService { void cancelProgress(String progressId); + void registerLanguage(LanguageParameter lang); + void unregisterLanguage(LanguageParameter lang); } diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricLanguageRouter.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricLanguageRouter.java index 37e07f654..a6ae7c466 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricLanguageRouter.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricLanguageRouter.java @@ -319,6 +319,7 @@ public void registerLanguage(LanguageParameter lang) { logger.info("registerLanguage({})", lang.getName()); var langService = getOrBuildLanguageService(lang); + langService.registerLanguage(lang); for (var extension: lang.getExtensions()) { this.languagesByExtension.put(extension, lang.getName()); @@ -335,6 +336,7 @@ public void registerLanguage(LanguageParameter lang) { public void unregisterLanguage(LanguageParameter lang) { logger.info("unregisterLanguage({})", lang.getName()); var removedLang = languageServices.remove(lang.getName()); + removedLang.unregisterLanguage(lang); // TODO Kill the delegate process for this language and clean up maps } diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/SingleLanguageServer.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/SingleLanguageServer.java index 2506438f6..23073a0c8 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/SingleLanguageServer.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/SingleLanguageServer.java @@ -85,6 +85,7 @@ import org.eclipse.lsp4j.WorkspaceEdit; import org.eclipse.lsp4j.jsonrpc.messages.Either; import org.eclipse.lsp4j.jsonrpc.messages.Either3; +import org.rascalmpl.vscode.lsp.parametric.LanguageRegistry.LanguageParameter; import org.rascalmpl.vscode.lsp.util.NamedThreadPool; public class SingleLanguageServer implements ISingleLanguageService { @@ -287,4 +288,14 @@ public void cancelProgress(String progressId) { docService.cancelProgress(progressId); } + @Override + public void registerLanguage(LanguageParameter lang) { + docService.registerLanguage(lang); + } + + @Override + public void unregisterLanguage(LanguageParameter lang) { + docService.unregisterLanguage(lang); + } + } From 47488e224bce9f5b2f39617649330b53d20a9bd8 Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Thu, 16 Apr 2026 16:30:28 +0200 Subject: [PATCH 13/23] Connect client to delegate servers. --- .../vscode/lsp/parametric/ISingleLanguageService.java | 3 ++- .../vscode/lsp/parametric/ParametricLanguageRouter.java | 6 +++++- .../vscode/lsp/parametric/SingleLanguageServer.java | 7 +++++++ 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ISingleLanguageService.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ISingleLanguageService.java index 3c1bc68a4..e5946848b 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ISingleLanguageService.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ISingleLanguageService.java @@ -26,11 +26,12 @@ */ package org.rascalmpl.vscode.lsp.parametric; +import org.eclipse.lsp4j.services.LanguageClientAware; import org.eclipse.lsp4j.services.TextDocumentService; import org.eclipse.lsp4j.services.WorkspaceService; import org.rascalmpl.vscode.lsp.parametric.LanguageRegistry.LanguageParameter; -public interface ISingleLanguageService extends TextDocumentService, WorkspaceService { +public interface ISingleLanguageService extends TextDocumentService, WorkspaceService, LanguageClientAware { void cancelProgress(String progressId); void registerLanguage(LanguageParameter lang); void unregisterLanguage(LanguageParameter lang); diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricLanguageRouter.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricLanguageRouter.java index a6ae7c466..de511a3a7 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricLanguageRouter.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricLanguageRouter.java @@ -308,7 +308,11 @@ public void initialized() { } private ISingleLanguageService getOrBuildLanguageService(LanguageParameter lang) { - return languageServices.computeIfAbsent(lang.getName(), l -> new SingleLanguageServer(l)); + return languageServices.computeIfAbsent(lang.getName(), l -> { + var s = new SingleLanguageServer(l); + s.connect(availableClient()); + return s; + }); } @Override diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/SingleLanguageServer.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/SingleLanguageServer.java index 23073a0c8..e269d9aca 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/SingleLanguageServer.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/SingleLanguageServer.java @@ -85,6 +85,7 @@ import org.eclipse.lsp4j.WorkspaceEdit; import org.eclipse.lsp4j.jsonrpc.messages.Either; import org.eclipse.lsp4j.jsonrpc.messages.Either3; +import org.eclipse.lsp4j.services.LanguageClient; import org.rascalmpl.vscode.lsp.parametric.LanguageRegistry.LanguageParameter; import org.rascalmpl.vscode.lsp.util.NamedThreadPool; @@ -298,4 +299,10 @@ public void unregisterLanguage(LanguageParameter lang) { docService.unregisterLanguage(lang); } + @Override + public void connect(LanguageClient client) { + docService.connect(client); + wsService.connect(client); + } + } From a2dc0e79157a8add0e546772220e2e93b5ab6088 Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Thu, 16 Apr 2026 16:45:48 +0200 Subject: [PATCH 14/23] Parametric doc service has at most one multiplexer. --- .../LanguageContributionsMultiplexer.java | 8 +++ .../ParametricTextDocumentService.java | 51 ++++++++++--------- .../lsp/parametric/SingleLanguageServer.java | 2 +- 3 files changed, 35 insertions(+), 26 deletions(-) diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/LanguageContributionsMultiplexer.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/LanguageContributionsMultiplexer.java index 62ce162f5..6c97d2fcf 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/LanguageContributionsMultiplexer.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/LanguageContributionsMultiplexer.java @@ -149,6 +149,14 @@ public boolean removeContributor(String contribKey) { return true; } + /** + * Remove all contributors. + */ + public void clearContributors() { + contributions.clear(); + calculateRouting(); + } + private synchronized void calculateRouting() { // after contributions have changed, we calculate the routing // this is to avoid doing this lookup every time we get a request 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 f335e838b..2af2f3795 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 @@ -177,13 +177,12 @@ public class ParametricTextDocumentService extends TextDocumentStateManager impl private @MonotonicNonNull LanguageClient client; private @MonotonicNonNull BaseWorkspaceService workspaceService; private @MonotonicNonNull CapabilityRegistration dynamicCapabilities; + private @MonotonicNonNull LanguageContributionsMultiplexer multiplexer; /** extension to language */ private final Map registeredExtensions = new ConcurrentHashMap<>(); /** language to facts */ private final Map facts = new ConcurrentHashMap<>(); - /** language to contribution */ - private final Map contributions = new ConcurrentHashMap<>(); // Create "renamed" constructor of "FileSystemChange" so we can build a list of DocumentEdit objects for didRenameFiles private final TypeStore typeStore = new TypeStore(); @@ -193,7 +192,7 @@ public class ParametricTextDocumentService extends TextDocumentStateManager impl tf.sourceLocationType(), "to"); @SuppressWarnings({"initialization", "methodref.receiver.bound"}) // this::getContents - public ParametricTextDocumentService(ExecutorService exec) { + public ParametricTextDocumentService(String langName, ExecutorService exec) { // The following call ensures that URIResolverRegistry is initialized before FallbackResolver is accessed this.exec = exec; URIResolverRegistry.getInstance(); @@ -212,6 +211,13 @@ private BaseWorkspaceService availableWorkspaceService() { return workspaceService; } + private LanguageContributionsMultiplexer availableMultiplexer() { + if (multiplexer == null) { + throw new IllegalStateException("Multiplexer is not initialized"); + } + return multiplexer; + } + @Override public void pair(BaseWorkspaceService workspaceService) { this.workspaceService = workspaceService; @@ -502,7 +508,7 @@ private Map> bundleRenamesByContributio var l = Locations.toLoc(rename.getNewUri()); var language = safeLanguage(l); if (language.isPresent()) { - ILanguageContributions contrib = contributions.get(language.get()); + ILanguageContributions contrib = multiplexer; if (contrib != null) { bundled.computeIfAbsent(contrib, k -> new ArrayList<>()).add(rename); } @@ -579,11 +585,11 @@ private static T last(List l) { private Optional safeLanguage(ISourceLocation loc) { var ext = extension(loc); if ("".equals(ext)) { - if (contributions.size() == 1) { - logger.trace("file was opened without an extension; falling back to the single registered language for: {}", loc); - return contributions.keySet().stream().findFirst(); + if (multiplexer != null) { + logger.trace("File was opened without an extension; falling back to the single registered language for: {}", loc); + return Optional.of(availableMultiplexer().getName()); } 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: {}", loc); return Optional.empty(); } } @@ -597,11 +603,7 @@ private String language(ISourceLocation loc) { } private ILanguageContributions contributions(ISourceLocation doc) { - return safeLanguage(doc) - .map(contributions::get) - .map(ILanguageContributions.class::cast) - .flatMap(Optional::ofNullable) - .orElseGet(() -> new NoContributions(extension(doc), exec)); + return Optional.ofNullable(multiplexer).orElse(new NoContributions(extension(doc), exec)); } private static String extension(ISourceLocation doc) { @@ -868,10 +870,11 @@ public CompletableFuture, CompletionList>> completio @Override public synchronized void registerLanguage(LanguageParameter lang) { logger.info("registerLanguage({})", lang.getName()); + logger.trace(lang); - var multiplexer = contributions.computeIfAbsent(lang.getName(), - t -> new LanguageContributionsMultiplexer(lang.getName(), exec) - ); + if (multiplexer == null) { + multiplexer = new LanguageContributionsMultiplexer(lang.getName(), exec); + } var fact = facts.computeIfAbsent(lang.getName(), t -> new ParametricFileFacts(exec, getColumnMaps(), multiplexer) ); @@ -925,17 +928,17 @@ public synchronized void registerLanguage(LanguageParameter lang) { */ private Collection buildLanguageParams() { var extensionsByLang = Maps.invert(registeredExtensions); - return contributions.entrySet().stream().map(e -> new ICapabilityParams() { + return Set.of(new ICapabilityParams() { @Override public ILanguageContributions contributions() { - return e.getValue(); + return multiplexer; } @Override public Set fileExtensions() { - return extensionsByLang.getOrDefault(e.getKey(), Collections.emptySet()); + return extensionsByLang.getOrDefault(multiplexer.getName(), Collections.emptySet()); } - }).collect(Collectors.toSet()); + }); } private void updateFileState(LanguageParameter lang, ISourceLocation f) { @@ -959,8 +962,7 @@ private static String buildContributionKey(LanguageParameter lang) { public synchronized void unregisterLanguage(LanguageParameter lang) { boolean removeAll = lang.getMainModule() == null || lang.getMainModule().isEmpty(); if (!removeAll) { - var contrib = contributions.get(lang.getName()); - if (contrib != null && !contrib.removeContributor(buildContributionKey(lang))) { + if (multiplexer != null && !multiplexer.removeContributor(buildContributionKey(lang))) { logger.error("unregisterLanguage cleared everything, so removing all"); // ok, so it was a clear after all removeAll = true; @@ -980,7 +982,7 @@ public synchronized void unregisterLanguage(LanguageParameter lang) { this.registeredExtensions.remove(extension); } facts.remove(lang.getName()); - contributions.remove(lang.getName()); + multiplexer.clearContributors(); } // TODO Update capabilities dynamically @@ -1014,8 +1016,7 @@ public CompletableFuture executeCommand(String languageName, String comm @Override public void cancelProgress(String progressId) { - contributions.values().forEach(plex -> - plex.cancelProgress(progressId)); + availableMultiplexer().cancelProgress(progressId); } diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/SingleLanguageServer.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/SingleLanguageServer.java index e269d9aca..9ecf79b44 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/SingleLanguageServer.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/SingleLanguageServer.java @@ -96,7 +96,7 @@ public class SingleLanguageServer implements ISingleLanguageService { /*package*/ SingleLanguageServer(String langName) { var exec = NamedThreadPool.cached(langName); - this.docService = new ParametricTextDocumentService(exec); + this.docService = new ParametricTextDocumentService(langName, exec); this.wsService = new ParametricWorkspaceService(exec); this.docService.pair(wsService); this.wsService.pair(docService); From 5538293e4224b9281da3a70f1dc6b3474c0d973c Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Thu, 16 Apr 2026 18:18:30 +0200 Subject: [PATCH 15/23] Fix various null checks & annotations. --- rascal-lsp/src/main/checkerframework/lsp4j.astub | 14 -------------- .../lsp/parametric/ParametricLanguageRouter.java | 13 ++++++++----- .../lsp/parametric/ParametricLanguageServer.java | 3 +-- .../parametric/ParametricTextDocumentService.java | 15 +++++++-------- .../lsp/parametric/TextDocumentStateManager.java | 14 ++++++++++---- .../lsp/rascal/RascalTextDocumentService.java | 8 +++----- 6 files changed, 29 insertions(+), 38 deletions(-) diff --git a/rascal-lsp/src/main/checkerframework/lsp4j.astub b/rascal-lsp/src/main/checkerframework/lsp4j.astub index a1bd7470e..1a35b6d0f 100644 --- a/rascal-lsp/src/main/checkerframework/lsp4j.astub +++ b/rascal-lsp/src/main/checkerframework/lsp4j.astub @@ -63,20 +63,6 @@ public class DocumentSymbol { } -package org.eclipse.lsp4j.services; - - -import org.eclipse.lsp4j.*; -import org.eclipse.lsp4j.jsonrpc.services.*; -import org.checkerframework.checker.nullness.qual.*; - -@JsonSegment("textDocument") -public interface TextDocumentService { - @JsonRequest - default CompletableFuture<@Nullable Hover> hover(HoverParams params) { } -} - - package org.eclipse.lsp4j; import org.checkerframework.checker.nullness.qual.*; diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricLanguageRouter.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricLanguageRouter.java index de511a3a7..4de88c96f 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricLanguageRouter.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricLanguageRouter.java @@ -38,6 +38,7 @@ import org.apache.logging.log4j.Logger; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.checker.nullness.qual.PolyNull; import org.eclipse.lsp4j.CallHierarchyIncomingCall; import org.eclipse.lsp4j.CallHierarchyIncomingCallsParams; import org.eclipse.lsp4j.CallHierarchyItem; @@ -180,12 +181,12 @@ private static String extension(ISourceLocation doc) { return URIUtil.getExtension(doc); } - private R route(TextDocumentIdentifier file, BiFunction func, P param) { + private @PolyNull R route(TextDocumentIdentifier file, BiFunction func, P param) { return route(Locations.toLoc(file.getUri()), func, param); } - private R route(ISourceLocation file, BiFunction func, P param) { - TextDocumentService lang = language(file); + private @PolyNull R route(ISourceLocation file, BiFunction func, P param) { + var lang = language(file); return func.apply(lang, param); } @@ -340,8 +341,10 @@ public void registerLanguage(LanguageParameter lang) { public void unregisterLanguage(LanguageParameter lang) { logger.info("unregisterLanguage({})", lang.getName()); var removedLang = languageServices.remove(lang.getName()); - removedLang.unregisterLanguage(lang); - // TODO Kill the delegate process for this language and clean up maps + if (removedLang != null) { + removedLang.unregisterLanguage(lang); + // TODO Kill the delegate process for this language and clean up maps + } } @Override 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 abd422e7a..a2aeaba65 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 @@ -31,7 +31,6 @@ import java.util.concurrent.ExecutorService; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Function; -import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.rascalmpl.vscode.lsp.BaseLanguageServer; import org.rascalmpl.vscode.lsp.parametric.LanguageRegistry.LanguageParameter; import org.rascalmpl.vscode.lsp.util.NamedThreadPool; @@ -46,7 +45,7 @@ public static void main(String[] args) { dedicatedLanguage = null; } - AtomicReference<@MonotonicNonNull ParametricLanguageRouter> router = new AtomicReference<>(); + AtomicReference router = new AtomicReference<>(); Function supplyService = exec -> router.updateAndGet(v -> v != null ? v : new ParametricLanguageRouter(exec, 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 2af2f3795..5b07861b5 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 @@ -49,7 +49,6 @@ 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.ApplyWorkspaceEditParams; import org.eclipse.lsp4j.CallHierarchyIncomingCall; import org.eclipse.lsp4j.CallHierarchyIncomingCallsParams; @@ -193,8 +192,8 @@ public class ParametricTextDocumentService extends TextDocumentStateManager impl @SuppressWarnings({"initialization", "methodref.receiver.bound"}) // this::getContents public ParametricTextDocumentService(String langName, ExecutorService exec) { - // The following call ensures that URIResolverRegistry is initialized before FallbackResolver is accessed this.exec = exec; + // The following call ensures that URIResolverRegistry is initialized before FallbackResolver is accessed URIResolverRegistry.getInstance(); FallbackResolver.getInstance().registerTextDocumentService(this); } @@ -753,12 +752,12 @@ public CompletableFuture> references(ReferenceParams pa } @Override - public CompletableFuture<@Nullable Hover> hover(HoverParams params) { + public CompletableFuture hover(HoverParams params) { logger.debug("Hover: {} at {}", params.getTextDocument(), params.getPosition()); return recoverExceptions( lookup(ParametricSummary::hovers, params.getTextDocument(), params.getPosition()) .thenApply(Hover::new) - , () -> null); + , Hover::new); } @Override @@ -876,7 +875,7 @@ public synchronized void registerLanguage(LanguageParameter lang) { multiplexer = new LanguageContributionsMultiplexer(lang.getName(), exec); } var fact = facts.computeIfAbsent(lang.getName(), t -> - new ParametricFileFacts(exec, getColumnMaps(), multiplexer) + new ParametricFileFacts(exec, getColumnMaps(), availableMultiplexer()) ); var parserConfig = lang.getPrecompiledParser(); @@ -931,12 +930,12 @@ private Collection buildLanguageParams() { return Set.of(new ICapabilityParams() { @Override public ILanguageContributions contributions() { - return multiplexer; + return availableMultiplexer(); } @Override public Set fileExtensions() { - return extensionsByLang.getOrDefault(multiplexer.getName(), Collections.emptySet()); + return extensionsByLang.getOrDefault(availableMultiplexer().getName(), Collections.emptySet()); } }); } @@ -982,7 +981,7 @@ public synchronized void unregisterLanguage(LanguageParameter lang) { this.registeredExtensions.remove(extension); } facts.remove(lang.getName()); - multiplexer.clearContributors(); + availableMultiplexer().clearContributors(); } // TODO Update capabilities dynamically diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/TextDocumentStateManager.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/TextDocumentStateManager.java index fc4a7f9d2..653ba0405 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/TextDocumentStateManager.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/TextDocumentStateManager.java @@ -37,6 +37,8 @@ import org.apache.commons.io.IOUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.checkerframework.checker.initialization.qual.UnknownInitialization; +import org.checkerframework.checker.nullness.qual.KeyFor; import org.checkerframework.checker.nullness.qual.Nullable; import org.eclipse.lsp4j.TextDocumentItem; import org.eclipse.lsp4j.jsonrpc.ResponseErrorException; @@ -56,9 +58,13 @@ public class TextDocumentStateManager { private static final Logger logger = LogManager.getLogger(TextDocumentStateManager.class); private final Map files = new ConcurrentHashMap<>(); - private final ColumnMaps columns = new ColumnMaps(this::getContents); + private final ColumnMaps columns; - public String getContents(ISourceLocation file) { + public TextDocumentStateManager() { + this.columns = new ColumnMaps(l -> getContents(l)); + } + + public String getContents(@UnknownInitialization TextDocumentStateManager this, ISourceLocation file) { file = file.top(); TextDocumentState ideState = getFile(file); if (ideState != null) { @@ -89,7 +95,7 @@ public LineColumnOffsetMap getColumnMap(ISourceLocation loc) { return columns.get(loc.top()); } - TextDocumentState getFile(ISourceLocation loc) { + TextDocumentState getFile(@UnknownInitialization TextDocumentStateManager this, ISourceLocation loc) { loc = loc.top(); TextDocumentState file = files.get(loc); if (file == null) { @@ -125,7 +131,7 @@ boolean removeFile(ISourceLocation loc) { return state; } - Set getOpenFiles() { + Set<@KeyFor("this.files") ISourceLocation> getOpenFiles() { return files.keySet(); } 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 544508107..1f0d6bc7e 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 @@ -482,17 +482,15 @@ private MessageParams setMessageParams(IConstructor message) { } @Override - public CompletableFuture<@Nullable Hover> hover(HoverParams params) { + public CompletableFuture hover(HoverParams params) { logger.debug("textDocument/hover: {} at {}", params.getTextDocument(), params.getPosition()); if (facts != null) { return recoverExceptions(facts.getSummary(Locations.toLoc(params.getTextDocument())) .handle((t, r) -> (t == null ? (new SummaryBridge()) : t)) .thenApply(s -> s.getTypeName(params.getPosition())) - .thenApply(n -> new Hover(new MarkupContent("plaintext", n))), () -> null); - } - else { - return CompletableFutureUtils.completedFuture(null, exec); + .thenApply(n -> new Hover(new MarkupContent("plaintext", n))), Hover::new); } + return CompletableFutureUtils.completedFuture(new Hover(), exec); } @Override From 1a6df0a732d4bd3c25117caed13d3ffa0222e712 Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Fri, 17 Apr 2026 09:54:47 +0200 Subject: [PATCH 16/23] Remove mapping of single language to facts/extensions. --- .../ParametricTextDocumentService.java | 87 ++++++------------- 1 file changed, 27 insertions(+), 60 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 5b07861b5..802350cba 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 @@ -32,13 +32,13 @@ import java.util.Collection; import java.util.Collections; import java.util.HashMap; +import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutorService; import java.util.function.BiFunction; import java.util.function.Function; @@ -145,7 +145,6 @@ import org.rascalmpl.vscode.lsp.rascal.conversion.SelectionRanges; import org.rascalmpl.vscode.lsp.rascal.conversion.SemanticTokenizer; import org.rascalmpl.vscode.lsp.uri.FallbackResolver; -import org.rascalmpl.vscode.lsp.util.Maps; import org.rascalmpl.vscode.lsp.util.Versioned; import org.rascalmpl.vscode.lsp.util.concurrent.CompletableFutureUtils; import org.rascalmpl.vscode.lsp.util.concurrent.InterruptibleFuture; @@ -178,10 +177,8 @@ public class ParametricTextDocumentService extends TextDocumentStateManager impl private @MonotonicNonNull CapabilityRegistration dynamicCapabilities; private @MonotonicNonNull LanguageContributionsMultiplexer multiplexer; - /** extension to language */ - private final Map registeredExtensions = new ConcurrentHashMap<>(); - /** language to facts */ - private final Map facts = new ConcurrentHashMap<>(); + private final Set registeredExtensions = new HashSet<>(); + private @MonotonicNonNull ParametricFileFacts facts; // Create "renamed" constructor of "FileSystemChange" so we can build a list of DocumentEdit objects for didRenameFiles private final TypeStore typeStore = new TypeStore(); @@ -232,7 +229,6 @@ private LanguageClient availableClient() { @Override public void connect(LanguageClient client) { this.client = client; - facts.values().forEach(v -> v.setClient(client)); } @Override @@ -274,7 +270,7 @@ public void didClose(DidCloseTextDocumentParams params) { if (removeFile(loc)) { throw new ResponseErrorException(unknownFileError(loc, params)); } - facts(loc).close(loc); + facts.close(loc); // If the closed file no longer exists (e.g., if an untitled file is closed without ever having been saved), // we mimic a delete event to ensure all diagnostics are cleared. if (!URIResolverRegistry.getInstance().exists(loc)) { @@ -302,9 +298,9 @@ private void triggerAnalyzer(VersionedTextDocumentIdentifier doc, Duration delay } private void triggerAnalyzer(ISourceLocation location, int version, Duration delay) { - if (safeLanguage(location).isPresent()) { + if (multiplexer != null) { logger.trace("Triggering analyzer for {}", location); - var fileFacts = facts(location); + var fileFacts = availableFileFacts(); fileFacts.invalidateAnalyzer(location); fileFacts.calculateAnalyzer(location, getFile(location).getCurrentTreeAsync(true), version, delay); } else { @@ -315,7 +311,7 @@ private void triggerAnalyzer(ISourceLocation location, int version, Duration del private void triggerBuilder(TextDocumentIdentifier doc) { logger.trace("Triggering builder for {}", doc.getUri()); var location = Locations.toLoc(doc); - var fileFacts = facts(location); + var fileFacts = availableFileFacts(); fileFacts.invalidateBuilder(location); fileFacts.calculateBuilder(location, getFile(location).getCurrentTreeAsync(true)); } @@ -334,7 +330,7 @@ private void handleParsingErrors(TextDocumentState file, CompletableFuture> bundleRenamesByContributio Map> bundled = new HashMap<>(); for (FileRename rename : allRenames) { var l = Locations.toLoc(rename.getNewUri()); - var language = safeLanguage(l); - if (language.isPresent()) { - ILanguageContributions contrib = multiplexer; - if (contrib != null) { - bundled.computeIfAbsent(contrib, k -> new ArrayList<>()).add(rename); - } + ILanguageContributions contrib = multiplexer; + if (contrib != null) { + bundled.computeIfAbsent(contrib, k -> new ArrayList<>()).add(rename); } } @@ -581,26 +574,6 @@ private static T last(List l) { return l.get(l.size() - 1); } - private Optional safeLanguage(ISourceLocation loc) { - var ext = extension(loc); - if ("".equals(ext)) { - if (multiplexer != null) { - logger.trace("File was opened without an extension; falling back to the single registered language for: {}", loc); - return Optional.of(availableMultiplexer().getName()); - } 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)); - } - - private String language(ISourceLocation loc) { - return safeLanguage(loc).orElseThrow(() -> - new UnsupportedOperationException(String.format("Rascal Parametric LSP has no support for this file, since no language is registered for extension '%s': %s", extension(loc), loc)) - ); - } - private ILanguageContributions contributions(ISourceLocation doc) { return Optional.ofNullable(multiplexer).orElse(new NoContributions(extension(doc), exec)); } @@ -609,14 +582,11 @@ private static String extension(ISourceLocation doc) { return URIUtil.getExtension(doc); } - private ParametricFileFacts facts(ISourceLocation doc) { - ParametricFileFacts fact = facts.get(language(doc)); - - if (fact == null) { - throw new ResponseErrorException(unknownFileError(doc, doc)); + private ParametricFileFacts availableFileFacts() { + if (facts == null) { + throw new IllegalStateException("No file facts registered"); } - - return fact; + return facts; } private TextDocumentState open(TextDocumentItem doc, long timestamp) { @@ -716,7 +686,7 @@ private CompletableFuture> lookup(SummaryLookup lookup, TextDocum var loc = Locations.toLoc(doc); return getFile(loc) .getCurrentTreeAsync(true) - .thenApply(tree -> facts(loc).lookupInSummaries(lookup, loc, tree, cursor)) + .thenApply(tree -> facts.lookupInSummaries(lookup, loc, tree, cursor)) .thenCompose(Function.identity()); } @@ -874,9 +844,9 @@ public synchronized void registerLanguage(LanguageParameter lang) { if (multiplexer == null) { multiplexer = new LanguageContributionsMultiplexer(lang.getName(), exec); } - var fact = facts.computeIfAbsent(lang.getName(), t -> - new ParametricFileFacts(exec, getColumnMaps(), availableMultiplexer()) - ); + if (facts == null) { + facts = new ParametricFileFacts(exec, getColumnMaps(), availableMultiplexer()); + } var parserConfig = lang.getPrecompiledParser(); if (parserConfig != null) { @@ -899,12 +869,10 @@ public synchronized void registerLanguage(LanguageParameter lang) { multiplexer.addContributor(buildContributionKey(lang), new InterpretedLanguageContributions(lang, this, availableWorkspaceService(), (IBaseLanguageClient)clientCopy, exec)); - fact.reloadContributions(); - fact.setClient(clientCopy); + facts.reloadContributions(); + facts.setClient(clientCopy); - for (var extension: lang.getExtensions()) { - this.registeredExtensions.put(extension, lang.getName()); - } + registeredExtensions.addAll(Set.of(lang.getExtensions())); // `CapabilityRegistration::update` should never be called asynchronously, since that might re-order incoming updates. // Since `registerLanguage` is called from a single-threaded pool, calling it here is safe. @@ -926,7 +894,6 @@ public synchronized void registerLanguage(LanguageParameter lang) { * As long as this in only called from synchronized {@link registerLanguage}/{@link unregisterLanguage}, this should work fine. */ private Collection buildLanguageParams() { - var extensionsByLang = Maps.invert(registeredExtensions); return Set.of(new ICapabilityParams() { @Override public ILanguageContributions contributions() { @@ -935,7 +902,7 @@ public ILanguageContributions contributions() { @Override public Set fileExtensions() { - return extensionsByLang.getOrDefault(availableMultiplexer().getName(), Collections.emptySet()); + return registeredExtensions; } }); } @@ -967,9 +934,8 @@ public synchronized void unregisterLanguage(LanguageParameter lang) { removeAll = true; } else { - var fact = facts.get(lang.getName()); - if (fact != null) { - fact.reloadContributions(); + if (facts != null) { + facts.reloadContributions(); } } } @@ -980,7 +946,8 @@ public synchronized void unregisterLanguage(LanguageParameter lang) { for (var extension : lang.getExtensions()) { this.registeredExtensions.remove(extension); } - facts.remove(lang.getName()); + // TODO What does clean-up look like? + // facts.remove(lang.getName()); availableMultiplexer().clearContributors(); } From df2b2a3da725ff79332c2731b1afa9639bf72570 Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Fri, 17 Apr 2026 09:55:27 +0200 Subject: [PATCH 17/23] Use utility list function. --- .../lsp/parametric/ParametricTextDocumentService.java | 7 ++----- 1 file changed, 2 insertions(+), 5 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 802350cba..da539d9da 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 @@ -145,6 +145,7 @@ import org.rascalmpl.vscode.lsp.rascal.conversion.SelectionRanges; import org.rascalmpl.vscode.lsp.rascal.conversion.SemanticTokenizer; import org.rascalmpl.vscode.lsp.uri.FallbackResolver; +import org.rascalmpl.vscode.lsp.util.Lists; import org.rascalmpl.vscode.lsp.util.Versioned; import org.rascalmpl.vscode.lsp.util.concurrent.CompletableFutureUtils; import org.rascalmpl.vscode.lsp.util.concurrent.InterruptibleFuture; @@ -251,7 +252,7 @@ public void didOpen(DidOpenTextDocumentParams params) { public void didChange(DidChangeTextDocumentParams params) { var timestamp = System.currentTimeMillis(); logger.debug("Did Change file: {}", params.getTextDocument().getUri()); - updateContents(params.getTextDocument(), last(params.getContentChanges()).getText(), timestamp); + updateContents(params.getTextDocument(), Lists.last(params.getContentChanges()).getText(), timestamp); triggerAnalyzer(params.getTextDocument(), NORMAL_DEBOUNCE); } @@ -570,10 +571,6 @@ private CodeLens locCommandTupleToCodeLense(String languageName, IValue v) { return new CodeLens(Locations.toRange(loc, getColumnMaps()), CodeActions.constructorToCommand(dedicatedLanguageName, languageName, command), null); } - private static T last(List l) { - return l.get(l.size() - 1); - } - private ILanguageContributions contributions(ISourceLocation doc) { return Optional.ofNullable(multiplexer).orElse(new NoContributions(extension(doc), exec)); } From f353ca758faf90e068bd9de6d6aa2a15e2fdea70 Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Fri, 17 Apr 2026 10:41:27 +0200 Subject: [PATCH 18/23] Fix opaque URIs and extensionless documents. --- .../parametric/ParametricLanguageRouter.java | 37 +++++++++++++------ 1 file changed, 26 insertions(+), 11 deletions(-) diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricLanguageRouter.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricLanguageRouter.java index 4de88c96f..ecdc2dae8 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricLanguageRouter.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricLanguageRouter.java @@ -27,8 +27,10 @@ package org.rascalmpl.vscode.lsp.parametric; import java.util.Collections; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutorService; @@ -153,30 +155,43 @@ protected ParametricLanguageRouter(ExecutorService exec, @Nullable LanguageParam //// LANGUAGE MANAGEMENT private ISingleLanguageService language(TextDocumentItem textDocument) { - return language(URIUtil.assumeCorrectLocation(textDocument.getUri())); + return language(Locations.toLoc(textDocument.getUri())); } private TextDocumentService language(VersionedTextDocumentIdentifier textDocument) { - return language(URIUtil.assumeCorrectLocation(textDocument.getUri())); + return language(Locations.toLoc(textDocument.getUri())); } private TextDocumentService language(TextDocumentIdentifier textDocument) { - return language(URIUtil.assumeCorrectLocation(textDocument.getUri())); + return language(Locations.toLoc(textDocument.getUri())); } private ISingleLanguageService language(ISourceLocation uri) { - var ext = extension(uri); - var lang = languagesByExtension.get(ext); - if (lang == null) { - throw new IllegalStateException("No language exists for extension:" + ext); - } + 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)) + ); var service = languageServices.get(lang); if (service == null) { - throw new IllegalStateException("No language service exists for language: " + lang); + throw new UnsupportedOperationException(String.format("Rascal Parametric LSP has no support for this file, since no language is registered with name '%s': '%s'", lang, uri)); } 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); } @@ -221,7 +236,7 @@ public void didDeleteFiles(DeleteFilesParams params) { // Since these files can belong to different languages, we map them to their respective language and // delegate to several services. params.getFiles().stream() - .collect(Collectors.toMap(f -> language(URIUtil.assumeCorrectLocation(f.getUri())), List::of, Lists::union)) + .collect(Collectors.toMap(f -> language(Locations.toLoc(f.getUri())), List::of, Lists::union)) .entrySet() .forEach(e -> e.getKey().didDeleteFiles(new DeleteFilesParams(e.getValue()))); @@ -234,7 +249,7 @@ public void didRenameFiles(RenameFilesParams params, List _work // Since these files can belong to different languages, we map them to their respective language and // delegate to several services. params.getFiles().stream() - .collect(Collectors.toMap(f -> language(URIUtil.assumeCorrectLocation(f.getOldUri())), List::of, Lists::union)) + .collect(Collectors.toMap(f -> language(Locations.toLoc(f.getOldUri())), List::of, Lists::union)) .entrySet() .forEach(e -> e.getKey().didRenameFiles(new RenameFilesParams(e.getValue()))); From 1c86272a4e925f0f01565328e0da7d615981f9fe Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Fri, 17 Apr 2026 10:45:05 +0200 Subject: [PATCH 19/23] Do not distribute renames over contributions. --- .../ParametricTextDocumentService.java | 82 ++++++++----------- 1 file changed, 32 insertions(+), 50 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 da539d9da..58004fcae 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 @@ -31,11 +31,9 @@ import java.util.Arrays; import java.util.Collection; import java.util.Collections; -import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; -import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.concurrent.CompletableFuture; @@ -458,57 +456,41 @@ public void didCreateFiles(CreateFilesParams params) { @Override public void didRenameFiles(RenameFilesParams params, List workspaceFolders) { - Map> byContrib = bundleRenamesByContribution(params.getFiles()); - for (var entry : byContrib.entrySet()) { - ILanguageContributions contrib = entry.getKey(); - List renames = entry.getValue(); - - IList renameDocumentEdits = renames.stream().map(rename -> fileRenameToDocumentEdit(rename)).collect(VF.listWriter()); - - contrib.didRenameFiles(renameDocumentEdits) - .thenAccept(res -> { - var edits = (IList) res.get(0); - var messages = (ISet) res.get(1); - var client = availableClient(); - showMessages(client, messages); - - if (edits.isEmpty()) { - return; - } + var renameDocumentEdits = params.getFiles() + .stream() + .map(rename -> fileRenameToDocumentEdit(rename)) + .collect(VF.listWriter()); + + availableMultiplexer().didRenameFiles(renameDocumentEdits) + .thenAccept(res -> { + var edits = (IList) res.get(0); + var messages = (ISet) res.get(1); + var client = availableClient(); + showMessages(client, messages); + + if (edits.isEmpty()) { + return; + } - WorkspaceEdit changes = DocumentChanges.translateDocumentChanges(edits, getColumnMaps()); - client.applyEdit(new ApplyWorkspaceEditParams(changes, "Rename files")).thenAccept(editResponse -> { - if (!editResponse.isApplied()) { - throw new RuntimeException("didRenameFiles resulted in a list of edits but applying them failed" - + (editResponse.getFailureReason() != null ? (": " + editResponse.getFailureReason()) : "")); - } - }); - }) - .get() - .exceptionally(e -> { - var cause = e.getCause(); - logger.catching(Level.ERROR, cause); - var message = "unknown error"; - if (cause != null && cause.getMessage() != null) { - message = cause.getMessage(); + WorkspaceEdit changes = DocumentChanges.translateDocumentChanges(edits, getColumnMaps()); + client.applyEdit(new ApplyWorkspaceEditParams(changes, "Rename files")).thenAccept(editResponse -> { + if (!editResponse.isApplied()) { + throw new RuntimeException("didRenameFiles resulted in a list of edits but applying them failed" + + (editResponse.getFailureReason() != null ? (": " + editResponse.getFailureReason()) : "")); } - availableClient().showMessage(new MessageParams(MessageType.Error, message)); - return null; // Return of type `Void` is unused, but required }); - } - } - - private Map> bundleRenamesByContribution(List allRenames) { - Map> bundled = new HashMap<>(); - for (FileRename rename : allRenames) { - var l = Locations.toLoc(rename.getNewUri()); - ILanguageContributions contrib = multiplexer; - if (contrib != null) { - bundled.computeIfAbsent(contrib, k -> new ArrayList<>()).add(rename); - } - } - - return bundled; + }) + .get() + .exceptionally(e -> { + var cause = e.getCause(); + logger.catching(Level.ERROR, cause); + var message = "unknown error"; + if (cause != null && cause.getMessage() != null) { + message = cause.getMessage(); + } + availableClient().showMessage(new MessageParams(MessageType.Error, message)); + return null; // Return of type `Void` is unused, but required + }); } private IConstructor fileRenameToDocumentEdit(FileRename rename) { From c92350ac8eadef8d0628b59f245af86a4f5b7365 Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Fri, 17 Apr 2026 10:45:35 +0200 Subject: [PATCH 20/23] Comments & synchronization. --- .../vscode/lsp/parametric/ParametricLanguageRouter.java | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricLanguageRouter.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricLanguageRouter.java index ecdc2dae8..0f157b7b2 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricLanguageRouter.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricLanguageRouter.java @@ -325,6 +325,7 @@ public void initialized() { private ISingleLanguageService getOrBuildLanguageService(LanguageParameter lang) { return languageServices.computeIfAbsent(lang.getName(), l -> { + // TODO Start a delegate process with the right versions on the classpath for this language var s = new SingleLanguageServer(l); s.connect(availableClient()); return s; @@ -332,10 +333,7 @@ private ISingleLanguageService getOrBuildLanguageService(LanguageParameter lang) } @Override - public void registerLanguage(LanguageParameter lang) { - // Main workhorse - // TODO Start a delegate process with the right versions on the classpath for this language - + public synchronized void registerLanguage(LanguageParameter lang) { logger.info("registerLanguage({})", lang.getName()); var langService = getOrBuildLanguageService(lang); @@ -353,7 +351,7 @@ public void registerLanguage(LanguageParameter lang) { } @Override - public void unregisterLanguage(LanguageParameter lang) { + public synchronized void unregisterLanguage(LanguageParameter lang) { logger.info("unregisterLanguage({})", lang.getName()); var removedLang = languageServices.remove(lang.getName()); if (removedLang != null) { From 5525b59f3f28feeb7bdb071fe095cf531030ee11 Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Fri, 17 Apr 2026 12:32:23 +0200 Subject: [PATCH 21/23] Implement executeCommand on router side. --- .../parametric/ParametricLanguageRouter.java | 25 ++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricLanguageRouter.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricLanguageRouter.java index 0f157b7b2..b05202dec 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricLanguageRouter.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricLanguageRouter.java @@ -26,6 +26,7 @@ */ package org.rascalmpl.vscode.lsp.parametric; +import com.google.gson.JsonPrimitive; import java.util.Collections; import java.util.HashSet; import java.util.List; @@ -68,6 +69,7 @@ import org.eclipse.lsp4j.DocumentSymbol; import org.eclipse.lsp4j.DocumentSymbolParams; import org.eclipse.lsp4j.ExecuteCommandOptions; +import org.eclipse.lsp4j.ExecuteCommandParams; import org.eclipse.lsp4j.FoldingRange; import org.eclipse.lsp4j.FoldingRangeRequestParams; import org.eclipse.lsp4j.Hover; @@ -104,6 +106,7 @@ import org.eclipse.lsp4j.jsonrpc.messages.Either; import org.eclipse.lsp4j.jsonrpc.messages.Either3; import org.eclipse.lsp4j.services.TextDocumentService; +import org.eclipse.lsp4j.services.WorkspaceService; import org.rascalmpl.uri.URIUtil; import org.rascalmpl.util.locations.ColumnMaps; import org.rascalmpl.util.locations.LineColumnOffsetMap; @@ -170,9 +173,13 @@ private ISingleLanguageService language(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 ISingleLanguageService languageByName(String lang) { var service = languageServices.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': '%s'", lang, uri)); + 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; } @@ -202,6 +209,14 @@ private static String extension(ISourceLocation doc) { private @PolyNull R route(ISourceLocation file, BiFunction func, P param) { var lang = language(file); + return route(lang, func, param); + } + + private @PolyNull R route(ISingleLanguageService lang, BiFunction func, P param) { + return func.apply(lang, param); + } + + private @PolyNull R routeWs(ISingleLanguageService lang, BiFunction func, P param) { return func.apply(lang, param); } @@ -507,4 +522,12 @@ public CompletableFuture semanticTokensRange(SemanticTokensRange return route(params.getTextDocument(), TextDocumentService::semanticTokensRange, params); } + @Override + public CompletableFuture executeCommand(ExecuteCommandParams commandParams) { + // TODO Use helper class for param types here and on creation? + var langName = ((JsonPrimitive) commandParams.getArguments().get(0)).getAsString(); + var lang = languageByName(langName); + return routeWs(lang, WorkspaceService::executeCommand, commandParams); + } + } From eb22f3535db7d8a98d0bb0447620443a54197f2e Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Mon, 20 Apr 2026 10:42:24 +0200 Subject: [PATCH 22/23] Finish implementing command execution. --- .../vscode/lsp/BaseWorkspaceService.java | 16 +++++------ .../vscode/lsp/IBaseTextDocumentService.java | 4 +++ .../parametric/ISingleLanguageService.java | 4 +++ .../parametric/ParametricLanguageRouter.java | 27 +++++++++---------- .../ParametricTextDocumentService.java | 6 ++--- .../ParametricWorkspaceService.java | 3 ++- .../lsp/parametric/SingleLanguageServer.java | 7 +++++ .../lsp/rascal/RascalTextDocumentService.java | 6 ++--- .../lsp/rascal/RascalWorkspaceService.java | 3 ++- .../lsp/rascal/conversion/CodeActions.java | 13 +++++---- 10 files changed, 50 insertions(+), 39 deletions(-) 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 a4352ae5f..8ddeddfc9 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 @@ -26,13 +26,13 @@ */ package org.rascalmpl.vscode.lsp; +import com.google.gson.JsonPrimitive; import java.util.Collections; import java.util.List; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.ExecutorService; import java.util.stream.Collectors; -import org.apache.commons.lang3.NotImplementedException; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; @@ -55,6 +55,7 @@ import org.eclipse.lsp4j.services.LanguageClientAware; import org.eclipse.lsp4j.services.WorkspaceService; import org.rascalmpl.vscode.lsp.util.Nullables; +import org.rascalmpl.vscode.lsp.util.concurrent.CompletableFutureUtils; import org.rascalmpl.vscode.lsp.util.locations.Locations; public abstract class BaseWorkspaceService implements WorkspaceService, LanguageClientAware { @@ -62,9 +63,7 @@ public abstract class BaseWorkspaceService implements WorkspaceService, Language private @MonotonicNonNull LanguageClient client; - public static final String RASCAL_LANGUAGE = "Rascal"; - public static final String RASCAL_META_COMMAND = "rascal-meta-command"; - public static final String RASCAL_COMMAND = "rascal-command"; + public final String commandNamePrefix; protected final ExecutorService exec; @@ -72,7 +71,8 @@ public abstract class BaseWorkspaceService implements WorkspaceService, Language private final CopyOnWriteArrayList workspaceFolders = new CopyOnWriteArrayList<>(); - protected BaseWorkspaceService(ExecutorService exec) { + protected BaseWorkspaceService(String commandNamePrefix, ExecutorService exec) { + this.commandNamePrefix = commandNamePrefix; this.exec = exec; } @@ -187,12 +187,9 @@ public void didDeleteFiles(DeleteFilesParams params) { @Override public CompletableFuture executeCommand(ExecuteCommandParams commandParams) { logger.debug("workspace/executeCommand: {}", commandParams); - // TODO Split for Rascal and parametric - throw new NotImplementedException("BaseWorkspaceService::executeCommand"); - /* return CompletableFutureUtils.completedFuture(commandParams, exec) .thenCompose(params -> { - if (params.getCommand().startsWith(RASCAL_META_COMMAND) || params.getCommand().startsWith(RASCAL_COMMAND)) { + if (params.getCommand().startsWith(commandNamePrefix)) { String languageName = ((JsonPrimitive) params.getArguments().get(0)).getAsString(); String command = ((JsonPrimitive) params.getArguments().get(1)).getAsString(); return availableDocumentService().executeCommand(languageName, command).thenApply(v -> v); @@ -200,7 +197,6 @@ public CompletableFuture executeCommand(ExecuteCommandParams commandPara return CompletableFutureUtils.completedFuture(params.getCommand() + " was ignored.", exec); }); - */ } protected final ExecutorService getExecutor() { 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 b1a980c83..fdbff7f64 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 @@ -28,6 +28,7 @@ import java.time.Duration; import java.util.List; +import java.util.concurrent.CompletableFuture; import org.checkerframework.checker.nullness.qual.Nullable; import org.eclipse.lsp4j.ClientCapabilities; import org.eclipse.lsp4j.CreateFilesParams; @@ -42,6 +43,7 @@ import org.rascalmpl.vscode.lsp.parametric.LanguageRegistry.LanguageParameter; import io.usethesource.vallang.ISourceLocation; +import io.usethesource.vallang.IValue; public interface IBaseTextDocumentService extends TextDocumentService { static final Duration NO_DEBOUNCE = Duration.ZERO; @@ -58,6 +60,8 @@ public interface IBaseTextDocumentService extends TextDocumentService { void projectAdded(String name, ISourceLocation projectRoot); void projectRemoved(String name, ISourceLocation projectRoot); + CompletableFuture executeCommand(String languageName, String command); + LineColumnOffsetMap getColumnMap(ISourceLocation file); ColumnMaps getColumnMaps(); // TODO Simplify return type to something that can be serialized over JSON-RPC diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ISingleLanguageService.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ISingleLanguageService.java index e5946848b..7aec3df92 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ISingleLanguageService.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ISingleLanguageService.java @@ -26,13 +26,17 @@ */ package org.rascalmpl.vscode.lsp.parametric; +import java.util.concurrent.CompletableFuture; import org.eclipse.lsp4j.services.LanguageClientAware; import org.eclipse.lsp4j.services.TextDocumentService; import org.eclipse.lsp4j.services.WorkspaceService; import org.rascalmpl.vscode.lsp.parametric.LanguageRegistry.LanguageParameter; +import io.usethesource.vallang.IValue; + public interface ISingleLanguageService extends TextDocumentService, WorkspaceService, LanguageClientAware { void cancelProgress(String progressId); void registerLanguage(LanguageParameter lang); void unregisterLanguage(LanguageParameter lang); + CompletableFuture executeCommand(String language, String command); } diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricLanguageRouter.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricLanguageRouter.java index b05202dec..da32914c2 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricLanguageRouter.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricLanguageRouter.java @@ -36,6 +36,7 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutorService; import java.util.function.BiFunction; +import java.util.function.Function; import java.util.stream.Collectors; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -117,11 +118,13 @@ import org.rascalmpl.vscode.lsp.parametric.capabilities.CapabilityRegistration; import org.rascalmpl.vscode.lsp.parametric.capabilities.CompletionCapability; import org.rascalmpl.vscode.lsp.parametric.capabilities.FileOperationCapability; +import org.rascalmpl.vscode.lsp.rascal.conversion.CodeActions; import org.rascalmpl.vscode.lsp.rascal.conversion.SemanticTokenizer; import org.rascalmpl.vscode.lsp.util.Lists; import org.rascalmpl.vscode.lsp.util.locations.Locations; import io.usethesource.vallang.ISourceLocation; +import io.usethesource.vallang.IValue; public class ParametricLanguageRouter extends BaseWorkspaceService implements IBaseTextDocumentService { @@ -144,7 +147,7 @@ public class ParametricLanguageRouter extends BaseWorkspaceService implements IB private final TextDocumentStateManager files = new TextDocumentStateManager(); protected ParametricLanguageRouter(ExecutorService exec, @Nullable LanguageParameter dedicatedLanguage) { - super(exec); + super(CodeActions.RASCAL_META_COMMAND, exec); this.dedicatedLanguage = dedicatedLanguage; if (dedicatedLanguage == null) { @@ -273,15 +276,6 @@ public void didRenameFiles(RenameFilesParams params, List _work //// GLOBAL SERVER STUFF - private String getRascalMetaCommandName() { - // 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()) { - return BaseWorkspaceService.RASCAL_META_COMMAND + "-" + dedicatedLanguageName; - } - return BaseWorkspaceService.RASCAL_META_COMMAND; - } - private CapabilityRegistration availableCapabilities() { if (dynamicCapabilities == null) { throw new IllegalStateException("Dynamic capabilities are `null` - the document service did not yet connect to a client."); @@ -311,7 +305,7 @@ public void initializeServerCapabilities(ClientCapabilities clientCapabilities, 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(CodeActions.getRascalMetaCommandName("", dedicatedLanguageName)))); result.setInlayHintProvider(true); result.setSelectionRangeProvider(true); result.setFoldingRangeProvider(true); @@ -525,9 +519,14 @@ public CompletableFuture semanticTokensRange(SemanticTokensRange @Override public CompletableFuture executeCommand(ExecuteCommandParams commandParams) { // TODO Use helper class for param types here and on creation? - var langName = ((JsonPrimitive) commandParams.getArguments().get(0)).getAsString(); - var lang = languageByName(langName); - return routeWs(lang, WorkspaceService::executeCommand, commandParams); + var language = ((JsonPrimitive) commandParams.getArguments().get(0)).getAsString(); + var command = ((JsonPrimitive) commandParams.getArguments().get(1)).getAsString(); + return this.executeCommand(language, command).thenApply(Function.identity()); + } + + @Override + public CompletableFuture executeCommand(String languageName, String command) { + return languageByName(languageName).executeCommand(languageName, command); } } 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 58004fcae..e751efc78 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 @@ -944,20 +944,18 @@ public void projectRemoved(String name, ISourceLocation projectRoot) { // No need to do anything } - /* @Override public CompletableFuture executeCommand(String languageName, String command) { - ILanguageContributions contribs = contributions.get(languageName); + ILanguageContributions contribs = availableMultiplexer(); if (contribs != null) { return contribs.execution(command).get(); } else { - logger.warn("ignoring command execution (no contributor configured for this language): {}, {} ", languageName, command); + logger.warn("Ignoring command execution (no contributor configured for this language): {}, {} ", languageName, command); return CompletableFutureUtils.completedFuture(IRascalValueFactory.getInstance().string("No contributions configured for the language: " + languageName), exec); } } - */ @Override public void cancelProgress(String progressId) { 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 f84770050..159b4dd51 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,9 +28,10 @@ import java.util.concurrent.ExecutorService; import org.rascalmpl.vscode.lsp.BaseWorkspaceService; +import org.rascalmpl.vscode.lsp.rascal.conversion.CodeActions; public class ParametricWorkspaceService extends BaseWorkspaceService { ParametricWorkspaceService(ExecutorService exec) { - super(exec); + super(CodeActions.RASCAL_META_COMMAND, exec); } } diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/SingleLanguageServer.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/SingleLanguageServer.java index 9ecf79b44..85ea55f94 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/SingleLanguageServer.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/SingleLanguageServer.java @@ -89,6 +89,8 @@ import org.rascalmpl.vscode.lsp.parametric.LanguageRegistry.LanguageParameter; import org.rascalmpl.vscode.lsp.util.NamedThreadPool; +import io.usethesource.vallang.IValue; + public class SingleLanguageServer implements ISingleLanguageService { private final ParametricTextDocumentService docService; @@ -269,6 +271,11 @@ public CompletableFuture executeCommand(ExecuteCommandParams params) { return wsService.executeCommand(params); } + @Override + public CompletableFuture executeCommand(String language, String command) { + return docService.executeCommand(language, command); + } + @Override public CompletableFuture willCreateFiles(CreateFilesParams params) { return wsService.willCreateFiles(params); 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 1f0d6bc7e..7b8f464cd 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 @@ -240,7 +240,7 @@ public void initializeServerCapabilities(ClientCapabilities clientCapabilities, result.setFoldingRangeProvider(true); result.setRenameProvider(new RenameOptions(true)); result.setCodeActionProvider(true); - result.setExecuteCommandProvider(new ExecuteCommandOptions(Collections.singletonList(BaseWorkspaceService.RASCAL_COMMAND))); + result.setExecuteCommandProvider(new ExecuteCommandOptions(Collections.singletonList(CodeActions.RASCAL_COMMAND))); result.setSelectionRangeProvider(true); } @@ -699,7 +699,7 @@ public CompletableFuture>> codeAction(CodeActio ; // final merging the two streams of commmands, and their conversion to LSP Command data-type - return CodeActions.mergeAndConvertCodeActions(this, "", BaseWorkspaceService.RASCAL_LANGUAGE, quickfixes, codeActions); + return CodeActions.mergeAndConvertCodeActions(this, "", CodeActions.RASCAL_LANGUAGE, quickfixes, codeActions); } private CompletableFuture computeCodeActions(final int startLine, final int startColumn, ITree tree, PathConfig pcfg) { @@ -717,12 +717,10 @@ private CodeLens makeRunCodeLens(CodeLensSuggestion detected) { ); } - /* @Override public CompletableFuture executeCommand(String extension, String command) { return availableRascalServices().executeCommand(command).get(); } - */ private static CompletableFuture recoverExceptions(CompletableFuture future, Supplier defaultValue) { return future 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 58fd9d1f6..855d05574 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,12 +43,13 @@ import org.eclipse.lsp4j.WorkspaceFolder; import org.eclipse.lsp4j.WorkspaceServerCapabilities; import org.rascalmpl.vscode.lsp.BaseWorkspaceService; +import org.rascalmpl.vscode.lsp.rascal.conversion.CodeActions; import org.rascalmpl.vscode.lsp.util.Nullables; public class RascalWorkspaceService extends BaseWorkspaceService { RascalWorkspaceService(ExecutorService exec) { - super(exec); + super(CodeActions.RASCAL_COMMAND, exec); } @Override diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/conversion/CodeActions.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/conversion/CodeActions.java index 9d80a818a..371e52701 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/conversion/CodeActions.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/conversion/CodeActions.java @@ -43,7 +43,6 @@ import org.eclipse.lsp4j.Diagnostic; import org.eclipse.lsp4j.jsonrpc.messages.Either; import org.rascalmpl.values.IRascalValueFactory; -import org.rascalmpl.vscode.lsp.BaseWorkspaceService; import org.rascalmpl.vscode.lsp.IBaseTextDocumentService; import org.rascalmpl.vscode.lsp.parametric.model.RascalADTs; import org.rascalmpl.vscode.lsp.util.concurrent.CompletableFutureUtils; @@ -59,6 +58,10 @@ */ public class CodeActions { + public static final String RASCAL_LANGUAGE = "Rascal"; + public static final String RASCAL_META_COMMAND = "rascal-meta-command"; + public static final String RASCAL_COMMAND = "rascal-command"; + private CodeActions() { /* hide implicit public constructor */ } /** @@ -172,13 +175,13 @@ public static String getRascalMetaCommandName(String language, String dedicatedL // 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()) { - return BaseWorkspaceService.RASCAL_META_COMMAND + "-" + dedicatedLanguageName; + return RASCAL_META_COMMAND + "-" + dedicatedLanguageName; } - else if (BaseWorkspaceService.RASCAL_LANGUAGE.equals(language)) { - return BaseWorkspaceService.RASCAL_COMMAND; + else if (RASCAL_LANGUAGE.equals(language)) { + return RASCAL_COMMAND; } else { - return BaseWorkspaceService.RASCAL_META_COMMAND; + return RASCAL_META_COMMAND; } } } From d9112a6523edf8cc61524d63c69db02c7a3ec1a8 Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Mon, 20 Apr 2026 11:59:49 +0200 Subject: [PATCH 23/23] Refactor command execution to simplify. --- .../vscode/lsp/BaseWorkspaceService.java | 20 ++++--------- .../vscode/lsp/IBaseTextDocumentService.java | 2 +- .../parametric/ISingleLanguageService.java | 2 +- .../parametric/ParametricLanguageRouter.java | 29 +++++++++--------- .../ParametricTextDocumentService.java | 17 ++++++----- .../ParametricWorkspaceService.java | 3 +- .../lsp/parametric/SingleLanguageServer.java | 13 ++++---- .../lsp/rascal/RascalTextDocumentService.java | 10 +++++-- .../lsp/rascal/RascalWorkspaceService.java | 3 +- .../lsp/rascal/conversion/CodeActions.java | 30 ++++--------------- .../lsp/rascal/conversion/Completion.java | 8 ++--- 11 files changed, 59 insertions(+), 78 deletions(-) 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 8ddeddfc9..1e4abd44b 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 @@ -32,6 +32,7 @@ import java.util.concurrent.CompletableFuture; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.ExecutorService; +import java.util.function.Function; import java.util.stream.Collectors; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -55,7 +56,6 @@ import org.eclipse.lsp4j.services.LanguageClientAware; import org.eclipse.lsp4j.services.WorkspaceService; import org.rascalmpl.vscode.lsp.util.Nullables; -import org.rascalmpl.vscode.lsp.util.concurrent.CompletableFutureUtils; import org.rascalmpl.vscode.lsp.util.locations.Locations; public abstract class BaseWorkspaceService implements WorkspaceService, LanguageClientAware { @@ -63,16 +63,13 @@ public abstract class BaseWorkspaceService implements WorkspaceService, Language private @MonotonicNonNull LanguageClient client; - public final String commandNamePrefix; - protected final ExecutorService exec; private @MonotonicNonNull IBaseTextDocumentService documentService; private final CopyOnWriteArrayList workspaceFolders = new CopyOnWriteArrayList<>(); - protected BaseWorkspaceService(String commandNamePrefix, ExecutorService exec) { - this.commandNamePrefix = commandNamePrefix; + protected BaseWorkspaceService(ExecutorService exec) { this.exec = exec; } @@ -187,16 +184,9 @@ public void didDeleteFiles(DeleteFilesParams params) { @Override public CompletableFuture executeCommand(ExecuteCommandParams commandParams) { logger.debug("workspace/executeCommand: {}", commandParams); - return CompletableFutureUtils.completedFuture(commandParams, exec) - .thenCompose(params -> { - if (params.getCommand().startsWith(commandNamePrefix)) { - String languageName = ((JsonPrimitive) params.getArguments().get(0)).getAsString(); - String command = ((JsonPrimitive) params.getArguments().get(1)).getAsString(); - return availableDocumentService().executeCommand(languageName, command).thenApply(v -> v); - } - - return CompletableFutureUtils.completedFuture(params.getCommand() + " was ignored.", exec); - }); + var language = ((JsonPrimitive) commandParams.getArguments().get(0)).getAsString(); + var command = ((JsonPrimitive) commandParams.getArguments().get(1)).getAsString(); + return availableDocumentService().executeCommand(language, command).thenApply(Function.identity()); } protected final ExecutorService getExecutor() { 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 fdbff7f64..5dd8895ae 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 @@ -60,7 +60,7 @@ public interface IBaseTextDocumentService extends TextDocumentService { void projectAdded(String name, ISourceLocation projectRoot); void projectRemoved(String name, ISourceLocation projectRoot); - CompletableFuture executeCommand(String languageName, String command); + CompletableFuture executeCommand(String language, String command); LineColumnOffsetMap getColumnMap(ISourceLocation file); ColumnMaps getColumnMaps(); diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ISingleLanguageService.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ISingleLanguageService.java index 7aec3df92..805173aa3 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ISingleLanguageService.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ISingleLanguageService.java @@ -38,5 +38,5 @@ public interface ISingleLanguageService extends TextDocumentService, WorkspaceSe void cancelProgress(String progressId); void registerLanguage(LanguageParameter lang); void unregisterLanguage(LanguageParameter lang); - CompletableFuture executeCommand(String language, String command); + CompletableFuture executeCommand(String command); } diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricLanguageRouter.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricLanguageRouter.java index da32914c2..455e281c6 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricLanguageRouter.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricLanguageRouter.java @@ -118,7 +118,6 @@ import org.rascalmpl.vscode.lsp.parametric.capabilities.CapabilityRegistration; import org.rascalmpl.vscode.lsp.parametric.capabilities.CompletionCapability; import org.rascalmpl.vscode.lsp.parametric.capabilities.FileOperationCapability; -import org.rascalmpl.vscode.lsp.rascal.conversion.CodeActions; import org.rascalmpl.vscode.lsp.rascal.conversion.SemanticTokenizer; import org.rascalmpl.vscode.lsp.util.Lists; import org.rascalmpl.vscode.lsp.util.locations.Locations; @@ -140,22 +139,14 @@ public class ParametricLanguageRouter extends BaseWorkspaceService implements IB // Server stuff private final @Nullable LanguageParameter dedicatedLanguage; - private final String dedicatedLanguageName; private @MonotonicNonNull CapabilityRegistration dynamicCapabilities; private final TextDocumentStateManager files = new TextDocumentStateManager(); protected ParametricLanguageRouter(ExecutorService exec, @Nullable LanguageParameter dedicatedLanguage) { - super(CodeActions.RASCAL_META_COMMAND, exec); - + super(exec); this.dedicatedLanguage = dedicatedLanguage; - if (dedicatedLanguage == null) { - this.dedicatedLanguageName = ""; - } - else { - this.dedicatedLanguageName = dedicatedLanguage.getName(); - } } //// LANGUAGE MANAGEMENT @@ -305,7 +296,7 @@ public void initializeServerCapabilities(ClientCapabilities clientCapabilities, result.setCodeActionProvider(true); result.setCodeLensProvider(new CodeLensOptions(false)); result.setRenameProvider(new RenameOptions(true)); - result.setExecuteCommandProvider(new ExecuteCommandOptions(Collections.singletonList(CodeActions.getRascalMetaCommandName("", dedicatedLanguageName)))); + result.setExecuteCommandProvider(new ExecuteCommandOptions(Collections.singletonList(metaCommandName()))); result.setInlayHintProvider(true); result.setSelectionRangeProvider(true); result.setFoldingRangeProvider(true); @@ -518,15 +509,25 @@ public CompletableFuture semanticTokensRange(SemanticTokensRange @Override public CompletableFuture executeCommand(ExecuteCommandParams commandParams) { - // TODO Use helper class for param types here and on creation? var language = ((JsonPrimitive) commandParams.getArguments().get(0)).getAsString(); var command = ((JsonPrimitive) commandParams.getArguments().get(1)).getAsString(); return this.executeCommand(language, command).thenApply(Function.identity()); } @Override - public CompletableFuture executeCommand(String languageName, String command) { - return languageByName(languageName).executeCommand(languageName, command); + public CompletableFuture executeCommand(String language, String command) { + return languageByName(language).executeCommand(command); + } + + public String metaCommandName() { + // 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 (dedicatedLanguage != null) { + return ParametricTextDocumentService.RASCAL_META_COMMAND + "-" + dedicatedLanguage.getName(); + } + else { + return ParametricTextDocumentService.RASCAL_META_COMMAND; + } } } 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 e751efc78..e76bae33f 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 @@ -167,9 +167,11 @@ public class ParametricTextDocumentService extends TextDocumentStateManager impl private static final IValueFactory VF = IRascalValueFactory.getInstance(); private static final Logger logger = LogManager.getLogger(ParametricTextDocumentService.class); + public static final String RASCAL_META_COMMAND = "rascal-meta-command"; + + private final String languageName; private final ExecutorService exec; - private final String dedicatedLanguageName = ""; private final SemanticTokenizer tokenizer = new SemanticTokenizer(); private @MonotonicNonNull LanguageClient client; private @MonotonicNonNull BaseWorkspaceService workspaceService; @@ -187,7 +189,8 @@ public class ParametricTextDocumentService extends TextDocumentStateManager impl tf.sourceLocationType(), "to"); @SuppressWarnings({"initialization", "methodref.receiver.bound"}) // this::getContents - public ParametricTextDocumentService(String langName, ExecutorService exec) { + public ParametricTextDocumentService(String languageName, ExecutorService exec) { + this.languageName = languageName; this.exec = exec; // The following call ensures that URIResolverRegistry is initialized before FallbackResolver is accessed URIResolverRegistry.getInstance(); @@ -550,7 +553,7 @@ private CodeLens locCommandTupleToCodeLense(String languageName, IValue v) { ISourceLocation loc = (ISourceLocation) t.get(0); IConstructor command = (IConstructor) t.get(1); - return new CodeLens(Locations.toRange(loc, getColumnMaps()), CodeActions.constructorToCommand(dedicatedLanguageName, languageName, command), null); + return new CodeLens(Locations.toRange(loc, getColumnMaps()), CodeActions.constructorToCommand(RASCAL_META_COMMAND, languageName, command), null); } private ILanguageContributions contributions(ISourceLocation doc) { @@ -646,7 +649,7 @@ public CompletableFuture>> codeAction(CodeActio ; // final merging the two streams of commmands, and their conversion to LSP Command data-type - return CodeActions.mergeAndConvertCodeActions(this, dedicatedLanguageName, contribs.getName(), quickfixes, codeActions); + return CodeActions.mergeAndConvertCodeActions(this, RASCAL_META_COMMAND, contribs.getName(), quickfixes, codeActions); } private CompletableFuture computeCodeActions(final ILanguageContributions contribs, final int startLine, final int startColumn, ITree tree) { @@ -810,7 +813,7 @@ public CompletableFuture, CompletionList>> completio var focus = TreeSearch.computeFocusList(t, loc.getBeginLine(), loc.getBeginColumn()); var cursorOffset = loc.getBeginColumn() - TreeAdapter.getLocation((ITree) focus.get(0)).getBeginColumn(); return contrib.completion(focus, VF.integer(cursorOffset), completion.triggerKindToRascal(params.getContext())).get() - .thenApply(ci -> completion.toLSP(this, ci, dedicatedLanguageName, contrib.getName(), loc.getBeginLine(), getColumnMap(loc))); + .thenApply(ci -> completion.toLSP(this, ci, RASCAL_META_COMMAND, contrib.getName(), loc.getBeginLine(), getColumnMap(loc))); }) .thenApply(Either::forLeft), () -> Either.forLeft(Collections.emptyList())); } @@ -945,14 +948,14 @@ public void projectRemoved(String name, ISourceLocation projectRoot) { } @Override - public CompletableFuture executeCommand(String languageName, String command) { + public CompletableFuture executeCommand(String _language, String command) { ILanguageContributions contribs = availableMultiplexer(); if (contribs != null) { return contribs.execution(command).get(); } else { - logger.warn("Ignoring command execution (no contributor configured for this language): {}, {} ", languageName, command); + logger.warn("Ignoring command execution (no contributor configured for this language): {}, {}", languageName, command); return CompletableFutureUtils.completedFuture(IRascalValueFactory.getInstance().string("No contributions configured for the language: " + languageName), 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 159b4dd51..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.rascal.conversion.CodeActions; public class ParametricWorkspaceService extends BaseWorkspaceService { ParametricWorkspaceService(ExecutorService exec) { - super(CodeActions.RASCAL_META_COMMAND, exec); + super(exec); } } diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/SingleLanguageServer.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/SingleLanguageServer.java index 85ea55f94..ebb80233a 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/SingleLanguageServer.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/SingleLanguageServer.java @@ -93,12 +93,15 @@ public class SingleLanguageServer implements ISingleLanguageService { + private final String languageName; private final ParametricTextDocumentService docService; private final ParametricWorkspaceService wsService; - /*package*/ SingleLanguageServer(String langName) { - var exec = NamedThreadPool.cached(langName); - this.docService = new ParametricTextDocumentService(langName, exec); + /*package*/ SingleLanguageServer(String languageName) { + var exec = NamedThreadPool.cached(languageName); + + this.languageName = languageName; + this.docService = new ParametricTextDocumentService(languageName, exec); this.wsService = new ParametricWorkspaceService(exec); this.docService.pair(wsService); this.wsService.pair(docService); @@ -272,8 +275,8 @@ public CompletableFuture executeCommand(ExecuteCommandParams params) { } @Override - public CompletableFuture executeCommand(String language, String command) { - return docService.executeCommand(language, command); + public CompletableFuture executeCommand(String command) { + return docService.executeCommand(languageName, command); } @Override 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 7b8f464cd..91d57442e 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 @@ -148,6 +148,9 @@ public class RascalTextDocumentService implements IBaseTextDocumentService, Lang private static final IValueFactory VF = IRascalValueFactory.getInstance(); private static final Logger logger = LogManager.getLogger(RascalTextDocumentService.class); + public static final String RASCAL_LANGUAGE = "Rascal"; + public static final String RASCAL_COMMAND = "rascal-command"; + private final ExecutorService exec; private @MonotonicNonNull RascalLanguageServices rascalServices; @@ -240,7 +243,7 @@ public void initializeServerCapabilities(ClientCapabilities clientCapabilities, result.setFoldingRangeProvider(true); result.setRenameProvider(new RenameOptions(true)); result.setCodeActionProvider(true); - result.setExecuteCommandProvider(new ExecuteCommandOptions(Collections.singletonList(CodeActions.RASCAL_COMMAND))); + result.setExecuteCommandProvider(new ExecuteCommandOptions(Collections.singletonList(RASCAL_COMMAND))); result.setSelectionRangeProvider(true); } @@ -699,7 +702,7 @@ public CompletableFuture>> codeAction(CodeActio ; // final merging the two streams of commmands, and their conversion to LSP Command data-type - return CodeActions.mergeAndConvertCodeActions(this, "", CodeActions.RASCAL_LANGUAGE, quickfixes, codeActions); + return CodeActions.mergeAndConvertCodeActions(this, RASCAL_COMMAND, RASCAL_LANGUAGE, quickfixes, codeActions); } private CompletableFuture computeCodeActions(final int startLine, final int startColumn, ITree tree, PathConfig pcfg) { @@ -718,10 +721,11 @@ private CodeLens makeRunCodeLens(CodeLensSuggestion detected) { } @Override - public CompletableFuture executeCommand(String extension, String command) { + public CompletableFuture executeCommand(String _language, String command) { return availableRascalServices().executeCommand(command).get(); } + private static CompletableFuture recoverExceptions(CompletableFuture future, Supplier defaultValue) { return future .exceptionally(e -> { 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 855d05574..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.rascal.conversion.CodeActions; import org.rascalmpl.vscode.lsp.util.Nullables; public class RascalWorkspaceService extends BaseWorkspaceService { RascalWorkspaceService(ExecutorService exec) { - super(CodeActions.RASCAL_COMMAND, exec); + super(exec); } @Override diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/conversion/CodeActions.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/conversion/CodeActions.java index 371e52701..862598bb9 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/conversion/CodeActions.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/conversion/CodeActions.java @@ -58,10 +58,6 @@ */ public class CodeActions { - public static final String RASCAL_LANGUAGE = "Rascal"; - public static final String RASCAL_META_COMMAND = "rascal-meta-command"; - public static final String RASCAL_COMMAND = "rascal-command"; - private CodeActions() { /* hide implicit public constructor */ } /** @@ -89,17 +85,17 @@ public static CompletableFuture> extractActionsFromDiagnostics(Co } /* merges two streams of CodeAction terms and then converts them to LSP objects */ - public static CompletableFuture>> mergeAndConvertCodeActions(IBaseTextDocumentService doc, String dedicatedLanguageName, String languageName, CompletableFuture> quickfixes, CompletableFuture> codeActions) { + public static CompletableFuture>> mergeAndConvertCodeActions(IBaseTextDocumentService doc, String metaCommandName, String languageName, CompletableFuture> quickfixes, CompletableFuture> codeActions) { return codeActions.thenCombine(quickfixes, (actions, quicks) -> Stream.concat(quicks, actions) .map(IConstructor.class::cast) - .map(cons -> constructorToCodeAction(doc, dedicatedLanguageName, languageName, cons)) + .map(cons -> constructorToCodeAction(doc, metaCommandName, languageName, cons)) .map(Either::forRight) .collect(Collectors.toList()) ); } - private static CodeAction constructorToCodeAction(IBaseTextDocumentService doc, String dedicatedLanguageName, String languageName, IConstructor codeAction) { + private static CodeAction constructorToCodeAction(IBaseTextDocumentService doc, String metaCommandName, String languageName, IConstructor codeAction) { IWithKeywordParameters kw = codeAction.asWithKeywordParameters(); IConstructor command = (IConstructor) kw.getParameter(RascalADTs.CodeActionFields.COMMAND); IString title = (IString) kw.getParameter(RascalADTs.CodeActionFields.TITLE); @@ -120,7 +116,7 @@ private static CodeAction constructorToCodeAction(IBaseTextDocumentService doc, CodeAction result = new CodeAction(title.getValue()); if (command != null) { - result.setCommand(constructorToCommand(dedicatedLanguageName, languageName, command)); + result.setCommand(constructorToCommand(metaCommandName, languageName, command)); } if (edits != null) { @@ -164,24 +160,10 @@ else if ("empty".equals(name)) { return name; } - public static Command constructorToCommand(String dedicatedLanguageName, String languageName, IConstructor command) { + public static Command constructorToCommand(String metaCommandName, String languageName, IConstructor command) { IWithKeywordParameters kw = command.asWithKeywordParameters(); IString possibleTitle = (IString) kw.getParameter(RascalADTs.CommandFields.TITLE); - return new Command(possibleTitle != null ? possibleTitle.getValue() : command.toString(), getRascalMetaCommandName(languageName, dedicatedLanguageName), Arrays.asList(languageName, command.toString())); - } - - public static String getRascalMetaCommandName(String language, 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()) { - return RASCAL_META_COMMAND + "-" + dedicatedLanguageName; - } - else if (RASCAL_LANGUAGE.equals(language)) { - return RASCAL_COMMAND; - } - else { - return RASCAL_META_COMMAND; - } + return new Command(possibleTitle != null ? possibleTitle.getValue() : command.toString(), metaCommandName, Arrays.asList(languageName, command.toString())); } } diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/conversion/Completion.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/conversion/Completion.java index 39fdedc60..0e7b358e8 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/conversion/Completion.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/conversion/Completion.java @@ -76,7 +76,7 @@ public Completion() { this.character = c -> VF.constructor(TF.constructor(store, completionTriggerAdt, CompletionFields.CHARACTER, TF.stringType(), CompletionFields.TRIGGER), c); } - public List toLSP(final IBaseTextDocumentService docService, IList items, String dedicatedLanguageName, String languageName, int editLine, LineColumnOffsetMap offsets) { + public List toLSP(final IBaseTextDocumentService docService, IList items, String metaCommandName, String languageName, int editLine, LineColumnOffsetMap offsets) { return items.stream() .map(IConstructor.class::cast) .map(c -> { @@ -103,7 +103,7 @@ public List toLSP(final IBaseTextDocumentService docService, ILi ci.setPreselect(KeywordParameter.get(CompletionFields.PRESELECT, kws, false)); ci.setCommitCharacters(KeywordParameter.get(CompletionFields.COMMIT_CHARACTERS, kws, List.of(), ch -> ((IString) ch).getValue())); ci.setAdditionalTextEdits(DocumentChanges.translateTextEdits(KeywordParameter.get(CompletionFields.ADDITIONAL_CHANGES, kws, VF.list()), docService.getColumnMaps())); - var command = getCommand(kws, dedicatedLanguageName, languageName); + var command = getCommand(kws, metaCommandName, languageName); if (command != null) { ci.setCommand(command); } @@ -113,12 +113,12 @@ public List toLSP(final IBaseTextDocumentService docService, ILi .collect(Collectors.toList()); } - private @Nullable Command getCommand(IWithKeywordParameters kws, String dedicatedLanguageName, String languageName) { + private @Nullable Command getCommand(IWithKeywordParameters kws, String metaCommandName, String languageName) { var command = (IConstructor) kws.getParameter(CompletionFields.COMMAND); if (command == null) { return null; } - return CodeActions.constructorToCommand(dedicatedLanguageName, languageName, command); + return CodeActions.constructorToCommand(metaCommandName, languageName, command); } private InsertReplaceEdit editToLSP(IConstructor edit, int currentLine, LineColumnOffsetMap offsets) {