/*
* Copyright (c) 2013, 2015 QNX Software Systems and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*/
package org.eclipse.cdt.internal.qt.ui.assist;
import java.util.ArrayList;
import java.util.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.QtKeywords;
import org.eclipse.cdt.internal.qt.core.index.IQProperty;
import org.eclipse.cdt.internal.qt.core.parser.QtParser;
import org.eclipse.cdt.internal.qt.ui.Activator;
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.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) {
Activator.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) {
Activator.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, Activator.getQtLogo(), attribute.identifier + " - Q_PROPERTY declaration parameter", relevance);
// Otherwise create a template where the content depends on the type of the attribute's parameter.
String display = attribute.identifier + ' ' + attribute.paramName;
String replacement = attribute.identifier;
if ("bool".equals(attribute.paramName))
replacement += " ${true}";
else if ("int".equals(attribute.paramName))
replacement += " ${0}";
else if (attribute.paramName != null)
replacement += " ${" + attribute.paramName + '}';
return templateProposal(contextId, context, display, replacement, relevance);
}
}
private static ICompletionProposal templateProposal(String contextId, ICEditorContentAssistInvocationContext context, String display, String replacement, int relevance) {
Template template = new Template(display, "Q_PROPERTY declaration parameter", contextId, replacement, true);
TemplateContextType ctxType = new CContextType();
ctxType.setId(contextId);
QtProposalContext templateCtx = new QtProposalContext(context, ctxType);
Region region = new Region(templateCtx.getCompletionOffset(), templateCtx.getCompletionLength());
return new QtTemplateProposal(template, templateCtx, region, relevance);
}
public List<ICompletionProposal> getProposals(String contextId, ICEditorContentAssistInvocationContext context) {
// Make no suggestions when the start of the current identifier is before the end of
// the "type name" portion of the declaration.
if (currIdentifier.start < startOfAttrs)
return Collections.emptyList();
// Propose nothing but READ as the first attribute. If the previous identifier is before
// the end of the typeName region, then we're currently at the first attribute.
if (prevIdentifier.start < startOfAttrs)
return Collections.singletonList(new Attribute(IQProperty.Attribute.READ).getProposal(contextId, context));
// If the previous token is an Attribute name that has a parameter then suggest appropriate
// values for that parameter. Otherwise suggest the other Attribute names.
String prefix = getPrefix();
// There are two types of proposals. If the previous identifier matches a known attribute name,
// then we propose possible values for that attribute. Otherwise we want to propose the identifiers
// that don't already appear in the expansion.
//
// This is implemented by iterating over the list of known attributes. If any of the attributes
// matches the previous identifier, then we build and return a list of valid proposals for that
// attribute.
//
// Otherwise, for each attribute we build a regular expression that checks to see if that token
// appears within the expansion. If it already appears, then the attribute is ignored. Otherwise
// it is added as an unspecified attribute. If the loop completes, then we create a list of proposals
// for from that unspecified list.
List<Attribute> unspecifiedAttributes = new ArrayList<Attribute>();
for(IQProperty.Attribute attr : IQProperty.Attribute.values()) {
if (attr.hasValue
&& (prevIdentifier != null && attr.identifier.equals(prevIdentifier.ident))) {
Collection<QPropertyAttributeProposal> attrProposals = QPropertyAttributeProposal.buildProposals(attr, context, type, name);
if (attrProposals != null) {
List<ICompletionProposal> proposals = new ArrayList<ICompletionProposal>();
for(QPropertyAttributeProposal value : attrProposals)
if (prefix == null
|| value.getIdentifier().startsWith(prefix))
proposals.add(value.createProposal(prefix, context.getInvocationOffset()));
return proposals;
}
return Collections.emptyList();
}
if (prefix != null) {
if (attr.identifier.startsWith(prefix)
&&(!expansion.matches(".*\\s+" + attr.identifier + "\\s+.*")
||attr.identifier.equals(currIdentifier.ident)))
unspecifiedAttributes.add(new Attribute(attr));
} else if (!expansion.matches(".*\\s+" + attr.identifier + "\\s+.*"))
unspecifiedAttributes.add(new Attribute(attr));
}
List<ICompletionProposal> proposals = new ArrayList<ICompletionProposal>();
for(Attribute attr : unspecifiedAttributes) {
ICompletionProposal proposal = attr.getProposal(contextId, context);
if (proposal != null)
proposals.add(proposal);
}
return proposals;
}
private static Identifier identifier(IDocument doc, CHeuristicScanner scanner, int cursor, int lower, int upper) {
try {
// If the cursor is in whitespace, then the current identifier is null. Scan backward to find
// the start of this whitespace.
if (Character.isWhitespace(doc.getChar(cursor - 1))) {
int prev = scanner.findNonWhitespaceBackward(cursor, lower);
return new Identifier(Math.min(cursor, prev + 1), null);
}
int tok = scanner.previousToken(cursor, lower);
if (tok != CHeuristicScanner.TokenIDENT)
return null;
int begin = scanner.getPosition() + 1;
tok = scanner.nextToken(begin, upper);
if (tok != CHeuristicScanner.TokenIDENT)
return null;
int end = scanner.getPosition();
return new Identifier(begin, doc.get(begin, end - begin));
} catch(BadLocationException e) {
Activator.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);
}
}