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