diff options
author | Tor Norbye <tnorbye@google.com> | 2010-12-04 13:52:52 -0800 |
---|---|---|
committer | Tor Norbye <tnorbye@google.com> | 2010-12-09 13:58:21 -0800 |
commit | 2a58932d3c4e2642cbdbfc161b4f7b884b3d7ea6 (patch) | |
tree | 9f9b6e4ffdd16c9894b14ef7a09827c75ba254fd /eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide | |
parent | f7bb5e5fe9c3e6cda5f6d2c4a1db4673a2f8f02d (diff) | |
download | sdk-2a58932d3c4e2642cbdbfc161b4f7b884b3d7ea6.zip sdk-2a58932d3c4e2642cbdbfc161b4f7b884b3d7ea6.tar.gz sdk-2a58932d3c4e2642cbdbfc161b4f7b884b3d7ea6.tar.bz2 |
Add a hyperlink resolved for Android XML files
This changeset adds basic hyperlink handling to Android XML files
(such as AndroidManifest.xml and layout xml files).
It registers a hyperlink detector for our XML files, and the hyperlink
detector looks up the XML model and finds the node and attributes
under the cursor. If found. it then checks these attributes for a set
of patterns that it can link to:
* If it finds an <activity> element, it looks up the activity name and
the package on the root element, and lets you jump to the activity.
Ditto for services.
* If it finds a @layout attribute value, it attempts to open the
corresponding layout file in the res/ folder in the project. Ditto
for other per-file resources like @drawable, etc.
* If it finds a value resource, like @string, @dimen, etc, it will
search through the various XML files in values/ and open up the
corresponding XML declaration in the editor with the declaration
selected.
Note that the resolver does NOT use proper full resource resolution
based on the SDK parsing that we have in use within the layout editor
etc. That's the natural next step.
Change-Id: I5880878fe67f26fb8d3b08b808c02baa1049f2c5
Diffstat (limited to 'eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide')
5 files changed, 987 insertions, 90 deletions
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/AdtPlugin.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/AdtPlugin.java index 5050afe..4ce6675 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/AdtPlugin.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/AdtPlugin.java @@ -34,6 +34,7 @@ import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper; import com.android.ide.eclipse.adt.internal.project.ProjectHelper; import com.android.ide.eclipse.adt.internal.resources.manager.GlobalProjectMonitor; import com.android.ide.eclipse.adt.internal.resources.manager.ProjectResources; +import com.android.ide.eclipse.adt.internal.resources.manager.ResourceFile; import com.android.ide.eclipse.adt.internal.resources.manager.ResourceFolder; import com.android.ide.eclipse.adt.internal.resources.manager.ResourceFolderType; import com.android.ide.eclipse.adt.internal.resources.manager.ResourceManager; @@ -44,6 +45,7 @@ import com.android.ide.eclipse.adt.internal.ui.EclipseUiHelper; import com.android.ide.eclipse.ddms.DdmsPlugin; import com.android.sdklib.IAndroidTarget; import com.android.sdklib.SdkConstants; +import com.android.sdklib.io.StreamException; import com.android.sdkstats.SdkStatsService; import org.eclipse.core.resources.IFile; @@ -98,6 +100,8 @@ import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; +import java.io.Reader; +import java.io.UnsupportedEncodingException; import java.net.MalformedURLException; import java.net.URL; import java.util.ArrayList; @@ -402,6 +406,240 @@ public class AdtPlugin extends AbstractUIPlugin implements ILogger { } /** + * Reads the contents of an {@link IFile} and return it as a String + * + * @param file the file to be read + * @return the String read from the file, or null if there was an error + */ + public static String readFile(IFile file) { + InputStream contents = null; + try { + contents = file.getContents(); + String charset = file.getCharset(); + return readFile(new InputStreamReader(contents, charset)); + } catch (CoreException e) { + // pass -- ignore files we can't read + } catch (UnsupportedEncodingException e) { + // pass -- ignore files we can't read + } finally { + try { + if (contents != null) { + contents.close(); + } + } catch (IOException e) { + AdtPlugin.log(e, "Can't read file %1$s", file); //NON-NLS-1$ + } + } + + return null; + } + + /** + * Returns true iff the given file contains the given String. + * + * @param file the file to look for the string in + * @param string the string to be searched for + * @return true if the file is found and contains the given string anywhere within it + */ + public static boolean fileContains(IFile file, String string) { + InputStream contents = null; + try { + contents = file.getContents(); + String charset = file.getCharset(); + return streamContains(new InputStreamReader(contents, charset), string); + } catch (Exception e) { + AdtPlugin.log(e, "Can't read file %1$s", file); //NON-NLS-1$ + } + + return false; + } + + /** + * Returns true iff the given input stream contains the given String. + * + * @param r the stream to look for the string in + * @param string the string to be searched for + * @return true if the file is found and contains the given string anywhere within it + */ + public static boolean streamContains(Reader r, String string) { + if (string.length() == 0) { + return true; + } + + PushbackReader reader = null; + try { + reader = new PushbackReader(r, string.length()); + char first = string.charAt(0); + while (true) { + int c = reader.read(); + if (c == -1) { + return false; + } else if (c == first) { + boolean matches = true; + for (int i = 1; i < string.length(); i++) { + c = reader.read(); + if (c == -1) { + return false; + } else if (string.charAt(i) != (char)c) { + matches = false; + // Back up the characters that did not match + reader.backup(i-1); + break; + } + } + if (matches) { + return true; + } + } + } + } catch (Exception e) { + AdtPlugin.log(e, "Can't read stream"); //NON-NLS-1$ + } finally { + try { + if (reader != null) { + reader.close(); + } + } catch (IOException e) { + AdtPlugin.log(e, "Can't read stream"); //NON-NLS-1$ + } + } + + return false; + + } + + /** + * A special reader that allows backing up in the input (up to a predefined maximum + * number of characters) + * <p> + * NOTE: This class ONLY works with the {@link #read()} method!! + */ + private static class PushbackReader extends BufferedReader { + /** + * Rolling/circular buffer. Can be a char rather than int since we never store EOF + * in it. + */ + private char[] mStorage; + + /** Points to the head of the queue. When equal to the tail, the queue is empty. */ + private int mHead; + + /** + * Points to the tail of the queue. This will move with each read of the actual + * wrapped reader, and the characters previous to it in the circular buffer are + * the most recently read characters. + */ + private int mTail; + + /** + * Creates a new reader with a given maximum number of backup characters + * + * @param reader the reader to wrap + * @param max the maximum number of characters to allow rollback for + */ + public PushbackReader(Reader reader, int max) { + super(reader); + mStorage = new char[max + 1]; + } + + @Override + public int read() throws IOException { + // Have we backed up? If so we should serve characters + // from the storage + if (mHead != mTail) { + char c = mStorage[mHead]; + mHead = (mHead + 1) % mStorage.length; + return c; + } + assert mHead == mTail; + + // No backup -- read the next character, but stash it into storage + // as well such that we can retrieve it if we must. + int c = super.read(); + mStorage[mHead] = (char) c; + mHead = mTail = (mHead + 1) % mStorage.length; + return c; + } + + /** + * Backs up the reader a given number of characters. The next N reads will yield + * the N most recently read characters prior to this backup. + * + * @param n the number of characters to be backed up + */ + public void backup(int n) { + if (n >= mStorage.length) { + throw new IllegalArgumentException("Exceeded backup limit"); + } + assert n < mStorage.length; + mHead -= n; + if (mHead < 0) { + mHead += mStorage.length; + } + } + } + + /** + * Reads the contents of a {@link ResourceFile} and returns it as a String + * + * @param file the file to be read + * @return the contents as a String, or null if reading failed + */ + public static String readFile(ResourceFile file) { + InputStream contents = null; + try { + contents = file.getFile().getContents(); + return readFile(new InputStreamReader(contents)); + } catch (StreamException e) { + // pass -- ignore files we can't read + } finally { + try { + if (contents != null) { + contents.close(); + } + } catch (IOException e) { + AdtPlugin.log(e, "Can't read layout file"); //NON-NLS-1$ + } + } + + return null; + } + + /** + * Reads the contents of an {@link InputStreamReader} and return it as a String + * + * @param inputStream the input stream to be read from + * @return the String read from the stream, or null if there was an error + */ + public static String readFile(Reader inputStream) { + BufferedReader reader = null; + try { + reader = new BufferedReader(inputStream); + StringBuilder sb = new StringBuilder(2000); + while (true) { + int c = reader.read(); + if (c == -1) { + return sb.toString(); + } else { + sb.append((char)c); + } + } + } catch (IOException e) { + // pass -- ignore files we can't read + } finally { + try { + if (reader != null) { + reader.close(); + } + } catch (IOException e) { + AdtPlugin.log(e, "Can't read input stream"); //NON-NLS-1$ + } + } + + return null; + } + + /** * Reads and returns the content of a text file embedded in the plugin jar * file. * @param filepath the file path to the text file diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/AndroidSourceViewerConfig.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/AndroidSourceViewerConfig.java index 3b41252..38d8844 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/AndroidSourceViewerConfig.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/AndroidSourceViewerConfig.java @@ -30,6 +30,7 @@ import org.eclipse.wst.xml.core.text.IXMLPartitions; import org.eclipse.wst.xml.ui.StructuredTextViewerConfigurationXML; import java.util.ArrayList; +import java.util.Map; /** * Base Source Viewer Configuration for Android resources. @@ -43,7 +44,7 @@ public class AndroidSourceViewerConfig extends StructuredTextViewerConfiguration super(); mProcessor = processor; } - + @Override public IContentAssistant getContentAssistant(ISourceViewer sourceViewer) { return super.getContentAssistant(sourceViewer); @@ -52,7 +53,7 @@ public class AndroidSourceViewerConfig extends StructuredTextViewerConfiguration /** * Returns the content assist processors that will be used for content * assist in the given source viewer and for the given partition type. - * + * * @param sourceViewer the source viewer to be configured by this * configuration * @param partitionType the partition type for which the content assist @@ -76,7 +77,7 @@ public class AndroidSourceViewerConfig extends StructuredTextViewerConfiguration IDocument doc = sourceViewer.getDocument(); if (doc != null) doc.toString(); - + processors.add(mProcessor); } @@ -87,14 +88,14 @@ public class AndroidSourceViewerConfig extends StructuredTextViewerConfiguration processors.add(p); } } - + if (processors.size() > 0) { return processors.toArray(new IContentAssistProcessor[processors.size()]); } else { return null; } } - + @Override public ITextHover getTextHover(ISourceViewer sourceViewer, String contentType) { // TODO text hover for android xml @@ -107,10 +108,21 @@ public class AndroidSourceViewerConfig extends StructuredTextViewerConfiguration // TODO auto edit strategies for android xml return super.getAutoEditStrategies(sourceViewer, contentType); } - + @Override public IContentFormatter getContentFormatter(ISourceViewer sourceViewer) { // TODO content formatter for android xml return super.getContentFormatter(sourceViewer); } + + @Override + protected Map<String, ?> getHyperlinkDetectorTargets(final ISourceViewer sourceViewer) { + @SuppressWarnings("unchecked") + Map<String, ?> targets = super.getHyperlinkDetectorTargets(sourceViewer); + // If we want to look up more context in our HyperlinkDetector via the + // getAdapter method, we should place an IAdaptable object into the map here. + targets.put("com.android.ide.eclipse.xmlCode", null); //$NON-NLS-1$ + return targets; + } + } diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/AndroidXmlEditor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/AndroidXmlEditor.java index 5ffc17e..0f02aae 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/AndroidXmlEditor.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/AndroidXmlEditor.java @@ -923,7 +923,6 @@ public abstract class AndroidXmlEditor extends FormEditor implements IResourceCh * tree. * @return True if the node was shown. */ - @SuppressWarnings("restriction") // Yes, this method relies a lot on restricted APIs public boolean show(Node xmlNode) { if (xmlNode instanceof IndexedRegion) { IndexedRegion region = (IndexedRegion)xmlNode; @@ -946,6 +945,21 @@ public abstract class AndroidXmlEditor extends FormEditor implements IResourceCh } /** + * Selects and reveals the given range in the text editor + * + * @param start the beginning offset + * @param length the length of the region to show + */ + public void show(int start, int length) { + IEditorPart textPage = getEditor(mTextPageIndex); + if (textPage instanceof StructuredTextEditor) { + StructuredTextEditor editor = (StructuredTextEditor) textPage; + setActivePage(AndroidXmlEditor.TEXT_EDITOR_ID); + editor.selectAndReveal(start, length); + } + } + + /** * Get the XML text directly from the editor. * * @param xmlNode The node whose XML text we want to obtain. diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/IncludeFinder.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/IncludeFinder.java index f33c296..0581cc0 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/IncludeFinder.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/IncludeFinder.java @@ -37,7 +37,6 @@ import com.android.ide.eclipse.adt.internal.resources.manager.ResourceManager.IR import com.android.ide.eclipse.adt.io.IFileWrapper; import com.android.sdklib.SdkConstants; import com.android.sdklib.annotations.VisibleForTesting; -import com.android.sdklib.io.StreamException; import org.eclipse.core.resources.IFile; import org.eclipse.core.resources.IMarker; @@ -57,12 +56,8 @@ import org.w3c.dom.NodeList; import org.xml.sax.InputSource; import org.xml.sax.SAXException; -import java.io.BufferedReader; import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; import java.io.StringReader; -import java.io.UnsupportedEncodingException; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; @@ -438,11 +433,11 @@ public class IncludeFinder { // If no XML model we have to read the XML contents and (possibly) // parse it if (!hadXmlModel) { - String xml = readFile(file); + String xml = AdtPlugin.readFile(file); includes = findIncludes(xml); } } else { - String xml = readFile(resourceFile); + String xml = AdtPlugin.readFile(resourceFile); includes = findIncludes(xml); } @@ -638,82 +633,6 @@ public class IncludeFinder { } } - // ----- I/O Utilities ----- - - /** Reads the contents of an {@link IFile} and return it as a String */ - private static String readFile(IFile file) { - InputStream contents = null; - try { - contents = file.getContents(); - String charset = file.getCharset(); - return readFile(new InputStreamReader(contents, charset)); - } catch (CoreException e) { - // pass -- ignore files we can't read - } catch (UnsupportedEncodingException e) { - // pass -- ignore files we can't read - } finally { - try { - if (contents != null) { - contents.close(); - } - } catch (IOException e) { - AdtPlugin.log(e, "Can't read layout file"); //NON-NLS-1$ - } - } - - return null; - } - - /** Reads the contents of a {@link ResourceFile} and returns it as a String */ - private static String readFile(ResourceFile file) { - InputStream contents = null; - try { - contents = file.getFile().getContents(); - return readFile(new InputStreamReader(contents)); - } catch (StreamException e) { - // pass -- ignore files we can't read - } finally { - try { - if (contents != null) { - contents.close(); - } - } catch (IOException e) { - AdtPlugin.log(e, "Can't read layout file"); //NON-NLS-1$ - } - } - - return null; - } - - /** Reads the contents of an {@link InputStreamReader} and return it as a String */ - private static String readFile(InputStreamReader is) { - BufferedReader reader = null; - try { - reader = new BufferedReader(is); - StringBuilder sb = new StringBuilder(2000); - while (true) { - int c = reader.read(); - if (c == -1) { - return sb.toString(); - } else { - sb.append((char) c); - } - } - } catch (IOException e) { - // pass -- ignore files we can't read - } finally { - try { - if (reader != null) { - reader.close(); - } - } catch (IOException e) { - AdtPlugin.log(e, "Can't read layout file"); //NON-NLS-1$ - } - } - - return null; - } - // ----- Cycle detection ----- private void detectCycles(String from) { diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/xml/XmlHyperlinkResolver.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/xml/XmlHyperlinkResolver.java new file mode 100644 index 0000000..24f5d33 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/xml/XmlHyperlinkResolver.java @@ -0,0 +1,714 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.adt.internal.editors.xml; + +import static com.android.ide.common.layout.LayoutConstants.ANDROID_URI; +import static com.android.ide.common.layout.LayoutConstants.ATTR_CLASS; +import static com.android.ide.common.layout.LayoutConstants.VIEW; +import static com.android.ide.eclipse.adt.AndroidConstants.EXT_XML; +import static com.android.ide.eclipse.adt.AndroidConstants.WS_RESOURCES; +import static com.android.ide.eclipse.adt.AndroidConstants.WS_SEP; +import static com.android.ide.eclipse.adt.internal.editors.resources.descriptors.ResourcesDescriptors.NAME_ATTR; +import static com.android.ide.eclipse.adt.internal.editors.resources.descriptors.ResourcesDescriptors.ROOT_ELEMENT; +import static com.android.sdklib.xml.AndroidManifest.ATTRIBUTE_NAME; +import static com.android.sdklib.xml.AndroidManifest.ATTRIBUTE_PACKAGE; +import static com.android.sdklib.xml.AndroidManifest.NODE_ACTIVITY; +import static com.android.sdklib.xml.AndroidManifest.NODE_SERVICE; + +import com.android.ide.common.layout.Pair; +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.AndroidConstants; +import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor; +import com.android.ide.eclipse.adt.internal.resources.ResourceType; +import com.android.ide.eclipse.adt.internal.resources.manager.FolderTypeRelationship; +import com.android.ide.eclipse.adt.internal.resources.manager.ProjectResources; +import com.android.ide.eclipse.adt.internal.resources.manager.ResourceFolder; +import com.android.ide.eclipse.adt.internal.resources.manager.ResourceFolderType; +import com.android.ide.eclipse.adt.internal.resources.manager.ResourceManager; +import com.android.ide.eclipse.adt.io.IFolderWrapper; +import com.android.sdklib.SdkConstants; +import com.android.sdklib.annotations.VisibleForTesting; + +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IFolder; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IResource; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.jdt.core.IJavaElement; +import org.eclipse.jdt.core.IJavaProject; +import org.eclipse.jdt.core.JavaCore; +import org.eclipse.jdt.core.JavaModelException; +import org.eclipse.jdt.ui.JavaUI; +import org.eclipse.jface.action.IStatusLineManager; +import org.eclipse.jface.text.IDocument; +import org.eclipse.jface.text.IRegion; +import org.eclipse.jface.text.ITextViewer; +import org.eclipse.jface.text.Region; +import org.eclipse.jface.text.hyperlink.AbstractHyperlinkDetector; +import org.eclipse.jface.text.hyperlink.IHyperlink; +import org.eclipse.swt.widgets.Display; +import org.eclipse.ui.IEditorInput; +import org.eclipse.ui.IEditorPart; +import org.eclipse.ui.IEditorSite; +import org.eclipse.ui.IFileEditorInput; +import org.eclipse.ui.IWorkbenchPage; +import org.eclipse.ui.IWorkbenchWindow; +import org.eclipse.ui.PartInitException; +import org.eclipse.ui.PlatformUI; +import org.eclipse.ui.ide.IDE; +import org.eclipse.wst.sse.core.StructuredModelManager; +import org.eclipse.wst.sse.core.internal.encoding.util.Logger; +import org.eclipse.wst.sse.core.internal.provisional.IStructuredModel; +import org.eclipse.wst.sse.core.internal.provisional.IndexedRegion; +import org.eclipse.wst.xml.core.internal.provisional.document.IDOMModel; +import org.w3c.dom.Attr; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.NamedNodeMap; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; + +import java.io.IOException; +import java.util.List; +import java.util.regex.Pattern; + +/** + * Resolver to create hyperlinks in Android XML files to jump to associated resources -- + * activities, layouts, strings, etc + */ +@SuppressWarnings("restriction") +public class XmlHyperlinkResolver extends AbstractHyperlinkDetector { + + /** Regular expression matching a FQCN for a view class */ + @VisibleForTesting + /* package */ static final Pattern CLASS_PATTERN = Pattern.compile( + "(([a-zA-Z_\\$][a-zA-Z0-9_\\$]*)+\\.)+[a-zA-Z_\\$][a-zA-Z0-9_\\$]*"); // NON-NLS-1$ + + /** + * Determines whether the given node/attribute corresponds to a target we can link to. + * + * @param node the node surrounding the cursor + * @param attribute the attribute surrounding the cursor + * @return true if the given node/attribute pair is a link target + */ + private boolean isLinkable(Node node, Attr attribute) { + if (isClassReference(node, attribute)) { + return true; + } + + // Everything else here is attribute based + if (attribute == null) { + return false; + } + + if (isActivity(node, attribute) || isService(node, attribute)) { + return true; + } + + Pair<ResourceType,String> resource = getResource(attribute.getValue()); + if (resource != null) { + ResourceType type = resource.getFirst(); + if (type != null) { + return true; + } + } + + return false; + } + + /** + * Returns true if this node/attribute pair corresponds to a manifest reference to + * an activity. + */ + private boolean isActivity(Node node, Attr attribute) { + // Is this an <activity> or <service> in an AndroidManifest.xml file? If so, jump + // to it + String nodeName = node.getNodeName(); + if (NODE_ACTIVITY.equals(nodeName) && ATTRIBUTE_NAME.equals(attribute.getLocalName()) + && ANDROID_URI.equals(attribute.getNamespaceURI())) { + return true; + } + + return false; + } + + private boolean isClassReference(Node node, Attr attribute) { + String tag = node.getNodeName(); + if (attribute != null && ATTR_CLASS.equals(attribute.getLocalName()) && VIEW.equals(tag)) { + return true; + } + + // If the element looks like a fully qualified class name (e.g. it's a custom view + // element) offer it as a link + if (tag.indexOf('.') != -1 && CLASS_PATTERN.matcher(tag).matches()) { + return true; + } + + return false; + } + + private String getClassFqcn(Node node, Attr attribute) { + String tag = node.getNodeName(); + if (attribute != null && ATTR_CLASS.equals(attribute.getLocalName()) && VIEW.equals(tag)) { + return attribute.getValue(); + } + + if (tag.indexOf('.') != -1 && CLASS_PATTERN.matcher(tag).matches()) { + return tag; + } + + return null; + } + + /** + * Returns true if this node/attribute pair corresponds to a manifest reference to + * an service. + */ + private boolean isService(Node node, Attr attribute) { + // Is this an <activity> or <service> in an AndroidManifest.xml file? If so, jump to it + String nodeName = node.getNodeName(); + if (NODE_SERVICE.equals(nodeName) && ATTRIBUTE_NAME.equals(attribute.getLocalName()) + && ANDROID_URI.equals(attribute.getNamespaceURI())) { + return true; + } + + return false; + } + + /** + * Returns the fully qualified class name of an activity referenced by the given + * AndroidManifest.xml node + */ + private String getActivityClassFqcn(Node node, Attr attribute) { + StringBuilder sb = new StringBuilder(); + Element root = node.getOwnerDocument().getDocumentElement(); + String pkg = root.getAttribute(ATTRIBUTE_PACKAGE); + sb.append(pkg); + String className = attribute.getValue(); + sb.append(className); + return sb.toString(); + } + + /** + * Returns the fully qualified class name of a service referenced by the given + * AndroidManifest.xml node + */ + private String getServiceClassFqcn(Node node, Attr attribute) { + // Same logic + return getActivityClassFqcn(node, attribute); + } + + /** + * Is this a resource that can be defined in any file within the "values" folder? + */ + private boolean isValueResource(ResourceType type) { + ResourceFolderType[] folderTypes = FolderTypeRelationship.getRelatedFolders(type); + for (ResourceFolderType folderType : folderTypes) { + if (folderType == ResourceFolderType.VALUES) { + return true; + } + } + + return false; + } + + /** + * Is this a resource that resides in a file whose name is determined by the + * resource name? + */ + private boolean isFileResource(ResourceType type) { + ResourceFolderType[] folderTypes = FolderTypeRelationship.getRelatedFolders(type); + for (ResourceFolderType folderType : folderTypes) { + if (folderType != ResourceFolderType.VALUES) { + return true; + } + } + + return false; + } + + /** + * Computes the actual exact location to jump to for a given node+attribute + * combination. Can optionally return an offset range within a file to highlight. + * + * @param node surrounding node + * @param attribute surrounding attribute + * @return a pair of a file location and an optional (or null) region within that file + */ + private boolean open(Node node, Attr attribute) { + IProject project = getProject(); + if (project == null) { + return false; + } + + if (isActivity(node, attribute)) { + String fqcn = getActivityClassFqcn(node, attribute); + return openJavaClass(project, fqcn); + } else if (isService(node, attribute)) { + String fqcn = getServiceClassFqcn(node, attribute); + return openJavaClass(project, fqcn); + } else if (isClassReference(node, attribute)) { + return openJavaClass(project, getClassFqcn(node, attribute)); + } else { + Pair<ResourceType,String> resource = getResource(attribute.getValue()); + if (resource != null) { + ResourceType type = resource.getFirst(); + if (type != null) { + String name = resource.getSecond(); + IResource member = null; + IRegion region = null; + + // Is this something found in a values/ folder? + if (isValueResource(type)) { + Pair<IFile,IRegion> def = findValueDefinition(project, type, name); + if (def != null) { + member = def.getFirst(); + region = def.getSecond(); + } + } + + // Is this something found in a file identified by the name? + // (Some URLs can be both -- for example, a color can be both + // listed in an xml files in values/ as well as under /res/color/). + if (member == null && isFileResource(type)) { + // It's a single file resource, like @layout/foo; open + // layout/foo.xml + member = findMember(project, type, name); + } + + try { + if (member != null && member instanceof IFile) { + IFile file = (IFile) member; + IEditorPart sourceEditor = getEditor(); + IWorkbenchPage page = sourceEditor.getEditorSite().getPage(); + IEditorPart targetEditor = IDE.openEditor(page, file, true); + if ((region != null) && (targetEditor instanceof AndroidXmlEditor)) { + ((AndroidXmlEditor) targetEditor).show(region.getOffset(), + region.getLength()); + } + + return true; + } + } catch (PartInitException pie) { + Logger.log(Logger.WARNING_DEBUG, pie.getMessage(), pie); + } + } + } + } + + return false; + } + + /** Opens a Java class for the given fully qualified class name */ + private boolean openJavaClass(IProject project, String fqcn) { + if (fqcn == null) { + return false; + } + + // Handle inner classes + if (fqcn.indexOf('$') != -1) { + fqcn = fqcn.replaceAll("\\$", "."); //NON-NLS-1$ //NON-NLS-2$ + } + + try { + if (project.hasNature(JavaCore.NATURE_ID)) { + IJavaProject javaProject = JavaCore.create(project); + IJavaElement result = javaProject.findType(fqcn); + if (result != null) { + return JavaUI.openInEditor(result) != null; + } + } + } catch (PartInitException e) { + AdtPlugin.log(e, "Can't open class %1$s", fqcn); // NON-NLS-1$ + } catch (JavaModelException e) { + AdtPlugin.log(e, "Can't open class %1$s", fqcn); // NON-NLS-1$ + Display.getCurrent().beep(); + } catch (CoreException e) { + AdtPlugin.log(e, "Can't open class %1$s", fqcn); // NON-NLS-1$ + } + + return false; + } + + /** Looks up the project member of the given type and the given name */ + private IResource findMember(IProject project, ResourceType type, String name) { + String relativePath; + IResource member; + String folder = WS_RESOURCES + WS_SEP + type.getName(); + relativePath = folder + WS_SEP + name + '.' + EXT_XML; + member = project.findMember(relativePath); + if (member == null) { + // Search for any file in the directory with the given basename; + // this is necessary for files like drawables that don't have + // .xml files. It's an error to have conflicts in basenames for + // these resources types so this is safe. + IResource d = project.findMember(folder); + if (d instanceof IFolder) { + IFolder dir = (IFolder) d; + member = findInFolder(name, dir); + } + + if (member == null) { + // Still couldn't find the member; it must not be defined in a "base" directory + // like "layout"; look in various variations + ResourceManager manager = ResourceManager.getInstance(); + ProjectResources resources = manager.getProjectResources(project); + + ResourceFolderType[] folderTypes = FolderTypeRelationship.getRelatedFolders(type); + for (ResourceFolderType folderType : folderTypes) { + if (folderType != ResourceFolderType.VALUES) { + List<ResourceFolder> folders = resources.getFolders(folderType); + for (ResourceFolder resourceFolder : folders) { + if (resourceFolder.getFolder() instanceof IFolderWrapper) { + IFolderWrapper wrapper = + (IFolderWrapper) resourceFolder.getFolder(); + IFolder iFolder = wrapper.getIFolder(); + member = findInFolder(name, iFolder); + if (member != null) { + break; + } + } + } + } + } + } + } + + return member; + } + + /** + * Finds a resource of the given name in the given folder, searching for possible file + * extensions + */ + private IResource findInFolder(String name, IFolder dir) { + try { + for (IResource child : dir.members()) { + String fileName = child.getName(); + int index = fileName.indexOf('.'); + if (index != -1) { + fileName = fileName.substring(0, index); + } + if (fileName.equals(name)) { + return child; + } + } + } catch (CoreException e) { + AdtPlugin.log(e, ""); // NON-NLS-1$ + } + + return null; + } + + /** + * Search for a resource of a "multi-file" type (like @string) where the value can be + * found in any file within the folder containing resource of that type (in the case + * of @string, "values", and in the case of @color, "colors", etc). + */ + private Pair<IFile, IRegion> findValueDefinition(IProject project, ResourceType type, + String name) { + // Search within the files in the values folder and find the value which defines + // the given resource. To be efficient, we will only parse XML files that contain + // a string match of the given token name. + + String values = AndroidConstants.WS_RESOURCES + AndroidConstants.WS_SEP + + SdkConstants.FD_VALUES; + IFolder f = project.getFolder(values); + if (f.exists()) { + try { + // Check XML files in values/ + for (IResource resource : f.members()) { + if (resource.exists() && !resource.isDerived() && resource instanceof IFile) { + IFile file = (IFile) resource; + // Must have an XML extension + if (EXT_XML.equals(file.getFileExtension())) { + Pair<IFile, IRegion> target = findInXml(type, name, file); + if (target != null) { + return target; + } + } + } + } + } catch (CoreException e) { + // pass + } + } + return null; + } + + /** Parses the given file and locates a definition of the given resource */ + private Pair<IFile, IRegion> findInXml(ResourceType type, String name, IFile file) { + IStructuredModel model = null; + try { + model = StructuredModelManager.getModelManager().getExistingModelForRead(file); + if (model == null) { + // There is no open or cached model for the file; see if the file looks + // like it's interesting (content contains the String name we are looking for) + if (AdtPlugin.fileContains(file, name)) { + // Yes, so parse content + model = StructuredModelManager.getModelManager().getModelForRead(file); + } + } + if (model instanceof IDOMModel) { + IDOMModel domModel = (IDOMModel) model; + Document document = domModel.getDocument(); + return findInDocument(type, name, file, document); + } + } catch (IOException e) { + AdtPlugin.log(e, "Can't parse %1$s", file); // NON-NLS-1$ + } catch (CoreException e) { + AdtPlugin.log(e, "Can't parse %1$s", file); // NON-NLS-1$ + } finally { + if (model != null) { + model.releaseFromRead(); + } + } + + return null; + } + + /** Looks within an XML DOM document for the given resource name and returns it */ + private Pair<IFile, IRegion> findInDocument(ResourceType type, String name, IFile file, + Document document) { + Element root = document.getDocumentElement(); + if (root.getTagName().equals(ROOT_ELEMENT)) { + NodeList children = root.getChildNodes(); + for (int i = 0, n = children.getLength(); i < n; i++) { + Node child = children.item(i); + if (child.getNodeType() == Node.ELEMENT_NODE) { + Element element = (Element)child; + if (element.getTagName().equals(type.getName())) { + String elementName = element.getAttribute(NAME_ATTR); + if (elementName.equals(name)) { + IRegion region = null; + if (element instanceof IndexedRegion) { + IndexedRegion r = (IndexedRegion) element; + // IndexedRegion.getLength() returns bogus values + int length = r.getEndOffset() - r.getStartOffset(); + region = new Region(r.getStartOffset(), length); + } + + return Pair.of(file, region); + } + } + } + } + } + + return null; + } + + /** Return the resource type of the given url, and the resource name */ + private Pair<ResourceType,String> getResource(String url) { + if (!url.startsWith("@")) { //$NON-NLS-1$ + return null; + } + int typeEnd = url.indexOf('/', 1); + if (typeEnd == -1) { + return null; + } + int nameBegin = typeEnd + 1; + + // Skip @ and @+ + int typeBegin = url.startsWith("@+") ? 2 : 1; // NON-NLS-1$ + + int colon = url.lastIndexOf(':', typeEnd); + if (colon != -1) { + typeBegin = colon + 1; + } + String typeName = url.substring(typeBegin, typeEnd); + ResourceType type = ResourceType.getEnum(typeName); + if (type == null) { + return null; + } + String name = url.substring(nameBegin); + + return Pair.of(type, name); + } + + // ----- Implements IHyperlinkDetector ----- + + public IHyperlink[] detectHyperlinks(ITextViewer textViewer, IRegion region, + boolean canShowMultipleHyperlinks) { + + if (region == null || textViewer == null) { + return null; + } + + IDocument document = textViewer.getDocument(); + Node currentNode = getCurrentNode(document, region.getOffset()); + if (currentNode == null || currentNode.getNodeType() != Node.ELEMENT_NODE) { + return null; + } + + Attr currentAttr = getCurrentAttrNode(currentNode, region.getOffset()); + IRegion range = null; + if (currentAttr == null) { + if (currentNode instanceof IndexedRegion) { + IndexedRegion r = (IndexedRegion) currentNode; + range = new Region(r.getStartOffset() + 1, currentNode.getNodeName().length()); + } + } else if (currentAttr instanceof IndexedRegion) { + IndexedRegion r = (IndexedRegion) currentAttr; + range = new Region(r.getStartOffset(), r.getLength()); + } + + if (isLinkable(currentNode, currentAttr) && range != null) { + IHyperlink hyperlink = new DeferredResolutionLink(currentNode, currentAttr, range); + if (hyperlink != null) { + return new IHyperlink[] { + hyperlink + }; + } + } + + return null; + } + + /** Returns the editor applicable to this hyperlink detection */ + private IEditorPart getEditor() { + // I would like to be able to find this via getAdapter(TextEditor.class) but + // couldn't find a way to initialize the editor context from + // AndroidSourceViewerConfig#getHyperlinkDetectorTargets (which only has + // a TextViewer, not a TextEditor, instance). + // + // Therefore, for now, use a hack. This hack is reasonable because hyperlink + // resolvers are only run for the front-most visible window in the active + // workbench. + IWorkbenchWindow window = PlatformUI.getWorkbench().getActiveWorkbenchWindow(); + if (window != null) { + IWorkbenchPage page = window.getActivePage(); + if (page != null) { + return page.getActiveEditor(); + } + } + + return null; + } + + /** Returns the project applicable to this hyperlink detection */ + private IProject getProject() { + IEditorPart editor = getEditor(); + if (editor != null) { + IEditorInput input = editor.getEditorInput(); + if (input instanceof IFileEditorInput) { + IFileEditorInput fileInput = (IFileEditorInput) input; + return fileInput.getFile().getProject(); + } + } + + return null; + } + + /** + * Hyperlink implementation which delays computing the actual file and offset target + * until it is asked to open the hyperlink + */ + class DeferredResolutionLink implements IHyperlink { + private Node mNode; + private Attr mAttribute; + private IRegion mRegion; + + public DeferredResolutionLink(Node mNode, Attr mAttribute, IRegion mRegion) { + super(); + this.mNode = mNode; + this.mAttribute = mAttribute; + this.mRegion = mRegion; + } + + public IRegion getHyperlinkRegion() { + return mRegion; + } + + public String getHyperlinkText() { + return null; + } + + public String getTypeLabel() { + return null; + } + + public void open() { + // Lazily compute the location to open + if (!XmlHyperlinkResolver.this.open(mNode, mAttribute)) { + // Failed: display message to the user + String message = String.format("Could not open %1$s", mAttribute.getValue()); + IEditorSite editorSite = getEditor().getEditorSite(); + IStatusLineManager status = editorSite.getActionBars().getStatusLineManager(); + status.setErrorMessage(message); + } + } + } + + // The below code are private utility methods copied from the XMLHyperlinkDetector + // in the Eclipse WTP. + + /** + * Returns the attribute node within node at offset. + * <p> + * Copy of Eclipse's XMLHyperlinkDetector#getCurrentAttrNode + */ + private Attr getCurrentAttrNode(Node node, int offset) { + if ((node instanceof IndexedRegion) && ((IndexedRegion) node).contains(offset) + && (node.hasAttributes())) { + NamedNodeMap attrs = node.getAttributes(); + // go through each attribute in node and if attribute contains + // offset, return that attribute + for (int i = 0; i < attrs.getLength(); ++i) { + // assumption that if parent node is of type IndexedRegion, + // then its attributes will also be of type IndexedRegion + IndexedRegion attRegion = (IndexedRegion) attrs.item(i); + if (attRegion.contains(offset)) { + return (Attr) attrs.item(i); + } + } + } + + return null; + } + + /** + * Returns the node the cursor is currently on in the document. null if no node is + * selected + * <p> + * Copy of Eclipse's XMLHyperlinkDetector#getCurrentNode + */ + private Node getCurrentNode(IDocument document, int offset) { + // get the current node at the offset (returns either: element, + // doc type, text) + IndexedRegion inode = null; + IStructuredModel sModel = null; + try { + sModel = StructuredModelManager.getModelManager().getExistingModelForRead(document); + if (sModel != null) { + inode = sModel.getIndexedRegion(offset); + if (inode == null) { + inode = sModel.getIndexedRegion(offset - 1); + } + } + } finally { + if (sModel != null) { + sModel.releaseFromRead(); + } + } + + if (inode instanceof Node) { + return (Node) inode; + } + return null; + } +} |