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