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