1 /* 2 * Copyright (C) 2010 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_PKG; 20 import static com.android.SdkConstants.ANDROID_PREFIX; 21 import static com.android.SdkConstants.ANDROID_STYLE_RESOURCE_PREFIX; 22 import static com.android.SdkConstants.ANDROID_THEME_PREFIX; 23 import static com.android.SdkConstants.ANDROID_URI; 24 import static com.android.SdkConstants.ATTR_CLASS; 25 import static com.android.SdkConstants.ATTR_CONTEXT; 26 import static com.android.SdkConstants.ATTR_ID; 27 import static com.android.SdkConstants.ATTR_NAME; 28 import static com.android.SdkConstants.ATTR_ON_CLICK; 29 import static com.android.SdkConstants.CLASS_ACTIVITY; 30 import static com.android.SdkConstants.EXT_XML; 31 import static com.android.SdkConstants.FD_DOCS; 32 import static com.android.SdkConstants.FD_DOCS_REFERENCE; 33 import static com.android.SdkConstants.FN_RESOURCE_BASE; 34 import static com.android.SdkConstants.FN_RESOURCE_CLASS; 35 import static com.android.SdkConstants.NEW_ID_PREFIX; 36 import static com.android.SdkConstants.PREFIX_RESOURCE_REF; 37 import static com.android.SdkConstants.PREFIX_THEME_REF; 38 import static com.android.SdkConstants.STYLE_RESOURCE_PREFIX; 39 import static com.android.SdkConstants.TAG_RESOURCES; 40 import static com.android.SdkConstants.TAG_STYLE; 41 import static com.android.SdkConstants.TOOLS_URI; 42 import static com.android.SdkConstants.VIEW; 43 import static com.android.SdkConstants.VIEW_FRAGMENT; 44 import static com.android.ide.common.resources.ResourceRepository.parseResource; 45 import static com.android.xml.AndroidManifest.ATTRIBUTE_NAME; 46 import static com.android.xml.AndroidManifest.ATTRIBUTE_PACKAGE; 47 import static com.android.xml.AndroidManifest.NODE_ACTIVITY; 48 import static com.android.xml.AndroidManifest.NODE_SERVICE; 49 50 import com.android.SdkConstants; 51 import com.android.annotations.NonNull; 52 import com.android.annotations.Nullable; 53 import com.android.annotations.VisibleForTesting; 54 import com.android.ide.common.resources.ResourceFile; 55 import com.android.ide.common.resources.ResourceFolder; 56 import com.android.ide.common.resources.ResourceRepository; 57 import com.android.ide.common.resources.configuration.FolderConfiguration; 58 import com.android.ide.eclipse.adt.AdtPlugin; 59 import com.android.ide.eclipse.adt.AdtUtils; 60 import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate; 61 import com.android.ide.eclipse.adt.internal.editors.layout.gle2.GraphicalEditorPart; 62 import com.android.ide.eclipse.adt.internal.editors.manifest.ManifestEditor; 63 import com.android.ide.eclipse.adt.internal.editors.manifest.ManifestInfo; 64 import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper; 65 import com.android.ide.eclipse.adt.internal.resources.ResourceHelper; 66 import com.android.ide.eclipse.adt.internal.resources.manager.ProjectResources; 67 import com.android.ide.eclipse.adt.internal.resources.manager.ResourceManager; 68 import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData; 69 import com.android.ide.eclipse.adt.internal.sdk.ProjectState; 70 import com.android.ide.eclipse.adt.internal.sdk.Sdk; 71 import com.android.ide.eclipse.adt.io.IFileWrapper; 72 import com.android.ide.eclipse.adt.io.IFolderWrapper; 73 import com.android.io.FileWrapper; 74 import com.android.io.IAbstractFile; 75 import com.android.io.IAbstractFolder; 76 import com.android.resources.ResourceFolderType; 77 import com.android.resources.ResourceType; 78 import com.android.sdklib.IAndroidTarget; 79 import com.android.utils.Pair; 80 81 import org.apache.xerces.parsers.DOMParser; 82 import org.apache.xerces.xni.Augmentations; 83 import org.apache.xerces.xni.NamespaceContext; 84 import org.apache.xerces.xni.QName; 85 import org.apache.xerces.xni.XMLAttributes; 86 import org.apache.xerces.xni.XMLLocator; 87 import org.apache.xerces.xni.XNIException; 88 import org.eclipse.core.filesystem.EFS; 89 import org.eclipse.core.filesystem.IFileStore; 90 import org.eclipse.core.resources.IContainer; 91 import org.eclipse.core.resources.IFile; 92 import org.eclipse.core.resources.IFolder; 93 import org.eclipse.core.resources.IProject; 94 import org.eclipse.core.resources.IResource; 95 import org.eclipse.core.runtime.CoreException; 96 import org.eclipse.core.runtime.IPath; 97 import org.eclipse.core.runtime.NullProgressMonitor; 98 import org.eclipse.core.runtime.Path; 99 import org.eclipse.jdt.core.Flags; 100 import org.eclipse.jdt.core.ICodeAssist; 101 import org.eclipse.jdt.core.IJavaElement; 102 import org.eclipse.jdt.core.IJavaProject; 103 import org.eclipse.jdt.core.IMethod; 104 import org.eclipse.jdt.core.IType; 105 import org.eclipse.jdt.core.JavaModelException; 106 import org.eclipse.jdt.core.search.IJavaSearchConstants; 107 import org.eclipse.jdt.core.search.IJavaSearchScope; 108 import org.eclipse.jdt.core.search.SearchEngine; 109 import org.eclipse.jdt.core.search.SearchMatch; 110 import org.eclipse.jdt.core.search.SearchParticipant; 111 import org.eclipse.jdt.core.search.SearchPattern; 112 import org.eclipse.jdt.core.search.SearchRequestor; 113 import org.eclipse.jdt.internal.ui.javaeditor.EditorUtility; 114 import org.eclipse.jdt.internal.ui.javaeditor.JavaEditor; 115 import org.eclipse.jdt.internal.ui.text.JavaWordFinder; 116 import org.eclipse.jdt.ui.JavaUI; 117 import org.eclipse.jdt.ui.actions.SelectionDispatchAction; 118 import org.eclipse.jface.action.IAction; 119 import org.eclipse.jface.action.IStatusLineManager; 120 import org.eclipse.jface.text.BadLocationException; 121 import org.eclipse.jface.text.IDocument; 122 import org.eclipse.jface.text.IRegion; 123 import org.eclipse.jface.text.ITextViewer; 124 import org.eclipse.jface.text.Region; 125 import org.eclipse.jface.text.hyperlink.AbstractHyperlinkDetector; 126 import org.eclipse.jface.text.hyperlink.IHyperlink; 127 import org.eclipse.ui.IEditorInput; 128 import org.eclipse.ui.IEditorPart; 129 import org.eclipse.ui.IEditorReference; 130 import org.eclipse.ui.IEditorSite; 131 import org.eclipse.ui.IWorkbenchPage; 132 import org.eclipse.ui.PartInitException; 133 import org.eclipse.ui.ide.IDE; 134 import org.eclipse.ui.part.FileEditorInput; 135 import org.eclipse.ui.part.MultiPageEditorPart; 136 import org.eclipse.ui.texteditor.ITextEditor; 137 import org.eclipse.wst.sse.core.StructuredModelManager; 138 import org.eclipse.wst.sse.core.internal.provisional.IStructuredModel; 139 import org.eclipse.wst.sse.core.internal.provisional.IndexedRegion; 140 import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocument; 141 import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocumentRegion; 142 import org.eclipse.wst.sse.core.internal.provisional.text.ITextRegion; 143 import org.eclipse.wst.sse.ui.StructuredTextEditor; 144 import org.eclipse.wst.xml.core.internal.provisional.document.IDOMModel; 145 import org.eclipse.wst.xml.core.internal.regions.DOMRegionContext; 146 import org.w3c.dom.Attr; 147 import org.w3c.dom.Document; 148 import org.w3c.dom.Element; 149 import org.w3c.dom.NamedNodeMap; 150 import org.w3c.dom.Node; 151 import org.w3c.dom.NodeList; 152 import org.xml.sax.InputSource; 153 import org.xml.sax.SAXException; 154 155 import java.io.File; 156 import java.io.FileInputStream; 157 import java.io.IOException; 158 import java.net.MalformedURLException; 159 import java.net.URL; 160 import java.util.ArrayList; 161 import java.util.Collections; 162 import java.util.Comparator; 163 import java.util.List; 164 import java.util.concurrent.atomic.AtomicBoolean; 165 import java.util.regex.Pattern; 166 167 /** 168 * Class containing hyperlink resolvers for XML and Java files to jump to associated 169 * resources -- Java Activity and Service classes, XML layout and string declarations, 170 * image drawables, etc. 171 */ 172 @SuppressWarnings("restriction") 173 public class Hyperlinks { 174 private static final String CATEGORY = "category"; //$NON-NLS-1$ 175 private static final String ACTION = "action"; //$NON-NLS-1$ 176 private static final String PERMISSION = "permission"; //$NON-NLS-1$ 177 private static final String USES_PERMISSION = "uses-permission"; //$NON-NLS-1$ 178 private static final String CATEGORY_PKG_PREFIX = "android.intent.category."; //$NON-NLS-1$ 179 private static final String ACTION_PKG_PREFIX = "android.intent.action."; //$NON-NLS-1$ 180 private static final String PERMISSION_PKG_PREFIX = "android.permission."; //$NON-NLS-1$ 181 182 private Hyperlinks() { 183 // Not instantiatable. This is a container class containing shared code 184 // for the various inner classes that are actual hyperlink resolvers. 185 } 186 187 /** Regular expression matching a FQCN for a view class */ 188 @VisibleForTesting 189 /* package */ static final Pattern CLASS_PATTERN = Pattern.compile( 190 "(([a-zA-Z_\\$][a-zA-Z0-9_\\$]*)+\\.)+[a-zA-Z_\\$][a-zA-Z0-9_\\$]*"); //$NON-NLS-1$ 191 192 /** Determines whether the given attribute <b>name</b> is linkable */ 193 private static boolean isAttributeNameLink(XmlContext context) { 194 // We could potentially allow you to link to builtin Android properties: 195 // ANDROID_URI.equals(attribute.getNamespaceURI()) 196 // and then jump into the res/values/attrs.xml document that is available 197 // in the SDK data directory (path found via 198 // IAndroidTarget.getPath(IAndroidTarget.ATTRIBUTES)). 199 // 200 // For now, we're not doing that. 201 // 202 // We could also allow to jump into custom attributes in custom view 203 // classes. Not yet implemented. 204 205 return false; 206 } 207 208 /** Determines whether the given attribute <b>value</b> is linkable */ 209 private static boolean isAttributeValueLink(XmlContext context) { 210 // Everything else here is attribute based 211 Attr attribute = context.getAttribute(); 212 if (attribute == null) { 213 return false; 214 } 215 216 if (isClassAttribute(context) || isOnClickAttribute(context) 217 || isManifestName(context) || isStyleAttribute(context)) { 218 return true; 219 } 220 221 String value = attribute.getValue(); 222 if (value.startsWith(NEW_ID_PREFIX)) { 223 // It's a value -declaration-, nowhere else to jump 224 // (though we could consider jumping to the R-file; would that 225 // be helpful?) 226 return !ATTR_ID.equals(attribute.getLocalName()); 227 } 228 229 Pair<ResourceType,String> resource = parseResource(value); 230 if (resource != null) { 231 ResourceType type = resource.getFirst(); 232 if (type != null) { 233 return true; 234 } 235 } 236 237 return false; 238 } 239 240 /** Determines whether the given element <b>name</b> is linkable */ 241 private static boolean isElementNameLink(XmlContext context) { 242 if (isClassElement(context)) { 243 return true; 244 } 245 246 return false; 247 } 248 249 /** 250 * Returns true if this node/attribute pair corresponds to a manifest reference to 251 * an activity. 252 */ 253 private static boolean isActivity(XmlContext context) { 254 // Is this an <activity> or <service> in an AndroidManifest.xml file? If so, jump 255 // to it 256 Attr attribute = context.getAttribute(); 257 String tagName = context.getElement().getTagName(); 258 if (NODE_ACTIVITY.equals(tagName) && ATTRIBUTE_NAME.equals(attribute.getLocalName()) 259 && ANDROID_URI.equals(attribute.getNamespaceURI())) { 260 return true; 261 } 262 263 return false; 264 } 265 266 /** 267 * Returns true if this node/attribute pair corresponds to a manifest android:name reference 268 */ 269 private static boolean isManifestName(XmlContext context) { 270 Attr attribute = context.getAttribute(); 271 if (attribute != null && ATTRIBUTE_NAME.equals(attribute.getLocalName()) 272 && ANDROID_URI.equals(attribute.getNamespaceURI())) { 273 if (getEditor() instanceof ManifestEditor) { 274 return true; 275 } 276 } 277 278 return false; 279 } 280 281 /** 282 * Opens the declaration corresponding to an android:name reference in the 283 * AndroidManifest.xml file 284 */ 285 private static boolean openManifestName(IProject project, XmlContext context) { 286 if (isActivity(context)) { 287 String fqcn = getActivityClassFqcn(context); 288 return AdtPlugin.openJavaClass(project, fqcn); 289 } else if (isService(context)) { 290 String fqcn = getServiceClassFqcn(context); 291 return AdtPlugin.openJavaClass(project, fqcn); 292 } else if (isBuiltinPermission(context)) { 293 String permission = context.getAttribute().getValue(); 294 // Mutate something like android.permission.ACCESS_CHECKIN_PROPERTIES 295 // into relative doc url android/Manifest.permission.html#ACCESS_CHECKIN_PROPERTIES 296 assert permission.startsWith(PERMISSION_PKG_PREFIX); 297 String relative = "android/Manifest.permission.html#" //$NON-NLS-1$ 298 + permission.substring(PERMISSION_PKG_PREFIX.length()); 299 300 URL url = getDocUrl(relative); 301 if (url != null) { 302 AdtPlugin.openUrl(url); 303 return true; 304 } else { 305 return false; 306 } 307 } else if (isBuiltinIntent(context)) { 308 String intent = context.getAttribute().getValue(); 309 // Mutate something like android.intent.action.MAIN into 310 // into relative doc url android/content/Intent.html#ACTION_MAIN 311 String relative; 312 if (intent.startsWith(ACTION_PKG_PREFIX)) { 313 relative = "android/content/Intent.html#ACTION_" //$NON-NLS-1$ 314 + intent.substring(ACTION_PKG_PREFIX.length()); 315 } else if (intent.startsWith(CATEGORY_PKG_PREFIX)) { 316 relative = "android/content/Intent.html#CATEGORY_" //$NON-NLS-1$ 317 + intent.substring(CATEGORY_PKG_PREFIX.length()); 318 } else { 319 return false; 320 } 321 URL url = getDocUrl(relative); 322 if (url != null) { 323 AdtPlugin.openUrl(url); 324 return true; 325 } else { 326 return false; 327 } 328 } 329 330 return false; 331 } 332 333 /** Returns true if this represents a style attribute */ 334 private static boolean isStyleAttribute(XmlContext context) { 335 String tag = context.getElement().getTagName(); 336 return TAG_STYLE.equals(tag); 337 } 338 339 /** 340 * Returns true if this represents a {@code <view class="foo.bar.Baz">} class 341 * attribute, or a {@code <fragment android:name="foo.bar.Baz">} class attribute 342 */ 343 private static boolean isClassAttribute(XmlContext context) { 344 Attr attribute = context.getAttribute(); 345 if (attribute == null) { 346 return false; 347 } 348 String tag = context.getElement().getTagName(); 349 String attributeName = attribute.getLocalName(); 350 return ATTR_CLASS.equals(attributeName) && (VIEW.equals(tag) || VIEW_FRAGMENT.equals(tag)) 351 || ATTR_NAME.equals(attributeName) && VIEW_FRAGMENT.equals(tag) 352 || (ATTR_CONTEXT.equals(attributeName) 353 && TOOLS_URI.equals(attribute.getNamespaceURI())); 354 } 355 356 /** Returns true if this represents an onClick attribute specifying a method handler */ 357 private static boolean isOnClickAttribute(XmlContext context) { 358 Attr attribute = context.getAttribute(); 359 if (attribute == null) { 360 return false; 361 } 362 return ATTR_ON_CLICK.equals(attribute.getLocalName()) && attribute.getValue().length() > 0; 363 } 364 365 /** Returns true if this represents a {@code <foo.bar.Baz>} custom view class element */ 366 private static boolean isClassElement(XmlContext context) { 367 if (context.getAttribute() != null) { 368 // Don't match the outer element if the user is hovering over a specific attribute 369 return false; 370 } 371 // If the element looks like a fully qualified class name (e.g. it's a custom view 372 // element) offer it as a link 373 String tag = context.getElement().getTagName(); 374 return (tag.indexOf('.') != -1 && CLASS_PATTERN.matcher(tag).matches()); 375 } 376 377 /** Returns the FQCN for a class declaration at the given context */ 378 private static String getClassFqcn(XmlContext context) { 379 if (isClassAttribute(context)) { 380 String value = context.getAttribute().getValue(); 381 if (!value.isEmpty() && value.charAt(0) == '.') { 382 IProject project = getProject(); 383 if (project != null) { 384 ManifestInfo info = ManifestInfo.get(project); 385 String pkg = info.getPackage(); 386 if (pkg != null) { 387 value = pkg + value; 388 } 389 } 390 } 391 return value; 392 } else if (isClassElement(context)) { 393 return context.getElement().getTagName(); 394 } 395 396 return null; 397 } 398 399 /** 400 * Returns true if this node/attribute pair corresponds to a manifest reference to 401 * an service. 402 */ 403 private static boolean isService(XmlContext context) { 404 Attr attribute = context.getAttribute(); 405 Element node = context.getElement(); 406 407 // Is this an <activity> or <service> in an AndroidManifest.xml file? If so, jump to it 408 String nodeName = node.getNodeName(); 409 if (NODE_SERVICE.equals(nodeName) && ATTRIBUTE_NAME.equals(attribute.getLocalName()) 410 && ANDROID_URI.equals(attribute.getNamespaceURI())) { 411 return true; 412 } 413 414 return false; 415 } 416 417 /** 418 * Returns a URL pointing to the Android reference documentation, either installed 419 * locally or the one on android.com 420 * 421 * @param relative a relative url to append to the root url 422 * @return a URL pointing to the documentation 423 */ 424 private static URL getDocUrl(String relative) { 425 // First try to find locally installed documentation 426 File sdkLocation = new File(Sdk.getCurrent().getSdkLocation()); 427 File docs = new File(sdkLocation, FD_DOCS + File.separator + FD_DOCS_REFERENCE); 428 try { 429 if (docs.exists()) { 430 String s = docs.toURI().toURL().toExternalForm(); 431 if (!s.endsWith("/")) { //$NON-NLS-1$ 432 s += "/"; //$NON-NLS-1$ 433 } 434 return new URL(s + relative); 435 } 436 // If not, fallback to the online documentation 437 return new URL("http://developer.android.com/reference/" + relative); //$NON-NLS-1$ 438 } catch (MalformedURLException e) { 439 AdtPlugin.log(e, "Can't create URL for %1$s", docs); 440 return null; 441 } 442 } 443 444 /** Returns true if the context is pointing to a permission name reference */ 445 private static boolean isBuiltinPermission(XmlContext context) { 446 Attr attribute = context.getAttribute(); 447 Element node = context.getElement(); 448 449 // Is this an <activity> or <service> in an AndroidManifest.xml file? If so, jump to it 450 String nodeName = node.getNodeName(); 451 if ((USES_PERMISSION.equals(nodeName) || PERMISSION.equals(nodeName)) 452 && ATTRIBUTE_NAME.equals(attribute.getLocalName()) 453 && ANDROID_URI.equals(attribute.getNamespaceURI())) { 454 String value = attribute.getValue(); 455 if (value.startsWith(PERMISSION_PKG_PREFIX)) { 456 return true; 457 } 458 } 459 460 return false; 461 } 462 463 /** Returns true if the context is pointing to an intent reference */ 464 private static boolean isBuiltinIntent(XmlContext context) { 465 Attr attribute = context.getAttribute(); 466 Element node = context.getElement(); 467 468 // Is this an <activity> or <service> in an AndroidManifest.xml file? If so, jump to it 469 String nodeName = node.getNodeName(); 470 if ((ACTION.equals(nodeName) || CATEGORY.equals(nodeName)) 471 && ATTRIBUTE_NAME.equals(attribute.getLocalName()) 472 && ANDROID_URI.equals(attribute.getNamespaceURI())) { 473 String value = attribute.getValue(); 474 if (value.startsWith(ACTION_PKG_PREFIX) || value.startsWith(CATEGORY_PKG_PREFIX)) { 475 return true; 476 } 477 } 478 479 return false; 480 } 481 482 483 /** 484 * Returns the fully qualified class name of an activity referenced by the given 485 * AndroidManifest.xml node 486 */ 487 private static String getActivityClassFqcn(XmlContext context) { 488 Attr attribute = context.getAttribute(); 489 Element node = context.getElement(); 490 StringBuilder sb = new StringBuilder(); 491 Element root = node.getOwnerDocument().getDocumentElement(); 492 String pkg = root.getAttribute(ATTRIBUTE_PACKAGE); 493 String className = attribute.getValue(); 494 if (className.startsWith(".")) { //$NON-NLS-1$ 495 sb.append(pkg); 496 } else if (className.indexOf('.') == -1) { 497 // According to the <activity> manifest element documentation, this is not 498 // valid ( http://developer.android.com/guide/topics/manifest/activity-element.html ) 499 // but it appears in manifest files and appears to be supported by the runtime 500 // so handle this in code as well: 501 sb.append(pkg); 502 sb.append('.'); 503 } // else: the class name is already a fully qualified class name 504 sb.append(className); 505 return sb.toString(); 506 } 507 508 /** 509 * Returns the fully qualified class name of a service referenced by the given 510 * AndroidManifest.xml node 511 */ 512 private static String getServiceClassFqcn(XmlContext context) { 513 // Same logic 514 return getActivityClassFqcn(context); 515 } 516 517 /** 518 * Returns the XML tag containing an element description for value items of the given 519 * resource type 520 * 521 * @param type the resource type to query the XML tag name for 522 * @return the tag name used for value declarations in XML of resources of the given 523 * type 524 */ 525 public static String getTagName(ResourceType type) { 526 if (type == ResourceType.ID) { 527 // Ids are recorded in <item> tags instead of <id> tags 528 return SdkConstants.TAG_ITEM; 529 } 530 531 return type.getName(); 532 } 533 534 /** 535 * Computes the actual exact location to jump to for a given XML context. 536 * 537 * @param context the XML context to be opened 538 * @return true if the request was handled successfully 539 */ 540 private static boolean open(XmlContext context) { 541 IProject project = getProject(); 542 if (project == null) { 543 return false; 544 } 545 546 if (isManifestName(context)) { 547 return openManifestName(project, context); 548 } else if (isClassElement(context) || isClassAttribute(context)) { 549 return AdtPlugin.openJavaClass(project, getClassFqcn(context)); 550 } else if (isOnClickAttribute(context)) { 551 return openOnClickMethod(project, context.getAttribute().getValue()); 552 } else { 553 return false; 554 } 555 } 556 557 /** Opens a path (which may not be in the workspace) */ 558 private static void openPath(IPath filePath, IRegion region, int offset) { 559 IEditorPart sourceEditor = getEditor(); 560 IWorkbenchPage page = sourceEditor.getEditorSite().getPage(); 561 562 IFile file = AdtUtils.pathToIFile(filePath); 563 if (file != null && file.exists()) { 564 try { 565 AdtPlugin.openFile(file, region); 566 return; 567 } catch (PartInitException ex) { 568 AdtPlugin.log(ex, "Can't open %$1s", filePath); //$NON-NLS-1$ 569 } 570 } else { 571 // It's not a path in the workspace; look externally 572 // (this is probably an @android: path) 573 if (filePath.isAbsolute()) { 574 IFileStore fileStore = EFS.getLocalFileSystem().getStore(filePath); 575 if (!fileStore.fetchInfo().isDirectory() && fileStore.fetchInfo().exists()) { 576 try { 577 IEditorPart target = IDE.openEditorOnFileStore(page, fileStore); 578 if (target instanceof MultiPageEditorPart) { 579 MultiPageEditorPart part = (MultiPageEditorPart) target; 580 IEditorPart[] editors = part.findEditors(target.getEditorInput()); 581 if (editors != null) { 582 for (IEditorPart editor : editors) { 583 if (editor instanceof StructuredTextEditor) { 584 StructuredTextEditor ste = (StructuredTextEditor) editor; 585 part.setActiveEditor(editor); 586 ste.selectAndReveal(offset, 0); 587 break; 588 } 589 } 590 } 591 } 592 593 return; 594 } catch (PartInitException ex) { 595 AdtPlugin.log(ex, "Can't open %$1s", filePath); //$NON-NLS-1$ 596 } 597 } 598 } 599 } 600 601 // Failed: display message to the user 602 displayError(String.format("Could not find resource %1$s", filePath)); 603 } 604 605 private static void displayError(String message) { 606 // Failed: display message to the user 607 IEditorSite editorSite = getEditor().getEditorSite(); 608 IStatusLineManager status = editorSite.getActionBars().getStatusLineManager(); 609 status.setErrorMessage(message); 610 } 611 612 /** 613 * Opens a Java method referenced by the given on click attribute method name 614 * 615 * @param project the project containing the click handler 616 * @param method the method name of the on click handler 617 * @return true if the method was opened, false otherwise 618 */ 619 public static boolean openOnClickMethod(IProject project, String method) { 620 // Search for the method in the Java index, filtering by the required click handler 621 // method signature (public and has a single View parameter), and narrowing the scope 622 // first to Activity classes, then to the whole workspace. 623 final AtomicBoolean success = new AtomicBoolean(false); 624 SearchRequestor requestor = new SearchRequestor() { 625 @Override 626 public void acceptSearchMatch(SearchMatch match) throws CoreException { 627 Object element = match.getElement(); 628 if (element instanceof IMethod) { 629 IMethod methodElement = (IMethod) element; 630 String[] parameterTypes = methodElement.getParameterTypes(); 631 if (parameterTypes != null 632 && parameterTypes.length == 1 633 && ("Qandroid.view.View;".equals(parameterTypes[0]) //$NON-NLS-1$ 634 || "QView;".equals(parameterTypes[0]))) { //$NON-NLS-1$ 635 // Check that it's public 636 if (Flags.isPublic(methodElement.getFlags())) { 637 JavaUI.openInEditor(methodElement); 638 success.getAndSet(true); 639 } 640 } 641 } 642 } 643 }; 644 try { 645 IJavaSearchScope scope = null; 646 IType activityType = null; 647 IJavaProject javaProject = BaseProjectHelper.getJavaProject(project); 648 if (javaProject != null) { 649 activityType = javaProject.findType(CLASS_ACTIVITY); 650 if (activityType != null) { 651 scope = SearchEngine.createHierarchyScope(activityType); 652 } 653 } 654 if (scope == null) { 655 scope = SearchEngine.createWorkspaceScope(); 656 } 657 658 SearchParticipant[] participants = new SearchParticipant[] { 659 SearchEngine.getDefaultSearchParticipant() 660 }; 661 int matchRule = SearchPattern.R_PATTERN_MATCH | SearchPattern.R_CASE_SENSITIVE; 662 SearchPattern pattern = SearchPattern.createPattern("*." + method, 663 IJavaSearchConstants.METHOD, IJavaSearchConstants.DECLARATIONS, matchRule); 664 SearchEngine engine = new SearchEngine(); 665 engine.search(pattern, participants, scope, requestor, new NullProgressMonitor()); 666 667 boolean ok = success.get(); 668 if (!ok && activityType != null) { 669 // TODO: Create a project+dependencies scope and search only that scope 670 671 // Try searching again with a complete workspace scope this time 672 scope = SearchEngine.createWorkspaceScope(); 673 engine.search(pattern, participants, scope, requestor, new NullProgressMonitor()); 674 675 // TODO: There could be more than one match; add code to consider them all 676 // and pick the most likely candidate and open only that one. 677 678 ok = success.get(); 679 } 680 return ok; 681 } catch (CoreException e) { 682 AdtPlugin.log(e, null); 683 } 684 return false; 685 } 686 687 /** 688 * Returns the current configuration, if the associated UI editor has been initialized 689 * and has an associated configuration 690 * 691 * @return the configuration for this file, or null 692 */ 693 private static FolderConfiguration getConfiguration() { 694 IEditorPart editor = getEditor(); 695 if (editor != null) { 696 LayoutEditorDelegate delegate = LayoutEditorDelegate.fromEditor(editor); 697 GraphicalEditorPart graphicalEditor = 698 delegate == null ? null : delegate.getGraphicalEditor(); 699 700 if (graphicalEditor != null) { 701 return graphicalEditor.getConfiguration(); 702 } else { 703 // TODO: Could try a few more things to get the configuration: 704 // (1) try to look at the file.getPersistentProperty(NAME_CONFIG_STATE) 705 // which will return previously saved state. This isn't necessary today 706 // since no editors seem to be lazily initialized. 707 // (2) attempt to use the configuration from any of the other open 708 // files, especially files in the same directory as this one. 709 } 710 711 // Create a configuration from the current file 712 IProject project = null; 713 IEditorInput editorInput = editor.getEditorInput(); 714 if (editorInput instanceof FileEditorInput) { 715 IFile file = ((FileEditorInput) editorInput).getFile(); 716 project = file.getProject(); 717 ProjectResources pr = ResourceManager.getInstance().getProjectResources(project); 718 IContainer parent = file.getParent(); 719 if (parent instanceof IFolder) { 720 ResourceFolder resFolder = pr.getResourceFolder((IFolder) parent); 721 if (resFolder != null) { 722 return resFolder.getConfiguration(); 723 } 724 } 725 } 726 727 // Might be editing a Java file, where there is no configuration context. 728 // Instead look at surrounding files in the workspace and obtain one valid 729 // configuration. 730 for (IEditorReference reference : editor.getSite().getPage().getEditorReferences()) { 731 IEditorPart part = reference.getEditor(false /*restore*/); 732 733 LayoutEditorDelegate refDelegate = LayoutEditorDelegate.fromEditor(part); 734 if (refDelegate != null) { 735 IProject refProject = refDelegate.getEditor().getProject(); 736 if (project == null || project == refProject) { 737 GraphicalEditorPart refGraphicalEditor = refDelegate.getGraphicalEditor(); 738 if (refGraphicalEditor != null) { 739 return refGraphicalEditor.getConfiguration(); 740 } 741 } 742 } 743 } 744 } 745 746 return null; 747 } 748 749 /** Returns the {@link IAndroidTarget} to be used for looking up system resources */ 750 private static IAndroidTarget getTarget(IProject project) { 751 IEditorPart editor = getEditor(); 752 LayoutEditorDelegate delegate = LayoutEditorDelegate.fromEditor(editor); 753 if (delegate != null) { 754 GraphicalEditorPart graphicalEditor = delegate.getGraphicalEditor(); 755 if (graphicalEditor != null) { 756 return graphicalEditor.getRenderingTarget(); 757 } 758 } 759 760 Sdk currentSdk = Sdk.getCurrent(); 761 if (currentSdk == null) { 762 return null; 763 } 764 765 return currentSdk.getTarget(project); 766 } 767 768 /** Return either the project resources or the framework resources (or null) */ 769 private static ResourceRepository getResources(IProject project, boolean framework) { 770 if (framework) { 771 IAndroidTarget target = getTarget(project); 772 773 if (target == null && project == null && framework) { 774 // No current project: probably jumped into some of the framework XML resource 775 // files and attempting to jump around. Attempt to figure out which target 776 // we're dealing with and continue looking within the same framework. 777 IEditorPart editor = getEditor(); 778 Sdk sdk = Sdk.getCurrent(); 779 if (sdk != null && editor instanceof AndroidXmlEditor) { 780 AndroidTargetData data = ((AndroidXmlEditor) editor).getTargetData(); 781 if (data != null) { 782 return data.getFrameworkResources(); 783 } 784 } 785 } 786 787 if (target == null) { 788 return null; 789 } 790 AndroidTargetData data = Sdk.getCurrent().getTargetData(target); 791 if (data == null) { 792 return null; 793 } 794 return data.getFrameworkResources(); 795 } else { 796 return ResourceManager.getInstance().getProjectResources(project); 797 } 798 } 799 800 /** 801 * Finds a definition of an id attribute in layouts. (Ids can also be defined as 802 * resources; use {@link #findValueInXml} or {@link #findValueInDocument} to locate it there.) 803 */ 804 private static Pair<IFile, IRegion> findIdDefinition(IProject project, String id) { 805 // FIRST look in the same file as the originating request, that's where you usually 806 // want to jump 807 IFile self = AdtUtils.getActiveFile(); 808 if (self != null && EXT_XML.equals(self.getFileExtension())) { 809 Pair<IFile, IRegion> target = findIdInXml(id, self); 810 if (target != null) { 811 return target; 812 } 813 } 814 815 // Look in the configuration folder: Search compatible configurations 816 ResourceRepository resources = getResources(project, false /* isFramework */); 817 FolderConfiguration configuration = getConfiguration(); 818 if (configuration != null) { // Not the case when searching from Java files for example 819 List<ResourceFolder> folders = resources.getFolders(ResourceFolderType.LAYOUT); 820 if (folders != null) { 821 for (ResourceFolder folder : folders) { 822 if (folder.getConfiguration().isMatchFor(configuration)) { 823 IAbstractFolder wrapper = folder.getFolder(); 824 if (wrapper instanceof IFolderWrapper) { 825 IFolder iFolder = ((IFolderWrapper) wrapper).getIFolder(); 826 Pair<IFile, IRegion> target = findIdInFolder(iFolder, id); 827 if (target != null) { 828 return target; 829 } 830 } 831 } 832 } 833 return null; 834 } 835 } 836 837 // Ugh. Search ALL layout files in the project! 838 List<ResourceFolder> folders = resources.getFolders(ResourceFolderType.LAYOUT); 839 if (folders != null) { 840 for (ResourceFolder folder : folders) { 841 IAbstractFolder wrapper = folder.getFolder(); 842 if (wrapper instanceof IFolderWrapper) { 843 IFolder iFolder = ((IFolderWrapper) wrapper).getIFolder(); 844 Pair<IFile, IRegion> target = findIdInFolder(iFolder, id); 845 if (target != null) { 846 return target; 847 } 848 } 849 } 850 } 851 852 return null; 853 } 854 855 /** 856 * Finds a definition of an id attribute in a particular layout folder. 857 */ 858 private static Pair<IFile, IRegion> findIdInFolder(IContainer f, String id) { 859 try { 860 // Check XML files in values/ 861 for (IResource resource : f.members()) { 862 if (resource.exists() && !resource.isDerived() && resource instanceof IFile) { 863 IFile file = (IFile) resource; 864 // Must have an XML extension 865 if (EXT_XML.equals(file.getFileExtension())) { 866 Pair<IFile, IRegion> target = findIdInXml(id, file); 867 if (target != null) { 868 return target; 869 } 870 } 871 } 872 } 873 } catch (CoreException e) { 874 AdtPlugin.log(e, ""); //$NON-NLS-1$ 875 } 876 877 return null; 878 } 879 880 /** Parses the given file and locates a definition of the given resource */ 881 private static Pair<IFile, IRegion> findValueInXml( 882 ResourceType type, String name, IFile file) { 883 IStructuredModel model = null; 884 try { 885 model = StructuredModelManager.getModelManager().getExistingModelForRead(file); 886 if (model == null) { 887 // There is no open or cached model for the file; see if the file looks 888 // like it's interesting (content contains the String name we are looking for) 889 if (AdtPlugin.fileContains(file, name)) { 890 // Yes, so parse content 891 model = StructuredModelManager.getModelManager().getModelForRead(file); 892 } 893 } 894 if (model instanceof IDOMModel) { 895 IDOMModel domModel = (IDOMModel) model; 896 Document document = domModel.getDocument(); 897 return findValueInDocument(type, name, file, document); 898 } 899 } catch (IOException e) { 900 AdtPlugin.log(e, "Can't parse %1$s", file); //$NON-NLS-1$ 901 } catch (CoreException e) { 902 AdtPlugin.log(e, "Can't parse %1$s", file); //$NON-NLS-1$ 903 } finally { 904 if (model != null) { 905 model.releaseFromRead(); 906 } 907 } 908 909 return null; 910 } 911 912 /** Looks within an XML DOM document for the given resource name and returns it */ 913 private static Pair<IFile, IRegion> findValueInDocument( 914 ResourceType type, String name, IFile file, Document document) { 915 String targetTag = getTagName(type); 916 Element root = document.getDocumentElement(); 917 if (root.getTagName().equals(TAG_RESOURCES)) { 918 NodeList topLevel = root.getChildNodes(); 919 Pair<IFile, IRegion> value = findValueInChildren(name, file, targetTag, topLevel); 920 if (value == null && type == ResourceType.ATTR) { 921 for (int i = 0, n = topLevel.getLength(); i < n; i++) { 922 Node child = topLevel.item(i); 923 if (child.getNodeType() == Node.ELEMENT_NODE) { 924 Element element = (Element)child; 925 String tagName = element.getTagName(); 926 if (tagName.equals("declare-styleable")) { 927 NodeList children = element.getChildNodes(); 928 value = findValueInChildren(name, file, targetTag, children); 929 if (value != null) { 930 return value; 931 } 932 } 933 } 934 } 935 } 936 937 return value; 938 } 939 940 return null; 941 } 942 943 private static Pair<IFile, IRegion> findValueInChildren(String name, IFile file, 944 String targetTag, NodeList children) { 945 for (int i = 0, n = children.getLength(); i < n; i++) { 946 Node child = children.item(i); 947 if (child.getNodeType() == Node.ELEMENT_NODE) { 948 Element element = (Element)child; 949 String tagName = element.getTagName(); 950 if (tagName.equals(targetTag)) { 951 String elementName = element.getAttribute(ATTR_NAME); 952 if (elementName.equals(name)) { 953 IRegion region = null; 954 if (element instanceof IndexedRegion) { 955 IndexedRegion r = (IndexedRegion) element; 956 // IndexedRegion.getLength() returns bogus values 957 int length = r.getEndOffset() - r.getStartOffset(); 958 region = new Region(r.getStartOffset(), length); 959 } 960 961 return Pair.of(file, region); 962 } 963 } 964 } 965 } 966 967 return null; 968 } 969 970 /** Parses the given file and locates a definition of the given resource */ 971 private static Pair<IFile, IRegion> findIdInXml(String id, IFile file) { 972 IStructuredModel model = null; 973 try { 974 model = StructuredModelManager.getModelManager().getExistingModelForRead(file); 975 if (model == null) { 976 // There is no open or cached model for the file; see if the file looks 977 // like it's interesting (content contains the String name we are looking for) 978 if (AdtPlugin.fileContains(file, id)) { 979 // Yes, so parse content 980 model = StructuredModelManager.getModelManager().getModelForRead(file); 981 } 982 } 983 if (model instanceof IDOMModel) { 984 IDOMModel domModel = (IDOMModel) model; 985 Document document = domModel.getDocument(); 986 return findIdInDocument(id, file, document); 987 } 988 } catch (IOException e) { 989 AdtPlugin.log(e, "Can't parse %1$s", file); //$NON-NLS-1$ 990 } catch (CoreException e) { 991 AdtPlugin.log(e, "Can't parse %1$s", file); //$NON-NLS-1$ 992 } finally { 993 if (model != null) { 994 model.releaseFromRead(); 995 } 996 } 997 998 return null; 999 } 1000 1001 /** Looks within an XML DOM document for the given resource name and returns it */ 1002 private static Pair<IFile, IRegion> findIdInDocument(String id, IFile file, 1003 Document document) { 1004 String targetAttribute = NEW_ID_PREFIX + id; 1005 Element root = document.getDocumentElement(); 1006 Pair<IFile, IRegion> result = findIdInElement(root, file, targetAttribute, 1007 true /*requireId*/); 1008 if (result == null) { 1009 result = findIdInElement(root, file, targetAttribute, false /*requireId*/); 1010 } 1011 return result; 1012 } 1013 1014 private static Pair<IFile, IRegion> findIdInElement( 1015 Element root, IFile file, String targetAttribute, boolean requireIdAttribute) { 1016 NamedNodeMap attributes = root.getAttributes(); 1017 for (int i = 0, n = attributes.getLength(); i < n; i++) { 1018 Node item = attributes.item(i); 1019 if (item instanceof Attr) { 1020 Attr attribute = (Attr) item; 1021 if (requireIdAttribute && !ATTR_ID.equals(attribute.getLocalName())) { 1022 continue; 1023 } 1024 String value = attribute.getValue(); 1025 if (value.equals(targetAttribute)) { 1026 // Select the element -containing- the id rather than the attribute itself 1027 IRegion region = null; 1028 Node element = attribute.getOwnerElement(); 1029 //if (attribute instanceof IndexedRegion) { 1030 if (element instanceof IndexedRegion) { 1031 IndexedRegion r = (IndexedRegion) element; 1032 int length = r.getEndOffset() - r.getStartOffset(); 1033 region = new Region(r.getStartOffset(), length); 1034 } 1035 1036 return Pair.of(file, region); 1037 } 1038 } 1039 } 1040 1041 NodeList children = root.getChildNodes(); 1042 for (int i = 0, n = children.getLength(); i < n; i++) { 1043 Node child = children.item(i); 1044 if (child.getNodeType() == Node.ELEMENT_NODE) { 1045 Element element = (Element)child; 1046 Pair<IFile, IRegion> result = findIdInElement(element, file, targetAttribute, 1047 requireIdAttribute); 1048 if (result != null) { 1049 return result; 1050 } 1051 } 1052 } 1053 1054 return null; 1055 } 1056 1057 /** Parses the given file and locates a definition of the given resource */ 1058 private static Pair<File, Integer> findValueInXml(ResourceType type, String name, File file) { 1059 // We can't use the StructureModelManager on files outside projects 1060 // There is no open or cached model for the file; see if the file looks 1061 // like it's interesting (content contains the String name we are looking for) 1062 if (AdtPlugin.fileContains(file, name)) { 1063 try { 1064 InputSource is = new InputSource(new FileInputStream(file)); 1065 OffsetTrackingParser parser = new OffsetTrackingParser(); 1066 parser.parse(is); 1067 Document document = parser.getDocument(); 1068 1069 return findValueInDocument(type, name, file, parser, document); 1070 } catch (SAXException e) { 1071 // pass -- ignore files we can't parse 1072 } catch (IOException e) { 1073 // pass -- ignore files we can't parse 1074 } 1075 } 1076 1077 return null; 1078 } 1079 1080 /** Looks within an XML DOM document for the given resource name and returns it */ 1081 private static Pair<File, Integer> findValueInDocument(ResourceType type, String name, 1082 File file, OffsetTrackingParser parser, Document document) { 1083 String targetTag = type.getName(); 1084 if (type == ResourceType.ID) { 1085 // Ids are recorded in <item> tags instead of <id> tags 1086 targetTag = "item"; //$NON-NLS-1$ 1087 } 1088 1089 Pair<File, Integer> result = findTag(name, file, parser, document, targetTag); 1090 if (result == null && type == ResourceType.ATTR) { 1091 // Attributes seem to be defined in <public> tags 1092 targetTag = "public"; //$NON-NLS-1$ 1093 result = findTag(name, file, parser, document, targetTag); 1094 } 1095 return result; 1096 } 1097 1098 private static Pair<File, Integer> findTag(String name, File file, OffsetTrackingParser parser, 1099 Document document, String targetTag) { 1100 NodeList children = document.getElementsByTagName(targetTag); 1101 for (int i = 0, n = children.getLength(); i < n; i++) { 1102 Node child = children.item(i); 1103 if (child.getNodeType() == Node.ELEMENT_NODE) { 1104 Element element = (Element) child; 1105 if (element.getTagName().equals(targetTag)) { 1106 String elementName = element.getAttribute(ATTR_NAME); 1107 if (elementName.equals(name)) { 1108 return Pair.of(file, parser.getOffset(element)); 1109 } 1110 } 1111 } 1112 } 1113 1114 return null; 1115 } 1116 1117 private static IHyperlink[] getStyleLinks(XmlContext context, IRegion range, String url) { 1118 Attr attribute = context.getAttribute(); 1119 if (attribute != null) { 1120 // Split up theme resource urls to the nearest dot forwards, such that you 1121 // can point to "Theme.Light" by placing the caret anywhere after the dot, 1122 // and point to just "Theme" by pointing before it. 1123 int caret = context.getInnerRegionCaretOffset(); 1124 String value = attribute.getValue(); 1125 int index = value.indexOf('.', caret); 1126 if (index != -1) { 1127 url = url.substring(0, index); 1128 range = new Region(range.getOffset(), 1129 range.getLength() - (value.length() - index)); 1130 } 1131 } 1132 1133 Pair<ResourceType,String> resource = parseResource(url); 1134 if (resource == null) { 1135 String androidStyle = ANDROID_STYLE_RESOURCE_PREFIX; 1136 if (url.startsWith(ANDROID_PREFIX)) { 1137 url = androidStyle + url.substring(ANDROID_PREFIX.length()); 1138 } else if (url.startsWith(ANDROID_THEME_PREFIX)) { 1139 url = androidStyle + url.substring(ANDROID_THEME_PREFIX.length()); 1140 } else if (url.startsWith(ANDROID_PKG + ':')) { 1141 url = androidStyle + url.substring(ANDROID_PKG.length() + 1); 1142 } else { 1143 url = STYLE_RESOURCE_PREFIX + url; 1144 } 1145 } 1146 return getResourceLinks(range, url); 1147 } 1148 1149 private static IHyperlink[] getResourceLinks(@Nullable IRegion range, @NonNull String url) { 1150 IProject project = Hyperlinks.getProject(); 1151 FolderConfiguration configuration = getConfiguration(); 1152 return getResourceLinks(range, url, project, configuration); 1153 } 1154 1155 /** 1156 * Computes hyperlinks to resource definitions for resource urls (e.g. 1157 * {@code @android:string/ok} or {@code @layout/foo}. May create multiple links. 1158 * @param range TBD 1159 * @param url the resource url 1160 * @param project the relevant project 1161 * @param configuration the applicable configuration 1162 * @return an array of hyperlinks, or null 1163 */ 1164 @Nullable 1165 public static IHyperlink[] getResourceLinks(@Nullable IRegion range, @NonNull String url, 1166 @NonNull IProject project, @Nullable FolderConfiguration configuration) { 1167 List<IHyperlink> links = new ArrayList<IHyperlink>(); 1168 1169 Pair<ResourceType,String> resource = parseResource(url); 1170 if (resource == null || resource.getFirst() == null) { 1171 return null; 1172 } 1173 ResourceType type = resource.getFirst(); 1174 String name = resource.getSecond(); 1175 1176 boolean isFramework = url.startsWith(ANDROID_PREFIX) 1177 || url.startsWith(ANDROID_THEME_PREFIX); 1178 if (project == null) { 1179 // Local reference *within* a framework 1180 isFramework = true; 1181 } 1182 1183 ResourceRepository resources = getResources(project, isFramework); 1184 if (resources == null) { 1185 return null; 1186 } 1187 List<ResourceFile> sourceFiles = resources.getSourceFiles(type, name, 1188 null /*configuration*/); 1189 if (sourceFiles == null) { 1190 ProjectState projectState = Sdk.getProjectState(project); 1191 if (projectState != null) { 1192 List<IProject> libraries = projectState.getFullLibraryProjects(); 1193 if (libraries != null && !libraries.isEmpty()) { 1194 for (IProject library : libraries) { 1195 resources = ResourceManager.getInstance().getProjectResources(library); 1196 sourceFiles = resources.getSourceFiles(type, name, null /*configuration*/); 1197 if (sourceFiles != null && !sourceFiles.isEmpty()) { 1198 break; 1199 } 1200 } 1201 } 1202 } 1203 } 1204 1205 ResourceFile best = null; 1206 if (configuration != null && sourceFiles != null && sourceFiles.size() > 0) { 1207 List<ResourceFile> bestFiles = resources.getSourceFiles(type, name, configuration); 1208 if (bestFiles != null && bestFiles.size() > 0) { 1209 best = bestFiles.get(0); 1210 } 1211 } 1212 if (sourceFiles != null) { 1213 List<ResourceFile> matches = new ArrayList<ResourceFile>(); 1214 for (ResourceFile resourceFile : sourceFiles) { 1215 matches.add(resourceFile); 1216 } 1217 1218 if (matches.size() > 0) { 1219 final ResourceFile fBest = best; 1220 Collections.sort(matches, new Comparator<ResourceFile>() { 1221 @Override 1222 public int compare(ResourceFile rf1, ResourceFile rf2) { 1223 // Sort best item to the front 1224 if (rf1 == fBest) { 1225 return -1; 1226 } else if (rf2 == fBest) { 1227 return 1; 1228 } else { 1229 return getFileName(rf1).compareTo(getFileName(rf2)); 1230 } 1231 } 1232 }); 1233 1234 // Is this something found in a values/ folder? 1235 boolean valueResource = ResourceHelper.isValueBasedResourceType(type); 1236 1237 for (ResourceFile file : matches) { 1238 String folderName = file.getFolder().getFolder().getName(); 1239 String label = String.format("Open Declaration in %1$s/%2$s", 1240 folderName, getFileName(file)); 1241 1242 // Only search for resource type within the file if it's an 1243 // XML file and it is a value resource 1244 ResourceLink link = new ResourceLink(label, range, file, 1245 valueResource ? type : null, name); 1246 links.add(link); 1247 } 1248 } 1249 } 1250 1251 // Id's are handled specially because they are typically defined 1252 // inline (though they -can- be defined in the values folder above as 1253 // well, in which case we will prefer that definition) 1254 if (!isFramework && type == ResourceType.ID && links.size() == 0) { 1255 // Must compute these lazily... 1256 links.add(new ResourceLink("Open XML Declaration", range, null, type, name)); 1257 } 1258 1259 if (links.size() > 0) { 1260 return links.toArray(new IHyperlink[links.size()]); 1261 } else { 1262 return null; 1263 } 1264 } 1265 1266 private static String getFileName(ResourceFile file) { 1267 return file.getFile().getName(); 1268 } 1269 1270 /** Detector for finding Android references in XML files */ 1271 public static class XmlResolver extends AbstractHyperlinkDetector { 1272 1273 @Override 1274 public IHyperlink[] detectHyperlinks(ITextViewer textViewer, IRegion region, 1275 boolean canShowMultipleHyperlinks) { 1276 1277 if (region == null || textViewer == null) { 1278 return null; 1279 } 1280 1281 IDocument document = textViewer.getDocument(); 1282 1283 XmlContext context = XmlContext.find(document, region.getOffset()); 1284 if (context == null) { 1285 return null; 1286 } 1287 1288 IRegion range = context.getInnerRange(document); 1289 boolean isLinkable = false; 1290 String type = context.getInnerRegion().getType(); 1291 if (type == DOMRegionContext.XML_TAG_ATTRIBUTE_VALUE) { 1292 if (isAttributeValueLink(context)) { 1293 isLinkable = true; 1294 // Strip out quotes 1295 range = new Region(range.getOffset() + 1, range.getLength() - 2); 1296 1297 Attr attribute = context.getAttribute(); 1298 if (isStyleAttribute(context)) { 1299 return getStyleLinks(context, range, attribute.getValue()); 1300 } 1301 if (attribute != null 1302 && (attribute.getValue().startsWith(PREFIX_RESOURCE_REF) 1303 || attribute.getValue().startsWith(PREFIX_THEME_REF))) { 1304 // Instantly create links for resources since we can use the existing 1305 // resolved maps for this and offer multiple choices for the user 1306 String url = attribute.getValue(); 1307 return getResourceLinks(range, url); 1308 } 1309 } 1310 } else if (type == DOMRegionContext.XML_TAG_ATTRIBUTE_NAME) { 1311 if (isAttributeNameLink(context)) { 1312 isLinkable = true; 1313 } 1314 } else if (type == DOMRegionContext.XML_TAG_NAME) { 1315 if (isElementNameLink(context)) { 1316 isLinkable = true; 1317 } 1318 } else if (type == DOMRegionContext.XML_CONTENT) { 1319 Node parentNode = context.getNode().getParentNode(); 1320 if (parentNode != null && parentNode.getNodeType() == Node.ELEMENT_NODE) { 1321 // Try to complete resources defined inline as text, such as 1322 // style definitions 1323 ITextRegion outer = context.getElementRegion(); 1324 ITextRegion inner = context.getInnerRegion(); 1325 int innerOffset = outer.getStart() + inner.getStart(); 1326 int caretOffset = innerOffset + context.getInnerRegionCaretOffset(); 1327 try { 1328 IRegion lineInfo = document.getLineInformationOfOffset(caretOffset); 1329 int lineStart = lineInfo.getOffset(); 1330 int lineEnd = Math.min(lineStart + lineInfo.getLength(), 1331 innerOffset + inner.getLength()); 1332 1333 // Compute the resource URL 1334 int urlStart = -1; 1335 int offset = caretOffset; 1336 while (offset > lineStart) { 1337 char c = document.getChar(offset); 1338 if (c == '@' || c == '?') { 1339 urlStart = offset; 1340 break; 1341 } else if (!isValidResourceUrlChar(c)) { 1342 break; 1343 } 1344 offset--; 1345 } 1346 1347 if (urlStart != -1) { 1348 offset = caretOffset; 1349 while (offset < lineEnd) { 1350 if (!isValidResourceUrlChar(document.getChar(offset))) { 1351 break; 1352 } 1353 offset++; 1354 } 1355 1356 int length = offset - urlStart; 1357 String url = document.get(urlStart, length); 1358 range = new Region(urlStart, length); 1359 return getResourceLinks(range, url); 1360 } 1361 } catch (BadLocationException e) { 1362 AdtPlugin.log(e, null); 1363 } 1364 } 1365 } 1366 1367 if (isLinkable) { 1368 IHyperlink hyperlink = new DeferredResolutionLink(context, range); 1369 if (hyperlink != null) { 1370 return new IHyperlink[] { 1371 hyperlink 1372 }; 1373 } 1374 } 1375 1376 return null; 1377 } 1378 } 1379 1380 private static boolean isValidResourceUrlChar(char c) { 1381 return Character.isJavaIdentifierPart(c) || c == ':' || c == '/' || c == '.' || c == '+'; 1382 1383 } 1384 1385 /** Detector for finding Android references in Java files */ 1386 public static class JavaResolver extends AbstractHyperlinkDetector { 1387 1388 @Override 1389 public IHyperlink[] detectHyperlinks(ITextViewer textViewer, IRegion region, 1390 boolean canShowMultipleHyperlinks) { 1391 // Most of this is identical to the builtin JavaElementHyperlinkDetector -- 1392 // everything down to the Android R filtering below 1393 1394 ITextEditor textEditor = (ITextEditor) getAdapter(ITextEditor.class); 1395 if (region == null || !(textEditor instanceof JavaEditor)) 1396 return null; 1397 1398 IAction openAction = textEditor.getAction("OpenEditor"); //$NON-NLS-1$ 1399 if (!(openAction instanceof SelectionDispatchAction)) 1400 return null; 1401 1402 int offset = region.getOffset(); 1403 1404 IJavaElement input = EditorUtility.getEditorInputJavaElement(textEditor, false); 1405 if (input == null) 1406 return null; 1407 1408 try { 1409 IDocument document = textEditor.getDocumentProvider().getDocument( 1410 textEditor.getEditorInput()); 1411 IRegion wordRegion = JavaWordFinder.findWord(document, offset); 1412 if (wordRegion == null || wordRegion.getLength() == 0) 1413 return null; 1414 1415 IJavaElement[] elements = null; 1416 elements = ((ICodeAssist) input).codeSelect(wordRegion.getOffset(), wordRegion 1417 .getLength()); 1418 1419 // Specific Android R class filtering: 1420 if (elements.length > 0) { 1421 IJavaElement element = elements[0]; 1422 if (element.getElementType() == IJavaElement.FIELD) { 1423 IJavaElement unit = element.getAncestor(IJavaElement.COMPILATION_UNIT); 1424 if (unit == null) { 1425 // Probably in a binary; see if this is an android.R resource 1426 IJavaElement type = element.getAncestor(IJavaElement.TYPE); 1427 if (type != null && type.getParent() != null) { 1428 IJavaElement parentType = type.getParent(); 1429 if (parentType.getElementType() == IJavaElement.CLASS_FILE) { 1430 String pn = parentType.getElementName(); 1431 String prefix = FN_RESOURCE_BASE + "$"; //$NON-NLS-1$ 1432 if (pn.startsWith(prefix)) { 1433 return createTypeLink(element, type, wordRegion, true); 1434 } 1435 } 1436 } 1437 } else if (FN_RESOURCE_CLASS.equals(unit.getElementName())) { 1438 // Yes, we're referencing the project R class. 1439 // Offer hyperlink navigation to XML resource files for 1440 // the various definitions 1441 IJavaElement type = element.getAncestor(IJavaElement.TYPE); 1442 if (type != null) { 1443 return createTypeLink(element, type, wordRegion, false); 1444 } 1445 } 1446 } 1447 1448 } 1449 return null; 1450 } catch (JavaModelException e) { 1451 return null; 1452 } 1453 } 1454 1455 private IHyperlink[] createTypeLink(IJavaElement element, IJavaElement type, 1456 IRegion wordRegion, boolean isFrameworkResource) { 1457 String typeName = type.getElementName(); 1458 // typeName will be "id", "layout", "string", etc 1459 if (isFrameworkResource) { 1460 typeName = ANDROID_PKG + ':' + typeName; 1461 } 1462 String elementName = element.getElementName(); 1463 String url = '@' + typeName + '/' + elementName; 1464 return getResourceLinks(wordRegion, url); 1465 } 1466 } 1467 1468 /** Returns the editor applicable to this hyperlink detection */ 1469 private static IEditorPart getEditor() { 1470 // I would like to be able to find this via getAdapter(TextEditor.class) but 1471 // couldn't find a way to initialize the editor context from 1472 // AndroidSourceViewerConfig#getHyperlinkDetectorTargets (which only has 1473 // a TextViewer, not a TextEditor, instance). 1474 // 1475 // Therefore, for now, use a hack. This hack is reasonable because hyperlink 1476 // resolvers are only run for the front-most visible window in the active 1477 // workbench. 1478 return AdtUtils.getActiveEditor(); 1479 } 1480 1481 /** Returns the project applicable to this hyperlink detection */ 1482 private static IProject getProject() { 1483 IFile file = AdtUtils.getActiveFile(); 1484 if (file != null) { 1485 return file.getProject(); 1486 } 1487 1488 return null; 1489 } 1490 1491 /** 1492 * Hyperlink implementation which delays computing the actual file and offset target 1493 * until it is asked to open the hyperlink 1494 */ 1495 private static class DeferredResolutionLink implements IHyperlink { 1496 private XmlContext mXmlContext; 1497 private IRegion mRegion; 1498 1499 public DeferredResolutionLink(XmlContext xmlContext, IRegion mRegion) { 1500 super(); 1501 this.mXmlContext = xmlContext; 1502 this.mRegion = mRegion; 1503 } 1504 1505 @Override 1506 public IRegion getHyperlinkRegion() { 1507 return mRegion; 1508 } 1509 1510 @Override 1511 public String getHyperlinkText() { 1512 return "Open XML Declaration"; 1513 } 1514 1515 @Override 1516 public String getTypeLabel() { 1517 return null; 1518 } 1519 1520 @Override 1521 public void open() { 1522 // Lazily compute the location to open 1523 if (mXmlContext != null && !Hyperlinks.open(mXmlContext)) { 1524 // Failed: display message to the user 1525 displayError("Could not open link"); 1526 } 1527 } 1528 } 1529 1530 /** 1531 * Hyperlink implementation which provides a link for a resource; the actual file name 1532 * is known, but the value location within XML files is deferred until the link is 1533 * actually opened. 1534 */ 1535 static class ResourceLink implements IHyperlink { 1536 private final String mLinkText; 1537 private final IRegion mLinkRegion; 1538 private final ResourceType mType; 1539 private final String mName; 1540 private final ResourceFile mFile; 1541 1542 /** 1543 * Constructs a new {@link ResourceLink}. 1544 * 1545 * @param linkText the description of the link to be shown in a popup when there 1546 * is more than one match 1547 * @param linkRegion the region corresponding to the link source highlight 1548 * @param file the target resource file containing the link definition 1549 * @param type the type of resource being linked to 1550 * @param name the name of the resource being linked to 1551 */ 1552 public ResourceLink(String linkText, IRegion linkRegion, ResourceFile file, 1553 ResourceType type, String name) { 1554 super(); 1555 mLinkText = linkText; 1556 mLinkRegion = linkRegion; 1557 mType = type; 1558 mName = name; 1559 mFile = file; 1560 } 1561 1562 @Override 1563 public IRegion getHyperlinkRegion() { 1564 return mLinkRegion; 1565 } 1566 1567 @Override 1568 public String getHyperlinkText() { 1569 // return "Open XML Declaration"; 1570 return mLinkText; 1571 } 1572 1573 @Override 1574 public String getTypeLabel() { 1575 return null; 1576 } 1577 1578 @Override 1579 public void open() { 1580 // We have to defer computation of ids until the link is clicked since we 1581 // don't have a fast map lookup for these 1582 if (mFile == null && mType == ResourceType.ID) { 1583 // Id's are handled specially because they are typically defined 1584 // inline (though they -can- be defined in the values folder above as well, 1585 // in which case we will prefer that definition) 1586 IProject project = getProject(); 1587 Pair<IFile,IRegion> def = findIdDefinition(project, mName); 1588 if (def != null) { 1589 try { 1590 AdtPlugin.openFile(def.getFirst(), def.getSecond()); 1591 } catch (PartInitException e) { 1592 AdtPlugin.log(e, null); 1593 } 1594 return; 1595 } 1596 1597 displayError(String.format("Could not find id %1$s", mName)); 1598 return; 1599 } 1600 1601 IAbstractFile wrappedFile = mFile != null ? mFile.getFile() : null; 1602 if (wrappedFile instanceof IFileWrapper) { 1603 IFile file = ((IFileWrapper) wrappedFile).getIFile(); 1604 try { 1605 // Lazily search for the target? 1606 IRegion region = null; 1607 String extension = file.getFileExtension(); 1608 if (mType != null && mName != null && EXT_XML.equals(extension)) { 1609 Pair<IFile, IRegion> target; 1610 if (mType == ResourceType.ID) { 1611 target = findIdInXml(mName, file); 1612 } else { 1613 target = findValueInXml(mType, mName, file); 1614 } 1615 if (target != null) { 1616 region = target.getSecond(); 1617 } 1618 } 1619 AdtPlugin.openFile(file, region); 1620 } catch (PartInitException e) { 1621 AdtPlugin.log(e, null); 1622 } 1623 } else if (wrappedFile instanceof FileWrapper) { 1624 File file = ((FileWrapper) wrappedFile); 1625 IPath path = new Path(file.getAbsolutePath()); 1626 int offset = 0; 1627 // Lazily search for the target? 1628 if (mType != null && mName != null && EXT_XML.equals(path.getFileExtension())) { 1629 if (file.exists()) { 1630 Pair<File, Integer> target = findValueInXml(mType, mName, file); 1631 if (target != null && target.getSecond() != null) { 1632 offset = target.getSecond(); 1633 } 1634 } 1635 } 1636 openPath(path, null, offset); 1637 } else { 1638 throw new IllegalArgumentException("Invalid link parameters"); 1639 } 1640 } 1641 1642 ResourceFile getFile() { 1643 return mFile; 1644 } 1645 } 1646 1647 /** 1648 * XML context containing node, potentially attribute, and text regions surrounding a 1649 * particular caret offset 1650 */ 1651 private static class XmlContext { 1652 private final Node mNode; 1653 private final Element mElement; 1654 private final Attr mAttribute; 1655 private final IStructuredDocumentRegion mOuterRegion; 1656 private final ITextRegion mInnerRegion; 1657 private final int mInnerRegionOffset; 1658 1659 public XmlContext(Node node, Element element, Attr attribute, 1660 IStructuredDocumentRegion outerRegion, 1661 ITextRegion innerRegion, int innerRegionOffset) { 1662 super(); 1663 mNode = node; 1664 mElement = element; 1665 mAttribute = attribute; 1666 mOuterRegion = outerRegion; 1667 mInnerRegion = innerRegion; 1668 mInnerRegionOffset = innerRegionOffset; 1669 } 1670 1671 /** 1672 * Gets the current node, never null 1673 * 1674 * @return the surrounding node 1675 */ 1676 public Node getNode() { 1677 return mNode; 1678 } 1679 1680 1681 /** 1682 * Gets the current node, may be null 1683 * 1684 * @return the surrounding node 1685 */ 1686 public Element getElement() { 1687 return mElement; 1688 } 1689 1690 /** 1691 * Returns the current attribute, or null if we are not over an attribute 1692 * 1693 * @return the attribute, or null 1694 */ 1695 public Attr getAttribute() { 1696 return mAttribute; 1697 } 1698 1699 /** 1700 * Gets the region of the element 1701 * 1702 * @return the region of the surrounding element, never null 1703 */ 1704 public ITextRegion getElementRegion() { 1705 return mOuterRegion; 1706 } 1707 1708 /** 1709 * Gets the inner region, which can be the tag name, an attribute name, an 1710 * attribute value, or some other portion of an XML element 1711 * @return the inner region, never null 1712 */ 1713 public ITextRegion getInnerRegion() { 1714 return mInnerRegion; 1715 } 1716 1717 /** 1718 * Gets the caret offset relative to the inner region 1719 * 1720 * @return the offset relative to the inner region 1721 */ 1722 public int getInnerRegionCaretOffset() { 1723 return mInnerRegionOffset; 1724 } 1725 1726 /** 1727 * Returns a range with suffix whitespace stripped out 1728 * 1729 * @param document the document containing the regions 1730 * @return the range of the inner region, minus any whitespace at the end 1731 */ 1732 public IRegion getInnerRange(IDocument document) { 1733 int start = mOuterRegion.getStart() + mInnerRegion.getStart(); 1734 int length = mInnerRegion.getLength(); 1735 try { 1736 String s = document.get(start, length); 1737 for (int i = s.length() - 1; i >= 0; i--) { 1738 if (Character.isWhitespace(s.charAt(i))) { 1739 length--; 1740 } 1741 } 1742 } catch (BadLocationException e) { 1743 AdtPlugin.log(e, ""); //$NON-NLS-1$ 1744 } 1745 return new Region(start, length); 1746 } 1747 1748 /** 1749 * Returns the node the cursor is currently on in the document. null if no node is 1750 * selected 1751 */ 1752 private static XmlContext find(IDocument document, int offset) { 1753 // Loosely based on getCurrentNode and getCurrentAttr in the WST's 1754 // XMLHyperlinkDetector. 1755 IndexedRegion inode = null; 1756 IStructuredModel model = null; 1757 try { 1758 model = StructuredModelManager.getModelManager().getExistingModelForRead(document); 1759 if (model != null) { 1760 inode = model.getIndexedRegion(offset); 1761 if (inode == null) { 1762 inode = model.getIndexedRegion(offset - 1); 1763 } 1764 1765 if (inode instanceof Element) { 1766 Element element = (Element) inode; 1767 Attr attribute = null; 1768 if (element.hasAttributes()) { 1769 NamedNodeMap attrs = element.getAttributes(); 1770 // go through each attribute in node and if attribute contains 1771 // offset, return that attribute 1772 for (int i = 0; i < attrs.getLength(); ++i) { 1773 // assumption that if parent node is of type IndexedRegion, 1774 // then its attributes will also be of type IndexedRegion 1775 IndexedRegion attRegion = (IndexedRegion) attrs.item(i); 1776 if (attRegion.contains(offset)) { 1777 attribute = (Attr) attrs.item(i); 1778 break; 1779 } 1780 } 1781 } 1782 1783 IStructuredDocument doc = model.getStructuredDocument(); 1784 IStructuredDocumentRegion region = doc.getRegionAtCharacterOffset(offset); 1785 if (region != null 1786 && DOMRegionContext.XML_TAG_NAME.equals(region.getType())) { 1787 ITextRegion subRegion = region.getRegionAtCharacterOffset(offset); 1788 if (subRegion == null) { 1789 return null; 1790 } 1791 int regionStart = region.getStartOffset(); 1792 int subregionStart = subRegion.getStart(); 1793 int relativeOffset = offset - (regionStart + subregionStart); 1794 return new XmlContext(element, element, attribute, region, subRegion, 1795 relativeOffset); 1796 } 1797 } else if (inode instanceof Node) { 1798 IStructuredDocument doc = model.getStructuredDocument(); 1799 IStructuredDocumentRegion region = doc.getRegionAtCharacterOffset(offset); 1800 if (region != null 1801 && DOMRegionContext.XML_CONTENT.equals(region.getType())) { 1802 ITextRegion subRegion = region.getRegionAtCharacterOffset(offset); 1803 int regionStart = region.getStartOffset(); 1804 int subregionStart = subRegion.getStart(); 1805 int relativeOffset = offset - (regionStart + subregionStart); 1806 return new XmlContext((Node) inode, null, null, region, subRegion, 1807 relativeOffset); 1808 } 1809 1810 } 1811 } 1812 } finally { 1813 if (model != null) { 1814 model.releaseFromRead(); 1815 } 1816 } 1817 1818 return null; 1819 } 1820 } 1821 1822 /** 1823 * DOM parser which records offsets in the element nodes such that it can return 1824 * offsets for elements later 1825 */ 1826 private static final class OffsetTrackingParser extends DOMParser { 1827 1828 private static final String KEY_OFFSET = "offset"; //$NON-NLS-1$ 1829 1830 private static final String KEY_NODE = 1831 "http://apache.org/xml/properties/dom/current-element-node"; //$NON-NLS-1$ 1832 1833 private XMLLocator mLocator; 1834 1835 public OffsetTrackingParser() throws SAXException { 1836 this.setFeature("http://apache.org/xml/features/dom/defer-node-expansion",//$NON-NLS-1$ 1837 false); 1838 } 1839 1840 public int getOffset(Node node) { 1841 Integer offset = (Integer) node.getUserData(KEY_OFFSET); 1842 if (offset != null) { 1843 return offset; 1844 } 1845 1846 return -1; 1847 } 1848 1849 @Override 1850 public void startElement(QName elementQName, XMLAttributes attrList, Augmentations augs) 1851 throws XNIException { 1852 int offset = mLocator.getCharacterOffset(); 1853 super.startElement(elementQName, attrList, augs); 1854 1855 try { 1856 Node node = (Node) this.getProperty(KEY_NODE); 1857 if (node != null) { 1858 node.setUserData(KEY_OFFSET, offset, null); 1859 } 1860 } catch (org.xml.sax.SAXException ex) { 1861 AdtPlugin.log(ex, ""); //$NON-NLS-1$ 1862 } 1863 } 1864 1865 @Override 1866 public void startDocument(XMLLocator locator, String encoding, 1867 NamespaceContext namespaceContext, Augmentations augs) throws XNIException { 1868 super.startDocument(locator, encoding, namespaceContext, augs); 1869 mLocator = locator; 1870 } 1871 } 1872 } 1873