Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
c8899ba
First version of "completion" design
PieterOlivier Aug 22, 2025
392711b
Completed first version of "completion for dsls" design.
PieterOlivier Aug 25, 2025
c1ab656
Updated completion service design based on better understanding of th…
PieterOlivier Aug 25, 2025
87000af
Fixed documentation
PieterOlivier Aug 26, 2025
92eef7b
Update rascal-lsp/src/main/rascal/library/util/LanguageServer.rsc
PieterOlivier Aug 26, 2025
11517b8
Update rascal-lsp/src/main/rascal/library/util/LanguageServer.rsc
PieterOlivier Aug 26, 2025
8dd1872
Update rascal-lsp/src/main/rascal/library/util/LanguageServer.rsc
PieterOlivier Aug 26, 2025
7ac0970
Update rascal-lsp/src/main/rascal/library/util/LanguageServer.rsc
PieterOlivier Aug 26, 2025
370169c
Update rascal-lsp/src/main/rascal/library/util/LanguageServer.rsc
PieterOlivier Aug 26, 2025
fa12957
Update rascal-lsp/src/main/rascal/library/util/LanguageServer.rsc
PieterOlivier Aug 26, 2025
9fd52b3
Update rascal-lsp/src/main/rascal/library/util/LanguageServer.rsc
PieterOlivier Aug 26, 2025
90d3d71
Processed review comments on "completion for DSL design" PR.
PieterOlivier Aug 26, 2025
07916c0
Merge branch 'completions/parametric' of github.com:usethesource/rasc…
PieterOlivier Aug 26, 2025
c329475
Documented differences between CompletionItemKind and DocumentSymbolK…
PieterOlivier Aug 26, 2025
5bd8b5d
Update rascal-lsp/src/main/rascal/library/demo/lang/pico/LanguageServ…
PieterOlivier Aug 27, 2025
75de3de
Replaced string command with Command.
PieterOlivier Aug 27, 2025
bfac3d9
Merge branch 'completions/parametric' of github.com:usethesource/rasc…
PieterOlivier Aug 27, 2025
77b9990
Merge branch 'main' into completions/parametric
PieterOlivier Aug 27, 2025
250b04e
Brought completion design more in line with lsp spec
PieterOlivier Sep 1, 2025
9f16fa5
Unified DocumentSymbolKind and CompletionItemKind again
PieterOlivier Sep 6, 2025
f0f1468
Updated Rascal completion example
PieterOlivier Sep 6, 2025
e50b42d
Update rascal-lsp/src/main/rascal/library/util/LanguageServer.rsc
PieterOlivier Sep 22, 2025
d37a1df
Added synopsis to new ADTs
PieterOlivier Sep 22, 2025
92758e2
Merge branch 'completions/parametric' of github.com:usethesource/rasc…
PieterOlivier Sep 22, 2025
fa18f89
Replaced cursor loc with an offset inside the inner focus tree
PieterOlivier Sep 24, 2025
9a5ae8d
Replaced CompletionDocumentation adt with just a simple str
PieterOlivier Sep 24, 2025
a224319
Merge remote-tracking branch 'origin/main' into completions/parametric
toinehartman Oct 20, 2025
79d07e7
Implement parametric completion.
toinehartman Oct 20, 2025
22d6e43
Multiplex completion requests by trigger character.
toinehartman Oct 21, 2025
c42e37e
Set missing positional fields.
toinehartman Oct 21, 2025
ff45aa5
Set markdown content.
toinehartman Oct 21, 2025
ed6a0b3
Dynamically register completion capability.
toinehartman Oct 21, 2025
1909749
Map completion edit and kind.
toinehartman Oct 21, 2025
e7cee55
Simplify conversion of edits.
toinehartman Oct 21, 2025
7d5ffd4
Safe keyword access.
toinehartman Oct 22, 2025
7de757c
Improved routing of completion requests.
toinehartman Oct 22, 2025
8f1440a
Set completion insert mode.
toinehartman Oct 22, 2025
80c9d36
Fix positions and offsets.
toinehartman Oct 22, 2025
3140133
Improved dynamic registration bookkeeping.
toinehartman Oct 22, 2025
16255ff
Synchronizing multiple contribution registrations.
toinehartman Oct 23, 2025
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
Original file line number Diff line number Diff line change
Expand Up @@ -283,7 +283,7 @@ public CompletableFuture<InitializeResult> initialize(InitializeParams params) {
logger.info("LSP connection started (connected to {} version {})", params.getClientInfo().getName(), params.getClientInfo().getVersion());
logger.debug("LSP client capabilities: {}", params.getCapabilities());
final InitializeResult initializeResult = new InitializeResult(new ServerCapabilities());
lspDocumentService.initializeServerCapabilities(initializeResult.getCapabilities());
lspDocumentService.initializeServerCapabilities(params.getCapabilities(), initializeResult.getCapabilities());
lspWorkspaceService.initialize(params.getCapabilities(), params.getWorkspaceFolders(), initializeResult.getCapabilities());
logger.debug("Initialized LSP connection with capabilities: {}", initializeResult);
return initializeResult;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
import java.util.List;
import java.util.concurrent.CompletableFuture;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.eclipse.lsp4j.ClientCapabilities;
import org.eclipse.lsp4j.DeleteFilesParams;
import org.eclipse.lsp4j.RenameFilesParams;
import org.eclipse.lsp4j.ServerCapabilities;
Expand All @@ -47,7 +48,7 @@ public interface IBaseTextDocumentService extends TextDocumentService {
static final Duration NO_DEBOUNCE = Duration.ZERO;
static final Duration NORMAL_DEBOUNCE = Duration.ofMillis(800);

void initializeServerCapabilities(ServerCapabilities result);
void initializeServerCapabilities(ClientCapabilities clientCapabilities, ServerCapabilities result);
void shutdown();
void connect(LanguageClient client);
void pair(BaseWorkspaceService workspaceService);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
import org.rascalmpl.values.parsetrees.ITree;
import org.rascalmpl.vscode.lsp.util.concurrent.InterruptibleFuture;
import io.usethesource.vallang.IConstructor;
import io.usethesource.vallang.IInteger;
import io.usethesource.vallang.IList;
import io.usethesource.vallang.ISet;
import io.usethesource.vallang.ISourceLocation;
Expand All @@ -58,11 +59,12 @@ public interface ILanguageContributions {
public InterruptibleFuture<ISet> implementation(IList focus);
public InterruptibleFuture<IList> codeAction(IList focus);
public InterruptibleFuture<IList> selectionRange(IList focus);

public InterruptibleFuture<ISourceLocation> prepareRename(IList focus);
public InterruptibleFuture<ITuple> rename(IList focus, String name);
public InterruptibleFuture<ITuple> didRenameFiles(IList fileRenames);
public InterruptibleFuture<IList> completion(IList focus, IInteger cursorOffset, IConstructor trigger);

public CompletableFuture<IList> completionTriggerCharacters();
public CompletableFuture<IList> parseCodeActions(String command);

public CompletableFuture<Boolean> hasAnalysis();
Expand All @@ -79,6 +81,7 @@ public interface ILanguageContributions {
public CompletableFuture<Boolean> hasCodeAction();
public CompletableFuture<Boolean> hasDidRenameFiles();
public CompletableFuture<Boolean> hasSelectionRange();
public CompletableFuture<Boolean> hasCompletion();

public CompletableFuture<Boolean> specialCaseHighlighting();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
import org.rascalmpl.vscode.lsp.util.concurrent.InterruptibleFuture;
import io.usethesource.vallang.IBool;
import io.usethesource.vallang.IConstructor;
import io.usethesource.vallang.IInteger;
import io.usethesource.vallang.IList;
import io.usethesource.vallang.ISet;
import io.usethesource.vallang.ISourceLocation;
Expand Down Expand Up @@ -94,6 +95,8 @@ public class InterpretedLanguageContributions implements ILanguageContributions
private final CompletableFuture<@Nullable IFunction> rename;
private final CompletableFuture<@Nullable IFunction> didRenameFiles;
private final CompletableFuture<@Nullable IFunction> selectionRange;
private final CompletableFuture<@Nullable IFunction> completion;
private final CompletableFuture<IList> completionTriggerCharacters;

private final CompletableFuture<Boolean> hasAnalysis;
private final CompletableFuture<Boolean> hasBuild;
Expand All @@ -109,6 +112,7 @@ public class InterpretedLanguageContributions implements ILanguageContributions
private final CompletableFuture<Boolean> hasRename;
private final CompletableFuture<Boolean> hasDidRenameFiles;
private final CompletableFuture<Boolean> hasSelectionRange;
private final CompletableFuture<Boolean> hasCompletion;

private final CompletableFuture<Boolean> specialCaseHighlighting;

Expand Down Expand Up @@ -155,6 +159,8 @@ public InterpretedLanguageContributions(LanguageParameter lang, IBaseTextDocumen
this.rename = getFunctionFor(contributions, LanguageContributions.RENAME);
this.didRenameFiles = getFunctionFor(contributions, LanguageContributions.DID_RENAME_FILES);
this.selectionRange = getFunctionFor(contributions, LanguageContributions.SELECTION_RANGE);
this.completion = getFunctionFor(contributions, LanguageContributions.COMPLETION);
this.completionTriggerCharacters = getContributionParameter(contributions, LanguageContributions.COMPLETION, LanguageContributions.COMPLETION_TRIGGER_CHARACTERS, VF.list());

// assign boolean properties once instead of wasting futures all the time
this.hasAnalysis = nonNull(this.analysis);
Expand All @@ -171,6 +177,7 @@ public InterpretedLanguageContributions(LanguageParameter lang, IBaseTextDocumen
this.hasRename = nonNull(this.rename);
this.hasDidRenameFiles = nonNull(this.didRenameFiles);
this.hasSelectionRange = nonNull(this.selectionRange);
this.hasCompletion = nonNull(this.completion);

this.specialCaseHighlighting = getContributionParameter(contributions,
LanguageContributions.PARSING,
Expand Down Expand Up @@ -229,8 +236,24 @@ private static boolean hasContribution(ISet contributions, String name) {

private static CompletableFuture<Boolean> getContributionParameter(
CompletableFuture<ISet> contributions, String name, String parameter) {
return getContributionParameter(contributions, name, parameter, VF.bool(false)).thenApply(IBool::getValue);
}

@SuppressWarnings("unchecked")
private static <T extends IValue> CompletableFuture<T> getContributionParameter(CompletableFuture<ISet> contributions, String name, String parameter, T defaultVal) {
return contributions.thenApply(c -> {
var contrib = getContribution(c, name);
if (contrib == null) {
return defaultVal;
}

return contributions.thenApply(c -> isTrue(getContribution(c, name), parameter));
var val = contrib.asWithKeywordParameters().getParameter(parameter);
try {
return (T) val;
} catch (ClassCastException e) {
return defaultVal;
}
});
}

private static boolean isTrue(@Nullable IConstructor constructor, String parameter) {
Expand Down Expand Up @@ -405,6 +428,17 @@ public InterruptibleFuture<IList> selectionRange(IList focus) {
return execFunction(LanguageContributions.SELECTION_RANGE, selectionRange, VF.list(), focus);
}

@Override
public InterruptibleFuture<IList> completion(IList focus, IInteger cursorOffset, IConstructor trigger) {
debug(LanguageContributions.COMPLETION, focus.length());
return execFunction(LanguageContributions.COMPLETION, completion, VF.list(), focus, cursorOffset, trigger);
}

@Override
public CompletableFuture<IList> completionTriggerCharacters() {
return completionTriggerCharacters;
}

private void debug(String name, Object param) {
logger.debug("{}({})", name, param);
}
Expand Down Expand Up @@ -473,6 +507,11 @@ public CompletableFuture<Boolean> hasSelectionRange() {
return hasSelectionRange;
}

@Override
public CompletableFuture<Boolean> hasCompletion() {
return hasCompletion;
}

@Override
public CompletableFuture<Boolean> hasAnalysis() {
return hasAnalysis;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,14 @@
import java.util.function.Function;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.rascalmpl.values.IRascalValueFactory;
import org.rascalmpl.values.parsetrees.ITree;
import org.rascalmpl.vscode.lsp.util.Completion;
import org.rascalmpl.vscode.lsp.util.concurrent.CompletableFutureUtils;
import org.rascalmpl.vscode.lsp.util.concurrent.InterruptibleFuture;

import io.usethesource.vallang.IConstructor;
import io.usethesource.vallang.IInteger;
import io.usethesource.vallang.IList;
import io.usethesource.vallang.ISet;
import io.usethesource.vallang.ISourceLocation;
Expand All @@ -45,6 +50,8 @@
@SuppressWarnings("java:S3077") // Fields in this class are read/written sequentially
public class LanguageContributionsMultiplexer implements ILanguageContributions {

private static final IRascalValueFactory VF = IRascalValueFactory.getInstance();

private final ExecutorService ownExecuter;
private final String name;

Expand Down Expand Up @@ -84,6 +91,7 @@ private static final <T> CompletableFuture<T> failedInitialization() {
private volatile CompletableFuture<Boolean> hasRename = failedInitialization();
private volatile CompletableFuture<Boolean> hasDidRenameFiles = failedInitialization();
private volatile CompletableFuture<Boolean> hasSelectionRange = failedInitialization();
private volatile CompletableFuture<Boolean> hasCompletion = failedInitialization();

private volatile CompletableFuture<Boolean> specialCaseHighlighting = failedInitialization();

Expand Down Expand Up @@ -176,6 +184,7 @@ private synchronized void calculateRouting() {
hasDidRenameFiles = anyTrue(ILanguageContributions::hasDidRenameFiles);
hasCodeAction = anyTrue(ILanguageContributions::hasCodeAction);
hasSelectionRange = anyTrue(ILanguageContributions::hasSelectionRange);
hasCompletion = anyTrue(ILanguageContributions::hasCompletion);

// Always use the special-case highlighting status of *the first*
// contribution (possibly using the default value in the Rascal ADT if
Expand Down Expand Up @@ -327,13 +336,29 @@ public InterruptibleFuture<IList> codeAction(IList focus) {
}

@Override
public CompletableFuture<Boolean> hasSelectionRange() {
return hasSelectionRange;
public InterruptibleFuture<IList> selectionRange(IList focus) {
return flatten(selectionRange, c -> c.selectionRange(focus));
}

@Override
public InterruptibleFuture<IList> selectionRange(IList focus) {
return flatten(selectionRange, c -> c.selectionRange(focus));
public InterruptibleFuture<IList> completion(IList focus, IInteger cursorOffset, IConstructor trigger) {
// Instead of pre-computing the completion contribution, we need to dynamically route based on the trigger here
var completion = findFirstOrDefault(c -> CompletableFutureUtils.and(
c.hasCompletion(),
() -> Completion.isTriggered(trigger, c.completionTriggerCharacters())
));

return flatten(completion, c -> c.completion(focus, cursorOffset, trigger));
}

@Override
public CompletableFuture<IList> completionTriggerCharacters() {
// A multiplexer supports the union of triggers of its implementations
return CompletableFutureUtils.flatten(
contributions.stream().map(c -> c.contrib.completionTriggerCharacters()),
VF::list,
IList::union // remove duplicates
);
}

@Override
Expand Down Expand Up @@ -391,6 +416,11 @@ public CompletableFuture<Boolean> hasInlayHint() {
return hasInlayHint;
}

@Override
public CompletableFuture<Boolean> hasSelectionRange() {
return hasSelectionRange;
}

@Override
public CompletableFuture<Boolean> hasRename() {
return hasRename;
Expand All @@ -401,6 +431,11 @@ public CompletableFuture<Boolean> hasDidRenameFiles() {
return hasDidRenameFiles;
}

@Override
public CompletableFuture<Boolean> hasCompletion() {
return hasCompletion;
}

@Override
public CompletableFuture<Boolean> specialCaseHighlighting() {
return specialCaseHighlighting;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,12 +48,17 @@
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.eclipse.lsp4j.ApplyWorkspaceEditParams;
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.CompletionOptions;
import org.eclipse.lsp4j.CompletionParams;
import org.eclipse.lsp4j.DefinitionParams;
import org.eclipse.lsp4j.DeleteFilesParams;
import org.eclipse.lsp4j.Diagnostic;
Expand Down Expand Up @@ -118,12 +123,15 @@
import org.rascalmpl.vscode.lsp.IBaseLanguageClient;
import org.rascalmpl.vscode.lsp.IBaseTextDocumentService;
import org.rascalmpl.vscode.lsp.TextDocumentState;
import org.rascalmpl.vscode.lsp.parametric.capabilities.CompletionCapability;
import org.rascalmpl.vscode.lsp.parametric.capabilities.DynamicCapabilities;
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;
import org.rascalmpl.vscode.lsp.terminal.ITerminalIDEServer.LanguageParameter;
import org.rascalmpl.vscode.lsp.uri.FallbackResolver;
import org.rascalmpl.vscode.lsp.util.CodeActions;
import org.rascalmpl.vscode.lsp.util.Completion;
import org.rascalmpl.vscode.lsp.util.Diagnostics;
import org.rascalmpl.vscode.lsp.util.DocumentChanges;
import org.rascalmpl.vscode.lsp.util.DocumentSymbols;
Expand Down Expand Up @@ -161,6 +169,7 @@ public class ParametricTextDocumentService implements IBaseTextDocumentService,
private final SemanticTokenizer tokenizer = new SemanticTokenizer();
private @MonotonicNonNull LanguageClient client;
private @MonotonicNonNull BaseWorkspaceService workspaceService;
private @MonotonicNonNull DynamicCapabilities dynamicCapabilities;

private final Map<ISourceLocation, TextDocumentState> files;
private final ColumnMaps columns;
Expand Down Expand Up @@ -224,7 +233,7 @@ public String getContents(ISourceLocation file) {
}
}

public void initializeServerCapabilities(ServerCapabilities result) {
public void initializeServerCapabilities(ClientCapabilities clientCapabilities, ServerCapabilities result) {
result.setDefinitionProvider(true);
result.setTextDocumentSync(TextDocumentSyncKind.Full);
result.setHoverProvider(true);
Expand All @@ -239,6 +248,11 @@ public void initializeServerCapabilities(ServerCapabilities result) {
result.setInlayHintProvider(true);
result.setSelectionRangeProvider(true);
result.setFoldingRangeProvider(true);

if (!clientCapabilities.getTextDocument().getCompletion().getDynamicRegistration()) {
// TODO Can we do our best to supply a reasonable set of default trigger characters here?
result.setCompletionProvider(new CompletionOptions(false, null));
}
}

private String getRascalMetaCommandName() {
Expand Down Expand Up @@ -272,6 +286,7 @@ private LanguageClient availableClient() {
@Override
public void connect(LanguageClient client) {
this.client = client;
this.dynamicCapabilities = new DynamicCapabilities(client, List.of(new CompletionCapability()), ownExecuter);
facts.values().forEach(v -> v.setClient(client));
if (dedicatedLanguage != null) {
// if there was one scheduled, we now start it up, since the connection has been made
Expand Down Expand Up @@ -828,6 +843,30 @@ public CompletableFuture<List<SelectionRange>> selectionRange(SelectionRangePara
Collections::emptyList);
}

@Override
public CompletableFuture<Either<List<CompletionItem>, CompletionList>> completion(CompletionParams params) {
logger.debug("Completion: {} at {} with {}", params.getTextDocument(), params.getPosition(), params.getContext());

var loc = Locations.toLoc(params.getTextDocument());
var contrib = contributions(loc);
var file = getFile(loc);

return recoverExceptions(file.getCurrentTreeAsync(true)
.thenApply(Versioned::get)
.thenCompose(t -> {
var completion = new Completion();
var lspPos = params.getPosition();
var rascalPos = Locations.toRascalPosition(loc, lspPos, columns);
var focus = TreeSearch.computeFocusList(t, rascalPos.getLine(), rascalPos.getCharacter());
var trigger = completion.triggerKindToRascal(params.getContext());
var cursorOffset = rascalPos.getCharacter() - TreeAdapter.getLocation((ITree) focus.get(0)).getBeginColumn();
return contrib.completion(focus, VF.integer(cursorOffset), trigger)
.get()
.thenApply(ci -> completion.toLSP((IBaseTextDocumentService) this, ci, dedicatedLanguageName, contrib.getName(), rascalPos.getLine(), columns.get(loc)));
})
.thenApply(Either::forLeft), () -> Either.forLeft(Collections.emptyList()));
}

@Override
public synchronized void registerLanguage(LanguageParameter lang) {
logger.info("registerLanguage({})", lang.getName());
Expand Down Expand Up @@ -864,10 +903,13 @@ public synchronized void registerLanguage(LanguageParameter lang) {
multiplexer.addContributor(buildContributionKey(lang),
new InterpretedLanguageContributions(lang, this, availableWorkspaceService(), (IBaseLanguageClient)clientCopy, ownExecuter));

dynamicCapabilities.registerCapabilities(multiplexer);

fact.reloadContributions();
fact.setClient(clientCopy);
}


private static String buildContributionKey(LanguageParameter lang) {
return lang.getMainFunction() + "::" + lang.getMainFunction();
}
Expand Down
Loading
Loading