/*
* Copyright (c) 2013, 2015 QNX Software Systems and others.
* 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
*/
package org.eclipse.cdt.internal.qt.ui.assist;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import org.eclipse.cdt.core.CCorePlugin;
import org.eclipse.cdt.core.dom.ast.DOMException;
import org.eclipse.cdt.core.dom.ast.IASTCompositeTypeSpecifier;
import org.eclipse.cdt.core.dom.ast.IASTEqualsInitializer;
import org.eclipse.cdt.core.dom.ast.IASTFileLocation;
import org.eclipse.cdt.core.dom.ast.IASTInitializer;
import org.eclipse.cdt.core.dom.ast.IASTInitializerClause;
import org.eclipse.cdt.core.dom.ast.IASTName;
import org.eclipse.cdt.core.dom.ast.IASTNode;
import org.eclipse.cdt.core.dom.ast.IASTNodeSelector;
import org.eclipse.cdt.core.dom.ast.IASTTranslationUnit;
import org.eclipse.cdt.core.dom.ast.IBasicType;
import org.eclipse.cdt.core.dom.ast.IBinding;
import org.eclipse.cdt.core.dom.ast.IType;
import org.eclipse.cdt.core.dom.ast.cpp.ICPPClassType;
import org.eclipse.cdt.core.dom.ast.cpp.ICPPFunctionType;
import org.eclipse.cdt.core.dom.ast.cpp.ICPPMethod;
import org.eclipse.cdt.core.dom.ast.cpp.ICPPParameter;
import org.eclipse.cdt.core.index.IIndex;
import org.eclipse.cdt.core.model.ICProject;
import org.eclipse.cdt.core.model.ITranslationUnit;
import org.eclipse.cdt.internal.core.dom.parser.cpp.CPPParameter;
import org.eclipse.cdt.internal.qt.core.index.IQMethod;
import org.eclipse.cdt.internal.qt.core.index.IQObject;
import org.eclipse.cdt.internal.qt.core.index.IQProperty;
import org.eclipse.cdt.internal.qt.core.index.QtIndex;
import org.eclipse.cdt.internal.qt.ui.Activator;
import org.eclipse.cdt.internal.ui.text.contentassist.CCompletionProposal;
import org.eclipse.cdt.ui.text.contentassist.ICEditorContentAssistInvocationContext;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.jface.text.contentassist.ICompletionProposal;
/**
* An attribute-based proposal depends on the both the attribute (the previous identifier) and the
* containing class definition. The class definition is not needed for all attribute types, but
* is used to build the list of proposals for attributes like READ, WRITE, etc.
*/
@SuppressWarnings("restriction")
public class QPropertyAttributeProposal {
private final int relevance;
private final String identifier;
private final String display;
public QPropertyAttributeProposal(String identifier, int relevance) {
this(identifier, identifier, relevance);
}
public ICompletionProposal createProposal(String prefix, int offset) {
int prefixLen = prefix == null ? 0 : prefix.length();
String disp = identifier.equals(display) ? display : ( identifier + " - " + display );
return new CCompletionProposal(identifier.substring(prefixLen), offset, prefixLen, Activator.getQtLogo(), disp, relevance);
}
private QPropertyAttributeProposal(String identifier, String display, int relevance) {
this.identifier = identifier;
this.display = display;
this.relevance = relevance;
}
public String getIdentifier() {
return identifier;
}
public static Collection<QPropertyAttributeProposal> buildProposals(IQProperty.Attribute attr, ICEditorContentAssistInvocationContext context, IType type, String name) {
switch(attr) {
// propose true/false for bool Attributes
case DESIGNABLE:
case SCRIPTABLE:
case STORED:
case USER:
return Arrays.asList(
new QPropertyAttributeProposal("true", IMethodAttribute.BaseRelevance + 11),
new QPropertyAttributeProposal("false", IMethodAttribute.BaseRelevance + 10));
// propose appropriate methods for method-based attributes
case READ:
case WRITE:
case RESET:
return getMethodProposals(context, get(attr, type, name));
// propose appropriate signals for NOTIFY
case NOTIFY:
return getSignalProposals(context, get(attr, type, name));
default:
break;
}
return Collections.emptyList();
}
private static Collection<QPropertyAttributeProposal> getMethodProposals(ICEditorContentAssistInvocationContext context, IMethodAttribute methodAttribute) {
ICPPClassType cls = getEnclosingClassDefinition(context);
if (cls == null)
return Collections.emptyList();
// Return all the methods, including inherited and non-visible ones.
ICPPMethod[] methods = cls.getMethods();
List<ICPPMethod> filtered = new ArrayList<ICPPMethod>(methods.length);
for(ICPPMethod method : methods)
if (methodAttribute.keep(method))
filtered.add(method);
// TODO Choose the overload that is the best match -- closest parameter type and fewest
// parameters with default values.
List<QPropertyAttributeProposal> proposals = new ArrayList<QPropertyAttributeProposal>();
for(ICPPMethod method : getMethods(context, methodAttribute))
proposals.add(new QPropertyAttributeProposal(method.getName(), getDisplay(cls, method), methodAttribute.getRelevance(method)));
return proposals;
}
private static Collection<QPropertyAttributeProposal> getSignalProposals(ICEditorContentAssistInvocationContext context, IMethodAttribute methodAttribute) {
ICPPClassType cls = getEnclosingClassDefinition(context);
if (cls == null)
return Collections.emptyList();
ICProject cProject = context.getProject();
if (cProject == null)
return Collections.emptyList();
QtIndex qtIndex = QtIndex.getIndex(cProject.getProject());
if (qtIndex == null)
return Collections.emptyList();
IQObject qobj = null;
try {
qobj = qtIndex.findQObject(cls.getQualifiedName());
} catch(DOMException e) {
Activator.log(e);
}
if (qobj == null)
return Collections.emptyList();
List<QPropertyAttributeProposal> proposals = new ArrayList<QPropertyAttributeProposal>();
for(IQMethod qMethod : qobj.getSignals().all())
proposals.add(new QPropertyAttributeProposal(qMethod.getName(), IMethodAttribute.BaseRelevance));
return proposals;
}
private static boolean isSameClass(ICPPClassType cls1, ICPPClassType cls2) {
// IType.isSameType doesn't work in this case. Given an instance of ICPPClassType, cls,
// the following returns false:
// cls.isSameType( cls.getMethods()[0].getOwner() )
//
// Instead we check the fully qualified names.
try {
String[] qn1 = cls1.getQualifiedName();
String[] qn2 = cls2.getQualifiedName();
if (qn1.length != qn2.length)
return false;
for(int i = 0; i < qn1.length; ++i)
if (!qn1[i].equals(qn2[i]))
return false;
return true;
} catch(DOMException e) {
return false;
}
}
private static String getDisplay(ICPPClassType referenceContext, ICPPMethod method) {
boolean includeClassname = !isSameClass(referenceContext, method.getClassOwner());
StringBuilder sig = new StringBuilder();
ICPPFunctionType type = method.getType();
sig.append(type.getReturnType().toString());
sig.append(' ');
if (includeClassname) {
sig.append(method.getOwner().getName());
sig.append("::");
}
sig.append(method.getName());
sig.append('(');
boolean first = true;
for(ICPPParameter param : method.getParameters()) {
if (first)
first = false;
else
sig.append(", ");
String defValue = null;
if (param instanceof CPPParameter) {
CPPParameter cppParam = (CPPParameter) param;
IASTInitializer defaultValue = cppParam.getInitializer();
if (defaultValue instanceof IASTEqualsInitializer) {
IASTInitializerClause clause = ((IASTEqualsInitializer) defaultValue).getInitializerClause();
defValue = clause.toString();
}
}
sig.append(defValue == null ? param.getType().toString() : defValue);
}
sig.append(')');
return sig.toString();
}
private static interface IMethodAttribute {
public boolean keep(ICPPMethod method);
public static final int BaseRelevance = 2000;
public int getRelevance(ICPPMethod method);
public static final IMethodAttribute Null = new IMethodAttribute() {
@Override
public boolean keep(ICPPMethod method) {
return false;
}
@Override
public int getRelevance(ICPPMethod method) {
return 0;
}
};
}
private static IMethodAttribute get(IQProperty.Attribute attr, IType type, String propertyName) {
switch(attr) {
case READ:
return new Read(type, propertyName);
case WRITE:
return new Write(type, propertyName);
case RESET:
return new Reset(type, propertyName);
default:
return IMethodAttribute.Null;
}
}
private static class Read implements IMethodAttribute {
private final IType type;
private final String propertyName;
public Read(IType type, String propertyName) {
this.type = type;
this.propertyName = propertyName;
}
// From the Qt docs, http://qt-project.org/doc/qt-4.8/properties.html:
// "A READ accessor function is required. It is for reading the property value. Ideally, a
// const function is used for this purpose, and it must return either the property's type
// or a pointer or reference to that type. e.g., QWidget::focus is a read-only property with
// READ function, QWidget::hasFocus().
@Override
public boolean keep(ICPPMethod method) {
// READ must have no params without default values
if (method.getParameters().length > 0
&& !method.getParameters()[0].hasDefaultValue())
return false;
// Make sure the return type of the method can be assigned to the property's type.
IType retType = method.getType().getReturnType();
if (!isAssignable(retType, type))
return false;
return true;
}
@Override
public int getRelevance(ICPPMethod method) {
String methodName = method.getName();
if (methodName == null)
return 0;
// exact match is the most relevant
if (methodName.equals(propertyName))
return BaseRelevance + 20;
// accessor with "get" prefix is the 2nd highest rank
if (methodName.equalsIgnoreCase("get" + propertyName))
return BaseRelevance + 19;
// method names that include the property name anywhere are the next
// most relevant
if (methodName.matches(".*(?i)" + propertyName + ".*"))
return BaseRelevance + 18;
// otherwise return default relevance
return 10;
}
}
private static class Write implements IMethodAttribute {
private final IType type;
private final String propertyName;
public Write(IType type, String propertyName) {
this.type = type;
this.propertyName = propertyName;
}
// From the Qt docs, http://qt-project.org/doc/qt-4.8/properties.html:
// A WRITE accessor function is optional. It is for setting the property value. It must
// return void and must take exactly one argument, either of the property's type or a
// pointer or reference to that type. e.g., QWidget::enabled has the WRITE function
// QWidget::setEnabled(). Read-only properties do not need WRITE functions. e.g., QWidget::focus
// has no WRITE function.
@Override
public boolean keep(ICPPMethod method) {
// The Qt moc doesn't seem to check that the return type is void, and I'm not sure why it
// would need to. This filter doesn't reject non-void methods.
// WRITE must have at least one parameter and no more than one param without default values
if (method.getParameters().length < 1
|| (method.getParameters().length > 1
&& !method.getParameters()[1].hasDefaultValue()))
return false;
// Make sure the property's type can be assigned to the type of the first parameter
IType paramType = method.getParameters()[0].getType();
if (!isAssignable(type, paramType))
return false;
return true;
}
@Override
public int getRelevance(ICPPMethod method) {
String methodName = method.getName();
if (methodName == null)
return 0;
// exact match is the most relevant
if (methodName.equals(propertyName))
return BaseRelevance + 20;
// accessor with "get" prefix is the 2nd highest rank
if (methodName.equalsIgnoreCase("set" + propertyName))
return BaseRelevance + 19;
// method names that include the property name anywhere are the next
// most relevant
if (methodName.matches(".*(?i)" + propertyName + ".*"))
return BaseRelevance + 18;
// otherwise return default relevance
return 10;
}
}
private static class Reset implements IMethodAttribute {
private final IType type;
private final String propertyName;
public Reset(IType type, String propertyName) {
this.type = type;
this.propertyName = propertyName;
}
// From the Qt docs, http://qt-project.org/doc/qt-4.8/properties.html:
// A RESET function is optional. It is for setting the property back to its context
// specific default value. e.g., QWidget::cursor has the typical READ and WRITE
// functions, QWidget::cursor() and QWidget::setCursor(), and it also has a RESET
// function, QWidget::unsetCursor(), since no call to QWidget::setCursor() can mean
// reset to the context specific cursor. The RESET function must return void and take
// no parameters.
@Override
public boolean keep(ICPPMethod method) {
// RESET must have void return type
IType retType = method.getType().getReturnType();
if (!(retType instanceof IBasicType)
|| ((IBasicType) retType).getKind() != IBasicType.Kind.eVoid)
return false;
// RESET must have no parameters
if (method.getParameters().length > 0)
return false;
return true;
}
@Override
public int getRelevance(ICPPMethod method) {
String methodName = method.getName();
if (methodName == null)
return 0;
// accessor with "reet" prefix is the most relevant
if (methodName.equalsIgnoreCase("reset" + propertyName))
return BaseRelevance + 20;
// method names that include the property name anywhere are the next
// most relevant
if (methodName.matches(".*(?i)" + propertyName + ".*"))
return BaseRelevance + 18;
// otherwise return default relevance
return 10;
}
}
private static ICPPClassType getEnclosingClassDefinition(ICEditorContentAssistInvocationContext context) {
try {
IIndex index = CCorePlugin.getIndexManager().getIndex(context.getProject());
ITranslationUnit tu = context.getTranslationUnit();
if (tu == null)
return null;
// Disable all unneeded parts of the parser.
IASTTranslationUnit astTU
= tu.getAST(
index,
ITranslationUnit.AST_SKIP_FUNCTION_BODIES
| ITranslationUnit.AST_SKIP_ALL_HEADERS
| ITranslationUnit.AST_CONFIGURE_USING_SOURCE_CONTEXT
| ITranslationUnit.AST_SKIP_TRIVIAL_EXPRESSIONS_IN_AGGREGATE_INITIALIZERS
| ITranslationUnit.AST_PARSE_INACTIVE_CODE);
if (astTU == null)
return null;
IASTNodeSelector selector = astTU.getNodeSelector(null);
// Macro expansions don't provide valid enclosing nodes. Backup until we are no longer in a
// macro expansions. A loop is needed because consecutive expansions have no valid node
// between them.
int offset = context.getInvocationOffset();
IASTNode enclosing;
do {
enclosing = selector.findEnclosingNode(offset, 0);
if (enclosing == null)
return null;
IASTFileLocation location = enclosing.getFileLocation();
if (location == null)
return null;
offset = location.getNodeOffset() - 1;
} while(offset > 0
&& !(enclosing instanceof IASTCompositeTypeSpecifier));
if (!(enclosing instanceof IASTCompositeTypeSpecifier))
return null;
IASTName name = ((IASTCompositeTypeSpecifier) enclosing).getName();
if (name == null)
return null;
IBinding binding = name.getBinding();
if (binding == null)
return null;
return (ICPPClassType) binding.getAdapter(ICPPClassType.class);
} catch(CoreException e) {
Activator.log(e);
}
return null;
}
/**
* Find and return all methods that are accessible in the class definition that encloses the argument
* invocation context. Does not return null.
*/
private static Collection<ICPPMethod> getMethods(ICEditorContentAssistInvocationContext context, IMethodAttribute methodAttribute) {
ICPPClassType cls = getEnclosingClassDefinition(context);
if (cls == null)
return Collections.emptyList();
// Return all the methods, including inherited and non-visible ones.
ICPPMethod[] methods = cls.getMethods();
List<ICPPMethod> filtered = new ArrayList<ICPPMethod>(methods.length);
for(ICPPMethod method : methods)
if (methodAttribute.keep(method))
filtered.add(method);
// TODO Choose the overload that is the best match -- closest parameter type and fewest
// parameters with default values.
return filtered;
}
private static boolean isAssignable(IType lhs, IType rhs) {
// TODO This needs a real assignment check. If the types are different by implicitly convertible
// then this should return true.
return lhs != null
&& rhs.isSameType(lhs);
}
}