Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

API and implementation of codefixes and code actions for Rascal itself #478

Open
wants to merge 30 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
fac1420
scaffolded an initial implementation of codefixes and code actioons f…
jurgenvinju Oct 16, 2024
05c9f9a
added first example code action for Rascal
jurgenvinju Oct 16, 2024
f60cff5
added default
jurgenvinju Oct 16, 2024
62f627f
final fixes
jurgenvinju Oct 16, 2024
ea0991d
added sort imports action
jurgenvinju Oct 16, 2024
46f76ea
fixed broken indentation
jurgenvinju Oct 16, 2024
55f6845
improved asyncronous command handling
jurgenvinju Oct 16, 2024
f08530e
removed superfluous newlines in sorted imports and extends
jurgenvinju Oct 17, 2024
0d0dcac
better grouping for imports and extends
jurgenvinju Oct 17, 2024
6be8c16
syntax better spaced
jurgenvinju Oct 17, 2024
0ab364f
added \'add missing license\' action for Rascal
jurgenvinju Oct 23, 2024
4a83365
grammar rules have 1 line distance
jurgenvinju Oct 23, 2024
0eea6fc
added UI test for Rascal code actions; factored out common assertLine…
jurgenvinju Oct 23, 2024
d799dc2
changed order of imports for testing purposes
jurgenvinju Oct 23, 2024
1e074bc
use arrow keys to skip the other actions
jurgenvinju Oct 23, 2024
17953b9
fixed typo
jurgenvinju Oct 23, 2024
4eecdaa
Merge branch 'main' into code-actions-for-rascal
jurgenvinju Oct 23, 2024
214dd88
reintroduced method lost in merge
jurgenvinju Oct 23, 2024
ed75af4
changed test because the action is not currently focused, it is the t…
jurgenvinju Oct 23, 2024
31f88d8
bringing mo to the mountain, just test the first action instead of th…
jurgenvinju Oct 24, 2024
e853737
added LICENSE to test project for testing purposes of the code action…
jurgenvinju Oct 24, 2024
eec1297
added LICENSE to original files
jurgenvinju Oct 24, 2024
e021fa5
make sure to revert changes made by code actions before we continue t…
jurgenvinju Oct 24, 2024
78250cd
factoring common functionality between Rascal LSP and Parametric DSL …
jurgenvinju Oct 24, 2024
f8b0702
acted on suggestions in review by @davylandman
jurgenvinju Oct 24, 2024
c07ba2e
fixed bug with command going to the wrong LSP server
jurgenvinju Oct 24, 2024
58997ec
adding license action only appears on module name now
jurgenvinju Oct 24, 2024
0644f00
factoring constants
jurgenvinju Oct 24, 2024
bbba2cb
fixed commands for Rascal itself
jurgenvinju Oct 24, 2024
d5362ac
fixed indentation
jurgenvinju Oct 24, 2024
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 @@ -46,7 +46,9 @@
import org.eclipse.lsp4j.services.WorkspaceService;

public class BaseWorkspaceService implements WorkspaceService, LanguageClientAware {
public static final String RASCAL_LANGUAGE = "Rascal";
public static final String RASCAL_META_COMMAND = "rascal-meta-command";
public static final String RASCAL_COMMAND = "rascal-command";

private final IBaseTextDocumentService documentService;
private final CopyOnWriteArrayList<WorkspaceFolder> workspaceFolders = new CopyOnWriteArrayList<>();
Expand Down Expand Up @@ -108,11 +110,12 @@ public void didChangeWorkspaceFolders(DidChangeWorkspaceFoldersParams params) {

@Override
public CompletableFuture<Object> executeCommand(ExecuteCommandParams params) {
if (params.getCommand().startsWith(RASCAL_META_COMMAND)) {
if (params.getCommand().startsWith(RASCAL_META_COMMAND) || params.getCommand().startsWith(RASCAL_COMMAND)) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if for RASCAL_COMMAND we also want to do this? as this is about delegating to the sub-language, but that doesn't apply for the rascal commands.

Shouldn't that be a separate branch? that only runs documentService.execute command directly? or are we always passing in the rascal-langauge name everytime?

String languageName = ((JsonPrimitive) params.getArguments().get(0)).getAsString();
String command = ((JsonPrimitive) params.getArguments().get(1)).getAsString();
return documentService.executeCommand(languageName, command).thenApply(v -> v);
}

return CompletableFuture.supplyAsync(() -> params.getCommand() + " was ignored.");
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,5 @@ public interface IBaseTextDocumentService extends TextDocumentService {
void unregisterLanguage(LanguageParameter lang);
CompletableFuture<IValue> executeCommand(String languageName, String command);
LineColumnOffsetMap getColumnMap(ISourceLocation file);

}
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,9 @@
import java.io.IOException;
import java.io.Reader;
import java.time.Duration;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.ConcurrentHashMap;
Expand All @@ -50,7 +48,6 @@
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.eclipse.lsp4j.CodeAction;
import org.eclipse.lsp4j.CodeActionKind;
import org.eclipse.lsp4j.CodeActionParams;
import org.eclipse.lsp4j.CodeLens;
import org.eclipse.lsp4j.CodeLensOptions;
Expand Down Expand Up @@ -90,7 +87,6 @@
import org.eclipse.lsp4j.TextDocumentItem;
import org.eclipse.lsp4j.TextDocumentSyncKind;
import org.eclipse.lsp4j.VersionedTextDocumentIdentifier;
import org.eclipse.lsp4j.WorkspaceEdit;
import org.eclipse.lsp4j.jsonrpc.ResponseErrorException;
import org.eclipse.lsp4j.jsonrpc.messages.Either;
import org.eclipse.lsp4j.jsonrpc.messages.ResponseError;
Expand All @@ -108,11 +104,10 @@
import org.rascalmpl.vscode.lsp.TextDocumentState;
import org.rascalmpl.vscode.lsp.parametric.model.ParametricFileFacts;
import org.rascalmpl.vscode.lsp.parametric.model.ParametricSummary;
import org.rascalmpl.vscode.lsp.parametric.model.RascalADTs;
import org.rascalmpl.vscode.lsp.parametric.model.ParametricSummary.SummaryLookup;
import org.rascalmpl.vscode.lsp.terminal.ITerminalIDEServer.LanguageParameter;
import org.rascalmpl.vscode.lsp.util.CodeActions;
import org.rascalmpl.vscode.lsp.util.Diagnostics;
import org.rascalmpl.vscode.lsp.util.DocumentChanges;
import org.rascalmpl.vscode.lsp.util.FoldingRanges;
import org.rascalmpl.vscode.lsp.util.DocumentSymbols;
import org.rascalmpl.vscode.lsp.util.SemanticTokenizer;
Expand All @@ -123,16 +118,13 @@
import org.rascalmpl.vscode.lsp.util.locations.Locations;
import org.rascalmpl.vscode.lsp.util.locations.impl.TreeSearch;

import com.google.gson.JsonPrimitive;

import io.usethesource.vallang.IBool;
import io.usethesource.vallang.IConstructor;
import io.usethesource.vallang.IList;
import io.usethesource.vallang.ISourceLocation;
import io.usethesource.vallang.IString;
import io.usethesource.vallang.ITuple;
import io.usethesource.vallang.IValue;
import io.usethesource.vallang.IWithKeywordParameters;
import io.usethesource.vallang.exceptions.FactParseError;

public class ParametricTextDocumentService implements IBaseTextDocumentService, LanguageClientAware {
Expand Down Expand Up @@ -386,80 +378,10 @@ private CodeLens locCommandTupleToCodeLense(String languageName, IValue v) {
ISourceLocation loc = (ISourceLocation) t.get(0);
IConstructor command = (IConstructor) t.get(1);

return new CodeLens(Locations.toRange(loc, columns), constructorToCommand(languageName, command), null);
}

private CodeAction constructorToCodeAction(String languageName, IConstructor codeAction) {
IWithKeywordParameters<?> kw = codeAction.asWithKeywordParameters();
IConstructor command = (IConstructor) kw.getParameter(RascalADTs.CodeActionFields.COMMAND);
IString title = (IString) kw.getParameter(RascalADTs.CodeActionFields.TITLE);
IList edits = (IList) kw.getParameter(RascalADTs.CodeActionFields.EDITS);
IConstructor kind = (IConstructor) kw.getParameter(RascalADTs.CodeActionFields.KIND);

// first deal with the defaults. Must mimick what's in util::LanguageServer with the `data CodeAction` declaration
if (title == null) {
if (command != null) {
title = (IString) command.asWithKeywordParameters().getParameter(RascalADTs.CommandFields.TITLE);
}

if (title == null) {
title = IRascalValueFactory.getInstance().string("");
}
}

CodeAction result = new CodeAction(title.getValue());

if (command != null) {
result.setCommand(constructorToCommand(languageName, command));
}

if (edits != null) {
result.setEdit(new WorkspaceEdit(DocumentChanges.translateDocumentChanges(this, edits)));
}

result.setKind(constructorToCodeActionKind(kind));

return result;
return new CodeLens(Locations.toRange(loc, columns), CodeActions.constructorToCommand(dedicatedLanguageName, languageName, command), null);
}

/**
* Translates `refactor(inline())` to `"refactor.inline"` and `empty()` to `""`, etc.
* `kind == null` signals absence of the optional parameter. This is factorede into
* this private function because otherwise every call has to check it.
*/
private String constructorToCodeActionKind(@Nullable IConstructor kind) {
if (kind == null) {
return CodeActionKind.QuickFix;
}

String name = kind.getName();

if (name.isEmpty()) {
return "";
}
else if (name.length() == 1) {
return name.toUpperCase();
}
else if ("empty".equals(name)) {
return "";
}
else {
var kw = kind.asWithKeywordParameters();
for (String kwn : kw.getParameterNames()) {
String nestedName = constructorToCodeActionKind((IConstructor) kw.getParameter(kwn));
name = name + (nestedName.isEmpty() ? "" : ("." + nestedName));
}
}

return name;
}

private Command constructorToCommand(String languageName, IConstructor command) {
IWithKeywordParameters<?> kw = command.asWithKeywordParameters();
IString possibleTitle = (IString) kw.getParameter(RascalADTs.CommandFields.TITLE);

return new Command(possibleTitle != null ? possibleTitle.getValue() : command.toString(), getRascalMetaCommandName(), Arrays.asList(languageName, command.toString()));
}

private void handleParsingErrors(TextDocumentState file) {
handleParsingErrors(file, file.getCurrentTreeAsync());
Expand Down Expand Up @@ -584,34 +506,21 @@ public CompletableFuture<SemanticTokens> semanticTokensRange(SemanticTokensRange

@Override
public CompletableFuture<List<Either<Command, CodeAction>>> codeAction(CodeActionParams params) {
logger.debug("codeActions: {}", params);
logger.debug("codeAction: {}", params);

final ILanguageContributions contribs = contributions(params.getTextDocument());

final var loc = Locations.toLoc(params.getTextDocument());
final var start = params.getRange().getStart();
// convert to Rascal 1-based line
final var startLine = start.getLine() + 1;
// convert to Rascal UTF-32 column width
final var startColumn = columns.get(loc).translateInverseColumn(start.getLine(), start.getCharacter(), false);
final var emptyListFuture = CompletableFuture.completedFuture(IRascalValueFactory.getInstance().list());

// first we make a future stream for filtering out the "fixes" that were optionally sent along with earlier diagnostics
// and which came back with the codeAction's list of relevant (in scope) diagnostics:
// CompletableFuture<Stream<IValue>>
CompletableFuture<Stream<IValue>> quickfixes
= params.getContext().getDiagnostics()
.stream()
.map(Diagnostic::getData)
.filter(Objects::nonNull)
.filter(JsonPrimitive.class::isInstance)
.map(JsonPrimitive.class::cast)
.map(JsonPrimitive::getAsString)
// this is the "magic" resurrection of command terms from the JSON data field
.map(contribs::parseCodeActions)
// this serializes the stream of futures and accumulates their results as a flat list again
.reduce(emptyListFuture, (acc, next) -> acc.thenCombine(next, IList::concat))
.thenApply(IList::stream)
;
var quickfixes = CodeActions.extractActionsFromDiagnostics(params, contribs::parseCodeActions);

// here we dynamically ask the contributions for more actions,
// based on the cursor position in the file and the current parse tree
Expand All @@ -625,13 +534,7 @@ public CompletableFuture<List<Either<Command, CodeAction>>> codeAction(CodeActio
;

// final merging the two streams of commmands, and their conversion to LSP Command data-type
return codeActions.thenCombine(quickfixes, (actions, quicks) ->
Stream.concat(quicks, actions)
.map(IConstructor.class::cast)
.map(cons -> constructorToCodeAction(contribs.getName(), cons))
.map(cmd -> Either.<Command,CodeAction>forRight(cmd))
.collect(Collectors.toList())
);
return CodeActions.mergeAndConvertCodeActions(this, dedicatedLanguageName, contribs.getName(), quickfixes, codeActions);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good that these got extracted out into a helper class.

}

private CompletableFuture<IList> computeCodeActions(final ILanguageContributions contribs, final int startLine, final int startColumn, ITree tree) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
import static org.rascalmpl.vscode.lsp.util.EvaluatorUtil.runEvaluator;

import java.io.IOException;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
Expand All @@ -51,6 +52,7 @@
import org.eclipse.lsp4j.jsonrpc.messages.ResponseErrorCode;
import org.rascalmpl.exceptions.Throw;
import org.rascalmpl.interpreter.Evaluator;
import org.rascalmpl.interpreter.env.ModuleEnvironment;
import org.rascalmpl.library.util.PathConfig;
import org.rascalmpl.values.IRascalValueFactory;
import org.rascalmpl.values.functions.IFunction;
Expand All @@ -59,17 +61,19 @@
import org.rascalmpl.vscode.lsp.BaseWorkspaceService;
import org.rascalmpl.vscode.lsp.IBaseLanguageClient;
import org.rascalmpl.vscode.lsp.RascalLSPMonitor;
import org.rascalmpl.vscode.lsp.util.EvaluatorUtil;
import org.rascalmpl.vscode.lsp.util.RascalServices;
import org.rascalmpl.vscode.lsp.util.concurrent.InterruptibleFuture;
import org.rascalmpl.vscode.lsp.util.locations.ColumnMaps;

import io.usethesource.vallang.IConstructor;
import io.usethesource.vallang.IList;
import io.usethesource.vallang.ISet;
import io.usethesource.vallang.ISourceLocation;
import io.usethesource.vallang.IString;
import io.usethesource.vallang.IValue;
import io.usethesource.vallang.IValueFactory;
import io.usethesource.vallang.exceptions.FactTypeUseException;
import io.usethesource.vallang.io.StandardTextReader;
import io.usethesource.vallang.type.Type;
import io.usethesource.vallang.type.TypeFactory;
import io.usethesource.vallang.type.TypeStore;
Expand All @@ -83,6 +87,8 @@ public class RascalLanguageServices {
private final CompletableFuture<Evaluator> semanticEvaluator;
private final CompletableFuture<Evaluator> compilerEvaluator;

private final CompletableFuture<TypeStore> actionStore;

private final TypeFactory tf = TypeFactory.getInstance();
private final TypeStore store = new TypeStore();
private final Type getPathConfigType = tf.functionType(tf.abstractDataType(store, "PathConfig"), tf.tupleType(tf.sourceLocationType()), tf.tupleEmpty());
Expand All @@ -98,8 +104,9 @@ public RascalLanguageServices(RascalTextDocumentService docService, BaseWorkspac
var monitor = new RascalLSPMonitor(client, logger);

documentSymbolEvaluator = makeFutureEvaluator(exec, docService, workspaceService, client, "Rascal document symbols", monitor, null, false, "lang::rascal::lsp::DocumentSymbols");
semanticEvaluator = makeFutureEvaluator(exec, docService, workspaceService, client, "Rascal semantics", monitor, null, true, "lang::rascalcore::check::Summary", "lang::rascal::lsp::refactor::Rename");
semanticEvaluator = makeFutureEvaluator(exec, docService, workspaceService, client, "Rascal semantics", monitor, null, true, "lang::rascalcore::check::Summary", "lang::rascal::lsp::refactor::Rename", "lang::rascal::lsp::Actions");
compilerEvaluator = makeFutureEvaluator(exec, docService, workspaceService, client, "Rascal compiler", monitor, null, true, "lang::rascalcore::check::Checker");
actionStore = semanticEvaluator.thenApply(e -> ((ModuleEnvironment) e.getModule("lang::rascal::lsp::Actions")).getStore());
}

public InterruptibleFuture<@Nullable IConstructor> getSummary(ISourceLocation occ, PathConfig pcfg) {
Expand All @@ -116,6 +123,8 @@ public RascalLanguageServices(RascalTextDocumentService docService, BaseWorkspac
}
}



private static IConstructor addResources(PathConfig pcfg) {
var result = pcfg.asConstructor();
return result.asWithKeywordParameters()
Expand Down Expand Up @@ -250,6 +259,51 @@ public List<CodeLensSuggestion> locateCodeLenses(ITree tree) {
return result;
}

public CompletableFuture<IList> parseCodeActions(String command) {
return actionStore.thenApply(commandStore -> {
try {
var TF = TypeFactory.getInstance();
return (IList) new StandardTextReader().read(VF, commandStore, TF.listType(commandStore.lookupAbstractDataType("CodeAction")), new StringReader(command));
} catch (FactTypeUseException | IOException e) {
// this should never happen as long as the Rascal code
// for creating errors is type-correct. So it _might_ happen
// when running the interpreter on broken code.
throw new IllegalArgumentException("The command could not be parsed", e);
}
});
}

public InterruptibleFuture<IValue> executeCommand(String command) {
logger.debug("executeCommand({}...) (full command value in TRACE level)", () -> command.substring(0, Math.min(10, command.length())));
logger.trace("Full command: {}", command);
var defaultMap = VF.mapWriter();
defaultMap.put(VF.string("result"), VF.bool(false));

return InterruptibleFuture.flatten(parseCommand(command).thenApply(cons ->
EvaluatorUtil.<IValue>runEvaluator(
"executeCommand",
semanticEvaluator,
ev -> ev.call("evaluateRascalCommand", cons),
defaultMap.done(),
exec,
true,
client
)
), exec);
}

private CompletableFuture<IConstructor> parseCommand(String command) {
return actionStore.thenApply(commandStore -> {
try {
return (IConstructor) new StandardTextReader().read(VF, commandStore, commandStore.lookupAbstractDataType("Command"), new StringReader(command));
}
catch (FactTypeUseException | IOException e) {
logger.catching(e);
jurgenvinju marked this conversation as resolved.
Show resolved Hide resolved
throw new IllegalArgumentException("The command could not be parsed", e);
}
});
}

public static final class CodeLensSuggestion {
private final ISourceLocation line;
private final String commandName;
Expand All @@ -268,7 +322,6 @@ public List<Object> getArguments() {
return arguments;
}


public ISourceLocation getLine() {
return line;
}
Expand All @@ -281,6 +334,13 @@ public String getCommandName() {
public String getShortName() {
return shortName;
}
}

public InterruptibleFuture<IList> codeActions(IList focus, PathConfig pcfg) {
return runEvaluator("Rascal codeActions", semanticEvaluator, eval -> {
Map<String,IValue> kws = Map.of("pcfg", pcfg.asConstructor());
return (IList) eval.call("rascalCodeActions", "lang::rascal::lsp::Actions", kws, focus);
},
VF.list(), exec, false, client);
}
}
Loading
Loading