blob: c9ab8532b5ca6e3d66f00ea7a5740544721842df [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2017, 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.completion;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import org.eclipse.acceleo.Error;
import org.eclipse.acceleo.Module;
import org.eclipse.acceleo.ModuleElement;
import org.eclipse.acceleo.aql.completion.proposals.AcceleoCompletionProposal;
import org.eclipse.acceleo.aql.parser.AcceleoAstResult;
import org.eclipse.acceleo.aql.parser.AcceleoParser;
import org.eclipse.acceleo.aql.validation.AcceleoValidator;
import org.eclipse.acceleo.aql.validation.IAcceleoValidationResult;
import org.eclipse.acceleo.query.runtime.namespace.IQualifiedNameQueryEnvironment;
import org.eclipse.emf.ecore.EObject;
/**
* Acceleo service for content assist / auto-completion.
*
* @author <a href="mailto:yvan.lussaud@obeo.fr">Yvan Lussaud</a>
*/
public class AcceleoCompletor {
/**
* The name space for completion.
*/
private static final String TO_COMPLETION_NAMESPACE = "_reserved_::to::completion";
/**
* Provides the {@link List} of {@link AcceleoCompletionProposal completion proposals} available for the
* given Acceleo source at the given position in the given environment.
*
* @param queryEnvironment
* the {@link IQualifiedNameQueryEnvironment}
* @param moduleFileName
* the (non-{@code null}) name of the file containing the module (without extension).
* @param source
* the (non-{@code null}) Acceleo text source contents.
* @param position
* the caret position in {@code source}.
* @return the {@link List} of {@link AcceleoCompletionProposal} for the given source at the given
* position
*/
public List<AcceleoCompletionProposal> getProposals(IQualifiedNameQueryEnvironment queryEnvironment,
String moduleFileName, String source, int position) {
String moduleQualifiedNameForCompletion = TO_COMPLETION_NAMESPACE + AcceleoParser.QUALIFIER_SEPARATOR
+ moduleFileName;
// First, parse the source contents up to the position.
final AcceleoParser acceleoParser = new AcceleoParser();
final String partialAcceleoSource = source.substring(0, position);
final AcceleoAstResult partialAcceleoAstResult = acceleoParser.parse(partialAcceleoSource,
moduleQualifiedNameForCompletion);
// Second, validate the AST - this is required further on for the AQL completion.
final AcceleoAstResult acceleoAstResult = acceleoParser.parse(source,
moduleQualifiedNameForCompletion);
queryEnvironment.getLookupEngine().getResolver().register(moduleQualifiedNameForCompletion,
acceleoAstResult.getModule());
final List<AcceleoCompletionProposal> proposals;
try {
final AcceleoValidator acceleoValidator = new AcceleoValidator(queryEnvironment);
IAcceleoValidationResult acceleoValidationResult = acceleoValidator.validate(
partialAcceleoAstResult, moduleQualifiedNameForCompletion);
// Find which element of the AST we are completing.
EObject acceleoElementToComplete = getElementToComplete(partialAcceleoAstResult);
queryEnvironment.getLookupEngine().pushImportsContext(moduleQualifiedNameForCompletion,
moduleQualifiedNameForCompletion);
try {
proposals = this.getProposals(queryEnvironment, partialAcceleoSource, acceleoValidationResult,
acceleoElementToComplete);
} finally {
queryEnvironment.getLookupEngine().popContext(moduleQualifiedNameForCompletion);
}
} finally {
queryEnvironment.getLookupEngine().getResolver().clear(Collections.singleton(
moduleQualifiedNameForCompletion));
queryEnvironment.getLookupEngine().clearContext(moduleQualifiedNameForCompletion);
}
return proposals;
}
/**
* Gets the containing {@link ModuleElement} for the given {@link EObject}.
*
* @param eObj
* the {@link EObject}
* @return the containing {@link ModuleElement} for the given {@link EObject} if any, <code>null</code>
* otherwise
*/
private ModuleElement getContainingModuleElement(EObject eObj) {
ModuleElement res = null;
EObject current = eObj;
while (current != null) {
if (current instanceof ModuleElement) {
res = (ModuleElement)current;
break;
}
current = current.eContainer();
}
return res;
}
/**
* Provides the {@link List} of {@link AcceleoCompletionProposal completion proposals} for the given
* {@link EObject} of an Acceleo AST in the given environment.
*
* @param queryEnvironment
* the (non-{@code null}) contextual {@link IQualifiedNameQueryEnvironment}.
* @param sourceFragment
* the module source fragment
* @param acceleoValidationResult
* the (non-{@code null}) contextual {@link IAcceleoValidationResult}.
* @param acceleoElementToComplete
* the {@link EObject Acceleo AST element} to complete.
* @return the {@link List} of {@link AcceleoCompletionProposal}.
*/
protected List<AcceleoCompletionProposal> getProposals(IQualifiedNameQueryEnvironment queryEnvironment,
String sourceFragment, IAcceleoValidationResult acceleoValidationResult,
EObject acceleoElementToComplete) {
final List<AcceleoCompletionProposal> completionProposals = new ArrayList<>();
AcceleoAstCompletor acceleoSyntaxCompletor = new AcceleoAstCompletor(queryEnvironment,
acceleoValidationResult);
completionProposals.addAll(acceleoSyntaxCompletor.getCompletion(sourceFragment,
acceleoElementToComplete));
return completionProposals;
}
/**
* Provides the Acceleo AST element to complete.
*
* @param acceleoAstResult
* the (non-{@code null}) {@link AcceleoAstResult} to complete.
* @return the Acceleo AST element to complete.
*/
private EObject getElementToComplete(AcceleoAstResult acceleoAstResult) {
final Error errorToComplete = getErrorToComplete(acceleoAstResult);
if (errorToComplete == null) {
final Module moduleToComplete = acceleoAstResult.getModule();
return moduleToComplete;
} else {
return errorToComplete;
}
}
/**
* Gets the {@link Error} to use for completion starting point. It's the first error whose
* {@link org.eclipse.acceleo.ASTNode#getEndPosition() end} is at the end of the {@link Module}.
*
* @param acceleoAstResultToComplete
* the (non-{@code null}) {@link AcceleoAstResult}.
* @return the {@link Error} to use as the completion starting point. {@code null} if there is
* {@link AcceleoAstResult#getErrors() no errors} in {@code acceleoAstResultToComplete}.
*/
private Error getErrorToComplete(AcceleoAstResult acceleoAstResultToComplete) {
Objects.requireNonNull(acceleoAstResultToComplete);
List<Error> errors = acceleoAstResultToComplete.getErrors();
if (errors.isEmpty()) {
return null;
} else {
Error currentError = errors.get(0);
int currentErrorEndPosition = acceleoAstResultToComplete.getEndPosition(currentError);
for (int i = 1; i < errors.size(); i++) {
final Error candidateError = errors.get(i);
int candidateErrorEnd = acceleoAstResultToComplete.getEndPosition(candidateError);
if (candidateErrorEnd > currentErrorEndPosition) {
currentError = candidateError;
currentErrorEndPosition = candidateErrorEnd;
}
}
return currentError;
}
}
}