Home | History | Annotate | Download | only in editors
      1 /*
      2  * Copyright (C) 2007 The Android Open Source Project
      3  *
      4  * Licensed under the Eclipse Public License, Version 1.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.eclipse.org/org/documents/epl-v10.php
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.ide.eclipse.adt.internal.editors;
     18 
     19 import static com.android.SdkConstants.ANDROID_URI;
     20 import static com.android.SdkConstants.ATTR_LAYOUT_RESOURCE_PREFIX;
     21 import static com.android.SdkConstants.PREFIX_ANDROID;
     22 import static com.android.SdkConstants.PREFIX_RESOURCE_REF;
     23 import static com.android.SdkConstants.PREFIX_THEME_REF;
     24 import static com.android.SdkConstants.UNIT_DP;
     25 import static com.android.SdkConstants.UNIT_IN;
     26 import static com.android.SdkConstants.UNIT_MM;
     27 import static com.android.SdkConstants.UNIT_PT;
     28 import static com.android.SdkConstants.UNIT_PX;
     29 import static com.android.SdkConstants.UNIT_SP;
     30 import static com.android.ide.eclipse.adt.internal.editors.descriptors.AttributeDescriptor.ATTRIBUTE_ICON_FILENAME;
     31 
     32 import com.android.ide.common.api.IAttributeInfo;
     33 import com.android.ide.common.api.IAttributeInfo.Format;
     34 import com.android.ide.eclipse.adt.AdtPlugin;
     35 import com.android.ide.eclipse.adt.internal.editors.descriptors.AttributeDescriptor;
     36 import com.android.ide.eclipse.adt.internal.editors.descriptors.ElementDescriptor;
     37 import com.android.ide.eclipse.adt.internal.editors.descriptors.IDescriptorProvider;
     38 import com.android.ide.eclipse.adt.internal.editors.descriptors.SeparatorAttributeDescriptor;
     39 import com.android.ide.eclipse.adt.internal.editors.descriptors.TextValueDescriptor;
     40 import com.android.ide.eclipse.adt.internal.editors.layout.gle2.DomUtilities;
     41 import com.android.ide.eclipse.adt.internal.editors.uimodel.UiAttributeNode;
     42 import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode;
     43 import com.android.ide.eclipse.adt.internal.editors.uimodel.UiFlagAttributeNode;
     44 import com.android.ide.eclipse.adt.internal.editors.uimodel.UiResourceAttributeNode;
     45 import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData;
     46 import com.android.utils.Pair;
     47 import com.android.utils.XmlUtils;
     48 
     49 import org.eclipse.core.runtime.IStatus;
     50 import org.eclipse.jdt.core.IType;
     51 import org.eclipse.jdt.ui.ISharedImages;
     52 import org.eclipse.jdt.ui.JavaUI;
     53 import org.eclipse.jface.text.BadLocationException;
     54 import org.eclipse.jface.text.IDocument;
     55 import org.eclipse.jface.text.IRegion;
     56 import org.eclipse.jface.text.ITextViewer;
     57 import org.eclipse.jface.text.contentassist.ICompletionProposal;
     58 import org.eclipse.jface.text.contentassist.IContentAssistProcessor;
     59 import org.eclipse.jface.text.contentassist.IContextInformation;
     60 import org.eclipse.jface.text.contentassist.IContextInformationValidator;
     61 import org.eclipse.jface.text.source.ISourceViewer;
     62 import org.eclipse.swt.graphics.Image;
     63 import org.eclipse.wst.sse.core.internal.provisional.IndexedRegion;
     64 import org.w3c.dom.Node;
     65 
     66 import java.util.ArrayList;
     67 import java.util.Arrays;
     68 import java.util.Comparator;
     69 import java.util.EnumSet;
     70 import java.util.HashMap;
     71 import java.util.List;
     72 import java.util.Map;
     73 import java.util.regex.Pattern;
     74 
     75 /**
     76  * Content Assist Processor for Android XML files
     77  * <p>
     78  * Remaining corner cases:
     79  * <ul>
     80  * <li>Completion does not work right if there is a space between the = and the opening
     81  *   quote.
     82  * <li>Replacement completion does not work right if the caret is to the left of the
     83  *   opening quote, where the opening quote is a single quote, and the replacement items use
     84  *   double quotes.
     85  * </ul>
     86  */
     87 @SuppressWarnings("restriction") // XML model
     88 public abstract class AndroidContentAssist implements IContentAssistProcessor {
     89 
     90     /** Regexp to detect a full attribute after an element tag.
     91      * <pre>Syntax:
     92      *    name = "..." quoted string with all but < and "
     93      * or:
     94      *    name = '...' quoted string with all but < and '
     95      * </pre>
     96      */
     97     private static Pattern sFirstAttribute = Pattern.compile(
     98             "^ *[a-zA-Z_:]+ *= *(?:\"[^<\"]*\"|'[^<']*')");  //$NON-NLS-1$
     99 
    100     /** Regexp to detect an element tag name */
    101     private static Pattern sFirstElementWord = Pattern.compile("^[a-zA-Z0-9_:.-]+"); //$NON-NLS-1$
    102 
    103     /** Regexp to detect whitespace */
    104     private static Pattern sWhitespace = Pattern.compile("\\s+"); //$NON-NLS-1$
    105 
    106     protected final static String ROOT_ELEMENT = "";
    107 
    108     /** Descriptor of the root of the XML hierarchy. This a "fake" ElementDescriptor which
    109      *  is used to list all the possible roots given by actual implementations.
    110      *  DO NOT USE DIRECTLY. Call {@link #getRootDescriptor()} instead. */
    111     private ElementDescriptor mRootDescriptor;
    112 
    113     private final int mDescriptorId;
    114 
    115     protected AndroidXmlEditor mEditor;
    116 
    117     /**
    118      * Constructor for AndroidContentAssist
    119      * @param descriptorId An id for {@link AndroidTargetData#getDescriptorProvider(int)}.
    120      *      The Id can be one of {@link AndroidTargetData#DESCRIPTOR_MANIFEST},
    121      *      {@link AndroidTargetData#DESCRIPTOR_LAYOUT},
    122      *      {@link AndroidTargetData#DESCRIPTOR_MENU},
    123      *      or {@link AndroidTargetData#DESCRIPTOR_OTHER_XML}.
    124      *      All other values will throw an {@link IllegalArgumentException} later at runtime.
    125      */
    126     public AndroidContentAssist(int descriptorId) {
    127         mDescriptorId = descriptorId;
    128     }
    129 
    130     /**
    131      * Returns a list of completion proposals based on the
    132      * specified location within the document that corresponds
    133      * to the current cursor position within the text viewer.
    134      *
    135      * @param viewer the viewer whose document is used to compute the proposals
    136      * @param offset an offset within the document for which completions should be computed
    137      * @return an array of completion proposals or <code>null</code> if no proposals are possible
    138      *
    139      * @see org.eclipse.jface.text.contentassist.IContentAssistProcessor#computeCompletionProposals(org.eclipse.jface.text.ITextViewer, int)
    140      */
    141     @Override
    142     public ICompletionProposal[] computeCompletionProposals(ITextViewer viewer, int offset) {
    143         String wordPrefix = extractElementPrefix(viewer, offset);
    144 
    145         if (mEditor == null) {
    146             mEditor = AndroidXmlEditor.fromTextViewer(viewer);
    147             if (mEditor == null) {
    148                 // This should not happen. Duck and forget.
    149                 AdtPlugin.log(IStatus.ERROR, "Editor not found during completion");
    150                 return null;
    151             }
    152         }
    153 
    154         // List of proposals, in the order presented to the user.
    155         List<ICompletionProposal> proposals = new ArrayList<ICompletionProposal>(80);
    156 
    157         // Look up the caret context - where in an element, or between elements, or
    158         // within an element's children, is the given caret offset located?
    159         Pair<Node, Node> context = DomUtilities.getNodeContext(viewer.getDocument(), offset);
    160         if (context == null) {
    161             return null;
    162         }
    163         Node parentNode = context.getFirst();
    164         Node currentNode = context.getSecond();
    165         assert parentNode != null || currentNode != null;
    166 
    167         UiElementNode rootUiNode = mEditor.getUiRootNode();
    168         if (currentNode == null || currentNode.getNodeType() == Node.TEXT_NODE) {
    169              UiElementNode parentUiNode =
    170                  rootUiNode == null ? null : rootUiNode.findXmlNode(parentNode);
    171              computeTextValues(proposals, offset, parentNode, currentNode, parentUiNode,
    172                     wordPrefix);
    173         } else if (currentNode.getNodeType() == Node.ELEMENT_NODE) {
    174             String parent = currentNode.getNodeName();
    175             AttribInfo info = parseAttributeInfo(viewer, offset, offset - wordPrefix.length());
    176             char nextChar = extractChar(viewer, offset);
    177             if (info != null) {
    178                 // check to see if we can find a UiElementNode matching this XML node
    179                 UiElementNode currentUiNode = rootUiNode == null
    180                     ? null : rootUiNode.findXmlNode(currentNode);
    181                 computeAttributeProposals(proposals, viewer, offset, wordPrefix, currentUiNode,
    182                         parentNode, currentNode, parent, info, nextChar);
    183             } else {
    184                 computeNonAttributeProposals(viewer, offset, wordPrefix, proposals, parentNode,
    185                         currentNode, parent, nextChar);
    186             }
    187         }
    188 
    189         return proposals.toArray(new ICompletionProposal[proposals.size()]);
    190     }
    191 
    192     private void computeNonAttributeProposals(ITextViewer viewer, int offset, String wordPrefix,
    193             List<ICompletionProposal> proposals, Node parentNode, Node currentNode, String parent,
    194             char nextChar) {
    195         if (startsWith(parent, wordPrefix)) {
    196             // We are still editing the element's tag name, not the attributes
    197             // (the element's tag name may not even be complete)
    198 
    199             Object[] choices = getChoicesForElement(parent, currentNode);
    200             if (choices == null || choices.length == 0) {
    201                 return;
    202             }
    203 
    204             int replaceLength = parent.length() - wordPrefix.length();
    205             boolean isNew = replaceLength == 0 && nextNonspaceChar(viewer, offset) == '<';
    206             // Special case: if we are right before the beginning of a new
    207             // element, wipe out the replace length such that we insert before it,
    208             // we don't edit the current element.
    209             if (wordPrefix.length() == 0 && nextChar == '<') {
    210                 replaceLength = 0;
    211                 isNew = true;
    212             }
    213 
    214             // If we found some suggestions, do we need to add an opening "<" bracket
    215             // for the element? We don't if the cursor is right after "<" or "</".
    216             // Per XML Spec, there's no whitespace between "<" or "</" and the tag name.
    217             char needTag = computeElementNeedTag(viewer, offset, wordPrefix);
    218 
    219             addMatchingProposals(proposals, choices, offset,
    220                     parentNode != null ? parentNode : null, wordPrefix, needTag,
    221                     false /* isAttribute */, isNew, false /*isComplete*/,
    222                     replaceLength);
    223         }
    224     }
    225 
    226     private void computeAttributeProposals(List<ICompletionProposal> proposals, ITextViewer viewer,
    227             int offset, String wordPrefix, UiElementNode currentUiNode, Node parentNode,
    228             Node currentNode, String parent, AttribInfo info, char nextChar) {
    229         // We're editing attributes in an element node (either the attributes' names
    230         // or their values).
    231 
    232         if (info.isInValue) {
    233             if (computeAttributeValues(proposals, offset, parent, info.name, currentNode,
    234                     wordPrefix, info.skipEndTag, info.replaceLength)) {
    235                 return;
    236             }
    237         }
    238 
    239         // Look up attribute proposals based on descriptors
    240         Object[] choices = getChoicesForAttribute(parent, currentNode, currentUiNode,
    241                 info, wordPrefix);
    242         if (choices == null || choices.length == 0) {
    243             return;
    244         }
    245 
    246         int replaceLength = info.replaceLength;
    247         if (info.correctedPrefix != null) {
    248             wordPrefix = info.correctedPrefix;
    249         }
    250         char needTag = info.needTag;
    251         // Look to the right and see if we're followed by whitespace
    252         boolean isNew = replaceLength == 0
    253             && (Character.isWhitespace(nextChar) || nextChar == '>' || nextChar == '/');
    254 
    255         addMatchingProposals(proposals, choices, offset, parentNode != null ? parentNode : null,
    256                 wordPrefix, needTag, true /* isAttribute */, isNew, info.skipEndTag,
    257                 replaceLength);
    258     }
    259 
    260     private char computeElementNeedTag(ITextViewer viewer, int offset, String wordPrefix) {
    261         char needTag = 0;
    262         int offset2 = offset - wordPrefix.length() - 1;
    263         char c1 = extractChar(viewer, offset2);
    264         if (!((c1 == '<') || (c1 == '/' && extractChar(viewer, offset2 - 1) == '<'))) {
    265             needTag = '<';
    266         }
    267         return needTag;
    268     }
    269 
    270     protected int computeTextReplaceLength(Node currentNode, int offset) {
    271         if (currentNode == null) {
    272             return 0;
    273         }
    274 
    275         assert currentNode != null && currentNode.getNodeType() == Node.TEXT_NODE;
    276 
    277         String nodeValue = currentNode.getNodeValue();
    278         int relativeOffset = offset - ((IndexedRegion) currentNode).getStartOffset();
    279         int lineEnd = nodeValue.indexOf('\n', relativeOffset);
    280         if (lineEnd == -1) {
    281             lineEnd = nodeValue.length();
    282         }
    283         return lineEnd - relativeOffset;
    284     }
    285 
    286     /**
    287      * Gets the choices when the user is editing the name of an XML element.
    288      * <p/>
    289      * The user is editing the name of an element (the "parent").
    290      * Find the grand-parent and if one is found, return its children element list.
    291      * The name which is being edited should be one of those.
    292      * <p/>
    293      * Example: <manifest><applic*cursor* => returns the list of all elements that
    294      * can be found under <manifest>, of which <application> is one of the choices.
    295      *
    296      * @return an ElementDescriptor[] or null if no valid element was found.
    297      */
    298     protected Object[] getChoicesForElement(String parent, Node currentNode) {
    299         ElementDescriptor grandparent = null;
    300         if (currentNode.getParentNode().getNodeType() == Node.ELEMENT_NODE) {
    301             grandparent = getDescriptor(currentNode.getParentNode().getNodeName());
    302         } else if (currentNode.getParentNode().getNodeType() == Node.DOCUMENT_NODE) {
    303             grandparent = getRootDescriptor();
    304         }
    305         if (grandparent != null) {
    306             for (ElementDescriptor e : grandparent.getChildren()) {
    307                 if (e.getXmlName().startsWith(parent)) {
    308                     return sort(grandparent.getChildren());
    309                 }
    310             }
    311         }
    312 
    313         return null;
    314     }
    315 
    316     /** Non-destructively sort a list of ElementDescriptors and return the result */
    317     protected static ElementDescriptor[] sort(ElementDescriptor[] elements) {
    318         if (elements != null && elements.length > 1) {
    319             // Sort alphabetically. Must make copy to not destroy original.
    320             ElementDescriptor[] copy = new ElementDescriptor[elements.length];
    321             System.arraycopy(elements, 0, copy, 0, elements.length);
    322 
    323             Arrays.sort(copy, new Comparator<ElementDescriptor>() {
    324                 @Override
    325                 public int compare(ElementDescriptor e1, ElementDescriptor e2) {
    326                     return e1.getXmlLocalName().compareTo(e2.getXmlLocalName());
    327                 }
    328             });
    329 
    330             return copy;
    331         }
    332 
    333         return elements;
    334     }
    335 
    336     /**
    337      * Gets the choices when the user is editing an XML attribute.
    338      * <p/>
    339      * In input, attrInfo contains details on the analyzed context, namely whether the
    340      * user is editing an attribute value (isInValue) or an attribute name.
    341      * <p/>
    342      * In output, attrInfo also contains two possible new values (this is a hack to circumvent
    343      * the lack of out-parameters in Java):
    344      * - AttribInfo.correctedPrefix if the user has been editing an attribute value and it has
    345      *   been detected that what the user typed is different from what extractElementPrefix()
    346      *   predicted. This happens because extractElementPrefix() stops when a character that
    347      *   cannot be an element name appears whereas parseAttributeInfo() uses a grammar more
    348      *   lenient as suitable for attribute values.
    349      * - AttribInfo.needTag will be non-zero if we find that the attribute completion proposal
    350      *   must be double-quoted.
    351      * @param currentUiNode
    352      *
    353      * @return an AttributeDescriptor[] if the user is editing an attribute name.
    354      *         a String[] if the user is editing an attribute value with some known values,
    355      *         or null if nothing is known about the context.
    356      */
    357     private Object[] getChoicesForAttribute(
    358             String parent, Node currentNode, UiElementNode currentUiNode, AttribInfo attrInfo,
    359             String wordPrefix) {
    360         Object[] choices = null;
    361         if (attrInfo.isInValue) {
    362             // Editing an attribute's value... Get the attribute name and then the
    363             // possible choices for the tuple(parent,attribute)
    364             String value = attrInfo.valuePrefix;
    365             if (value.startsWith("'") || value.startsWith("\"")) {   //$NON-NLS-1$   //$NON-NLS-2$
    366                 value = value.substring(1);
    367                 // The prefix that was found at the beginning only scan for characters
    368                 // valid for tag name. We now know the real prefix for this attribute's
    369                 // value, which is needed to generate the completion choices below.
    370                 attrInfo.correctedPrefix = value;
    371             } else {
    372                 attrInfo.needTag = '"';
    373             }
    374 
    375             if (currentUiNode != null) {
    376                 // look for an UI attribute matching the current attribute name
    377                 String attrName = attrInfo.name;
    378                 // remove any namespace prefix from the attribute name
    379                 int pos = attrName.indexOf(':');
    380                 if (pos >= 0) {
    381                     attrName = attrName.substring(pos + 1);
    382                 }
    383 
    384                 UiAttributeNode currAttrNode = null;
    385                 for (UiAttributeNode attrNode : currentUiNode.getAllUiAttributes()) {
    386                     if (attrNode.getDescriptor().getXmlLocalName().equals(attrName)) {
    387                         currAttrNode = attrNode;
    388                         break;
    389                     }
    390                 }
    391 
    392                 if (currAttrNode != null) {
    393                     choices = getAttributeValueChoices(currAttrNode, attrInfo, value);
    394                 }
    395             }
    396 
    397             if (choices == null) {
    398                 // fallback on the older descriptor-only based lookup.
    399 
    400                 // in order to properly handle the special case of the name attribute in
    401                 // the action tag, we need the grandparent of the action node, to know
    402                 // what type of actions we need.
    403                 // e.g. activity -> intent-filter -> action[@name]
    404                 String greatGrandParentName = null;
    405                 Node grandParent = currentNode.getParentNode();
    406                 if (grandParent != null) {
    407                     Node greatGrandParent = grandParent.getParentNode();
    408                     if (greatGrandParent != null) {
    409                         greatGrandParentName = greatGrandParent.getLocalName();
    410                     }
    411                 }
    412 
    413                 AndroidTargetData data = mEditor.getTargetData();
    414                 if (data != null) {
    415                     choices = data.getAttributeValues(parent, attrInfo.name, greatGrandParentName);
    416                 }
    417             }
    418         } else {
    419             // Editing an attribute's name... Get attributes valid for the parent node.
    420             if (currentUiNode != null) {
    421                 choices = currentUiNode.getAttributeDescriptors();
    422             } else {
    423                 ElementDescriptor parentDesc = getDescriptor(parent);
    424                 if (parentDesc != null) {
    425                     choices = parentDesc.getAttributes();
    426                 }
    427             }
    428         }
    429         return choices;
    430     }
    431 
    432     protected Object[] getAttributeValueChoices(UiAttributeNode currAttrNode, AttribInfo attrInfo,
    433             String value) {
    434         Object[] choices;
    435         int pos;
    436         choices = currAttrNode.getPossibleValues(value);
    437         if (choices != null && currAttrNode instanceof UiResourceAttributeNode) {
    438             attrInfo.skipEndTag = false;
    439         }
    440 
    441         if (currAttrNode instanceof UiFlagAttributeNode) {
    442             // A "flag" can consist of several values separated by "or" (|).
    443             // If the correct prefix contains such a pipe character, we change
    444             // it so that only the currently edited value is completed.
    445             pos = value.lastIndexOf('|');
    446             if (pos >= 0) {
    447                 attrInfo.correctedPrefix = value = value.substring(pos + 1);
    448                 attrInfo.needTag = 0;
    449             }
    450 
    451             attrInfo.skipEndTag = false;
    452         }
    453 
    454         // Should we do suffix completion on dimension units etc?
    455         choices = completeSuffix(choices, value, currAttrNode);
    456 
    457         // Check to see if the user is attempting resource completion
    458         AttributeDescriptor attributeDescriptor = currAttrNode.getDescriptor();
    459         IAttributeInfo attributeInfo = attributeDescriptor.getAttributeInfo();
    460         if (value.startsWith(PREFIX_RESOURCE_REF)
    461                 && !attributeInfo.getFormats().contains(Format.REFERENCE)) {
    462             // Special case: If the attribute value looks like a reference to a
    463             // resource, offer to complete it, since in many cases our metadata
    464             // does not correctly state whether a resource value is allowed. We don't
    465             // offer these for an empty completion context, but if the user has
    466             // actually typed "@", in that case list resource matches.
    467             // For example, for android:minHeight this makes completion on @dimen/
    468             // possible.
    469             choices = UiResourceAttributeNode.computeResourceStringMatches(
    470                     mEditor, attributeDescriptor, value);
    471             attrInfo.skipEndTag = false;
    472         } else if (value.startsWith(PREFIX_THEME_REF)
    473                 && !attributeInfo.getFormats().contains(Format.REFERENCE)) {
    474             choices = UiResourceAttributeNode.computeResourceStringMatches(
    475                     mEditor, attributeDescriptor, value);
    476             attrInfo.skipEndTag = false;
    477         }
    478 
    479         return choices;
    480     }
    481 
    482     /**
    483      * Compute attribute values. Return true if the complete set of values was
    484      * added, so addition descriptor information should not be added.
    485      */
    486     protected boolean computeAttributeValues(List<ICompletionProposal> proposals, int offset,
    487             String parentTagName, String attributeName, Node node, String wordPrefix,
    488             boolean skipEndTag, int replaceLength) {
    489         return false;
    490     }
    491 
    492     protected void computeTextValues(List<ICompletionProposal> proposals, int offset,
    493             Node parentNode, Node currentNode, UiElementNode uiParent,
    494             String wordPrefix) {
    495 
    496        if (parentNode != null) {
    497            // Examine the parent of the text node.
    498            Object[] choices = getElementChoicesForTextNode(parentNode);
    499            if (choices != null && choices.length > 0) {
    500                ISourceViewer viewer = mEditor.getStructuredSourceViewer();
    501                char needTag = computeElementNeedTag(viewer, offset, wordPrefix);
    502 
    503                int replaceLength = 0;
    504                addMatchingProposals(proposals, choices,
    505                        offset, parentNode, wordPrefix, needTag,
    506                                false /* isAttribute */,
    507                                false /*isNew*/,
    508                                false /*isComplete*/,
    509                                replaceLength);
    510            }
    511        }
    512     }
    513 
    514     /**
    515      * Gets the choices when the user is editing an XML text node.
    516      * <p/>
    517      * This means the user is editing outside of any XML element or attribute.
    518      * Simply return the list of XML elements that can be present there, based on the
    519      * parent of the current node.
    520      *
    521      * @return An ElementDescriptor[] or null.
    522      */
    523     protected ElementDescriptor[] getElementChoicesForTextNode(Node parentNode) {
    524         ElementDescriptor[] choices = null;
    525         String parent;
    526         if (parentNode.getNodeType() == Node.ELEMENT_NODE) {
    527             // We're editing a text node which parent is an element node. Limit
    528             // content assist to elements valid for the parent.
    529             parent = parentNode.getNodeName();
    530             ElementDescriptor desc = getDescriptor(parent);
    531             if (desc == null && parent.indexOf('.') != -1) {
    532                 // The parent is a custom view and we don't have metadata about its
    533                 // allowable children, so just assume any normal layout tag is
    534                 // legal
    535                 desc = mRootDescriptor;
    536             }
    537 
    538             if (desc != null) {
    539                 choices = sort(desc.getChildren());
    540             }
    541         } else if (parentNode.getNodeType() == Node.DOCUMENT_NODE) {
    542             // We're editing a text node at the first level (i.e. root node).
    543             // Limit content assist to the only valid root elements.
    544             choices = sort(getRootDescriptor().getChildren());
    545         }
    546 
    547         return choices;
    548     }
    549 
    550      /**
    551      * Given a list of choices, adds in any that match the current prefix into the
    552      * proposals list.
    553      * <p/>
    554      * Choices is an object array. Items of the array can be:
    555      * - ElementDescriptor: a possible element descriptor which XML name should be completed.
    556      * - AttributeDescriptor: a possible attribute descriptor which XML name should be completed.
    557      * - String: string values to display as-is to the user. Typically those are possible
    558      *           values for a given attribute.
    559      * - Pair of Strings: the first value is the keyword to insert, and the second value
    560      *           is the tooltip/help for the value to be displayed in the documentation popup.
    561      */
    562     protected void addMatchingProposals(List<ICompletionProposal> proposals, Object[] choices,
    563             int offset, Node currentNode, String wordPrefix, char needTag,
    564             boolean isAttribute, boolean isNew, boolean skipEndTag, int replaceLength) {
    565         if (choices == null) {
    566             return;
    567         }
    568 
    569         Map<String, String> nsUriMap = new HashMap<String, String>();
    570         boolean haveLayoutParams = false;
    571 
    572         for (Object choice : choices) {
    573             String keyword = null;
    574             String nsPrefix = null;
    575             String nsUri = null;
    576             Image icon = null;
    577             String tooltip = null;
    578             if (choice instanceof ElementDescriptor) {
    579                 keyword = ((ElementDescriptor)choice).getXmlName();
    580                 icon    = ((ElementDescriptor)choice).getGenericIcon();
    581                 // Tooltip computed lazily in {@link CompletionProposal}
    582             } else if (choice instanceof TextValueDescriptor) {
    583                 continue; // Value nodes are not part of the completion choices
    584             } else if (choice instanceof SeparatorAttributeDescriptor) {
    585                 continue; // not real attribute descriptors
    586             } else if (choice instanceof AttributeDescriptor) {
    587                 keyword = ((AttributeDescriptor)choice).getXmlLocalName();
    588                 icon    = ((AttributeDescriptor)choice).getGenericIcon();
    589                 // Tooltip computed lazily in {@link CompletionProposal}
    590 
    591                 // Get the namespace URI for the attribute. Note that some attributes
    592                 // do not have a namespace and thus return null here.
    593                 nsUri = ((AttributeDescriptor)choice).getNamespaceUri();
    594                 if (nsUri != null) {
    595                     nsPrefix = nsUriMap.get(nsUri);
    596                     if (nsPrefix == null) {
    597                         nsPrefix = XmlUtils.lookupNamespacePrefix(currentNode, nsUri, false);
    598                         nsUriMap.put(nsUri, nsPrefix);
    599                     }
    600                 }
    601                 if (nsPrefix != null) {
    602                     nsPrefix += ":"; //$NON-NLS-1$
    603                 }
    604 
    605             } else if (choice instanceof String) {
    606                 keyword = (String) choice;
    607                 if (isAttribute) {
    608                     icon = IconFactory.getInstance().getIcon(ATTRIBUTE_ICON_FILENAME);
    609                 }
    610             } else if (choice instanceof Pair<?, ?>) {
    611                 @SuppressWarnings("unchecked")
    612                 Pair<String, String> pair = (Pair<String, String>) choice;
    613                 keyword = pair.getFirst();
    614                 tooltip = pair.getSecond();
    615                 if (isAttribute) {
    616                     icon = IconFactory.getInstance().getIcon(ATTRIBUTE_ICON_FILENAME);
    617                 }
    618             } else if (choice instanceof IType) {
    619                 IType type = (IType) choice;
    620                 keyword = type.getFullyQualifiedName();
    621                 icon = JavaUI.getSharedImages().getImage(ISharedImages.IMG_OBJS_CUNIT);
    622             } else {
    623                 continue; // discard unknown choice
    624             }
    625 
    626             String nsKeyword = nsPrefix == null ? keyword : (nsPrefix + keyword);
    627 
    628             if (nameStartsWith(nsKeyword, wordPrefix, nsPrefix)) {
    629                 keyword = nsKeyword;
    630                 String endTag = ""; //$NON-NLS-1$
    631                 if (needTag != 0) {
    632                     if (needTag == '"') {
    633                         keyword = needTag + keyword;
    634                         endTag = String.valueOf(needTag);
    635                     } else if (needTag == '<') {
    636                         if (elementCanHaveChildren(choice)) {
    637                             endTag = String.format("></%1$s>", keyword);  //$NON-NLS-1$
    638                         } else {
    639                             endTag = "/>";  //$NON-NLS-1$
    640                         }
    641                         keyword = needTag + keyword + ' ';
    642                     } else if (needTag == ' ') {
    643                         keyword = needTag + keyword;
    644                     }
    645                 } else if (!isAttribute && isNew) {
    646                     if (elementCanHaveChildren(choice)) {
    647                         endTag = String.format("></%1$s>", keyword);  //$NON-NLS-1$
    648                     } else {
    649                         endTag = "/>";  //$NON-NLS-1$
    650                     }
    651                     keyword = keyword + ' ';
    652                 }
    653 
    654                 final String suffix;
    655                 int cursorPosition;
    656                 final String displayString;
    657                 if (choice instanceof AttributeDescriptor && isNew) {
    658                     // Special case for attributes: insert ="" stuff and locate caret inside ""
    659                     suffix = "=\"\""; //$NON-NLS-1$
    660                     cursorPosition = keyword.length() + suffix.length() - 1;
    661                     displayString = keyword + endTag; // don't include suffix;
    662                 } else {
    663                     suffix = endTag;
    664                     cursorPosition = keyword.length();
    665                     displayString = null;
    666                 }
    667 
    668                 if (skipEndTag) {
    669                     assert isAttribute;
    670                     cursorPosition++;
    671                 }
    672 
    673                 if (nsPrefix != null &&
    674                         keyword.startsWith(ATTR_LAYOUT_RESOURCE_PREFIX, nsPrefix.length())) {
    675                     haveLayoutParams = true;
    676                 }
    677 
    678                 // For attributes, automatically insert ns:attribute="" and place the cursor
    679                 // inside the quotes.
    680                 // Special case for attributes: insert ="" stuff and locate caret inside ""
    681                 proposals.add(new CompletionProposal(
    682                     this,
    683                     choice,
    684                     keyword + suffix,                   // String replacementString
    685                     offset - wordPrefix.length(),       // int replacementOffset
    686                     wordPrefix.length() + replaceLength,// int replacementLength
    687                     cursorPosition,                     // cursorPosition
    688                     icon,                               // Image image
    689                     displayString,                      // displayString
    690                     null,                               // IContextInformation contextInformation
    691                     tooltip,                            // String additionalProposalInfo
    692                     nsPrefix,
    693                     nsUri
    694                 ));
    695             }
    696         }
    697 
    698         if (wordPrefix.length() > 0 && haveLayoutParams
    699                 && !wordPrefix.startsWith(ATTR_LAYOUT_RESOURCE_PREFIX)) {
    700             // Sort layout parameters to the front if we automatically inserted some
    701             // that you didn't request. For example, you typed "width" and we match both
    702             // "width" and "layout_width" - should match layout_width.
    703             String nsPrefix = nsUriMap.get(ANDROID_URI);
    704             if (nsPrefix == null) {
    705                 nsPrefix = PREFIX_ANDROID;
    706             } else {
    707                 nsPrefix += ':';
    708             }
    709             if (!(wordPrefix.startsWith(nsPrefix)
    710                     && wordPrefix.startsWith(ATTR_LAYOUT_RESOURCE_PREFIX, nsPrefix.length()))) {
    711                 int nextLayoutIndex = 0;
    712                 for (int i = 0, n = proposals.size(); i < n; i++) {
    713                     ICompletionProposal proposal = proposals.get(i);
    714                     String keyword = proposal.getDisplayString();
    715                     if (keyword.startsWith(nsPrefix) &&
    716                             keyword.startsWith(ATTR_LAYOUT_RESOURCE_PREFIX, nsPrefix.length())
    717                             && i != nextLayoutIndex) {
    718                         // Swap to front
    719                         ICompletionProposal temp = proposals.get(nextLayoutIndex);
    720                         proposals.set(nextLayoutIndex, proposal);
    721                         proposals.set(i, temp);
    722                         nextLayoutIndex++;
    723                     }
    724                 }
    725             }
    726         }
    727     }
    728 
    729     /**
    730      * Returns true if the given word starts with the given prefix. The comparison is not
    731      * case sensitive.
    732      *
    733      * @param word the word to test
    734      * @param prefix the prefix the word should start with
    735      * @return true if the given word starts with the given prefix
    736      */
    737     protected static boolean startsWith(String word, String prefix) {
    738         int prefixLength = prefix.length();
    739         int wordLength = word.length();
    740         if (wordLength < prefixLength) {
    741             return false;
    742         }
    743 
    744         for (int i = 0; i < prefixLength; i++) {
    745             if (Character.toLowerCase(prefix.charAt(i))
    746                     != Character.toLowerCase(word.charAt(i))) {
    747                 return false;
    748             }
    749         }
    750 
    751         return true;
    752     }
    753 
    754     /** @return the editor associated with this content assist */
    755     AndroidXmlEditor getEditor() {
    756         return mEditor;
    757     }
    758 
    759     /**
    760      * This method performs a prefix match for the given word and prefix, with a couple of
    761      * Android code completion specific twists:
    762      * <ol>
    763      * <li> The match is not case sensitive, so {word="fOo",prefix="FoO"} is a match.
    764      * <li>If the word to be matched has a namespace prefix, the typed prefix doesn't have
    765      * to match it. So {word="android:foo", prefix="foo"} is a match.
    766      * <li>If the attribute name part starts with "layout_" it can be omitted. So
    767      * {word="android:layout_marginTop",prefix="margin"} is a match, as is
    768      * {word="android:layout_marginTop",prefix="android:margin"}.
    769      * </ol>
    770      *
    771      * @param word the full word to be matched, including namespace if any
    772      * @param prefix the prefix to check
    773      * @param nsPrefix the namespace prefix (android: or local definition of android
    774      *            namespace prefix)
    775      * @return true if the prefix matches for code completion
    776      */
    777     protected static boolean nameStartsWith(String word, String prefix, String nsPrefix) {
    778         if (nsPrefix == null) {
    779             nsPrefix = ""; //$NON-NLS-1$
    780         }
    781 
    782         int wordStart = nsPrefix.length();
    783         int prefixStart = 0;
    784 
    785         if (startsWith(prefix, nsPrefix)) {
    786             // Already matches up through the namespace prefix:
    787             prefixStart = wordStart;
    788         } else if (startsWith(nsPrefix, prefix)) {
    789             return true;
    790         }
    791 
    792         int prefixLength = prefix.length();
    793         int wordLength = word.length();
    794 
    795         if (wordLength - wordStart < prefixLength - prefixStart) {
    796             return false;
    797         }
    798 
    799         boolean matches = true;
    800         for (int i = prefixStart, j = wordStart; i < prefixLength; i++, j++) {
    801             char c1 = Character.toLowerCase(prefix.charAt(i));
    802             char c2 = Character.toLowerCase(word.charAt(j));
    803             if (c1 != c2) {
    804                 matches = false;
    805                 break;
    806             }
    807         }
    808 
    809         if (!matches && word.startsWith(ATTR_LAYOUT_RESOURCE_PREFIX, wordStart)
    810                 && !prefix.startsWith(ATTR_LAYOUT_RESOURCE_PREFIX, prefixStart)) {
    811             wordStart += ATTR_LAYOUT_RESOURCE_PREFIX.length();
    812 
    813             if (wordLength - wordStart < prefixLength - prefixStart) {
    814                 return false;
    815             }
    816 
    817             for (int i = prefixStart, j = wordStart; i < prefixLength; i++, j++) {
    818                 char c1 = Character.toLowerCase(prefix.charAt(i));
    819                 char c2 = Character.toLowerCase(word.charAt(j));
    820                 if (c1 != c2) {
    821                     return false;
    822                 }
    823             }
    824 
    825             return true;
    826         }
    827 
    828         return matches;
    829     }
    830 
    831     /**
    832      * Indicates whether this descriptor describes an element that can potentially
    833      * have children (either sub-elements or text value). If an element can have children,
    834      * we want to explicitly write an opening and a separate closing tag.
    835      * <p/>
    836      * Elements can have children if the descriptor has children element descriptors
    837      * or if one of the attributes is a TextValueDescriptor.
    838      *
    839      * @param descriptor An ElementDescriptor or an AttributeDescriptor
    840      * @return True if the descriptor is an ElementDescriptor that can have children or a text
    841      *         value
    842      */
    843     private boolean elementCanHaveChildren(Object descriptor) {
    844         if (descriptor instanceof ElementDescriptor) {
    845             ElementDescriptor desc = (ElementDescriptor) descriptor;
    846             if (desc.hasChildren()) {
    847                 return true;
    848             }
    849             for (AttributeDescriptor attrDesc : desc.getAttributes()) {
    850                 if (attrDesc instanceof TextValueDescriptor) {
    851                     return true;
    852                 }
    853             }
    854         }
    855         return false;
    856     }
    857 
    858     /**
    859      * Returns the element descriptor matching a given XML node name or null if it can't be
    860      * found.
    861      * <p/>
    862      * This is simplistic; ideally we should consider the parent's chain to make sure we
    863      * can differentiate between different hierarchy trees. Right now the first match found
    864      * is returned.
    865      */
    866     private ElementDescriptor getDescriptor(String nodeName) {
    867         return getRootDescriptor().findChildrenDescriptor(nodeName, true /* recursive */);
    868     }
    869 
    870     @Override
    871     public IContextInformation[] computeContextInformation(ITextViewer viewer, int offset) {
    872         return null;
    873     }
    874 
    875     /**
    876      * Returns the characters which when entered by the user should
    877      * automatically trigger the presentation of possible completions.
    878      *
    879      * In our case, we auto-activate on opening tags and attributes namespace.
    880      *
    881      * @return the auto activation characters for completion proposal or <code>null</code>
    882      *      if no auto activation is desired
    883      */
    884     @Override
    885     public char[] getCompletionProposalAutoActivationCharacters() {
    886         return new char[]{ '<', ':', '=' };
    887     }
    888 
    889     @Override
    890     public char[] getContextInformationAutoActivationCharacters() {
    891         return null;
    892     }
    893 
    894     @Override
    895     public IContextInformationValidator getContextInformationValidator() {
    896         return null;
    897     }
    898 
    899     @Override
    900     public String getErrorMessage() {
    901         return null;
    902     }
    903 
    904     /**
    905      * Heuristically extracts the prefix used for determining template relevance
    906      * from the viewer's document. The default implementation returns the String from
    907      * offset backwards that forms a potential XML element name, attribute name or
    908      * attribute value.
    909      *
    910      * The part were we access the document was extracted from
    911      * org.eclipse.jface.text.templatesTemplateCompletionProcessor and adapted to our needs.
    912      *
    913      * @param viewer the viewer
    914      * @param offset offset into document
    915      * @return the prefix to consider
    916      */
    917     protected String extractElementPrefix(ITextViewer viewer, int offset) {
    918         int i = offset;
    919         IDocument document = viewer.getDocument();
    920         if (i > document.getLength()) return ""; //$NON-NLS-1$
    921 
    922         try {
    923             for (; i > 0; --i) {
    924                 char ch = document.getChar(i - 1);
    925 
    926                 // We want all characters that can form a valid:
    927                 // - element name, e.g. anything that is a valid Java class/variable literal.
    928                 // - attribute name, including : for the namespace
    929                 // - attribute value.
    930                 // Before we were inclusive and that made the code fragile. So now we're
    931                 // going to be exclusive: take everything till we get one of:
    932                 // - any form of whitespace
    933                 // - any xml separator, e.g. < > ' " and =
    934                 if (Character.isWhitespace(ch) ||
    935                         ch == '<' || ch == '>' || ch == '\'' || ch == '"' || ch == '=') {
    936                     break;
    937                 }
    938             }
    939 
    940             return document.get(i, offset - i);
    941         } catch (BadLocationException e) {
    942             return ""; //$NON-NLS-1$
    943         }
    944     }
    945 
    946     /**
    947      * Extracts the character at the given offset.
    948      * Returns 0 if the offset is invalid.
    949      */
    950     protected char extractChar(ITextViewer viewer, int offset) {
    951         IDocument document = viewer.getDocument();
    952         if (offset > document.getLength()) return 0;
    953 
    954         try {
    955             return document.getChar(offset);
    956         } catch (BadLocationException e) {
    957             return 0;
    958         }
    959     }
    960 
    961     /**
    962      * Search forward and find the first non-space character and return it. Returns 0 if no
    963      * such character was found.
    964      */
    965     private char nextNonspaceChar(ITextViewer viewer, int offset) {
    966         IDocument document = viewer.getDocument();
    967         int length = document.getLength();
    968         for (; offset < length; offset++) {
    969             try {
    970                 char c = document.getChar(offset);
    971                 if (!Character.isWhitespace(c)) {
    972                     return c;
    973                 }
    974             } catch (BadLocationException e) {
    975                 return 0;
    976             }
    977         }
    978 
    979         return 0;
    980     }
    981 
    982     /**
    983      * Information about the current edit of an attribute as reported by parseAttributeInfo.
    984      */
    985     protected static class AttribInfo {
    986         public AttribInfo() {
    987         }
    988 
    989         /** True if the cursor is located in an attribute's value, false if in an attribute name */
    990         public boolean isInValue = false;
    991         /** The attribute name. Null when not set. */
    992         public String name = null;
    993         /** The attribute value top the left of the cursor. Null when not set. The value
    994          * *may* start with a quote (' or "), in which case we know we don't need to quote
    995          * the string for the user */
    996         public String valuePrefix = null;
    997         /** String typed by the user so far (i.e. right before requesting code completion),
    998          *  which will be corrected if we find a possible completion for an attribute value.
    999          *  See the long comment in getChoicesForAttribute(). */
   1000         public String correctedPrefix = null;
   1001         /** Non-zero if an attribute value need a start/end tag (i.e. quotes or brackets) */
   1002         public char needTag = 0;
   1003         /** Number of characters to replace after the prefix */
   1004         public int replaceLength = 0;
   1005         /** Should the cursor advance through the end tag when inserted? */
   1006         public boolean skipEndTag = false;
   1007     }
   1008 
   1009     /**
   1010      * Try to guess if the cursor is editing an element's name or an attribute following an
   1011      * element. If it's an attribute, try to find if an attribute name is being defined or
   1012      * its value.
   1013      * <br/>
   1014      * This is currently *only* called when we know the cursor is after a complete element
   1015      * tag name, so it should never return null.
   1016      * <br/>
   1017      * Reference for XML syntax: http://www.w3.org/TR/2006/REC-xml-20060816/#sec-starttags
   1018      * <br/>
   1019      * @return An AttribInfo describing which attribute is being edited or null if the cursor is
   1020      *         not editing an attribute (in which case it must be an element's name).
   1021      */
   1022     private AttribInfo parseAttributeInfo(ITextViewer viewer, int offset, int prefixStartOffset) {
   1023         AttribInfo info = new AttribInfo();
   1024         int originalOffset = offset;
   1025 
   1026         IDocument document = viewer.getDocument();
   1027         int n = document.getLength();
   1028         if (offset <= n) {
   1029             try {
   1030                 // Look to the right to make sure we aren't sitting on the boundary of the
   1031                 // beginning of a new element with whitespace before it
   1032                 if (offset < n && document.getChar(offset) == '<') {
   1033                     return null;
   1034                 }
   1035 
   1036                 n = offset;
   1037                 for (;offset > 0; --offset) {
   1038                     char ch = document.getChar(offset - 1);
   1039                     if (ch == '>') break;
   1040                     if (ch == '<') break;
   1041                 }
   1042 
   1043                 // text will contain the full string of the current element,
   1044                 // i.e. whatever is after the "<" to the current cursor
   1045                 String text = document.get(offset, n - offset);
   1046 
   1047                 // Normalize whitespace to single spaces
   1048                 text = sWhitespace.matcher(text).replaceAll(" "); //$NON-NLS-1$
   1049 
   1050                 // Remove the leading element name. By spec, it must be after the < without
   1051                 // any whitespace. If there's nothing left, no attribute has been defined yet.
   1052                 // Be sure to keep any whitespace after the initial word if any, as it matters.
   1053                 text = sFirstElementWord.matcher(text).replaceFirst("");  //$NON-NLS-1$
   1054 
   1055                 // There MUST be space after the element name. If not, the cursor is still
   1056                 // defining the element name.
   1057                 if (!text.startsWith(" ")) { //$NON-NLS-1$
   1058                     return null;
   1059                 }
   1060 
   1061                 // Remove full attributes:
   1062                 // Syntax:
   1063                 //    name = "..." quoted string with all but < and "
   1064                 // or:
   1065                 //    name = '...' quoted string with all but < and '
   1066                 String temp;
   1067                 do {
   1068                     temp = text;
   1069                     text = sFirstAttribute.matcher(temp).replaceFirst("");  //$NON-NLS-1$
   1070                 } while(!temp.equals(text));
   1071 
   1072                 IRegion lineInfo = document.getLineInformationOfOffset(originalOffset);
   1073                 int lineStart = lineInfo.getOffset();
   1074                 String line = document.get(lineStart, lineInfo.getLength());
   1075                 int cursorColumn = originalOffset - lineStart;
   1076                 int prefixLength = originalOffset - prefixStartOffset;
   1077 
   1078                 // Now we're left with 3 cases:
   1079                 // - nothing: either there is no attribute definition or the cursor located after
   1080                 //   a completed attribute definition.
   1081                 // - a string with no =: the user is writing an attribute name. This case can be
   1082                 //   merged with the previous one.
   1083                 // - string with an = sign, optionally followed by a quote (' or "): the user is
   1084                 //   writing the value of the attribute.
   1085                 int posEqual = text.indexOf('=');
   1086                 if (posEqual == -1) {
   1087                     info.isInValue = false;
   1088                     info.name = text.trim();
   1089 
   1090                     // info.name is currently just the prefix of the attribute name.
   1091                     // Look at the text buffer to find the complete name (since we need
   1092                     // to know its bounds in order to replace it when a different attribute
   1093                     // that matches this prefix is chosen)
   1094                     int nameStart = cursorColumn;
   1095                     for (int nameEnd = nameStart; nameEnd < line.length(); nameEnd++) {
   1096                         char c = line.charAt(nameEnd);
   1097                         if (!(Character.isLetter(c) || c == ':' || c == '_')) {
   1098                             String nameSuffix = line.substring(nameStart, nameEnd);
   1099                             info.name = text.trim() + nameSuffix;
   1100                             break;
   1101                         }
   1102                     }
   1103 
   1104                     info.replaceLength = info.name.length() - prefixLength;
   1105 
   1106                     if (info.name.length() == 0 && originalOffset > 0) {
   1107                         // Ensure that attribute names are properly separated
   1108                         char prevChar = extractChar(viewer, originalOffset - 1);
   1109                         if (prevChar == '"' || prevChar == '\'') {
   1110                             // Ensure that the attribute is properly separated from the
   1111                             // previous element
   1112                             info.needTag = ' ';
   1113                         }
   1114                     }
   1115                     info.skipEndTag = false;
   1116                 } else {
   1117                     info.isInValue = true;
   1118                     info.name = text.substring(0, posEqual).trim();
   1119                     info.valuePrefix = text.substring(posEqual + 1);
   1120 
   1121                     char quoteChar = '"'; // Does " or ' surround the XML value?
   1122                     for (int i = posEqual + 1; i < text.length(); i++) {
   1123                         if (!Character.isWhitespace(text.charAt(i))) {
   1124                             quoteChar = text.charAt(i);
   1125                             break;
   1126                         }
   1127                     }
   1128 
   1129                     // Must compute the complete value
   1130                     int valueStart = cursorColumn;
   1131                     int valueEnd = valueStart;
   1132                     for (; valueEnd < line.length(); valueEnd++) {
   1133                         char c = line.charAt(valueEnd);
   1134                         if (c == quoteChar) {
   1135                             // Make sure this isn't the *opening* quote of the value,
   1136                             // which is the case if we invoke code completion with the
   1137                             // caret between the = and the opening quote; in that case
   1138                             // we consider it value completion, and offer items including
   1139                             // the quotes, but we shouldn't bail here thinking we have found
   1140                             // the end of the value.
   1141                             // Look backwards to make sure we find another " before
   1142                             // we find a =
   1143                             boolean isFirst = false;
   1144                             for (int j = valueEnd - 1; j >= 0; j--) {
   1145                                 char pc = line.charAt(j);
   1146                                 if (pc == '=') {
   1147                                     isFirst = true;
   1148                                     break;
   1149                                 } else if (pc == quoteChar) {
   1150                                     valueStart = j;
   1151                                     break;
   1152                                 }
   1153                             }
   1154                             if (!isFirst) {
   1155                                 info.skipEndTag = true;
   1156                                 break;
   1157                             }
   1158                         }
   1159                     }
   1160                     int valueEndOffset = valueEnd + lineStart;
   1161                     info.replaceLength = valueEndOffset - (prefixStartOffset + prefixLength);
   1162                     // Is the caret to the left of the value quote? If so, include it in
   1163                     // the replace length.
   1164                     int valueStartOffset = valueStart + lineStart;
   1165                     if (valueStartOffset == prefixStartOffset && valueEnd > valueStart) {
   1166                         info.replaceLength++;
   1167                     }
   1168                 }
   1169                 return info;
   1170             } catch (BadLocationException e) {
   1171                 // pass
   1172             }
   1173         }
   1174 
   1175         return null;
   1176     }
   1177 
   1178     /** Returns the root descriptor id to use */
   1179     protected int getRootDescriptorId() {
   1180         return mDescriptorId;
   1181     }
   1182 
   1183     /**
   1184      * Computes (if needed) and returns the root descriptor.
   1185      */
   1186     protected ElementDescriptor getRootDescriptor() {
   1187         if (mRootDescriptor == null) {
   1188             AndroidTargetData data = mEditor.getTargetData();
   1189             if (data != null) {
   1190                 IDescriptorProvider descriptorProvider =
   1191                     data.getDescriptorProvider(getRootDescriptorId());
   1192 
   1193                 if (descriptorProvider != null) {
   1194                     mRootDescriptor = new ElementDescriptor("",     //$NON-NLS-1$
   1195                             descriptorProvider.getRootElementDescriptors());
   1196                 }
   1197             }
   1198         }
   1199 
   1200         return mRootDescriptor;
   1201     }
   1202 
   1203     /**
   1204      * Fixed list of dimension units, along with user documentation, for use by
   1205      * {@link #completeSuffix}.
   1206      */
   1207     private static final String[] sDimensionUnits = new String[] {
   1208         UNIT_DP,
   1209         "<b>Density-independent Pixels</b> - an abstract unit that is based on the physical "
   1210                 + "density of the screen.",
   1211 
   1212         UNIT_SP,
   1213         "<b>Scale-independent Pixels</b> - this is like the dp unit, but it is also scaled by "
   1214                 + "the user's font size preference.",
   1215 
   1216         UNIT_PT,
   1217         "<b>Points</b> - 1/72 of an inch based on the physical size of the screen.",
   1218 
   1219         UNIT_MM,
   1220         "<b>Millimeters</b> - based on the physical size of the screen.",
   1221 
   1222         UNIT_IN,
   1223         "<b>Inches</b> - based on the physical size of the screen.",
   1224 
   1225         UNIT_PX,
   1226         "<b>Pixels</b> - corresponds to actual pixels on the screen. Not recommended.",
   1227     };
   1228 
   1229     /**
   1230      * Fixed list of fractional units, along with user documentation, for use by
   1231      * {@link #completeSuffix}
   1232      */
   1233     private static final String[] sFractionUnits = new String[] {
   1234         "%",  //$NON-NLS-1$
   1235         "<b>Fraction</b> - a percentage of the base size",
   1236 
   1237         "%p", //$NON-NLS-1$
   1238         "<b>Fraction</b> - a percentage relative to parent container",
   1239     };
   1240 
   1241     /**
   1242      * Completes suffixes for applicable types (like dimensions and fractions) such that
   1243      * after a dimension number you get completion on unit types like "px".
   1244      */
   1245     private Object[] completeSuffix(Object[] choices, String value, UiAttributeNode currAttrNode) {
   1246         IAttributeInfo attributeInfo = currAttrNode.getDescriptor().getAttributeInfo();
   1247         EnumSet<Format> formats = attributeInfo.getFormats();
   1248         List<Object> suffixes = new ArrayList<Object>();
   1249 
   1250         if (value.length() > 0 && Character.isDigit(value.charAt(0))) {
   1251             boolean hasDimension = formats.contains(Format.DIMENSION);
   1252             boolean hasFraction = formats.contains(Format.FRACTION);
   1253 
   1254             if (hasDimension || hasFraction) {
   1255                 // Split up the value into a numeric part (the prefix) and the
   1256                 // unit part (the suffix)
   1257                 int suffixBegin = 0;
   1258                 for (; suffixBegin < value.length(); suffixBegin++) {
   1259                     if (!Character.isDigit(value.charAt(suffixBegin))) {
   1260                         break;
   1261                     }
   1262                 }
   1263                 String number = value.substring(0, suffixBegin);
   1264                 String suffix = value.substring(suffixBegin);
   1265 
   1266                 // Add in the matching dimension and/or fraction units, if any
   1267                 if (hasDimension) {
   1268                     // Each item has two entries in the array of strings: the first odd numbered
   1269                     // ones are the unit names and the second even numbered ones are the
   1270                     // corresponding descriptions.
   1271                     for (int i = 0; i < sDimensionUnits.length; i += 2) {
   1272                         String unit = sDimensionUnits[i];
   1273                         if (startsWith(unit, suffix)) {
   1274                             String description = sDimensionUnits[i + 1];
   1275                             suffixes.add(Pair.of(number + unit, description));
   1276                         }
   1277                     }
   1278 
   1279                     // Allow "dip" completion but don't offer it ("dp" is preferred)
   1280                     if (startsWith(suffix, "di") || startsWith(suffix, "dip")) { //$NON-NLS-1$ //$NON-NLS-2$
   1281                         suffixes.add(Pair.of(number + "dip", "Alternative name for \"dp\"")); //$NON-NLS-1$
   1282                     }
   1283                 }
   1284                 if (hasFraction) {
   1285                     for (int i = 0; i < sFractionUnits.length; i += 2) {
   1286                         String unit = sFractionUnits[i];
   1287                         if (startsWith(unit, suffix)) {
   1288                             String description = sFractionUnits[i + 1];
   1289                             suffixes.add(Pair.of(number + unit, description));
   1290                         }
   1291                     }
   1292                 }
   1293             }
   1294         }
   1295 
   1296         boolean hasFlag = formats.contains(Format.FLAG);
   1297         if (hasFlag) {
   1298             boolean isDone = false;
   1299             String[] flagValues = attributeInfo.getFlagValues();
   1300             for (String flagValue : flagValues) {
   1301                 if (flagValue.equals(value)) {
   1302                     isDone = true;
   1303                     break;
   1304                 }
   1305             }
   1306             if (isDone) {
   1307                 // Add in all the new values with a separator of |
   1308                 String currentValue = currAttrNode.getCurrentValue();
   1309                 for (String flagValue : flagValues) {
   1310                     if (currentValue == null || !currentValue.contains(flagValue)) {
   1311                         suffixes.add(value + '|' + flagValue);
   1312                     }
   1313                 }
   1314             }
   1315         }
   1316 
   1317         if (suffixes.size() > 0) {
   1318             // Merge previously added choices (from attribute enums etc) with the new matches
   1319             List<Object> all = new ArrayList<Object>();
   1320             if (choices != null) {
   1321                 for (Object s : choices) {
   1322                     all.add(s);
   1323                 }
   1324             }
   1325             all.addAll(suffixes);
   1326             choices = all.toArray();
   1327         }
   1328 
   1329         return choices;
   1330     }
   1331 }
   1332