diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ILanguageContributions.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ILanguageContributions.java index 14df49361..daaaf0a46 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ILanguageContributions.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ILanguageContributions.java @@ -29,9 +29,11 @@ import java.util.concurrent.CompletableFuture; import java.util.function.BiFunction; import java.util.function.Function; +import org.apache.commons.lang3.tuple.Pair; import org.rascalmpl.values.IRascalValueFactory; import org.rascalmpl.values.parsetrees.ITree; import org.rascalmpl.vscode.lsp.util.concurrent.InterruptibleFuture; + import io.usethesource.vallang.IConstructor; import io.usethesource.vallang.IList; import io.usethesource.vallang.ISet; @@ -58,6 +60,8 @@ public interface ILanguageContributions { public InterruptibleFuture implementation(IList focus); public InterruptibleFuture codeAction(IList focus); public InterruptibleFuture selectionRange(IList focus); + public InterruptibleFuture> prepareCallHierarchy(IList focus); + public InterruptibleFuture> incomingOutgoingCalls(Function hierarchyItem, Function direction); public InterruptibleFuture prepareRename(IList focus); public InterruptibleFuture rename(IList focus, String name); @@ -79,6 +83,7 @@ public interface ILanguageContributions { public CompletableFuture hasCodeAction(); public CompletableFuture hasDidRenameFiles(); public CompletableFuture hasSelectionRange(); + public CompletableFuture hasCallHierarchy(); public CompletableFuture specialCaseHighlighting(); diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/InterpretedLanguageContributions.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/InterpretedLanguageContributions.java index e7c90aac1..e70c26348 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/InterpretedLanguageContributions.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/InterpretedLanguageContributions.java @@ -31,10 +31,12 @@ import java.util.Objects; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutorService; +import java.util.function.Function; +import org.apache.commons.lang3.tuple.Pair; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; import org.rascalmpl.interpreter.Evaluator; import org.rascalmpl.interpreter.env.ModuleEnvironment; import org.rascalmpl.library.util.PathConfig; @@ -53,6 +55,7 @@ import org.rascalmpl.vscode.lsp.util.EvaluatorUtil; import org.rascalmpl.vscode.lsp.util.EvaluatorUtil.LSPContext; import org.rascalmpl.vscode.lsp.util.concurrent.InterruptibleFuture; + import io.usethesource.vallang.IBool; import io.usethesource.vallang.IConstructor; import io.usethesource.vallang.IList; @@ -94,6 +97,8 @@ public class InterpretedLanguageContributions implements ILanguageContributions private final CompletableFuture<@Nullable IFunction> rename; private final CompletableFuture<@Nullable IFunction> didRenameFiles; private final CompletableFuture<@Nullable IFunction> selectionRange; + private final CompletableFuture<@Nullable IFunction> prepareCallHierarchy; + private final CompletableFuture<@Nullable IFunction> callHierarchyService; private final CompletableFuture hasAnalysis; private final CompletableFuture hasBuild; @@ -109,6 +114,7 @@ public class InterpretedLanguageContributions implements ILanguageContributions private final CompletableFuture hasRename; private final CompletableFuture hasDidRenameFiles; private final CompletableFuture hasSelectionRange; + private final CompletableFuture hasCallHierarchy; private final CompletableFuture specialCaseHighlighting; @@ -155,6 +161,8 @@ public InterpretedLanguageContributions(LanguageParameter lang, IBaseTextDocumen this.rename = getFunctionFor(contributions, LanguageContributions.RENAME); this.didRenameFiles = getFunctionFor(contributions, LanguageContributions.DID_RENAME_FILES); this.selectionRange = getFunctionFor(contributions, LanguageContributions.SELECTION_RANGE); + this.prepareCallHierarchy = getFunctionFor(contributions, LanguageContributions.CALL_HIERARCHY, 0); + this.callHierarchyService = getFunctionFor(contributions, LanguageContributions.CALL_HIERARCHY, 1); // assign boolean properties once instead of wasting futures all the time this.hasAnalysis = nonNull(this.analysis); @@ -171,6 +179,7 @@ public InterpretedLanguageContributions(LanguageParameter lang, IBaseTextDocumen this.hasRename = nonNull(this.rename); this.hasDidRenameFiles = nonNull(this.didRenameFiles); this.hasSelectionRange = nonNull(this.selectionRange); + this.hasCallHierarchy = nonNull(this.prepareCallHierarchy); this.specialCaseHighlighting = getContributionParameter(contributions, LanguageContributions.PARSING, @@ -301,7 +310,11 @@ private static CompletableFuture requireFunction(CompletableFuture getFunctionFor(CompletableFuture contributions, String cons) { - return getContribution(contributions, cons).thenApply(contribution -> contribution != null ? (IFunction) contribution.get(0) : null); + return getFunctionFor(contributions, cons, 0); + } + + private static CompletableFuture<@Nullable IFunction> getFunctionFor(CompletableFuture contributions, String cons, int argumentPos) { + return getContribution(contributions, cons).thenApply(contribution -> contribution != null ? (IFunction) contribution.get(argumentPos) : null); } private static CompletableFuture<@Nullable IFunction> getKeywordParamFunctionFor(CompletableFuture contributions, String cons, String kwParam) { @@ -405,6 +418,26 @@ public InterruptibleFuture selectionRange(IList focus) { return execFunction(LanguageContributions.SELECTION_RANGE, selectionRange, VF.list(), focus); } + @Override + public InterruptibleFuture> prepareCallHierarchy(IList focus) { + debug(LanguageContributions.CALL_HIERARCHY, "prepare", focus.length()); + return withStore(execFunction(LanguageContributions.CALL_HIERARCHY, prepareCallHierarchy, VF.list(), focus)); + } + + @Override + public InterruptibleFuture> incomingOutgoingCalls(Function computeHierarchyItem, Function computeDirection) { + return withStore(InterruptibleFuture.flatten(store.thenApply(store -> { + var hierarchyItem = computeHierarchyItem.apply(store); + var direction = computeDirection.apply(store); + debug(LanguageContributions.CALL_HIERARCHY, hierarchyItem.has("name") ? hierarchyItem.get("name") : "?", direction.getName()); + return execFunction(LanguageContributions.CALL_HIERARCHY, callHierarchyService, VF.list(), hierarchyItem, direction); + }), exec)); + } + + private InterruptibleFuture> withStore(InterruptibleFuture contrib) { + return contrib.thenCombineAsync(store, Pair::of, exec); + } + private void debug(String name, Object param) { logger.debug("{}({})", name, param); } @@ -473,6 +506,11 @@ public CompletableFuture hasSelectionRange() { return hasSelectionRange; } + @Override + public CompletableFuture hasCallHierarchy() { + return hasCallHierarchy; + } + @Override public CompletableFuture hasAnalysis() { return hasAnalysis; diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/LanguageContributionsMultiplexer.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/LanguageContributionsMultiplexer.java index 011fc854e..e38b502a4 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/LanguageContributionsMultiplexer.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/LanguageContributionsMultiplexer.java @@ -31,16 +31,19 @@ import java.util.concurrent.ExecutorService; import java.util.function.BinaryOperator; import java.util.function.Function; +import org.apache.commons.lang3.tuple.Pair; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.checkerframework.checker.nullness.qual.Nullable; import org.rascalmpl.values.parsetrees.ITree; import org.rascalmpl.vscode.lsp.util.concurrent.InterruptibleFuture; + import io.usethesource.vallang.IConstructor; import io.usethesource.vallang.IList; import io.usethesource.vallang.ISet; import io.usethesource.vallang.ISourceLocation; import io.usethesource.vallang.ITuple; import io.usethesource.vallang.IValue; +import io.usethesource.vallang.type.TypeStore; @SuppressWarnings("java:S3077") // Fields in this class are read/written sequentially public class LanguageContributionsMultiplexer implements ILanguageContributions { @@ -69,6 +72,8 @@ private static final CompletableFuture failedInitialization() { private volatile CompletableFuture rename = failedInitialization(); private volatile CompletableFuture didRenameFiles = failedInitialization(); private volatile CompletableFuture selectionRange = failedInitialization(); + private volatile CompletableFuture prepareCallHierarchy = failedInitialization(); + private volatile CompletableFuture incomingOutgoingCalls = failedInitialization(); private volatile CompletableFuture hasAnalysis = failedInitialization(); private volatile CompletableFuture hasBuild = failedInitialization(); @@ -84,6 +89,7 @@ private static final CompletableFuture failedInitialization() { private volatile CompletableFuture hasRename = failedInitialization(); private volatile CompletableFuture hasDidRenameFiles = failedInitialization(); private volatile CompletableFuture hasSelectionRange = failedInitialization(); + private volatile CompletableFuture hasCallHierarchy = failedInitialization(); private volatile CompletableFuture specialCaseHighlighting = failedInitialization(); @@ -161,6 +167,8 @@ private synchronized void calculateRouting() { prepareRename = findFirstOrDefault(ILanguageContributions::hasRename); didRenameFiles = findFirstOrDefault(ILanguageContributions::hasDidRenameFiles); selectionRange = findFirstOrDefault(ILanguageContributions::hasSelectionRange); + prepareCallHierarchy = findFirstOrDefault(ILanguageContributions::hasCallHierarchy); + incomingOutgoingCalls = findFirstOrDefault(ILanguageContributions::hasCallHierarchy); hasAnalysis = anyTrue(ILanguageContributions::hasAnalysis); hasBuild = anyTrue(ILanguageContributions::hasBuild); @@ -172,10 +180,11 @@ private synchronized void calculateRouting() { hasDefinition = anyTrue(ILanguageContributions::hasDefinition); hasReferences = anyTrue(ILanguageContributions::hasReferences); hasImplementation = anyTrue(ILanguageContributions::hasImplementation); + hasCodeAction = anyTrue(ILanguageContributions::hasCodeAction); hasRename = anyTrue(ILanguageContributions::hasRename); hasDidRenameFiles = anyTrue(ILanguageContributions::hasDidRenameFiles); - hasCodeAction = anyTrue(ILanguageContributions::hasCodeAction); hasSelectionRange = anyTrue(ILanguageContributions::hasSelectionRange); + hasCallHierarchy = anyTrue(ILanguageContributions::hasCallHierarchy); // Always use the special-case highlighting status of *the first* // contribution (possibly using the default value in the Rascal ADT if @@ -336,6 +345,16 @@ public InterruptibleFuture selectionRange(IList focus) { return flatten(selectionRange, c -> c.selectionRange(focus)); } + @Override + public InterruptibleFuture> prepareCallHierarchy(IList focus) { + return flatten(prepareCallHierarchy, c -> c.prepareCallHierarchy(focus)); + } + + @Override + public InterruptibleFuture> incomingOutgoingCalls(Function hierarchyItem, Function direction) { + return flatten(incomingOutgoingCalls, c -> c.incomingOutgoingCalls(hierarchyItem, direction)); + } + @Override public CompletableFuture hasCodeAction() { return hasCodeAction; @@ -401,6 +420,11 @@ public CompletableFuture hasDidRenameFiles() { return hasDidRenameFiles; } + @Override + public CompletableFuture hasCallHierarchy() { + return hasCallHierarchy; + } + @Override public CompletableFuture specialCaseHighlighting() { return specialCaseHighlighting; @@ -425,4 +449,5 @@ public CompletableFuture getOndemandSummaryConfig() { public void cancelProgress(String progressId) { contributions.forEach(klc -> klc.contrib.cancelProgress(progressId)); } + } 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 d7f4ab776..6e6ca82df 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,11 +32,13 @@ import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutorService; +import java.util.function.BiFunction; import java.util.function.Function; import java.util.function.Supplier; import java.util.stream.Collectors; @@ -48,6 +50,12 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.checkerframework.checker.nullness.qual.Nullable; import org.eclipse.lsp4j.ApplyWorkspaceEditParams; +import org.eclipse.lsp4j.CallHierarchyIncomingCall; +import org.eclipse.lsp4j.CallHierarchyIncomingCallsParams; +import org.eclipse.lsp4j.CallHierarchyItem; +import org.eclipse.lsp4j.CallHierarchyOutgoingCall; +import org.eclipse.lsp4j.CallHierarchyOutgoingCallsParams; +import org.eclipse.lsp4j.CallHierarchyPrepareParams; import org.eclipse.lsp4j.CodeAction; import org.eclipse.lsp4j.CodeActionParams; import org.eclipse.lsp4j.CodeLens; @@ -123,11 +131,13 @@ import org.rascalmpl.vscode.lsp.parametric.model.ParametricSummary.SummaryLookup; import org.rascalmpl.vscode.lsp.terminal.ITerminalIDEServer.LanguageParameter; import org.rascalmpl.vscode.lsp.uri.FallbackResolver; +import org.rascalmpl.vscode.lsp.util.CallHierarchy; 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.DocumentSymbols; import org.rascalmpl.vscode.lsp.util.FoldingRanges; +import org.rascalmpl.vscode.lsp.util.Lists; import org.rascalmpl.vscode.lsp.util.SelectionRanges; import org.rascalmpl.vscode.lsp.util.SemanticTokenizer; import org.rascalmpl.vscode.lsp.util.Versioned; @@ -239,6 +249,7 @@ public void initializeServerCapabilities(ServerCapabilities result) { result.setInlayHintProvider(true); result.setSelectionRangeProvider(true); result.setFoldingRangeProvider(true); + result.setCallHierarchyProvider(true); } private String getRascalMetaCommandName() { @@ -828,6 +839,66 @@ public CompletableFuture> selectionRange(SelectionRangePara Collections::emptyList); } + @Override + public CompletableFuture> prepareCallHierarchy(CallHierarchyPrepareParams params) { + final var loc = Locations.toLoc(params.getTextDocument()); + final var contrib = contributions(loc); + final var file = getFile(loc); + + return recoverExceptions(file.getCurrentTreeAsync(true) + .thenApply(Versioned::get) + .thenCompose(t -> { + final var pos = Locations.toRascalPosition(loc, params.getPosition(), columns); + final var focus = TreeSearch.computeFocusList(t, pos.getLine(), pos.getCharacter()); + return contrib.prepareCallHierarchy(focus) + .get() + .thenApply(p -> { + var items = p.getLeft(); + var store = p.getRight(); + var ch = new CallHierarchy(store); + return items.stream() + .map(IConstructor.class::cast) + .map(ci -> ch.toLSP(ci, columns)) + .collect(Collectors.toList()); + }); + }), Collections::emptyList); + } + + private CompletableFuture> incomingOutgoingCalls(BiFunction, T> constructor, CallHierarchyItem source, CallHierarchy.Direction direction) { + final var contrib = contributions(Locations.toLoc(source.getUri())); + return contrib.incomingOutgoingCalls( + store -> CallHierarchy.toRascal(store, source, columns), + store -> CallHierarchy.direction(store, direction) + ).get() + .thenApply(res -> { + var callRel = res.getLeft(); + var store = res.getRight(); + var ch = new CallHierarchy(store); + return callRel.stream() + .map(ITuple.class::cast) + .collect(Collectors.toMap( + t -> ch.toLSP((IConstructor) t.get(0), columns), + t -> List.of(Locations.toRange((ISourceLocation) t.get(1), columns)), + Lists::union, + LinkedHashMap::new + )); + }) + .thenApply(map -> map.entrySet().stream() + .map(e -> constructor.apply(e.getKey(), e.getValue())) + .collect(Collectors.toList())); + } + + @Override + public CompletableFuture> callHierarchyIncomingCalls(CallHierarchyIncomingCallsParams params) { + return recoverExceptions(incomingOutgoingCalls(CallHierarchyIncomingCall::new, params.getItem(), CallHierarchy.Direction.INCOMING), Collections::emptyList); + } + + @Override + public CompletableFuture> callHierarchyOutgoingCalls(CallHierarchyOutgoingCallsParams params) { + return recoverExceptions(incomingOutgoingCalls(CallHierarchyOutgoingCall::new, params.getItem(), CallHierarchy.Direction.OUTGOING), Collections::emptyList); + } + + @Override public synchronized void registerLanguage(LanguageParameter lang) { logger.info("registerLanguage({})", lang.getName()); diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParserOnlyContribution.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParserOnlyContribution.java index 13131b2d1..3fa51e336 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParserOnlyContribution.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParserOnlyContribution.java @@ -32,6 +32,8 @@ import java.io.Writer; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutorService; +import java.util.function.Function; +import org.apache.commons.lang3.tuple.Pair; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.checkerframework.checker.nullness.qual.Nullable; @@ -55,6 +57,7 @@ import io.usethesource.vallang.IValue; import io.usethesource.vallang.IValueFactory; import io.usethesource.vallang.exceptions.FactTypeUseException; +import io.usethesource.vallang.type.TypeStore; public class ParserOnlyContribution implements ILanguageContributions { private static final Logger logger = LogManager.getLogger(ParserOnlyContribution.class); @@ -201,6 +204,16 @@ public InterruptibleFuture selectionRange(IList focus) { return InterruptibleFuture.completedFuture(VF.list()); } + @Override + public InterruptibleFuture> prepareCallHierarchy(IList focus) { + return InterruptibleFuture.completedFuture(Pair.of(VF.list(), new TypeStore())); + } + + @Override + public InterruptibleFuture> incomingOutgoingCalls(Function hierarchyItem, Function direction) { + return InterruptibleFuture.completedFuture(Pair.of(VF.list(), new TypeStore())); + } + @Override public CompletableFuture hasHover() { return CompletableFuture.completedFuture(false); @@ -271,6 +284,11 @@ public CompletableFuture hasSelectionRange() { return CompletableFuture.completedFuture(false); } + @Override + public CompletableFuture hasCallHierarchy() { + return CompletableFuture.completedFuture(false); + } + @Override public CompletableFuture specialCaseHighlighting() { return specialCaseHighlighting; diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/model/RascalADTs.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/model/RascalADTs.java index 09c90f8a3..43b6d3bb6 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/model/RascalADTs.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/model/RascalADTs.java @@ -48,6 +48,7 @@ private LanguageContributions () {} public static final String IMPLEMENTATION = "implementation"; public static final String CODE_ACTION = "codeAction"; public static final String SELECTION_RANGE = "selectionRange"; + public static final String CALL_HIERARCHY = "callHierarchy"; public static final String RENAME_SERVICE = "renameService"; public static final String PREPARE_RENAME_SERVICE = "prepareRenameService"; diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/util/CallHierarchy.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/util/CallHierarchy.java new file mode 100644 index 000000000..08713d389 --- /dev/null +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/util/CallHierarchy.java @@ -0,0 +1,155 @@ +/* + * Copyright (c) 2018-2025, NWO-I CWI and Swat.engineering + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +package org.rascalmpl.vscode.lsp.util; + +import com.google.gson.JsonPrimitive; +import java.io.IOException; +import java.io.StringReader; +import java.util.List; +import java.util.Map; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.eclipse.lsp4j.CallHierarchyItem; +import org.rascalmpl.values.IRascalValueFactory; +import org.rascalmpl.vscode.lsp.util.locations.ColumnMaps; +import org.rascalmpl.vscode.lsp.util.locations.Locations; + +import io.usethesource.vallang.IConstructor; +import io.usethesource.vallang.ISet; +import io.usethesource.vallang.ISourceLocation; +import io.usethesource.vallang.IString; +import io.usethesource.vallang.IValue; +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; + +public class CallHierarchy { + private static final IRascalValueFactory VF = IRascalValueFactory.getInstance(); + private static final TypeFactory TF = TypeFactory.getInstance(); + + public enum Direction { + INCOMING, + OUTGOING + } + + private final IConstructor incoming; + private final IConstructor outgoing; + + private final Type callHierarchyItemCons; + private final @Nullable Type callHierarchyDataAdt; + private final @Nullable Type callHierarchyDataCons; + + private static final String NAME = "name"; + private static final String KIND = "kind"; + private static final String DEFINITION = "src"; + private static final String SELECTION = "selection"; + private static final String TAGS = "tags"; + private static final String DETAIL = "detail"; + private static final String DATA = "data"; + private final TypeStore store; + + + public CallHierarchy(TypeStore store) { + this.store = store; + Type directionAdt = store.lookupAbstractDataType("CallDirection"); + this.incoming = VF.constructor(store.lookupConstructor(directionAdt, "incoming", TF.tupleEmpty())); + this.outgoing = VF.constructor(store.lookupConstructor(directionAdt, "outgoing", TF.tupleEmpty())); + this.callHierarchyItemCons = store.lookupConstructor(store.lookupAbstractDataType("CallHierarchyItem"), "callHierarchyItem").iterator().next(); // first and only + this.callHierarchyDataAdt = store.lookupAbstractDataType("CallHierarchyData"); + this.callHierarchyDataCons = store.lookupConstructor(callHierarchyDataAdt, "none").iterator().next(); + } + + public static IConstructor direction(TypeStore store, CallHierarchy.Direction direction) { + var ch = new CallHierarchy(store); + return ch.direction(direction); + } + + public IConstructor direction(Direction dir) { + switch (dir) { + case INCOMING: return this.incoming; + case OUTGOING: return this.outgoing; + default: throw new IllegalArgumentException(); + } + } + + public CallHierarchyItem toLSP(IConstructor cons, ColumnMaps columns) { + var name = ((IString) cons.get(NAME)).getValue(); + var kind = DocumentSymbols.symbolKindToLSP((IConstructor) cons.get(KIND)); + var def = (ISourceLocation) cons.get(DEFINITION); + var definitionRange = Locations.toRange(def, columns); + var selection = (ISourceLocation) cons.get(SELECTION); + var selectionRange = Locations.toRange(selection, columns); + + var ci = new CallHierarchyItem(name, kind, def.top().getURI().toString(), definitionRange, selectionRange); + var kws = cons.asWithKeywordParameters(); + if (kws.hasParameter(TAGS)) { + ci.setTags(DocumentSymbols.symbolTagsToLSP((ISet) kws.getParameter(TAGS))); + } + if (kws.hasParameter(DETAIL)) { + ci.setDetail(((IString) kws.getParameter(DETAIL)).getValue()); + } + if (kws.hasParameter(DATA)) { + ci.setData(serializeData((IConstructor) kws.getParameter(DATA))); + } + + return ci; + } + + private String serializeData(IConstructor data) { + return data.toString(); + } + + private IConstructor deserializeData(@Nullable Object data) { + if (data == null) { + return VF.constructor(callHierarchyDataCons); + } + try { + return (IConstructor) new StandardTextReader().read(VF, store, callHierarchyDataAdt, new StringReader(((JsonPrimitive) data).getAsString())); + } catch (FactTypeUseException | IOException e) { + throw new IllegalArgumentException("The call hierarchy item data could not be parsed", e); + } + } + + public static IConstructor toRascal(TypeStore store, CallHierarchyItem source, ColumnMaps columns) { + var ch = new CallHierarchy(store); + return ch.toRascal(source, columns); + } + + public IConstructor toRascal(CallHierarchyItem ci, ColumnMaps columns) { + return VF.constructor(callHierarchyItemCons, List.of( + VF.string(ci.getName()), + DocumentSymbols.symbolKindToRascal(ci.getKind()), + Locations.setRange(Locations.toLoc(ci.getUri()), ci.getRange(), columns), + Locations.setRange(Locations.toLoc(ci.getUri()), ci.getSelectionRange(), columns) + ).toArray(new IValue[0]), Map.of( + TAGS, DocumentSymbols.symbolTagsToRascal(ci.getTags()), + DETAIL, VF.string(ci.getDetail()), + DATA, deserializeData(ci.getData()) + )); + } +} diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/util/DocumentSymbols.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/util/DocumentSymbols.java index 0eb72f34e..a58a45003 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/util/DocumentSymbols.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/util/DocumentSymbols.java @@ -30,12 +30,14 @@ import java.util.List; import java.util.stream.Collectors; +import org.checkerframework.checker.nullness.qual.Nullable; import org.eclipse.lsp4j.DocumentSymbol; import org.eclipse.lsp4j.Range; import org.eclipse.lsp4j.SymbolInformation; import org.eclipse.lsp4j.SymbolKind; import org.eclipse.lsp4j.SymbolTag; import org.eclipse.lsp4j.jsonrpc.messages.Either; +import org.rascalmpl.values.IRascalValueFactory; import org.rascalmpl.vscode.lsp.util.locations.LineColumnOffsetMap; import org.rascalmpl.vscode.lsp.util.locations.Locations; @@ -45,8 +47,18 @@ import io.usethesource.vallang.ISourceLocation; import io.usethesource.vallang.IString; import io.usethesource.vallang.IWithKeywordParameters; +import io.usethesource.vallang.type.Type; +import io.usethesource.vallang.type.TypeFactory; +import io.usethesource.vallang.type.TypeStore; public class DocumentSymbols { + private static final IRascalValueFactory VF = IRascalValueFactory.getInstance(); + private static final TypeFactory TF = TypeFactory.getInstance(); + private static final TypeStore store = new TypeStore(); + + private static final Type symbolKindAdt = TF.abstractDataType(store, "DocumentSymbolKind"); + private static final Type symbolTagAdt = TF.abstractDataType(store, "DocumentSymbolTag"); + // hide constructor for static class private DocumentSymbols() {} @@ -83,8 +95,7 @@ public static DocumentSymbol toLSP(IConstructor symbol, final LineColumnOffsetMa .collect(Collectors.toList()) : Collections.emptyList(); - String kindName = ((IConstructor) symbol.get("kind")).getName(); - SymbolKind kind = SymbolKind.valueOf(capitalize(kindName)); + SymbolKind kind = symbolKindToLSP((IConstructor) symbol.get("kind")); String symbolName = ((IString) symbol.get("name")).getValue(); Range range = Locations.toRange((ISourceLocation) symbol.get("range"), om); Range selection = kwp.hasParameter("selection") @@ -92,17 +103,48 @@ public static DocumentSymbol toLSP(IConstructor symbol, final LineColumnOffsetMa : range; String detail = kwp.hasParameter("detail") ? ((IString) kwp.getParameter("detail")).getValue() : null; List tags = kwp.hasParameter("tags") ? - ((ISet) kwp.getParameter("tags")) - .stream() - .map(IConstructor.class::cast) - .map(IConstructor::getName) - .map(DocumentSymbols::capitalize) - .map(SymbolTag::valueOf) - .collect(Collectors.toList()) + symbolTagsToLSP((ISet) kwp.getParameter("tags")) : Collections.emptyList(); var lspSymbol = new DocumentSymbol(symbolName, kind, range, selection, detail, children); lspSymbol.setTags(tags); // since 3.16 return lspSymbol; } + + public static SymbolKind symbolKindToLSP(IConstructor kind) { + return SymbolKind.valueOf(capitalize(kind.getName())); + } + + public static IConstructor symbolKindToRascal(SymbolKind kind) { + return VF.constructor(TF.constructor(store, symbolKindAdt, kind.name().toLowerCase())); + } + + public static List symbolTagsToLSP(@Nullable ISet tags) { + if (tags == null) { + return Collections.emptyList(); + } + return tags.stream() + .map(IConstructor.class::cast) + .map(IConstructor::getName) + .map(DocumentSymbols::capitalize) + .map(SymbolTag::valueOf) + .collect(Collectors.toList()); + } + + public static ISet symbolTagsToRascal(@Nullable List tags) { + if (tags == null) { + return VF.set(); + } + return tags.stream() + .map(t -> VF.constructor(TF.constructor(store, symbolTagAdt, t.name().toLowerCase()))) + .collect(VF.setWriter()); + } + + public static Type getSymbolKindType() { + return symbolKindAdt; + } + + public static TypeStore getStore() { + return store; + } } diff --git a/rascal-lsp/src/main/rascal/library/util/LanguageServer.rsc b/rascal-lsp/src/main/rascal/library/util/LanguageServer.rsc index 2716255f5..2fb951492 100644 --- a/rascal-lsp/src/main/rascal/library/util/LanguageServer.rsc +++ b/rascal-lsp/src/main/rascal/library/util/LanguageServer.rsc @@ -159,7 +159,7 @@ alias Implementer = set[loc] (loc _origin, Tree _fullTree, Tree _lexicalAtCursor @synopsis{Each kind of service contibutes the implementation of one (or several) IDE features.} @description{ Each LanguageService constructor provides one aspect of definining the language server protocol (LSP). -Their names coincide exactly with the services which are documented [here](https://microsoft.github.io/language-server-protocol/). +Their names coincide with the services which are documented [here](https://microsoft.github.io/language-server-protocol/). * The ((parsing)) service that maps source code strings to a ((ParseTree::Tree)) is essential and non-optional. All other other services are optional. @@ -212,6 +212,9 @@ hover documentation, definition with uses, references to declarations, implement * The optional `prepareRename` service argument discovers places in the editor where a ((util::LanguageServer::rename)) is possible. If renameing the location is not supported, it should throw an exception. * The ((didRenameFiles)) service collects ((DocumentEdit))s corresponding to renamed files (e.g. to rename a class when the class file was renamed). The IDE applies the edits after moving the files. It might fail and report why in diagnostics. * The ((selectionRange)) service discovers selections around a cursor, that a user might want to select. It expects the list of source locations to be in ascending order of size (each location should be contained by the next) - similar to ((Focus)) trees. +* The ((callHierarchy)) service discovers callable definitions and call sites. It consists of two subservices. + 1. The first argument, `callableItem`, computes ((CallHierarchyItem))s (definitions) for a given cursor. + 2. The second argument, `calculateCalls`, computes ((incoming)) or ((outgoing)) calls (uses) of a given ((CallHierarchyItem)) `ci`. It returns a list relation of ((CallHierarchyItem))s and the location(s) of the call(s) to `ci` these definitions have. Many services receive a ((Focus)) parameter. The focus lists the syntactical constructs under the current cursor, from the current leaf all the way up to the root of the tree. This list helps to create functionality that is syntax-directed, and always relevant to the @@ -277,11 +280,43 @@ data LanguageService , loc (Focus _focus) prepareRenameService = defaultPrepareRenameService) | didRenameFiles(tuple[list[DocumentEdit], set[Message]] (list[DocumentEdit] fileRenames) didRenameFilesService) | selectionRange(list[loc](Focus _focus) selectionRangeService) + | callHierarchy ( + list[CallHierarchyItem] (Focus _focus) prepareService, + lrel[CallHierarchyItem item, loc call] (CallHierarchyItem _ci, CallDirection _dir) callsService) ; loc defaultPrepareRenameService(Focus _:[Tree tr, *_]) = tr.src when tr.src?; default loc defaultPrepareRenameService(Focus focus) { throw IllegalArgument(focus, "Element under cursor does not have source location"); } +@synopsis{A node in a call hierarchy, either a caller or a callee.} +@description{ +A ((CallHierarchyItem)) represents a single function, method, or procedure in the call hierarchy. +* `name` is the name of the callable/calling entity. +* `kind` is the ((DocumentSymbolKind)) of the callable/calling entity, e.g., function, method, constructor, etc. +* `src` is the location of the definition of the callable/calling entity. +* `selection` is the location of the name of the definition of the callable/calling entity, or another range within `src` to select when the hierarchy item is clicked. +* `tags` are additional metadata tags for the item, e.g., `deprecated`. +* `detail` has additional information about the callable/calling entity, e.g., the function signature. +* `data` can be used to store state that is shared between the `prepareService` and `callsService`. +} +data CallHierarchyItem + = callHierarchyItem( + str name, + DocumentSymbolKind kind, + loc src, // location of the definition + loc selection, // location of the name of the definition + set[DocumentSymbolTag] tags = {}, + str detail = "", // detailed description, e.g. the function signature + CallHierarchyData \data = none() // shared state between `callHierarchy::prepareService` and `callHierarchy::callsService` + ); + +data CallHierarchyData = none(); + +data CallDirection + = incoming() + | outgoing() + ; + @deprecated{Backward compatible with ((parsing)).} @synopsis{Construct a `parsing` ((LanguageService))} LanguageService parser(Parser parser) = parsing(parser);