1
0
Fork 0
mirror of https://github.com/eclipse-cdt/cdt synced 2025-04-29 19:45:01 +02:00

Content assistant for Qt elements

This adds content assistants for QObject::connect function calls and
Q_PROPERTY expansions.

QObject::connect function calls look like:

    QObject::connect(
        sender,   SIGNAL(someSignalFunction(int),
        receiver, SLOT(someSlotFunction(int));

The assistant provides proposals in the SIGNAL and SLOT expansions.  The
QObject for the corresponding type is used to create a list of signal or
slot function signatures.

Q_PROPERTY expansions look like:

    Q_PROPERTY( type name READ someFunction ... )

[The ... is a list of optional attributes.]  The assistant proposes
attribute names that have not yet been added.  It also proposals
appropriate values for the attribute.

This patch also adds test cases for this feature.

Change-Id: I0eb25235bb423c1cfcd743075331f90f269afea7
Signed-off-by: Andrew Eidsness <eclipse@jfront.com>
Reviewed-on: https://git.eclipse.org/r/19721
Tested-by: Hudson CI
Reviewed-by: Doug Schaefer <dschaefer@qnx.com>
IP-Clean: Doug Schaefer <dschaefer@qnx.com>
This commit is contained in:
Andrew Eidsness 2013-12-12 08:07:59 -05:00 committed by Doug Schaefer
parent c6c1ef94fc
commit ff690ab953
22 changed files with 2651 additions and 347 deletions

View file

@ -12,6 +12,8 @@ Require-Bundle: org.eclipse.core.runtime,
Bundle-RequiredExecutionEnvironment: JavaSE-1.6
Bundle-ActivationPolicy: lazy
Bundle-Localization: plugin
Export-Package: org.eclipse.cdt.qt.core,
org.eclipse.cdt.qt.core.index,
org.eclipse.cdt.internal.qt.core.index;x-friends:="org.eclipse.cdt.qt.tests"
Export-Package: org.eclipse.cdt.internal.qt.core;x-friends:="org.eclipse.cdt.qt.ui",
org.eclipse.cdt.internal.qt.core.index;x-friends:="org.eclipse.cdt.qt.tests",
org.eclipse.cdt.internal.qt.core.parser;x-friends:="org.eclipse.cdt.qt.ui",
org.eclipse.cdt.qt.core,
org.eclipse.cdt.qt.core.index

View file

@ -0,0 +1,217 @@
/*
* Copyright (c) 2013 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.core;
import java.util.Collection;
import java.util.LinkedHashSet;
import java.util.Set;
import java.util.regex.Pattern;
import org.eclipse.cdt.core.dom.ast.IASTDeclaration;
import org.eclipse.cdt.core.dom.ast.IASTExpression;
import org.eclipse.cdt.core.dom.ast.IASTFunctionCallExpression;
import org.eclipse.cdt.core.dom.ast.IASTIdExpression;
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.IASTTranslationUnit;
import org.eclipse.cdt.core.dom.ast.IBinding;
import org.eclipse.cdt.core.dom.ast.IType;
import org.eclipse.cdt.core.dom.ast.cpp.ICPPASTCompositeTypeSpecifier;
import org.eclipse.cdt.core.dom.ast.cpp.ICPPASTExpression;
import org.eclipse.cdt.core.dom.ast.cpp.ICPPASTFieldReference;
import org.eclipse.cdt.core.dom.ast.cpp.ICPPASTInitializerClause;
import org.eclipse.cdt.core.dom.ast.cpp.ICPPASTVisibilityLabel;
import org.eclipse.cdt.core.dom.ast.cpp.ICPPClassType;
import org.eclipse.cdt.core.dom.ast.cpp.ICPPMethod;
import org.eclipse.cdt.core.model.ICProject;
import org.eclipse.cdt.core.model.ITranslationUnit;
import org.eclipse.cdt.internal.core.dom.parser.ITypeContainer;
import org.eclipse.cdt.internal.core.dom.parser.cpp.ICPPEvaluation;
import org.eclipse.cdt.internal.core.dom.parser.cpp.ICPPInternalBinding;
import org.eclipse.cdt.qt.core.index.IQMethod;
import org.eclipse.cdt.qt.core.index.IQObject;
import org.eclipse.core.resources.IProject;
@SuppressWarnings("restriction")
public class ASTUtil {
/**
* A convenience method to find the project that contains the given node. Returns null if
* the project cannot be found.
*/
public static IProject getProject(IASTNode node) {
IASTTranslationUnit astTU = node.getTranslationUnit();
if (astTU == null)
return null;
ITranslationUnit tu = astTU.getOriginatingTranslationUnit();
if (tu == null)
return null;
ICProject cProject = tu.getCProject();
if (cProject == null)
return null;
return cProject.getProject();
}
// NOTE: This expression allows embedded line terminators (?s) for cases where the code looks like:
// QObject::connect( &a, SIGNAL(
// sig1(
// int
// ), ...
// The two patterns are nearly identical. The difference is because the first is for matching SIGNAL/
// SLOT expansions. The second is for matching the argument to that expansion.
public static final Pattern Regex_SignalSlotExpansion = Pattern.compile("(?s)(SIGNAL|SLOT)\\s*\\(\\s*(.*?)\\s*\\)\\s*");
public static final Pattern Regex_FunctionCall = Pattern.compile("(?s)\\s*(.*)\\s*\\(\\s*(.*?)\\s*\\)\\s*");
public static IType getBaseType(IType type) {
while (type instanceof ITypeContainer)
type = ((ITypeContainer) type).getType();
return type;
}
public static IType getBaseType(IASTNode node) {
if (node instanceof IASTIdExpression)
return getBaseType((IASTIdExpression) node);
if (node instanceof IASTFunctionCallExpression)
return getReceiverType((IASTFunctionCallExpression) node);
if (node instanceof IASTExpression)
return getBaseType(((IASTExpression) node).getExpressionType());
return null;
}
public static IType getBaseType(IASTInitializerClause init) {
if (!(init instanceof ICPPASTInitializerClause))
return null;
ICPPASTInitializerClause cppInit = (ICPPASTInitializerClause) init;
ICPPEvaluation eval = cppInit.getEvaluation();
return eval == null ? null : getBaseType(eval.getTypeOrFunctionSet(cppInit));
}
public static ICPPClassType getReceiverType(IASTFunctionCallExpression fncall) {
// NOTE: This cannot rely on the Evaluation because we're in a contest assist context.
// At this point is likely that the full function call is not complete, so at least
// some of the eval leads to a Problem. We don't need the Eval anyhow, just lookup
// the type of the receiver.
IASTExpression fnName = fncall.getFunctionNameExpression();
if (!(fnName instanceof ICPPASTFieldReference))
return null;
ICPPASTFieldReference fieldRef = (ICPPASTFieldReference) fnName;
ICPPASTExpression receiver = fieldRef.getFieldOwner();
IType recvType = getBaseType(receiver);
return recvType instanceof ICPPClassType ? (ICPPClassType) recvType : null;
}
/**
* Does not return null.
*/
public static Collection<IQMethod> findMethods(IQObject qobj, QtSignalSlotReference ref) {
Set<IQMethod> bindings = new LinkedHashSet<IQMethod>();
Iterable<IQMethod> methods = null;
switch(ref.type)
{
case Signal:
methods = qobj.getSignals().withoutOverrides();
break;
case Slot:
methods = qobj.getSlots().withoutOverrides();
break;
}
if (methods != null) {
String qtNormalizedSig = QtMethodUtil.getQtNormalizedMethodSignature(ref.signature);
if (qtNormalizedSig == null)
return bindings;
for (IQMethod method : methods)
for(String signature : method.getSignatures())
if (signature.equals(qtNormalizedSig))
bindings.add(method);
}
return bindings;
}
public static IBinding resolveFunctionBinding(IASTFunctionCallExpression fnCall) {
IASTName fnName = null;
IASTExpression fnNameExpr = fnCall.getFunctionNameExpression();
if (fnNameExpr instanceof IASTIdExpression)
fnName = ((IASTIdExpression) fnNameExpr).getName();
else if (fnNameExpr instanceof ICPPASTFieldReference)
fnName = ((ICPPASTFieldReference) fnNameExpr).getFieldName();
return fnName == null ? null : fnName.resolveBinding();
}
public static ICPPASTVisibilityLabel findVisibilityLabel(ICPPMethod method, IASTNode ast) {
// the visibility cannot be found without an ast
if (ast == null)
return null;
// We need to get the CompTypeSpec in order to see the token that created the method's
// visibility specifier. The ast parameter will be either the method definition or a
// declaration. If it happens to be a declaration, then the CompTypeSpec is a parent of
// the AST and it can be accessed through public API. However, if the ast parameter happens
// to be a definition, then there isn't any public API (that I've found) to get to the
// CompTypeSpec. Instead, we cheat and use the InternalBinding.
MethodSpec methodSpec = new MethodSpec(ast);
if (methodSpec.clsSpec == null
&& method instanceof ICPPInternalBinding)
{
ICPPInternalBinding internalBinding = (ICPPInternalBinding) method;
IASTNode[] decls = internalBinding.getDeclarations();
for (int i = 0; methodSpec.clsSpec == null && i < decls.length; ++i)
methodSpec = new MethodSpec(decls[i]);
}
if(methodSpec.clsSpec == null)
return null;
ICPPASTVisibilityLabel lastLabel = null;
for (IASTDeclaration decl : methodSpec.clsSpec.getMembers()) {
if (decl instanceof ICPPASTVisibilityLabel)
lastLabel = (ICPPASTVisibilityLabel) decl;
else if (decl == methodSpec.methodDecl)
return lastLabel;
}
return null;
}
private static class MethodSpec
{
public final ICPPASTCompositeTypeSpecifier clsSpec;
public final IASTNode methodDecl;
public MethodSpec( IASTNode node )
{
ICPPASTCompositeTypeSpecifier cls = null;
IASTNode mth = node;
while( mth != null && cls == null )
{
IASTNode parent = mth.getParent();
if (parent instanceof ICPPASTCompositeTypeSpecifier)
cls = (ICPPASTCompositeTypeSpecifier) parent;
else
mth = parent;
}
clsSpec = cls;
methodDecl = mth;
}
}
}

View file

@ -0,0 +1,132 @@
/*
* Copyright (c) 2013 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.core;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.eclipse.cdt.core.dom.ast.IASTCompletionContext;
import org.eclipse.cdt.core.dom.ast.IASTFunctionCallExpression;
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.IBinding;
import org.eclipse.cdt.qt.core.QtKeywords;
/**
* Utility for managing interaction with QObject::connect and QObject::disconnect function
* calls. These function calls can contain two expansions. The first is always SIGNAL,
* the second is either SIGNAL (which will cause the second signal to be emitted when the
* first is received) or SLOT (which will cause the slot to be evaluate when the signal
* is received). This class follows the Qt convention of calling the first SIGNAL expansion
* the sender and the second (which could be SIGNAL or SLOT) the receiver.
*
* In the following examples, the type of the signal is the type of the q_sender variable.
* The type of the method is the type of the q_receiver variable. The variable q_unrelated is
* some instance that is not needed for either case.
* <pre>
* QObject::connect( q_sender, SIGNAL(destroyed()), q_receiver, SIGNAL() );
* QObject::connect( q_sender, SIGNAL(destroyed()), q_receiver, SLOT(deleteLater()) );
* QObject::connect( q_sender, SIGNAL(destroyed()), q_receiver, SIGNAL(), Qt::AutoConnection );
* QObject::connect( q_sender, SIGNAL(destroyed()), q_receiver, SLOT(deleteLater()), Qt::AutoConnection );
* q_unrelated->connect( q_sender, SIGNAL(destroyed()), q_receiver, SIGNAL() );
* q_unrelated->connect( q_sender, SIGNAL(destroyed()), q_receiver, SLOT(deleteLater()) );
* q_unrelated->connect( q_sender, SIGNAL(destroyed()), q_receiver, SIGNAL(), Qt::AutoConnection );
* q_unrelated->connect( q_sender, SIGNAL(destroyed()), q_receiver, SLOT(deleteLater()), Qt::AutoConnection );
*
* q_receiver->connect( q_sender, SIGNAL(destroyed()), SIGNAL() );
* q_receiver->connect( q_sender, SIGNAL(destroyed()), SLOT() );
* q_receiver->connect( q_sender, SIGNAL(destroyed()), SIGNAL(), Qt::AutoConnection );
* q_receiver->connect( q_sender, SIGNAL(destroyed()), SLOT(), Qt::AutoConnection );
*
* QObject::disconnect( q_sender, SIGNAL(), q_receiver, SIGNAL() );
* QObject::disconnect( q_sender, SIGNAL(), q_receiver, SLOT() );
* q_unrelated->disconnect( q_sender, SIGNAL(), q_receiver, SIGNAL() );
* q_unrelated->disconnect( q_sender, SIGNAL(), q_receiver, SLOT() );
*
* q_sender->disconnect( SIGNAL(), q_receiver, SIGNAL() );
* q_sender->disconnect( SIGNAL(), q_receiver, SLOT() );
* q_sender->disconnect( SIGNAL(), q_receiver );
* q_sender->disconnect( SIGNAL() );
* q_sender->disconnect();
* </pre>
*/
public class QtFunctionCallUtil {
private static final Pattern SignalRegex = Pattern.compile("^\\s*" + QtKeywords.SIGNAL + ".*");
private static final Pattern MethodRegex = Pattern.compile("^\\s*(?:" + QtKeywords.SIGNAL + '|' + QtKeywords.SLOT + ").*");
/**
* Return true if the specified name is a QObject::connect or QObject::disconnect function
* and false otherwise.
*/
public static boolean isQObjectFunctionCall(IASTCompletionContext astContext, boolean isPrefix, IASTName name) {
if (name == null)
return false;
// Bug332201: Qt content assist should always be applied to the most specific part of
// the target name.
IBinding[] funcBindings = astContext.findBindings(name.getLastName(), isPrefix);
for (IBinding funcBinding : funcBindings)
if (QtKeywords.is_QObject_connect(funcBinding)
|| QtKeywords.is_QObject_disconnect(funcBinding))
return true;
return false;
}
/**
* Returns true if the given function call argument is a SIGNAL or SLOT expansion
* and false otherwise.
public static boolean isQtMethodExpansion(IASTInitializerClause arg) {
return MethodRegex.matcher(arg.getRawSignature()).matches();
}
*/
/**
* If the given argument is a SIGNAL or SLOT expansion then find and return the node in the AST
* that will be used for this method. Returns null if the argument is not a Qt method call or
* if the associated node cannot be found.
*/
public static IASTNode getTypeNode(IASTFunctionCallExpression call, IASTInitializerClause[] args, int argIndex) {
int sigExpIndex = getExpansionArgIndex(args, 0, SignalRegex);
if (argIndex == sigExpIndex)
return getSignalTargetNode(sigExpIndex, call, args);
int methodExpIndex = getExpansionArgIndex(args, sigExpIndex + 1, MethodRegex);
if (argIndex == methodExpIndex)
return getMethodTargetNode(methodExpIndex, sigExpIndex, call, args);
// Otherwise the given argument is not a SIGNAL or SLOT expansion.
return null;
}
private static IASTNode getSignalTargetNode(int sigExpIndex, IASTFunctionCallExpression call, IASTInitializerClause[] args) {
// When the SIGNAL expansion is first, the type is based on the receiver of
// the function call. Otherwise the type is the previous argument.
return sigExpIndex == 0 ? call : args[sigExpIndex - 1];
}
private static IASTNode getMethodTargetNode(int methodExpIndex, int sigExpIndex, IASTFunctionCallExpression call, IASTInitializerClause[] args) {
// If the method is right after the signal, then the type is based on the receiver
// of the function call. Otherwise the method type is based on the parameter right
// before the expansion.
return (methodExpIndex == (sigExpIndex + 1)) ? call : args[methodExpIndex - 1];
}
private static int getExpansionArgIndex(IASTInitializerClause[] args, int begin, Pattern macroNameRegex) {
for(int i = begin; i < args.length; ++i) {
IASTInitializerClause arg = args[i];
String raw = arg.getRawSignature();
Matcher m = macroNameRegex.matcher(raw);
if (m.matches())
return i;
}
return -1;
}
}

View file

@ -0,0 +1,108 @@
/*
* Copyright (c) 2013 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.core;
import java.util.regex.Matcher;
import org.eclipse.cdt.core.dom.ast.IASTName;
import org.eclipse.cdt.core.dom.ast.IASTNode;
import org.eclipse.cdt.core.dom.ast.IBinding;
import org.eclipse.cdt.qt.core.QtKeywords;
public class QtSignalSlotReference
{
public static enum Type
{
Signal( "sender", "SIGNAL", "signal" ),
Slot ( "receiver", "SLOT", "slot" );
public final String roleName;
public final String macroName;
public final String paramName;
public boolean matches( Type other )
{
if( other == null )
return false;
// The signal parameter must be a SIGNAL, but the slot could be a
// SLOT or a SIGNAL.
return this == Signal ? other == Signal : true;
}
private Type( String roleName, String macroName, String paramName )
{
this.roleName = roleName;
this.macroName = macroName;
this.paramName = paramName;
}
}
public final IASTNode parent;
public final IASTNode node;
public final Type type;
public final String signature;
public final int offset;
public final int length;
private QtSignalSlotReference( IASTNode parent, IASTNode node, Type type, String signature, int offset, int length )
{
this.parent = parent;
this.node = node;
this.type = type;
this.signature = signature;
this.offset = offset;
this.length = length;
}
public IASTName createName( IBinding binding )
{
return new QtSignalSlotReferenceName( parent, node, signature, offset, length, binding );
}
public static QtSignalSlotReference parse( IASTNode parent, IASTNode arg )
{
// This check will miss cases like:
// #define MY_SIG1 SIGNAL
// #define MY_SIG2(s) SIGNAL(s)
// #define MY_SIG3(s) SIGNAL(signal())
// connect( &a, MY_SIG1(signal()), ...
// connect( &a, MY_SIG2(signal()), ...
// connect( &a, MY_SIG2, ...
// This could be improved by adding tests when arg represents a macro expansion. However, I'm
// not sure if we would be able to follow the more complicated case of macros that call functions
// that use the SIGNAL macro. For now I've implemented the simpler check of forcing the call to
// use the SIGNAL/SLOT macro directly.
String raw = arg.getRawSignature();
Matcher m = ASTUtil.Regex_SignalSlotExpansion.matcher( raw );
if( ! m.matches() )
return null;
Type type;
String macroName = m.group( 1 );
if( QtKeywords.SIGNAL.equals( macroName ) )
type = Type.Signal;
else if( QtKeywords.SLOT.equals( macroName ) )
type = Type.Slot;
else
return null;
// Get the argument to the SIGNAL/SLOT macro and the offset/length of that argument within the
// complete function argument. E.g., with this argument to QObject::connect
// SIGNAL( signal(int) )
// the values are
// expansionArgs: "signal(int)"
// expansionOffset: 8
// expansionLength: 11
String expansionArgs = m.group( 2 );
int expansionOffset = m.start( 2 );
int expansionLength = m.end( 2 ) - expansionOffset;
return new QtSignalSlotReference( parent, arg, type, expansionArgs, expansionOffset, expansionLength );
}
}

View file

@ -0,0 +1,82 @@
/*
* Copyright (c) 2013 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.core;
import org.eclipse.cdt.core.dom.ast.IASTFileLocation;
import org.eclipse.cdt.core.dom.ast.IASTImageLocation;
import org.eclipse.cdt.core.dom.ast.IASTPreprocessorIncludeStatement;
/**
* The location of the signal/slot reference is stored as the location of the parent
* macro expansion + an offset, which is the number of characters between the start
* of the expansion and the start of the argument (including whitespace). E.g. in,
*
* <pre>
* SIGNAL( signal1( int ) )
* ^ ^ ^ c: end of reference name
* | +------------- b: start of reference name
* +--------------------- a: start of macro expansion
* </pre>
*
* The offset is b - a and length is c - a. This means that the result of 'Find
* References' will highlight just "signal( int )".
*
* @see QtSignalSlotReferenceName
*/
public class QtSignalSlotReferenceLocation implements IASTImageLocation {
private final IASTFileLocation referenceLocation;
private final int offset;
private final int length;
public QtSignalSlotReferenceLocation(IASTFileLocation referenceLocation, int offset, int length) {
this.referenceLocation = referenceLocation;
this.offset = offset;
this.length = length;
}
@Override
public int getLocationKind() {
return IASTImageLocation.ARGUMENT_TO_MACRO_EXPANSION;
}
@Override
public int getNodeOffset() {
return referenceLocation.getNodeOffset() + offset;
}
@Override
public int getNodeLength() {
return length;
}
@Override
public String getFileName() {
return referenceLocation.getFileName();
}
@Override
public IASTFileLocation asFileLocation() {
return referenceLocation;
}
@Override
public int getEndingLineNumber() {
return referenceLocation.getEndingLineNumber();
}
@Override
public int getStartingLineNumber() {
return referenceLocation.getStartingLineNumber();
}
@Override
public IASTPreprocessorIncludeStatement getContextInclusionStatement() {
return referenceLocation.getContextInclusionStatement();
}
}

View file

@ -0,0 +1,287 @@
/*
* Copyright (c) 2013 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.core;
import org.eclipse.cdt.core.dom.ILinkage;
import org.eclipse.cdt.core.dom.ast.ASTNodeProperty;
import org.eclipse.cdt.core.dom.ast.ASTVisitor;
import org.eclipse.cdt.core.dom.ast.ExpansionOverlapsBoundaryException;
import org.eclipse.cdt.core.dom.ast.IASTCompletionContext;
import org.eclipse.cdt.core.dom.ast.IASTFileLocation;
import org.eclipse.cdt.core.dom.ast.IASTImageLocation;
import org.eclipse.cdt.core.dom.ast.IASTName;
import org.eclipse.cdt.core.dom.ast.IASTNameOwner;
import org.eclipse.cdt.core.dom.ast.IASTNode;
import org.eclipse.cdt.core.dom.ast.IASTNodeLocation;
import org.eclipse.cdt.core.dom.ast.IASTTranslationUnit;
import org.eclipse.cdt.core.dom.ast.IBinding;
import org.eclipse.cdt.core.parser.IToken;
/**
* Signals are connected to slots by referencing them within an expansion of SIGNAL
* or SLOT. E.g.,
*
* <pre>
* class A : public QObject
* {
* Q_SIGNAL void signal1( int );
* Q_SLOT void slot1();
* };
* A a;
* QObject::connect( &a, SIGNAL( signal1( int ) ), &a, SLOT( slot1() ) );
* </pre>
*
* The goal is for 'Find References' on the function declarations to find the references
* in the macro expansions. The PDOM stores references as a linked list from the binding
* for the function.
*
* This class represents the name within the expansion, i.e., "signal1( int )" within
* "SIGNAL( signal1( int ) )" and "slot1()" within "SLOT( slot1() )".
*/
public class QtSignalSlotReferenceName implements IASTName {
private final IASTNode referenceNode;
private final String argument;
private final IBinding binding;
private final IASTImageLocation location;
private IASTNode parent;
private ASTNodeProperty propertyInParent;
public QtSignalSlotReferenceName(IASTNode parent, IASTNode referenceNode, String argument, int offset, int length, IBinding binding) {
this.parent = parent;
this.referenceNode = referenceNode;
this.argument = argument;
this.binding = binding;
IASTFileLocation referenceLocation = referenceNode.getFileLocation();
this.location
= referenceLocation == null
? null
: new QtSignalSlotReferenceLocation(referenceLocation, offset, length);
}
@Override
public char[] toCharArray() {
return argument.toCharArray();
}
@Override
public char[] getSimpleID() {
return toCharArray();
}
@Override
public char[] getLookupKey() {
return toCharArray();
}
@Override
public IASTTranslationUnit getTranslationUnit() {
return referenceNode.getTranslationUnit();
}
@Override
public IASTFileLocation getFileLocation() {
return getImageLocation();
}
@Override
public IASTNodeLocation[] getNodeLocations() {
// The javadoc says that locations that are completely enclosed within a
// macro expansion return only the location of that expansion.
return referenceNode.getNodeLocations();
}
@Override
public String getContainingFilename() {
return referenceNode.getContainingFilename();
}
@Override
public boolean isPartOfTranslationUnitFile() {
return referenceNode.isPartOfTranslationUnitFile();
}
@Override
public IASTNode[] getChildren() {
return new IASTNode[0];
}
@Override
public IASTNode getParent() {
return parent;
}
@Override
public void setParent(IASTNode node) {
this.parent = node;
}
@Override
public ASTNodeProperty getPropertyInParent() {
return propertyInParent;
}
@Override
public void setPropertyInParent(ASTNodeProperty property) {
propertyInParent = property;
}
@Override
public boolean accept(ASTVisitor visitor) {
// The signal/slot reference has nothing to visit. It will have been
// reached by the reference node, so we can't visit that, and there is
// nothing else.
return false;
}
@Override
public String getRawSignature() {
// The raw signature of the reference is the text of the argument.
return argument;
}
@Override
public boolean contains(IASTNode node) {
// There aren't any nodes contained within the signal/slot reference.
return false;
}
@Override
public IToken getLeadingSyntax() throws ExpansionOverlapsBoundaryException, UnsupportedOperationException {
// The parent is the macro reference name, and this is the entire
// content of the arguments. Since there is nothing between these, there
// will not be any leading syntax.
return null;
}
@Override
public IToken getTrailingSyntax() throws ExpansionOverlapsBoundaryException, UnsupportedOperationException {
// The parent is the macro reference name, and this is the entire
// content of the arguments. Since there is nothing between these, there
// will not be any leading syntax.
return null;
}
@Override
public IToken getSyntax() throws ExpansionOverlapsBoundaryException {
// This reference to the signal/slot function is fully contained within
// a preprocessor node, which does not support syntax.
throw new UnsupportedOperationException();
}
@Override
public boolean isFrozen() {
return referenceNode.isFrozen();
}
@Override
public boolean isActive() {
return referenceNode.isActive();
}
@Override
public int getRoleOfName(boolean allowResolution) {
return IASTNameOwner.r_reference;
}
@Override
public boolean isDeclaration() {
return false;
}
@Override
public boolean isReference() {
return true;
}
@Override
public boolean isDefinition() {
return false;
}
@Override
public IBinding getBinding() {
return binding;
}
@Override
public IBinding resolveBinding() {
return getBinding();
}
@Override
public IASTCompletionContext getCompletionContext() {
// Signal/slot references are fully contained within a macro expansion,
// so there is no completion context.
return null;
}
@Override
public ILinkage getLinkage() {
return referenceNode instanceof IASTName ? ((IASTName) referenceNode).getLinkage() : null;
}
@Override
public IASTImageLocation getImageLocation() {
return location;
}
@Override
public IASTName getLastName() {
// Signal/slot references are not qualified, so return itself.
return this;
}
@Override
public boolean isQualified() {
return false;
}
@Override
public IASTName copy() {
// Signal/slot references are preprocessor nodes, so they don't support
// copying.
throw new UnsupportedOperationException();
}
@Override
public IASTName copy(CopyStyle style) {
// Signal/slot references are preprocessor nodes, so they don't support
// copying.
throw new UnsupportedOperationException();
}
@Override
public IASTNode getOriginalNode() {
return this;
}
@Override
public void setBinding(IBinding binding) {
// Signal/slot references find their binding on instantiation, they
// never allow it to be replaced.
throw new UnsupportedOperationException();
}
@Override
public IBinding getPreBinding() {
return getBinding();
}
@Override
public IBinding resolvePreBinding() {
return getBinding();
}
@Override
public String toString() {
return "QtSignalSlotReference(" + new String(toCharArray()) + ')';
}
}

View file

@ -48,6 +48,9 @@ public class QObject implements IQObject {
for(QtPDOMQObject base : pdomQObject.findBases()) {
QObject baseQObj = new QObject(qtIndex, cdtIndex, base);
this.bases.add(baseQObj);
baseSlots.addAll(baseQObj.getSlots().all());
baseSignals.addAll(baseQObj.getSignals().all());
baseInvokables.addAll(baseQObj.getInvokables().all());
baseProps.addAll(baseQObj.getProperties().all());
}

View file

@ -8,6 +8,10 @@
package org.eclipse.cdt.qt.core;
import org.eclipse.cdt.core.dom.ast.DOMException;
import org.eclipse.cdt.core.dom.ast.IBinding;
import org.eclipse.cdt.core.dom.ast.cpp.ICPPFunction;
/**
* Declares constants related to tokens that are special in Qt applications.
*/
@ -36,4 +40,50 @@ public class QtKeywords {
public static final String SIGNALS = "signals";
public static final String SLOT = "SLOT";
public static final String SLOTS = "slots";
/**
* Returns true if the argument binding is for the QObject::connect function
* and false otherwise.
*/
public static boolean is_QObject_connect(IBinding binding) {
if (binding == null)
return false;
// IBinding#getAdapter returns null when binding is an instance of
// PDOMCPPMethod.
if (!(binding instanceof ICPPFunction))
return false;
try {
String[] qualName = ((ICPPFunction) binding).getQualifiedName();
return qualName.length == 2
&& QOBJECT.equals(qualName[0])
&& CONNECT.equals(qualName[1]);
} catch (DOMException e) {
return false;
}
}
/**
* Returns true if the argument binding is for the QObject::disconnect function
* and false otherwise.
*/
public static boolean is_QObject_disconnect(IBinding binding) {
if (binding == null)
return false;
// IBinding#getAdapter returns null when binding is an instance of
// PDOMCPPMethod.
if (!(binding instanceof ICPPFunction))
return false;
try {
String[] qualName = ((ICPPFunction) binding).getQualifiedName();
return qualName.length == 2
&& QOBJECT.equals(qualName[0])
&& DISCONNECT.equals(qualName[1]);
} catch (DOMException e) {
return false;
}
}
}

View file

@ -31,8 +31,7 @@
<artifactId>tycho-surefire-plugin</artifactId>
<version>${tycho-version}</version>
<configuration>
<useUIHarness>false</useUIHarness>
<!-- Core tests actually use eclipse.ui classes, see CProjectHelper -->
<useUIHarness>true</useUIHarness>
<argLine>${base.ui.test.vmargs} -ea -Xms256m -Xmx512m -XX:MaxPermSize=256M</argLine>
<includes>
<include>**/AllQtTests.*</include>

View file

@ -15,8 +15,9 @@ public class AllQtTests extends TestSuite {
public static Test suite() throws Exception {
return
new TestSuite(
SimpleTests.class,
QMakeTests.class,
QObjectTests.class,
QtContentAssistantTests.class,
QtIndexTests.class,
QtRegressionTests.class);
}

View file

@ -18,7 +18,7 @@ import org.eclipse.cdt.internal.qt.core.index.QMakeInfo;
import org.eclipse.cdt.internal.qt.core.index.QMakeParser;
import org.eclipse.cdt.internal.qt.core.index.QMakeVersion;
public class SimpleTests extends TestCase {
public class QMakeTests extends TestCase {
public void testQMakeVersion() throws Exception {
// Make sure null is returned for invalid version strings.

View file

@ -0,0 +1,188 @@
/*
* Copyright (c) 2013 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.qt.tests;
import junit.framework.TestCase;
import org.eclipse.cdt.core.dom.ast.IASTCompletionNode;
import org.eclipse.cdt.core.model.ICProject;
import org.eclipse.cdt.core.model.ITranslationUnit;
import org.eclipse.cdt.internal.qt.ui.assist.QPropertyExpansion;
import org.eclipse.cdt.ui.text.contentassist.ICEditorContentAssistInvocationContext;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.Document;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.ITextViewer;
import org.eclipse.ui.IEditorPart;
public class QtContentAssistantTests extends TestCase {
public void testQPropertyProposals() throws Exception {
String decl = "Q_PROPERTY( type name READ accessor WRITE modifier )";
int atEnd = decl.length();
int atRParen = decl.indexOf(')');
int afterModifier = decl.indexOf("modifier") + "modifier".length();
int afterWRITE = decl.indexOf("WRITE") + "WRITE".length();
int inModifier = decl.indexOf("modifier") + 3;
int inWRITE = decl.indexOf("WRITE") + 3;
IDocument doc = new Document(decl);
// The expansion is not applicable when invoked after the closing paren
QPropertyExpansion exp = QPropertyExpansion.create(new Context(doc, atEnd));
assertNull(exp);
exp = QPropertyExpansion.create(new Context(doc, atRParen));
assertNotNull(exp);
assertNull(exp.getCurrIdentifier());
assertEquals("modifier", exp.getPrevIdentifier());
exp = QPropertyExpansion.create(new Context(doc, afterModifier));
assertNotNull(exp);
assertEquals("modifier", exp.getCurrIdentifier());
assertEquals("WRITE", exp.getPrevIdentifier());
exp = QPropertyExpansion.create(new Context(doc, afterWRITE));
assertNotNull(exp);
assertEquals("WRITE", exp.getCurrIdentifier());
assertEquals("accessor", exp.getPrevIdentifier());
exp = QPropertyExpansion.create(new Context(doc, inModifier));
assertNotNull(exp);
assertEquals("modifier", exp.getCurrIdentifier());
assertEquals("WRITE", exp.getPrevIdentifier());
exp = QPropertyExpansion.create(new Context(doc, inWRITE));
assertNotNull(exp);
assertEquals("WRITE", exp.getCurrIdentifier());
assertEquals("accessor", exp.getPrevIdentifier());
}
public void testQPropertyWithoutLeadingWhitespace() throws Exception {
String decl = "Q_PROPERTY(type name READ accessor )";
int atRParen = decl.indexOf(')');
IDocument doc = new Document(decl);
// The expansion should be created even when there is no leading whitesapce in the
// expansion parameter.
QPropertyExpansion exp = QPropertyExpansion.create(new Context(doc, atRParen));
assertNotNull(exp);
}
public void testQPropertyPrefixes() throws Exception {
String decl = "Q_PROPERTY( type name READ accessor WRITE )";
int len = decl.length();
IDocument doc = new Document(decl);
// The expansion is not applicable when invoked after the closing paren
QPropertyExpansion atEnd = QPropertyExpansion.create(new Context(doc, len));
assertNull(atEnd);
QPropertyExpansion inWS = QPropertyExpansion.create(new Context(doc, len - 2));
assertNotNull(inWS);
assertNull(inWS.getPrefix());
assertNull(inWS.getCurrIdentifier());
assertEquals("WRITE", inWS.getPrevIdentifier());
QPropertyExpansion afterWRITE = QPropertyExpansion.create(new Context(doc, len - 3));
assertNotNull(afterWRITE);
assertEquals("WRITE", afterWRITE.getPrefix());
assertEquals("WRITE", afterWRITE.getCurrIdentifier());
assertEquals("accessor", afterWRITE.getPrevIdentifier());
QPropertyExpansion inWRITE_e = QPropertyExpansion.create(new Context(doc, len - 4));
assertNotNull(inWRITE_e);
assertEquals("WRIT", inWRITE_e.getPrefix());
assertEquals("WRITE", inWRITE_e.getCurrIdentifier());
assertEquals("accessor", inWRITE_e.getPrevIdentifier());
QPropertyExpansion inWRITE_b = QPropertyExpansion.create(new Context(doc, len - 6));
assertNotNull(inWRITE_b);
assertEquals("WR", inWRITE_b.getPrefix());
assertEquals("WRITE", inWRITE_b.getCurrIdentifier());
assertEquals("accessor", inWRITE_b.getPrevIdentifier());
QPropertyExpansion startWRITE = QPropertyExpansion.create(new Context(doc, len - 8));
assertNotNull(startWRITE);
assertNull(startWRITE.getPrefix());
assertNull(startWRITE.getCurrIdentifier());
assertEquals("accessor", startWRITE.getPrevIdentifier());
}
// This implements only the parts that are known to be used in the QPropertyExpansion
// implementation.
private static class Context implements ICEditorContentAssistInvocationContext {
private final IDocument doc;
private final int contextOffset;
private final int invokedOffset;
public Context(IDocument doc, int invoked) {
this.doc = doc;
this.contextOffset = doc.get().indexOf('(') + 1;
this.invokedOffset = invoked;
}
@Override
public int getInvocationOffset() {
return invokedOffset;
}
@Override
public int getContextInformationOffset() {
return contextOffset;
}
@Override
public IDocument getDocument() {
return doc;
}
@Override
public boolean isContextInformationStyle() {
return false;
}
@Override
public ITextViewer getViewer() {
return null;
}
@Override
public ITranslationUnit getTranslationUnit() {
return null;
}
@Override
public ICProject getProject() {
return null;
}
@Override
public int getParseOffset() {
return 0;
}
@Override
public IEditorPart getEditor() {
return null;
}
@Override
public IASTCompletionNode getCompletionNode() {
return null;
}
@Override
public CharSequence computeIdentifierPrefix() throws BadLocationException {
return null;
}
};
}

View file

@ -12,6 +12,10 @@ Require-Bundle: org.eclipse.ui,
org.eclipse.cdt.core,
org.eclipse.cdt.qt.core;bundle-version="[1.1.0,2.0.0)",
org.eclipse.jface.text,
org.eclipse.core.resources
org.eclipse.core.resources,
org.eclipse.ui.workbench.texteditor,
org.eclipse.ui.editors
Bundle-RequiredExecutionEnvironment: JavaSE-1.6
Bundle-ActivationPolicy: lazy
Export-Package: org.eclipse.cdt.internal.qt.ui.assist;x-friends:="org.eclipse.cdt.qt.tests",
org.eclipse.cdt.qt.ui

View file

@ -0,0 +1,326 @@
/*
* Copyright (c) 2013 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;
import java.util.ArrayList;
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.IASTCompletionContext;
import org.eclipse.cdt.core.dom.ast.IASTExpression;
import org.eclipse.cdt.core.dom.ast.IASTFunctionCallExpression;
import org.eclipse.cdt.core.dom.ast.IASTIdExpression;
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.IBinding;
import org.eclipse.cdt.core.dom.ast.IType;
import org.eclipse.cdt.core.dom.ast.cpp.ICPPASTFieldReference;
import org.eclipse.cdt.core.dom.ast.cpp.ICPPClassType;
import org.eclipse.cdt.internal.qt.core.ASTUtil;
import org.eclipse.cdt.internal.qt.core.QtFunctionCallUtil;
import org.eclipse.cdt.internal.ui.text.contentassist.CCompletionProposal;
import org.eclipse.cdt.internal.ui.text.contentassist.RelevanceConstants;
import org.eclipse.cdt.qt.core.QtKeywords;
import org.eclipse.cdt.qt.core.index.IQMethod;
import org.eclipse.cdt.qt.core.index.IQObject;
import org.eclipse.cdt.qt.core.index.QtIndex;
import org.eclipse.cdt.qt.ui.QtUIPlugin;
import org.eclipse.cdt.ui.text.contentassist.ICEditorContentAssistInvocationContext;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.contentassist.ICompletionProposal;
@SuppressWarnings("restriction")
public class QObjectConnectCompletion {
// These suggestions are populated from the index, so the case is always an exact match.
// Secondly, these suggestions should appear above generic variable and method matches, since
// have based the calculation on the exact function that is being called.
private static final int MACRO_RELEVANCE
= RelevanceConstants.CASE_MATCH_RELEVANCE + RelevanceConstants.LOCAL_VARIABLE_TYPE_RELEVANCE + 2;
private static final int MACRO_PARAM_RELEVANCE
= RelevanceConstants.CASE_MATCH_RELEVANCE + RelevanceConstants.METHOD_TYPE_RELEVANCE + 1;
/**
* Different suggestions should be proposed for each parameter of the QObject::connect
* function call. The 'sender' parameter should suggest SIGNAL, but 'member' can be
* either SLOT or SIGNAL.
*/
public enum Param {
Signal,
Member,
Generic
}
private final Param param;
private final Data data;
public QObjectConnectCompletion(Param param) {
this.param = param;
this.data = null;
}
public QObjectConnectCompletion(String replacement) {
this.param = Param.Generic;
this.data = new Data(replacement);
}
/**
* The data used to produce the completions varies depending on the role of the
* parameter that is being completed.
*/
private static class Data
{
public final String replacement;
public final String display;
public final int cursorOffset;
public static final Data SIGNAL = new Data("SIGNAL()", "SIGNAL(a)", -1);
public static final Data SLOT = new Data("SLOT()", "SLOT(a)", -1);
public Data(String replacement) {
this(replacement, replacement, 0);
}
public Data(String replacement, String display, int cursorOffset) {
this.replacement = replacement;
this.display = display;
this.cursorOffset = cursorOffset;
}
public ICompletionProposal createProposal(ICEditorContentAssistInvocationContext context, int relevance) {
int repLength = replacement.length();
int repOffset = context.getInvocationOffset();
CCompletionProposal p
= new CCompletionProposal(replacement, repOffset, repLength, QtUIPlugin.getQtLogo(), display, relevance, context.getViewer());
p.setCursorPosition(repLength + cursorOffset);
return p;
}
}
private static void addProposal(Collection<ICompletionProposal> proposals, ICEditorContentAssistInvocationContext context, Data data, int relevance) {
if (data == null)
return;
ICompletionProposal proposal = data.createProposal(context, relevance);
if (proposal != null)
proposals.add(proposal);
}
private void addProposals(Collection<ICompletionProposal> proposals, ICEditorContentAssistInvocationContext context) {
if (data != null)
addProposal(proposals, context, data, MACRO_PARAM_RELEVANCE);
else
switch(param) {
case Signal:
addProposal(proposals, context, Data.SIGNAL, MACRO_RELEVANCE);
break;
case Member:
addProposal(proposals, context, Data.SLOT, MACRO_RELEVANCE);
addProposal(proposals, context, Data.SIGNAL, MACRO_RELEVANCE - 1);
break;
default:
break;
}
}
private static boolean is_QObject_connect(ICEditorContentAssistInvocationContext context, IASTCompletionContext astContext, IASTName name) {
// Bug332201: Qt content assist should always be applied to the most specific part of
// the target name.
IBinding[] funcBindings = astContext.findBindings(name.getLastName(), !context.isContextInformationStyle());
for (IBinding funcBinding : funcBindings)
if (QtKeywords.is_QObject_connect(funcBinding))
return true;
return false;
}
// Copied from org.eclipse.cdt.internal.ui.text.CParameterListValidator
private static int indexOfClosingPeer(String code, char left, char right, int pos) {
int level = 0;
final int length = code.length();
while (pos < length) {
char ch = code.charAt(pos);
if (ch == left) {
++level;
} else if (ch == right) {
if (--level == 0) {
return pos;
}
}
++pos;
}
return -1;
}
// Copied from org.eclipse.cdt.internal.ui.text.CParameterListValidator
private static int[] computeCommaPositions(String code) {
final int length = code.length();
int pos = 0;
List<Integer> positions = new ArrayList<Integer>();
positions.add(new Integer(-1));
while (pos < length && pos != -1) {
char ch = code.charAt(pos);
switch (ch) {
case ',':
positions.add(new Integer(pos));
break;
case '(':
pos = indexOfClosingPeer(code, '(', ')', pos);
break;
case '<':
pos = indexOfClosingPeer(code, '<', '>', pos);
break;
case '[':
pos = indexOfClosingPeer(code, '[', ']', pos);
break;
default:
break;
}
if (pos != -1)
pos++;
}
positions.add(new Integer(length));
int[] fields = new int[positions.size()];
for (int i = 0; i < fields.length; i++)
fields[i] = positions.get(i).intValue();
return fields;
}
private static Collection<QObjectConnectCompletion> getCompletionsFor(IASTNode targetNode, IASTInitializerClause arg) {
IType targetType = ASTUtil.getBaseType(targetNode);
if (!(targetType instanceof ICPPClassType))
return null;
ICPPClassType cls = (ICPPClassType) targetType;
QtIndex qtIndex = QtIndex.getIndex(ASTUtil.getProject(targetNode));
if (qtIndex == null)
return null;
IQObject qobj = null;
try {
qobj = qtIndex.findQObject(cls.getQualifiedName());
} catch(DOMException e) {
CCorePlugin.log(e);
}
// QtIndex.findQObject will return null in some cases, e.g., when the parameter is null
if (qobj == null)
return null;
Collection<QObjectConnectCompletion> completions = new ArrayList<QObjectConnectCompletion>();
String raw = arg.getRawSignature();
if (raw.startsWith(QtKeywords.SIGNAL))
for(IQMethod method : qobj.getSignals().withoutOverrides())
for(String signature : method.getSignatures())
completions.add(new QObjectConnectCompletion(signature));
if (raw.startsWith(QtKeywords.SLOT))
for(IQMethod method : qobj.getSlots().withoutOverrides())
for(String signature : method.getSignatures())
completions.add(new QObjectConnectCompletion(signature));
return completions;
}
public static Collection<QObjectConnectCompletion> getConnectProposals(
ICEditorContentAssistInvocationContext context, IASTName name, IASTCompletionContext astContext, IASTNode astNode) {
if (QtFunctionCallUtil.isQObjectFunctionCall(astContext, !context.isContextInformationStyle(), name)) {
int parseOffset = context.getParseOffset();
int invocationOffset = context.getInvocationOffset();
String unparsed = "";
try {
unparsed = context.getDocument().get(parseOffset, invocationOffset - parseOffset);
} catch (BadLocationException e) {
CCorePlugin.log(e);
}
if (unparsed.length() > 0 && unparsed.charAt(0) == '(')
unparsed = unparsed.substring(1);
int[] commas = computeCommaPositions(unparsed);
switch (commas.length) {
case 2:
case 3:
// Across all possible connect/disconnect overloads, the first and second arguments
// can be SIGNAL expansion.
return Collections.singletonList(new QObjectConnectCompletion(QObjectConnectCompletion.Param.Signal));
case 4:
case 5:
// Across all possible connect/disconnect overloads, the first and second arguments
// can be SIGNAL or SLOT expansions.
return Collections.singletonList(new QObjectConnectCompletion(QObjectConnectCompletion.Param.Member));
}
return null;
}
if (astNode.getPropertyInParent() == IASTFunctionCallExpression.ARGUMENT) {
IASTNode parent = astNode.getParent();
if (!(parent instanceof IASTFunctionCallExpression))
return null;
// NOTE: QtConnectFunctionCall cannot be used here because that class expects a
// valid expression. During content assist the function is still being
// created.
IASTFunctionCallExpression call = (IASTFunctionCallExpression) parent;
IASTExpression nameExpr = call.getFunctionNameExpression();
IASTName funcName = null;
if (nameExpr instanceof IASTIdExpression)
funcName = ((IASTIdExpression) nameExpr).getName();
else if (nameExpr instanceof ICPPASTFieldReference)
funcName = ((ICPPASTFieldReference) nameExpr).getFieldName();
// If this isn't a QObject::connect or QObject::disconnect function call then
// look no further.
if (!QtFunctionCallUtil.isQObjectFunctionCall(astContext, !context.isContextInformationStyle(), funcName))
return null;
// In a content assist context the argument that is currently being entered is
// last in the function call.
IASTInitializerClause[] args = call.getArguments();
if (args == null
|| args.length < 0)
return null;
int argIndex = args.length - 1;
// Find the type node that is used for this expansion.
IASTNode typeNode = QtFunctionCallUtil.getTypeNode(call, args, argIndex);
if (typeNode == null)
return null;
// Returns completions for the given expansion using the given type as the
// source for Qt methods.
return getCompletionsFor(typeNode, args[argIndex]);
}
return null;
}
public static Collection<ICompletionProposal> getProposals(
ICEditorContentAssistInvocationContext context, IASTName name, IASTCompletionContext astContext, IASTNode astNode) {
Collection<QObjectConnectCompletion> qtProposals = getConnectProposals(context, name, astContext, astNode);
if (qtProposals == null
|| qtProposals.isEmpty())
return null;
Collection<ICompletionProposal> proposals = new ArrayList<ICompletionProposal>();
for (QObjectConnectCompletion qtProposal : qtProposals)
qtProposal.addProposals(proposals, context);
return proposals;
}
}

View file

@ -0,0 +1,75 @@
/*
* Copyright (c) 2013 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;
import java.util.Collection;
import java.util.Collections;
import org.eclipse.cdt.core.dom.ast.IASTName;
import org.eclipse.cdt.internal.corext.template.c.CContextType;
import org.eclipse.cdt.internal.corext.template.c.TranslationUnitContext;
import org.eclipse.cdt.internal.corext.template.c.TranslationUnitContextType;
import org.eclipse.cdt.internal.ui.text.template.TemplateEngine.CTemplateProposal;
import org.eclipse.cdt.qt.ui.QtUIPlugin;
import org.eclipse.cdt.ui.text.contentassist.ICEditorContentAssistInvocationContext;
import org.eclipse.jface.text.IRegion;
import org.eclipse.jface.text.ITextSelection;
import org.eclipse.jface.text.Position;
import org.eclipse.jface.text.Region;
import org.eclipse.jface.text.contentassist.ICompletionProposal;
import org.eclipse.jface.text.templates.Template;
import org.eclipse.jface.viewers.ISelection;
import org.eclipse.jface.viewers.ISelectionProvider;
import org.eclipse.ui.texteditor.ITextEditor;
@SuppressWarnings("restriction")
public class QObjectDeclarationCompletion {
private static final String TEMPLATE = "class ${name} : public ${QObject}\n{\nQ_OBJECT\n\n${cursor}\n};";
private final static TranslationUnitContextType context;
static {
context = new CContextType();
context.setId(CContextType.ID);
}
public static Collection<ICompletionProposal> getProposals(ICEditorContentAssistInvocationContext ctx, IASTName name) {
String token = name.getLastName().toString();
if (token.isEmpty()
|| !"class".startsWith(token))
return null;
Position position = getPosition(ctx);
if (position == null)
return null;
TranslationUnitContext tuCtx = context.createContext(ctx.getDocument(), position, ctx.getTranslationUnit());
IRegion region = new Region(position.getOffset(), position.getLength());
Template template = new Template( "class", "QObject declaration", CContextType.ID, TEMPLATE, true);
return Collections.<ICompletionProposal>singletonList(new CTemplateProposal(template, tuCtx, region, QtUIPlugin.getQtLogo()));
}
private static Position getPosition(ICEditorContentAssistInvocationContext context) {
ITextEditor textEditor = (ITextEditor) context.getEditor().getAdapter(ITextEditor.class);
if (textEditor == null)
return null;
ISelectionProvider selectionProvider = textEditor.getSelectionProvider();
if (selectionProvider == null)
return null;
ISelection selection = selectionProvider.getSelection();
if (!(selection instanceof ITextSelection))
return null;
ITextSelection text = (ITextSelection) selection;
return new Position(text.getOffset(), text.getLength());
}
}

View file

@ -0,0 +1,59 @@
/*
* Copyright (c) 2013 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;
import java.util.Collection;
import java.util.Collections;
import org.eclipse.cdt.core.dom.ast.IASTCompletionContext;
import org.eclipse.cdt.core.dom.ast.IASTName;
import org.eclipse.cdt.core.dom.ast.IASTNode;
import org.eclipse.cdt.internal.corext.template.c.CContextType;
import org.eclipse.cdt.internal.qt.ui.assist.QPropertyExpansion;
import org.eclipse.cdt.internal.qt.ui.assist.QtProposalContext;
import org.eclipse.cdt.internal.qt.ui.assist.QtTemplateProposal;
import org.eclipse.cdt.qt.core.QtKeywords;
import org.eclipse.cdt.qt.ui.QtUIPlugin;
import org.eclipse.cdt.ui.text.contentassist.ICEditorContentAssistInvocationContext;
import org.eclipse.jface.text.Region;
import org.eclipse.jface.text.contentassist.ICompletionProposal;
import org.eclipse.jface.text.templates.Template;
import org.eclipse.jface.text.templates.TemplateContextType;
@SuppressWarnings("restriction")
public class QPropertyCompletion {
private static final String CONTEXT_ID = QtUIPlugin.PLUGIN_ID + ".proposal.Q_PROPERTY";
private static final Template QPropertyTemplate
= new Template("Q_PROPERTY", "Q_PROPERTY declaration", CONTEXT_ID, "Q_PROPERTY( ${type} ${name} READ ${accessor} ${cursor} )", true);
public static Collection<ICompletionProposal> getAttributeProposals(ICEditorContentAssistInvocationContext context) {
QPropertyExpansion expansion = QPropertyExpansion.create(context);
return expansion == null
? Collections.<ICompletionProposal>emptyList()
: expansion.getProposals(CONTEXT_ID, context);
}
public static Collection<ICompletionProposal> getProposals(
ICEditorContentAssistInvocationContext context, IASTName name, IASTCompletionContext astContext, IASTNode astNode) {
String token = name.getLastName().toString();
if (token.isEmpty()
|| !QtKeywords.Q_PROPERTY.startsWith(token))
return Collections.emptyList();
TemplateContextType ctxType = new CContextType();
ctxType.setId(CONTEXT_ID);
QtProposalContext templateCtx = new QtProposalContext(context, ctxType);
Region region = new Region(templateCtx.getCompletionOffset(), templateCtx.getCompletionLength());
return Collections.<ICompletionProposal>singletonList(new QtTemplateProposal(QPropertyTemplate, templateCtx, region));
}
}

View file

@ -5,56 +5,59 @@
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*/
package org.eclipse.cdt.internal.qt.ui;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import org.eclipse.cdt.core.CCorePlugin;
import org.eclipse.cdt.core.dom.ast.ASTTypeUtil;
import org.eclipse.cdt.core.dom.ast.IASTCompletionContext;
import org.eclipse.cdt.core.dom.ast.IASTCompletionNode;
import org.eclipse.cdt.core.dom.ast.IASTExpression;
import org.eclipse.cdt.core.dom.ast.IASTFunctionCallExpression;
import org.eclipse.cdt.core.dom.ast.IASTIdExpression;
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.IBinding;
import org.eclipse.cdt.core.dom.ast.IPointerType;
import org.eclipse.cdt.core.dom.ast.IType;
import org.eclipse.cdt.core.dom.ast.cpp.ICPPASTInitializerClause;
import org.eclipse.cdt.core.dom.ast.cpp.ICPPClassType;
import org.eclipse.cdt.core.dom.ast.cpp.ICPPFunction;
import org.eclipse.cdt.core.dom.ast.cpp.ICPPMethod;
import org.eclipse.cdt.core.dom.ast.cpp.ICPPParameter;
import org.eclipse.cdt.core.dom.ast.tag.ITag;
import org.eclipse.cdt.core.dom.ast.tag.ITagReader;
import org.eclipse.cdt.core.model.ICProject;
import org.eclipse.cdt.core.model.ITranslationUnit;
import org.eclipse.cdt.internal.core.dom.parser.cpp.ICPPEvaluation;
import org.eclipse.cdt.internal.ui.text.contentassist.CCompletionProposal;
import org.eclipse.cdt.internal.ui.text.contentassist.CContentAssistInvocationContext;
import org.eclipse.cdt.internal.ui.text.contentassist.ParsingBasedProposalComputer;
import org.eclipse.cdt.internal.ui.text.contentassist.RelevanceConstants;
import org.eclipse.cdt.qt.core.QtKeywords;
import org.eclipse.cdt.qt.core.QtNature;
import org.eclipse.cdt.qt.core.QtPlugin;
import org.eclipse.cdt.qt.ui.QtUIPlugin;
import org.eclipse.cdt.ui.CUIPlugin;
import org.eclipse.cdt.ui.text.contentassist.ContentAssistInvocationContext;
import org.eclipse.cdt.ui.text.contentassist.ICEditorContentAssistInvocationContext;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.jface.text.contentassist.ICompletionProposal;
@SuppressWarnings("restriction")
public class QtCompletionProposalComputer extends ParsingBasedProposalComputer {
@Override
public List<ICompletionProposal> computeCompletionProposals(
ContentAssistInvocationContext context, IProgressMonitor monitor) {
// this is overridden in order to find proposals when the completion node is null
try {
if (context instanceof CContentAssistInvocationContext) {
CContentAssistInvocationContext cContext = (CContentAssistInvocationContext) context;
String prefix = null;
IASTCompletionNode completionNode = cContext.getCompletionNode();
// the parent implementation gives up when this condition is false
if (completionNode != null)
prefix = completionNode.getPrefix();
if (prefix == null)
prefix = cContext.computeIdentifierPrefix().toString();
return computeCompletionProposals(cContext, completionNode, prefix);
}
} catch (Exception e) {
QtUIPlugin.log(e);
}
return Collections.emptyList();
}
private boolean isApplicable(ICEditorContentAssistInvocationContext context) {
ITranslationUnit tu = context.getTranslationUnit();
if (tu == null)
@ -68,324 +71,52 @@ public class QtCompletionProposalComputer extends ParsingBasedProposalComputer {
if (project == null)
return false;
try {
return project.hasNature(QtNature.ID);
} catch (CoreException e) {
CUIPlugin.log(e);
return false;
}
}
private static boolean is_QObject_connect(
ICEditorContentAssistInvocationContext context,
IASTCompletionContext astContext, IASTName name) {
IASTName connectName = name.getLastName();
if (!QtKeywords.CONNECT.equals(new String(connectName.getSimpleID())))
return false;
IBinding[] funcBindings = astContext.findBindings(connectName,
!context.isContextInformationStyle());
for (IBinding funcBinding : funcBindings)
if (funcBinding instanceof ICPPFunction) {
IBinding ownerBinding = ((ICPPFunction) funcBinding).getOwner();
if (ownerBinding != null
&& QtKeywords.QOBJECT.equals(ownerBinding.getName()))
return true;
}
return false;
}
private static class Completion {
private final String replacement;
private final String display;
private final int cursorOffset;
public static final Completion SIGNAL = new Completion("SIGNAL()",
"SIGNAL(a)", -1);
public static final Completion SLOT = new Completion("SLOT()",
"SLOT(a)", -1);
public Completion(String replacement) {
this(replacement, replacement, 0);
}
public Completion(String replacement, String display, int cursorOffset) {
this.replacement = replacement;
this.display = display;
this.cursorOffset = cursorOffset;
}
public ICompletionProposal createProposal(
ICEditorContentAssistInvocationContext context) {
int repLength = replacement.length();
int repOffset = context.getInvocationOffset();
CCompletionProposal p = new CCompletionProposal(replacement,
repOffset, repLength, null, display,
RelevanceConstants.DEFAULT_TYPE_RELEVANCE,
context.getViewer());
p.setCursorPosition(repLength + cursorOffset);
return p;
}
@Override
public String toString() {
if (replacement == null)
return super.toString();
return replacement + '@' + cursorOffset;
}
}
private static interface MethodFilter {
public boolean keep(ICPPMethod method);
public static class Qt {
public static final MethodFilter Signal = new MethodFilter() {
@Override
public boolean keep(ICPPMethod method) {
ITagReader tagReader = CCorePlugin.getTagService()
.findTagReader(method);
if (tagReader == null)
return false;
ITag tag = tagReader.getTag(QtPlugin.SIGNAL_SLOT_TAGGER_ID);
if (tag == null)
return false;
int result = tag.getByte(0);
return result != ITag.FAIL
&& ((result & QtPlugin.SignalSlot_Mask_signal) == QtPlugin.SignalSlot_Mask_signal);
}
};
public static final MethodFilter Slot = new MethodFilter() {
@Override
public boolean keep(ICPPMethod method) {
ITagReader tagReader = CCorePlugin.getTagService()
.findTagReader(method);
if (tagReader == null)
return false;
ITag tag = tagReader.getTag(QtPlugin.SIGNAL_SLOT_TAGGER_ID);
if (tag == null)
return false;
int result = tag.getByte(0);
return result != ITag.FAIL
&& ((result & QtPlugin.SignalSlot_Mask_slot) == QtPlugin.SignalSlot_Mask_slot);
}
};
}
}
private static Iterable<ICPPMethod> filterMethods(final ICPPClassType cls,
final MethodFilter filter) {
return new Iterable<ICPPMethod>() {
@Override
public Iterator<ICPPMethod> iterator() {
return new Iterator<ICPPMethod>() {
private int index = 0;
private final ICPPMethod[] methods = cls.getMethods();
@Override
public boolean hasNext() {
for (; index < methods.length; ++index)
if (filter.keep(methods[index]))
return true;
return false;
}
@Override
public ICPPMethod next() {
return methods[index++];
}
@Override
public void remove() {
}
};
}
};
}
private static String getSignature(ICPPMethod method) {
StringBuilder signature = new StringBuilder();
signature.append(method.getName());
signature.append('(');
boolean first = true;
for (ICPPParameter param : method.getParameters()) {
if (first)
first = false;
else
signature.append(", ");
signature.append(ASTTypeUtil.getType(param.getType()));
}
signature.append(')');
return signature.toString();
}
private static void addCompletionsFor(Collection<Completion> completions,
IASTInitializerClause init, MethodFilter filter) {
if (!(init instanceof ICPPASTInitializerClause))
return;
ICPPEvaluation eval = ((ICPPASTInitializerClause) init).getEvaluation();
if (eval == null)
return;
IType type = eval.getTypeOrFunctionSet(init);
while (type instanceof IPointerType)
type = ((IPointerType) type).getType();
if (type instanceof ICPPClassType)
for (ICPPMethod signal : filterMethods((ICPPClassType) type, filter))
completions.add(new Completion(getSignature(signal)));
}
// Copied from org.eclipse.cdt.internal.ui.text.CParameterListValidator
private static int indexOfClosingPeer(String code, char left, char right,
int pos) {
int level = 0;
final int length = code.length();
while (pos < length) {
char ch = code.charAt(pos);
if (ch == left) {
++level;
} else if (ch == right) {
if (--level == 0) {
return pos;
}
}
++pos;
}
return -1;
}
// Copied from org.eclipse.cdt.internal.ui.text.CParameterListValidator
private static int[] computeCommaPositions(String code) {
final int length = code.length();
int pos = 0;
List<Integer> positions = new ArrayList<Integer>();
positions.add(new Integer(-1));
while (pos < length && pos != -1) {
char ch = code.charAt(pos);
switch (ch) {
case ',':
positions.add(new Integer(pos));
break;
case '(':
pos = indexOfClosingPeer(code, '(', ')', pos);
break;
case '<':
pos = indexOfClosingPeer(code, '<', '>', pos);
break;
case '[':
pos = indexOfClosingPeer(code, '[', ']', pos);
break;
default:
break;
}
if (pos != -1)
pos++;
}
positions.add(new Integer(length));
int[] fields = new int[positions.size()];
for (int i = 0; i < fields.length; i++)
fields[i] = positions.get(i).intValue();
return fields;
}
private void addConnectParameterCompletions(
List<ICompletionProposal> proposals,
ICEditorContentAssistInvocationContext context,
IASTCompletionNode completionNode, String prefix) {
IASTName[] names = completionNode.getNames();
List<Completion> completions = new LinkedList<Completion>();
for (IASTName name : names) {
// The node isn't properly hooked up, must have backtracked out of
// this node
if (name.getTranslationUnit() == null)
continue;
IASTCompletionContext astContext = name.getCompletionContext();
if (astContext == null || !(astContext instanceof IASTNode))
continue;
IASTNode astNode = (IASTNode) astContext;
if (is_QObject_connect(context, astContext, name)) {
int parseOffset = context.getParseOffset();
int invocationOffset = context.getInvocationOffset();
String unparsed = "";
try {
unparsed = context.getDocument().get(parseOffset,
invocationOffset - parseOffset);
} catch (BadLocationException e) {
QtUIPlugin.log(e);
}
if (unparsed.length() > 0 && unparsed.charAt(0) == '(')
unparsed = unparsed.substring(1);
int[] commas = computeCommaPositions(unparsed);
switch (commas.length) {
case 3:
completions.add(Completion.SIGNAL);
break;
case 5:
completions.add(Completion.SLOT);
break;
}
} else if (astNode.getPropertyInParent() == IASTFunctionCallExpression.ARGUMENT) {
IASTNode parent = astNode.getParent();
if (!(parent instanceof IASTFunctionCallExpression))
continue;
IASTFunctionCallExpression call = (IASTFunctionCallExpression) parent;
IASTExpression nameExpr = call.getFunctionNameExpression();
if (!(nameExpr instanceof IASTIdExpression))
continue;
IASTIdExpression funcNameIdExpr = (IASTIdExpression) nameExpr;
IASTName funcName = funcNameIdExpr.getName();
if (!is_QObject_connect(context, astContext, funcName))
continue;
IASTInitializerClause[] args = call.getArguments();
switch (args.length) {
case 2:
addCompletionsFor(completions, args[0],
MethodFilter.Qt.Signal);
break;
case 4:
addCompletionsFor(completions, args[2],
MethodFilter.Qt.Slot);
break;
}
}
}
for (Completion completion : completions) {
ICompletionProposal proposal = completion.createProposal(context);
if (proposal != null)
proposals.add(proposal);
}
}
return QtNature.hasNature(project);
}
@Override
protected List<ICompletionProposal> computeCompletionProposals(
CContentAssistInvocationContext context,
IASTCompletionNode completionNode, String prefix)
throws CoreException {
CContentAssistInvocationContext context, IASTCompletionNode completionNode, String prefix) throws CoreException {
// make sure this is a Qt project
if (!isApplicable(context))
return Collections.emptyList();
List<ICompletionProposal> proposals = new ArrayList<ICompletionProposal>();
addConnectParameterCompletions(proposals, context, completionNode,
prefix);
return proposals;
List<ICompletionProposal> proposals = null;
if (completionNode != null) {
IASTName[] names = completionNode.getNames();
for (IASTName name : names) {
// the node isn't properly hooked up, must have backtracked out of this node
if (name.getTranslationUnit() == null)
continue;
IASTCompletionContext astContext = name.getCompletionContext();
if (astContext == null || !(astContext instanceof IASTNode))
continue;
IASTNode astNode = (IASTNode) astContext;
proposals = addAll(proposals, QObjectConnectCompletion.getProposals(context, name, astContext, astNode));
proposals = addAll(proposals, QObjectDeclarationCompletion.getProposals(context, name));
proposals = addAll(proposals, QPropertyCompletion.getProposals(context, name, astContext, astNode));
}
}
// Attributes within Q_PROPERTY declarations
proposals = addAll(proposals, QPropertyCompletion.getAttributeProposals(context));
return proposals == null ? Collections.<ICompletionProposal>emptyList() : proposals;
}
private static <T> List<T> addAll(List<T> list, Collection<T> toAdd) {
if (toAdd == null
|| toAdd.isEmpty())
return list;
if (list == null)
return new ArrayList<T>(toAdd);
list.addAll(toAdd);
return list;
}
}

View file

@ -0,0 +1,506 @@
/*
* Copyright (c) 2013 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.ui.text.contentassist.CCompletionProposal;
import org.eclipse.cdt.qt.core.index.IQMethod;
import org.eclipse.cdt.qt.core.index.IQObject;
import org.eclipse.cdt.qt.core.index.IQProperty;
import org.eclipse.cdt.qt.core.index.QtIndex;
import org.eclipse.cdt.qt.ui.QtUIPlugin;
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, QtUIPlugin.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) {
QtUIPlugin.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.getDefaultValue();
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) {
QtUIPlugin.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);
}
}

View file

@ -0,0 +1,397 @@
/*
* Copyright (c) 2013 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.Collection;
import java.util.Collections;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.eclipse.cdt.core.dom.ast.IASTDeclarator;
import org.eclipse.cdt.core.dom.ast.IType;
import org.eclipse.cdt.core.dom.ast.cpp.ICPPASTTypeId;
import org.eclipse.cdt.internal.core.dom.parser.cpp.semantics.CPPVisitor;
import org.eclipse.cdt.internal.corext.template.c.CContextType;
import org.eclipse.cdt.internal.qt.core.parser.QtParser;
import org.eclipse.cdt.internal.ui.text.CHeuristicScanner;
import org.eclipse.cdt.internal.ui.text.Symbols;
import org.eclipse.cdt.internal.ui.text.contentassist.CCompletionProposal;
import org.eclipse.cdt.qt.core.QtKeywords;
import org.eclipse.cdt.qt.core.index.IQProperty;
import org.eclipse.cdt.qt.ui.QtUIPlugin;
import org.eclipse.cdt.ui.text.contentassist.ICEditorContentAssistInvocationContext;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.Region;
import org.eclipse.jface.text.contentassist.ICompletionProposal;
import org.eclipse.jface.text.templates.Template;
import org.eclipse.jface.text.templates.TemplateContextType;
/**
* A utility class for accessing parts of the Q_PROPERTY expansion that have already
* been entered as well as the offset of various parts of the declaration. This is
* used for things like proposing only parameters that are not already used, offering
* appropriate suggestions for a specific parameter, etc.
*/
@SuppressWarnings("restriction")
public class QPropertyExpansion {
/** The full text of the expansion */
private final String expansion;
/** The offset of the first character in the attributes section. This is usually the
* start of READ. */
private final int startOfAttrs;
/** The offset of the cursor in the expansion. */
private final int cursor;
/** The parsed type of the property. */
private final IType type;
/** The parsed name of the property. This is the last identifier before the first attribute. */
private final String name;
/** The identifier at which the cursor is currently pointing. */
private final Identifier currIdentifier;
/** The identifier before the one where the cursor is pointing. This is needed to figure out what
* values are valid for an attribute like READ, WRITE, etc. */
private final Identifier prevIdentifier;
// The type/name section ends right before the first attribute.
private static final Pattern TYPENAME_REGEX;
static {
StringBuilder regexBuilder = new StringBuilder();
regexBuilder.append("^(?:Q_PROPERTY\\s*\\()?\\s*(.*?)(\\s+)(?:");
for(IQProperty.Attribute attr : IQProperty.Attribute.values()) {
if (attr.ordinal() > 0)
regexBuilder.append('|');
regexBuilder.append("(?:");
regexBuilder.append(attr.identifier);
regexBuilder.append(")");
}
regexBuilder.append(").*$");
TYPENAME_REGEX = Pattern.compile(regexBuilder.toString());
}
/**
* A small utility to store the important parts of an identifier. This is just the starting
* offset and the text of the identifier.
*/
private static class Identifier {
public final int start;
public final String ident;
public Identifier(int start, String ident) {
this.start = start;
this.ident = ident;
}
@Override
public String toString() {
return Integer.toString(start) + ':' + ident;
}
}
public static QPropertyExpansion create(ICEditorContentAssistInvocationContext context) {
// Extract the substring that likely contributes to this Q_PROPERTY declaration. The declaration
// could be in any state of being entered, so use the HeuristicScanner to guess about the
// possible structure. The fixed assumptions are that the content assistant was invoked within
// the expansion parameter of Q_PROPERTY. We try to guess at the end of the String, which is
// either the closing paren (within 512 characters from the opening paren) or the current cursor
// location.
// The offset is always right after the opening paren, use it to get to a fixed point in the
// declaration.
int offset = context.getContextInformationOffset();
if (offset < 0)
return null;
IDocument doc = context.getDocument();
CHeuristicScanner scanner = new CHeuristicScanner(doc);
// We should only need to backup the length of Q_PROPERTY, but allow extra to deal
// with whitespace.
int lowerBound = Math.max(0, offset - 64);
// Allow up to 512 characters from the opening paren.
int upperBound = Math.min(doc.getLength(), offset + 512);
int openingParen = scanner.findOpeningPeer(offset, lowerBound, '(', ')');
if (openingParen == CHeuristicScanner.NOT_FOUND)
return null;
int token = scanner.previousToken(scanner.getPosition() - 1, lowerBound);
if (token != Symbols.TokenIDENT)
return null;
// Find the start of the previous identifier. This scans backward, so it stops one
// position before the identifier (unless the identifer is at the start of the content).
int begin = scanner.getPosition();
if (begin != 0)
++begin;
String identifier = null;
try {
identifier = doc.get(begin, openingParen - begin);
} catch (BadLocationException e) {
QtUIPlugin.log(e);
}
if (!QtKeywords.Q_PROPERTY.equals(identifier))
return null;
// advance past the opening paren
++openingParen;
String expansion = null;
int closingParen = scanner.findClosingPeer(openingParen, upperBound, '(', ')');
// This expansion is not applicable if the assistant was invoked after the closing paren.
if (closingParen != CHeuristicScanner.NOT_FOUND
&& context.getInvocationOffset() > scanner.getPosition())
return null;
try {
if (closingParen != CHeuristicScanner.NOT_FOUND)
expansion = doc.get(openingParen, closingParen - openingParen);
else
expansion = doc.get(openingParen, context.getInvocationOffset() - openingParen );
} catch (BadLocationException e) {
QtUIPlugin.log(e);
}
if (expansion == null)
return null;
int cursor = context.getInvocationOffset();
Identifier currIdentifier = identifier(doc, scanner, cursor, lowerBound, upperBound);
if (currIdentifier == null)
return null;
Identifier prevIdentifier = identifier(doc, scanner, currIdentifier.start - 1, lowerBound, upperBound);
// There are two significant regions in a Q_PROPERTY declaration. The first is everything
// between the opening paren and the first parameter. This region specifies the type and the
// name. The other is the region that declares all the parameters. There is an arbitrary
// amount of whitespace between these regions.
//
// This function finds and returns the offset of the end of the region containing the type and
// name. Returns 0 if the type/name region cannot be found.
IType type = null;
String name = null;
int endOfTypeName = 0;
Matcher m = TYPENAME_REGEX.matcher(expansion);
if (m.matches()) {
endOfTypeName = openingParen + m.end(2);
// parse the type/name part and then extract the type and name from the result
ICPPASTTypeId typeId = QtParser.parseTypeId(m.group(1));
type = CPPVisitor.createType(typeId);
IASTDeclarator declarator = typeId.getAbstractDeclarator();
if (declarator != null
&& declarator.getName() != null)
name = declarator.getName().toString();
}
return new QPropertyExpansion(expansion, endOfTypeName, cursor, type, name, prevIdentifier, currIdentifier);
}
private QPropertyExpansion(String expansion, int startOfAttrs, int cursor, IType type, String name, Identifier prev, Identifier curr) {
this.expansion = expansion;
this.startOfAttrs = startOfAttrs;
this.cursor = cursor;
this.type = type;
this.name = name;
this.prevIdentifier = prev;
this.currIdentifier = curr;
}
public String getCurrIdentifier() {
return currIdentifier.ident;
}
public String getPrevIdentifier() {
return prevIdentifier.ident;
}
public String getPrefix() {
if (currIdentifier.ident == null)
return null;
if (cursor > currIdentifier.start + currIdentifier.ident.length())
return null;
return currIdentifier.ident.substring(0, cursor - currIdentifier.start);
}
private static class Attribute {
public final IQProperty.Attribute attribute;
public final int relevance;
public Attribute(IQProperty.Attribute attribute) {
this.attribute = attribute;
// Give attribute proposals the same order as the Qt documentation.
switch(attribute) {
case READ: this.relevance = 11; break;
case WRITE: this.relevance = 10; break;
case RESET: this.relevance = 9; break;
case NOTIFY: this.relevance = 8; break;
case REVISION: this.relevance = 7; break;
case DESIGNABLE: this.relevance = 6; break;
case SCRIPTABLE: this.relevance = 5; break;
case STORED: this.relevance = 4; break;
case USER: this.relevance = 3; break;
case CONSTANT: this.relevance = 2; break;
case FINAL: this.relevance = 1; break;
default: this.relevance = 0; break;
}
}
public ICompletionProposal getProposal(String contextId, ICEditorContentAssistInvocationContext context) {
// Attributes without values propose only their own identifier.
if (!attribute.hasValue)
return new CCompletionProposal(attribute.identifier, context.getInvocationOffset(), 0, QtUIPlugin.getQtLogo(), attribute.identifier + " - Q_PROPERTY declaration parameter", relevance);
// Otherwise create a template where the content depends on the type of the attribute's parameter.
String display = attribute.identifier + ' ' + attribute.paramName;
String replacement = attribute.identifier;
if ("bool".equals(attribute.paramName))
replacement += " ${true}";
else if ("int".equals(attribute.paramName))
replacement += " ${0}";
else if (attribute.paramName != null)
replacement += " ${" + attribute.paramName + '}';
return templateProposal(contextId, context, display, replacement, relevance);
}
}
private static ICompletionProposal templateProposal(String contextId, ICEditorContentAssistInvocationContext context, String display, String replacement, int relevance) {
Template template = new Template(display, "Q_PROPERTY declaration parameter", contextId, replacement, true);
TemplateContextType ctxType = new CContextType();
ctxType.setId(contextId);
QtProposalContext templateCtx = new QtProposalContext(context, ctxType);
Region region = new Region(templateCtx.getCompletionOffset(), templateCtx.getCompletionLength());
return new QtTemplateProposal(template, templateCtx, region, relevance);
}
public List<ICompletionProposal> getProposals(String contextId, ICEditorContentAssistInvocationContext context) {
// Make no suggestions when the start of the current identifier is before the end of
// the "type name" portion of the declaration.
if (currIdentifier.start < startOfAttrs)
return Collections.emptyList();
// Propose nothing but READ as the first attribute. If the previous identifier is before
// the end of the typeName region, then we're currently at the first attribute.
if (prevIdentifier.start < startOfAttrs)
return Collections.singletonList(new Attribute(IQProperty.Attribute.READ).getProposal(contextId, context));
// If the previous token is an Attribute name that has a parameter then suggest appropriate
// values for that parameter. Otherwise suggest the other Attribute names.
String prefix = getPrefix();
// There are two types of proposals. If the previous identifier matches a known attribute name,
// then we propose possible values for that attribute. Otherwise we want to propose the identifiers
// that don't already appear in the expansion.
//
// This is implemented by iterating over the list of known attributes. If any of the attributes
// matches the previous identifier, then we build and return a list of valid proposals for that
// attribute.
//
// Otherwise, for each attribute we build a regular expression that checks to see if that token
// appears within the expansion. If it already appears, then the attribute is ignored. Otherwise
// it is added as an unspecified attribute. If the loop completes, then we create a list of proposals
// for from that unspecified list.
List<Attribute> unspecifiedAttributes = new ArrayList<Attribute>();
for(IQProperty.Attribute attr : IQProperty.Attribute.values()) {
if (attr.hasValue
&& (prevIdentifier != null && attr.identifier.equals(prevIdentifier.ident))) {
Collection<QPropertyAttributeProposal> attrProposals = QPropertyAttributeProposal.buildProposals(attr, context, type, name);
if (attrProposals != null) {
List<ICompletionProposal> proposals = new ArrayList<ICompletionProposal>();
for(QPropertyAttributeProposal value : attrProposals)
if (prefix == null
|| value.getIdentifier().startsWith(prefix))
proposals.add(value.createProposal(prefix, context.getInvocationOffset()));
return proposals;
}
return Collections.emptyList();
}
if (prefix != null) {
if (attr.identifier.startsWith(prefix)
&&(!expansion.matches(".*\\s+" + attr.identifier + "\\s+.*")
||attr.identifier.equals(currIdentifier.ident)))
unspecifiedAttributes.add(new Attribute(attr));
} else if (!expansion.matches(".*\\s+" + attr.identifier + "\\s+.*"))
unspecifiedAttributes.add(new Attribute(attr));
}
List<ICompletionProposal> proposals = new ArrayList<ICompletionProposal>();
for(Attribute attr : unspecifiedAttributes) {
ICompletionProposal proposal = attr.getProposal(contextId, context);
if (proposal != null)
proposals.add(proposal);
}
return proposals;
}
private static Identifier identifier(IDocument doc, CHeuristicScanner scanner, int cursor, int lower, int upper) {
try {
// If the cursor is in whitespace, then the current identifier is null. Scan backward to find
// the start of this whitespace.
if (Character.isWhitespace(doc.getChar(cursor - 1))) {
int prev = scanner.findNonWhitespaceBackward(cursor, lower);
return new Identifier(Math.min(cursor, prev + 1), null);
}
int tok = scanner.previousToken(cursor, lower);
if (tok != CHeuristicScanner.TokenIDENT)
return null;
int begin = scanner.getPosition() + 1;
tok = scanner.nextToken(begin, upper);
if (tok != CHeuristicScanner.TokenIDENT)
return null;
int end = scanner.getPosition();
return new Identifier(begin, doc.get(begin, end - begin));
} catch(BadLocationException e) {
QtUIPlugin.log(e);
}
return null;
}
@Override
public String toString() {
if (expansion == null)
return super.toString();
if (cursor >= expansion.length())
return expansion + '|';
if (cursor < 0)
return "|" + expansion;
return expansion.substring(0, cursor) + '|' + expansion.substring(cursor);
}
}

View file

@ -0,0 +1,94 @@
/*
* Copyright (c) 2013 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 org.eclipse.cdt.core.dom.ast.IASTCompletionNode;
import org.eclipse.cdt.internal.corext.template.c.CContext;
import org.eclipse.cdt.ui.text.contentassist.ICEditorContentAssistInvocationContext;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.ITextViewer;
import org.eclipse.jface.text.Position;
import org.eclipse.jface.text.templates.Template;
import org.eclipse.jface.text.templates.TemplateContextType;
import org.eclipse.swt.graphics.Point;
@SuppressWarnings("restriction")
public class QtProposalContext extends CContext {
private final String contextId;
public QtProposalContext(ICEditorContentAssistInvocationContext context, TemplateContextType ctxType) {
super(ctxType, context.getDocument(), getCompletionPosition(context), context.getTranslationUnit());
this.contextId = ctxType.getId();
}
private static Position getCompletionPosition(ICEditorContentAssistInvocationContext context) {
// The normal CDT behaviour is to not offer template proposals when text is selected. I
// don't know why they avoid it, so I've opted to replace the selection instead.
int adjustment = 0;
IASTCompletionNode node = context.getCompletionNode();
if (node != null) {
String prefix = node.getPrefix();
if (prefix != null)
adjustment -= prefix.length();
}
int length = -adjustment;
ITextViewer viewer = context.getViewer();
if (viewer != null) {
Point selection = viewer.getSelectedRange();
if (selection != null
&& selection.y > 0)
length += selection.y;
}
int offset = context.getInvocationOffset() + adjustment;
return new Position(offset, length);
}
@Override
public boolean canEvaluate(Template template) {
// The base implementation uses a length of 0 to create an empty string for the key
// and then refuses to apply the template. This override offers all templates that
// have the right ID. This is ok, because only the templates that apply were proposed.
return contextId.equals(template.getContextTypeId());
}
@Override
public int getStart() {
// The base implementation creates a different offset when the replacement length
// is not 0. We need to use the same start of the replacement region regardless of
// whether or not characters are selected.
try {
IDocument document= getDocument();
int start= getCompletionOffset();
int end= getCompletionOffset() + getCompletionLength();
while (start != 0 && isUnicodeIdentifierPartOrPoundSign(document.getChar(start - 1)))
start--;
while (start != end && Character.isWhitespace(document.getChar(start)))
start++;
if (start == end)
start= getCompletionOffset();
return start;
} catch (BadLocationException e) {
return super.getStart();
}
}
private boolean isUnicodeIdentifierPartOrPoundSign(char c) {
return Character.isUnicodeIdentifierPart(c) || c == '#';
}
}

View file

@ -0,0 +1,34 @@
/*
* Copyright (c) 2013 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 org.eclipse.cdt.qt.ui.QtUIPlugin;
import org.eclipse.cdt.ui.text.ICCompletionProposal;
import org.eclipse.jface.text.IRegion;
import org.eclipse.jface.text.templates.Template;
import org.eclipse.jface.text.templates.TemplateContext;
import org.eclipse.jface.text.templates.TemplateProposal;
public class QtTemplateProposal extends TemplateProposal implements ICCompletionProposal {
// The Qt proposals are made more relevant than the default built- proposals.
private static int BASE_RELEVANCE = 1100;
public QtTemplateProposal(Template template, TemplateContext context, IRegion region) {
this(template, context, region, 0);
}
public QtTemplateProposal(Template template, TemplateContext context, IRegion region, int relevance) {
super(template, context, region, QtUIPlugin.getQtLogo(), BASE_RELEVANCE + relevance);
}
@Override
public String getIdString() {
return getDisplayString();
}
}

View file

@ -11,6 +11,7 @@ import org.eclipse.cdt.core.model.CModelException;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.swt.graphics.Image;
import org.eclipse.ui.plugin.AbstractUIPlugin;
import org.osgi.framework.BundleContext;
@ -31,6 +32,14 @@ public class QtUIPlugin extends AbstractUIPlugin {
public QtUIPlugin() {
}
public static Image getQtLogo() {
return null;
}
public static Image getQtLogoLarge() {
return null;
}
/*
* (non-Javadoc)
* @see org.eclipse.ui.plugin.AbstractUIPlugin#start(org.osgi.framework.BundleContext)