Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import org.opencds.cqf.cql.ls.server.manager.CqlTranslationManager;
import org.opencds.cqf.cql.ls.server.manager.TranslatorOptionsManager;
import org.opencds.cqf.cql.ls.server.plugin.CommandContribution;
import org.opencds.cqf.cql.ls.server.provider.GoToDefinitionProvider;
import org.opencds.cqf.cql.ls.server.provider.FormattingProvider;
import org.opencds.cqf.cql.ls.server.provider.HoverProvider;
import org.opencds.cqf.cql.ls.server.service.ActiveContentService;
Expand Down Expand Up @@ -68,9 +69,9 @@ public CompletableFuture<LanguageClient> languageClient() {

@Bean
public CqlTextDocumentService cqlTextDocumentService(
CompletableFuture<LanguageClient> languageClient, HoverProvider hoverProvider,
CompletableFuture<LanguageClient> languageClient, HoverProvider hoverProvider, GoToDefinitionProvider goToDefinitionProvider,
FormattingProvider formattingProvider, EventBus eventBus) {
return new CqlTextDocumentService(languageClient, hoverProvider, formattingProvider,
return new CqlTextDocumentService(languageClient, hoverProvider, formattingProvider, goToDefinitionProvider,
eventBus);
}

Expand Down Expand Up @@ -105,6 +106,11 @@ HoverProvider hoverProvider(CqlTranslationManager cqlTranslationManager) {
return new HoverProvider(cqlTranslationManager);
}

@Bean
GoToDefinitionProvider definitionProvider (CqlTranslationManager cqlTranslationManager) {
return new GoToDefinitionProvider(cqlTranslationManager);
}

@Bean
FormattingProvider formattingProvider(ContentService contentService) {
return new FormattingProvider(contentService);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
package org.opencds.cqf.cql.ls.server.provider;

import org.apache.commons.lang3.tuple.ImmutablePair;
import org.cqframework.cql.cql2elm.CqlTranslator;
import org.cqframework.cql.elm.tracking.TrackBack;
import org.eclipse.lsp4j.*;
import org.eclipse.lsp4j.jsonrpc.messages.Either;
import org.hl7.cql.model.DataType;
import org.hl7.elm.r1.*;
import org.opencds.cqf.cql.ls.core.utility.Uris;
import org.opencds.cqf.cql.ls.server.manager.CqlTranslationManager;
import org.opencds.cqf.cql.ls.server.service.FileContentService;
import org.opencds.cqf.cql.ls.server.utility.CursorOverlappingElements;
import org.opencds.cqf.cql.ls.server.visitor.ExpressionOverlapVisitor;
import org.opencds.cqf.cql.ls.server.visitor.ExpressionOverlapVisitorContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.File;
import java.net.URI;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;

public class GoToDefinitionProvider {
private static final Logger log = LoggerFactory.getLogger(GoToDefinitionProvider.class);
private CqlTranslationManager cqlTranslationManager;


public GoToDefinitionProvider(CqlTranslationManager cqlTranslationManager) {
this.cqlTranslationManager = cqlTranslationManager;
}

public Either<List<? extends Location>, List<? extends LocationLink>> getDefinitionLocation(DefinitionParams params) {

ArrayList<LocationLink> locations = new ArrayList<>();

URI uri = Uris.parseOrNull(params.getTextDocument().getUri());
if (uri == null) {
return Either.forRight(locations);
}

// Cql handles position indexes different then VsCode
Position initialPosition = params.getPosition();
Position cqlPosition = new Position(initialPosition.getLine() + 1, initialPosition.getCharacter() + 1);

// This translates on the fly. We may want to consider maintaining
// an ELM index to reduce the need to do retranslation.
CqlTranslator translator = this.cqlTranslationManager.translate(uri);
if (translator == null) {
return Either.forRight(locations);
}

Library library = translator.getTranslatedLibrary().getLibrary();
Element specificElement = CursorOverlappingElements.getMostSpecificElementAtPosition(cqlPosition, library);

LocationLink locationLink = this.getLocationLinkOfElement(specificElement, library, uri);
if (locationLink != null) {
locations.add(locationLink);
}

return Either.forRight(locations);
}

protected LocationLink getLocationLinkOfElement(Element element, Library currentLibrary, URI currentUri) {

if (element instanceof ExpressionRef) {
ExpressionRef elExpressionRef = (ExpressionRef) element;

ImmutablePair<URI, Library> searchLibraryPair;

// If libraryName exists search for it, else use current.
if (elExpressionRef.getLibraryName() == null) {
searchLibraryPair = new ImmutablePair<URI, Library>(currentUri, currentLibrary);
} else {
searchLibraryPair = findIncludedLibrary(currentLibrary, elExpressionRef.getLibraryName(), Uris.getHead(currentUri));
}

if (searchLibraryPair != null) {

List<ExpressionDef> expressionDefCandidates = GoToDefinitionProvider.getExpressionDefCandidatesByName(searchLibraryPair.getRight(), elExpressionRef.getName());

// If one exact match for either ExpressionRef or FunctionRef, go to it
if (expressionDefCandidates.size() == 1) {
return GoToDefinitionProvider.getLocationLinkForExpressionDef(expressionDefCandidates.get(0), searchLibraryPair.getLeft(), elExpressionRef);
}

// If more than one match for FunctionRef only, search for the best match among overloads based on arguments
else if (expressionDefCandidates.size() > 1 && element instanceof FunctionRef) {
FunctionRef elFunctionRef = (FunctionRef) element;
FunctionDef functionDefCandidate = GoToDefinitionProvider.findMatchingFunctionDefInLibrary(expressionDefCandidates, elFunctionRef);
if (functionDefCandidate != null) {
return GoToDefinitionProvider.getLocationLinkForExpressionDef(functionDefCandidate, searchLibraryPair.getLeft(), elFunctionRef);
}
}
}
}

return null;
}

protected static List<ExpressionDef> getExpressionDefCandidatesByName(Library library, String searchName) {
return library.getStatements().getDef().stream().filter(expressionDef -> expressionDef.getName().equals(searchName)).collect(Collectors.toList());
}

protected static LocationLink getLocationLinkForExpressionDef(ExpressionDef foundExpressionDef, URI libraryUri, Element selectionElement) {
Range targetRange = GoToDefinitionProvider.getRangeOfElement(foundExpressionDef);
Range originSelectionRange = GoToDefinitionProvider.getRangeOfElement(selectionElement);
return new LocationLink(libraryUri.toString(), targetRange, targetRange, originSelectionRange);
}

protected static FunctionDef findMatchingFunctionDefInLibrary(List<ExpressionDef> expressionDefCandidates, FunctionRef elFunctionRef) {

for (ExpressionDef expressionDef : expressionDefCandidates) {
if (expressionDef instanceof FunctionDef) {
FunctionDef functionDefCandidate = (FunctionDef) expressionDef;

// Should already be filtered by name but doesnt hurt
if (!elFunctionRef.getName().equals(functionDefCandidate.getName())) {
continue;
}

// Must have same number of arguments
if (elFunctionRef.getOperand().size() != functionDefCandidate.getOperand().size()) {
continue;
}

boolean doesMatch = true;

for (int i = 0; i < elFunctionRef.getOperand().size(); i++) {
OperandDef operandDefArg = functionDefCandidate.getOperand().get(i);
Expression operandRefArg = elFunctionRef.getOperand().get(i);

doesMatch = GoToDefinitionProvider.areFunctionArgsCompatible(operandDefArg, operandRefArg);

if (!doesMatch) {
break;
}
}

if (doesMatch) {
return functionDefCandidate;
}
}
}

return null;
}

protected ImmutablePair<URI, Library> findIncludedLibrary(Library searchLibrary, String libraryName, URI cqlDirectory) {
File foundFile = null;
for (IncludeDef includeDef : searchLibrary.getIncludes().getDef()) {
// Use LocalIdentifier for lookup
if (includeDef.getLocalIdentifier().equals(libraryName)) {
VersionedIdentifier includeIdentifier = new VersionedIdentifier()
.withId(includeDef.getPath())
.withVersion(includeDef.getVersion());

foundFile = FileContentService.searchFolder(cqlDirectory, includeIdentifier);
break;
}
}

if (foundFile == null) {
return null;
}

CqlTranslator translator = this.cqlTranslationManager.translate(foundFile.toURI());
if (translator == null) {
return null;
}

return new ImmutablePair<URI, Library>(foundFile.toURI(), translator.getTranslatedLibrary().getLibrary());

}

public static boolean areFunctionArgsCompatible(OperandDef functionDefArg, Expression functionRefArg) {
DataType functionDefArgType = functionDefArg.getOperandTypeSpecifier().getResultType();
DataType functionRefArgType = functionRefArg.getResultType();
return functionDefArgType.isCompatibleWith(functionRefArgType);
}

// public static boolean compareTypeSpecifier (TypeSpecifier a, TypeSpecifier b) {
// if (a == null || b == null || a.getClass() != b.getClass()) {
// return false;
// }
//
// if (a instanceof NamedTypeSpecifier) {
// QName aName = ((NamedTypeSpecifier) a).getName();
// QName bName = ((NamedTypeSpecifier) b).getName();
// return aName.getLocalPart().equals(bName.getLocalPart()) && aName.getNamespaceURI().equals(bName.getNamespaceURI()) && aName.getPrefix().equals(bName.getPrefix());
// } else if (a instanceof ListTypeSpecifier) {
// ListTypeSpecifier aList = (ListTypeSpecifier) a;
// ListTypeSpecifier bList = (ListTypeSpecifier) b;
// return GoToDefinitionProvider.compareTypeSpecifier(aList.getElementType(), bList.getElementType());
// } else if (a instanceof IntervalTypeSpecifier) {
// IntervalTypeSpecifier aInterval = (IntervalTypeSpecifier) a;
// IntervalTypeSpecifier bInterval = (IntervalTypeSpecifier) b;
// return GoToDefinitionProvider.compareTypeSpecifier(aInterval.getPointType(), bInterval.getPointType());
// }
//
// return false;
// }


public static Range getRangeOfElement(Element element) {
if (element.getTrackbacks().size() == 0) {
return null;
}
TrackBack trackBack = element.getTrackbacks().get(0);
Position startPosition = new Position(trackBack.getStartLine() - 1, trackBack.getStartChar() - 1);
Position endPosition = new Position(trackBack.getEndLine() - 1, trackBack.getEndChar());
return new Range(startPosition, endPosition);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,17 @@

import java.util.List;
import java.util.concurrent.CompletableFuture;
import org.eclipse.lsp4j.DidChangeTextDocumentParams;
import org.eclipse.lsp4j.DidCloseTextDocumentParams;
import org.eclipse.lsp4j.DidOpenTextDocumentParams;
import org.eclipse.lsp4j.DidSaveTextDocumentParams;
import org.eclipse.lsp4j.DocumentFormattingParams;
import org.eclipse.lsp4j.Hover;
import org.eclipse.lsp4j.HoverParams;
import org.eclipse.lsp4j.InitializeParams;
import org.eclipse.lsp4j.MessageParams;
import org.eclipse.lsp4j.MessageType;
import org.eclipse.lsp4j.ServerCapabilities;
import org.eclipse.lsp4j.TextDocumentSyncKind;
import org.eclipse.lsp4j.TextEdit;

import org.eclipse.lsp4j.*;
import org.eclipse.lsp4j.jsonrpc.messages.Either;
import org.eclipse.lsp4j.services.LanguageClient;
import org.eclipse.lsp4j.services.TextDocumentService;
import org.greenrobot.eventbus.EventBus;
import org.opencds.cqf.cql.ls.server.event.DidChangeTextDocumentEvent;
import org.opencds.cqf.cql.ls.server.event.DidCloseTextDocumentEvent;
import org.opencds.cqf.cql.ls.server.event.DidOpenTextDocumentEvent;
import org.opencds.cqf.cql.ls.server.event.DidSaveTextDocumentEvent;
import org.opencds.cqf.cql.ls.server.provider.GoToDefinitionProvider;
import org.opencds.cqf.cql.ls.server.provider.FormattingProvider;
import org.opencds.cqf.cql.ls.server.provider.HoverProvider;
import org.slf4j.Logger;
Expand All @@ -34,13 +25,15 @@ public class CqlTextDocumentService implements TextDocumentService {
private final CompletableFuture<LanguageClient> client;
private final FormattingProvider formattingProvider;
private final HoverProvider hoverProvider;
private final GoToDefinitionProvider goToDefinitionProvider;
private final EventBus eventBus;

public CqlTextDocumentService(CompletableFuture<LanguageClient> client,
HoverProvider hoverProvider, FormattingProvider formattingProvider, EventBus eventBus) {
HoverProvider hoverProvider, FormattingProvider formattingProvider, GoToDefinitionProvider goToDefinitionProvider, EventBus eventBus) {
this.client = client;
this.formattingProvider = formattingProvider;
this.hoverProvider = hoverProvider;
this.goToDefinitionProvider = goToDefinitionProvider;
this.eventBus = eventBus;
}

Expand All @@ -52,6 +45,8 @@ public void initialize(InitializeParams params, ServerCapabilities serverCapabil
serverCapabilities.setDocumentFormattingProvider(true);
// serverCapabilities.setDocumentRangeFormattingProvider(false);
serverCapabilities.setHoverProvider(true);
serverCapabilities.setDefinitionProvider(true);
// serverCapabilities.set
// c.setReferencesProvider(true);
// c.setDocumentSymbolProvider(true);
// c.setCodeActionProvider(true);
Expand All @@ -65,6 +60,11 @@ public CompletableFuture<Hover> hover(HoverParams position) {
.exceptionally(this::notifyClient);
}

@Override
public CompletableFuture<Either<List<? extends Location>, List<? extends LocationLink>>> definition(DefinitionParams params) {
return CompletableFuture.supplyAsync(() -> this.goToDefinitionProvider.getDefinitionLocation(params))
.exceptionally(this::notifyClient);
}

@Override
public CompletableFuture<List<? extends TextEdit>> formatting(DocumentFormattingParams params) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package org.opencds.cqf.cql.ls.server.utility;

import org.cqframework.cql.elm.tracking.TrackBack;
import org.eclipse.lsp4j.Position;
import org.hl7.elm.r1.Element;
import org.hl7.elm.r1.Library;
import org.opencds.cqf.cql.ls.server.visitor.ExpressionOverlapVisitor;
import org.opencds.cqf.cql.ls.server.visitor.ExpressionOverlapVisitorContext;

import java.util.Collections;
import java.util.Comparator;
import java.util.List;

// Here we want to find the most specific element in a list of elements that match
// a particular cursor position. So after collecting all elements that overlap,
// we sort them from smallest to largest and take the smallest matching element.
public class CursorOverlappingElements {

public static Element getMostSpecificElementAtPosition (Position searchPosition, Library library) {
List<Element> elements = CursorOverlappingElements.getElementsAtPosition(searchPosition, library);
if (elements.size() == 0) {
return null;
}
elements = CursorOverlappingElements.sortElementsFromSmallestToLargest(elements);
return elements.get(0);
}

public static List<Element> getElementsAtPosition (Position searchPosition, Library library) {
ExpressionOverlapVisitor expressionOverlapVisitor = new ExpressionOverlapVisitor();
ExpressionOverlapVisitorContext context = new ExpressionOverlapVisitorContext(searchPosition);
expressionOverlapVisitor.visitLibrary(library, context);
return context.getOverlappingElements();
}

public static List <Element> sortElementsFromSmallestToLargest (List <Element> elements) {
Comparator<Element> compareElements = (o1, o2) -> {
if (o1.getTrackbacks().size() == 0) {
return 1;
}

if (o2.getTrackbacks().size() == 0) {
return -1;
}

TrackBack trackBackOne = o1.getTrackbacks().get(0);
TrackBack trackBackTwo = o2.getTrackbacks().get(0);

if (trackBackOne.getStartLine() == trackBackTwo.getStartLine()
&& trackBackOne.getStartChar() == trackBackTwo.getStartChar()
&& trackBackOne.getEndLine() == trackBackTwo.getEndLine()
&& trackBackOne.getEndChar() == trackBackTwo.getEndChar()
) {
return 0;
}

if (trackBackOne.getStartLine() < trackBackTwo.getStartLine()) {
return 1;
}

if (trackBackOne.getStartLine() == trackBackTwo.getStartLine() && trackBackOne.getStartChar() < trackBackTwo.getStartChar()) {
return 1;
}

if (trackBackOne.getEndLine() > trackBackTwo.getEndLine()) {
return 1;
}

if (trackBackOne.getEndLine() == trackBackTwo.getEndLine() && trackBackOne.getEndChar() > trackBackTwo.getEndChar()) {
return 1;
}

return -1;
};

Collections.sort(elements, compareElements);

return elements;
}
}
Loading