| /******************************************************************************* |
| * 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.workspace; |
| |
| import java.net.URI; |
| import java.util.ArrayList; |
| import java.util.Collections; |
| import java.util.HashSet; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Objects; |
| import java.util.Set; |
| import java.util.TreeMap; |
| |
| import org.eclipse.acceleo.Import; |
| import org.eclipse.acceleo.aql.AcceleoEnvironment; |
| import org.eclipse.acceleo.aql.IAcceleoEnvironment; |
| import org.eclipse.acceleo.aql.ls.AcceleoLanguageServer; |
| import org.eclipse.acceleo.aql.ls.services.textdocument.AcceleoTextDocument; |
| import org.eclipse.acceleo.query.runtime.namespace.IQualifiedNameLookupEngine; |
| import org.eclipse.acceleo.query.runtime.namespace.IQualifiedNameResolver; |
| |
| /** |
| * A representation, in the {@link AcceleoLanguageServer} of a container of {@link AcceleoTextDocument |
| * AcceleoTextDocuments} that share a same {@link IAcceleoEnvironment}. It may or may not correspond to a |
| * physical element in the client. |
| * |
| * @author Florent Latombe |
| */ |
| public class AcceleoProject { |
| |
| /** |
| * The {@link Map} that indexes the {@link AcceleoTextDocument} of this project by their {@link URI}. |
| */ |
| private final Map<URI, AcceleoTextDocument> acceleoTextDocumentsIndex = new TreeMap<>(); |
| |
| /** |
| * The {@link AcceleoWorkspace} that contains this {@link AcceleoProject}. |
| */ |
| private AcceleoWorkspace workspace; |
| |
| /** |
| * The {@link String label} for this {@link AcceleoProject}. |
| */ |
| private String label; |
| |
| /** |
| * The {@link IQualifiedNameResolver} of this {@link AcceleoProject}. |
| */ |
| private IQualifiedNameResolver resolver; |
| |
| /** |
| * The constructor. |
| * |
| * @param label |
| * the (non-{@code null}) {@link String label} for the project. |
| * @param resolver |
| * the (non-{@code null}) {@link IQualifiedNameResolver} for the project. |
| */ |
| public AcceleoProject(String label, IQualifiedNameResolver resolver) { |
| this.label = Objects.requireNonNull(label); |
| this.resolver = Objects.requireNonNull(resolver); |
| } |
| |
| /** |
| * Provides the {@link String label} of this {@link AcceleoProject}. |
| * |
| * @return the (non-{@code null}) {@link String label} of this {@link AcceleoProject}. |
| */ |
| public String getLabel() { |
| return this.label; |
| } |
| |
| /** |
| * Modifes the {@link String label} of this {@link AcceleoProject}. |
| * |
| * @param newLabel |
| * the (non-{@code null}) new {@link String label} of the project. |
| */ |
| public void setLabel(String newLabel) { |
| this.label = newLabel; |
| } |
| |
| /** |
| * Provides the owning {@link AcceleoLanguageServer}. |
| * |
| * @return the (maybe-{@code null}) owning {@link AcceleoLanguageServer}. |
| */ |
| public AcceleoLanguageServer getLanguageServer() { |
| if (this.getWorkspace() != null) { |
| return this.getWorkspace().getOwner(); |
| } else { |
| return null; |
| } |
| } |
| |
| /** |
| * Affects this {@link AcceleoProject} to a container {@link AcceleoWorkspace}. |
| * |
| * @param acceleoWorkspace |
| * the (maybe-{@code null}) container {@link AcceleoWorkspace}. |
| */ |
| public void setWorkspace(AcceleoWorkspace acceleoWorkspace) { |
| this.workspace = acceleoWorkspace; |
| |
| // The workspace has changed so the environment has implicitly changed. |
| this.getTextDocuments().forEach(textDocument -> textDocument.resolverChanged()); |
| } |
| |
| /** |
| * Provides the {@link IQualifiedNameResolver} of this {@link AcceleoProject}. |
| * |
| * @return the (non-{@code null}) {@link IQualifiedNameResolver} of this {@link AcceleoProject}. |
| */ |
| public IQualifiedNameResolver getResolver() { |
| return this.resolver; |
| } |
| |
| /** |
| * Provides the {@link List} of all {@link AcceleoTextDocument} in this {@link AcceleoProject}. |
| * |
| * @return the (non-{@code null}) unmodifiable {@link List} of all {@link AcceleoTextDocument} in this |
| * {@link AcceleoProject}. |
| */ |
| public List<AcceleoTextDocument> getTextDocuments() { |
| return Collections.unmodifiableList(new ArrayList<>(this.acceleoTextDocumentsIndex.values())); |
| } |
| |
| /** |
| * Sets the {@link IQualifiedNameResolver} of this {@link AcceleoProject}. |
| * |
| * @param resolver |
| * the new {@link IQualifiedNameResolver} of this {@link AcceleoProject}. |
| */ |
| public void setResolver(IQualifiedNameResolver resolver) { |
| this.resolver = resolver; |
| |
| // The resolver has changed so we notify our documents. |
| this.getTextDocuments().forEach(textDocument -> textDocument.resolverChanged()); |
| } |
| |
| /** |
| * Adds an {@link AcceleoTextDocument} to this {@link AcceleoProject}. |
| * |
| * @param textDocumentToAdd |
| * the (non-{@code null}) {@link AcceleoTextDocument} to add to this {@link AcceleoProject}. |
| */ |
| public void addTextDocument(AcceleoTextDocument textDocumentToAdd) { |
| if (this.acceleoTextDocumentsIndex.containsKey(textDocumentToAdd.getUri())) { |
| throw new IllegalArgumentException("There was an issue while trying to add text document " |
| + textDocumentToAdd.getUri() + " to project " + this.getLabel() |
| + ": there is already a known text document with this URI."); |
| } |
| textDocumentToAdd.setProject(this); |
| this.acceleoTextDocumentsIndex.put(textDocumentToAdd.getUri(), textDocumentToAdd); |
| // The impact of adding a document to this project is similar as if we had just saved it. |
| this.workspace.documentSaved(textDocumentToAdd); |
| } |
| |
| /** |
| * Removes an {@link AcceleoTextDocument} from this {@link AcceleoProject}. |
| * |
| * @param textDocumentToRemove |
| * the (non-{@code null}) {@link AcceleoTextDocument} to remove from this |
| * {@link AcceleoProject}. |
| */ |
| public void removeTextDocument(AcceleoTextDocument textDocumentToRemove) { |
| if (!this.acceleoTextDocumentsIndex.containsKey(textDocumentToRemove.getUri())) { |
| throw new IllegalArgumentException("There was an issue while trying to remove text document " |
| + textDocumentToRemove.getUri() + " from project " + this.getLabel() |
| + ": there is no known text document with this URI."); |
| } |
| textDocumentToRemove.setProject(null); |
| this.acceleoTextDocumentsIndex.remove(textDocumentToRemove.getUri()); |
| this.workspace.documentRemoved(textDocumentToRemove); |
| } |
| |
| /** |
| * Provides the {@link AcceleoTextDocument} found at the given {@link URI}. |
| * |
| * @param uri |
| * the (non-{@code null}) {@link URI} of the {@link AcceleoTextDocument} we want to retrieve. |
| * @return the corresponding {@link AcceleoTextDocument}. {@code null} if there was none. |
| */ |
| public AcceleoTextDocument getTextDocument(URI uri) { |
| return this.acceleoTextDocumentsIndex.get(uri); |
| } |
| |
| /** |
| * Provides the owner {@link AcceleoWorkspace} of this {@link AcceleoProject}. |
| * |
| * @return the (maybe-{@code null}) owner {@link AcceleoWorkspace} of this {@link AcceleoProject}. |
| */ |
| public AcceleoWorkspace getWorkspace() { |
| return this.workspace; |
| } |
| |
| /** |
| * We receive this notification when an {@link AcceleoTextDocument} in our {@link AcceleoEnvironment} has |
| * changed and saved. We want to re-validate any of our modules that depended on it. |
| * |
| * @param savedTextDocument |
| * the (non-{@code null}) {@link AcceleoTextDocument} that was saved. |
| */ |
| public void documentSaved(AcceleoTextDocument savedTextDocument) { |
| final String qualifiedNameOfSavedModule = savedTextDocument.getModuleQualifiedName(); |
| final IQualifiedNameLookupEngine lookupEngine = savedTextDocument.getAcceleoEnvironment() |
| .getQueryEnvironment().getLookupEngine(); |
| |
| // First clear the environment for the document that was changed. |
| lookupEngine.clearContext(qualifiedNameOfSavedModule); |
| lookupEngine.getResolver().clear(Collections.singleton(qualifiedNameOfSavedModule)); |
| |
| // Then update the environment with the new version of the module from the saved document. |
| lookupEngine.getResolver().register(qualifiedNameOfSavedModule, savedTextDocument |
| .getAcceleoAstResult().getModule()); |
| |
| // Re-validate all modules that depend on the changed module. |
| Set<AcceleoTextDocument> consumers = getTextDocumentsThatDependOn(qualifiedNameOfSavedModule); |
| for (AcceleoTextDocument consumer : consumers) { |
| consumer.resolverChanged(); |
| } |
| |
| // If the saved document belongs to us, we propagate the notification up to the workspace. |
| if (this.getTextDocuments().contains(savedTextDocument) && this.getWorkspace() != null) { |
| this.getWorkspace().documentSaved(savedTextDocument); |
| } |
| } |
| |
| /** |
| * We receive this notification when an {@link AcceleoTextDocument} in our {@link AcceleoEnvironment} has |
| * been removed. We want to unregister any services it has contributed and re-parse and re-validate any of |
| * our modules that depended on it. |
| * |
| * @param removedTextDocument |
| * the (non-{@code null}) {@link AcceleoTextDocument} that has been removed. |
| */ |
| public void documentRemoved(AcceleoTextDocument removedTextDocument) { |
| final IQualifiedNameLookupEngine lookupEngine = removedTextDocument.getAcceleoEnvironment() |
| .getQueryEnvironment().getLookupEngine(); |
| |
| // Since the qualified name of a module depends on its environment, we want the qualified name |
| // according to our environment. |
| String removedModuleQualifiedName = lookupEngine.getResolver().getQualifiedName(removedTextDocument |
| .getUrl()); |
| |
| // First unregister it from the environment. |
| lookupEngine.getResolver().clear(Collections.singleton(removedModuleQualifiedName)); |
| lookupEngine.clearContext(removedModuleQualifiedName); |
| |
| // Re-validate all modules that depend on the changed module. |
| Set<AcceleoTextDocument> consumers = getTextDocumentsThatDependOn(removedModuleQualifiedName); |
| for (AcceleoTextDocument consumer : consumers) { |
| consumer.resolverChanged(); |
| } |
| |
| // Unlike in documentSaved, we do not need to propagate the notification as it cannot come from the |
| // document itself. |
| } |
| |
| /** |
| * Provides the {@link AcceleoTextDocument text documents} of this project that depend on the given |
| * qualified name. |
| * |
| * @param moduleQualifiedName |
| * the (non-{@code null}) qualified name of an Acceleo Module. |
| * @return the {@link Set} of {@link AcceleoTextDocument} that depend on {@code moduleQualifiedName}. |
| */ |
| private Set<AcceleoTextDocument> getTextDocumentsThatDependOn(String moduleQualifiedName) { |
| Set<AcceleoTextDocument> res = new HashSet<>(); |
| for (AcceleoTextDocument acceleoTextDocument : this.getTextDocuments()) { |
| if (!moduleQualifiedName.equals(acceleoTextDocument.getModuleQualifiedName()) && dependsOn( |
| acceleoTextDocument.getAcceleoAstResult().getModule(), moduleQualifiedName)) { |
| res.add(acceleoTextDocument); |
| } |
| } |
| return res; |
| } |
| |
| private static boolean dependsOn(org.eclipse.acceleo.Module module, String moduleQualifiedName) { |
| boolean res = false; |
| if (module.getExtends() != null && moduleQualifiedName.equals(module.getExtends() |
| .getQualifiedName())) { |
| res = true; |
| } else { |
| for (Import imp : module.getImports()) { |
| if (moduleQualifiedName.equals(imp.getModule().getQualifiedName())) { |
| res = true; |
| break; |
| } |
| } |
| } |
| return res; |
| } |
| } |