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