| /******************************************************************************* |
| * Copyright (c) 2020, 2021 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.net.URI; |
| import java.util.ArrayList; |
| import java.util.HashMap; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Objects; |
| import java.util.concurrent.CompletableFuture; |
| import java.util.stream.Collectors; |
| |
| import org.eclipse.acceleo.Module; |
| import org.eclipse.acceleo.aql.completion.AcceleoCompletor; |
| import org.eclipse.acceleo.aql.completion.proposals.AcceleoCompletionProposal; |
| import org.eclipse.acceleo.aql.location.common.AbstractLocationLink; |
| import org.eclipse.acceleo.aql.ls.AcceleoLanguageServer; |
| import org.eclipse.acceleo.aql.ls.common.AcceleoLanguageServerPositionUtils; |
| import org.eclipse.acceleo.aql.ls.common.AcceleoLanguageServerServicesUtils; |
| 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.parser.AcceleoAstUtils; |
| 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 {@link URI}. |
| */ |
| private final Map<URI, AcceleoTextDocument> openedDocumentsIndex = new HashMap<>(); |
| |
| /** |
| * The {@link AcceleoLocationLinkResolver} helps dealing with {@link AbstractLocationLink} provided by the |
| * Acceleo API. |
| */ |
| private final AcceleoLocationLinkResolver acceleoLocationLinkResolver = new AcceleoLocationLinkResolver( |
| this); |
| |
| /** |
| * The owner {@link AcceleoLanguageServer} of this service. |
| */ |
| private final AcceleoLanguageServer server; |
| |
| /** |
| * The current client. |
| */ |
| private LanguageClient languageClient; |
| |
| /** |
| * Creates a new {@link AcceleoTextDocumentService}. |
| * |
| * @param acceleoLanguageServer |
| * the (non-{@code null}) owning {@link AcceleoLanguageServer}. |
| */ |
| public AcceleoTextDocumentService(AcceleoLanguageServer acceleoLanguageServer) { |
| this.server = Objects.requireNonNull(acceleoLanguageServer); |
| } |
| |
| // LanguageClientAware API. |
| @Override |
| public void connect(LanguageClient newLanguageClient) { |
| this.languageClient = newLanguageClient; |
| this.openedDocumentsIndex.clear(); |
| } |
| //// |
| |
| // Mandatory TextDocumentService API. |
| @Override |
| public void didOpen(DidOpenTextDocumentParams params) { |
| URI openedDocumentUri = AcceleoLanguageServerServicesUtils.toUri(params.getTextDocument().getUri()); |
| AcceleoTextDocument openedAcceleoTextDocument = this.server.getWorkspace().getTextDocument( |
| openedDocumentUri); |
| if (openedAcceleoTextDocument == null) { |
| throw new IllegalStateException("Could not find the Acceleo Text Document at URI " |
| + openedDocumentUri); |
| } else { |
| this.openedDocumentsIndex.put(openedDocumentUri, openedAcceleoTextDocument); |
| } |
| } |
| |
| @Override |
| public void didChange(DidChangeTextDocumentParams params) { |
| URI changedDocumentUri = AcceleoLanguageServerServicesUtils.toUri(params.getTextDocument().getUri()); |
| this.checkDocumentIsOpened(changedDocumentUri); |
| |
| List<TextDocumentContentChangeEvent> textDocumentContentchangeEvents = params.getContentChanges(); |
| AcceleoTextDocument changedAcceleoTextDocument = this.server.getWorkspace().getTextDocument( |
| changedDocumentUri); |
| changedAcceleoTextDocument.applyChanges(textDocumentContentchangeEvents); |
| } |
| |
| @Override |
| public void didClose(DidCloseTextDocumentParams params) { |
| URI closedDocumentUri = AcceleoLanguageServerServicesUtils.toUri(params.getTextDocument().getUri()); |
| checkDocumentIsOpened(closedDocumentUri); |
| this.openedDocumentsIndex.remove(closedDocumentUri); |
| } |
| |
| @Override |
| public void didSave(DidSaveTextDocumentParams params) { |
| URI savedDocumentUri = AcceleoLanguageServerServicesUtils.toUri(params.getTextDocument().getUri()); |
| AcceleoTextDocument savedTextDocument = this.server.getWorkspace().getTextDocument(savedDocumentUri); |
| savedTextDocument.documentSaved(); |
| } |
| //// |
| |
| /** |
| * Retrieves the latest {@link IAcceleoValidationResult} of the given {@link AcceleoTextDocument} and |
| * publishes them to the client if there is one. |
| * |
| * @param acceleoTextDocument |
| * the (non-{@code null}) {@link AcceleoTextDocument}. |
| */ |
| public void publishValidationResults(AcceleoTextDocument acceleoTextDocument) { |
| if (this.languageClient != null) { |
| IAcceleoValidationResult validationResults = acceleoTextDocument.getAcceleoValidationResults(); |
| List<Diagnostic> diagnosticsToPublish = AcceleoLanguageServerServicesUtils.transform( |
| validationResults, acceleoTextDocument.getContents()); |
| this.languageClient.publishDiagnostics(new PublishDiagnosticsParams(acceleoTextDocument.getUri() |
| .toString(), diagnosticsToPublish)); |
| } |
| } |
| |
| /** |
| * Provides the {@link AcceleoTextDocument} that defines the given {@link Module}. |
| * |
| * @param definedModule |
| * the (non-{@code null}) {@link Module}. |
| * @return the {@link AcceleoTextDocument} that defines {@code definedModule}, or {@code null} if it could |
| * not be determined. |
| */ |
| public AcceleoTextDocument findTextDocumentDefining(Module definedModule) { |
| AcceleoTextDocument definingTextDocument = null; |
| |
| // // First look in the already loaded documents. |
| // for (AcceleoTextDocument candidate : this.loadedDocumentsIndex.values()) { |
| // if (documentDefinesModule(candidate, definedModule)) { |
| // definingTextDocument = candidate; |
| // break; |
| // } |
| // } |
| // |
| // if (definingTextDocument == null) { |
| // // Otherwise, search in the workspace. |
| // CompletableFuture<List<WorkspaceFolder>> futureWorkspaceFolders = this.languageClient |
| // .workspaceFolders(); |
| // try { |
| // List<WorkspaceFolder> workspaceFolders = futureWorkspaceFolders.get(); |
| // for (WorkspaceFolder workspaceFolder : workspaceFolders) { |
| // List<AcceleoTextDocument> acceleoTextDocuments = this.server.loadAllAcceleoDocumentsIn( |
| // workspaceFolder.getUri()); |
| // for (AcceleoTextDocument candidateAcceleoDocument : acceleoTextDocuments) { |
| // if (documentDefinesModule(candidateAcceleoDocument, definedModule)) { |
| // definingTextDocument = candidateAcceleoDocument; |
| // } |
| // } |
| // } |
| // } catch (InterruptedException | ExecutionException exception) { |
| // throw new RuntimeException(exception); |
| // } |
| // } |
| |
| List<AcceleoTextDocument> allTextDocuments = this.server.getWorkspace().getAllTextDocuments(); |
| for (AcceleoTextDocument candidate : allTextDocuments) { |
| if (documentDefinesModule(candidate, definedModule)) { |
| definingTextDocument = candidate; |
| break; |
| } |
| } |
| |
| return definingTextDocument; |
| } |
| |
| /** |
| * Provides whether the given {@link AcceleoTextDocument} defines the given {@link Module} or not. |
| * |
| * @param acceleoTextDocument |
| * the (non-{@code null} {@link AcceleoTextDocument}. |
| * @param definedModule |
| * the (non-{@code null}) Acceleo {@link Module}. |
| * @return {@code true} if {@code acceleoTextDocument} defines {@code definedModule}, {@code false} |
| * otherwise. |
| */ |
| private static boolean documentDefinesModule(AcceleoTextDocument acceleoTextDocument, |
| Module definedModule) { |
| // We could also simply compare the "unique Module ID" that is placed in the EMF Resource that |
| // "contains" the Module. |
| return AcceleoAstUtils.isEqualStructurally(acceleoTextDocument.getAcceleoAstResult().getModule(), |
| definedModule); |
| } |
| |
| // Implementation of the various capabilities declared by the {@link AcceleoLanguageServer}. |
| /** |
| * 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 {@link URI} of the document. |
| */ |
| protected void checkDocumentIsOpened(URI 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 URI textDocumentUri = AcceleoLanguageServerServicesUtils.toUri(params.getTextDocument() |
| .getUri()); |
| checkDocumentIsOpened(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(); |
| String source = acceleoTextDocument.getContents(); |
| int atIndex = AcceleoLanguageServerPositionUtils.getCorrespondingCharacterIndex(position, source); |
| List<AcceleoCompletionProposal> completionProposals = acceleoCompletor.getProposals( |
| acceleoTextDocument.getQueryEnvironment(), acceleoTextDocument |
| .getFileNameWithoutExtension(), 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 URI textDocumentUri = AcceleoLanguageServerServicesUtils.toUri(params.getTextDocument() |
| .getUri()); |
| checkDocumentIsOpened(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 CompletableFuture<Either<List<? extends Location>, List<? extends LocationLink>>> declaration( |
| AcceleoTextDocument acceleoTextDocument, Position position) { |
| return CompletableFutures.computeAsync(canceler -> { |
| canceler.checkCanceled(); |
| |
| int atIndex = AcceleoLanguageServerPositionUtils.getCorrespondingCharacterIndex(position, |
| acceleoTextDocument.getContents()); |
| List<AbstractLocationLink<?, ?>> declarationLocations = acceleoTextDocument |
| .getDeclarationLocations(atIndex); |
| |
| canceler.checkCanceled(); |
| List<LocationLink> locationLinks = declarationLocations.stream().map( |
| acceleoLocationLinkResolver::transform).collect(Collectors.toList()); |
| |
| canceler.checkCanceled(); |
| return Either.forRight(locationLinks); |
| }); |
| } |
| |
| @Override |
| public CompletableFuture<Either<List<? extends Location>, List<? extends LocationLink>>> definition( |
| TextDocumentPositionParams params) { |
| final URI textDocumentUri = AcceleoLanguageServerServicesUtils.toUri(params.getTextDocument() |
| .getUri()); |
| checkDocumentIsOpened(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 CompletableFuture<Either<List<? extends Location>, List<? extends LocationLink>>> definition( |
| AcceleoTextDocument acceleoTextDocument, Position position) { |
| return CompletableFutures.computeAsync(canceler -> { |
| canceler.checkCanceled(); |
| |
| int atIndex = AcceleoLanguageServerPositionUtils.getCorrespondingCharacterIndex(position, |
| acceleoTextDocument.getContents()); |
| List<AbstractLocationLink<?, ?>> definitionLocations = acceleoTextDocument.getDefinitionLocations( |
| atIndex); |
| |
| canceler.checkCanceled(); |
| List<LocationLink> locationLinks = definitionLocations.stream().map( |
| acceleoLocationLinkResolver::transform).collect(Collectors.toList()); |
| |
| canceler.checkCanceled(); |
| return Either.forRight(locationLinks); |
| }); |
| } |
| |
| @Override |
| public CompletableFuture<List<Either<SymbolInformation, DocumentSymbol>>> documentSymbol( |
| DocumentSymbolParams params) { |
| final URI textDocumentUri = AcceleoLanguageServerServicesUtils.toUri(params.getTextDocument() |
| .getUri()); |
| checkDocumentIsOpened(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(); |
| |
| List<Either<SymbolInformation, DocumentSymbol>> documentSymbols = new ArrayList<>(); |
| if (acceleoTextDocument.getAcceleoValidationResults() != null) { |
| // Acceleo provides an API to access all defined symbols |
| final AcceleoOutliner acceleoOutliner = new AcceleoOutliner(); |
| List<AcceleoSymbol> acceleoSymbols = acceleoOutliner.getAllDeclaredSymbols(acceleoTextDocument |
| .getAcceleoValidationResults()); |
| |
| canceler.checkCanceled(); |
| documentSymbols = acceleoSymbols.stream().map( |
| acceleoSymbol -> AcceleoLanguageServerServicesUtils.transform(acceleoSymbol, |
| acceleoTextDocument.getContents())).map( |
| Either::<SymbolInformation, DocumentSymbol> forRight).collect( |
| Collectors.toList()); |
| } |
| |
| canceler.checkCanceled(); |
| return documentSymbols; |
| }); |
| } |
| //// |
| |
| } |