diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricTextDocumentService.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricTextDocumentService.java index 4ea343dc4..bf14fb5b7 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricTextDocumentService.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricTextDocumentService.java @@ -1013,10 +1013,14 @@ private static String buildContributionKey(LanguageParameter lang) { return lang.getMainFunction() + "::" + lang.getMainFunction(); } + public static boolean isLanguageCompletelyRemoved(LanguageParameter lang) { + return lang.getMainModule() == null || lang.getMainModule().isEmpty(); + } + @Override public synchronized void unregisterLanguage(LanguageParameter lang) { logger.info("unregisterLanguage({})", lang.getName()); - boolean removeAll = lang.getMainModule() == null || lang.getMainModule().isEmpty(); + boolean removeAll = isLanguageCompletelyRemoved(lang); if (!removeAll) { var contrib = contributions.get(lang.getName()); if (contrib != null && !contrib.removeContributor(buildContributionKey(lang))) { diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/capabilities/AbstractDynamicCapability.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/capabilities/AbstractDynamicCapability.java index 594b74558..0a104af25 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/capabilities/AbstractDynamicCapability.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/capabilities/AbstractDynamicCapability.java @@ -150,7 +150,8 @@ public boolean equals(@Nullable Object obj) { } var other = (AbstractDynamicCapability) obj; return Objects.equals(id, other.id) - && Objects.equals(methodName, other.methodName); + && Objects.equals(methodName, other.methodName) + && Objects.equals(preferStaticRegistration, other.preferStaticRegistration); } @Override diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/capabilities/CapabilityRegistration.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/capabilities/CapabilityRegistration.java index f848ea052..452f9e154 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/capabilities/CapabilityRegistration.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/capabilities/CapabilityRegistration.java @@ -75,6 +75,8 @@ public class CapabilityRegistration { private final Set> staticCapabilities; private final AtomicReference> lastParams = new AtomicReference<>(Collections.emptyList()); + private final AtomicReference> currentUpdate; + // Map of method names with current registration values private final Map currentRegistrations = new ConcurrentHashMap<>(); @@ -87,6 +89,7 @@ public CapabilityRegistration(LanguageClient client, Executor exec, ClientCapabi this.client = client; this.exec = exec; this.noop = CompletableFutureUtils.completedFuture(null, exec); + this.currentUpdate = new AtomicReference<>(noop); // Check whether to register capabilities dynamically or statically var dynamicCaps = new HashSet>(); @@ -120,9 +123,7 @@ public void registerStaticCapabilities(ServerCapabilities result) { * @return A future that completes with a boolean that is false when any registration failed, and true otherwise. */ public CompletableFuture update(Collection languages) { - logger.debug("Updating {} dynamic capabilities for {} languages", dynamicCapabilities.size(), languages.size()); // Copy the contributions so we know we are looking at a stable collection of elements. - /* *VERY IMPORTANT* Setting the atomic reference from the thread that called us. We need to be sure that the `lastContributions` reference actually points to the most recently known contributions. @@ -130,6 +131,11 @@ public CompletableFuture update(Collection languages) { Additionally, this function should be called from a thread pool with predictable execution order. */ lastParams.set(List.copyOf(languages)); + return currentUpdate.updateAndGet(current -> current.thenComposeAsync(v -> doUpdate(languages), exec)); + } + + private CompletableFuture doUpdate(Collection languages) { + logger.debug("Updating {} dynamic capabilities for {} languages", dynamicCapabilities.size(), languages.size()); return CompletableFutureUtils.reduce(dynamicCapabilities.stream().map(this::updateRegistration), exec) .thenAccept(_v -> logger.debug("Done updating dynamic capabilities")); } @@ -200,6 +206,7 @@ private CompletableFuture updateRegistration(AbstractDynamicCapability result = register(registration, existingRegistration); } + // TODO Check/Fix that if one capability fails, the rest are still registered return result.handle((_v, t) -> { if (t != null) { // An error occurred. Inform the user and do not recurse. diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/ActualRoutingLanguageServer.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/ActualRoutingLanguageServer.java index 792239ba6..76e06405c 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/ActualRoutingLanguageServer.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/ActualRoutingLanguageServer.java @@ -113,7 +113,7 @@ public class ActualRoutingLanguageServer extends BaseLanguageServer.ActualLangua private final Map> languageServers = new ConcurrentHashMap<>(); private final Map languagesByExtension = new ConcurrentHashMap<>(); - private final MultipleClientProxy client = new MultipleClientProxy(); + private @MonotonicNonNull MultipleClientProxy remoteClient; private @MonotonicNonNull InitializeParams initializeParams; private final JsonWriter logForwarder; @@ -173,7 +173,7 @@ public CompletableFuture route(ISourceLocation lo @Override public void connect(LanguageClient client) { super.connect(client); // first let the super class proxy the client - this.client.connect(availableClient()); + this.remoteClient = new MultipleClientProxy(availableClient(), getExecutor()); } private static String extension(ISourceLocation doc) { @@ -349,6 +349,11 @@ public void write(JsonWriter writer, ProxiedIValue proxiedValue) throws IOExcept } private @Nullable CompletableFuture startServer(LanguageParameter lang) { + if (remoteClient == null) { + // This should never happen, since it's initialized by `connect` before we are able to receive any `registerLanguage` requests. + throw new IllegalStateException("Remote client is not initialized"); + } + var serverParams = BaseLanguageServer.DEPLOY_MODE ? startServerProcess(lang) : connectToServer(lang) @@ -360,7 +365,7 @@ public void write(JsonWriter writer, ProxiedIValue proxiedValue) throws IOExcept var serverLauncher = new Launcher.Builder() .setRemoteInterface(IBaseLanguageServerExtensions.class) - .setLocalService(client) + .setLocalService(remoteClient) .setInput(serverParams.getLeft()) .setOutput(serverParams.getMiddle()) .configureGson(ActualRoutingLanguageServer::configureProxyGson) @@ -468,27 +473,23 @@ public synchronized CompletableFuture sendRegisterLanguage(LanguageParamet public synchronized CompletableFuture sendUnregisterLanguage(LanguageParameter lang) { logger.debug("rascal/sendUnregisterLanguage({})", lang.getName()); - var work = route(lang.getName()) - .thenCompose(s -> s.sendUnregisterLanguage(lang)); - - // Note: this should be handled for the deployed scenario by the process onExit hook. - boolean removeAll = lang.getMainModule() == null || lang.getMainModule().isEmpty(); - if (removeAll) { - // clear the whole language - logger.trace("unregisterLanguage({}) completely", lang.getName()); + if (ParametricTextDocumentService.isLanguageCompletelyRemoved(lang)) { + // Do not remove the connection to the server. + // For the deployed scenario, this is handled by the process onExit hook. + // For the development scenario, we maintain the connection, since the remote server does not exit. for (var extension : lang.getExtensions()) { this.languagesByExtension.remove(extension); } - var removed = languageServers.remove(lang.getName()); - if (removed != null) { - work = work - .thenCompose(ignored -> removed) - .thenCompose(server -> server.shutdown().thenAccept(ignored -> server.exit())); - } } - return work; + return route(lang.getName()).handle((s, t) -> { + if (s == null) { + // Nothing to unregister + return CompletableFutureUtils.completedFuture(null, getExecutor()); + } + return s.sendUnregisterLanguage(lang); + }).thenCompose(Function.identity()); } @Override diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/MultipleClientProxy.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/MultipleClientProxy.java index be6d604bd..f18c00523 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/MultipleClientProxy.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/MultipleClientProxy.java @@ -28,7 +28,15 @@ import java.net.URI; import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CopyOnWriteArraySet; +import java.util.concurrent.ExecutorService; +import java.util.function.Predicate; +import java.util.stream.Collectors; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.checkerframework.checker.nullness.qual.Nullable; @@ -41,18 +49,21 @@ import org.eclipse.lsp4j.ProgressParams; import org.eclipse.lsp4j.PublishDiagnosticsParams; import org.eclipse.lsp4j.Range; +import org.eclipse.lsp4j.Registration; import org.eclipse.lsp4j.RegistrationParams; import org.eclipse.lsp4j.ShowDocumentParams; import org.eclipse.lsp4j.ShowDocumentResult; import org.eclipse.lsp4j.ShowMessageRequestParams; +import org.eclipse.lsp4j.TextDocumentContentRefreshParams; +import org.eclipse.lsp4j.Unregistration; import org.eclipse.lsp4j.UnregistrationParams; import org.eclipse.lsp4j.WorkDoneProgressCreateParams; import org.eclipse.lsp4j.WorkspaceFolder; import org.eclipse.lsp4j.services.LanguageClient; -import org.eclipse.lsp4j.services.LanguageClientAware; import org.rascalmpl.uri.remote.jsonrpc.ISourceLocationChanged; import org.rascalmpl.vscode.lsp.IBaseLanguageClient; import org.rascalmpl.vscode.lsp.parametric.LanguageRegistry.LanguageParameter; +import org.rascalmpl.vscode.lsp.util.concurrent.CompletableFutureUtils; import io.usethesource.vallang.IInteger; import io.usethesource.vallang.IString; @@ -60,156 +71,225 @@ /** * Client proxy implementation that aggregates results from multiple servers before forwarding to its own client. */ -public class MultipleClientProxy implements IBaseLanguageClient, LanguageClientAware { +public class MultipleClientProxy implements IBaseLanguageClient { private static final Logger logger = LogManager.getLogger(MultipleClientProxy.class); - private IBaseLanguageClient client; + private final IBaseLanguageClient client; + private final ExecutorService exec; + private final CompletableFuture noop; - @Override - public void connect(LanguageClient client) { - this.client = (IBaseLanguageClient) client; - } + private final Map> currentRegistrations = new ConcurrentHashMap<>(); - protected IBaseLanguageClient availableClient() { - if (client == null) { - throw new IllegalStateException("Language Client has not been connected yet"); - } - return client; + + protected MultipleClientProxy(LanguageClient client, ExecutorService exec) { + this.client = (IBaseLanguageClient) client; + this.exec = exec; + this.noop = CompletableFutureUtils.completedFuture(null, exec); } @Override public void telemetryEvent(Object object) { - availableClient().telemetryEvent(object); + client.telemetryEvent(object); } @Override public void publishDiagnostics(PublishDiagnosticsParams diagnostics) { - availableClient().publishDiagnostics(diagnostics); + client.publishDiagnostics(diagnostics); } @Override public void showMessage(MessageParams messageParams) { - availableClient().showMessage(messageParams); + client.showMessage(messageParams); } @Override public CompletableFuture showMessageRequest(ShowMessageRequestParams requestParams) { - return availableClient().showMessageRequest(requestParams); + return client.showMessageRequest(requestParams); } @Override public void logMessage(MessageParams message) { - availableClient().logMessage(message); + client.logMessage(message); } @Override public void showContent(URI uri, IString title, IInteger viewColumn) { - availableClient().showContent(uri, title, viewColumn); + client.showContent(uri, title, viewColumn); } @Override public void receiveRegisterLanguage(LanguageParameter lang) { logger.debug("rascal/receiveRegisterLanguage({}, {})", lang.getName(), lang.getMainFunction()); - availableClient().receiveRegisterLanguage(lang); + client.receiveRegisterLanguage(lang); } @Override public void receiveUnregisterLanguage(LanguageParameter lang) { logger.debug("rascal/receiveUnregisterLanguage({}, {})", lang.getName(), lang.getMainFunction()); - availableClient().receiveUnregisterLanguage(lang); + client.receiveUnregisterLanguage(lang); } @Override public void editDocument(URI uri, @Nullable Range range, int viewColumn) { - availableClient().editDocument(uri, range, viewColumn); + client.editDocument(uri, range, viewColumn); } @Override public void startDebuggingSession(int serverPort) { - availableClient().startDebuggingSession(serverPort); + client.startDebuggingSession(serverPort); } @Override public void registerDebugServerPort(int processID, int serverPort) { - availableClient().registerDebugServerPort(processID, serverPort); + client.registerDebugServerPort(processID, serverPort); } @Override public CompletableFuture createProgress(WorkDoneProgressCreateParams params) { - return availableClient().createProgress(params); + return client.createProgress(params); } @Override public void notifyProgress(ProgressParams params) { - availableClient().notifyProgress(params); + client.notifyProgress(params); } @Override public CompletableFuture applyEdit(ApplyWorkspaceEditParams params) { - return availableClient().applyEdit(params); + return client.applyEdit(params); } @Override public CompletableFuture> configuration(ConfigurationParams configurationParams) { - return availableClient().configuration(configurationParams); + return client.configuration(configurationParams); } @Override public void logTrace(LogTraceParams params) { - availableClient().logTrace(params); + client.logTrace(params); } @Override public CompletableFuture refreshCodeLenses() { - return availableClient().refreshCodeLenses(); + return client.refreshCodeLenses(); } @Override public CompletableFuture refreshDiagnostics() { - return availableClient().refreshDiagnostics(); + return client.refreshDiagnostics(); + } + + @Override + public CompletableFuture refreshFoldingRanges() { + return client.refreshFoldingRanges(); } @Override public CompletableFuture refreshInlayHints() { - return availableClient().refreshInlayHints(); + return client.refreshInlayHints(); } @Override public CompletableFuture refreshInlineValues() { - return availableClient().refreshInlineValues(); + return client.refreshInlineValues(); } @Override public CompletableFuture refreshSemanticTokens() { - return availableClient().refreshSemanticTokens(); + return client.refreshSemanticTokens(); + } + + @Override + public CompletableFuture refreshTextDocumentContent(TextDocumentContentRefreshParams params) { + return client.refreshTextDocumentContent(params); } @Override public CompletableFuture showDocument(ShowDocumentParams params) { - return availableClient().showDocument(params); + return client.showDocument(params); } @Override public CompletableFuture registerCapability(RegistrationParams params) { - // TODO Collect/maintain capabilities of all delegate servers, combine, and unregister capabilities if necessary based on that. - return availableClient().registerCapability(params); + return CompletableFutureUtils + .reduce(params + .getRegistrations() + .parallelStream() + .map(this::registerCapability), exec) + .thenAccept(v -> {}); // convert to Void + } + + private CompletableFuture registerCapability(Registration r) { + var c = currentRegistrations.computeIfAbsent(r.getMethod(), k -> new CopyOnWriteArraySet<>()); + // Lock on the registrations for this method for a moment + synchronized (c) { + var similarRegOpt = c.stream().filter(rr -> Objects.equals(rr.getRegisterOptions(), r.getRegisterOptions())).findAny(); + if (similarRegOpt.isPresent()) { + logger.trace("We already have a registration for {} with the same options; ignoring this one", r.getMethod()); + return noop; + } + + c.add(r); + return client.registerCapability(new RegistrationParams(List.of(r))) + .exceptionally(t -> { + logger.error("Exception while registering {}", r, t); + c.remove(r); + return null; + }); + } } @Override public CompletableFuture unregisterCapability(UnregistrationParams params) { - // TODO Collect/maintain capabilities of all delegate servers, combine, and unregister capabilities if necessary based on that. - return availableClient().unregisterCapability(params); + return CompletableFutureUtils + .reduce(params + .getUnregisterations() + .parallelStream() + .map(this::unregisterCapability), exec) + .thenAccept(v -> {}); // convert to Void + } + + private boolean matches(Registration r, Unregistration u) { + return r.getId().equals(u.getId()) + && r.getMethod().equals(u.getMethod()); + } + + private Predicate matches(Unregistration u) { + return r -> matches(r, u); + } + + private CompletableFuture unregisterCapability(Unregistration u) { + var c = currentRegistrations.get(u.getMethod()); + if (c == null) { + return noop; + } + + synchronized (c) { + var cs = c.stream().filter(matches(u)).collect(Collectors.toSet()); + if (cs.isEmpty()) { + // No registrations => nothing to unregister + return noop; + } + + c.removeAll(cs); + return client.unregisterCapability(new UnregistrationParams(List.of(u))) + .exceptionally(t -> { + logger.error("Exception while unregistering {} ({})", u.getMethod(), u, t); + c.addAll(cs); + return null; + }); + } } @Override public CompletableFuture> workspaceFolders() { - return availableClient().workspaceFolders(); + return client.workspaceFolders(); } @Override public void sourceLocationChanged(ISourceLocationChanged changed) { - availableClient().sourceLocationChanged(changed); + client.sourceLocationChanged(changed); } } diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingTextDocumentService.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingTextDocumentService.java index 15cd072cb..01c49a51e 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingTextDocumentService.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingTextDocumentService.java @@ -30,6 +30,7 @@ import java.util.List; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutorService; +import java.util.function.BiFunction; import java.util.stream.Collectors; import org.apache.logging.log4j.LogManager; @@ -85,6 +86,7 @@ import org.eclipse.lsp4j.SemanticTokensRangeParams; import org.eclipse.lsp4j.ServerCapabilities; import org.eclipse.lsp4j.SymbolInformation; +import org.eclipse.lsp4j.TextDocumentIdentifier; import org.eclipse.lsp4j.WorkspaceEdit; import org.eclipse.lsp4j.WorkspaceFolder; import org.eclipse.lsp4j.jsonrpc.messages.Either; @@ -157,6 +159,24 @@ public CompletableFuture route(String language) { return availableServerRouter().route(language).thenApply(LanguageServer::getTextDocumentService); } + private CompletableFuture route(TextDocumentIdentifier doc, BiFunction> endpoint, P params) { + return routeCompose(route(doc), endpoint, params); + } + + private CompletableFuture route(ISourceLocation loc, BiFunction> endpoint, P params) { + return routeCompose(route(loc), endpoint, params); + } + + private CompletableFuture routeCompose(CompletableFuture sf, BiFunction> endpoint, P params) { + return sf.thenCompose(s -> { + logger.trace("Calling endpoint with params: {}", params); + return endpoint.apply(s, params); + }).thenApply(r -> { + logger.trace("Return value: {}", r); + return r; + }); + } + @Override public Collection extensions() { throw new UnsupportedOperationException("extensions() should not be called on the routing server, but only on delegate servers."); @@ -280,103 +300,103 @@ public void didDeleteFiles(DeleteFilesParams params) { @Override public CompletableFuture> callHierarchyIncomingCalls( CallHierarchyIncomingCallsParams params) { - return route(Locations.toLoc(params.getItem().getUri())).thenCompose(s -> s.callHierarchyIncomingCalls(params)); + return route(Locations.toLoc(params.getItem().getUri()), TextDocumentService::callHierarchyIncomingCalls, params); } @Override public CompletableFuture> callHierarchyOutgoingCalls( CallHierarchyOutgoingCallsParams params) { - return route(Locations.toLoc(params.getItem().getUri())).thenCompose(s -> s.callHierarchyOutgoingCalls(params)); + return route(Locations.toLoc(params.getItem().getUri()), TextDocumentService::callHierarchyOutgoingCalls, params); } @Override public CompletableFuture, CompletionList>> completion(CompletionParams position) { - return route(position.getTextDocument()).thenCompose(s -> s.completion(position)); + return route(position.getTextDocument(), TextDocumentService::completion, position); } @Override public CompletableFuture, List>> definition( DefinitionParams params) { - return route(params.getTextDocument()).thenCompose(s -> s.definition(params)); + return route(params.getTextDocument(), TextDocumentService::definition, params); } @Override public CompletableFuture> prepareCallHierarchy(CallHierarchyPrepareParams params) { - return route(params.getTextDocument()).thenCompose(s -> s.prepareCallHierarchy(params)); + return route(params.getTextDocument(), TextDocumentService::prepareCallHierarchy, params); } @Override public CompletableFuture semanticTokensFull(SemanticTokensParams params) { - return route(params.getTextDocument()).thenCompose(s -> s.semanticTokensFull(params)); + return route(params.getTextDocument(), TextDocumentService::semanticTokensFull, params); } @Override public CompletableFuture> semanticTokensFullDelta( SemanticTokensDeltaParams params) { - return route(params.getTextDocument()).thenCompose(s -> s.semanticTokensFullDelta(params)); + return route(params.getTextDocument(), TextDocumentService::semanticTokensFullDelta, params); } @Override public CompletableFuture semanticTokensRange(SemanticTokensRangeParams params) { - return route(params.getTextDocument()).thenCompose(s -> s.semanticTokensRange(params)); + return route(params.getTextDocument(), TextDocumentService::semanticTokensRange, params); } @Override public CompletableFuture> codeLens(CodeLensParams params) { - return route(params.getTextDocument()).thenCompose(s -> s.codeLens(params)); + return route(params.getTextDocument(), TextDocumentService::codeLens, params); } @Override public CompletableFuture> prepareRename( PrepareRenameParams params) { - return route(params.getTextDocument()).thenCompose(s -> s.prepareRename(params)); + return route(params.getTextDocument(), TextDocumentService::prepareRename, params); } @Override public CompletableFuture rename(RenameParams params) { - return route(params.getTextDocument()).thenCompose(s -> s.rename(params)); + return route(params.getTextDocument(), TextDocumentService::rename, params); } @Override public CompletableFuture> inlayHint(InlayHintParams params) { - return route(params.getTextDocument()).thenCompose(s -> s.inlayHint(params)); + return route(params.getTextDocument(), TextDocumentService::inlayHint, params); } @Override public CompletableFuture>> codeAction(CodeActionParams params) { - return route(params.getTextDocument()).thenCompose(s -> s.codeAction(params)); + return route(params.getTextDocument(), TextDocumentService::codeAction, params); } @Override public CompletableFuture>> documentSymbol( DocumentSymbolParams params) { - return route(params.getTextDocument()).thenCompose(s -> s.documentSymbol(params)); + return route(params.getTextDocument(), TextDocumentService::documentSymbol, params); } @Override public CompletableFuture, List>> implementation( ImplementationParams params) { - return route(params.getTextDocument()).thenCompose(s -> s.implementation(params)); + return route(params.getTextDocument(), TextDocumentService::implementation, params); } @Override public CompletableFuture> references(ReferenceParams params) { - return route(params.getTextDocument()).thenCompose(s -> s.references(params)); + return route(params.getTextDocument(), TextDocumentService::references, params); } @Override public CompletableFuture> foldingRange(FoldingRangeRequestParams params) { - return route(params.getTextDocument()).thenCompose(s -> s.foldingRange(params)); + return route(params.getTextDocument(), TextDocumentService::foldingRange, params); } @Override public CompletableFuture<@Nullable Hover> hover(HoverParams params) { - return route(params.getTextDocument()).<@Nullable Hover>thenCompose(s -> s.hover(params)); + return this.route(params.getTextDocument(), TextDocumentService::hover, params); } @Override public CompletableFuture> selectionRange(SelectionRangeParams params) { - return route(params.getTextDocument()).thenCompose(s -> s.selectionRange(params)); + return route(params.getTextDocument(), TextDocumentService::selectionRange, params); } } diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingWorkspaceService.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingWorkspaceService.java index cfe16492c..6a678e768 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingWorkspaceService.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/routing/RoutingWorkspaceService.java @@ -32,13 +32,17 @@ import java.util.concurrent.ExecutorService; import java.util.stream.Collectors; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; +import org.eclipse.lsp4j.CreateFilesParams; +import org.eclipse.lsp4j.DeleteFilesParams; import org.eclipse.lsp4j.ExecuteCommandParams; +import org.eclipse.lsp4j.RenameFilesParams; import org.eclipse.lsp4j.services.LanguageServer; import org.eclipse.lsp4j.services.WorkspaceService; import org.rascalmpl.vscode.lsp.BaseWorkspaceService; import org.rascalmpl.vscode.lsp.IBaseLanguageServerExtensions; import org.rascalmpl.vscode.lsp.util.DocumentRouter; import org.rascalmpl.vscode.lsp.util.concurrent.CompletableFutureUtils; +import org.rascalmpl.vscode.lsp.util.locations.Locations; import io.usethesource.vallang.ISourceLocation; @@ -92,4 +96,25 @@ public CompletableFuture executeCommand(ExecuteCommandParams commandPara return CompletableFutureUtils.completedFuture(commandParams.getCommand() + " was ignored, since it is not a Rascal LSP command.", getExecutor()); } + @Override + public void didCreateFiles(CreateFilesParams params) { + params.getFiles().stream() + .collect(Collectors.groupingBy(f -> route(Locations.toLoc(f.getUri())))) + .forEach((r, creates) -> r.thenAccept(s -> s.didCreateFiles(new CreateFilesParams(creates)))); + } + + @Override + public void didDeleteFiles(DeleteFilesParams params) { + params.getFiles().stream() + .collect(Collectors.groupingBy(f -> route(Locations.toLoc(f.getUri())))) + .forEach((r, deletes) -> r.thenAccept(s -> s.didDeleteFiles(new DeleteFilesParams(deletes)))); + } + + @Override + public void didRenameFiles(RenameFilesParams params) { + params.getFiles().stream() + .collect(Collectors.groupingBy(f -> route(Locations.toLoc(f.getOldUri())))) // like VS Code, notify language associated with the old file name + .forEach((r, renames) -> r.thenAccept(s -> s.didRenameFiles(new RenameFilesParams(renames)))); + } + } diff --git a/rascal-vscode-extension/src/test/vscode-suite/dsl-mix.test.ts b/rascal-vscode-extension/src/test/vscode-suite/dsl-mix.test.ts index 548155596..9c165d745 100644 --- a/rascal-vscode-extension/src/test/vscode-suite/dsl-mix.test.ts +++ b/rascal-vscode-extension/src/test/vscode-suite/dsl-mix.test.ts @@ -65,10 +65,9 @@ describe('DSL [multi-language]', function () { bench = new Workbench(); await ignoreFails(browser.waitForWorkbench()); ide = new IDEOperations(browser); - await ide.load(); + await ide.load("Trace"); await loadLanguages(); ide = new IDEOperations(browser); - await ide.load(); }); after(async () => { diff --git a/rascal-vscode-extension/src/test/vscode-suite/dsl.test.ts b/rascal-vscode-extension/src/test/vscode-suite/dsl.test.ts index 15412f945..9a4ea0249 100644 --- a/rascal-vscode-extension/src/test/vscode-suite/dsl.test.ts +++ b/rascal-vscode-extension/src/test/vscode-suite/dsl.test.ts @@ -55,6 +55,9 @@ parameterizedDescribe(function (errorRecovery: boolean) { } async function loadPico() { + const notifications = await bench.openNotificationsCenter(); + await notifications.clearAllNotifications(); + const repl = new RascalREPL(bench, driver); await repl.start(); await repl.execute("import testing::lang::pico::LanguageServer;", false, Delays.extremelySlow); @@ -81,11 +84,10 @@ parameterizedDescribe(function (errorRecovery: boolean) { bench = new Workbench(); await ignoreFails(browser.waitForWorkbench()); ide = new IDEOperations(browser); - await ide.load(); + await ide.load("Trace"); await loadPico(); protectedFiles = await ProtectedFiles.protect(TestWorkspace.picoFile); ide = new IDEOperations(browser); - await ide.load(); }); after(async() => { @@ -260,8 +262,7 @@ end expect(editorText).to.contain("z := 2"); }); - // TODO Implement this test in a later PR - it.skip("renaming files works", async function() { + it("renaming files works", async function() { if (errorRecovery) { this.skip(); } const newDir = path.join(TestWorkspace.testProject, "src", "main", "pico", "rename-test"); await fs.rm(newDir, {recursive: true, force: true}); @@ -312,8 +313,7 @@ end }, Delays.normal, "Call hierarchy should show `multiply` and its two outgoing calls."); }); - // TODO Implement this test in a later PR - it.skip("completion works", async function() { + it("completion works", async function() { const editor = await ide.openModule(TestWorkspace.picoFile); try { await editor.setTextAtLine(6, " aa : natural;"); @@ -327,8 +327,7 @@ end } }); - // TODO Implement this test in a later PR - it.skip("completion by trigger character works", async function() { + it("completion by trigger character works", async function() { // We will be typing and introducing parse errors, so this only works with error recovery if (!errorRecovery) { this.skip(); } diff --git a/rascal-vscode-extension/src/test/vscode-suite/utils.ts b/rascal-vscode-extension/src/test/vscode-suite/utils.ts index b3b917997..9a27de94f 100644 --- a/rascal-vscode-extension/src/test/vscode-suite/utils.ts +++ b/rascal-vscode-extension/src/test/vscode-suite/utils.ts @@ -217,7 +217,7 @@ export class IDEOperations { this.driver = browser.driver; } - async load() { + async load(logLevel: LogLevel = "Debug") { await ignoreFails(this.browser.waitForWorkbench(Delays.slow)); for (let t = 0; t < 5; t++) { try { @@ -235,7 +235,7 @@ export class IDEOperations { const center = await ignoreFails(new Workbench().openNotificationsCenter()); await ignoreFails(center?.clearAllNotifications()); await ignoreFails(center?.close()); - await assureDebugLevelLoggingIsEnabled(); + await setLogLevel(logLevel); } async cleanup() { @@ -508,17 +508,13 @@ async function showRascalOutput(bbp: BottomBarPanel, channel: string) { return outputView; } -let alreadySetup = false; +type LogLevel = "Trace" | "Debug" | "Info" | "Warning" | "Error" | "Off"; -async function assureDebugLevelLoggingIsEnabled() { - if (alreadySetup) { - return; - } - alreadySetup = true; // to avoid doing this twice/parallel +async function setLogLevel(logLevel: LogLevel) { const prompt = await new Workbench().openCommandPrompt(); await prompt.setText(">workbench.action.setLogLevel"); await prompt.confirm(); - await prompt.setText("Debug"); + await prompt.setText(logLevel); await prompt.confirm(); } @@ -604,14 +600,16 @@ export function isLanguageLoading(bench: Workbench, language: string): () => Pro } export async function expectCompletions(driver: WebDriver, editor: TextEditor, expectedLabels: string[]) { - const completions = await driver.wait(async () => { - const completionMenu = new ContentAssist(editor); - return await ignoreFails(completionMenu.getItems()); - }, Delays.fast, "Completion items not found"); - - expect(completions).to.have.length(expectedLabels.length); - const labels: string[] = await Promise.all(completions!.map(c => c.getLabel())); - expect(labels).deep.equal(expectedLabels); + await driver.wait(async () => { + try { + const completionMenu = new ContentAssist(editor); + const completions = await completionMenu.getItems(); + const labels = await Promise.all(completions.map(c => c.getLabel())); + return labels === expectedLabels; + } catch (e) { + return false; + } + }, Delays.fast, `Completion items do not equal ${expectedLabels}`); }