Skip to content

Commit

Permalink
Merge pull request #478 from usethesource/code-actions-for-rascal
Browse files Browse the repository at this point in the history
API and implementation of codefixes and code actions for Rascal itself
  • Loading branch information
jurgenvinju authored Oct 31, 2024
2 parents 86be386 + 4d98fc0 commit b555025
Show file tree
Hide file tree
Showing 14 changed files with 589 additions and 146 deletions.
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)) {
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,54 +506,30 @@ 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());

var range = Locations.toRascalRange(params.getTextDocument(), params.getRange(), columns);

// 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
CompletableFuture<Stream<IValue>> codeActions = recoverExceptions(
getFile(params.getTextDocument())
.getCurrentTreeAsync()
.thenApply(Versioned::get)
.thenCompose(tree -> computeCodeActions(contribs, startLine, startColumn, tree))
.thenCompose(tree -> computeCodeActions(contribs, range.getStart().getLine(), range.getStart().getCharacter(), tree))
.thenApply(IList::stream)
, () -> Stream.<IValue>empty())
;

// 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);
}

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 @@ -479,14 +479,13 @@ public void invalidate() {
return null;
}

var line = cursor.getLine() + 1;
var translatedOffset = columns.get(file).translateInverseColumn(line, cursor.getCharacter(), false);
var focus = TreeSearch.computeFocusList(tree.get(), line, translatedOffset);
var pos = Locations.toRascalPosition(file, cursor, columns);
var focus = TreeSearch.computeFocusList(tree.get(), pos.getLine(), pos.getCharacter());

InterruptibleFuture<ISet> set = null;

if (focus.isEmpty()) {
logger.trace("{}: could not find substree at line {} and offset {}", logName, line, translatedOffset);
logger.trace("{}: could not find substree at line {} and offset {}", logName, pos.getLine(), pos.getCharacter());
set = InterruptibleFuture.completedFuture(IRascalValueFactory.getInstance().set());
} else {
logger.trace("{}: looked up focus with length: {}, now calling dedicated function", logName, focus.length());
Expand Down
Loading

0 comments on commit b555025

Please sign in to comment.