diff options
Diffstat (limited to 'eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/Hyperlinks.java')
-rw-r--r-- | eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/Hyperlinks.java | 1893 |
1 files changed, 1893 insertions, 0 deletions
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/Hyperlinks.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/Hyperlinks.java new file mode 100644 index 000000000..95cec47e6 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/Hyperlinks.java @@ -0,0 +1,1893 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.adt.internal.editors; + +import static com.android.SdkConstants.ANDROID_PKG; +import static com.android.SdkConstants.ANDROID_PREFIX; +import static com.android.SdkConstants.ANDROID_STYLE_RESOURCE_PREFIX; +import static com.android.SdkConstants.ANDROID_THEME_PREFIX; +import static com.android.SdkConstants.ANDROID_URI; +import static com.android.SdkConstants.ATTR_CLASS; +import static com.android.SdkConstants.ATTR_CONTEXT; +import static com.android.SdkConstants.ATTR_ID; +import static com.android.SdkConstants.ATTR_NAME; +import static com.android.SdkConstants.ATTR_ON_CLICK; +import static com.android.SdkConstants.CLASS_ACTIVITY; +import static com.android.SdkConstants.EXT_XML; +import static com.android.SdkConstants.FD_DOCS; +import static com.android.SdkConstants.FD_DOCS_REFERENCE; +import static com.android.SdkConstants.FN_RESOURCE_BASE; +import static com.android.SdkConstants.FN_RESOURCE_CLASS; +import static com.android.SdkConstants.NEW_ID_PREFIX; +import static com.android.SdkConstants.PREFIX_RESOURCE_REF; +import static com.android.SdkConstants.PREFIX_THEME_REF; +import static com.android.SdkConstants.STYLE_RESOURCE_PREFIX; +import static com.android.SdkConstants.TAG_RESOURCES; +import static com.android.SdkConstants.TAG_STYLE; +import static com.android.SdkConstants.TOOLS_URI; +import static com.android.SdkConstants.VIEW; +import static com.android.SdkConstants.VIEW_FRAGMENT; +import static com.android.xml.AndroidManifest.ATTRIBUTE_NAME; +import static com.android.xml.AndroidManifest.ATTRIBUTE_PACKAGE; +import static com.android.xml.AndroidManifest.NODE_ACTIVITY; +import static com.android.xml.AndroidManifest.NODE_SERVICE; + +import com.android.SdkConstants; +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.annotations.VisibleForTesting; +import com.android.ide.common.resources.ResourceFile; +import com.android.ide.common.resources.ResourceFolder; +import com.android.ide.common.resources.ResourceRepository; +import com.android.ide.common.resources.ResourceUrl; +import com.android.ide.common.resources.configuration.FolderConfiguration; +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.AdtUtils; +import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate; +import com.android.ide.eclipse.adt.internal.editors.layout.gle2.GraphicalEditorPart; +import com.android.ide.eclipse.adt.internal.editors.manifest.ManifestEditor; +import com.android.ide.eclipse.adt.internal.editors.manifest.ManifestInfo; +import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper; +import com.android.ide.eclipse.adt.internal.resources.ResourceHelper; +import com.android.ide.eclipse.adt.internal.resources.manager.ProjectResources; +import com.android.ide.eclipse.adt.internal.resources.manager.ResourceManager; +import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData; +import com.android.ide.eclipse.adt.internal.sdk.ProjectState; +import com.android.ide.eclipse.adt.internal.sdk.Sdk; +import com.android.ide.eclipse.adt.io.IFileWrapper; +import com.android.ide.eclipse.adt.io.IFolderWrapper; +import com.android.io.FileWrapper; +import com.android.io.IAbstractFile; +import com.android.io.IAbstractFolder; +import com.android.resources.ResourceFolderType; +import com.android.resources.ResourceType; +import com.android.sdklib.IAndroidTarget; +import com.android.utils.Pair; + +import org.apache.xerces.parsers.DOMParser; +import org.apache.xerces.xni.Augmentations; +import org.apache.xerces.xni.NamespaceContext; +import org.apache.xerces.xni.QName; +import org.apache.xerces.xni.XMLAttributes; +import org.apache.xerces.xni.XMLLocator; +import org.apache.xerces.xni.XNIException; +import org.eclipse.core.filesystem.EFS; +import org.eclipse.core.filesystem.IFileStore; +import org.eclipse.core.resources.IContainer; +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IFolder; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IResource; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.IPath; +import org.eclipse.core.runtime.NullProgressMonitor; +import org.eclipse.core.runtime.Path; +import org.eclipse.jdt.core.Flags; +import org.eclipse.jdt.core.ICodeAssist; +import org.eclipse.jdt.core.IJavaElement; +import org.eclipse.jdt.core.IJavaProject; +import org.eclipse.jdt.core.IMethod; +import org.eclipse.jdt.core.IType; +import org.eclipse.jdt.core.JavaModelException; +import org.eclipse.jdt.core.search.IJavaSearchConstants; +import org.eclipse.jdt.core.search.IJavaSearchScope; +import org.eclipse.jdt.core.search.SearchEngine; +import org.eclipse.jdt.core.search.SearchMatch; +import org.eclipse.jdt.core.search.SearchParticipant; +import org.eclipse.jdt.core.search.SearchPattern; +import org.eclipse.jdt.core.search.SearchRequestor; +import org.eclipse.jdt.internal.ui.javaeditor.EditorUtility; +import org.eclipse.jdt.internal.ui.javaeditor.JavaEditor; +import org.eclipse.jdt.internal.ui.text.JavaWordFinder; +import org.eclipse.jdt.ui.JavaUI; +import org.eclipse.jdt.ui.actions.SelectionDispatchAction; +import org.eclipse.jface.action.IAction; +import org.eclipse.jface.action.IStatusLineManager; +import org.eclipse.jface.text.BadLocationException; +import org.eclipse.jface.text.IDocument; +import org.eclipse.jface.text.IRegion; +import org.eclipse.jface.text.ITextViewer; +import org.eclipse.jface.text.Region; +import org.eclipse.jface.text.hyperlink.AbstractHyperlinkDetector; +import org.eclipse.jface.text.hyperlink.IHyperlink; +import org.eclipse.ui.IEditorInput; +import org.eclipse.ui.IEditorPart; +import org.eclipse.ui.IEditorReference; +import org.eclipse.ui.IEditorSite; +import org.eclipse.ui.IWorkbenchPage; +import org.eclipse.ui.PartInitException; +import org.eclipse.ui.ide.IDE; +import org.eclipse.ui.part.FileEditorInput; +import org.eclipse.ui.part.MultiPageEditorPart; +import org.eclipse.ui.texteditor.ITextEditor; +import org.eclipse.wst.sse.core.StructuredModelManager; +import org.eclipse.wst.sse.core.internal.provisional.IStructuredModel; +import org.eclipse.wst.sse.core.internal.provisional.IndexedRegion; +import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocument; +import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocumentRegion; +import org.eclipse.wst.sse.core.internal.provisional.text.ITextRegion; +import org.eclipse.wst.sse.ui.StructuredTextEditor; +import org.eclipse.wst.xml.core.internal.provisional.document.IDOMModel; +import org.eclipse.wst.xml.core.internal.regions.DOMRegionContext; +import org.w3c.dom.Attr; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.NamedNodeMap; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * Class containing hyperlink resolvers for XML and Java files to jump to associated + * resources -- Java Activity and Service classes, XML layout and string declarations, + * image drawables, etc. + */ +@SuppressWarnings("restriction") +public class Hyperlinks { + private static final String CATEGORY = "category"; //$NON-NLS-1$ + private static final String ACTION = "action"; //$NON-NLS-1$ + private static final String PERMISSION = "permission"; //$NON-NLS-1$ + private static final String USES_PERMISSION = "uses-permission"; //$NON-NLS-1$ + private static final String CATEGORY_PKG_PREFIX = "android.intent.category."; //$NON-NLS-1$ + private static final String ACTION_PKG_PREFIX = "android.intent.action."; //$NON-NLS-1$ + private static final String PERMISSION_PKG_PREFIX = "android.permission."; //$NON-NLS-1$ + + private Hyperlinks() { + // Not instantiatable. This is a container class containing shared code + // for the various inner classes that are actual hyperlink resolvers. + } + + /** + * Returns whether a string represents a valid fully qualified name for a view class. + * Does not check for existence. + */ + @VisibleForTesting + static boolean isViewClassName(String name) { + int length = name.length(); + if (length < 2 || name.indexOf('.') == -1) { + return false; + } + + boolean lastWasDot = true; + for (int i = 0; i < length; i++) { + char c = name.charAt(i); + if (lastWasDot) { + if (!Character.isJavaIdentifierStart(c)) { + return false; + } + lastWasDot = false; + } else { + if (c == '.') { + lastWasDot = true; + } else if (!Character.isJavaIdentifierPart(c)) { + return false; + } + } + } + + return !lastWasDot; + } + + /** Determines whether the given attribute <b>name</b> is linkable */ + private static boolean isAttributeNameLink(XmlContext context) { + // We could potentially allow you to link to builtin Android properties: + // ANDROID_URI.equals(attribute.getNamespaceURI()) + // and then jump into the res/values/attrs.xml document that is available + // in the SDK data directory (path found via + // IAndroidTarget.getPath(IAndroidTarget.ATTRIBUTES)). + // + // For now, we're not doing that. + // + // We could also allow to jump into custom attributes in custom view + // classes. Not yet implemented. + + return false; + } + + /** Determines whether the given attribute <b>value</b> is linkable */ + private static boolean isAttributeValueLink(XmlContext context) { + // Everything else here is attribute based + Attr attribute = context.getAttribute(); + if (attribute == null) { + return false; + } + + if (isClassAttribute(context) || isOnClickAttribute(context) + || isManifestName(context) || isStyleAttribute(context)) { + return true; + } + + String value = attribute.getValue(); + if (value.startsWith(NEW_ID_PREFIX)) { + // It's a value -declaration-, nowhere else to jump + // (though we could consider jumping to the R-file; would that + // be helpful?) + return !ATTR_ID.equals(attribute.getLocalName()); + } + + ResourceUrl resource = ResourceUrl.parse(value); + if (resource != null) { + return true; + } + + return false; + } + + /** Determines whether the given element <b>name</b> is linkable */ + private static boolean isElementNameLink(XmlContext context) { + if (isClassElement(context)) { + return true; + } + + return false; + } + + /** + * Returns true if this node/attribute pair corresponds to a manifest reference to + * an activity. + */ + private static boolean isActivity(XmlContext context) { + // Is this an <activity> or <service> in an AndroidManifest.xml file? If so, jump + // to it + Attr attribute = context.getAttribute(); + String tagName = context.getElement().getTagName(); + if (NODE_ACTIVITY.equals(tagName) && ATTRIBUTE_NAME.equals(attribute.getLocalName()) + && ANDROID_URI.equals(attribute.getNamespaceURI())) { + return true; + } + + return false; + } + + /** + * Returns true if this node/attribute pair corresponds to a manifest android:name reference + */ + private static boolean isManifestName(XmlContext context) { + Attr attribute = context.getAttribute(); + if (attribute != null && ATTRIBUTE_NAME.equals(attribute.getLocalName()) + && ANDROID_URI.equals(attribute.getNamespaceURI())) { + if (getEditor() instanceof ManifestEditor) { + return true; + } + } + + return false; + } + + /** + * Opens the declaration corresponding to an android:name reference in the + * AndroidManifest.xml file + */ + private static boolean openManifestName(IProject project, XmlContext context) { + if (isActivity(context)) { + String fqcn = getActivityClassFqcn(context); + return AdtPlugin.openJavaClass(project, fqcn); + } else if (isService(context)) { + String fqcn = getServiceClassFqcn(context); + return AdtPlugin.openJavaClass(project, fqcn); + } else if (isBuiltinPermission(context)) { + String permission = context.getAttribute().getValue(); + // Mutate something like android.permission.ACCESS_CHECKIN_PROPERTIES + // into relative doc url android/Manifest.permission.html#ACCESS_CHECKIN_PROPERTIES + assert permission.startsWith(PERMISSION_PKG_PREFIX); + String relative = "android/Manifest.permission.html#" //$NON-NLS-1$ + + permission.substring(PERMISSION_PKG_PREFIX.length()); + + URL url = getDocUrl(relative); + if (url != null) { + AdtPlugin.openUrl(url); + return true; + } else { + return false; + } + } else if (isBuiltinIntent(context)) { + String intent = context.getAttribute().getValue(); + // Mutate something like android.intent.action.MAIN into + // into relative doc url android/content/Intent.html#ACTION_MAIN + String relative; + if (intent.startsWith(ACTION_PKG_PREFIX)) { + relative = "android/content/Intent.html#ACTION_" //$NON-NLS-1$ + + intent.substring(ACTION_PKG_PREFIX.length()); + } else if (intent.startsWith(CATEGORY_PKG_PREFIX)) { + relative = "android/content/Intent.html#CATEGORY_" //$NON-NLS-1$ + + intent.substring(CATEGORY_PKG_PREFIX.length()); + } else { + return false; + } + URL url = getDocUrl(relative); + if (url != null) { + AdtPlugin.openUrl(url); + return true; + } else { + return false; + } + } + + return false; + } + + /** Returns true if this represents a style attribute */ + private static boolean isStyleAttribute(XmlContext context) { + String tag = context.getElement().getTagName(); + return TAG_STYLE.equals(tag); + } + + /** + * Returns true if this represents a {@code <view class="foo.bar.Baz">} class + * attribute, or a {@code <fragment android:name="foo.bar.Baz">} class attribute + */ + private static boolean isClassAttribute(XmlContext context) { + Attr attribute = context.getAttribute(); + if (attribute == null) { + return false; + } + String tag = context.getElement().getTagName(); + String attributeName = attribute.getLocalName(); + return ATTR_CLASS.equals(attributeName) && (VIEW.equals(tag) || VIEW_FRAGMENT.equals(tag)) + || ATTR_NAME.equals(attributeName) && VIEW_FRAGMENT.equals(tag) + || (ATTR_CONTEXT.equals(attributeName) + && TOOLS_URI.equals(attribute.getNamespaceURI())); + } + + /** Returns true if this represents an onClick attribute specifying a method handler */ + private static boolean isOnClickAttribute(XmlContext context) { + Attr attribute = context.getAttribute(); + if (attribute == null) { + return false; + } + return ATTR_ON_CLICK.equals(attribute.getLocalName()) && attribute.getValue().length() > 0; + } + + /** Returns true if this represents a {@code <foo.bar.Baz>} custom view class element */ + private static boolean isClassElement(XmlContext context) { + if (context.getAttribute() != null) { + // Don't match the outer element if the user is hovering over a specific attribute + return false; + } + // If the element looks like a fully qualified class name (e.g. it's a custom view + // element) offer it as a link + String tag = context.getElement().getTagName(); + return isViewClassName(tag); + } + + /** Returns the FQCN for a class declaration at the given context */ + private static String getClassFqcn(XmlContext context) { + if (isClassAttribute(context)) { + String value = context.getAttribute().getValue(); + if (!value.isEmpty() && value.charAt(0) == '.') { + IProject project = getProject(); + if (project != null) { + ManifestInfo info = ManifestInfo.get(project); + String pkg = info.getPackage(); + if (pkg != null) { + value = pkg + value; + } + } + } + return value; + } else if (isClassElement(context)) { + return context.getElement().getTagName(); + } + + return null; + } + + /** + * Returns true if this node/attribute pair corresponds to a manifest reference to + * an service. + */ + private static boolean isService(XmlContext context) { + Attr attribute = context.getAttribute(); + Element node = context.getElement(); + + // Is this an <activity> or <service> in an AndroidManifest.xml file? If so, jump to it + String nodeName = node.getNodeName(); + if (NODE_SERVICE.equals(nodeName) && ATTRIBUTE_NAME.equals(attribute.getLocalName()) + && ANDROID_URI.equals(attribute.getNamespaceURI())) { + return true; + } + + return false; + } + + /** + * Returns a URL pointing to the Android reference documentation, either installed + * locally or the one on android.com + * + * @param relative a relative url to append to the root url + * @return a URL pointing to the documentation + */ + private static URL getDocUrl(String relative) { + // First try to find locally installed documentation + File sdkLocation = new File(Sdk.getCurrent().getSdkOsLocation()); + File docs = new File(sdkLocation, FD_DOCS + File.separator + FD_DOCS_REFERENCE); + try { + if (docs.exists()) { + String s = docs.toURI().toURL().toExternalForm(); + if (!s.endsWith("/")) { //$NON-NLS-1$ + s += "/"; //$NON-NLS-1$ + } + return new URL(s + relative); + } + // If not, fallback to the online documentation + return new URL("http://developer.android.com/reference/" + relative); //$NON-NLS-1$ + } catch (MalformedURLException e) { + AdtPlugin.log(e, "Can't create URL for %1$s", docs); + return null; + } + } + + /** Returns true if the context is pointing to a permission name reference */ + private static boolean isBuiltinPermission(XmlContext context) { + Attr attribute = context.getAttribute(); + Element node = context.getElement(); + + // Is this an <activity> or <service> in an AndroidManifest.xml file? If so, jump to it + String nodeName = node.getNodeName(); + if ((USES_PERMISSION.equals(nodeName) || PERMISSION.equals(nodeName)) + && ATTRIBUTE_NAME.equals(attribute.getLocalName()) + && ANDROID_URI.equals(attribute.getNamespaceURI())) { + String value = attribute.getValue(); + if (value.startsWith(PERMISSION_PKG_PREFIX)) { + return true; + } + } + + return false; + } + + /** Returns true if the context is pointing to an intent reference */ + private static boolean isBuiltinIntent(XmlContext context) { + Attr attribute = context.getAttribute(); + Element node = context.getElement(); + + // Is this an <activity> or <service> in an AndroidManifest.xml file? If so, jump to it + String nodeName = node.getNodeName(); + if ((ACTION.equals(nodeName) || CATEGORY.equals(nodeName)) + && ATTRIBUTE_NAME.equals(attribute.getLocalName()) + && ANDROID_URI.equals(attribute.getNamespaceURI())) { + String value = attribute.getValue(); + if (value.startsWith(ACTION_PKG_PREFIX) || value.startsWith(CATEGORY_PKG_PREFIX)) { + return true; + } + } + + return false; + } + + + /** + * Returns the fully qualified class name of an activity referenced by the given + * AndroidManifest.xml node + */ + private static String getActivityClassFqcn(XmlContext context) { + Attr attribute = context.getAttribute(); + Element node = context.getElement(); + StringBuilder sb = new StringBuilder(); + Element root = node.getOwnerDocument().getDocumentElement(); + String pkg = root.getAttribute(ATTRIBUTE_PACKAGE); + String className = attribute.getValue(); + if (className.startsWith(".")) { //$NON-NLS-1$ + sb.append(pkg); + } else if (className.indexOf('.') == -1) { + // According to the <activity> manifest element documentation, this is not + // valid ( http://developer.android.com/guide/topics/manifest/activity-element.html ) + // but it appears in manifest files and appears to be supported by the runtime + // so handle this in code as well: + sb.append(pkg); + sb.append('.'); + } // else: the class name is already a fully qualified class name + sb.append(className); + return sb.toString(); + } + + /** + * Returns the fully qualified class name of a service referenced by the given + * AndroidManifest.xml node + */ + private static String getServiceClassFqcn(XmlContext context) { + // Same logic + return getActivityClassFqcn(context); + } + + /** + * Returns the XML tag containing an element description for value items of the given + * resource type + * + * @param type the resource type to query the XML tag name for + * @return the tag name used for value declarations in XML of resources of the given + * type + */ + public static String getTagName(ResourceType type) { + if (type == ResourceType.ID) { + // Ids are recorded in <item> tags instead of <id> tags + return SdkConstants.TAG_ITEM; + } + + return type.getName(); + } + + /** + * Computes the actual exact location to jump to for a given XML context. + * + * @param context the XML context to be opened + * @return true if the request was handled successfully + */ + private static boolean open(XmlContext context) { + IProject project = getProject(); + if (project == null) { + return false; + } + + if (isManifestName(context)) { + return openManifestName(project, context); + } else if (isClassElement(context) || isClassAttribute(context)) { + return AdtPlugin.openJavaClass(project, getClassFqcn(context)); + } else if (isOnClickAttribute(context)) { + return openOnClickMethod(project, context.getAttribute().getValue()); + } else { + return false; + } + } + + /** Opens a path (which may not be in the workspace) */ + private static void openPath(IPath filePath, IRegion region, int offset) { + IEditorPart sourceEditor = getEditor(); + IWorkbenchPage page = sourceEditor.getEditorSite().getPage(); + + IFile file = AdtUtils.pathToIFile(filePath); + if (file != null && file.exists()) { + try { + AdtPlugin.openFile(file, region); + return; + } catch (PartInitException ex) { + AdtPlugin.log(ex, "Can't open %$1s", filePath); //$NON-NLS-1$ + } + } else { + // It's not a path in the workspace; look externally + // (this is probably an @android: path) + if (filePath.isAbsolute()) { + IFileStore fileStore = EFS.getLocalFileSystem().getStore(filePath); + if (!fileStore.fetchInfo().isDirectory() && fileStore.fetchInfo().exists()) { + try { + IEditorPart target = IDE.openEditorOnFileStore(page, fileStore); + if (target instanceof MultiPageEditorPart) { + MultiPageEditorPart part = (MultiPageEditorPart) target; + IEditorPart[] editors = part.findEditors(target.getEditorInput()); + if (editors != null) { + for (IEditorPart editor : editors) { + if (editor instanceof StructuredTextEditor) { + StructuredTextEditor ste = (StructuredTextEditor) editor; + part.setActiveEditor(editor); + ste.selectAndReveal(offset, 0); + break; + } + } + } + } + + return; + } catch (PartInitException ex) { + AdtPlugin.log(ex, "Can't open %$1s", filePath); //$NON-NLS-1$ + } + } + } + } + + // Failed: display message to the user + displayError(String.format("Could not find resource %1$s", filePath)); + } + + private static void displayError(String message) { + // Failed: display message to the user + IEditorSite editorSite = getEditor().getEditorSite(); + IStatusLineManager status = editorSite.getActionBars().getStatusLineManager(); + status.setErrorMessage(message); + } + + /** + * Opens a Java method referenced by the given on click attribute method name + * + * @param project the project containing the click handler + * @param method the method name of the on click handler + * @return true if the method was opened, false otherwise + */ + public static boolean openOnClickMethod(IProject project, String method) { + // Search for the method in the Java index, filtering by the required click handler + // method signature (public and has a single View parameter), and narrowing the scope + // first to Activity classes, then to the whole workspace. + final AtomicBoolean success = new AtomicBoolean(false); + SearchRequestor requestor = new SearchRequestor() { + @Override + public void acceptSearchMatch(SearchMatch match) throws CoreException { + Object element = match.getElement(); + if (element instanceof IMethod) { + IMethod methodElement = (IMethod) element; + String[] parameterTypes = methodElement.getParameterTypes(); + if (parameterTypes != null + && parameterTypes.length == 1 + && ("Qandroid.view.View;".equals(parameterTypes[0]) //$NON-NLS-1$ + || "QView;".equals(parameterTypes[0]))) { //$NON-NLS-1$ + // Check that it's public + if (Flags.isPublic(methodElement.getFlags())) { + JavaUI.openInEditor(methodElement); + success.getAndSet(true); + } + } + } + } + }; + try { + IJavaSearchScope scope = null; + IType activityType = null; + IJavaProject javaProject = BaseProjectHelper.getJavaProject(project); + if (javaProject != null) { + activityType = javaProject.findType(CLASS_ACTIVITY); + if (activityType != null) { + scope = SearchEngine.createHierarchyScope(activityType); + } + } + if (scope == null) { + scope = SearchEngine.createWorkspaceScope(); + } + + SearchParticipant[] participants = new SearchParticipant[] { + SearchEngine.getDefaultSearchParticipant() + }; + int matchRule = SearchPattern.R_PATTERN_MATCH | SearchPattern.R_CASE_SENSITIVE; + SearchPattern pattern = SearchPattern.createPattern("*." + method, + IJavaSearchConstants.METHOD, IJavaSearchConstants.DECLARATIONS, matchRule); + SearchEngine engine = new SearchEngine(); + engine.search(pattern, participants, scope, requestor, new NullProgressMonitor()); + + boolean ok = success.get(); + if (!ok && activityType != null) { + // TODO: Create a project+dependencies scope and search only that scope + + // Try searching again with a complete workspace scope this time + scope = SearchEngine.createWorkspaceScope(); + engine.search(pattern, participants, scope, requestor, new NullProgressMonitor()); + + // TODO: There could be more than one match; add code to consider them all + // and pick the most likely candidate and open only that one. + + ok = success.get(); + } + return ok; + } catch (CoreException e) { + AdtPlugin.log(e, null); + } + return false; + } + + /** + * Returns the current configuration, if the associated UI editor has been initialized + * and has an associated configuration + * + * @return the configuration for this file, or null + */ + private static FolderConfiguration getConfiguration() { + IEditorPart editor = getEditor(); + if (editor != null) { + LayoutEditorDelegate delegate = LayoutEditorDelegate.fromEditor(editor); + GraphicalEditorPart graphicalEditor = + delegate == null ? null : delegate.getGraphicalEditor(); + + if (graphicalEditor != null) { + return graphicalEditor.getConfiguration(); + } else { + // TODO: Could try a few more things to get the configuration: + // (1) try to look at the file.getPersistentProperty(NAME_CONFIG_STATE) + // which will return previously saved state. This isn't necessary today + // since no editors seem to be lazily initialized. + // (2) attempt to use the configuration from any of the other open + // files, especially files in the same directory as this one. + } + + // Create a configuration from the current file + IProject project = null; + IEditorInput editorInput = editor.getEditorInput(); + if (editorInput instanceof FileEditorInput) { + IFile file = ((FileEditorInput) editorInput).getFile(); + project = file.getProject(); + ProjectResources pr = ResourceManager.getInstance().getProjectResources(project); + IContainer parent = file.getParent(); + if (parent instanceof IFolder) { + ResourceFolder resFolder = pr.getResourceFolder((IFolder) parent); + if (resFolder != null) { + return resFolder.getConfiguration(); + } + } + } + + // Might be editing a Java file, where there is no configuration context. + // Instead look at surrounding files in the workspace and obtain one valid + // configuration. + for (IEditorReference reference : editor.getSite().getPage().getEditorReferences()) { + IEditorPart part = reference.getEditor(false /*restore*/); + + LayoutEditorDelegate refDelegate = LayoutEditorDelegate.fromEditor(part); + if (refDelegate != null) { + IProject refProject = refDelegate.getEditor().getProject(); + if (project == null || project == refProject) { + GraphicalEditorPart refGraphicalEditor = refDelegate.getGraphicalEditor(); + if (refGraphicalEditor != null) { + return refGraphicalEditor.getConfiguration(); + } + } + } + } + } + + return null; + } + + /** Returns the {@link IAndroidTarget} to be used for looking up system resources */ + private static IAndroidTarget getTarget(IProject project) { + IEditorPart editor = getEditor(); + LayoutEditorDelegate delegate = LayoutEditorDelegate.fromEditor(editor); + if (delegate != null) { + GraphicalEditorPart graphicalEditor = delegate.getGraphicalEditor(); + if (graphicalEditor != null) { + return graphicalEditor.getRenderingTarget(); + } + } + + Sdk currentSdk = Sdk.getCurrent(); + if (currentSdk == null) { + return null; + } + + return currentSdk.getTarget(project); + } + + /** Return either the project resources or the framework resources (or null) */ + private static ResourceRepository getResources(IProject project, boolean framework) { + if (framework) { + IAndroidTarget target = getTarget(project); + + if (target == null && project == null && framework) { + // No current project: probably jumped into some of the framework XML resource + // files and attempting to jump around. Attempt to figure out which target + // we're dealing with and continue looking within the same framework. + IEditorPart editor = getEditor(); + Sdk sdk = Sdk.getCurrent(); + if (sdk != null && editor instanceof AndroidXmlEditor) { + AndroidTargetData data = ((AndroidXmlEditor) editor).getTargetData(); + if (data != null) { + return data.getFrameworkResources(); + } + } + } + + if (target == null) { + return null; + } + AndroidTargetData data = Sdk.getCurrent().getTargetData(target); + if (data == null) { + return null; + } + return data.getFrameworkResources(); + } else { + return ResourceManager.getInstance().getProjectResources(project); + } + } + + /** + * Finds a definition of an id attribute in layouts. (Ids can also be defined as + * resources; use {@link #findValueInXml} or {@link #findValueInDocument} to locate it there.) + */ + private static Pair<IFile, IRegion> findIdDefinition(IProject project, String id) { + // FIRST look in the same file as the originating request, that's where you usually + // want to jump + IFile self = AdtUtils.getActiveFile(); + if (self != null && EXT_XML.equals(self.getFileExtension())) { + Pair<IFile, IRegion> target = findIdInXml(id, self); + if (target != null) { + return target; + } + } + + // Look in the configuration folder: Search compatible configurations + ResourceRepository resources = getResources(project, false /* isFramework */); + FolderConfiguration configuration = getConfiguration(); + if (configuration != null) { // Not the case when searching from Java files for example + List<ResourceFolder> folders = resources.getFolders(ResourceFolderType.LAYOUT); + if (folders != null) { + for (ResourceFolder folder : folders) { + if (folder.getConfiguration().isMatchFor(configuration)) { + IAbstractFolder wrapper = folder.getFolder(); + if (wrapper instanceof IFolderWrapper) { + IFolder iFolder = ((IFolderWrapper) wrapper).getIFolder(); + Pair<IFile, IRegion> target = findIdInFolder(iFolder, id); + if (target != null) { + return target; + } + } + } + } + return null; + } + } + + // Ugh. Search ALL layout files in the project! + List<ResourceFolder> folders = resources.getFolders(ResourceFolderType.LAYOUT); + if (folders != null) { + for (ResourceFolder folder : folders) { + IAbstractFolder wrapper = folder.getFolder(); + if (wrapper instanceof IFolderWrapper) { + IFolder iFolder = ((IFolderWrapper) wrapper).getIFolder(); + Pair<IFile, IRegion> target = findIdInFolder(iFolder, id); + if (target != null) { + return target; + } + } + } + } + + return null; + } + + /** + * Finds a definition of an id attribute in a particular layout folder. + */ + private static Pair<IFile, IRegion> findIdInFolder(IContainer f, String id) { + try { + // Check XML files in values/ + for (IResource resource : f.members()) { + if (resource.exists() && !resource.isDerived() && resource instanceof IFile) { + IFile file = (IFile) resource; + // Must have an XML extension + if (EXT_XML.equals(file.getFileExtension())) { + Pair<IFile, IRegion> target = findIdInXml(id, file); + if (target != null) { + return target; + } + } + } + } + } catch (CoreException e) { + AdtPlugin.log(e, ""); //$NON-NLS-1$ + } + + return null; + } + + /** Parses the given file and locates a definition of the given resource */ + private static Pair<IFile, IRegion> findValueInXml( + ResourceType type, String name, IFile file) { + IStructuredModel model = null; + try { + model = StructuredModelManager.getModelManager().getExistingModelForRead(file); + if (model == null) { + // There is no open or cached model for the file; see if the file looks + // like it's interesting (content contains the String name we are looking for) + if (AdtPlugin.fileContains(file, name)) { + // Yes, so parse content + model = StructuredModelManager.getModelManager().getModelForRead(file); + } + } + if (model instanceof IDOMModel) { + IDOMModel domModel = (IDOMModel) model; + Document document = domModel.getDocument(); + return findValueInDocument(type, name, file, document); + } + } catch (IOException e) { + AdtPlugin.log(e, "Can't parse %1$s", file); //$NON-NLS-1$ + } catch (CoreException e) { + AdtPlugin.log(e, "Can't parse %1$s", file); //$NON-NLS-1$ + } finally { + if (model != null) { + model.releaseFromRead(); + } + } + + return null; + } + + /** Looks within an XML DOM document for the given resource name and returns it */ + private static Pair<IFile, IRegion> findValueInDocument( + ResourceType type, String name, IFile file, Document document) { + String targetTag = getTagName(type); + Element root = document.getDocumentElement(); + if (root.getTagName().equals(TAG_RESOURCES)) { + NodeList topLevel = root.getChildNodes(); + Pair<IFile, IRegion> value = findValueInChildren(name, file, targetTag, topLevel); + if (value == null && type == ResourceType.ATTR) { + for (int i = 0, n = topLevel.getLength(); i < n; i++) { + Node child = topLevel.item(i); + if (child.getNodeType() == Node.ELEMENT_NODE) { + Element element = (Element)child; + String tagName = element.getTagName(); + if (tagName.equals("declare-styleable")) { + NodeList children = element.getChildNodes(); + value = findValueInChildren(name, file, targetTag, children); + if (value != null) { + return value; + } + } + } + } + } + + return value; + } + + return null; + } + + private static Pair<IFile, IRegion> findValueInChildren(String name, IFile file, + String targetTag, NodeList children) { + for (int i = 0, n = children.getLength(); i < n; i++) { + Node child = children.item(i); + if (child.getNodeType() == Node.ELEMENT_NODE) { + Element element = (Element)child; + String tagName = element.getTagName(); + if (tagName.equals(targetTag)) { + String elementName = element.getAttribute(ATTR_NAME); + if (elementName.equals(name)) { + IRegion region = null; + if (element instanceof IndexedRegion) { + IndexedRegion r = (IndexedRegion) element; + // IndexedRegion.getLength() returns bogus values + int length = r.getEndOffset() - r.getStartOffset(); + region = new Region(r.getStartOffset(), length); + } + + return Pair.of(file, region); + } + } + } + } + + return null; + } + + /** Parses the given file and locates a definition of the given resource */ + private static Pair<IFile, IRegion> findIdInXml(String id, IFile file) { + IStructuredModel model = null; + try { + model = StructuredModelManager.getModelManager().getExistingModelForRead(file); + if (model == null) { + // There is no open or cached model for the file; see if the file looks + // like it's interesting (content contains the String name we are looking for) + if (AdtPlugin.fileContains(file, id)) { + // Yes, so parse content + model = StructuredModelManager.getModelManager().getModelForRead(file); + } + } + if (model instanceof IDOMModel) { + IDOMModel domModel = (IDOMModel) model; + Document document = domModel.getDocument(); + return findIdInDocument(id, file, document); + } + } catch (IOException e) { + AdtPlugin.log(e, "Can't parse %1$s", file); //$NON-NLS-1$ + } catch (CoreException e) { + AdtPlugin.log(e, "Can't parse %1$s", file); //$NON-NLS-1$ + } finally { + if (model != null) { + model.releaseFromRead(); + } + } + + return null; + } + + /** Looks within an XML DOM document for the given resource name and returns it */ + private static Pair<IFile, IRegion> findIdInDocument(String id, IFile file, + Document document) { + String targetAttribute = NEW_ID_PREFIX + id; + Element root = document.getDocumentElement(); + Pair<IFile, IRegion> result = findIdInElement(root, file, targetAttribute, + true /*requireId*/); + if (result == null) { + result = findIdInElement(root, file, targetAttribute, false /*requireId*/); + } + return result; + } + + private static Pair<IFile, IRegion> findIdInElement( + Element root, IFile file, String targetAttribute, boolean requireIdAttribute) { + NamedNodeMap attributes = root.getAttributes(); + for (int i = 0, n = attributes.getLength(); i < n; i++) { + Node item = attributes.item(i); + if (item instanceof Attr) { + Attr attribute = (Attr) item; + if (requireIdAttribute && !ATTR_ID.equals(attribute.getLocalName())) { + continue; + } + String value = attribute.getValue(); + if (value.equals(targetAttribute)) { + // Select the element -containing- the id rather than the attribute itself + IRegion region = null; + Node element = attribute.getOwnerElement(); + //if (attribute instanceof IndexedRegion) { + if (element instanceof IndexedRegion) { + IndexedRegion r = (IndexedRegion) element; + int length = r.getEndOffset() - r.getStartOffset(); + region = new Region(r.getStartOffset(), length); + } + + return Pair.of(file, region); + } + } + } + + NodeList children = root.getChildNodes(); + for (int i = 0, n = children.getLength(); i < n; i++) { + Node child = children.item(i); + if (child.getNodeType() == Node.ELEMENT_NODE) { + Element element = (Element)child; + Pair<IFile, IRegion> result = findIdInElement(element, file, targetAttribute, + requireIdAttribute); + if (result != null) { + return result; + } + } + } + + return null; + } + + /** Parses the given file and locates a definition of the given resource */ + private static Pair<File, Integer> findValueInXml(ResourceType type, String name, File file) { + // We can't use the StructureModelManager on files outside projects + // There is no open or cached model for the file; see if the file looks + // like it's interesting (content contains the String name we are looking for) + if (AdtPlugin.fileContains(file, name)) { + try { + InputSource is = new InputSource(new FileInputStream(file)); + OffsetTrackingParser parser = new OffsetTrackingParser(); + parser.parse(is); + Document document = parser.getDocument(); + + return findValueInDocument(type, name, file, parser, document); + } catch (SAXException e) { + // pass -- ignore files we can't parse + } catch (IOException e) { + // pass -- ignore files we can't parse + } + } + + return null; + } + + /** Looks within an XML DOM document for the given resource name and returns it */ + private static Pair<File, Integer> findValueInDocument(ResourceType type, String name, + File file, OffsetTrackingParser parser, Document document) { + String targetTag = type.getName(); + if (type == ResourceType.ID) { + // Ids are recorded in <item> tags instead of <id> tags + targetTag = "item"; //$NON-NLS-1$ + } + + Pair<File, Integer> result = findTag(name, file, parser, document, targetTag); + if (result == null && type == ResourceType.ATTR) { + // Attributes seem to be defined in <public> tags + targetTag = "public"; //$NON-NLS-1$ + result = findTag(name, file, parser, document, targetTag); + } + return result; + } + + private static Pair<File, Integer> findTag(String name, File file, OffsetTrackingParser parser, + Document document, String targetTag) { + NodeList children = document.getElementsByTagName(targetTag); + for (int i = 0, n = children.getLength(); i < n; i++) { + Node child = children.item(i); + if (child.getNodeType() == Node.ELEMENT_NODE) { + Element element = (Element) child; + if (element.getTagName().equals(targetTag)) { + String elementName = element.getAttribute(ATTR_NAME); + if (elementName.equals(name)) { + return Pair.of(file, parser.getOffset(element)); + } + } + } + } + + return null; + } + + private static IHyperlink[] getStyleLinks(XmlContext context, IRegion range, String url) { + Attr attribute = context.getAttribute(); + if (attribute != null) { + // Split up theme resource urls to the nearest dot forwards, such that you + // can point to "Theme.Light" by placing the caret anywhere after the dot, + // and point to just "Theme" by pointing before it. + int caret = context.getInnerRegionCaretOffset(); + String value = attribute.getValue(); + int index = value.indexOf('.', caret); + if (index != -1) { + url = url.substring(0, index); + range = new Region(range.getOffset(), + range.getLength() - (value.length() - index)); + } + } + + ResourceUrl resource = ResourceUrl.parse(url); + if (resource == null) { + String androidStyle = ANDROID_STYLE_RESOURCE_PREFIX; + if (url.startsWith(ANDROID_PREFIX)) { + url = androidStyle + url.substring(ANDROID_PREFIX.length()); + } else if (url.startsWith(ANDROID_THEME_PREFIX)) { + url = androidStyle + url.substring(ANDROID_THEME_PREFIX.length()); + } else if (url.startsWith(ANDROID_PKG + ':')) { + url = androidStyle + url.substring(ANDROID_PKG.length() + 1); + } else { + url = STYLE_RESOURCE_PREFIX + url; + } + } + return getResourceLinks(range, url); + } + + private static IHyperlink[] getResourceLinks(@Nullable IRegion range, @NonNull String url) { + IProject project = Hyperlinks.getProject(); + FolderConfiguration configuration = getConfiguration(); + return getResourceLinks(range, url, project, configuration); + } + + /** + * Computes hyperlinks to resource definitions for resource urls (e.g. + * {@code @android:string/ok} or {@code @layout/foo}. May create multiple links. + * @param range TBD + * @param url the resource url + * @param project the relevant project + * @param configuration the applicable configuration + * @return an array of hyperlinks, or null + */ + @Nullable + public static IHyperlink[] getResourceLinks(@Nullable IRegion range, @NonNull String url, + @Nullable IProject project, @Nullable FolderConfiguration configuration) { + List<IHyperlink> links = new ArrayList<IHyperlink>(); + + ResourceUrl resource = ResourceUrl.parse(url); + if (resource == null) { + return null; + } + ResourceType type = resource.type; + String name = resource.name; + boolean isFramework = resource.framework; + if (project == null) { + // Local reference *within* a framework + isFramework = true; + } + + ResourceRepository resources = getResources(project, isFramework); + if (resources == null) { + return null; + } + List<ResourceFile> sourceFiles = resources.getSourceFiles(type, name, + null /*configuration*/); + if (sourceFiles == null) { + ProjectState projectState = Sdk.getProjectState(project); + if (projectState != null) { + List<IProject> libraries = projectState.getFullLibraryProjects(); + if (libraries != null && !libraries.isEmpty()) { + for (IProject library : libraries) { + resources = ResourceManager.getInstance().getProjectResources(library); + sourceFiles = resources.getSourceFiles(type, name, null /*configuration*/); + if (sourceFiles != null && !sourceFiles.isEmpty()) { + break; + } + } + } + } + } + + ResourceFile best = null; + if (configuration != null && sourceFiles != null && sourceFiles.size() > 0) { + List<ResourceFile> bestFiles = resources.getSourceFiles(type, name, configuration); + if (bestFiles != null && bestFiles.size() > 0) { + best = bestFiles.get(0); + } + } + if (sourceFiles != null) { + List<ResourceFile> matches = new ArrayList<ResourceFile>(); + for (ResourceFile resourceFile : sourceFiles) { + matches.add(resourceFile); + } + + if (matches.size() > 0) { + final ResourceFile fBest = best; + Collections.sort(matches, new Comparator<ResourceFile>() { + @Override + public int compare(ResourceFile rf1, ResourceFile rf2) { + // Sort best item to the front + if (rf1 == fBest) { + return -1; + } else if (rf2 == fBest) { + return 1; + } else { + return getFileName(rf1).compareTo(getFileName(rf2)); + } + } + }); + + // Is this something found in a values/ folder? + boolean valueResource = ResourceHelper.isValueBasedResourceType(type); + + for (ResourceFile file : matches) { + String folderName = file.getFolder().getFolder().getName(); + String label = String.format("Open Declaration in %1$s/%2$s", + folderName, getFileName(file)); + + // Only search for resource type within the file if it's an + // XML file and it is a value resource + ResourceLink link = new ResourceLink(label, range, file, + valueResource ? type : null, name); + links.add(link); + } + } + } + + // Id's are handled specially because they are typically defined + // inline (though they -can- be defined in the values folder above as + // well, in which case we will prefer that definition) + if (!isFramework && type == ResourceType.ID && links.size() == 0) { + // Must compute these lazily... + links.add(new ResourceLink("Open XML Declaration", range, null, type, name)); + } + + if (links.size() > 0) { + return links.toArray(new IHyperlink[links.size()]); + } else { + return null; + } + } + + private static String getFileName(ResourceFile file) { + return file.getFile().getName(); + } + + /** Detector for finding Android references in XML files */ + public static class XmlResolver extends AbstractHyperlinkDetector { + + @Override + public IHyperlink[] detectHyperlinks(ITextViewer textViewer, IRegion region, + boolean canShowMultipleHyperlinks) { + + if (region == null || textViewer == null) { + return null; + } + + IDocument document = textViewer.getDocument(); + + XmlContext context = XmlContext.find(document, region.getOffset()); + if (context == null) { + return null; + } + + IRegion range = context.getInnerRange(document); + boolean isLinkable = false; + String type = context.getInnerRegion().getType(); + if (type == DOMRegionContext.XML_TAG_ATTRIBUTE_VALUE) { + if (isAttributeValueLink(context)) { + isLinkable = true; + // Strip out quotes + range = new Region(range.getOffset() + 1, range.getLength() - 2); + + Attr attribute = context.getAttribute(); + if (isStyleAttribute(context)) { + return getStyleLinks(context, range, attribute.getValue()); + } + if (attribute != null + && (attribute.getValue().startsWith(PREFIX_RESOURCE_REF) + || attribute.getValue().startsWith(PREFIX_THEME_REF))) { + // Instantly create links for resources since we can use the existing + // resolved maps for this and offer multiple choices for the user + String url = attribute.getValue(); + return getResourceLinks(range, url); + } + } + } else if (type == DOMRegionContext.XML_TAG_ATTRIBUTE_NAME) { + if (isAttributeNameLink(context)) { + isLinkable = true; + } + } else if (type == DOMRegionContext.XML_TAG_NAME) { + if (isElementNameLink(context)) { + isLinkable = true; + } + } else if (type == DOMRegionContext.XML_CONTENT) { + Node parentNode = context.getNode().getParentNode(); + if (parentNode != null && parentNode.getNodeType() == Node.ELEMENT_NODE) { + // Try to complete resources defined inline as text, such as + // style definitions + ITextRegion outer = context.getElementRegion(); + ITextRegion inner = context.getInnerRegion(); + int innerOffset = outer.getStart() + inner.getStart(); + int caretOffset = innerOffset + context.getInnerRegionCaretOffset(); + try { + IRegion lineInfo = document.getLineInformationOfOffset(caretOffset); + int lineStart = lineInfo.getOffset(); + int lineEnd = Math.min(lineStart + lineInfo.getLength(), + innerOffset + inner.getLength()); + + // Compute the resource URL + int urlStart = -1; + int offset = caretOffset; + while (offset > lineStart) { + char c = document.getChar(offset); + if (c == '@' || c == '?') { + urlStart = offset; + break; + } else if (!isValidResourceUrlChar(c)) { + break; + } + offset--; + } + + if (urlStart != -1) { + offset = caretOffset; + while (offset < lineEnd) { + if (!isValidResourceUrlChar(document.getChar(offset))) { + break; + } + offset++; + } + + int length = offset - urlStart; + String url = document.get(urlStart, length); + range = new Region(urlStart, length); + return getResourceLinks(range, url); + } + } catch (BadLocationException e) { + AdtPlugin.log(e, null); + } + } + } + + if (isLinkable) { + IHyperlink hyperlink = new DeferredResolutionLink(context, range); + if (hyperlink != null) { + return new IHyperlink[] { + hyperlink + }; + } + } + + return null; + } + } + + private static boolean isValidResourceUrlChar(char c) { + return Character.isJavaIdentifierPart(c) || c == ':' || c == '/' || c == '.' || c == '+'; + + } + + /** Detector for finding Android references in Java files */ + public static class JavaResolver extends AbstractHyperlinkDetector { + + @Override + public IHyperlink[] detectHyperlinks(ITextViewer textViewer, IRegion region, + boolean canShowMultipleHyperlinks) { + // Most of this is identical to the builtin JavaElementHyperlinkDetector -- + // everything down to the Android R filtering below + + ITextEditor textEditor = (ITextEditor) getAdapter(ITextEditor.class); + if (region == null || !(textEditor instanceof JavaEditor)) + return null; + + IAction openAction = textEditor.getAction("OpenEditor"); //$NON-NLS-1$ + if (!(openAction instanceof SelectionDispatchAction)) + return null; + + int offset = region.getOffset(); + + IJavaElement input = EditorUtility.getEditorInputJavaElement(textEditor, false); + if (input == null) + return null; + + try { + IDocument document = textEditor.getDocumentProvider().getDocument( + textEditor.getEditorInput()); + IRegion wordRegion = JavaWordFinder.findWord(document, offset); + if (wordRegion == null || wordRegion.getLength() == 0) + return null; + + IJavaElement[] elements = null; + elements = ((ICodeAssist) input).codeSelect(wordRegion.getOffset(), wordRegion + .getLength()); + + // Specific Android R class filtering: + if (elements.length > 0) { + IJavaElement element = elements[0]; + if (element.getElementType() == IJavaElement.FIELD) { + IJavaElement unit = element.getAncestor(IJavaElement.COMPILATION_UNIT); + if (unit == null) { + // Probably in a binary; see if this is an android.R resource + IJavaElement type = element.getAncestor(IJavaElement.TYPE); + if (type != null && type.getParent() != null) { + IJavaElement parentType = type.getParent(); + if (parentType.getElementType() == IJavaElement.CLASS_FILE) { + String pn = parentType.getElementName(); + String prefix = FN_RESOURCE_BASE + "$"; //$NON-NLS-1$ + if (pn.startsWith(prefix)) { + return createTypeLink(element, type, wordRegion, true); + } + } + } + } else if (FN_RESOURCE_CLASS.equals(unit.getElementName())) { + // Yes, we're referencing the project R class. + // Offer hyperlink navigation to XML resource files for + // the various definitions + IJavaElement type = element.getAncestor(IJavaElement.TYPE); + if (type != null) { + return createTypeLink(element, type, wordRegion, false); + } + } + } + + } + return null; + } catch (JavaModelException e) { + return null; + } + } + + private IHyperlink[] createTypeLink(IJavaElement element, IJavaElement type, + IRegion wordRegion, boolean isFrameworkResource) { + String typeName = type.getElementName(); + // typeName will be "id", "layout", "string", etc + if (isFrameworkResource) { + typeName = ANDROID_PKG + ':' + typeName; + } + String elementName = element.getElementName(); + String url = '@' + typeName + '/' + elementName; + return getResourceLinks(wordRegion, url); + } + } + + /** Returns the editor applicable to this hyperlink detection */ + private static IEditorPart getEditor() { + // I would like to be able to find this via getAdapter(TextEditor.class) but + // couldn't find a way to initialize the editor context from + // AndroidSourceViewerConfig#getHyperlinkDetectorTargets (which only has + // a TextViewer, not a TextEditor, instance). + // + // Therefore, for now, use a hack. This hack is reasonable because hyperlink + // resolvers are only run for the front-most visible window in the active + // workbench. + return AdtUtils.getActiveEditor(); + } + + /** Returns the project applicable to this hyperlink detection */ + @Nullable + private static IProject getProject() { + IFile file = AdtUtils.getActiveFile(); + if (file != null) { + return file.getProject(); + } + + return null; + } + + /** + * Hyperlink implementation which delays computing the actual file and offset target + * until it is asked to open the hyperlink + */ + private static class DeferredResolutionLink implements IHyperlink { + private XmlContext mXmlContext; + private IRegion mRegion; + + public DeferredResolutionLink(XmlContext xmlContext, IRegion mRegion) { + super(); + this.mXmlContext = xmlContext; + this.mRegion = mRegion; + } + + @Override + public IRegion getHyperlinkRegion() { + return mRegion; + } + + @Override + public String getHyperlinkText() { + return "Open XML Declaration"; + } + + @Override + public String getTypeLabel() { + return null; + } + + @Override + public void open() { + // Lazily compute the location to open + if (mXmlContext != null && !Hyperlinks.open(mXmlContext)) { + // Failed: display message to the user + displayError("Could not open link"); + } + } + } + + /** + * Hyperlink implementation which provides a link for a resource; the actual file name + * is known, but the value location within XML files is deferred until the link is + * actually opened. + */ + static class ResourceLink implements IHyperlink { + private final String mLinkText; + private final IRegion mLinkRegion; + private final ResourceType mType; + private final String mName; + private final ResourceFile mFile; + + /** + * Constructs a new {@link ResourceLink}. + * + * @param linkText the description of the link to be shown in a popup when there + * is more than one match + * @param linkRegion the region corresponding to the link source highlight + * @param file the target resource file containing the link definition + * @param type the type of resource being linked to + * @param name the name of the resource being linked to + */ + public ResourceLink(String linkText, IRegion linkRegion, ResourceFile file, + ResourceType type, String name) { + super(); + mLinkText = linkText; + mLinkRegion = linkRegion; + mType = type; + mName = name; + mFile = file; + } + + @Override + public IRegion getHyperlinkRegion() { + return mLinkRegion; + } + + @Override + public String getHyperlinkText() { + // return "Open XML Declaration"; + return mLinkText; + } + + @Override + public String getTypeLabel() { + return null; + } + + @Override + public void open() { + // We have to defer computation of ids until the link is clicked since we + // don't have a fast map lookup for these + if (mFile == null && mType == ResourceType.ID) { + // Id's are handled specially because they are typically defined + // inline (though they -can- be defined in the values folder above as well, + // in which case we will prefer that definition) + IProject project = getProject(); + Pair<IFile,IRegion> def = findIdDefinition(project, mName); + if (def != null) { + try { + AdtPlugin.openFile(def.getFirst(), def.getSecond()); + } catch (PartInitException e) { + AdtPlugin.log(e, null); + } + return; + } + + displayError(String.format("Could not find id %1$s", mName)); + return; + } + + IAbstractFile wrappedFile = mFile != null ? mFile.getFile() : null; + if (wrappedFile instanceof IFileWrapper) { + IFile file = ((IFileWrapper) wrappedFile).getIFile(); + try { + // Lazily search for the target? + IRegion region = null; + String extension = file.getFileExtension(); + if (mType != null && mName != null && EXT_XML.equals(extension)) { + Pair<IFile, IRegion> target; + if (mType == ResourceType.ID) { + target = findIdInXml(mName, file); + } else { + target = findValueInXml(mType, mName, file); + } + if (target != null) { + region = target.getSecond(); + } + } + AdtPlugin.openFile(file, region); + } catch (PartInitException e) { + AdtPlugin.log(e, null); + } + } else if (wrappedFile instanceof FileWrapper) { + File file = ((FileWrapper) wrappedFile); + IPath path = new Path(file.getAbsolutePath()); + int offset = 0; + // Lazily search for the target? + if (mType != null && mName != null && EXT_XML.equals(path.getFileExtension())) { + if (file.exists()) { + Pair<File, Integer> target = findValueInXml(mType, mName, file); + if (target != null && target.getSecond() != null) { + offset = target.getSecond(); + } + } + } + openPath(path, null, offset); + } else { + throw new IllegalArgumentException("Invalid link parameters"); + } + } + + ResourceFile getFile() { + return mFile; + } + } + + /** + * XML context containing node, potentially attribute, and text regions surrounding a + * particular caret offset + */ + private static class XmlContext { + private final Node mNode; + private final Element mElement; + private final Attr mAttribute; + private final IStructuredDocumentRegion mOuterRegion; + private final ITextRegion mInnerRegion; + private final int mInnerRegionOffset; + + public XmlContext(Node node, Element element, Attr attribute, + IStructuredDocumentRegion outerRegion, + ITextRegion innerRegion, int innerRegionOffset) { + super(); + mNode = node; + mElement = element; + mAttribute = attribute; + mOuterRegion = outerRegion; + mInnerRegion = innerRegion; + mInnerRegionOffset = innerRegionOffset; + } + + /** + * Gets the current node, never null + * + * @return the surrounding node + */ + public Node getNode() { + return mNode; + } + + + /** + * Gets the current node, may be null + * + * @return the surrounding node + */ + public Element getElement() { + return mElement; + } + + /** + * Returns the current attribute, or null if we are not over an attribute + * + * @return the attribute, or null + */ + public Attr getAttribute() { + return mAttribute; + } + + /** + * Gets the region of the element + * + * @return the region of the surrounding element, never null + */ + public ITextRegion getElementRegion() { + return mOuterRegion; + } + + /** + * Gets the inner region, which can be the tag name, an attribute name, an + * attribute value, or some other portion of an XML element + * @return the inner region, never null + */ + public ITextRegion getInnerRegion() { + return mInnerRegion; + } + + /** + * Gets the caret offset relative to the inner region + * + * @return the offset relative to the inner region + */ + public int getInnerRegionCaretOffset() { + return mInnerRegionOffset; + } + + /** + * Returns a range with suffix whitespace stripped out + * + * @param document the document containing the regions + * @return the range of the inner region, minus any whitespace at the end + */ + public IRegion getInnerRange(IDocument document) { + int start = mOuterRegion.getStart() + mInnerRegion.getStart(); + int length = mInnerRegion.getLength(); + try { + String s = document.get(start, length); + for (int i = s.length() - 1; i >= 0; i--) { + if (Character.isWhitespace(s.charAt(i))) { + length--; + } + } + } catch (BadLocationException e) { + AdtPlugin.log(e, ""); //$NON-NLS-1$ + } + return new Region(start, length); + } + + /** + * Returns the node the cursor is currently on in the document. null if no node is + * selected + */ + private static XmlContext find(IDocument document, int offset) { + // Loosely based on getCurrentNode and getCurrentAttr in the WST's + // XMLHyperlinkDetector. + IndexedRegion inode = null; + IStructuredModel model = null; + try { + model = StructuredModelManager.getModelManager().getExistingModelForRead(document); + if (model != null) { + inode = model.getIndexedRegion(offset); + if (inode == null) { + inode = model.getIndexedRegion(offset - 1); + } + + if (inode instanceof Element) { + Element element = (Element) inode; + Attr attribute = null; + if (element.hasAttributes()) { + NamedNodeMap attrs = element.getAttributes(); + // go through each attribute in node and if attribute contains + // offset, return that attribute + for (int i = 0; i < attrs.getLength(); ++i) { + // assumption that if parent node is of type IndexedRegion, + // then its attributes will also be of type IndexedRegion + IndexedRegion attRegion = (IndexedRegion) attrs.item(i); + if (attRegion.contains(offset)) { + attribute = (Attr) attrs.item(i); + break; + } + } + } + + IStructuredDocument doc = model.getStructuredDocument(); + IStructuredDocumentRegion region = doc.getRegionAtCharacterOffset(offset); + if (region != null + && DOMRegionContext.XML_TAG_NAME.equals(region.getType())) { + ITextRegion subRegion = region.getRegionAtCharacterOffset(offset); + if (subRegion == null) { + return null; + } + int regionStart = region.getStartOffset(); + int subregionStart = subRegion.getStart(); + int relativeOffset = offset - (regionStart + subregionStart); + return new XmlContext(element, element, attribute, region, subRegion, + relativeOffset); + } + } else if (inode instanceof Node) { + IStructuredDocument doc = model.getStructuredDocument(); + IStructuredDocumentRegion region = doc.getRegionAtCharacterOffset(offset); + if (region != null + && DOMRegionContext.XML_CONTENT.equals(region.getType())) { + ITextRegion subRegion = region.getRegionAtCharacterOffset(offset); + int regionStart = region.getStartOffset(); + int subregionStart = subRegion.getStart(); + int relativeOffset = offset - (regionStart + subregionStart); + return new XmlContext((Node) inode, null, null, region, subRegion, + relativeOffset); + } + + } + } + } finally { + if (model != null) { + model.releaseFromRead(); + } + } + + return null; + } + } + + /** + * DOM parser which records offsets in the element nodes such that it can return + * offsets for elements later + */ + private static final class OffsetTrackingParser extends DOMParser { + + private static final String KEY_OFFSET = "offset"; //$NON-NLS-1$ + + private static final String KEY_NODE = + "http://apache.org/xml/properties/dom/current-element-node"; //$NON-NLS-1$ + + private XMLLocator mLocator; + + public OffsetTrackingParser() throws SAXException { + this.setFeature("http://apache.org/xml/features/dom/defer-node-expansion",//$NON-NLS-1$ + false); + } + + public int getOffset(Node node) { + Integer offset = (Integer) node.getUserData(KEY_OFFSET); + if (offset != null) { + return offset; + } + + return -1; + } + + @Override + public void startElement(QName elementQName, XMLAttributes attrList, Augmentations augs) + throws XNIException { + int offset = mLocator.getCharacterOffset(); + super.startElement(elementQName, attrList, augs); + + try { + Node node = (Node) this.getProperty(KEY_NODE); + if (node != null) { + node.setUserData(KEY_OFFSET, offset, null); + } + } catch (org.xml.sax.SAXException ex) { + AdtPlugin.log(ex, ""); //$NON-NLS-1$ + } + } + + @Override + public void startDocument(XMLLocator locator, String encoding, + NamespaceContext namespaceContext, Augmentations augs) throws XNIException { + super.startDocument(locator, encoding, namespaceContext, augs); + mLocator = locator; + } + } +} |