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