| /* |
| * 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; |
| } |
| } |
| } |