blob: 7ed1740a558df806fc9b401e537b20382389552e [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2019 Nicolaj Hoess.
*
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License 2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Nicolaj Hoess - initial API and implementation
*******************************************************************************/
package org.eclipse.jdt.internal.ui.text.java;
import org.eclipse.core.runtime.Assert;
import org.eclipse.jdt.core.CompletionContext;
import org.eclipse.jdt.core.ICompilationUnit;
import org.eclipse.jdt.core.IJavaElement;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.ASTParser;
import org.eclipse.jdt.core.dom.ASTVisitor;
import org.eclipse.jdt.core.dom.BooleanLiteral;
import org.eclipse.jdt.core.dom.ClassInstanceCreation;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.dom.ExpressionStatement;
import org.eclipse.jdt.core.dom.IBinding;
import org.eclipse.jdt.core.dom.InfixExpression;
import org.eclipse.jdt.core.dom.ParenthesizedExpression;
import org.eclipse.jdt.core.dom.QualifiedName;
import org.eclipse.jdt.core.dom.SimpleName;
import org.eclipse.jdt.core.dom.SimpleType;
import org.eclipse.jdt.core.dom.StringLiteral;
import org.eclipse.jdt.internal.corext.dom.IASTSharedValues;
import org.eclipse.jdt.internal.corext.template.java.JavaPostfixContextType;
import org.eclipse.jdt.internal.ui.JavaPlugin;
import org.eclipse.jdt.internal.ui.text.java.AbstractTemplateCompletionProposalComputer;
import org.eclipse.jdt.internal.ui.text.template.contentassist.PostfixTemplateEngine;
import org.eclipse.jdt.internal.ui.text.template.contentassist.TemplateEngine;
import org.eclipse.jdt.ui.PreferenceConstants;
import org.eclipse.jdt.ui.text.java.CompletionProposalCollector;
import org.eclipse.jdt.ui.text.java.JavaContentAssistInvocationContext;
import org.eclipse.jface.preference.IPreferenceStore;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.templates.TemplateContextType;
import org.eclipse.text.templates.ContextTypeRegistry;
/**
* Computer that computes the template proposals for the Java postfix type.
*/
public class PostfixCompletionProposalComputer extends AbstractTemplateCompletionProposalComputer {
private final PostfixTemplateEngine postfixCompletionTemplateEngine;
public PostfixCompletionProposalComputer() {
ContextTypeRegistry templateContextRegistry= JavaPlugin.getDefault().getTemplateContextRegistry();
postfixCompletionTemplateEngine= createTemplateEngine(templateContextRegistry, JavaPostfixContextType.ID_ALL);
}
private static PostfixTemplateEngine createTemplateEngine(ContextTypeRegistry templateContextRegistry, String contextTypeId) {
TemplateContextType contextType= templateContextRegistry.getContextType(contextTypeId);
Assert.isNotNull(contextType);
return new PostfixTemplateEngine(contextType);
}
@Override
protected TemplateEngine computeCompletionEngine(JavaContentAssistInvocationContext context) {
ICompilationUnit unit= context.getCompilationUnit();
if (unit == null)
return null;
IJavaProject javaProject= unit.getJavaProject();
if (javaProject == null)
return null;
if (context.getViewer().getSelectedRange().y > 0) {
// If there is an active selection we must not contribute to the CA
return null;
}
CompletionContext coreContext= context.getCoreContext();
if (coreContext != null) {
int tokenLocation= coreContext.getTokenLocation();
int tokenStart= coreContext.getTokenStart();
int tokenKind= coreContext.getTokenKind();
if ((tokenLocation == 0 && tokenStart > -1)
|| ((tokenLocation & CompletionContext.TL_MEMBER_START) != 0 && tokenKind == CompletionContext.TOKEN_KIND_NAME && tokenStart > -1)
|| (tokenLocation == 0 && isAfterTrigger(context.getDocument(), context.getInvocationOffset()))) {
analyzeCoreContext(context, coreContext);
return postfixCompletionTemplateEngine;
}
}
return null;
}
private void analyzeCoreContext(JavaContentAssistInvocationContext context,
CompletionContext coreContext) {
// If the coreContext is not extended atm for some reason we have to extend it ourself in order get to the needed information
if (coreContext.isExtended()) {
updateTemplateEngine(coreContext);
} else {
final ICompilationUnit cu= context.getCompilationUnit();
final CompletionProposalCollector collector= new CompletionProposalCollector(cu) {
@Override
public void acceptContext(final CompletionContext c) {
super.acceptContext(c);
updateTemplateEngine(c);
}
};
collector.setInvocationContext(context);
collector.setRequireExtendedContext(true);
try {
cu.codeComplete(context.getInvocationOffset(), collector);
} catch (JavaModelException e) {
// continue
}
}
}
private void updateTemplateEngine(CompletionContext context) {
IJavaElement enclosingElement= context.getEnclosingElement();
if (enclosingElement == null) {
return;
}
ICompilationUnit cu= (ICompilationUnit) enclosingElement.getAncestor(IJavaElement.COMPILATION_UNIT);
ASTParser parser= createParser(cu);
IBinding[] res;
try {
res= parser.createBindings(new IJavaElement[] { enclosingElement }, null);
} catch (Exception e) {
return;
}
if (res.length > 0 && res[0] != null) {
parser= createParser(cu);
CompilationUnit cuRoot= (CompilationUnit) parser.createAST(null);
ASTNode completionNode= cuRoot.findDeclaringNode(res[0].getKey());
ASTNode[] bestNode= new ASTNode[] { completionNode };
int tokenLength= context.getToken() != null ? context.getToken().length : 0;
int invOffset= context.getOffset() - tokenLength - 1;
completionNode.accept(new ASTVisitor() {
@Override
public boolean visit(StringLiteral node) {
int start= node.getStartPosition();
if (invOffset > start && start > bestNode[0].getStartPosition()) {
bestNode[0]= node;
}
return true;
}
@Override
public boolean visit(ExpressionStatement node) {
int start= node.getStartPosition();
if (invOffset > start && start > bestNode[0].getStartPosition()) {
bestNode[0]= node;
}
return true;
}
@Override
public boolean visit(SimpleName node) {
int start= node.getStartPosition();
if (invOffset > start && start > bestNode[0].getStartPosition()) {
bestNode[0]= node;
}
return true;
}
@Override
public boolean visit(QualifiedName node) {
int start= node.getStartPosition();
if (invOffset > start && start > bestNode[0].getStartPosition()) {
bestNode[0]= node;
}
return true;
}
@Override
public boolean visit(BooleanLiteral node) {
int start= node.getStartPosition();
if (invOffset > start && start > bestNode[0].getStartPosition()) {
bestNode[0]= node;
}
return true;
}
});
completionNode= bestNode[0];
ASTNode completionNodeParent= findBestMatchingParentNode(completionNode);
postfixCompletionTemplateEngine.setASTNodes(completionNode, completionNodeParent);
postfixCompletionTemplateEngine.setContext(context);
}
}
/**
* This method determines the best matching parent {@link ASTNode} of the given {@link ASTNode}.
* Consider the following example for the definition of <i>best matching parent</i>:<br/>
* <code>("two" + 2).var$</code> has <code>"two"</code> as completion {@link ASTNode}. The
* parent node is <code>"two" + 2</code> which will result in a syntactically incorrect result,
* if the template is applied, because the parentheses aren't taken into account.
*
* @param node The current {@link ASTNode}
* @return {@link ASTNode} which either is the parent of the given node or another predecessor
* {@link ASTNode} in the abstract syntax tree.
*/
private ASTNode findBestMatchingParentNode(ASTNode node) {
ASTNode result= node.getParent();
if (result instanceof InfixExpression) {
ASTNode completionNodeGrandParent= result.getParent();
int safeGuard= 0;
while (completionNodeGrandParent != null
&& completionNodeGrandParent instanceof ParenthesizedExpression
&& safeGuard++ < 64) {
result= completionNodeGrandParent;
completionNodeGrandParent= result.getParent();
}
}
if (node instanceof SimpleName && result instanceof SimpleType) {
ASTNode completionNodeGrandParent= result.getParent();
if (completionNodeGrandParent instanceof ClassInstanceCreation) {
result= completionNodeGrandParent;
}
}
return result;
}
/**
* Returns true if the given offset is directly after an assist trigger character.
*
* @param document the actual document of type {@link IDocument}
* @param offset the current location in the document
* @return <code>true</code> if the given offset is directly after an assist trigger character,
* <code>false</code> otherwise. If the given offset is out of the given document
* <code>false</code> is returned.
*/
private boolean isAfterTrigger(IDocument document, int offset) {
IPreferenceStore preferenceStore= JavaPlugin.getDefault().getPreferenceStore();
String triggers= preferenceStore.getString(PreferenceConstants.CODEASSIST_AUTOACTIVATION_TRIGGERS_JAVA);
try {
return triggers.contains(document.get(offset - 1, 1));
} catch (BadLocationException e) {
return false;
}
}
private static ASTParser createParser(ICompilationUnit cu) {
ASTParser parser= ASTParser.newParser(IASTSharedValues.SHARED_AST_LEVEL);
parser.setKind(ASTParser.K_COMPILATION_UNIT);
parser.setProject(cu.getJavaProject());
parser.setSource(cu);
parser.setResolveBindings(true);
parser.setBindingsRecovery(true);
parser.setStatementsRecovery(true);
return parser;
}
}