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