diff --git a/qt/org.eclipse.cdt.qt.core/META-INF/MANIFEST.MF b/qt/org.eclipse.cdt.qt.core/META-INF/MANIFEST.MF index e11a2c6eb58..fd9fe45eae8 100644 --- a/qt/org.eclipse.cdt.qt.core/META-INF/MANIFEST.MF +++ b/qt/org.eclipse.cdt.qt.core/META-INF/MANIFEST.MF @@ -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 diff --git a/qt/org.eclipse.cdt.qt.core/src/org/eclipse/cdt/internal/qt/core/ASTUtil.java b/qt/org.eclipse.cdt.qt.core/src/org/eclipse/cdt/internal/qt/core/ASTUtil.java new file mode 100644 index 00000000000..d75ebe75aac --- /dev/null +++ b/qt/org.eclipse.cdt.qt.core/src/org/eclipse/cdt/internal/qt/core/ASTUtil.java @@ -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 findMethods(IQObject qobj, QtSignalSlotReference ref) { + Set bindings = new LinkedHashSet(); + + Iterable 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; + } + } +} diff --git a/qt/org.eclipse.cdt.qt.core/src/org/eclipse/cdt/internal/qt/core/QtFunctionCallUtil.java b/qt/org.eclipse.cdt.qt.core/src/org/eclipse/cdt/internal/qt/core/QtFunctionCallUtil.java new file mode 100644 index 00000000000..01a074d934e --- /dev/null +++ b/qt/org.eclipse.cdt.qt.core/src/org/eclipse/cdt/internal/qt/core/QtFunctionCallUtil.java @@ -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. + *
+ * 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();
+ * 
+ */ +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; + } +} diff --git a/qt/org.eclipse.cdt.qt.core/src/org/eclipse/cdt/internal/qt/core/QtSignalSlotReference.java b/qt/org.eclipse.cdt.qt.core/src/org/eclipse/cdt/internal/qt/core/QtSignalSlotReference.java new file mode 100644 index 00000000000..dac7389bc04 --- /dev/null +++ b/qt/org.eclipse.cdt.qt.core/src/org/eclipse/cdt/internal/qt/core/QtSignalSlotReference.java @@ -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 ); + } +} diff --git a/qt/org.eclipse.cdt.qt.core/src/org/eclipse/cdt/internal/qt/core/QtSignalSlotReferenceLocation.java b/qt/org.eclipse.cdt.qt.core/src/org/eclipse/cdt/internal/qt/core/QtSignalSlotReferenceLocation.java new file mode 100644 index 00000000000..5b3679638b5 --- /dev/null +++ b/qt/org.eclipse.cdt.qt.core/src/org/eclipse/cdt/internal/qt/core/QtSignalSlotReferenceLocation.java @@ -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, + * + *
+ * SIGNAL( signal1( int ) )
+ * ^       ^            ^ c: end of reference name
+ * |       +------------- b: start of reference name
+ * +--------------------- a: start of macro expansion
+ * 
+ * + * 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(); + } +} diff --git a/qt/org.eclipse.cdt.qt.core/src/org/eclipse/cdt/internal/qt/core/QtSignalSlotReferenceName.java b/qt/org.eclipse.cdt.qt.core/src/org/eclipse/cdt/internal/qt/core/QtSignalSlotReferenceName.java new file mode 100644 index 00000000000..46f7259bf9f --- /dev/null +++ b/qt/org.eclipse.cdt.qt.core/src/org/eclipse/cdt/internal/qt/core/QtSignalSlotReferenceName.java @@ -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., + * + *
+ * class A : public QObject
+ * {
+ *     Q_SIGNAL void signal1( int );
+ *     Q_SLOT   void slot1();
+ * };
+ * A a;
+ * QObject::connect( &a, SIGNAL( signal1( int ) ), &a, SLOT( slot1() ) );
+ * 
+ * + * 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()) + ')'; + } +} diff --git a/qt/org.eclipse.cdt.qt.core/src/org/eclipse/cdt/internal/qt/core/index/QObject.java b/qt/org.eclipse.cdt.qt.core/src/org/eclipse/cdt/internal/qt/core/index/QObject.java index 60157b7409a..9c958957ad9 100644 --- a/qt/org.eclipse.cdt.qt.core/src/org/eclipse/cdt/internal/qt/core/index/QObject.java +++ b/qt/org.eclipse.cdt.qt.core/src/org/eclipse/cdt/internal/qt/core/index/QObject.java @@ -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()); } diff --git a/qt/org.eclipse.cdt.qt.core/src/org/eclipse/cdt/qt/core/QtKeywords.java b/qt/org.eclipse.cdt.qt.core/src/org/eclipse/cdt/qt/core/QtKeywords.java index cefcfc148b1..86e935beb6f 100644 --- a/qt/org.eclipse.cdt.qt.core/src/org/eclipse/cdt/qt/core/QtKeywords.java +++ b/qt/org.eclipse.cdt.qt.core/src/org/eclipse/cdt/qt/core/QtKeywords.java @@ -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; + } + } } diff --git a/qt/org.eclipse.cdt.qt.tests/pom.xml b/qt/org.eclipse.cdt.qt.tests/pom.xml index 00e53027593..486b72828be 100644 --- a/qt/org.eclipse.cdt.qt.tests/pom.xml +++ b/qt/org.eclipse.cdt.qt.tests/pom.xml @@ -31,8 +31,7 @@ tycho-surefire-plugin ${tycho-version} - false - + true ${base.ui.test.vmargs} -ea -Xms256m -Xmx512m -XX:MaxPermSize=256M **/AllQtTests.* diff --git a/qt/org.eclipse.cdt.qt.tests/src/org/eclipse/cdt/qt/tests/AllQtTests.java b/qt/org.eclipse.cdt.qt.tests/src/org/eclipse/cdt/qt/tests/AllQtTests.java index fb9ee92b740..6bae3815f45 100644 --- a/qt/org.eclipse.cdt.qt.tests/src/org/eclipse/cdt/qt/tests/AllQtTests.java +++ b/qt/org.eclipse.cdt.qt.tests/src/org/eclipse/cdt/qt/tests/AllQtTests.java @@ -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); } diff --git a/qt/org.eclipse.cdt.qt.tests/src/org/eclipse/cdt/qt/tests/SimpleTests.java b/qt/org.eclipse.cdt.qt.tests/src/org/eclipse/cdt/qt/tests/QMakeTests.java similarity index 98% rename from qt/org.eclipse.cdt.qt.tests/src/org/eclipse/cdt/qt/tests/SimpleTests.java rename to qt/org.eclipse.cdt.qt.tests/src/org/eclipse/cdt/qt/tests/QMakeTests.java index f3784f0a28e..13aaf72aaef 100644 --- a/qt/org.eclipse.cdt.qt.tests/src/org/eclipse/cdt/qt/tests/SimpleTests.java +++ b/qt/org.eclipse.cdt.qt.tests/src/org/eclipse/cdt/qt/tests/QMakeTests.java @@ -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. diff --git a/qt/org.eclipse.cdt.qt.tests/src/org/eclipse/cdt/qt/tests/QtContentAssistantTests.java b/qt/org.eclipse.cdt.qt.tests/src/org/eclipse/cdt/qt/tests/QtContentAssistantTests.java new file mode 100644 index 00000000000..48b18d0c656 --- /dev/null +++ b/qt/org.eclipse.cdt.qt.tests/src/org/eclipse/cdt/qt/tests/QtContentAssistantTests.java @@ -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; + } + }; +} diff --git a/qt/org.eclipse.cdt.qt.ui/META-INF/MANIFEST.MF b/qt/org.eclipse.cdt.qt.ui/META-INF/MANIFEST.MF index ddddef63eeb..822d3594c6e 100644 --- a/qt/org.eclipse.cdt.qt.ui/META-INF/MANIFEST.MF +++ b/qt/org.eclipse.cdt.qt.ui/META-INF/MANIFEST.MF @@ -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 diff --git a/qt/org.eclipse.cdt.qt.ui/src/org/eclipse/cdt/internal/qt/ui/QObjectConnectCompletion.java b/qt/org.eclipse.cdt.qt.ui/src/org/eclipse/cdt/internal/qt/ui/QObjectConnectCompletion.java new file mode 100644 index 00000000000..a68dd4fec89 --- /dev/null +++ b/qt/org.eclipse.cdt.qt.ui/src/org/eclipse/cdt/internal/qt/ui/QObjectConnectCompletion.java @@ -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 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 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 positions = new ArrayList(); + 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 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 completions = new ArrayList(); + 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 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 getProposals( + ICEditorContentAssistInvocationContext context, IASTName name, IASTCompletionContext astContext, IASTNode astNode) { + + Collection qtProposals = getConnectProposals(context, name, astContext, astNode); + if (qtProposals == null + || qtProposals.isEmpty()) + return null; + + Collection proposals = new ArrayList(); + for (QObjectConnectCompletion qtProposal : qtProposals) + qtProposal.addProposals(proposals, context); + return proposals; + } +} diff --git a/qt/org.eclipse.cdt.qt.ui/src/org/eclipse/cdt/internal/qt/ui/QObjectDeclarationCompletion.java b/qt/org.eclipse.cdt.qt.ui/src/org/eclipse/cdt/internal/qt/ui/QObjectDeclarationCompletion.java new file mode 100644 index 00000000000..3fc354352b1 --- /dev/null +++ b/qt/org.eclipse.cdt.qt.ui/src/org/eclipse/cdt/internal/qt/ui/QObjectDeclarationCompletion.java @@ -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 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.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()); + } +} diff --git a/qt/org.eclipse.cdt.qt.ui/src/org/eclipse/cdt/internal/qt/ui/QPropertyCompletion.java b/qt/org.eclipse.cdt.qt.ui/src/org/eclipse/cdt/internal/qt/ui/QPropertyCompletion.java new file mode 100644 index 00000000000..31e1734b52e --- /dev/null +++ b/qt/org.eclipse.cdt.qt.ui/src/org/eclipse/cdt/internal/qt/ui/QPropertyCompletion.java @@ -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 getAttributeProposals(ICEditorContentAssistInvocationContext context) { + QPropertyExpansion expansion = QPropertyExpansion.create(context); + return expansion == null + ? Collections.emptyList() + : expansion.getProposals(CONTEXT_ID, context); + } + + public static Collection 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.singletonList(new QtTemplateProposal(QPropertyTemplate, templateCtx, region)); + } +} diff --git a/qt/org.eclipse.cdt.qt.ui/src/org/eclipse/cdt/internal/qt/ui/QtCompletionProposalComputer.java b/qt/org.eclipse.cdt.qt.ui/src/org/eclipse/cdt/internal/qt/ui/QtCompletionProposalComputer.java index 78511171803..baa0e1808c4 100644 --- a/qt/org.eclipse.cdt.qt.ui/src/org/eclipse/cdt/internal/qt/ui/QtCompletionProposalComputer.java +++ b/qt/org.eclipse.cdt.qt.ui/src/org/eclipse/cdt/internal/qt/ui/QtCompletionProposalComputer.java @@ -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 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 filterMethods(final ICPPClassType cls, - final MethodFilter filter) { - return new Iterable() { - @Override - public Iterator iterator() { - return new Iterator() { - 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 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 positions = new ArrayList(); - 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 proposals, - ICEditorContentAssistInvocationContext context, - IASTCompletionNode completionNode, String prefix) { - IASTName[] names = completionNode.getNames(); - List completions = new LinkedList(); - - 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 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 proposals = new ArrayList(); - addConnectParameterCompletions(proposals, context, completionNode, - prefix); - return proposals; + List 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.emptyList() : proposals; + } + + private static List addAll(List list, Collection toAdd) { + if (toAdd == null + || toAdd.isEmpty()) + return list; + + if (list == null) + return new ArrayList(toAdd); + + list.addAll(toAdd); + return list; } } diff --git a/qt/org.eclipse.cdt.qt.ui/src/org/eclipse/cdt/internal/qt/ui/assist/QPropertyAttributeProposal.java b/qt/org.eclipse.cdt.qt.ui/src/org/eclipse/cdt/internal/qt/ui/assist/QPropertyAttributeProposal.java new file mode 100644 index 00000000000..be8ad1810c4 --- /dev/null +++ b/qt/org.eclipse.cdt.qt.ui/src/org/eclipse/cdt/internal/qt/ui/assist/QPropertyAttributeProposal.java @@ -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 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 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 filtered = new ArrayList(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 proposals = new ArrayList(); + for(ICPPMethod method : getMethods(context, methodAttribute)) + proposals.add(new QPropertyAttributeProposal(method.getName(), getDisplay(cls, method), methodAttribute.getRelevance(method))); + + return proposals; + } + + private static Collection 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 proposals = new ArrayList(); + 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 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 filtered = new ArrayList(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); + } +} diff --git a/qt/org.eclipse.cdt.qt.ui/src/org/eclipse/cdt/internal/qt/ui/assist/QPropertyExpansion.java b/qt/org.eclipse.cdt.qt.ui/src/org/eclipse/cdt/internal/qt/ui/assist/QPropertyExpansion.java new file mode 100644 index 00000000000..20ce678358f --- /dev/null +++ b/qt/org.eclipse.cdt.qt.ui/src/org/eclipse/cdt/internal/qt/ui/assist/QPropertyExpansion.java @@ -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 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 unspecifiedAttributes = new ArrayList(); + for(IQProperty.Attribute attr : IQProperty.Attribute.values()) { + if (attr.hasValue + && (prevIdentifier != null && attr.identifier.equals(prevIdentifier.ident))) { + + Collection attrProposals = QPropertyAttributeProposal.buildProposals(attr, context, type, name); + if (attrProposals != null) { + List proposals = new ArrayList(); + 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 proposals = new ArrayList(); + 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); + } +} diff --git a/qt/org.eclipse.cdt.qt.ui/src/org/eclipse/cdt/internal/qt/ui/assist/QtProposalContext.java b/qt/org.eclipse.cdt.qt.ui/src/org/eclipse/cdt/internal/qt/ui/assist/QtProposalContext.java new file mode 100644 index 00000000000..b4dc4574878 --- /dev/null +++ b/qt/org.eclipse.cdt.qt.ui/src/org/eclipse/cdt/internal/qt/ui/assist/QtProposalContext.java @@ -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 == '#'; + } +} diff --git a/qt/org.eclipse.cdt.qt.ui/src/org/eclipse/cdt/internal/qt/ui/assist/QtTemplateProposal.java b/qt/org.eclipse.cdt.qt.ui/src/org/eclipse/cdt/internal/qt/ui/assist/QtTemplateProposal.java new file mode 100644 index 00000000000..4c7fd6cd96f --- /dev/null +++ b/qt/org.eclipse.cdt.qt.ui/src/org/eclipse/cdt/internal/qt/ui/assist/QtTemplateProposal.java @@ -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(); + } +} diff --git a/qt/org.eclipse.cdt.qt.ui/src/org/eclipse/cdt/qt/ui/QtUIPlugin.java b/qt/org.eclipse.cdt.qt.ui/src/org/eclipse/cdt/qt/ui/QtUIPlugin.java index a24fb4de002..a88b877f2e9 100644 --- a/qt/org.eclipse.cdt.qt.ui/src/org/eclipse/cdt/qt/ui/QtUIPlugin.java +++ b/qt/org.eclipse.cdt.qt.ui/src/org/eclipse/cdt/qt/ui/QtUIPlugin.java @@ -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)