From fac14205425e6ed5a5530698feb5bff0bc067d0c Mon Sep 17 00:00:00 2001 From: "Jurgen J. Vinju" Date: Wed, 16 Oct 2024 10:50:42 +0200 Subject: [PATCH 01/29] scaffolded an initial implementation of codefixes and code actioons for Rascal itself --- .../vscode/lsp/IBaseTextDocumentService.java | 1 + .../ParametricTextDocumentService.java | 84 +---------- .../lsp/rascal/RascalLanguageServices.java | 69 +++++++++- .../lsp/rascal/RascalTextDocumentService.java | 90 +++++++++++- .../vscode/lsp/util/CodeActions.java | 130 ++++++++++++++++++ .../main/rascal/lang/rascal/lsp/Actions.rsc | 43 ++++++ 6 files changed, 332 insertions(+), 85 deletions(-) create mode 100644 rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/util/CodeActions.java create mode 100644 rascal-lsp/src/main/rascal/lang/rascal/lsp/Actions.rsc diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/IBaseTextDocumentService.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/IBaseTextDocumentService.java index 288cf66d5..2615b4a0a 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/IBaseTextDocumentService.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/IBaseTextDocumentService.java @@ -44,4 +44,5 @@ public interface IBaseTextDocumentService extends TextDocumentService { void unregisterLanguage(LanguageParameter lang); CompletableFuture executeCommand(String languageName, String command); LineColumnOffsetMap getColumnMap(ISourceLocation file); + } 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 ba5f53e55..3017a7353 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 @@ -29,7 +29,6 @@ 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; @@ -50,7 +49,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; @@ -90,7 +88,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; @@ -108,11 +105,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.Outline; import org.rascalmpl.vscode.lsp.util.SemanticTokenizer; @@ -132,7 +128,6 @@ 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 { @@ -387,80 +382,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); + return new CodeLens(Locations.toRange(loc, columns), CodeActions.constructorToCommand(dedicatedLanguageName, 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; - } - - /** - * 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()); @@ -588,6 +513,7 @@ public CompletableFuture>> codeAction(CodeActio logger.debug("codeActions: {}", 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 @@ -629,7 +555,7 @@ public CompletableFuture>> codeAction(CodeActio return codeActions.thenCombine(quickfixes, (actions, quicks) -> Stream.concat(quicks, actions) .map(IConstructor.class::cast) - .map(cons -> constructorToCodeAction(contribs.getName(), cons)) + .map(cons -> CodeActions.constructorToCodeAction(this, dedicatedLanguageName, contribs.getName(), cons)) .map(cmd -> Either.forRight(cmd)) .collect(Collectors.toList()) ); @@ -637,7 +563,7 @@ public CompletableFuture>> codeAction(CodeActio private CompletableFuture computeCodeActions(final ILanguageContributions contribs, final int startLine, final int startColumn, ITree tree) { IList focus = TreeSearch.computeFocusList(tree, startLine, startColumn); - + if (!focus.isEmpty()) { return contribs.codeActions(focus).get(); } diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/RascalLanguageServices.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/RascalLanguageServices.java index 2ace1ba1d..f26f8dc5d 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/RascalLanguageServices.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/RascalLanguageServices.java @@ -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; @@ -37,6 +38,7 @@ import java.util.Map; import java.util.Set; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; import java.util.concurrent.Executor; import java.util.concurrent.ExecutorService; import java.util.function.Function; @@ -51,6 +53,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; @@ -59,10 +62,10 @@ 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; @@ -70,6 +73,8 @@ 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; @@ -82,6 +87,9 @@ public class RascalLanguageServices { private final CompletableFuture outlineEvaluator; private final CompletableFuture semanticEvaluator; private final CompletableFuture compilerEvaluator; + private final CompletableFuture actionEvaluator; + + private final CompletableFuture actionStore; private final TypeFactory tf = TypeFactory.getInstance(); private final TypeStore store = new TypeStore(); @@ -100,6 +108,8 @@ public RascalLanguageServices(RascalTextDocumentService docService, BaseWorkspac outlineEvaluator = makeFutureEvaluator(exec, docService, workspaceService, client, "Rascal outline", monitor, null, false, "lang::rascal::lsp::Outline"); semanticEvaluator = makeFutureEvaluator(exec, docService, workspaceService, client, "Rascal semantics", monitor, null, true, "lang::rascalcore::check::Summary", "lang::rascal::lsp::refactor::Rename"); compilerEvaluator = makeFutureEvaluator(exec, docService, workspaceService, client, "Rascal compiler", monitor, null, true, "lang::rascalcore::check::Checker"); + actionEvaluator = makeFutureEvaluator(exec, docService, workspaceService, client, "Rascal actions", monitor, null, true, "lang::rascal::lsp::Actions"); + actionStore = actionEvaluator.thenApply(e -> ((ModuleEnvironment) e.getModule("lang::rascal::lsp::Actions")).getStore()); } public InterruptibleFuture<@Nullable IConstructor> getSummary(ISourceLocation occ, PathConfig pcfg) { @@ -116,6 +126,8 @@ public RascalLanguageServices(RascalTextDocumentService docService, BaseWorkspac } } + + private static IConstructor addResources(PathConfig pcfg) { var result = pcfg.asConstructor(); return result.asWithKeywordParameters() @@ -250,6 +262,55 @@ public List locateCodeLenses(ITree tree) { return result; } + public CompletableFuture 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 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); + + // TODO: properly chain this + IConstructor cons; + try { + cons = parseCommand(command).get(); + + return EvaluatorUtil.runEvaluator( + "executeCommand", + actionEvaluator, + ev -> ev.call("evaluateRascalCommand", cons), + null, + exec, + true, + client + ); + } catch (InterruptedException | ExecutionException e) { + logger.catching(e); + return InterruptibleFuture.completedFuture(VF.bool(false)); + } + } + + private CompletableFuture 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); + throw new IllegalArgumentException("The command could not be parsed", e); + } + }); + } + public static final class CodeLensSuggestion { private final ISourceLocation line; private final String commandName; @@ -268,7 +329,6 @@ public List getArguments() { return arguments; } - public ISourceLocation getLine() { return line; } @@ -281,6 +341,11 @@ public String getCommandName() { public String getShortName() { return shortName; } + } + public InterruptibleFuture codeActions(IList focus) { + return runEvaluator("Rascal makeSummary", semanticEvaluator, eval -> { + return (IList) eval.call("rascalCodeActions", "lang::rascal::lsp::Actions", focus); + }, null, exec, false, client); } } 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 5e41f55ad..0795d5212 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 @@ -32,16 +32,22 @@ import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionException; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutorService; +import java.util.function.Supplier; import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.apache.logging.log4j.Level; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; +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; @@ -86,6 +92,7 @@ import org.rascalmpl.library.Prelude; import org.rascalmpl.parser.gtd.exception.ParseError; import org.rascalmpl.uri.URIResolverRegistry; +import org.rascalmpl.values.IRascalValueFactory; import org.rascalmpl.values.parsetrees.ITree; import org.rascalmpl.vscode.lsp.BaseWorkspaceService; import org.rascalmpl.vscode.lsp.IBaseLanguageClient; @@ -95,6 +102,7 @@ import org.rascalmpl.vscode.lsp.rascal.model.FileFacts; import org.rascalmpl.vscode.lsp.rascal.model.SummaryBridge; 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; @@ -104,7 +112,12 @@ import org.rascalmpl.vscode.lsp.util.locations.ColumnMaps; import org.rascalmpl.vscode.lsp.util.locations.LineColumnOffsetMap; 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.IConstructor; +import io.usethesource.vallang.IList; import io.usethesource.vallang.ISourceLocation; import io.usethesource.vallang.IValue; @@ -157,6 +170,7 @@ public void initializeServerCapabilities(ServerCapabilities result) { result.setCodeLensProvider(new CodeLensOptions(false)); result.setFoldingRangeProvider(true); result.setRenameProvider(true); + result.setCodeActionProvider(true); } @Override public void pair(BaseWorkspaceService workspaceService) { @@ -415,6 +429,69 @@ public CompletableFuture> codeLens(CodeLensParams param ; } + @Override + public CompletableFuture>> codeAction(CodeActionParams params) { + logger.debug("codeActions: {}", params); + + 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> + CompletableFuture> 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(rascalServices::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) + ; + + // here we dynamically ask the contributions for more actions, + // based on the cursor position in the file and the current parse tree + CompletableFuture> codeActions = recoverExceptions( + getFile(params.getTextDocument()) + .getCurrentTreeAsync() + .thenApply(Versioned::get) + .thenCompose((ITree tree) -> computeCodeActions(startLine, startColumn, tree)) + .thenApply(IList::stream) + , () -> Stream.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((IConstructor cons) -> (CodeAction) CodeActions.constructorToCodeAction(this, "", "Rascal", cons)) + .map(cmd -> Either.forRight(cmd)) + .collect(Collectors.toList()) + ); + } + + private CompletableFuture computeCodeActions(final int startLine, final int startColumn, ITree tree) { + IList focus = TreeSearch.computeFocusList(tree, startLine, startColumn); + + if (!focus.isEmpty()) { + return rascalServices.codeActions(focus).get(); + } + else { + logger.log(Level.DEBUG, "no tree focus found at {}:{}", startLine, startColumn); + return CompletableFuture.completedFuture(IRascalValueFactory.getInstance().list()); + } + } + private CodeLens makeRunCodeLens(CodeLensSuggestion detected) { return new CodeLens( Locations.toRange(detected.getLine(), columns), @@ -425,10 +502,15 @@ private CodeLens makeRunCodeLens(CodeLensSuggestion detected) { @Override public CompletableFuture executeCommand(String extension, String command) { - // there is currently no way the Rascal LSP can receive this, but the Rascal DSL LSP does. - logger.warn("ignoring execute command in Rascal LSP: {}, {}", extension, command); - return CompletableFuture.completedFuture(null); + // TODO @DavyLandman is this the right way to convert from interruptible to completable? + return rascalServices.executeCommand(command).get(); } - + private static CompletableFuture recoverExceptions(CompletableFuture future, Supplier defaultValue) { + return future + .exceptionally(e -> { + logger.error("Operation failed with", e); + return defaultValue.get(); + }); + } } diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/util/CodeActions.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/util/CodeActions.java new file mode 100644 index 000000000..ff82ca361 --- /dev/null +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/util/CodeActions.java @@ -0,0 +1,130 @@ +/* + * Copyright (c) 2018-2023, 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.util; + +import java.util.Arrays; + +import org.checkerframework.checker.nullness.qual.Nullable; +import org.eclipse.lsp4j.CodeAction; +import org.eclipse.lsp4j.CodeActionKind; +import org.eclipse.lsp4j.Command; +import org.eclipse.lsp4j.WorkspaceEdit; +import org.rascalmpl.values.IRascalValueFactory; +import org.rascalmpl.vscode.lsp.BaseWorkspaceService; +import org.rascalmpl.vscode.lsp.IBaseTextDocumentService; +import org.rascalmpl.vscode.lsp.parametric.model.RascalADTs; + +import io.usethesource.vallang.IConstructor; +import io.usethesource.vallang.IList; +import io.usethesource.vallang.IString; +import io.usethesource.vallang.IWithKeywordParameters; + +/** + * Reusable utilities for code actions and commands (maps between Rascal and LSP world) + */ +public class CodeActions { + public static CodeAction constructorToCodeAction(IBaseTextDocumentService doc, String dedicatedLanguageName, 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(dedicatedLanguageName, languageName, command)); + } + + if (edits != null) { + result.setEdit(new WorkspaceEdit(DocumentChanges.translateDocumentChanges(doc, edits))); + } + + result.setKind(constructorToCodeActionKind(kind)); + + return result; + } + + /** + * 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 static 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; + } + + public static Command constructorToCommand(String dedicatedLanguageName, 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(dedicatedLanguageName), Arrays.asList(languageName, command.toString())); + } + + public static String getRascalMetaCommandName(String dedicatedLanguageName) { + // if we run in dedicated mode, we prefix the commands with our language name + // to avoid ambiguity with other dedicated languages and the generic rascal plugin + if (!dedicatedLanguageName.isEmpty()) { + return BaseWorkspaceService.RASCAL_META_COMMAND + "-" + dedicatedLanguageName; + } + return BaseWorkspaceService.RASCAL_META_COMMAND; + } +} diff --git a/rascal-lsp/src/main/rascal/lang/rascal/lsp/Actions.rsc b/rascal-lsp/src/main/rascal/lang/rascal/lsp/Actions.rsc new file mode 100644 index 000000000..dd85852d1 --- /dev/null +++ b/rascal-lsp/src/main/rascal/lang/rascal/lsp/Actions.rsc @@ -0,0 +1,43 @@ +@license{ +Copyright (c) 2018-2023, 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. +} +@bootstrapParser +@synopsis{Defines both the command evaluator and the codeAction retriever for Rascal} +module lang::rascal::lsp::Actions + +import lang::rascal::\syntax::Rascal; +import util::LanguageServer; + +@synopsis{Detects (on-demand) source actions to register with specific places near the current cursor} +list[CodeAction] rascalCodeActions(Focus focus) { + return []; // TODO +} + +@synopsis{Evaluates all commands and quickfixes produces by ((rascalCodeActions)) and the type-checker} +value evaluateRascalCommand(Command _) { + return true; // TODO +} + From 05c9f9ab9ff3bfcf056a45c14084218bd06b5da0 Mon Sep 17 00:00:00 2001 From: "Jurgen J. Vinju" Date: Wed, 16 Oct 2024 13:56:44 +0200 Subject: [PATCH 02/29] added first example code action for Rascal --- .../lsp/rascal/RascalLanguageServices.java | 4 ++-- .../main/rascal/lang/rascal/lsp/Actions.rsc | 22 ++++++++++++++++++- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/RascalLanguageServices.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/RascalLanguageServices.java index f26f8dc5d..8bd21b947 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/RascalLanguageServices.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/RascalLanguageServices.java @@ -344,8 +344,8 @@ public String getShortName() { } public InterruptibleFuture codeActions(IList focus) { - return runEvaluator("Rascal makeSummary", semanticEvaluator, eval -> { - return (IList) eval.call("rascalCodeActions", "lang::rascal::lsp::Actions", focus); + return runEvaluator("Rascal codeActions", actionEvaluator, eval -> { + return (IList) eval.call("rascalCodeActions", focus); }, null, exec, false, client); } } diff --git a/rascal-lsp/src/main/rascal/lang/rascal/lsp/Actions.rsc b/rascal-lsp/src/main/rascal/lang/rascal/lsp/Actions.rsc index dd85852d1..1c87557de 100644 --- a/rascal-lsp/src/main/rascal/lang/rascal/lsp/Actions.rsc +++ b/rascal-lsp/src/main/rascal/lang/rascal/lsp/Actions.rsc @@ -30,10 +30,30 @@ module lang::rascal::lsp::Actions import lang::rascal::\syntax::Rascal; import util::LanguageServer; +import analysis::diff::edits::TextEdits; @synopsis{Detects (on-demand) source actions to register with specific places near the current cursor} list[CodeAction] rascalCodeActions(Focus focus) { - return []; // TODO + result = []; + + if ([*_, Toplevel t, *_] := focus) { + result += toplevelCodeActions(t); + } + + return result; +} + +@synopsis{Rewrite immediate return to expression.} +list[CodeAction] toplevelCodeActions(Toplevel t: + (Toplevel) ` { + ' return ; + '}`) { + + result = (Toplevel) ` = `; + + edits=[changed(t@\loc.top, [replace(t@\loc, "")])]; + + return actions(edits=edits, title="Rewrite block return to simpler rewrite rule."); } @synopsis{Evaluates all commands and quickfixes produces by ((rascalCodeActions)) and the type-checker} From f60cff50ebf6977d696dad0e3d3d11e4f4d30d40 Mon Sep 17 00:00:00 2001 From: "Jurgen J. Vinju" Date: Wed, 16 Oct 2024 13:58:30 +0200 Subject: [PATCH 03/29] added default --- rascal-lsp/src/main/rascal/lang/rascal/lsp/Actions.rsc | 2 ++ 1 file changed, 2 insertions(+) diff --git a/rascal-lsp/src/main/rascal/lang/rascal/lsp/Actions.rsc b/rascal-lsp/src/main/rascal/lang/rascal/lsp/Actions.rsc index 1c87557de..4ea8de8bd 100644 --- a/rascal-lsp/src/main/rascal/lang/rascal/lsp/Actions.rsc +++ b/rascal-lsp/src/main/rascal/lang/rascal/lsp/Actions.rsc @@ -56,6 +56,8 @@ list[CodeAction] toplevelCodeActions(Toplevel t: return actions(edits=edits, title="Rewrite block return to simpler rewrite rule."); } +default list[CodeAction] toplevelCodeActions(Toplevel _) = []; + @synopsis{Evaluates all commands and quickfixes produces by ((rascalCodeActions)) and the type-checker} value evaluateRascalCommand(Command _) { return true; // TODO From 62f627ff6312aa44baa607aba881b565dedfdbb9 Mon Sep 17 00:00:00 2001 From: "Jurgen J. Vinju" Date: Wed, 16 Oct 2024 16:30:31 +0200 Subject: [PATCH 04/29] final fixes --- .../lsp/rascal/RascalLanguageServices.java | 8 ++-- .../lsp/rascal/RascalTextDocumentService.java | 10 +++-- .../main/rascal/lang/rascal/lsp/Actions.rsc | 37 +++++++++++++++---- 3 files changed, 41 insertions(+), 14 deletions(-) diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/RascalLanguageServices.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/RascalLanguageServices.java index 8bd21b947..b32d4c6c2 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/RascalLanguageServices.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/RascalLanguageServices.java @@ -343,9 +343,11 @@ public String getShortName() { } } - public InterruptibleFuture codeActions(IList focus) { + public InterruptibleFuture codeActions(IList focus, PathConfig pcfg) { return runEvaluator("Rascal codeActions", actionEvaluator, eval -> { - return (IList) eval.call("rascalCodeActions", focus); - }, null, exec, false, client); + Map kws = Map.of("pcfg", pcfg.asConstructor()); + return (IList) eval.call("rascalCodeActions", "lang::rascal::lsp::Actions", kws, focus); + }, + null, exec, false, client); } } 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 0795d5212..4fdbb6b48 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 @@ -61,6 +61,7 @@ import org.eclipse.lsp4j.DidSaveTextDocumentParams; import org.eclipse.lsp4j.DocumentSymbol; import org.eclipse.lsp4j.DocumentSymbolParams; +import org.eclipse.lsp4j.ExecuteCommandOptions; import org.eclipse.lsp4j.FoldingRange; import org.eclipse.lsp4j.FoldingRangeRequestParams; import org.eclipse.lsp4j.Hover; @@ -90,6 +91,7 @@ import org.eclipse.lsp4j.services.LanguageClient; import org.eclipse.lsp4j.services.LanguageClientAware; import org.rascalmpl.library.Prelude; +import org.rascalmpl.library.util.PathConfig; import org.rascalmpl.parser.gtd.exception.ParseError; import org.rascalmpl.uri.URIResolverRegistry; import org.rascalmpl.values.IRascalValueFactory; @@ -171,7 +173,9 @@ public void initializeServerCapabilities(ServerCapabilities result) { result.setFoldingRangeProvider(true); result.setRenameProvider(true); result.setCodeActionProvider(true); + result.setExecuteCommandProvider(new ExecuteCommandOptions(Collections.singletonList(BaseWorkspaceService.RASCAL_META_COMMAND))); } + @Override public void pair(BaseWorkspaceService workspaceService) { this.workspaceService = workspaceService; @@ -465,7 +469,7 @@ public CompletableFuture>> codeAction(CodeActio getFile(params.getTextDocument()) .getCurrentTreeAsync() .thenApply(Versioned::get) - .thenCompose((ITree tree) -> computeCodeActions(startLine, startColumn, tree)) + .thenCompose((ITree tree) -> computeCodeActions(startLine, startColumn, tree, facts.getPathConfig(loc))) .thenApply(IList::stream) , () -> Stream.empty()) ; @@ -480,11 +484,11 @@ public CompletableFuture>> codeAction(CodeActio ); } - private CompletableFuture computeCodeActions(final int startLine, final int startColumn, ITree tree) { + private CompletableFuture computeCodeActions(final int startLine, final int startColumn, ITree tree, PathConfig pcfg) { IList focus = TreeSearch.computeFocusList(tree, startLine, startColumn); if (!focus.isEmpty()) { - return rascalServices.codeActions(focus).get(); + return rascalServices.codeActions(focus, pcfg).get(); } else { logger.log(Level.DEBUG, "no tree focus found at {}:{}", startLine, startColumn); diff --git a/rascal-lsp/src/main/rascal/lang/rascal/lsp/Actions.rsc b/rascal-lsp/src/main/rascal/lang/rascal/lsp/Actions.rsc index 4ea8de8bd..00ce81073 100644 --- a/rascal-lsp/src/main/rascal/lang/rascal/lsp/Actions.rsc +++ b/rascal-lsp/src/main/rascal/lang/rascal/lsp/Actions.rsc @@ -31,35 +31,56 @@ module lang::rascal::lsp::Actions import lang::rascal::\syntax::Rascal; import util::LanguageServer; import analysis::diff::edits::TextEdits; +import ParseTree; +import String; +import lang::rascal::vis::ImportGraph; +import util::Reflective; + +@synopsis{Here we list Rascal-specific code commands} +@description{ +The commands must be evaluated by ((evaluateRascalCommand)) +} +data Command + = visualImportGraphCommand(PathConfig pcfg) + ; @synopsis{Detects (on-demand) source actions to register with specific places near the current cursor} -list[CodeAction] rascalCodeActions(Focus focus) { +list[CodeAction] rascalCodeActions(Focus focus, PathConfig pcfg=pathConfig()) { result = []; if ([*_, Toplevel t, *_] := focus) { result += toplevelCodeActions(t); } + if ([*_, Header h, *_] := focus) { + result += [action(command=visualImportGraphCommand(pcfg), title="Visualize project import graph")]; + } + return result; } @synopsis{Rewrite immediate return to expression.} list[CodeAction] toplevelCodeActions(Toplevel t: - (Toplevel) ` { + (Toplevel) ` + ' { ' return ; '}`) { - result = (Toplevel) ` = `; + result = (Toplevel) ` + ' = ;`; - edits=[changed(t@\loc.top, [replace(t@\loc, "")])]; + edits=[changed(t@\loc.top, [replace(t@\loc, trim(""))])]; - return actions(edits=edits, title="Rewrite block return to simpler rewrite rule."); + return [action(edits=edits, title="Rewrite block return to simpler rewrite rule.", kind=refactor())]; } default list[CodeAction] toplevelCodeActions(Toplevel _) = []; -@synopsis{Evaluates all commands and quickfixes produces by ((rascalCodeActions)) and the type-checker} -value evaluateRascalCommand(Command _) { - return true; // TODO +@synopsis{Evaluates all commands and quickfixes produced by ((rascalCodeActions)) and the type-checker} +default value evaluateRascalCommand(Command _) = ("result" : false); + +value evaluateRascalCommand(visualImportGraphCommand(PathConfig pcfg)) { + importGraph(pcfg); + return ("result" : true); } From ea0991d57e2ea1e6bb423631b34b5eb5673fa985 Mon Sep 17 00:00:00 2001 From: "Jurgen J. Vinju" Date: Wed, 16 Oct 2024 17:03:28 +0200 Subject: [PATCH 05/29] added sort imports action --- .../main/rascal/lang/rascal/lsp/Actions.rsc | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/rascal-lsp/src/main/rascal/lang/rascal/lsp/Actions.rsc b/rascal-lsp/src/main/rascal/lang/rascal/lsp/Actions.rsc index 00ce81073..44d798d8f 100644 --- a/rascal-lsp/src/main/rascal/lang/rascal/lsp/Actions.rsc +++ b/rascal-lsp/src/main/rascal/lang/rascal/lsp/Actions.rsc @@ -35,6 +35,8 @@ import ParseTree; import String; import lang::rascal::vis::ImportGraph; import util::Reflective; +import util::IDEServices; +import List; @synopsis{Here we list Rascal-specific code commands} @description{ @@ -42,6 +44,7 @@ The commands must be evaluated by ((evaluateRascalCommand)) } data Command = visualImportGraphCommand(PathConfig pcfg) + | sortImportsAndExtends(Header h) ; @synopsis{Detects (on-demand) source actions to register with specific places near the current cursor} @@ -53,7 +56,9 @@ list[CodeAction] rascalCodeActions(Focus focus, PathConfig pcfg=pathConfig()) { } if ([*_, Header h, *_] := focus) { - result += [action(command=visualImportGraphCommand(pcfg), title="Visualize project import graph")]; + result += [action(command=visualImportGraphCommand(pcfg), title="Visualize project import graph")] + + [action(command=sortImportsAndExtends(h), title="Sort imports and extends")] + ; } return result; @@ -84,3 +89,18 @@ value evaluateRascalCommand(visualImportGraphCommand(PathConfig pcfg)) { return ("result" : true); } +value evaluateRascalCommand(sortImportsAndExtends(Header h)) { + extends = [trim("") | i <- h.imports, i is \extend]; + imports = [trim("") | i <- h.imports, i is \default]; + grammar = [trim("") | i <- h.imports, i is \syntax]; + + newHeader = " + '<}> + ' + '<}> + ' + '<}>"; + + applyDocumentsEdits([changed(h@\loc.top, [replace(h.imports@\loc, newHeader)])]); + return ("result":true); +} From 46f76ea41b4e41e21a0c52027015f4e8bf4941c4 Mon Sep 17 00:00:00 2001 From: "Jurgen J. Vinju" Date: Wed, 16 Oct 2024 17:12:08 +0200 Subject: [PATCH 06/29] fixed broken indentation --- .../vscode/lsp/rascal/RascalLanguageServices.java | 10 ++++++---- rascal-lsp/src/main/rascal/lang/rascal/lsp/Actions.rsc | 4 ++-- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/RascalLanguageServices.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/RascalLanguageServices.java index b32d4c6c2..5fcda4965 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/RascalLanguageServices.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/RascalLanguageServices.java @@ -294,9 +294,10 @@ public InterruptibleFuture executeCommand(String command) { true, client ); - } catch (InterruptedException | ExecutionException e) { - logger.catching(e); - return InterruptibleFuture.completedFuture(VF.bool(false)); + } + catch (InterruptedException | ExecutionException e) { + logger.catching(e); + return InterruptibleFuture.completedFuture(VF.bool(false)); } } @@ -304,7 +305,8 @@ private CompletableFuture 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) { + } + catch (FactTypeUseException | IOException e) { logger.catching(e); throw new IllegalArgumentException("The command could not be parsed", e); } diff --git a/rascal-lsp/src/main/rascal/lang/rascal/lsp/Actions.rsc b/rascal-lsp/src/main/rascal/lang/rascal/lsp/Actions.rsc index 44d798d8f..63ebfb804 100644 --- a/rascal-lsp/src/main/rascal/lang/rascal/lsp/Actions.rsc +++ b/rascal-lsp/src/main/rascal/lang/rascal/lsp/Actions.rsc @@ -56,8 +56,8 @@ list[CodeAction] rascalCodeActions(Focus focus, PathConfig pcfg=pathConfig()) { } if ([*_, Header h, *_] := focus) { - result += [action(command=visualImportGraphCommand(pcfg), title="Visualize project import graph")] - + [action(command=sortImportsAndExtends(h), title="Sort imports and extends")] + result += [action(command=visualImportGraphCommand(pcfg), title="Visualize project import graph", kind=source())] + + [action(command=sortImportsAndExtends(h), title="Sort imports and extends", kind=source())] ; } From 55f684599564edacccfe8049aba75d8e787123b6 Mon Sep 17 00:00:00 2001 From: "Jurgen J. Vinju" Date: Wed, 16 Oct 2024 17:30:41 +0200 Subject: [PATCH 07/29] improved asyncronous command handling --- .../lsp/rascal/RascalLanguageServices.java | 17 ++++------------- .../lsp/rascal/RascalTextDocumentService.java | 1 + 2 files changed, 5 insertions(+), 13 deletions(-) diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/RascalLanguageServices.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/RascalLanguageServices.java index 5fcda4965..e0c70259b 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/RascalLanguageServices.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/RascalLanguageServices.java @@ -38,7 +38,6 @@ import java.util.Map; import java.util.Set; import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ExecutionException; import java.util.concurrent.Executor; import java.util.concurrent.ExecutorService; import java.util.function.Function; @@ -280,12 +279,8 @@ public InterruptibleFuture 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); - // TODO: properly chain this - IConstructor cons; - try { - cons = parseCommand(command).get(); - - return EvaluatorUtil.runEvaluator( + return InterruptibleFuture.flatten(parseCommand(command).thenApply(cons -> + EvaluatorUtil.runEvaluator( "executeCommand", actionEvaluator, ev -> ev.call("evaluateRascalCommand", cons), @@ -293,12 +288,8 @@ public InterruptibleFuture executeCommand(String command) { exec, true, client - ); - } - catch (InterruptedException | ExecutionException e) { - logger.catching(e); - return InterruptibleFuture.completedFuture(VF.bool(false)); - } + ) + ), exec); } private CompletableFuture parseCommand(String command) { 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 4fdbb6b48..bbee176bf 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 @@ -37,6 +37,7 @@ import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionException; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.function.Supplier; import java.util.stream.Collectors; From f08530eeecbdde3e52b6ca93eca4c24d2201df28 Mon Sep 17 00:00:00 2001 From: "Jurgen J. Vinju" Date: Thu, 17 Oct 2024 13:53:51 +0200 Subject: [PATCH 08/29] removed superfluous newlines in sorted imports and extends --- .../src/main/rascal/lang/rascal/lsp/Actions.rsc | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/rascal-lsp/src/main/rascal/lang/rascal/lsp/Actions.rsc b/rascal-lsp/src/main/rascal/lang/rascal/lsp/Actions.rsc index 63ebfb804..81d38f4dd 100644 --- a/rascal-lsp/src/main/rascal/lang/rascal/lsp/Actions.rsc +++ b/rascal-lsp/src/main/rascal/lang/rascal/lsp/Actions.rsc @@ -56,8 +56,8 @@ list[CodeAction] rascalCodeActions(Focus focus, PathConfig pcfg=pathConfig()) { } if ([*_, Header h, *_] := focus) { - result += [action(command=visualImportGraphCommand(pcfg), title="Visualize project import graph", kind=source())] - + [action(command=sortImportsAndExtends(h), title="Sort imports and extends", kind=source())] + result += [action(command=visualImportGraphCommand(pcfg), title="Visualize project import graph")] + + [action(command=sortImportsAndExtends(h), title="Sort imports and extends")] ; } @@ -95,11 +95,11 @@ value evaluateRascalCommand(sortImportsAndExtends(Header h)) { grammar = [trim("") | i <- h.imports, i is \syntax]; newHeader = " - '<}> - ' - '<}> - ' - '<}>"; + '<}>"[..-1] + + " + '<}>"[..-1] + + " + '<}>"[..-1]; applyDocumentsEdits([changed(h@\loc.top, [replace(h.imports@\loc, newHeader)])]); return ("result":true); From 0d0dcacaf5e6d3e0d0343c174bc47dfb3e276aea Mon Sep 17 00:00:00 2001 From: "Jurgen J. Vinju" Date: Thu, 17 Oct 2024 14:01:08 +0200 Subject: [PATCH 09/29] better grouping for imports and extends --- rascal-lsp/src/main/rascal/lang/rascal/lsp/Actions.rsc | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/rascal-lsp/src/main/rascal/lang/rascal/lsp/Actions.rsc b/rascal-lsp/src/main/rascal/lang/rascal/lsp/Actions.rsc index 81d38f4dd..88ba26852 100644 --- a/rascal-lsp/src/main/rascal/lang/rascal/lsp/Actions.rsc +++ b/rascal-lsp/src/main/rascal/lang/rascal/lsp/Actions.rsc @@ -96,9 +96,13 @@ value evaluateRascalCommand(sortImportsAndExtends(Header h)) { newHeader = " '<}>"[..-1] + - " + " + ' + '<}> '<}>"[..-1] + - " + " + ' + '<}> '<}>"[..-1]; applyDocumentsEdits([changed(h@\loc.top, [replace(h.imports@\loc, newHeader)])]); From 6be8c162ed1a5b34c3ecf0b7fca634a0d7799277 Mon Sep 17 00:00:00 2001 From: "Jurgen J. Vinju" Date: Thu, 17 Oct 2024 14:02:58 +0200 Subject: [PATCH 10/29] syntax better spaced --- rascal-lsp/src/main/rascal/lang/rascal/lsp/Actions.rsc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rascal-lsp/src/main/rascal/lang/rascal/lsp/Actions.rsc b/rascal-lsp/src/main/rascal/lang/rascal/lsp/Actions.rsc index 88ba26852..8ae0e7fe7 100644 --- a/rascal-lsp/src/main/rascal/lang/rascal/lsp/Actions.rsc +++ b/rascal-lsp/src/main/rascal/lang/rascal/lsp/Actions.rsc @@ -100,7 +100,7 @@ value evaluateRascalCommand(sortImportsAndExtends(Header h)) { ' '<}> '<}>"[..-1] + - " + " ' '<}> '<}>"[..-1]; From 0ab364f39394f3a8aaf340d75e64bb8cd3433cc9 Mon Sep 17 00:00:00 2001 From: "Jurgen J. Vinju" Date: Wed, 23 Oct 2024 09:52:23 +0200 Subject: [PATCH 11/29] added \'add missing license\' action for Rascal --- .../lsp/rascal/RascalTextDocumentService.java | 1 - .../main/rascal/lang/rascal/lsp/Actions.rsc | 42 +++++++++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) 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 bbee176bf..4fdbb6b48 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 @@ -37,7 +37,6 @@ import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionException; import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.function.Supplier; import java.util.stream.Collectors; diff --git a/rascal-lsp/src/main/rascal/lang/rascal/lsp/Actions.rsc b/rascal-lsp/src/main/rascal/lang/rascal/lsp/Actions.rsc index 8ae0e7fe7..2fc92e753 100644 --- a/rascal-lsp/src/main/rascal/lang/rascal/lsp/Actions.rsc +++ b/rascal-lsp/src/main/rascal/lang/rascal/lsp/Actions.rsc @@ -37,6 +37,7 @@ import lang::rascal::vis::ImportGraph; import util::Reflective; import util::IDEServices; import List; +import IO; @synopsis{Here we list Rascal-specific code commands} @description{ @@ -51,6 +52,10 @@ data Command list[CodeAction] rascalCodeActions(Focus focus, PathConfig pcfg=pathConfig()) { result = []; + if ([*_, start[Module] top] := focus) { + result += addLicenseAction(top, pcfg); + } + if ([*_, Toplevel t, *_] := focus) { result += toplevelCodeActions(t); } @@ -64,6 +69,43 @@ list[CodeAction] rascalCodeActions(Focus focus, PathConfig pcfg=pathConfig()) { return result; } +@synopsis{Add a license header if there isn't one.} +list[CodeAction] addLicenseAction(start[Module] \module, PathConfig pcfg) { + Tags tags = \module.top.header.tags; + + if ((Tags) ` @license ` !:= tags) { + license = findLicense(pcfg); + if (license != "") { + license = "@license{ + ' + '}\n"; + return [action(edits=[makeLicenseEdit(\module@\loc, license)], title="Add missing license header")]; + } + } + + return []; +} + +str findLicense(PathConfig pcfg) { + for (loc src <- pcfg.srcs) { + while (!exists(src + "pom.xml") && src.path != "" && src.path != "/") { + src = src.parent; + } + + if (exists(src + "LICENSE")) { + return trim(readFile(src + "LICENSE")); + } + else if (exists(src + "LICENSE.md")) { + return trim(readFile(src + "LICENSE.md")); + } + } + + return ""; +} + +DocumentEdit makeLicenseEdit(loc \module, str license) + = changed(\module.top, [replace(\module.top(0, 0), license)]); + @synopsis{Rewrite immediate return to expression.} list[CodeAction] toplevelCodeActions(Toplevel t: (Toplevel) ` From 4a83365e9c0068d6e4186428cad1404f8d763219 Mon Sep 17 00:00:00 2001 From: "Jurgen J. Vinju" Date: Wed, 23 Oct 2024 10:05:35 +0200 Subject: [PATCH 12/29] grammar rules have 1 line distance --- rascal-lsp/src/main/rascal/lang/rascal/lsp/Actions.rsc | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/rascal-lsp/src/main/rascal/lang/rascal/lsp/Actions.rsc b/rascal-lsp/src/main/rascal/lang/rascal/lsp/Actions.rsc index 2fc92e753..d55db88b9 100644 --- a/rascal-lsp/src/main/rascal/lang/rascal/lsp/Actions.rsc +++ b/rascal-lsp/src/main/rascal/lang/rascal/lsp/Actions.rsc @@ -145,7 +145,8 @@ value evaluateRascalCommand(sortImportsAndExtends(Header h)) { " ' '<}> - '<}>"[..-1]; + ' + '<}>"[..-2]; applyDocumentsEdits([changed(h@\loc.top, [replace(h.imports@\loc, newHeader)])]); return ("result":true); From 0eea6fc48420f58312f63b1dbd000c20707869b9 Mon Sep 17 00:00:00 2001 From: "Jurgen J. Vinju" Date: Wed, 23 Oct 2024 10:52:58 +0200 Subject: [PATCH 13/29] added UI test for Rascal code actions; factored out common assertLineBecomes functionn from dsl tests --- .../main/rascal/lang/rascal/lsp/Actions.rsc | 4 ++-- .../src/test/vscode-suite/dsl.test.ts | 11 ++--------- .../src/test/vscode-suite/ide.test.ts | 18 ++++++++++++++++++ .../src/test/vscode-suite/utils.ts | 7 ++++++- 4 files changed, 28 insertions(+), 12 deletions(-) diff --git a/rascal-lsp/src/main/rascal/lang/rascal/lsp/Actions.rsc b/rascal-lsp/src/main/rascal/lang/rascal/lsp/Actions.rsc index d55db88b9..8e22ef4fd 100644 --- a/rascal-lsp/src/main/rascal/lang/rascal/lsp/Actions.rsc +++ b/rascal-lsp/src/main/rascal/lang/rascal/lsp/Actions.rsc @@ -86,7 +86,7 @@ list[CodeAction] addLicenseAction(start[Module] \module, PathConfig pcfg) { return []; } -str findLicense(PathConfig pcfg) { +private str findLicense(PathConfig pcfg) { for (loc src <- pcfg.srcs) { while (!exists(src + "pom.xml") && src.path != "" && src.path != "/") { src = src.parent; @@ -103,7 +103,7 @@ str findLicense(PathConfig pcfg) { return ""; } -DocumentEdit makeLicenseEdit(loc \module, str license) +private DocumentEdit makeLicenseEdit(loc \module, str license) = changed(\module.top, [replace(\module.top(0, 0), license)]); @synopsis{Rewrite immediate return to expression.} 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 287331323..0a273edf4 100644 --- a/rascal-vscode-extension/src/test/vscode-suite/dsl.test.ts +++ b/rascal-vscode-extension/src/test/vscode-suite/dsl.test.ts @@ -141,18 +141,11 @@ describe('DSL', function () { await driver.wait(async ()=> (await editor.getCoordinates())[0] === 3, Delays.slow, "Cursor should have moved to line 3"); }); - function assertLineBecomes(editor: TextEditor, lineNumber: number, lineContents: string, msg: string, wait = Delays.verySlow) : Promise { - return driver.wait(async () => { - const currentContent = (await editor.getTextAtLine(lineNumber)).trim(); - return currentContent === lineContents; - }, wait, msg, 100); - } - it("code lens works", async () => { const editor = await ide.openModule(TestWorkspace.picoFile); const lens = await driver.wait(() => editor.getCodeLens("Rename variables a to b."), Delays.verySlow, "Rename lens should be available"); await lens!.click(); - await assertLineBecomes(editor, 9, "b := 2;", "a variable should be changed to b"); + await ide.assertLineBecomes(editor, 9, "b := 2;", "a variable should be changed to b"); }); it("quick fix works", async() => { @@ -171,7 +164,7 @@ describe('DSL', function () { // menu container works a bit strangely, it ask the focus to keep track of it, // and manages clicks and menus on the highest level (not per item). await menuContainer.sendKeys(Key.RETURN); - await assertLineBecomes(editor, 9, "a := 2;", "a variable should be changed back to a", Delays.extremelySlow); + await ide.assertLineBecomes(editor, 9, "a := 2;", "a variable should be changed back to a", Delays.extremelySlow); }); }); diff --git a/rascal-vscode-extension/src/test/vscode-suite/ide.test.ts b/rascal-vscode-extension/src/test/vscode-suite/ide.test.ts index 08ceef2e9..3a7227c51 100644 --- a/rascal-vscode-extension/src/test/vscode-suite/ide.test.ts +++ b/rascal-vscode-extension/src/test/vscode-suite/ide.test.ts @@ -187,5 +187,23 @@ describe('IDE', function () { expect(editorText).to.contain("i - 1"); expect(editorText).to.contain("i -2"); }); + + it("code actions work", async() { + const editor = await ide.openModule(TestWorkspace.libCallFile); + await editor.moveCursor(3,1); // in the list of imports + + // trigger the code actions + const inputarea = await editor.findElement(By.className('inputarea')); + await inputarea.sendKeys(Key.chord(TextEditor.ctlKey, ".")); + + // finds an open menu with the right item in it (Change to a) and then select + // the parent that handles UI events like click() and sendKeys() + const menuContainer = await ide.hasElement(editor, By.xpath("//div[contains(@class, 'focused') and contains(@class, 'action')]/span[contains(text(), 'Sort imports and extends')]//ancestor::*[contains(@class, 'monaco-list')]"), Delays.normal, "Sort imports and extends should be available and focused"); + + // menu container works a bit strangely, it ask the focus to keep track of it, + // and manages clicks and menus on the highest level (not per item). + await menuContainer.sendKeys(Key.RETURN); + await ide.assertLineBecomes(editor, 3, "import Lib;", "import Lib should have switched with import IO", Delays.extremelySlow); + }) }); diff --git a/rascal-vscode-extension/src/test/vscode-suite/utils.ts b/rascal-vscode-extension/src/test/vscode-suite/utils.ts index 680991d24..ba9a7e1ec 100644 --- a/rascal-vscode-extension/src/test/vscode-suite/utils.ts +++ b/rascal-vscode-extension/src/test/vscode-suite/utils.ts @@ -214,7 +214,12 @@ export class IDEOperations { await ignoreFails(center?.close()); } - + assertLineBecomes(editor: TextEditor, lineNumber: number, lineContents: string, msg: string, wait = Delays.verySlow) : Promise { + return this.driver.wait(async () => { + const currentContent = (await editor.getTextAtLine(lineNumber)).trim(); + return currentContent === lineContents; + }, wait, msg, 100); + } hasElement(editor: TextEditor, selector: Locator, timeout: number, message: string): Promise { return this.driver.wait(scopedElementLocated(editor, selector), timeout, message, 50); From d799dc2a71936bdd35c537cac78c3bdf487ccd6a Mon Sep 17 00:00:00 2001 From: "Jurgen J. Vinju" Date: Wed, 23 Oct 2024 10:55:19 +0200 Subject: [PATCH 14/29] changed order of imports for testing purposes --- .../test-workspace/test-project/src/main/rascal/LibCall.rsc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rascal-vscode-extension/test-workspace/test-project/src/main/rascal/LibCall.rsc b/rascal-vscode-extension/test-workspace/test-project/src/main/rascal/LibCall.rsc index 03b61c70f..3384f82a4 100644 --- a/rascal-vscode-extension/test-workspace/test-project/src/main/rascal/LibCall.rsc +++ b/rascal-vscode-extension/test-workspace/test-project/src/main/rascal/LibCall.rsc @@ -1,7 +1,7 @@ module LibCall -import IO; import Lib; +import IO; int main() { println(fib(4)); From 1e074bcddae45199439334b1e85b480e3ddf1d55 Mon Sep 17 00:00:00 2001 From: "Jurgen J. Vinju" Date: Wed, 23 Oct 2024 10:56:59 +0200 Subject: [PATCH 15/29] use arrow keys to skip the other actions --- rascal-vscode-extension/src/test/vscode-suite/ide.test.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/rascal-vscode-extension/src/test/vscode-suite/ide.test.ts b/rascal-vscode-extension/src/test/vscode-suite/ide.test.ts index 3a7227c51..79663e2bb 100644 --- a/rascal-vscode-extension/src/test/vscode-suite/ide.test.ts +++ b/rascal-vscode-extension/src/test/vscode-suite/ide.test.ts @@ -202,6 +202,8 @@ describe('IDE', function () { // menu container works a bit strangely, it ask the focus to keep track of it, // and manages clicks and menus on the highest level (not per item). + await menuContainer.sendKeys(Key.DOWN); // skip the other two actions + await menuContainer.sendKeys(Key.DOWN); await menuContainer.sendKeys(Key.RETURN); await ide.assertLineBecomes(editor, 3, "import Lib;", "import Lib should have switched with import IO", Delays.extremelySlow); }) From 17953b92fd90c198fecca273bd4adcd48bb64f78 Mon Sep 17 00:00:00 2001 From: "Jurgen J. Vinju" Date: Wed, 23 Oct 2024 11:30:15 +0200 Subject: [PATCH 16/29] fixed typo --- rascal-vscode-extension/src/test/vscode-suite/ide.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rascal-vscode-extension/src/test/vscode-suite/ide.test.ts b/rascal-vscode-extension/src/test/vscode-suite/ide.test.ts index 79663e2bb..5304b27e6 100644 --- a/rascal-vscode-extension/src/test/vscode-suite/ide.test.ts +++ b/rascal-vscode-extension/src/test/vscode-suite/ide.test.ts @@ -188,7 +188,7 @@ describe('IDE', function () { expect(editorText).to.contain("i -2"); }); - it("code actions work", async() { + it("code actions work", async() => { const editor = await ide.openModule(TestWorkspace.libCallFile); await editor.moveCursor(3,1); // in the list of imports From 214dd8892019ba97c4013b322535c2d04f7b316a Mon Sep 17 00:00:00 2001 From: "Jurgen J. Vinju" Date: Wed, 23 Oct 2024 19:47:47 +0200 Subject: [PATCH 17/29] reintroduced method lost in merge --- .../vscode/lsp/rascal/RascalTextDocumentService.java | 8 ++++++++ 1 file changed, 8 insertions(+) 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 b5b217735..f52a35c75 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 @@ -507,4 +507,12 @@ private CodeLens makeRunCodeLens(CodeLensSuggestion detected) { public CompletableFuture executeCommand(String extension, String command) { return rascalServices.executeCommand(command).get(); } + + private static CompletableFuture recoverExceptions(CompletableFuture future, Supplier defaultValue) { + return future + .exceptionally(e -> { + logger.error("Operation failed with", e); + return defaultValue.get(); + }); + } } From ed75af496f663dbeccbfe82de03ed0b2779b4756 Mon Sep 17 00:00:00 2001 From: "Jurgen J. Vinju" Date: Wed, 23 Oct 2024 20:04:43 +0200 Subject: [PATCH 18/29] changed test because the action is not currently focused, it is the third option in the menu --- rascal-vscode-extension/src/test/vscode-suite/ide.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rascal-vscode-extension/src/test/vscode-suite/ide.test.ts b/rascal-vscode-extension/src/test/vscode-suite/ide.test.ts index 5304b27e6..46f8bf109 100644 --- a/rascal-vscode-extension/src/test/vscode-suite/ide.test.ts +++ b/rascal-vscode-extension/src/test/vscode-suite/ide.test.ts @@ -198,7 +198,7 @@ describe('IDE', function () { // finds an open menu with the right item in it (Change to a) and then select // the parent that handles UI events like click() and sendKeys() - const menuContainer = await ide.hasElement(editor, By.xpath("//div[contains(@class, 'focused') and contains(@class, 'action')]/span[contains(text(), 'Sort imports and extends')]//ancestor::*[contains(@class, 'monaco-list')]"), Delays.normal, "Sort imports and extends should be available and focused"); + const menuContainer = await ide.hasElement(editor, By.xpath("//div[contains(@class, 'action')]/span[contains(text(), 'Sort imports and extends')]//ancestor::*[contains(@class, 'monaco-list')]"), Delays.normal, "Sort imports and extends should be available"); // menu container works a bit strangely, it ask the focus to keep track of it, // and manages clicks and menus on the highest level (not per item). From 31f88d86966bb67a7cdc9d506ee06ca3696f02f8 Mon Sep 17 00:00:00 2001 From: "Jurgen J. Vinju" Date: Thu, 24 Oct 2024 12:07:17 +0200 Subject: [PATCH 19/29] bringing mo to the mountain, just test the first action instead of the third --- rascal-vscode-extension/src/test/vscode-suite/ide.test.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/rascal-vscode-extension/src/test/vscode-suite/ide.test.ts b/rascal-vscode-extension/src/test/vscode-suite/ide.test.ts index 46f8bf109..24a74cb79 100644 --- a/rascal-vscode-extension/src/test/vscode-suite/ide.test.ts +++ b/rascal-vscode-extension/src/test/vscode-suite/ide.test.ts @@ -198,14 +198,12 @@ describe('IDE', function () { // finds an open menu with the right item in it (Change to a) and then select // the parent that handles UI events like click() and sendKeys() - const menuContainer = await ide.hasElement(editor, By.xpath("//div[contains(@class, 'action')]/span[contains(text(), 'Sort imports and extends')]//ancestor::*[contains(@class, 'monaco-list')]"), Delays.normal, "Sort imports and extends should be available"); + const menuContainer = await ide.hasElement(editor, By.xpath("//div[contains(@class, 'focused') and contains(@class, 'action')]/span[contains(text(), 'Add missing license header')]//ancestor::*[contains(@class, 'monaco-list')]"), Delays.normal, "Add-license action should be available and focused"); // menu container works a bit strangely, it ask the focus to keep track of it, // and manages clicks and menus on the highest level (not per item). - await menuContainer.sendKeys(Key.DOWN); // skip the other two actions - await menuContainer.sendKeys(Key.DOWN); await menuContainer.sendKeys(Key.RETURN); - await ide.assertLineBecomes(editor, 3, "import Lib;", "import Lib should have switched with import IO", Delays.extremelySlow); + await ide.assertLineBecomes(editor, 1, "@license{", "license header should have been added", Delays.extremelySlow); }) }); From e85373798a88b99bfc3b72992c2cbd626518e864 Mon Sep 17 00:00:00 2001 From: "Jurgen J. Vinju" Date: Thu, 24 Oct 2024 12:17:50 +0200 Subject: [PATCH 20/29] added LICENSE to test project for testing purposes of the code action that adds a license --- .../main/rascal/lang/rascal/lsp/Actions.rsc | 3 +++ .../test-workspace/test-project/LICENSE | 24 +++++++++++++++++++ 2 files changed, 27 insertions(+) create mode 100644 rascal-vscode-extension/test-workspace/test-project/LICENSE diff --git a/rascal-lsp/src/main/rascal/lang/rascal/lsp/Actions.rsc b/rascal-lsp/src/main/rascal/lang/rascal/lsp/Actions.rsc index 8e22ef4fd..d4eb5a4ee 100644 --- a/rascal-lsp/src/main/rascal/lang/rascal/lsp/Actions.rsc +++ b/rascal-lsp/src/main/rascal/lang/rascal/lsp/Actions.rsc @@ -98,6 +98,9 @@ private str findLicense(PathConfig pcfg) { else if (exists(src + "LICENSE.md")) { return trim(readFile(src + "LICENSE.md")); } + else if (exists(src + "LICENSE.txt")) { + return trim(readFile(src + "LICENSE.txt")); + } } return ""; diff --git a/rascal-vscode-extension/test-workspace/test-project/LICENSE b/rascal-vscode-extension/test-workspace/test-project/LICENSE new file mode 100644 index 000000000..eb1ffea66 --- /dev/null +++ b/rascal-vscode-extension/test-workspace/test-project/LICENSE @@ -0,0 +1,24 @@ +Copyright (c) 2018-2021, 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. From eec1297441a9532389d6f729c435215029101a51 Mon Sep 17 00:00:00 2001 From: "Jurgen J. Vinju" Date: Thu, 24 Oct 2024 12:29:06 +0200 Subject: [PATCH 21/29] added LICENSE to original files --- rascal-vscode-extension/src/test/vscode-suite/utils.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/rascal-vscode-extension/src/test/vscode-suite/utils.ts b/rascal-vscode-extension/src/test/vscode-suite/utils.ts index 05d699917..2422739a7 100644 --- a/rascal-vscode-extension/src/test/vscode-suite/utils.ts +++ b/rascal-vscode-extension/src/test/vscode-suite/utils.ts @@ -56,6 +56,7 @@ export class TestWorkspace { public static readonly mainFile = path.join(src(this.testProject), 'Main.rsc'); public static readonly mainFileTpl = path.join(target(this.testProject),'$Main.tpl'); public static readonly libCallFile = path.join(src(this.testProject), 'LibCall.rsc'); + public static readonly licenseFile = path.join(this.testProject, 'LICENSE'); public static readonly libCallFileTpl = path.join(target(this.testProject),'$LibCall.tpl'); public static readonly libFile = path.join(src(this.libProject), 'Lib.rsc'); public static readonly libFileTpl = path.join(target(this.libProject),'$Lib.tpl'); From e021fa5dd28c0ed19989778b1f303f737c1234e9 Mon Sep 17 00:00:00 2001 From: "Jurgen J. Vinju" Date: Thu, 24 Oct 2024 13:58:52 +0200 Subject: [PATCH 22/29] make sure to revert changes made by code actions before we continue to the next test --- .../src/test/vscode-suite/dsl.test.ts | 23 +++++++++------ .../src/test/vscode-suite/ide.test.ts | 29 +++++++++++-------- 2 files changed, 31 insertions(+), 21 deletions(-) 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 e04c6a813..bed1a5725 100644 --- a/rascal-vscode-extension/src/test/vscode-suite/dsl.test.ts +++ b/rascal-vscode-extension/src/test/vscode-suite/dsl.test.ts @@ -166,17 +166,22 @@ describe('DSL', function () { await editor.moveCursor(9,3); // it's where the undeclared variable `az` is await ide.hasErrorSquiggly(editor, Delays.verySlow); // just make sure there is indeed something to fix - const inputarea = await editor.findElement(By.className('inputarea')); - await inputarea.sendKeys(Key.chord(TextEditor.ctlKey, ".")); + try { + const inputarea = await editor.findElement(By.className('inputarea')); + await inputarea.sendKeys(Key.chord(TextEditor.ctlKey, ".")); - // finds an open menu with the right item in it (Change to a) and then select - // the parent that handles UI events like click() and sendKeys() - const menuContainer = await ide.hasElement(editor, By.xpath("//div[contains(@class, 'focused') and contains(@class, 'action')]/span[contains(text(), 'Change to a')]//ancestor::*[contains(@class, 'monaco-list')]"), Delays.normal, "The Change to a option should be available and focussed by default"); + // finds an open menu with the right item in it (Change to a) and then select + // the parent that handles UI events like click() and sendKeys() + const menuContainer = await ide.hasElement(editor, By.xpath("//div[contains(@class, 'focused') and contains(@class, 'action')]/span[contains(text(), 'Change to a')]//ancestor::*[contains(@class, 'monaco-list')]"), Delays.normal, "The Change to a option should be available and focussed by default"); - // menu container works a bit strangely, it ask the focus to keep track of it, - // and manages clicks and menus on the highest level (not per item). - await menuContainer.sendKeys(Key.RETURN); - await ide.assertLineBecomes(editor, 9, "a := 2;", "a variable should be changed back to a", Delays.extremelySlow); + // menu container works a bit strangely, it ask the focus to keep track of it, + // and manages clicks and menus on the highest level (not per item). + await menuContainer.sendKeys(Key.RETURN); + await ide.assertLineBecomes(editor, 9, "a := 2;", "a variable should be changed back to a", Delays.extremelySlow); + } + finally { + await ide.revertOpenChanges(); + } }); }); diff --git a/rascal-vscode-extension/src/test/vscode-suite/ide.test.ts b/rascal-vscode-extension/src/test/vscode-suite/ide.test.ts index 24a74cb79..e73dc3ffb 100644 --- a/rascal-vscode-extension/src/test/vscode-suite/ide.test.ts +++ b/rascal-vscode-extension/src/test/vscode-suite/ide.test.ts @@ -192,18 +192,23 @@ describe('IDE', function () { const editor = await ide.openModule(TestWorkspace.libCallFile); await editor.moveCursor(3,1); // in the list of imports - // trigger the code actions - const inputarea = await editor.findElement(By.className('inputarea')); - await inputarea.sendKeys(Key.chord(TextEditor.ctlKey, ".")); - - // finds an open menu with the right item in it (Change to a) and then select - // the parent that handles UI events like click() and sendKeys() - const menuContainer = await ide.hasElement(editor, By.xpath("//div[contains(@class, 'focused') and contains(@class, 'action')]/span[contains(text(), 'Add missing license header')]//ancestor::*[contains(@class, 'monaco-list')]"), Delays.normal, "Add-license action should be available and focused"); - - // menu container works a bit strangely, it ask the focus to keep track of it, - // and manages clicks and menus on the highest level (not per item). - await menuContainer.sendKeys(Key.RETURN); - await ide.assertLineBecomes(editor, 1, "@license{", "license header should have been added", Delays.extremelySlow); + try { + // trigger the code actions + const inputarea = await editor.findElement(By.className('inputarea')); + await inputarea.sendKeys(Key.chord(TextEditor.ctlKey, ".")); + + // finds an open menu with the right item in it (Change to a) and then select + // the parent that handles UI events like click() and sendKeys() + const menuContainer = await ide.hasElement(editor, By.xpath("//div[contains(@class, 'focused') and contains(@class, 'action')]/span[contains(text(), 'Add missing license header')]//ancestor::*[contains(@class, 'monaco-list')]"), Delays.normal, "Add-license action should be available and focused"); + + // menu container works a bit strangely, it ask the focus to keep track of it, + // and manages clicks and menus on the highest level (not per item). + await menuContainer.sendKeys(Key.RETURN); + await ide.assertLineBecomes(editor, 1, "@license{", "license header should have been added", Delays.extremelySlow); + } + finally { + await ide.revertOpenChanges(); + } }) }); From 78250cd9cbc55facb99278f3f5717009a149397a Mon Sep 17 00:00:00 2001 From: "Jurgen J. Vinju" Date: Thu, 24 Oct 2024 14:22:46 +0200 Subject: [PATCH 23/29] factoring common functionality between Rascal LSP and Parametric DSL with respect to code actions --- .../ParametricTextDocumentService.java | 24 +-------- .../lsp/rascal/RascalTextDocumentService.java | 27 +--------- .../vscode/lsp/util/CodeActions.java | 51 ++++++++++++++++++- 3 files changed, 54 insertions(+), 48 deletions(-) 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 090533689..17295c979 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 @@ -519,25 +519,11 @@ public CompletableFuture>> codeAction(CodeActio 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> - CompletableFuture> 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 @@ -551,13 +537,7 @@ public CompletableFuture>> 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 -> CodeActions.constructorToCodeAction(this, dedicatedLanguageName, contribs.getName(), cons)) - .map(cmd -> Either.forRight(cmd)) - .collect(Collectors.toList()) - ); + return CodeActions.mergeAndConvertCodeActions(this, dedicatedLanguageName, contribs.getName(), quickfixes, codeActions); } private CompletableFuture computeCodeActions(final ILanguageContributions contribs, final int startLine, final int startColumn, ITree tree) { 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 f52a35c75..477b7c1e1 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 @@ -32,7 +32,6 @@ import java.util.Collections; import java.util.List; import java.util.Map; -import java.util.Objects; import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionException; @@ -116,9 +115,6 @@ 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.IConstructor; import io.usethesource.vallang.IList; import io.usethesource.vallang.ISourceLocation; import io.usethesource.vallang.IValue; @@ -442,25 +438,12 @@ public CompletableFuture>> codeAction(CodeActio 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> CompletableFuture> 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(rascalServices::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) - ; + = CodeActions.extractActionsFromDiagnostics(params, rascalServices::parseCodeActions); // here we dynamically ask the contributions for more actions, // based on the cursor position in the file and the current parse tree @@ -474,13 +457,7 @@ public CompletableFuture>> 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((IConstructor cons) -> (CodeAction) CodeActions.constructorToCodeAction(this, "", "Rascal", cons)) - .map(cmd -> Either.forRight(cmd)) - .collect(Collectors.toList()) - ); + return CodeActions.mergeAndConvertCodeActions(this, "", "Rascal", quickfixes, codeActions); } private CompletableFuture computeCodeActions(final int startLine, final int startColumn, ITree tree, PathConfig pcfg) { diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/util/CodeActions.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/util/CodeActions.java index ff82ca361..35b673263 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/util/CodeActions.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/util/CodeActions.java @@ -27,27 +27,76 @@ package org.rascalmpl.vscode.lsp.util; import java.util.Arrays; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.CompletableFuture; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; 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.Command; +import org.eclipse.lsp4j.Diagnostic; import org.eclipse.lsp4j.WorkspaceEdit; +import org.eclipse.lsp4j.jsonrpc.messages.Either; import org.rascalmpl.values.IRascalValueFactory; import org.rascalmpl.vscode.lsp.BaseWorkspaceService; import org.rascalmpl.vscode.lsp.IBaseTextDocumentService; import org.rascalmpl.vscode.lsp.parametric.model.RascalADTs; +import com.google.gson.JsonPrimitive; + import io.usethesource.vallang.IConstructor; import io.usethesource.vallang.IList; import io.usethesource.vallang.IString; +import io.usethesource.vallang.IValue; import io.usethesource.vallang.IWithKeywordParameters; /** * Reusable utilities for code actions and commands (maps between Rascal and LSP world) */ public class CodeActions { - public static CodeAction constructorToCodeAction(IBaseTextDocumentService doc, String dedicatedLanguageName, String languageName, IConstructor codeAction) { + /** + * Makes 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. + * + * @param params diagnostics directly from the client (holding embedded action terms) + * @param actionParser provides the parser with a scope that imports the right definitions of Command terms. + * @return a future stream of parsed and type-checked Rascal CodeAction terms. + */ + public static CompletableFuture> extractActionsFromDiagnostics(CodeActionParams params, Function> actionParser) { + final var emptyListFuture = CompletableFuture.completedFuture(IRascalValueFactory.getInstance().list()); + + return 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(actionParser) + // 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) + ; + } + + /* merges two streams of CodeAction terms and then converts them to LSP objects */ + public static CompletableFuture>> mergeAndConvertCodeActions(IBaseTextDocumentService doc, String dedicatedLanguageName, String languageName, CompletableFuture> quickfixes, CompletableFuture> codeActions) { + return codeActions.thenCombine(quickfixes, (actions, quicks) -> + Stream.concat(quicks, actions) + .map(IConstructor.class::cast) + .map(cons -> constructorToCodeAction(doc, dedicatedLanguageName, languageName, cons)) + .map(cmd -> Either.forRight(cmd)) + .collect(Collectors.toList()) + ); + } + + private static CodeAction constructorToCodeAction(IBaseTextDocumentService doc, String dedicatedLanguageName, String languageName, IConstructor codeAction) { IWithKeywordParameters kw = codeAction.asWithKeywordParameters(); IConstructor command = (IConstructor) kw.getParameter(RascalADTs.CodeActionFields.COMMAND); IString title = (IString) kw.getParameter(RascalADTs.CodeActionFields.TITLE); From f8b0702f02db696badc27af37ad66390166c2721 Mon Sep 17 00:00:00 2001 From: "Jurgen J. Vinju" Date: Thu, 24 Oct 2024 16:09:12 +0200 Subject: [PATCH 24/29] acted on suggestions in review by @davylandman --- .../ParametricTextDocumentService.java | 5 +---- .../lsp/rascal/RascalLanguageServices.java | 16 ++++++++-------- .../lsp/rascal/RascalTextDocumentService.java | 2 +- 3 files changed, 10 insertions(+), 13 deletions(-) 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 17295c979..a2ac230f9 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 @@ -32,7 +32,6 @@ 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; @@ -119,8 +118,6 @@ 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; @@ -509,7 +506,7 @@ public CompletableFuture semanticTokensRange(SemanticTokensRange @Override public CompletableFuture>> codeAction(CodeActionParams params) { - logger.debug("codeActions: {}", params); + logger.debug("codeAction: {}", params); final ILanguageContributions contribs = contributions(params.getTextDocument()); diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/RascalLanguageServices.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/RascalLanguageServices.java index 1f60bbd66..2d44bdf0f 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/RascalLanguageServices.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/RascalLanguageServices.java @@ -86,7 +86,6 @@ public class RascalLanguageServices { private final CompletableFuture documentSymbolEvaluator; private final CompletableFuture semanticEvaluator; private final CompletableFuture compilerEvaluator; - private final CompletableFuture actionEvaluator; private final CompletableFuture actionStore; @@ -105,10 +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"); - actionEvaluator = makeFutureEvaluator(exec, docService, workspaceService, client, "Rascal actions", monitor, null, true, "lang::rascal::lsp::Actions"); - actionStore = actionEvaluator.thenApply(e -> ((ModuleEnvironment) e.getModule("lang::rascal::lsp::Actions")).getStore()); + actionStore = semanticEvaluator.thenApply(e -> ((ModuleEnvironment) e.getModule("lang::rascal::lsp::Actions")).getStore()); } public InterruptibleFuture<@Nullable IConstructor> getSummary(ISourceLocation occ, PathConfig pcfg) { @@ -278,13 +276,15 @@ public CompletableFuture parseCodeActions(String command) { public InterruptibleFuture 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.runEvaluator( "executeCommand", - actionEvaluator, + semanticEvaluator, ev -> ev.call("evaluateRascalCommand", cons), - null, + defaultMap.done(), exec, true, client @@ -337,10 +337,10 @@ public String getShortName() { } public InterruptibleFuture codeActions(IList focus, PathConfig pcfg) { - return runEvaluator("Rascal codeActions", actionEvaluator, eval -> { + return runEvaluator("Rascal codeActions", semanticEvaluator, eval -> { Map kws = Map.of("pcfg", pcfg.asConstructor()); return (IList) eval.call("rascalCodeActions", "lang::rascal::lsp::Actions", kws, focus); }, - null, exec, false, client); + VF.list(), exec, false, client); } } 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 477b7c1e1..ed166b57b 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 @@ -430,7 +430,7 @@ public CompletableFuture> codeLens(CodeLensParams param @Override public CompletableFuture>> codeAction(CodeActionParams params) { - logger.debug("codeActions: {}", params); + logger.debug("codeAction: {}", params); final var loc = Locations.toLoc(params.getTextDocument()); final var start = params.getRange().getStart(); From c07ba2e0cd41896ffed41cee21ced8806798fe31 Mon Sep 17 00:00:00 2001 From: "Jurgen J. Vinju" Date: Thu, 24 Oct 2024 16:21:50 +0200 Subject: [PATCH 25/29] fixed bug with command going to the wrong LSP server --- .../rascalmpl/vscode/lsp/BaseWorkspaceService.java | 1 + .../vscode/lsp/rascal/RascalTextDocumentService.java | 2 +- .../org/rascalmpl/vscode/lsp/util/CodeActions.java | 11 ++++++++--- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/BaseWorkspaceService.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/BaseWorkspaceService.java index b83c27430..3ba5c3943 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/BaseWorkspaceService.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/BaseWorkspaceService.java @@ -47,6 +47,7 @@ public class BaseWorkspaceService implements WorkspaceService, LanguageClientAware { 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 workspaceFolders = new CopyOnWriteArrayList<>(); 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 ed166b57b..f94bb8ff3 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 @@ -169,7 +169,7 @@ public void initializeServerCapabilities(ServerCapabilities result) { result.setFoldingRangeProvider(true); result.setRenameProvider(true); result.setCodeActionProvider(true); - result.setExecuteCommandProvider(new ExecuteCommandOptions(Collections.singletonList(BaseWorkspaceService.RASCAL_META_COMMAND))); + result.setExecuteCommandProvider(new ExecuteCommandOptions(Collections.singletonList(BaseWorkspaceService.RASCAL_COMMAND))); } @Override diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/util/CodeActions.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/util/CodeActions.java index 35b673263..4c06bce89 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/util/CodeActions.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/util/CodeActions.java @@ -165,15 +165,20 @@ public static Command constructorToCommand(String dedicatedLanguageName, String IWithKeywordParameters kw = command.asWithKeywordParameters(); IString possibleTitle = (IString) kw.getParameter(RascalADTs.CommandFields.TITLE); - return new Command(possibleTitle != null ? possibleTitle.getValue() : command.toString(), getRascalMetaCommandName(dedicatedLanguageName), Arrays.asList(languageName, command.toString())); + return new Command(possibleTitle != null ? possibleTitle.getValue() : command.toString(), getRascalMetaCommandName(languageName, dedicatedLanguageName), Arrays.asList(languageName, command.toString())); } - public static String getRascalMetaCommandName(String dedicatedLanguageName) { + public static String getRascalMetaCommandName(String language, String dedicatedLanguageName) { // if we run in dedicated mode, we prefix the commands with our language name // to avoid ambiguity with other dedicated languages and the generic rascal plugin if (!dedicatedLanguageName.isEmpty()) { return BaseWorkspaceService.RASCAL_META_COMMAND + "-" + dedicatedLanguageName; } - return BaseWorkspaceService.RASCAL_META_COMMAND; + else if ("Rascal".equals(language)) { + return BaseWorkspaceService.RASCAL_COMMAND; + } + else { + return BaseWorkspaceService.RASCAL_META_COMMAND; + } } } From 58997ec52f8fdf53fdd5ef480cf1608fdbb48658 Mon Sep 17 00:00:00 2001 From: "Jurgen J. Vinju" Date: Thu, 24 Oct 2024 16:30:09 +0200 Subject: [PATCH 26/29] adding license action only appears on module name now --- rascal-lsp/src/main/rascal/lang/rascal/lsp/Actions.rsc | 2 +- rascal-vscode-extension/src/test/vscode-suite/ide.test.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/rascal-lsp/src/main/rascal/lang/rascal/lsp/Actions.rsc b/rascal-lsp/src/main/rascal/lang/rascal/lsp/Actions.rsc index d4eb5a4ee..0c07d6d8f 100644 --- a/rascal-lsp/src/main/rascal/lang/rascal/lsp/Actions.rsc +++ b/rascal-lsp/src/main/rascal/lang/rascal/lsp/Actions.rsc @@ -52,7 +52,7 @@ data Command list[CodeAction] rascalCodeActions(Focus focus, PathConfig pcfg=pathConfig()) { result = []; - if ([*_, start[Module] top] := focus) { + if ([*_, QualifiedName _, *_, Header _, *_, start[Module] top] := focus) { result += addLicenseAction(top, pcfg); } diff --git a/rascal-vscode-extension/src/test/vscode-suite/ide.test.ts b/rascal-vscode-extension/src/test/vscode-suite/ide.test.ts index e73dc3ffb..936ca45c6 100644 --- a/rascal-vscode-extension/src/test/vscode-suite/ide.test.ts +++ b/rascal-vscode-extension/src/test/vscode-suite/ide.test.ts @@ -190,7 +190,7 @@ describe('IDE', function () { it("code actions work", async() => { const editor = await ide.openModule(TestWorkspace.libCallFile); - await editor.moveCursor(3,1); // in the list of imports + await editor.moveCursor(1,8); // in the module name try { // trigger the code actions From 0644f009d6728f88ae55b9f670dc0539f9a76c59 Mon Sep 17 00:00:00 2001 From: "Jurgen J. Vinju" Date: Thu, 24 Oct 2024 16:39:07 +0200 Subject: [PATCH 27/29] factoring constants --- .../java/org/rascalmpl/vscode/lsp/BaseWorkspaceService.java | 1 + .../rascalmpl/vscode/lsp/rascal/RascalTextDocumentService.java | 2 +- .../main/java/org/rascalmpl/vscode/lsp/util/CodeActions.java | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/BaseWorkspaceService.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/BaseWorkspaceService.java index 3ba5c3943..30fbe10bb 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/BaseWorkspaceService.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/BaseWorkspaceService.java @@ -46,6 +46,7 @@ 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"; 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 f94bb8ff3..8a33294fa 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 @@ -457,7 +457,7 @@ public CompletableFuture>> codeAction(CodeActio ; // final merging the two streams of commmands, and their conversion to LSP Command data-type - return CodeActions.mergeAndConvertCodeActions(this, "", "Rascal", quickfixes, codeActions); + return CodeActions.mergeAndConvertCodeActions(this, "", BaseWorkspaceService.RASCAL_LANGUAGE, quickfixes, codeActions); } private CompletableFuture computeCodeActions(final int startLine, final int startColumn, ITree tree, PathConfig pcfg) { diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/util/CodeActions.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/util/CodeActions.java index 4c06bce89..ef1a7e14d 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/util/CodeActions.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/util/CodeActions.java @@ -174,7 +174,7 @@ public static String getRascalMetaCommandName(String language, String dedicatedL if (!dedicatedLanguageName.isEmpty()) { return BaseWorkspaceService.RASCAL_META_COMMAND + "-" + dedicatedLanguageName; } - else if ("Rascal".equals(language)) { + else if (BaseWorkspaceService.RASCAL_LANGUAGE.equals(language)) { return BaseWorkspaceService.RASCAL_COMMAND; } else { From bbba2cbbcc4cd0b685b4262284972aa87feca3e6 Mon Sep 17 00:00:00 2001 From: "Jurgen J. Vinju" Date: Thu, 24 Oct 2024 16:56:54 +0200 Subject: [PATCH 28/29] fixed commands for Rascal itself --- .../java/org/rascalmpl/vscode/lsp/BaseWorkspaceService.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/BaseWorkspaceService.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/BaseWorkspaceService.java index 30fbe10bb..f9b9e3fa9 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/BaseWorkspaceService.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/BaseWorkspaceService.java @@ -110,11 +110,12 @@ public void didChangeWorkspaceFolders(DidChangeWorkspaceFoldersParams params) { @Override public CompletableFuture 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."); } From d5362acd3e4fa09e7552fb6c02505d31c08ece92 Mon Sep 17 00:00:00 2001 From: "Jurgen J. Vinju" Date: Thu, 24 Oct 2024 17:14:55 +0200 Subject: [PATCH 29/29] fixed indentation --- .../vscode/lsp/util/CodeActions.java | 37 +++++++++---------- 1 file changed, 18 insertions(+), 19 deletions(-) diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/util/CodeActions.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/util/CodeActions.java index ef1a7e14d..b0dd9a9a5 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/util/CodeActions.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/util/CodeActions.java @@ -71,29 +71,28 @@ public static CompletableFuture> extractActionsFromDiagnostics(Co final var emptyListFuture = CompletableFuture.completedFuture(IRascalValueFactory.getInstance().list()); return 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(actionParser) - // 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) - ; + .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(actionParser) + // 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); } /* merges two streams of CodeAction terms and then converts them to LSP objects */ public static CompletableFuture>> mergeAndConvertCodeActions(IBaseTextDocumentService doc, String dedicatedLanguageName, String languageName, CompletableFuture> quickfixes, CompletableFuture> codeActions) { - return codeActions.thenCombine(quickfixes, (actions, quicks) -> - Stream.concat(quicks, actions) - .map(IConstructor.class::cast) - .map(cons -> constructorToCodeAction(doc, dedicatedLanguageName, languageName, cons)) - .map(cmd -> Either.forRight(cmd)) - .collect(Collectors.toList()) - ); + return codeActions.thenCombine(quickfixes, (actions, quicks) -> + Stream.concat(quicks, actions) + .map(IConstructor.class::cast) + .map(cons -> constructorToCodeAction(doc, dedicatedLanguageName, languageName, cons)) + .map(cmd -> Either.forRight(cmd)) + .collect(Collectors.toList()) + ); } private static CodeAction constructorToCodeAction(IBaseTextDocumentService doc, String dedicatedLanguageName, String languageName, IConstructor codeAction) {