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