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/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..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,21 +63,20 @@ 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"; + protected final ExecutorService exec; - private final ExecutorService exec; - - private final IBaseTextDocumentService documentService; + private @MonotonicNonNull IBaseTextDocumentService documentService; private final CopyOnWriteArrayList workspaceFolders = new CopyOnWriteArrayList<>(); - protected BaseWorkspaceService(ExecutorService exec, IBaseTextDocumentService documentService) { - this.documentService = documentService; + protected BaseWorkspaceService(ExecutorService exec) { this.exec = exec; } + public void pair(IBaseTextDocumentService documentService) { + this.documentService = documentService; + } + public void initialize(ClientCapabilities clientCap, @Nullable List currentWorkspaceFolders, ServerCapabilities capabilities) { this.workspaceFolders.clear(); if (currentWorkspaceFolders != null) { @@ -93,6 +92,21 @@ public void initialize(ClientCapabilities clientCap, @Nullable List workspaceFolders() { return Collections.unmodifiableList(workspaceFolders); } @@ -126,7 +140,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 +148,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 +156,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,29 +171,22 @@ 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 public CompletableFuture executeCommand(ExecuteCommandParams commandParams) { logger.debug("workspace/executeCommand: {}", commandParams); - return CompletableFutureUtils.completedFuture(commandParams, exec) - .thenCompose(params -> { - 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 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 d7fb14096..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,9 +60,11 @@ 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(); + // 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 new file mode 100644 index 000000000..805173aa3 --- /dev/null +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ISingleLanguageService.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2018-2025, NWO-I CWI and Swat.engineering + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +package org.rascalmpl.vscode.lsp.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 command); +} 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/ParametricLanguageRouter.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricLanguageRouter.java new file mode 100644 index 000000000..455e281c6 --- /dev/null +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricLanguageRouter.java @@ -0,0 +1,533 @@ +/* + * Copyright (c) 2018-2025, NWO-I CWI and Swat.engineering + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +package org.rascalmpl.vscode.lsp.parametric; + +import com.google.gson.JsonPrimitive; +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; +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; +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; +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.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.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.eclipse.lsp4j.services.WorkspaceService; +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.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 { + + 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 @MonotonicNonNull CapabilityRegistration dynamicCapabilities; + + private final TextDocumentStateManager files = new TextDocumentStateManager(); + + protected ParametricLanguageRouter(ExecutorService exec, @Nullable LanguageParameter dedicatedLanguage) { + super(exec); + this.dedicatedLanguage = dedicatedLanguage; + } + + //// LANGUAGE MANAGEMENT + + private ISingleLanguageService language(TextDocumentItem textDocument) { + return language(Locations.toLoc(textDocument.getUri())); + } + + private TextDocumentService language(VersionedTextDocumentIdentifier textDocument) { + return language(Locations.toLoc(textDocument.getUri())); + } + + private TextDocumentService language(TextDocumentIdentifier textDocument) { + return language(Locations.toLoc(textDocument.getUri())); + } + + 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'", lang)); + } + return service; + } + + private Optional safeLanguage(ISourceLocation loc) { + var ext = extension(loc); + if ("".equals(ext)) { + var languages = new HashSet<>(languagesByExtension.values()); + if (languages.size() == 1) { + logger.trace("File was opened without an extension; falling back to the single registered language for: {}", loc); + return languages.stream().findFirst(); + } else { + logger.error("File was opened without an extension and there are multiple languages registered, so we cannot pick a fallback for: {}", loc); + return Optional.empty(); + } + } + return Optional.ofNullable(languagesByExtension.get(ext)); + } + + private static String extension(ISourceLocation doc) { + return URIUtil.getExtension(doc); + } + + private @PolyNull R route(TextDocumentIdentifier file, BiFunction func, P param) { + return route(Locations.toLoc(file.getUri()), func, param); + } + + 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); + } + + //// WORKSPACE REQUESTS + + @Override + public void didOpen(DidOpenTextDocumentParams params) { + language(params.getTextDocument()).didOpen(params); + } + + @Override + public void didChange(DidChangeTextDocumentParams params) { + files.updateFile(Locations.toLoc(params.getTextDocument())); + language(params.getTextDocument()).didChange(params); + } + + @Override + public void didSave(DidSaveTextDocumentParams params) { + language(params.getTextDocument()).didSave(params); + } + + @Override + public void didClose(DidCloseTextDocumentParams params) { + var loc = Locations.toLoc(params.getTextDocument()); + files.removeFile(loc); + language(loc).didClose(params); + } + + @Override + 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(Locations.toLoc(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 + 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(Locations.toLoc(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 + + 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 + // 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(metaCommandName()))); + result.setInlayHintProvider(true); + result.setSelectionRangeProvider(true); + result.setFoldingRangeProvider(true); + result.setCallHierarchyProvider(true); + } + + @Override + public void shutdown() { + // TODO Kill all delegate processes + exec.shutdown(); + } + + @Override + public void pair(BaseWorkspaceService workspaceService) { + // Nothing to do; no need to pair with ourselves + } + + @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); + } + } + + 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; + }); + } + + @Override + public synchronized 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()); + } + + // `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()); + } + + @Override + public synchronized void unregisterLanguage(LanguageParameter lang) { + logger.info("unregisterLanguage({})", lang.getName()); + var removedLang = languageServices.remove(lang.getName()); + if (removedLang != null) { + removedLang.unregisterLanguage(lang); + // TODO Kill the delegate process for this language and clean up maps + } + } + + @Override + public void projectAdded(String name, ISourceLocation projectRoot) { + // No need to do anything + } + + @Override + public void projectRemoved(String name, ISourceLocation projectRoot) { + // No need to do anything + } + + @Override + public LineColumnOffsetMap getColumnMap(ISourceLocation file) { + return files.getColumnMap(file); + } + + @Override + public ColumnMaps getColumnMaps() { + return files.getColumnMaps(); + } + + @Override + public @Nullable TextDocumentState getDocumentState(ISourceLocation file) { + return files.getDocumentState(file); + } + + @Override + public boolean isManagingFile(ISourceLocation file) { + return files.isManagingFile(file); + } + + @Override + public void cancelProgress(String progressId) { + languageServices.values().stream().forEach(l -> l.cancelProgress(progressId)); + } + + @Override + public CompletableFuture> callHierarchyIncomingCalls( + CallHierarchyIncomingCallsParams params) { + return route(Locations.toLoc(params.getItem().getUri()), TextDocumentService::callHierarchyIncomingCalls, params); + } + + @Override + public CompletableFuture> callHierarchyOutgoingCalls( + CallHierarchyOutgoingCallsParams params) { + return route(Locations.toLoc(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(), TextDocumentService::semanticTokensFull, params); + } + + @Override + public CompletableFuture> semanticTokensFullDelta( + SemanticTokensDeltaParams params) { + return route(params.getTextDocument(), TextDocumentService::semanticTokensFullDelta, params); + } + + @Override + public CompletableFuture semanticTokensRange(SemanticTokensRangeParams params) { + return route(params.getTextDocument(), TextDocumentService::semanticTokensRange, params); + } + + @Override + public CompletableFuture executeCommand(ExecuteCommandParams 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 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/ParametricLanguageServer.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricLanguageServer.java index 5913f1a12..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 @@ -27,12 +27,14 @@ 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.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 +45,14 @@ public static void main(String[] args) { dedicatedLanguage = null; } + AtomicReference 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..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 @@ -26,21 +26,17 @@ */ 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; 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; @@ -50,9 +46,7 @@ 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; import org.eclipse.lsp4j.CallHierarchyIncomingCall; import org.eclipse.lsp4j.CallHierarchyIncomingCallsParams; @@ -64,7 +58,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 +73,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 +95,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 +107,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; @@ -129,8 +119,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; @@ -140,8 +128,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; @@ -157,7 +143,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.Maps; +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; @@ -177,29 +163,23 @@ 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); + 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; private @MonotonicNonNull CapabilityRegistration dynamicCapabilities; + private @MonotonicNonNull LanguageContributionsMultiplexer multiplexer; - private final Map files; - private final ColumnMaps columns; - - /** 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<>(); - - private final @Nullable LanguageParameter dedicatedLanguage; + 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(); @@ -209,89 +189,17 @@ public class ParametricTextDocumentService implements IBaseTextDocumentService, tf.sourceLocationType(), "to"); @SuppressWarnings({"initialization", "methodref.receiver.bound"}) // this::getContents - public ParametricTextDocumentService(ExecutorService exec, @Nullable LanguageParameter dedicatedLanguage) { + 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(); - - 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); } @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 ""; - } - } - - 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; + public void initializeServerCapabilities(ClientCapabilities clientCapabilities, ServerCapabilities result) { + // Nothing to do } private BaseWorkspaceService availableWorkspaceService() { @@ -301,6 +209,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; @@ -316,16 +231,11 @@ private LanguageClient availableClient() { @Override public void connect(LanguageClient client) { this.client = client; - facts.values().forEach(v -> v.setClient(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 @@ -343,7 +253,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); } @@ -359,10 +269,10 @@ 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); + 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)) { @@ -390,9 +300,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 { @@ -403,7 +313,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)); } @@ -411,18 +321,18 @@ 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()); - facts(file.getLocation()).reportParseErrors(file.getLocation(), diagnostics.version(), parseErrors); + availableFileFacts().reportParseErrors(file.getLocation(), diagnostics.version(), parseErrors); }); } @@ -459,7 +369,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) @@ -496,7 +406,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(); } @@ -549,60 +459,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, columns); - 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()); - var language = safeLanguage(l); - if (language.isPresent()) { - ILanguageContributions contrib = contributions.get(language.get()); - if (contrib != null) { - bundled.computeIfAbsent(contrib, k -> new ArrayList<>()).add(rename); + }) + .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(); } - } - } - - return bundled; + availableClient().showMessage(new MessageParams(MessageType.Error, message)); + return null; // Return of type `Void` is unused, but required + }); } private IConstructor fileRenameToDocumentEdit(FileRename rename) { @@ -647,7 +538,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(" ")); @@ -662,67 +553,26 @@ 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); - } - - 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 (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(); - } 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)) - ); + return new CodeLens(Locations.toRange(loc, getColumnMaps()), CodeActions.constructorToCommand(RASCAL_META_COMMAND, languageName, command), null); } 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) { 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) { - 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() { @@ -771,7 +621,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); } @@ -779,7 +629,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 @@ -799,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) { @@ -818,7 +668,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()); } @@ -854,12 +704,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 @@ -890,17 +740,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); @@ -913,7 +763,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); } @@ -921,7 +771,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 @@ -930,10 +780,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()); }); } @@ -952,7 +802,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); @@ -963,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(), columns.get(loc))); + .thenApply(ci -> completion.toLSP(this, ci, RASCAL_META_COMMAND, contrib.getName(), loc.getBeginLine(), getColumnMap(loc))); }) .thenApply(Either::forLeft), () -> Either.forLeft(Collections.emptyList())); } @@ -971,13 +821,14 @@ 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) - ); - var fact = facts.computeIfAbsent(lang.getName(), t -> - new ParametricFileFacts(exec, columns, multiplexer) - ); + if (multiplexer == null) { + multiplexer = new LanguageContributionsMultiplexer(lang.getName(), exec); + } + if (facts == null) { + facts = new ParametricFileFacts(exec, getColumnMaps(), availableMultiplexer()); + } var parserConfig = lang.getPrecompiledParser(); if (parserConfig != null) { @@ -1000,21 +851,20 @@ 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. // 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()); - for (var f : files.keySet()) { + for (var f : getOpenFiles()) { if (extensions.contains(extension(f))) { updateFileState(lang, f); } @@ -1026,26 +876,23 @@ 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 contributions.entrySet().stream().map(e -> new ICapabilityParams() { + return Set.of(new ICapabilityParams() { @Override public ILanguageContributions contributions() { - return e.getValue(); + return availableMultiplexer(); } @Override public Set fileExtensions() { - return extensionsByLang.getOrDefault(e.getKey(), Collections.emptySet()); + return registeredExtensions; } - }).collect(Collectors.toSet()); + }); } 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; @@ -1063,16 +910,14 @@ 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; } else { - var fact = facts.get(lang.getName()); - if (fact != null) { - fact.reloadContributions(); + if (facts != null) { + facts.reloadContributions(); } } } @@ -1083,11 +928,13 @@ public synchronized void unregisterLanguage(LanguageParameter lang) { for (var extension : lang.getExtensions()) { this.registeredExtensions.remove(extension); } - facts.remove(lang.getName()); - contributions.remove(lang.getName()); + // TODO What does clean-up look like? + // facts.remove(lang.getName()); + availableMultiplexer().clearContributors(); } - availableCapabilities().update(buildLanguageParams()); + // TODO Update capabilities dynamically + // availableCapabilities().update(buildLanguageParams()); } @Override @@ -1101,35 +948,22 @@ public void projectRemoved(String name, ISourceLocation projectRoot) { } @Override - public CompletableFuture executeCommand(String languageName, String command) { - ILanguageContributions contribs = contributions.get(languageName); + 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); } } - @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)); + availableMultiplexer().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/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/parametric/SingleLanguageServer.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/SingleLanguageServer.java new file mode 100644 index 000000000..ebb80233a --- /dev/null +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/SingleLanguageServer.java @@ -0,0 +1,318 @@ +/* + * 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.Command; +import org.eclipse.lsp4j.CompletionItem; +import org.eclipse.lsp4j.CompletionList; +import org.eclipse.lsp4j.CompletionParams; +import org.eclipse.lsp4j.CreateFilesParams; +import org.eclipse.lsp4j.DefinitionParams; +import org.eclipse.lsp4j.DeleteFilesParams; +import org.eclipse.lsp4j.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.DocumentFormattingParams; +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.Location; +import org.eclipse.lsp4j.LocationLink; +import org.eclipse.lsp4j.PrepareRenameDefaultBehavior; +import org.eclipse.lsp4j.PrepareRenameParams; +import org.eclipse.lsp4j.PrepareRenameResult; +import org.eclipse.lsp4j.Range; +import org.eclipse.lsp4j.ReferenceParams; +import org.eclipse.lsp4j.RenameFilesParams; +import org.eclipse.lsp4j.RenameParams; +import org.eclipse.lsp4j.SelectionRange; +import org.eclipse.lsp4j.SelectionRangeParams; +import org.eclipse.lsp4j.SemanticTokens; +import org.eclipse.lsp4j.SemanticTokensDelta; +import org.eclipse.lsp4j.SemanticTokensDeltaParams; +import org.eclipse.lsp4j.SemanticTokensParams; +import org.eclipse.lsp4j.SemanticTokensRangeParams; +import org.eclipse.lsp4j.SymbolInformation; +import org.eclipse.lsp4j.TextEdit; +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; + +import io.usethesource.vallang.IValue; + +public class SingleLanguageServer implements ISingleLanguageService { + + private final String languageName; + private final ParametricTextDocumentService docService; + private final ParametricWorkspaceService wsService; + + /*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); + } + + @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, CompletionList>> completion(CompletionParams position) { + return docService.completion(position); + } + + @Override + public CompletableFuture, List>> definition( + DefinitionParams params) { + return docService.definition(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> prepareCallHierarchy(CallHierarchyPrepareParams params) { + return docService.prepareCallHierarchy(params); + } + + @Override + public CompletableFuture> prepareRename( + PrepareRenameParams params) { + return docService.prepareRename(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> 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 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 executeCommand(String command) { + return docService.executeCommand(languageName, command); + } + + @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); + } + + @Override + 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); + } + + @Override + public void connect(LanguageClient client) { + docService.connect(client); + wsService.connect(client); + } + +} 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..653ba0405 --- /dev/null +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/TextDocumentStateManager.java @@ -0,0 +1,141 @@ +/* + * 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.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; +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; + + 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) { + 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(@UnknownInitialization TextDocumentStateManager this, 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<@KeyFor("this.files") ISourceLocation> getOpenFiles() { + return files.keySet(); + } + + static 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/rascal/RascalTextDocumentService.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/RascalTextDocumentService.java index a13d62b58..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(BaseWorkspaceService.RASCAL_COMMAND))); + result.setExecuteCommandProvider(new ExecuteCommandOptions(Collections.singletonList(RASCAL_COMMAND))); result.setSelectionRangeProvider(true); } @@ -482,17 +485,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 @@ -701,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, "", BaseWorkspaceService.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) { @@ -720,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 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 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..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 @@ -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; @@ -86,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); @@ -117,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) { @@ -161,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 BaseWorkspaceService.RASCAL_META_COMMAND + "-" + dedicatedLanguageName; - } - else if (BaseWorkspaceService.RASCAL_LANGUAGE.equals(language)) { - return BaseWorkspaceService.RASCAL_COMMAND; - } - else { - return BaseWorkspaceService.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) { 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 new file mode 100644 index 000000000..71de9bc83 --- /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, null); + // 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(); + } +}