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 */ 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 */ 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 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 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 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 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 */ 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 */ 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 */ 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 */ 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 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 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 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 844 private ElementDescriptor getDescriptor(String nodeName) { 845 return getRootDescriptor().findChildrenDescriptor(nodeName, true /* recursive */); 846 } 847 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 */ 861 public char[] getCompletionProposalAutoActivationCharacters() { 862 return new char[]{ '<', ':', '=' }; 863 } 864 865 public char[] getContextInformationAutoActivationCharacters() { 866 return null; 867 } 868 869 public IContextInformationValidator getContextInformationValidator() { 870 return null; 871 } 872 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 */ 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 */ 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 */ 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 { 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 */ 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 */ 1152 protected int getRootDescriptorId() { 1153 return mDescriptorId; 1154 } 1155 1156 /** 1157 * Computes (if needed) and returns the root descriptor. 1158 */ 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 */ 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