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