blob: b7a426c126ffe89458731e69bc6dec77282bd580 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2020 Obeo.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Obeo - initial API and implementation
*******************************************************************************/
package org.eclipse.acceleo.aql.ls.services.textdocument;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;
import org.eclipse.acceleo.aql.IAcceleoEnvironment;
import org.eclipse.acceleo.aql.completion.AcceleoCompletor;
import org.eclipse.acceleo.aql.completion.proposals.AcceleoCompletionProposal;
import org.eclipse.acceleo.aql.location.AcceleoLocationLink;
import org.eclipse.acceleo.aql.location.AcceleoLocator;
import org.eclipse.acceleo.aql.ls.common.AcceleoLanguageServerServicesUtils;
import org.eclipse.acceleo.aql.ls.common.AcceleoLanguageServerStringUtils;
import org.eclipse.acceleo.aql.ls.services.exceptions.LanguageServerProtocolException;
import org.eclipse.acceleo.aql.outline.AcceleoOutliner;
import org.eclipse.acceleo.aql.outline.AcceleoSymbol;
import org.eclipse.acceleo.aql.validation.IAcceleoValidationResult;
import org.eclipse.lsp4j.CompletionItem;
import org.eclipse.lsp4j.CompletionList;
import org.eclipse.lsp4j.CompletionParams;
import org.eclipse.lsp4j.Diagnostic;
import org.eclipse.lsp4j.DidChangeTextDocumentParams;
import org.eclipse.lsp4j.DidCloseTextDocumentParams;
import org.eclipse.lsp4j.DidOpenTextDocumentParams;
import org.eclipse.lsp4j.DidSaveTextDocumentParams;
import org.eclipse.lsp4j.DocumentSymbol;
import org.eclipse.lsp4j.DocumentSymbolParams;
import org.eclipse.lsp4j.Location;
import org.eclipse.lsp4j.LocationLink;
import org.eclipse.lsp4j.Position;
import org.eclipse.lsp4j.PublishDiagnosticsParams;
import org.eclipse.lsp4j.SymbolInformation;
import org.eclipse.lsp4j.TextDocumentContentChangeEvent;
import org.eclipse.lsp4j.TextDocumentPositionParams;
import org.eclipse.lsp4j.jsonrpc.CompletableFutures;
import org.eclipse.lsp4j.jsonrpc.messages.Either;
import org.eclipse.lsp4j.services.LanguageClient;
import org.eclipse.lsp4j.services.LanguageClientAware;
import org.eclipse.lsp4j.services.TextDocumentService;
/**
* The {@link TextDocumentService} implementation for Acceleo.
*
* @author Florent Latombe
*/
public class AcceleoTextDocumentService implements TextDocumentService, LanguageClientAware {
/**
* {@link Map} of the opened documents, uniquely identified by their URI.
*/
private final Map<String, AcceleoTextDocument> openedDocumentsIndex = new HashMap<>();
/**
* The current client.
*/
private LanguageClient languageClient;
/**
* Creates a new {@link AcceleoTextDocumentService}.
*/
public AcceleoTextDocumentService() {
}
// LanguageClientAware API.
@Override
public void connect(LanguageClient newLanguageClient) {
this.languageClient = newLanguageClient;
}
////
// Mandatory TextDocumentService API.
@Override
public void didOpen(DidOpenTextDocumentParams params) {
String openedDocumentUri = params.getTextDocument().getUri();
String openedDocumentText = params.getTextDocument().getText();
AcceleoTextDocument openedAcceleoTextDocument = new AcceleoTextDocument(openedDocumentUri,
openedDocumentText);
this.openedDocumentsIndex.put(openedDocumentUri, openedAcceleoTextDocument);
}
@Override
public void didChange(DidChangeTextDocumentParams params) {
String changedDocumentUri = params.getTextDocument().getUri();
this.checkDocumentUriIsOpened(changedDocumentUri);
List<TextDocumentContentChangeEvent> textDocumentContentchangeEvents = params.getContentChanges();
AcceleoTextDocument changedAcceleoTextDocument = this.openedDocumentsIndex.get(changedDocumentUri);
changedAcceleoTextDocument.applyChanges(textDocumentContentchangeEvents);
// Upon every change, re-validate the module and send the results to the client.
if (this.languageClient != null) {
IAcceleoValidationResult validationResults = changedAcceleoTextDocument.getValidationResults();
List<Diagnostic> diagnostics = AcceleoLanguageServerServicesUtils.transform(validationResults,
changedAcceleoTextDocument.getContents());
this.languageClient.publishDiagnostics(new PublishDiagnosticsParams(changedDocumentUri,
diagnostics));
}
}
@Override
public void didClose(DidCloseTextDocumentParams params) {
String closedDocumentUri = params.getTextDocument().getUri();
checkDocumentUriIsOpened(closedDocumentUri);
this.openedDocumentsIndex.remove(closedDocumentUri);
}
@Override
public void didSave(DidSaveTextDocumentParams params) {
// TODO: Upon saving a document, we probably want to make sure it does not make other documents
// invalid?
}
////
/**
* Checks that this service knows of an open document with the given URI. Otherwise, a
* {@link LanguageServerProtocolException} is thrown because we are not supposed to receive requests on
* open documents before the document is opened.
*
* @param documentUri
* the candidate {@link String document URI}.
*/
private void checkDocumentUriIsOpened(String documentUri) {
if (!this.openedDocumentsIndex.containsKey(documentUri)) {
throw new LanguageServerProtocolException("Received a notification for document \"" + documentUri
+ "\" but it has not previously been opened. This should never happen.");
}
}
@Override
public CompletableFuture<Either<List<CompletionItem>, CompletionList>> completion(
CompletionParams params) {
final String textDocumentUri = params.getTextDocument().getUri();
checkDocumentUriIsOpened(textDocumentUri);
AcceleoTextDocument acceleoTextDocument = this.openedDocumentsIndex.get(textDocumentUri);
Position position = params.getPosition();
return completion(acceleoTextDocument, position);
}
/**
* Provides the completion for a {@link Position} in a {@link AcceleoTextDocument}.
*
* @param acceleoTextDocument
* the (non-{@code null}) {@link AcceleoTextDocument}.
* @param position
* the (non-{@code null}) {@link Position}.
* @return the asynchronous computation of the completion proposals provided by an
* {@link AcceleoCompletor}.
*/
private static CompletableFuture<Either<List<CompletionItem>, CompletionList>> completion(
AcceleoTextDocument acceleoTextDocument, Position position) {
return CompletableFutures.computeAsync(canceler -> {
canceler.checkCanceled();
// Acceleo provides an API to access completion proposals.
final AcceleoCompletor acceleoCompletor = new AcceleoCompletor();
IAcceleoEnvironment acceleoEnvironment = acceleoTextDocument.getAcceleoEnvironment();
String source = acceleoTextDocument.getContents();
int atIndex = AcceleoLanguageServerStringUtils.getCorrespondingCharacterIndex(position, source);
List<AcceleoCompletionProposal> completionProposals = acceleoCompletor.getProposals(
acceleoEnvironment, source, atIndex);
canceler.checkCanceled();
List<CompletionItem> completionItems = AcceleoLanguageServerServicesUtils.transform(
completionProposals);
canceler.checkCanceled();
return Either.forLeft(completionItems);
});
}
@Override
public CompletableFuture<CompletionItem> resolveCompletionItem(CompletionItem unresolved) {
return CompletableFutures.computeAsync(canceler -> {
canceler.checkCanceled();
// For now, the completion already provides fully-resolved items.
return unresolved;
});
}
@Override
public CompletableFuture<Either<List<? extends Location>, List<? extends LocationLink>>> declaration(
TextDocumentPositionParams params) {
final String textDocumentUri = params.getTextDocument().getUri();
checkDocumentUriIsOpened(textDocumentUri);
AcceleoTextDocument acceleoTextDocument = this.openedDocumentsIndex.get(textDocumentUri);
Position position = params.getPosition();
return declaration(acceleoTextDocument, position);
}
/**
* Provides the "go to declaration" results for a {@link Position} in a {@link AcceleoTextDocument}.
*
* @param acceleoTextDocument
* the (non-{@code null}) {@link AcceleoTextDocument}.
* @param position
* the (non-{@code null}) {@link Position}.
* @return the asynchronous computation of the "go to declaration" proposals.
*/
private static CompletableFuture<Either<List<? extends Location>, List<? extends LocationLink>>> declaration(
AcceleoTextDocument acceleoTextDocument, Position position) {
return CompletableFutures.computeAsync(canceler -> {
canceler.checkCanceled();
// Acceleo provides an API to access declaration locations.
final AcceleoLocator acceleoLocator = new AcceleoLocator();
IAcceleoEnvironment acceleoEnvironment = acceleoTextDocument.getAcceleoEnvironment();
String source = acceleoTextDocument.getContents();
int atIndex = AcceleoLanguageServerStringUtils.getCorrespondingCharacterIndex(position, source);
List<AcceleoLocationLink> declarationLocations = acceleoLocator.getDeclarationLocations(
acceleoEnvironment, source, atIndex);
canceler.checkCanceled();
List<LocationLink> locationLinks = declarationLocations.stream().map(
AcceleoLanguageServerServicesUtils::transform).collect(Collectors.toList());
canceler.checkCanceled();
return Either.forRight(locationLinks);
});
}
@Override
public CompletableFuture<Either<List<? extends Location>, List<? extends LocationLink>>> definition(
TextDocumentPositionParams params) {
final String textDocumentUri = params.getTextDocument().getUri();
checkDocumentUriIsOpened(textDocumentUri);
AcceleoTextDocument acceleoTextDocument = this.openedDocumentsIndex.get(textDocumentUri);
Position position = params.getPosition();
return definition(acceleoTextDocument, position);
}
/**
* Provides the "go to definition" results for a {@link Position} in a {@link AcceleoTextDocument}.
*
* @param acceleoTextDocument
* the (non-{@code null}) {@link AcceleoTextDocument}.
* @param position
* the (non-{@code null}) {@link Position}.
* @return the asynchronous computation of the "go to definition" proposals.
*/
private static CompletableFuture<Either<List<? extends Location>, List<? extends LocationLink>>> definition(
AcceleoTextDocument acceleoTextDocument, Position position) {
return CompletableFutures.computeAsync(canceler -> {
canceler.checkCanceled();
// Acceleo provides an API to access definition locations.
final AcceleoLocator acceleoLocator = new AcceleoLocator();
IAcceleoEnvironment acceleoEnvironment = acceleoTextDocument.getAcceleoEnvironment();
String source = acceleoTextDocument.getContents();
int atIndex = AcceleoLanguageServerStringUtils.getCorrespondingCharacterIndex(position, source);
List<AcceleoLocationLink> definitionLocations = acceleoLocator.getDefinitionLocations(
acceleoEnvironment, source, atIndex);
canceler.checkCanceled();
List<LocationLink> locationLinks = definitionLocations.stream().map(
AcceleoLanguageServerServicesUtils::transform).collect(Collectors.toList());
canceler.checkCanceled();
return Either.forRight(locationLinks);
});
}
@Override
public CompletableFuture<List<Either<SymbolInformation, DocumentSymbol>>> documentSymbol(
DocumentSymbolParams params) {
final String textDocumentUri = params.getTextDocument().getUri();
checkDocumentUriIsOpened(textDocumentUri);
AcceleoTextDocument acceleoTextDocument = this.openedDocumentsIndex.get(textDocumentUri);
return documentSymbol(acceleoTextDocument);
}
/**
* Provides all the symbols (templates, queries, etc.) defined in a {@link AcceleoTextDocument}.
*
* @param acceleoTextDocument
* the (non-{@code null}) {@link AcceleoTextDocument}.
* @return the asynchronous computation of all the symbols defined in the document.
*/
private static CompletableFuture<List<Either<SymbolInformation, DocumentSymbol>>> documentSymbol(
AcceleoTextDocument acceleoTextDocument) {
return CompletableFutures.computeAsync(canceler -> {
canceler.checkCanceled();
// Acceleo provides an API to access all defined symbols
final AcceleoOutliner acceleoOutliner = new AcceleoOutliner();
List<AcceleoSymbol> acceleoSymbols = acceleoOutliner.getAllDeclaredSymbols(acceleoTextDocument
.getValidationResults());
canceler.checkCanceled();
List<Either<SymbolInformation, DocumentSymbol>> documentSymbols = acceleoSymbols.stream().map(
acceleoSymbol -> AcceleoLanguageServerServicesUtils.transform(acceleoSymbol,
acceleoTextDocument.getContents())).map(
Either::<SymbolInformation, DocumentSymbol> forRight).collect(Collectors
.toList());
canceler.checkCanceled();
return documentSymbols;
});
}
}