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