/* * Copyright (c) 2013, 2016 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.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.qt.core.QtKeywords; import org.eclipse.cdt.internal.qt.core.index.IQMethod; import org.eclipse.cdt.internal.qt.core.index.IQObject; import org.eclipse.cdt.internal.qt.core.index.QtIndex; import org.eclipse.cdt.internal.ui.text.contentassist.CCompletionProposal; import org.eclipse.cdt.internal.ui.text.contentassist.RelevanceConstants; 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, Activator.getQtLogo(), display, relevance, context.getViewer()); p.setCursorPosition(repLength + cursorOffset); return p; } } private static void addProposal(Collection<ICompletionProposal> proposals, ICEditorContentAssistInvocationContext context, Data data, int relevance) { if (data == null) return; ICompletionProposal proposal = data.createProposal(context, relevance); if (proposal != null) proposals.add(proposal); } private void addProposals(Collection<ICompletionProposal> proposals, ICEditorContentAssistInvocationContext context) { if (data != null) addProposal(proposals, context, data, MACRO_PARAM_RELEVANCE); else switch(param) { case Signal: addProposal(proposals, context, Data.SIGNAL, MACRO_RELEVANCE); break; case Member: addProposal(proposals, context, Data.SLOT, MACRO_RELEVANCE); addProposal(proposals, context, Data.SIGNAL, MACRO_RELEVANCE - 1); break; default: break; } } // Copied from org.eclipse.cdt.internal.ui.text.CParameterListValidator private static int indexOfClosingPeer(String code, char left, char right, int pos) { int level = 0; final int length = code.length(); while (pos < length) { char ch = code.charAt(pos); if (ch == left) { ++level; } else if (ch == right) { if (--level == 0) { return pos; } } ++pos; } return -1; } // Copied from org.eclipse.cdt.internal.ui.text.CParameterListValidator private static int[] computeCommaPositions(String code) { final int length = code.length(); int pos = 0; List<Integer> positions = new ArrayList<Integer>(); positions.add(-1); while (pos < length && pos != -1) { char ch = code.charAt(pos); switch (ch) { case ',': positions.add(Integer.valueOf(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(Integer.valueOf(length)); int[] fields = new int[positions.size()]; for (int i = 0; i < fields.length; i++) fields[i] = positions.get(i).intValue(); return fields; } private static Collection<QObjectConnectCompletion> getCompletionsFor(IType targetType, IASTInitializerClause arg) { if (!(targetType instanceof ICPPClassType)) return null; ICPPClassType cls = (ICPPClassType) targetType; QtIndex qtIndex = QtIndex.getIndex(ASTUtil.getProject(arg)); if (qtIndex == null) return null; IQObject qobj = null; try { qobj = qtIndex.findQObject(cls.getQualifiedName()); } catch(DOMException e) { CCorePlugin.log(e); } // QtIndex.findQObject will return null in some cases, e.g., when the parameter is null if (qobj == null) return null; Collection<QObjectConnectCompletion> completions = new ArrayList<QObjectConnectCompletion>(); String raw = arg.getRawSignature(); if (raw.startsWith(QtKeywords.SIGNAL)) for(IQMethod method : qobj.getSignals().withoutOverrides()) for(String signature : method.getSignatures()) completions.add(new QObjectConnectCompletion(signature)); if (raw.startsWith(QtKeywords.SLOT)) for(IQMethod method : qobj.getSlots().withoutOverrides()) for(String signature : method.getSignatures()) completions.add(new QObjectConnectCompletion(signature)); return completions; } public static Collection<QObjectConnectCompletion> getConnectProposals( ICEditorContentAssistInvocationContext context, IASTName name, IASTCompletionContext astContext, IASTNode astNode) { if (QtFunctionCallUtil.isQObjectFunctionCall(astContext, !context.isContextInformationStyle(), name)) { int parseOffset = context.getParseOffset(); int invocationOffset = context.getInvocationOffset(); String unparsed = ""; try { unparsed = context.getDocument().get(parseOffset, invocationOffset - parseOffset); } catch (BadLocationException e) { CCorePlugin.log(e); } if (unparsed.length() > 0 && unparsed.charAt(0) == '(') unparsed = unparsed.substring(1); int[] commas = computeCommaPositions(unparsed); switch (commas.length) { case 2: case 3: // Across all possible connect/disconnect overloads, the first and second arguments // can be SIGNAL expansion. return Collections.singletonList(new QObjectConnectCompletion(QObjectConnectCompletion.Param.Signal)); case 4: case 5: // Across all possible connect/disconnect overloads, the first and second arguments // can be SIGNAL or SLOT expansions. return Collections.singletonList(new QObjectConnectCompletion(QObjectConnectCompletion.Param.Member)); } return null; } if (astNode.getPropertyInParent() == IASTFunctionCallExpression.ARGUMENT) { IASTNode parent = astNode.getParent(); if (!(parent instanceof IASTFunctionCallExpression)) return null; // NOTE: QtConnectFunctionCall cannot be used here because that class expects a // valid expression. During content assist the function is still being // created. IASTFunctionCallExpression call = (IASTFunctionCallExpression) parent; IASTExpression nameExpr = call.getFunctionNameExpression(); IASTName funcName = null; if (nameExpr instanceof IASTIdExpression) funcName = ((IASTIdExpression) nameExpr).getName(); else if (nameExpr instanceof ICPPASTFieldReference) funcName = ((ICPPASTFieldReference) nameExpr).getFieldName(); // If this isn't a QObject::connect or QObject::disconnect function call then // look no further. if (!QtFunctionCallUtil.isQObjectFunctionCall(astContext, !context.isContextInformationStyle(), funcName)) return null; // In a content assist context the argument that is currently being entered is // last in the function call. IASTInitializerClause[] args = call.getArguments(); if (args == null || args.length < 0) return null; int argIndex = args.length - 1; // Find the type node that is used for this expansion. IType targetType = QtFunctionCallUtil.getTargetType(call, args, argIndex); if (targetType == null) return null; // Returns completions for the given expansion using the given type as the // source for Qt methods. return getCompletionsFor(targetType, args[argIndex]); } return null; } public static Collection<ICompletionProposal> getProposals( ICEditorContentAssistInvocationContext context, IASTName name, IASTCompletionContext astContext, IASTNode astNode) { Collection<QObjectConnectCompletion> qtProposals = getConnectProposals(context, name, astContext, astNode); if (qtProposals == null || qtProposals.isEmpty()) return null; Collection<ICompletionProposal> proposals = new ArrayList<ICompletionProposal>(); for (QObjectConnectCompletion qtProposal : qtProposals) qtProposal.addProposals(proposals, context); return proposals; } }