diff options
Diffstat (limited to 'eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/AndroidContentAssist.java')
-rw-r--r-- | eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/AndroidContentAssist.java | 1331 |
1 files changed, 0 insertions, 1331 deletions
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/AndroidContentAssist.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/AndroidContentAssist.java deleted file mode 100644 index 5aac51f68..000000000 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/AndroidContentAssist.java +++ /dev/null @@ -1,1331 +0,0 @@ -/* - * Copyright (C) 2007 The Android Open Source Project - * - * Licensed under the Eclipse Public License, Version 1.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.eclipse.org/org/documents/epl-v10.php - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.ide.eclipse.adt.internal.editors; - -import static com.android.SdkConstants.ANDROID_URI; -import static com.android.SdkConstants.ATTR_LAYOUT_RESOURCE_PREFIX; -import static com.android.SdkConstants.PREFIX_ANDROID; -import static com.android.SdkConstants.PREFIX_RESOURCE_REF; -import static com.android.SdkConstants.PREFIX_THEME_REF; -import static com.android.SdkConstants.UNIT_DP; -import static com.android.SdkConstants.UNIT_IN; -import static com.android.SdkConstants.UNIT_MM; -import static com.android.SdkConstants.UNIT_PT; -import static com.android.SdkConstants.UNIT_PX; -import static com.android.SdkConstants.UNIT_SP; -import static com.android.ide.eclipse.adt.internal.editors.descriptors.AttributeDescriptor.ATTRIBUTE_ICON_FILENAME; - -import com.android.ide.common.api.IAttributeInfo; -import com.android.ide.common.api.IAttributeInfo.Format; -import com.android.ide.eclipse.adt.AdtPlugin; -import com.android.ide.eclipse.adt.internal.editors.descriptors.AttributeDescriptor; -import com.android.ide.eclipse.adt.internal.editors.descriptors.ElementDescriptor; -import com.android.ide.eclipse.adt.internal.editors.descriptors.IDescriptorProvider; -import com.android.ide.eclipse.adt.internal.editors.descriptors.SeparatorAttributeDescriptor; -import com.android.ide.eclipse.adt.internal.editors.descriptors.TextValueDescriptor; -import com.android.ide.eclipse.adt.internal.editors.layout.gle2.DomUtilities; -import com.android.ide.eclipse.adt.internal.editors.uimodel.UiAttributeNode; -import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode; -import com.android.ide.eclipse.adt.internal.editors.uimodel.UiFlagAttributeNode; -import com.android.ide.eclipse.adt.internal.editors.uimodel.UiResourceAttributeNode; -import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData; -import com.android.utils.Pair; -import com.android.utils.XmlUtils; - -import org.eclipse.core.runtime.IStatus; -import org.eclipse.jdt.core.IType; -import org.eclipse.jdt.ui.ISharedImages; -import org.eclipse.jdt.ui.JavaUI; -import org.eclipse.jface.text.BadLocationException; -import org.eclipse.jface.text.IDocument; -import org.eclipse.jface.text.IRegion; -import org.eclipse.jface.text.ITextViewer; -import org.eclipse.jface.text.contentassist.ICompletionProposal; -import org.eclipse.jface.text.contentassist.IContentAssistProcessor; -import org.eclipse.jface.text.contentassist.IContextInformation; -import org.eclipse.jface.text.contentassist.IContextInformationValidator; -import org.eclipse.jface.text.source.ISourceViewer; -import org.eclipse.swt.graphics.Image; -import org.eclipse.wst.sse.core.internal.provisional.IndexedRegion; -import org.w3c.dom.Node; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Comparator; -import java.util.EnumSet; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.regex.Pattern; - -/** - * Content Assist Processor for Android XML files - * <p> - * Remaining corner cases: - * <ul> - * <li>Completion does not work right if there is a space between the = and the opening - * quote. - * <li>Replacement completion does not work right if the caret is to the left of the - * opening quote, where the opening quote is a single quote, and the replacement items use - * double quotes. - * </ul> - */ -@SuppressWarnings("restriction") // XML model -public abstract class AndroidContentAssist implements IContentAssistProcessor { - - /** Regexp to detect a full attribute after an element tag. - * <pre>Syntax: - * name = "..." quoted string with all but < and " - * or: - * name = '...' quoted string with all but < and ' - * </pre> - */ - private static Pattern sFirstAttribute = Pattern.compile( - "^ *[a-zA-Z_:]+ *= *(?:\"[^<\"]*\"|'[^<']*')"); //$NON-NLS-1$ - - /** Regexp to detect an element tag name */ - private static Pattern sFirstElementWord = Pattern.compile("^[a-zA-Z0-9_:.-]+"); //$NON-NLS-1$ - - /** Regexp to detect whitespace */ - private static Pattern sWhitespace = Pattern.compile("\\s+"); //$NON-NLS-1$ - - protected final static String ROOT_ELEMENT = ""; - - /** Descriptor of the root of the XML hierarchy. This a "fake" ElementDescriptor which - * is used to list all the possible roots given by actual implementations. - * DO NOT USE DIRECTLY. Call {@link #getRootDescriptor()} instead. */ - private ElementDescriptor mRootDescriptor; - - private final int mDescriptorId; - - protected AndroidXmlEditor mEditor; - - /** - * Constructor for AndroidContentAssist - * @param descriptorId An id for {@link AndroidTargetData#getDescriptorProvider(int)}. - * The Id can be one of {@link AndroidTargetData#DESCRIPTOR_MANIFEST}, - * {@link AndroidTargetData#DESCRIPTOR_LAYOUT}, - * {@link AndroidTargetData#DESCRIPTOR_MENU}, - * or {@link AndroidTargetData#DESCRIPTOR_OTHER_XML}. - * All other values will throw an {@link IllegalArgumentException} later at runtime. - */ - public AndroidContentAssist(int descriptorId) { - mDescriptorId = descriptorId; - } - - /** - * Returns a list of completion proposals based on the - * specified location within the document that corresponds - * to the current cursor position within the text viewer. - * - * @param viewer the viewer whose document is used to compute the proposals - * @param offset an offset within the document for which completions should be computed - * @return an array of completion proposals or <code>null</code> if no proposals are possible - * - * @see org.eclipse.jface.text.contentassist.IContentAssistProcessor#computeCompletionProposals(org.eclipse.jface.text.ITextViewer, int) - */ - @Override - public ICompletionProposal[] computeCompletionProposals(ITextViewer viewer, int offset) { - String wordPrefix = extractElementPrefix(viewer, offset); - - if (mEditor == null) { - mEditor = AndroidXmlEditor.fromTextViewer(viewer); - if (mEditor == null) { - // This should not happen. Duck and forget. - AdtPlugin.log(IStatus.ERROR, "Editor not found during completion"); - return null; - } - } - - // List of proposals, in the order presented to the user. - List<ICompletionProposal> proposals = new ArrayList<ICompletionProposal>(80); - - // Look up the caret context - where in an element, or between elements, or - // within an element's children, is the given caret offset located? - Pair<Node, Node> context = DomUtilities.getNodeContext(viewer.getDocument(), offset); - if (context == null) { - return null; - } - Node parentNode = context.getFirst(); - Node currentNode = context.getSecond(); - assert parentNode != null || currentNode != null; - - UiElementNode rootUiNode = mEditor.getUiRootNode(); - if (currentNode == null || currentNode.getNodeType() == Node.TEXT_NODE) { - UiElementNode parentUiNode = - rootUiNode == null ? null : rootUiNode.findXmlNode(parentNode); - computeTextValues(proposals, offset, parentNode, currentNode, parentUiNode, - wordPrefix); - } else if (currentNode.getNodeType() == Node.ELEMENT_NODE) { - String parent = currentNode.getNodeName(); - AttribInfo info = parseAttributeInfo(viewer, offset, offset - wordPrefix.length()); - char nextChar = extractChar(viewer, offset); - if (info != null) { - // check to see if we can find a UiElementNode matching this XML node - UiElementNode currentUiNode = rootUiNode == null - ? null : rootUiNode.findXmlNode(currentNode); - computeAttributeProposals(proposals, viewer, offset, wordPrefix, currentUiNode, - parentNode, currentNode, parent, info, nextChar); - } else { - computeNonAttributeProposals(viewer, offset, wordPrefix, proposals, parentNode, - currentNode, parent, nextChar); - } - } - - return proposals.toArray(new ICompletionProposal[proposals.size()]); - } - - private void computeNonAttributeProposals(ITextViewer viewer, int offset, String wordPrefix, - List<ICompletionProposal> proposals, Node parentNode, Node currentNode, String parent, - char nextChar) { - if (startsWith(parent, wordPrefix)) { - // We are still editing the element's tag name, not the attributes - // (the element's tag name may not even be complete) - - Object[] choices = getChoicesForElement(parent, currentNode); - if (choices == null || choices.length == 0) { - return; - } - - int replaceLength = parent.length() - wordPrefix.length(); - boolean isNew = replaceLength == 0 && nextNonspaceChar(viewer, offset) == '<'; - // Special case: if we are right before the beginning of a new - // element, wipe out the replace length such that we insert before it, - // we don't edit the current element. - if (wordPrefix.length() == 0 && nextChar == '<') { - replaceLength = 0; - isNew = true; - } - - // If we found some suggestions, do we need to add an opening "<" bracket - // for the element? We don't if the cursor is right after "<" or "</". - // Per XML Spec, there's no whitespace between "<" or "</" and the tag name. - char needTag = computeElementNeedTag(viewer, offset, wordPrefix); - - addMatchingProposals(proposals, choices, offset, - parentNode != null ? parentNode : null, wordPrefix, needTag, - false /* isAttribute */, isNew, false /*isComplete*/, - replaceLength); - } - } - - private void computeAttributeProposals(List<ICompletionProposal> proposals, ITextViewer viewer, - int offset, String wordPrefix, UiElementNode currentUiNode, Node parentNode, - Node currentNode, String parent, AttribInfo info, char nextChar) { - // We're editing attributes in an element node (either the attributes' names - // or their values). - - if (info.isInValue) { - if (computeAttributeValues(proposals, offset, parent, info.name, currentNode, - wordPrefix, info.skipEndTag, info.replaceLength)) { - return; - } - } - - // Look up attribute proposals based on descriptors - Object[] choices = getChoicesForAttribute(parent, currentNode, currentUiNode, - info, wordPrefix); - if (choices == null || choices.length == 0) { - return; - } - - int replaceLength = info.replaceLength; - if (info.correctedPrefix != null) { - wordPrefix = info.correctedPrefix; - } - char needTag = info.needTag; - // Look to the right and see if we're followed by whitespace - boolean isNew = replaceLength == 0 - && (Character.isWhitespace(nextChar) || nextChar == '>' || nextChar == '/'); - - addMatchingProposals(proposals, choices, offset, parentNode != null ? parentNode : null, - wordPrefix, needTag, true /* isAttribute */, isNew, info.skipEndTag, - replaceLength); - } - - private char computeElementNeedTag(ITextViewer viewer, int offset, String wordPrefix) { - char needTag = 0; - int offset2 = offset - wordPrefix.length() - 1; - char c1 = extractChar(viewer, offset2); - if (!((c1 == '<') || (c1 == '/' && extractChar(viewer, offset2 - 1) == '<'))) { - needTag = '<'; - } - return needTag; - } - - protected int computeTextReplaceLength(Node currentNode, int offset) { - if (currentNode == null) { - return 0; - } - - assert currentNode != null && currentNode.getNodeType() == Node.TEXT_NODE; - - String nodeValue = currentNode.getNodeValue(); - int relativeOffset = offset - ((IndexedRegion) currentNode).getStartOffset(); - int lineEnd = nodeValue.indexOf('\n', relativeOffset); - if (lineEnd == -1) { - lineEnd = nodeValue.length(); - } - return lineEnd - relativeOffset; - } - - /** - * Gets the choices when the user is editing the name of an XML element. - * <p/> - * The user is editing the name of an element (the "parent"). - * Find the grand-parent and if one is found, return its children element list. - * The name which is being edited should be one of those. - * <p/> - * Example: <manifest><applic*cursor* => returns the list of all elements that - * can be found under <manifest>, of which <application> is one of the choices. - * - * @return an ElementDescriptor[] or null if no valid element was found. - */ - protected Object[] getChoicesForElement(String parent, Node currentNode) { - ElementDescriptor grandparent = null; - if (currentNode.getParentNode().getNodeType() == Node.ELEMENT_NODE) { - grandparent = getDescriptor(currentNode.getParentNode().getNodeName()); - } else if (currentNode.getParentNode().getNodeType() == Node.DOCUMENT_NODE) { - grandparent = getRootDescriptor(); - } - if (grandparent != null) { - for (ElementDescriptor e : grandparent.getChildren()) { - if (e.getXmlName().startsWith(parent)) { - return sort(grandparent.getChildren()); - } - } - } - - return null; - } - - /** Non-destructively sort a list of ElementDescriptors and return the result */ - protected static ElementDescriptor[] sort(ElementDescriptor[] elements) { - if (elements != null && elements.length > 1) { - // Sort alphabetically. Must make copy to not destroy original. - ElementDescriptor[] copy = new ElementDescriptor[elements.length]; - System.arraycopy(elements, 0, copy, 0, elements.length); - - Arrays.sort(copy, new Comparator<ElementDescriptor>() { - @Override - public int compare(ElementDescriptor e1, ElementDescriptor e2) { - return e1.getXmlLocalName().compareTo(e2.getXmlLocalName()); - } - }); - - return copy; - } - - return elements; - } - - /** - * Gets the choices when the user is editing an XML attribute. - * <p/> - * In input, attrInfo contains details on the analyzed context, namely whether the - * user is editing an attribute value (isInValue) or an attribute name. - * <p/> - * In output, attrInfo also contains two possible new values (this is a hack to circumvent - * the lack of out-parameters in Java): - * - AttribInfo.correctedPrefix if the user has been editing an attribute value and it has - * been detected that what the user typed is different from what extractElementPrefix() - * predicted. This happens because extractElementPrefix() stops when a character that - * cannot be an element name appears whereas parseAttributeInfo() uses a grammar more - * lenient as suitable for attribute values. - * - AttribInfo.needTag will be non-zero if we find that the attribute completion proposal - * must be double-quoted. - * @param currentUiNode - * - * @return an AttributeDescriptor[] if the user is editing an attribute name. - * a String[] if the user is editing an attribute value with some known values, - * or null if nothing is known about the context. - */ - private Object[] getChoicesForAttribute( - String parent, Node currentNode, UiElementNode currentUiNode, AttribInfo attrInfo, - String wordPrefix) { - Object[] choices = null; - if (attrInfo.isInValue) { - // Editing an attribute's value... Get the attribute name and then the - // possible choices for the tuple(parent,attribute) - String value = attrInfo.valuePrefix; - if (value.startsWith("'") || value.startsWith("\"")) { //$NON-NLS-1$ //$NON-NLS-2$ - value = value.substring(1); - // The prefix that was found at the beginning only scan for characters - // valid for tag name. We now know the real prefix for this attribute's - // value, which is needed to generate the completion choices below. - attrInfo.correctedPrefix = value; - } else { - attrInfo.needTag = '"'; - } - - if (currentUiNode != null) { - // look for an UI attribute matching the current attribute name - String attrName = attrInfo.name; - // remove any namespace prefix from the attribute name - int pos = attrName.indexOf(':'); - if (pos >= 0) { - attrName = attrName.substring(pos + 1); - } - - UiAttributeNode currAttrNode = null; - for (UiAttributeNode attrNode : currentUiNode.getAllUiAttributes()) { - if (attrNode.getDescriptor().getXmlLocalName().equals(attrName)) { - currAttrNode = attrNode; - break; - } - } - - if (currAttrNode != null) { - choices = getAttributeValueChoices(currAttrNode, attrInfo, value); - } - } - - if (choices == null) { - // fallback on the older descriptor-only based lookup. - - // in order to properly handle the special case of the name attribute in - // the action tag, we need the grandparent of the action node, to know - // what type of actions we need. - // e.g. activity -> intent-filter -> action[@name] - String greatGrandParentName = null; - Node grandParent = currentNode.getParentNode(); - if (grandParent != null) { - Node greatGrandParent = grandParent.getParentNode(); - if (greatGrandParent != null) { - greatGrandParentName = greatGrandParent.getLocalName(); - } - } - - AndroidTargetData data = mEditor.getTargetData(); - if (data != null) { - choices = data.getAttributeValues(parent, attrInfo.name, greatGrandParentName); - } - } - } else { - // Editing an attribute's name... Get attributes valid for the parent node. - if (currentUiNode != null) { - choices = currentUiNode.getAttributeDescriptors(); - } else { - ElementDescriptor parentDesc = getDescriptor(parent); - if (parentDesc != null) { - choices = parentDesc.getAttributes(); - } - } - } - return choices; - } - - protected Object[] getAttributeValueChoices(UiAttributeNode currAttrNode, AttribInfo attrInfo, - String value) { - Object[] choices; - int pos; - choices = currAttrNode.getPossibleValues(value); - if (choices != null && currAttrNode instanceof UiResourceAttributeNode) { - attrInfo.skipEndTag = false; - } - - if (currAttrNode instanceof UiFlagAttributeNode) { - // A "flag" can consist of several values separated by "or" (|). - // If the correct prefix contains such a pipe character, we change - // it so that only the currently edited value is completed. - pos = value.lastIndexOf('|'); - if (pos >= 0) { - attrInfo.correctedPrefix = value = value.substring(pos + 1); - attrInfo.needTag = 0; - } - - attrInfo.skipEndTag = false; - } - - // Should we do suffix completion on dimension units etc? - choices = completeSuffix(choices, value, currAttrNode); - - // Check to see if the user is attempting resource completion - AttributeDescriptor attributeDescriptor = currAttrNode.getDescriptor(); - IAttributeInfo attributeInfo = attributeDescriptor.getAttributeInfo(); - if (value.startsWith(PREFIX_RESOURCE_REF) - && !attributeInfo.getFormats().contains(Format.REFERENCE)) { - // Special case: If the attribute value looks like a reference to a - // resource, offer to complete it, since in many cases our metadata - // does not correctly state whether a resource value is allowed. We don't - // offer these for an empty completion context, but if the user has - // actually typed "@", in that case list resource matches. - // For example, for android:minHeight this makes completion on @dimen/ - // possible. - choices = UiResourceAttributeNode.computeResourceStringMatches( - mEditor, attributeDescriptor, value); - attrInfo.skipEndTag = false; - } else if (value.startsWith(PREFIX_THEME_REF) - && !attributeInfo.getFormats().contains(Format.REFERENCE)) { - choices = UiResourceAttributeNode.computeResourceStringMatches( - mEditor, attributeDescriptor, value); - attrInfo.skipEndTag = false; - } - - return choices; - } - - /** - * Compute attribute values. Return true if the complete set of values was - * added, so addition descriptor information should not be added. - */ - protected boolean computeAttributeValues(List<ICompletionProposal> proposals, int offset, - String parentTagName, String attributeName, Node node, String wordPrefix, - boolean skipEndTag, int replaceLength) { - return false; - } - - protected void computeTextValues(List<ICompletionProposal> proposals, int offset, - Node parentNode, Node currentNode, UiElementNode uiParent, - String wordPrefix) { - - if (parentNode != null) { - // Examine the parent of the text node. - Object[] choices = getElementChoicesForTextNode(parentNode); - if (choices != null && choices.length > 0) { - ISourceViewer viewer = mEditor.getStructuredSourceViewer(); - char needTag = computeElementNeedTag(viewer, offset, wordPrefix); - - int replaceLength = 0; - addMatchingProposals(proposals, choices, - offset, parentNode, wordPrefix, needTag, - false /* isAttribute */, - false /*isNew*/, - false /*isComplete*/, - replaceLength); - } - } - } - - /** - * Gets the choices when the user is editing an XML text node. - * <p/> - * This means the user is editing outside of any XML element or attribute. - * Simply return the list of XML elements that can be present there, based on the - * parent of the current node. - * - * @return An ElementDescriptor[] or null. - */ - protected ElementDescriptor[] getElementChoicesForTextNode(Node parentNode) { - ElementDescriptor[] choices = null; - String parent; - if (parentNode.getNodeType() == Node.ELEMENT_NODE) { - // We're editing a text node which parent is an element node. Limit - // content assist to elements valid for the parent. - parent = parentNode.getNodeName(); - ElementDescriptor desc = getDescriptor(parent); - if (desc == null && parent.indexOf('.') != -1) { - // The parent is a custom view and we don't have metadata about its - // allowable children, so just assume any normal layout tag is - // legal - desc = mRootDescriptor; - } - - if (desc != null) { - choices = sort(desc.getChildren()); - } - } else if (parentNode.getNodeType() == Node.DOCUMENT_NODE) { - // We're editing a text node at the first level (i.e. root node). - // Limit content assist to the only valid root elements. - choices = sort(getRootDescriptor().getChildren()); - } - - return choices; - } - - /** - * Given a list of choices, adds in any that match the current prefix into the - * proposals list. - * <p/> - * Choices is an object array. Items of the array can be: - * - ElementDescriptor: a possible element descriptor which XML name should be completed. - * - AttributeDescriptor: a possible attribute descriptor which XML name should be completed. - * - String: string values to display as-is to the user. Typically those are possible - * values for a given attribute. - * - Pair of Strings: the first value is the keyword to insert, and the second value - * is the tooltip/help for the value to be displayed in the documentation popup. - */ - protected void addMatchingProposals(List<ICompletionProposal> proposals, Object[] choices, - int offset, Node currentNode, String wordPrefix, char needTag, - boolean isAttribute, boolean isNew, boolean skipEndTag, int replaceLength) { - if (choices == null) { - return; - } - - Map<String, String> nsUriMap = new HashMap<String, String>(); - boolean haveLayoutParams = false; - - for (Object choice : choices) { - String keyword = null; - String nsPrefix = null; - String nsUri = null; - Image icon = null; - String tooltip = null; - if (choice instanceof ElementDescriptor) { - keyword = ((ElementDescriptor)choice).getXmlName(); - icon = ((ElementDescriptor)choice).getGenericIcon(); - // Tooltip computed lazily in {@link CompletionProposal} - } else if (choice instanceof TextValueDescriptor) { - continue; // Value nodes are not part of the completion choices - } else if (choice instanceof SeparatorAttributeDescriptor) { - continue; // not real attribute descriptors - } else if (choice instanceof AttributeDescriptor) { - keyword = ((AttributeDescriptor)choice).getXmlLocalName(); - icon = ((AttributeDescriptor)choice).getGenericIcon(); - // Tooltip computed lazily in {@link CompletionProposal} - - // Get the namespace URI for the attribute. Note that some attributes - // do not have a namespace and thus return null here. - nsUri = ((AttributeDescriptor)choice).getNamespaceUri(); - if (nsUri != null) { - nsPrefix = nsUriMap.get(nsUri); - if (nsPrefix == null) { - nsPrefix = XmlUtils.lookupNamespacePrefix(currentNode, nsUri, false); - nsUriMap.put(nsUri, nsPrefix); - } - } - if (nsPrefix != null) { - nsPrefix += ":"; //$NON-NLS-1$ - } - - } else if (choice instanceof String) { - keyword = (String) choice; - if (isAttribute) { - icon = IconFactory.getInstance().getIcon(ATTRIBUTE_ICON_FILENAME); - } - } else if (choice instanceof Pair<?, ?>) { - @SuppressWarnings("unchecked") - Pair<String, String> pair = (Pair<String, String>) choice; - keyword = pair.getFirst(); - tooltip = pair.getSecond(); - if (isAttribute) { - icon = IconFactory.getInstance().getIcon(ATTRIBUTE_ICON_FILENAME); - } - } else if (choice instanceof IType) { - IType type = (IType) choice; - keyword = type.getFullyQualifiedName(); - icon = JavaUI.getSharedImages().getImage(ISharedImages.IMG_OBJS_CUNIT); - } else { - continue; // discard unknown choice - } - - String nsKeyword = nsPrefix == null ? keyword : (nsPrefix + keyword); - - if (nameStartsWith(nsKeyword, wordPrefix, nsPrefix)) { - keyword = nsKeyword; - String endTag = ""; //$NON-NLS-1$ - if (needTag != 0) { - if (needTag == '"') { - keyword = needTag + keyword; - endTag = String.valueOf(needTag); - } else if (needTag == '<') { - if (elementCanHaveChildren(choice)) { - endTag = String.format("></%1$s>", keyword); //$NON-NLS-1$ - } else { - endTag = "/>"; //$NON-NLS-1$ - } - keyword = needTag + keyword + ' '; - } else if (needTag == ' ') { - keyword = needTag + keyword; - } - } else if (!isAttribute && isNew) { - if (elementCanHaveChildren(choice)) { - endTag = String.format("></%1$s>", keyword); //$NON-NLS-1$ - } else { - endTag = "/>"; //$NON-NLS-1$ - } - keyword = keyword + ' '; - } - - final String suffix; - int cursorPosition; - final String displayString; - if (choice instanceof AttributeDescriptor && isNew) { - // Special case for attributes: insert ="" stuff and locate caret inside "" - suffix = "=\"\""; //$NON-NLS-1$ - cursorPosition = keyword.length() + suffix.length() - 1; - displayString = keyword + endTag; // don't include suffix; - } else { - suffix = endTag; - cursorPosition = keyword.length(); - displayString = null; - } - - if (skipEndTag) { - assert isAttribute; - cursorPosition++; - } - - if (nsPrefix != null && - keyword.startsWith(ATTR_LAYOUT_RESOURCE_PREFIX, nsPrefix.length())) { - haveLayoutParams = true; - } - - // For attributes, automatically insert ns:attribute="" and place the cursor - // inside the quotes. - // Special case for attributes: insert ="" stuff and locate caret inside "" - proposals.add(new CompletionProposal( - this, - choice, - keyword + suffix, // String replacementString - offset - wordPrefix.length(), // int replacementOffset - wordPrefix.length() + replaceLength,// int replacementLength - cursorPosition, // cursorPosition - icon, // Image image - displayString, // displayString - null, // IContextInformation contextInformation - tooltip, // String additionalProposalInfo - nsPrefix, - nsUri - )); - } - } - - if (wordPrefix.length() > 0 && haveLayoutParams - && !wordPrefix.startsWith(ATTR_LAYOUT_RESOURCE_PREFIX)) { - // Sort layout parameters to the front if we automatically inserted some - // that you didn't request. For example, you typed "width" and we match both - // "width" and "layout_width" - should match layout_width. - String nsPrefix = nsUriMap.get(ANDROID_URI); - if (nsPrefix == null) { - nsPrefix = PREFIX_ANDROID; - } else { - nsPrefix += ':'; - } - if (!(wordPrefix.startsWith(nsPrefix) - && wordPrefix.startsWith(ATTR_LAYOUT_RESOURCE_PREFIX, nsPrefix.length()))) { - int nextLayoutIndex = 0; - for (int i = 0, n = proposals.size(); i < n; i++) { - ICompletionProposal proposal = proposals.get(i); - String keyword = proposal.getDisplayString(); - if (keyword.startsWith(nsPrefix) && - keyword.startsWith(ATTR_LAYOUT_RESOURCE_PREFIX, nsPrefix.length()) - && i != nextLayoutIndex) { - // Swap to front - ICompletionProposal temp = proposals.get(nextLayoutIndex); - proposals.set(nextLayoutIndex, proposal); - proposals.set(i, temp); - nextLayoutIndex++; - } - } - } - } - } - - /** - * Returns true if the given word starts with the given prefix. The comparison is not - * case sensitive. - * - * @param word the word to test - * @param prefix the prefix the word should start with - * @return true if the given word starts with the given prefix - */ - protected static boolean startsWith(String word, String prefix) { - int prefixLength = prefix.length(); - int wordLength = word.length(); - if (wordLength < prefixLength) { - return false; - } - - for (int i = 0; i < prefixLength; i++) { - if (Character.toLowerCase(prefix.charAt(i)) - != Character.toLowerCase(word.charAt(i))) { - return false; - } - } - - return true; - } - - /** @return the editor associated with this content assist */ - AndroidXmlEditor getEditor() { - return mEditor; - } - - /** - * This method performs a prefix match for the given word and prefix, with a couple of - * Android code completion specific twists: - * <ol> - * <li> The match is not case sensitive, so {word="fOo",prefix="FoO"} is a match. - * <li>If the word to be matched has a namespace prefix, the typed prefix doesn't have - * to match it. So {word="android:foo", prefix="foo"} is a match. - * <li>If the attribute name part starts with "layout_" it can be omitted. So - * {word="android:layout_marginTop",prefix="margin"} is a match, as is - * {word="android:layout_marginTop",prefix="android:margin"}. - * </ol> - * - * @param word the full word to be matched, including namespace if any - * @param prefix the prefix to check - * @param nsPrefix the namespace prefix (android: or local definition of android - * namespace prefix) - * @return true if the prefix matches for code completion - */ - protected static boolean nameStartsWith(String word, String prefix, String nsPrefix) { - if (nsPrefix == null) { - nsPrefix = ""; //$NON-NLS-1$ - } - - int wordStart = nsPrefix.length(); - int prefixStart = 0; - - if (startsWith(prefix, nsPrefix)) { - // Already matches up through the namespace prefix: - prefixStart = wordStart; - } else if (startsWith(nsPrefix, prefix)) { - return true; - } - - int prefixLength = prefix.length(); - int wordLength = word.length(); - - if (wordLength - wordStart < prefixLength - prefixStart) { - return false; - } - - boolean matches = true; - for (int i = prefixStart, j = wordStart; i < prefixLength; i++, j++) { - char c1 = Character.toLowerCase(prefix.charAt(i)); - char c2 = Character.toLowerCase(word.charAt(j)); - if (c1 != c2) { - matches = false; - break; - } - } - - if (!matches && word.startsWith(ATTR_LAYOUT_RESOURCE_PREFIX, wordStart) - && !prefix.startsWith(ATTR_LAYOUT_RESOURCE_PREFIX, prefixStart)) { - wordStart += ATTR_LAYOUT_RESOURCE_PREFIX.length(); - - if (wordLength - wordStart < prefixLength - prefixStart) { - return false; - } - - for (int i = prefixStart, j = wordStart; i < prefixLength; i++, j++) { - char c1 = Character.toLowerCase(prefix.charAt(i)); - char c2 = Character.toLowerCase(word.charAt(j)); - if (c1 != c2) { - return false; - } - } - - return true; - } - - return matches; - } - - /** - * Indicates whether this descriptor describes an element that can potentially - * have children (either sub-elements or text value). If an element can have children, - * we want to explicitly write an opening and a separate closing tag. - * <p/> - * Elements can have children if the descriptor has children element descriptors - * or if one of the attributes is a TextValueDescriptor. - * - * @param descriptor An ElementDescriptor or an AttributeDescriptor - * @return True if the descriptor is an ElementDescriptor that can have children or a text - * value - */ - private boolean elementCanHaveChildren(Object descriptor) { - if (descriptor instanceof ElementDescriptor) { - ElementDescriptor desc = (ElementDescriptor) descriptor; - if (desc.hasChildren()) { - return true; - } - for (AttributeDescriptor attrDesc : desc.getAttributes()) { - if (attrDesc instanceof TextValueDescriptor) { - return true; - } - } - } - return false; - } - - /** - * Returns the element descriptor matching a given XML node name or null if it can't be - * found. - * <p/> - * This is simplistic; ideally we should consider the parent's chain to make sure we - * can differentiate between different hierarchy trees. Right now the first match found - * is returned. - */ - private ElementDescriptor getDescriptor(String nodeName) { - return getRootDescriptor().findChildrenDescriptor(nodeName, true /* recursive */); - } - - @Override - public IContextInformation[] computeContextInformation(ITextViewer viewer, int offset) { - return null; - } - - /** - * Returns the characters which when entered by the user should - * automatically trigger the presentation of possible completions. - * - * In our case, we auto-activate on opening tags and attributes namespace. - * - * @return the auto activation characters for completion proposal or <code>null</code> - * if no auto activation is desired - */ - @Override - public char[] getCompletionProposalAutoActivationCharacters() { - return new char[]{ '<', ':', '=' }; - } - - @Override - public char[] getContextInformationAutoActivationCharacters() { - return null; - } - - @Override - public IContextInformationValidator getContextInformationValidator() { - return null; - } - - @Override - public String getErrorMessage() { - return null; - } - - /** - * Heuristically extracts the prefix used for determining template relevance - * from the viewer's document. The default implementation returns the String from - * offset backwards that forms a potential XML element name, attribute name or - * attribute value. - * - * The part were we access the document was extracted from - * org.eclipse.jface.text.templatesTemplateCompletionProcessor and adapted to our needs. - * - * @param viewer the viewer - * @param offset offset into document - * @return the prefix to consider - */ - protected String extractElementPrefix(ITextViewer viewer, int offset) { - int i = offset; - IDocument document = viewer.getDocument(); - if (i > document.getLength()) return ""; //$NON-NLS-1$ - - try { - for (; i > 0; --i) { - char ch = document.getChar(i - 1); - - // We want all characters that can form a valid: - // - element name, e.g. anything that is a valid Java class/variable literal. - // - attribute name, including : for the namespace - // - attribute value. - // Before we were inclusive and that made the code fragile. So now we're - // going to be exclusive: take everything till we get one of: - // - any form of whitespace - // - any xml separator, e.g. < > ' " and = - if (Character.isWhitespace(ch) || - ch == '<' || ch == '>' || ch == '\'' || ch == '"' || ch == '=') { - break; - } - } - - return document.get(i, offset - i); - } catch (BadLocationException e) { - return ""; //$NON-NLS-1$ - } - } - - /** - * Extracts the character at the given offset. - * Returns 0 if the offset is invalid. - */ - protected char extractChar(ITextViewer viewer, int offset) { - IDocument document = viewer.getDocument(); - if (offset > document.getLength()) return 0; - - try { - return document.getChar(offset); - } catch (BadLocationException e) { - return 0; - } - } - - /** - * Search forward and find the first non-space character and return it. Returns 0 if no - * such character was found. - */ - private char nextNonspaceChar(ITextViewer viewer, int offset) { - IDocument document = viewer.getDocument(); - int length = document.getLength(); - for (; offset < length; offset++) { - try { - char c = document.getChar(offset); - if (!Character.isWhitespace(c)) { - return c; - } - } catch (BadLocationException e) { - return 0; - } - } - - return 0; - } - - /** - * Information about the current edit of an attribute as reported by parseAttributeInfo. - */ - protected static class AttribInfo { - public AttribInfo() { - } - - /** True if the cursor is located in an attribute's value, false if in an attribute name */ - public boolean isInValue = false; - /** The attribute name. Null when not set. */ - public String name = null; - /** The attribute value top the left of the cursor. Null when not set. The value - * *may* start with a quote (' or "), in which case we know we don't need to quote - * the string for the user */ - public String valuePrefix = null; - /** String typed by the user so far (i.e. right before requesting code completion), - * which will be corrected if we find a possible completion for an attribute value. - * See the long comment in getChoicesForAttribute(). */ - public String correctedPrefix = null; - /** Non-zero if an attribute value need a start/end tag (i.e. quotes or brackets) */ - public char needTag = 0; - /** Number of characters to replace after the prefix */ - public int replaceLength = 0; - /** Should the cursor advance through the end tag when inserted? */ - public boolean skipEndTag = false; - } - - /** - * Try to guess if the cursor is editing an element's name or an attribute following an - * element. If it's an attribute, try to find if an attribute name is being defined or - * its value. - * <br/> - * This is currently *only* called when we know the cursor is after a complete element - * tag name, so it should never return null. - * <br/> - * Reference for XML syntax: http://www.w3.org/TR/2006/REC-xml-20060816/#sec-starttags - * <br/> - * @return An AttribInfo describing which attribute is being edited or null if the cursor is - * not editing an attribute (in which case it must be an element's name). - */ - private AttribInfo parseAttributeInfo(ITextViewer viewer, int offset, int prefixStartOffset) { - AttribInfo info = new AttribInfo(); - int originalOffset = offset; - - IDocument document = viewer.getDocument(); - int n = document.getLength(); - if (offset <= n) { - try { - // Look to the right to make sure we aren't sitting on the boundary of the - // beginning of a new element with whitespace before it - if (offset < n && document.getChar(offset) == '<') { - return null; - } - - n = offset; - for (;offset > 0; --offset) { - char ch = document.getChar(offset - 1); - if (ch == '>') break; - if (ch == '<') break; - } - - // text will contain the full string of the current element, - // i.e. whatever is after the "<" to the current cursor - String text = document.get(offset, n - offset); - - // Normalize whitespace to single spaces - text = sWhitespace.matcher(text).replaceAll(" "); //$NON-NLS-1$ - - // Remove the leading element name. By spec, it must be after the < without - // any whitespace. If there's nothing left, no attribute has been defined yet. - // Be sure to keep any whitespace after the initial word if any, as it matters. - text = sFirstElementWord.matcher(text).replaceFirst(""); //$NON-NLS-1$ - - // There MUST be space after the element name. If not, the cursor is still - // defining the element name. - if (!text.startsWith(" ")) { //$NON-NLS-1$ - return null; - } - - // Remove full attributes: - // Syntax: - // name = "..." quoted string with all but < and " - // or: - // name = '...' quoted string with all but < and ' - String temp; - do { - temp = text; - text = sFirstAttribute.matcher(temp).replaceFirst(""); //$NON-NLS-1$ - } while(!temp.equals(text)); - - IRegion lineInfo = document.getLineInformationOfOffset(originalOffset); - int lineStart = lineInfo.getOffset(); - String line = document.get(lineStart, lineInfo.getLength()); - int cursorColumn = originalOffset - lineStart; - int prefixLength = originalOffset - prefixStartOffset; - - // Now we're left with 3 cases: - // - nothing: either there is no attribute definition or the cursor located after - // a completed attribute definition. - // - a string with no =: the user is writing an attribute name. This case can be - // merged with the previous one. - // - string with an = sign, optionally followed by a quote (' or "): the user is - // writing the value of the attribute. - int posEqual = text.indexOf('='); - if (posEqual == -1) { - info.isInValue = false; - info.name = text.trim(); - - // info.name is currently just the prefix of the attribute name. - // Look at the text buffer to find the complete name (since we need - // to know its bounds in order to replace it when a different attribute - // that matches this prefix is chosen) - int nameStart = cursorColumn; - for (int nameEnd = nameStart; nameEnd < line.length(); nameEnd++) { - char c = line.charAt(nameEnd); - if (!(Character.isLetter(c) || c == ':' || c == '_')) { - String nameSuffix = line.substring(nameStart, nameEnd); - info.name = text.trim() + nameSuffix; - break; - } - } - - info.replaceLength = info.name.length() - prefixLength; - - if (info.name.length() == 0 && originalOffset > 0) { - // Ensure that attribute names are properly separated - char prevChar = extractChar(viewer, originalOffset - 1); - if (prevChar == '"' || prevChar == '\'') { - // Ensure that the attribute is properly separated from the - // previous element - info.needTag = ' '; - } - } - info.skipEndTag = false; - } else { - info.isInValue = true; - info.name = text.substring(0, posEqual).trim(); - info.valuePrefix = text.substring(posEqual + 1); - - char quoteChar = '"'; // Does " or ' surround the XML value? - for (int i = posEqual + 1; i < text.length(); i++) { - if (!Character.isWhitespace(text.charAt(i))) { - quoteChar = text.charAt(i); - break; - } - } - - // Must compute the complete value - int valueStart = cursorColumn; - int valueEnd = valueStart; - for (; valueEnd < line.length(); valueEnd++) { - char c = line.charAt(valueEnd); - if (c == quoteChar) { - // Make sure this isn't the *opening* quote of the value, - // which is the case if we invoke code completion with the - // caret between the = and the opening quote; in that case - // we consider it value completion, and offer items including - // the quotes, but we shouldn't bail here thinking we have found - // the end of the value. - // Look backwards to make sure we find another " before - // we find a = - boolean isFirst = false; - for (int j = valueEnd - 1; j >= 0; j--) { - char pc = line.charAt(j); - if (pc == '=') { - isFirst = true; - break; - } else if (pc == quoteChar) { - valueStart = j; - break; - } - } - if (!isFirst) { - info.skipEndTag = true; - break; - } - } - } - int valueEndOffset = valueEnd + lineStart; - info.replaceLength = valueEndOffset - (prefixStartOffset + prefixLength); - // Is the caret to the left of the value quote? If so, include it in - // the replace length. - int valueStartOffset = valueStart + lineStart; - if (valueStartOffset == prefixStartOffset && valueEnd > valueStart) { - info.replaceLength++; - } - } - return info; - } catch (BadLocationException e) { - // pass - } - } - - return null; - } - - /** Returns the root descriptor id to use */ - protected int getRootDescriptorId() { - return mDescriptorId; - } - - /** - * Computes (if needed) and returns the root descriptor. - */ - protected ElementDescriptor getRootDescriptor() { - if (mRootDescriptor == null) { - AndroidTargetData data = mEditor.getTargetData(); - if (data != null) { - IDescriptorProvider descriptorProvider = - data.getDescriptorProvider(getRootDescriptorId()); - - if (descriptorProvider != null) { - mRootDescriptor = new ElementDescriptor("", //$NON-NLS-1$ - descriptorProvider.getRootElementDescriptors()); - } - } - } - - return mRootDescriptor; - } - - /** - * Fixed list of dimension units, along with user documentation, for use by - * {@link #completeSuffix}. - */ - private static final String[] sDimensionUnits = new String[] { - UNIT_DP, - "<b>Density-independent Pixels</b> - an abstract unit that is based on the physical " - + "density of the screen.", - - UNIT_SP, - "<b>Scale-independent Pixels</b> - this is like the dp unit, but it is also scaled by " - + "the user's font size preference.", - - UNIT_PT, - "<b>Points</b> - 1/72 of an inch based on the physical size of the screen.", - - UNIT_MM, - "<b>Millimeters</b> - based on the physical size of the screen.", - - UNIT_IN, - "<b>Inches</b> - based on the physical size of the screen.", - - UNIT_PX, - "<b>Pixels</b> - corresponds to actual pixels on the screen. Not recommended.", - }; - - /** - * Fixed list of fractional units, along with user documentation, for use by - * {@link #completeSuffix} - */ - private static final String[] sFractionUnits = new String[] { - "%", //$NON-NLS-1$ - "<b>Fraction</b> - a percentage of the base size", - - "%p", //$NON-NLS-1$ - "<b>Fraction</b> - a percentage relative to parent container", - }; - - /** - * Completes suffixes for applicable types (like dimensions and fractions) such that - * after a dimension number you get completion on unit types like "px". - */ - private Object[] completeSuffix(Object[] choices, String value, UiAttributeNode currAttrNode) { - IAttributeInfo attributeInfo = currAttrNode.getDescriptor().getAttributeInfo(); - EnumSet<Format> formats = attributeInfo.getFormats(); - List<Object> suffixes = new ArrayList<Object>(); - - if (value.length() > 0 && Character.isDigit(value.charAt(0))) { - boolean hasDimension = formats.contains(Format.DIMENSION); - boolean hasFraction = formats.contains(Format.FRACTION); - - if (hasDimension || hasFraction) { - // Split up the value into a numeric part (the prefix) and the - // unit part (the suffix) - int suffixBegin = 0; - for (; suffixBegin < value.length(); suffixBegin++) { - if (!Character.isDigit(value.charAt(suffixBegin))) { - break; - } - } - String number = value.substring(0, suffixBegin); - String suffix = value.substring(suffixBegin); - - // Add in the matching dimension and/or fraction units, if any - if (hasDimension) { - // Each item has two entries in the array of strings: the first odd numbered - // ones are the unit names and the second even numbered ones are the - // corresponding descriptions. - for (int i = 0; i < sDimensionUnits.length; i += 2) { - String unit = sDimensionUnits[i]; - if (startsWith(unit, suffix)) { - String description = sDimensionUnits[i + 1]; - suffixes.add(Pair.of(number + unit, description)); - } - } - - // Allow "dip" completion but don't offer it ("dp" is preferred) - if (startsWith(suffix, "di") || startsWith(suffix, "dip")) { //$NON-NLS-1$ //$NON-NLS-2$ - suffixes.add(Pair.of(number + "dip", "Alternative name for \"dp\"")); //$NON-NLS-1$ - } - } - if (hasFraction) { - for (int i = 0; i < sFractionUnits.length; i += 2) { - String unit = sFractionUnits[i]; - if (startsWith(unit, suffix)) { - String description = sFractionUnits[i + 1]; - suffixes.add(Pair.of(number + unit, description)); - } - } - } - } - } - - boolean hasFlag = formats.contains(Format.FLAG); - if (hasFlag) { - boolean isDone = false; - String[] flagValues = attributeInfo.getFlagValues(); - for (String flagValue : flagValues) { - if (flagValue.equals(value)) { - isDone = true; - break; - } - } - if (isDone) { - // Add in all the new values with a separator of | - String currentValue = currAttrNode.getCurrentValue(); - for (String flagValue : flagValues) { - if (currentValue == null || !currentValue.contains(flagValue)) { - suffixes.add(value + '|' + flagValue); - } - } - } - } - - if (suffixes.size() > 0) { - // Merge previously added choices (from attribute enums etc) with the new matches - List<Object> all = new ArrayList<Object>(); - if (choices != null) { - for (Object s : choices) { - all.add(s); - } - } - all.addAll(suffixes); - choices = all.toArray(); - } - - return choices; - } -} |