Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
627c6a5
Set document selectors on dynamic capabilities.
toinehartman Jun 15, 2026
d746799
Remove duplicate extensions from contributions.
toinehartman Jun 16, 2026
b1fe84a
Use orderless mock verification.
toinehartman Jun 16, 2026
c806874
Dynamic registration of semantic token capability.
toinehartman Jun 16, 2026
7904a4e
Discover extension-less schemes from associated files.
toinehartman Jun 16, 2026
05efdb5
Merge remote-tracking branch 'origin/main' into fix/capability-docume…
toinehartman Jun 16, 2026
81a9f66
Implement semantic token capability option merge.
toinehartman Jun 16, 2026
9100f66
Refresh editors using LSP instead of close+open.
toinehartman Jun 17, 2026
db04507
Simplify filter construction.
toinehartman Jun 17, 2026
a371d8d
Get client once.
toinehartman Jun 17, 2026
657e3e9
Only apply contributions to parametric language.
toinehartman Jun 17, 2026
6fd91bf
Use thread-safe set.
toinehartman Jun 17, 2026
030cf1f
Move/add comments on thread-safety of capability updates.
toinehartman Jun 17, 2026
fc8db21
Clarify selector wrapping.
toinehartman Jun 17, 2026
8586f9a
Test that open, associated editor is updated on language registration.
toinehartman Jun 17, 2026
9cc7a34
Skip second consecutive unregistration.
toinehartman Jun 18, 2026
d00f8a9
Merge branch 'main' into fix/capability-document-selectors
toinehartman Jun 18, 2026
a0651ea
Always add contribution-based document selectors to capability-specif…
toinehartman Jun 18, 2026
64d67fd
Merge remote-tracking branch 'origin/main' into fix/capability-docume…
toinehartman Jun 18, 2026
8d5d03e
Merge remote-tracking branch 'origin/main' into fix/capability-docume…
toinehartman Jun 22, 2026
aa52ffa
Merge branch 'main' into fix/capability-document-selectors
toinehartman Jun 22, 2026
b955cd1
Merge remote-tracking branch 'origin/main' into fix/capability-docume…
toinehartman Jun 22, 2026
8437074
Do not print expected exceptions.
toinehartman Jun 22, 2026
db8c580
Fix file rename test.
toinehartman Jun 22, 2026
573add7
Fix test script after #1129.
toinehartman Jun 22, 2026
f45f9c0
Fix license check for esbuild after #1129.
toinehartman Jun 22, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions rascal-lsp/src/main/checkerframework/lsp4j.astub
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, RelativePattern> 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) {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ public String getName() {

@Override
public CompletableFuture<ITree> parsing(ISourceLocation loc, String input) {
return CompletableFuture.failedFuture(new NoContributionException("parsing"));
throw new NoContributionException("parsing");
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -180,6 +182,8 @@

private final String dedicatedLanguageName;
private final SemanticTokenizer tokenizer = new SemanticTokenizer();
private final Set<String> extensionLessSchemes = new CopyOnWriteArraySet<>();

private @MonotonicNonNull LanguageClient client;
private @MonotonicNonNull BaseWorkspaceService workspaceService;
private @MonotonicNonNull CapabilityRegistration dynamicCapabilities;
Expand Down Expand Up @@ -224,6 +228,7 @@
// 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)
);
Expand All @@ -235,7 +240,6 @@
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));
Expand Down Expand Up @@ -303,6 +307,22 @@
TextDocumentState file = open(params.getTextDocument(), timestamp);
handleParsingErrors(file, file.getCurrentDiagnosticsAsync());
triggerAnalyzer(file, NORMAL_DEBOUNCE);

// Discover capabilities
discoverExtensionLessScheme(URIUtil.assumeCorrect(params.getTextDocument().getUri()));
Comment thread
toinehartman marked this conversation as resolved.
}

/**
* 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
Expand Down Expand Up @@ -917,11 +937,6 @@
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()) {
Expand All @@ -930,6 +945,32 @@
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();
Comment thread
toinehartman marked this conversation as resolved.
}

private CompletableFuture<Void> refreshEditors() {
var client = availableClient();

Check warning on line 958 in rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricTextDocumentService.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Rename "client" which hides the field declared at line 187.

See more on https://sonarcloud.io/project/issues?id=usethesource_rascal-language-servers&issues=AZ7Uuo9ZJJR7jXdyz4oX&open=AZ7Uuo9ZJJR7jXdyz4oX&pullRequest=1118
// 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<Void> updateCapabilities() {
return availableCapabilities()
.update(buildLanguageParams())
.thenCompose(v -> refreshEditors());
}

/**
Expand All @@ -948,6 +989,11 @@
public Set<String> fileExtensions() {
return extensionsByLang.getOrDefault(e.getKey(), Collections.emptySet());
}

@Override
public Set<String> extensionLessSchemes() {
return extensionLessSchemes;
}
}).collect(Collectors.toSet());
}

Expand Down Expand Up @@ -997,7 +1043,8 @@
contributions.remove(lang.getName());
}

availableCapabilities().update(buildLanguageParams());
// Should be called from the main, single-threaded request pool
updateCapabilities();
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -287,15 +291,43 @@ private <T> void handleError(@Nullable Throwable t, AbstractDynamicCapability<T>
}

// 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().<CompletableFuture<@Nullable T>>map(cap::options).collect(Collectors.toList());
var allOpts = providingLanguages.stream().<CompletableFuture<@Nullable T>>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<ICapabilityParams> 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()));
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,5 @@
public interface ICapabilityParams {
ILanguageContributions contributions();
Set<String> fileExtensions();
Set<String> extensionLessSchemes();
}
Original file line number Diff line number Diff line change
@@ -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<SemanticTokensWithRegistrationOptions> {

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<Boolean> 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());
}

}


Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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());

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -192,6 +193,11 @@ public ILanguageContributions contributions() {
public Set<String> fileExtensions() {
return Set.of(lang.getExtensions());
}

@Override
public Set<String> extensionLessSchemes() {
return Set.of();
}
};
}

Expand Down Expand Up @@ -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));
}
Expand Down
2 changes: 1 addition & 1 deletion rascal-vscode-extension/license-config.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
Loading
Loading