diff --git a/rascal-lsp/src/main/checkerframework/lsp4j.astub b/rascal-lsp/src/main/checkerframework/lsp4j.astub index a1bd7470e..a39323ad8 100644 --- a/rascal-lsp/src/main/checkerframework/lsp4j.astub +++ b/rascal-lsp/src/main/checkerframework/lsp4j.astub @@ -451,3 +451,20 @@ public class SemanticTokensCapabilities extends DynamicRegistrationCapabilities public @Nullable Boolean getServerCancelSupport() {} public @Nullable Boolean getAugmentsSyntaxTokens() {} } + +package org.eclipse.lsp4j; + +import org.checkerframework.checker.nullness.qual.*; + +public class DocumentFilter { + public DocumentFilter(@Nullable String language, @Nullable String scheme, @Nullable Either pattern) {} +} + +package org.eclipse.lsp4j; + +import org.checkerframework.checker.nullness.qual.*; + +public class Registration { + public Registration(String id, String method) {} + public Registration(String id, String method, @Nullable Object registerOptions) {} +} diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/TextDocumentState.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/TextDocumentState.java index a8579ea9d..bb509f81f 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/TextDocumentState.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/TextDocumentState.java @@ -244,7 +244,7 @@ private void parse() { } }, exec); } catch (NoContributionException e) { - logger.debug("Ignoring missing parser for {}", location, e); + logger.debug("Ignoring missing parser for {}", location); treeAsync.completeOnTimeout(new Versioned<>(version, IRascalValueFactory.getInstance().character(0), timestamp), 60, TimeUnit.SECONDS); } } diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/NoContributions.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/NoContributions.java index b30c31360..d8b999f2a 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/NoContributions.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/NoContributions.java @@ -87,7 +87,7 @@ public String getName() { @Override public CompletableFuture parsing(ISourceLocation loc, String input) { - return CompletableFuture.failedFuture(new NoContributionException("parsing")); + throw new NoContributionException("parsing"); } @Override diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricTextDocumentService.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricTextDocumentService.java index 1e029683e..a7a7bfebf 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,6 +26,7 @@ */ package org.rascalmpl.vscode.lsp.parametric; +import java.net.URI; import java.time.Duration; import java.util.ArrayList; import java.util.Arrays; @@ -39,6 +40,7 @@ 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.BiFunction; import java.util.function.Function; @@ -112,7 +114,6 @@ 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; import org.eclipse.lsp4j.jsonrpc.ResponseErrorException; @@ -138,6 +139,7 @@ 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.capabilities.SemanticTokensCapability; import org.rascalmpl.vscode.lsp.parametric.model.ParametricFileFacts; import org.rascalmpl.vscode.lsp.parametric.model.ParametricSummary; import org.rascalmpl.vscode.lsp.parametric.model.ParametricSummary.SummaryLookup; @@ -180,6 +182,8 @@ public class ParametricTextDocumentService extends TextDocumentStateManager impl private final String dedicatedLanguageName; private final SemanticTokenizer tokenizer = new SemanticTokenizer(); + private final Set extensionLessSchemes = new CopyOnWriteArraySet<>(); + private @MonotonicNonNull LanguageClient client; private @MonotonicNonNull BaseWorkspaceService workspaceService; private @MonotonicNonNull CapabilityRegistration dynamicCapabilities; @@ -224,6 +228,7 @@ public void initializeServerCapabilities(ClientCapabilities clientCapabilities, // 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 SemanticTokensCapability() , new CompletionCapability() , /* new FileOperationCapability.DidCreateFiles(exec), */ new FileOperationCapability.DidRenameFiles(exec), new FileOperationCapability.DidDeleteFiles(exec) ); @@ -235,7 +240,6 @@ public void initializeServerCapabilities(ClientCapabilities clientCapabilities, 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)); @@ -303,6 +307,22 @@ public void didOpen(DidOpenTextDocumentParams params) { TextDocumentState file = open(params.getTextDocument(), timestamp); handleParsingErrors(file, file.getCurrentDiagnosticsAsync()); triggerAnalyzer(file, NORMAL_DEBOUNCE); + + // Discover capabilities + discoverExtensionLessScheme(URIUtil.assumeCorrect(params.getTextDocument().getUri())); + } + + /** + * Captures extensions-less schemes and updates dynamic capabilities to apply to them. + * @param uri A URI that should be associated with the parametric server. + */ + private void discoverExtensionLessScheme(URI uri) { + var ext = URIUtil.getExtension(Locations.toLoc(uri)); + // We want the original scheme, not the one possibly modified when converting URI -> loc + if (ext.equals("") && extensionLessSchemes.add(uri.getScheme())) { + // Should be called from the main, single-threaded request pool + updateCapabilities(); + } } @Override @@ -917,11 +937,6 @@ public synchronized void registerLanguage(LanguageParameter lang) { this.registeredExtensions.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. - 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 : getOpenFiles()) { @@ -930,6 +945,32 @@ public synchronized void registerLanguage(LanguageParameter lang) { refreshFileState(f); } } + + // Do this last, after the parser for open editors has been updated, + // since this might trigger new requests on the editor contents. + // `updateCapabilities`/`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: `updateCapabilities` returns a void future, which we do not have to wait on. + updateCapabilities(); + } + + private CompletableFuture refreshEditors() { + var client = availableClient(); + // Do all in parallel, and return a future that completes when each of these completes + return CompletableFutureUtils.reduce(List.of( + client.refreshCodeLenses(), + client.refreshDiagnostics(), + client.refreshFoldingRanges(), + client.refreshInlayHints(), + client.refreshInlineValues(), + client.refreshSemanticTokens() + ), (v1, v2) -> null); + } + + private synchronized CompletableFuture updateCapabilities() { + return availableCapabilities() + .update(buildLanguageParams()) + .thenCompose(v -> refreshEditors()); } /** @@ -948,6 +989,11 @@ public ILanguageContributions contributions() { public Set fileExtensions() { return extensionsByLang.getOrDefault(e.getKey(), Collections.emptySet()); } + + @Override + public Set extensionLessSchemes() { + return extensionLessSchemes; + } }).collect(Collectors.toSet()); } @@ -997,7 +1043,8 @@ public synchronized void unregisterLanguage(LanguageParameter lang) { contributions.remove(lang.getName()); } - availableCapabilities().update(buildLanguageParams()); + // Should be called from the main, single-threaded request pool + updateCapabilities(); } @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 cb5c6fb72..a262d92be 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 @@ -40,18 +40,22 @@ import java.util.concurrent.atomic.AtomicReference; import java.util.function.Function; import java.util.stream.Collectors; +import java.util.stream.Stream; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.checkerframework.checker.nullness.qual.Nullable; import org.eclipse.lsp4j.ClientCapabilities; +import org.eclipse.lsp4j.DocumentFilter; import org.eclipse.lsp4j.MessageParams; import org.eclipse.lsp4j.MessageType; import org.eclipse.lsp4j.Registration; import org.eclipse.lsp4j.RegistrationParams; import org.eclipse.lsp4j.ServerCapabilities; +import org.eclipse.lsp4j.TextDocumentRegistrationOptions; import org.eclipse.lsp4j.Unregistration; import org.eclipse.lsp4j.UnregistrationParams; import org.eclipse.lsp4j.jsonrpc.ResponseErrorException; +import org.eclipse.lsp4j.jsonrpc.messages.Either; import org.eclipse.lsp4j.services.LanguageClient; import org.rascalmpl.vscode.lsp.util.concurrent.CompletableFutureUtils; @@ -287,15 +291,43 @@ private void handleError(@Nullable Throwable t, AbstractDynamicCapability } // Filter contributions by providing this capability - return CompletableFutureUtils.filter(languages, cap::isProvidedBy).<@Nullable Registration>thenCompose(cs -> { - if (cs.isEmpty()) { + return CompletableFutureUtils.filter(languages, cap::isProvidedBy).<@Nullable Registration>thenCompose(providingLanguages -> { + if (providingLanguages.isEmpty()) { return CompletableFutureUtils.completedFuture(null, exec); } - var allOpts = cs.stream().>map(cap::options).collect(Collectors.toList()); + var allOpts = providingLanguages.stream().>map(cap::options).collect(Collectors.toList()); return CompletableFutureUtils.reduce(allOpts, cap::mergeNullableOptions) // non-empty, so no need to provide a reduction identity + .<@Nullable T>thenApply(opts -> { + if (opts instanceof TextDocumentRegistrationOptions) { + setDocumentSelector((TextDocumentRegistrationOptions) opts, providingLanguages); + } + return opts; + }) .thenApply(opts -> new Registration(cap.id(), cap.methodName(), opts)); }); } + /** + * Add document selectors for extensions and schemes if not already present + */ + private void setDocumentSelector(TextDocumentRegistrationOptions opts, Collection params) { + // extension & scheme selectors + var additionalSelectors = params.stream() + .flatMap(c -> { + var extSelectors = c.fileExtensions().stream().map(ext -> new DocumentFilter("parametric-rascalmpl", null, Either.forLeft(String.format("**/*.%s", ext)))); + var schemeSelectors = c.extensionLessSchemes().stream().map(scheme -> new DocumentFilter("parametric-rascalmpl", scheme, null)); + return Stream.concat(extSelectors, schemeSelectors); + }) + .distinct(); + + if (opts.getDocumentSelector() == null) { + // No need to concatenate anything + opts.setDocumentSelector(additionalSelectors.collect(Collectors.toList())); + return; + } + + opts.setDocumentSelector(Stream.concat(opts.getDocumentSelector().stream(), additionalSelectors).distinct().collect(Collectors.toList())); + } + } diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/capabilities/ICapabilityParams.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/capabilities/ICapabilityParams.java index 87f3bcd9b..4282bf7e0 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/capabilities/ICapabilityParams.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/capabilities/ICapabilityParams.java @@ -35,4 +35,5 @@ public interface ICapabilityParams { ILanguageContributions contributions(); Set fileExtensions(); + Set extensionLessSchemes(); } diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/capabilities/SemanticTokensCapability.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/capabilities/SemanticTokensCapability.java new file mode 100644 index 000000000..d22b0bac3 --- /dev/null +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/capabilities/SemanticTokensCapability.java @@ -0,0 +1,74 @@ +/* + * 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.capabilities; + +import java.util.concurrent.CompletableFuture; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.eclipse.lsp4j.ClientCapabilities; +import org.eclipse.lsp4j.SemanticTokensCapabilities; +import org.eclipse.lsp4j.SemanticTokensWithRegistrationOptions; +import org.eclipse.lsp4j.ServerCapabilities; +import org.eclipse.lsp4j.TextDocumentClientCapabilities; +import org.rascalmpl.vscode.lsp.rascal.conversion.SemanticTokenizer; +import org.rascalmpl.vscode.lsp.util.Nullables; + +public class SemanticTokensCapability extends AbstractDynamicCapability { + + public SemanticTokensCapability() { + super("textDocument/semanticTokens"); + } + + @Override + protected CompletableFuture<@Nullable SemanticTokensWithRegistrationOptions> options(ICapabilityParams language) { + // Note: `mergeOptions` is implemented based on the assumptions that we return a constant here. + return CompletableFuture.completedFuture(SemanticTokenizer.options()); + } + + @Override + protected CompletableFuture isProvidedBy(ICapabilityParams params) { + return CompletableFuture.completedFuture(true); + } + + @Override + protected SemanticTokensWithRegistrationOptions mergeOptions(SemanticTokensWithRegistrationOptions o1, SemanticTokensWithRegistrationOptions o2) { + // Since `SemanticTokenCapability::options` always returns this, we don't need to do a smart merge here. + return SemanticTokenizer.options(); + } + + @Override + protected boolean isDynamicallySupportedBy(ClientCapabilities clientCapabilities) { + return Nullables.has(clientCapabilities.getTextDocument(), TextDocumentClientCapabilities::getSemanticTokens, SemanticTokensCapabilities::getDynamicRegistration); + } + + @Override + protected void registerStatically(ServerCapabilities capabilities) { + capabilities.setSemanticTokensProvider(SemanticTokenizer.options()); + } + +} + + 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 d793273cb..40833baef 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 @@ -191,7 +191,7 @@ public void initializeServerCapabilities(ClientCapabilities clientCapabilities, result.setTextDocumentSync(TextDocumentSyncKind.Full); result.setDocumentSymbolProvider(true); result.setHoverProvider(true); - result.setSemanticTokensProvider(tokenizer.options()); + result.setSemanticTokensProvider(SemanticTokenizer.options()); result.setCodeLensProvider(new CodeLensOptions(false)); result.setFoldingRangeProvider(true); result.setRenameProvider(new RenameOptions(true)); diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/conversion/SemanticTokenizer.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/conversion/SemanticTokenizer.java index 2fe9be185..153782d58 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/conversion/SemanticTokenizer.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/conversion/SemanticTokenizer.java @@ -76,7 +76,7 @@ public SemanticTokens semanticTokensFull(ITree tree, boolean specialCaseHighligh return new SemanticTokens(tokens.getTheList()); } - public SemanticTokensWithRegistrationOptions options() { + public static SemanticTokensWithRegistrationOptions options() { SemanticTokensWithRegistrationOptions result = new SemanticTokensWithRegistrationOptions(); SemanticTokensLegend legend = new SemanticTokensLegend(TokenTypes.getTokenTypes(), TokenTypes.getTokenModifiers()); diff --git a/rascal-lsp/src/test/java/org/rascalmpl/vscode/lsp/parametric/capabilities/CapabilityRegistrationTest.java b/rascal-lsp/src/test/java/org/rascalmpl/vscode/lsp/parametric/capabilities/CapabilityRegistrationTest.java index 6a560ad3a..d49b7bd3b 100644 --- a/rascal-lsp/src/test/java/org/rascalmpl/vscode/lsp/parametric/capabilities/CapabilityRegistrationTest.java +++ b/rascal-lsp/src/test/java/org/rascalmpl/vscode/lsp/parametric/capabilities/CapabilityRegistrationTest.java @@ -38,6 +38,7 @@ import static org.mockito.Mockito.never; import static org.mockito.Mockito.only; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.when; import java.util.ArrayList; @@ -192,6 +193,11 @@ public ILanguageContributions contributions() { public Set fileExtensions() { return Set.of(lang.getExtensions()); } + + @Override + public Set extensionLessSchemes() { + return Set.of(); + } }; } @@ -271,9 +277,8 @@ private void assertRegistrationOptionEquals(String method, Object expected, Obje public void registerIdenticalContribution() throws InterruptedException, ExecutionException { registerSequentially(List.of(".", "::"), List.of(".", "::")); - InOrder inOrder = inOrder(client); - inOrder.verify(client).registerCapability(registrationCaptor.capture()); - inOrder.verifyNoMoreInteractions(); + verify(client).registerCapability(registrationCaptor.capture()); + verifyNoMoreInteractions(client); assertSingleRegistration("textDocument/completion", new CompletionRegistrationOptions(List.of(".", "::"), false), registrationCaptor.getAllValues().get(0)); } diff --git a/rascal-vscode-extension/license-config.json b/rascal-vscode-extension/license-config.json index 7eda977b0..7603b9956 100644 --- a/rascal-vscode-extension/license-config.json +++ b/rascal-vscode-extension/license-config.json @@ -3,7 +3,7 @@ "ignoreFile": ".gitignore", "ignore": ["**/.DS_Store",".vscode-test","**/*.jar", "**/*.vsix", "**/*.md", ".editorconfig", ".vscodeignore", "**/*.gitignore", "**/*.svg", "webpack.config.js", "test-workspace/**/*", "**/.npmignore", ".node-version"], "licenseFormats": { - "js|ts": { + "js|ts|mjs": { "prepend": "/*", "append": " */", "eachLine": { diff --git a/rascal-vscode-extension/src/lsp/ParameterizedLanguageServer.ts b/rascal-vscode-extension/src/lsp/ParameterizedLanguageServer.ts index 616a52340..63d3257a8 100644 --- a/rascal-vscode-extension/src/lsp/ParameterizedLanguageServer.ts +++ b/rascal-vscode-extension/src/lsp/ParameterizedLanguageServer.ts @@ -102,8 +102,7 @@ export class ParameterizedLanguageServer implements vscode.Disposable { for (const doc of vscode.workspace.textDocuments) { const ext = path.extname(doc.uri.path); if (ext !== "" && lang.extensions.includes(ext.substring(1))) { - // (Re)set the language ID to re-trigger contribution requests - await vscode.languages.setTextDocumentLanguage(doc, "plaintext"); + // Associate open editors with the given extensions with this language server await vscode.languages.setTextDocumentLanguage(doc, this.languageId); } } 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 954da6e2d..72eec55e6 100644 --- a/rascal-vscode-extension/src/test/vscode-suite/dsl.test.ts +++ b/rascal-vscode-extension/src/test/vscode-suite/dsl.test.ts @@ -31,7 +31,7 @@ import { Delays, expectCompletions, IDEOperations, ignoreFails, printRascalOutpu import { expect } from 'chai'; import * as fs from 'fs/promises'; import { Suite } from 'mocha'; -import * as path from 'path'; +import * as path from 'path/posix'; function parameterizedDescribe(body: (this: Suite, errorRecovery: boolean) => void) { describe('DSL', function() { body.apply(this, [false]); }); @@ -49,6 +49,11 @@ parameterizedDescribe(function (errorRecovery: boolean) { printRascalOutputOnFailure('Language Parametric Rascal'); + async function unloadPico(repl: RascalREPL) { + await repl.execute("import util::LanguageServer;"); + await repl.execute('unregisterLanguage("Pico", {"pico", "pico-new"});'); + } + async function loadPico() { const repl = new RascalREPL(bench, driver); await repl.start(); @@ -63,6 +68,14 @@ parameterizedDescribe(function (errorRecovery: boolean) { await repl.terminate(); } + async function setParametricLanguage(editor: TextEditor) { + await (await editor.getTab()).select(); + await bench.executeCommand("workbench.action.editor.changeLanguageMode"); + const inputBox = new InputBox(); + await inputBox.setText("parametric-rascalmpl"); + await inputBox.confirm(); + } + before(async () => { browser = VSBrowser.instance; driver = browser.driver; @@ -128,11 +141,7 @@ parameterizedDescribe(function (errorRecovery: boolean) { const editor = await ide.newUntitledFile(bench, driver, 1); expect(editor).to.not.be.undefined; - await bench.executeCommand("workbench.action.editor.changeLanguageMode"); - - const inputBox = new InputBox(); - await inputBox.setText("parametric-rascalmpl"); - await inputBox.confirm(); + await setParametricLanguage(editor); await editor.setText(`begin declare @@ -265,8 +274,8 @@ end await ide.moveFile("testing.pico", "dest", bench); await driver.wait(async() => { - const text = await testFile.getText(); - return text.indexOf("%% File moved from") !== -1; + const text = await ignoreFails(testFile.getText()); + return text?.indexOf("%% File moved from") !== -1; }, Delays.extremelySlow, "Pico file should contain evidence of move", Delays.normal); await fs.rm(newDir, {recursive: true, force: true}); @@ -428,4 +437,20 @@ end }, Delays.normal, "Static content should be shown"); }); + it("updates open editors on registration", async function() { + if (errorRecovery) { this.skip(); } + + const repl = new RascalREPL(bench, driver); + await repl.start(); + await unloadPico(repl); + await repl.terminate(); + + const editor = await ide.openModule(TestWorkspace.picoFile); + await setParametricLanguage(editor); + + await loadPico(); + + // Dynamically registered capability + await ide.hasSyntaxHighlighting(editor, Delays.slow); + }); }); diff --git a/rascal-vscode-extension/src/test/vscode-suite/utils.ts b/rascal-vscode-extension/src/test/vscode-suite/utils.ts index 4060be4cb..6bdffe9c6 100644 --- a/rascal-vscode-extension/src/test/vscode-suite/utils.ts +++ b/rascal-vscode-extension/src/test/vscode-suite/utils.ts @@ -338,7 +338,7 @@ export class IDEOperations { return result; } return undefined; - }, Delays.normal, "Could not open file") as Promise; + }, Delays.normal, `Could not open file: ${file}`) as Promise; } async appendSpace(editor: TextEditor, line = 1) { diff --git a/runUItests.sh b/runUItests.sh index 368ab9414..96ea069e0 100755 --- a/runUItests.sh +++ b/runUItests.sh @@ -17,7 +17,7 @@ rm -rf $UITESTS || true # compiling the TS code as well as the test TS code at least once is required before execution # this assumes you have run `npm ci` at least once since a large update -npm run compile-tests +npm run compile:tests # test what was compiled VSCODE_VERSION=$(grep '"vscode":' package.json | awk -F^ '{ print $2 }' | awk -F\" '{ print $1 }')