diff options
author | Tor Norbye <tnorbye@google.com> | 2010-12-09 15:22:29 -0800 |
---|---|---|
committer | Android Code Review <code-review@android.com> | 2010-12-09 15:22:29 -0800 |
commit | eadfb94154126f670fa0ada62d36fec5f6188bd9 (patch) | |
tree | 9f9b6e4ffdd16c9894b14ef7a09827c75ba254fd /eclipse/plugins/com.android.ide.eclipse.adt/src | |
parent | f7bb5e5fe9c3e6cda5f6d2c4a1db4673a2f8f02d (diff) | |
parent | 2a58932d3c4e2642cbdbfc161b4f7b884b3d7ea6 (diff) | |
download | sdk-eadfb94154126f670fa0ada62d36fec5f6188bd9.zip sdk-eadfb94154126f670fa0ada62d36fec5f6188bd9.tar.gz sdk-eadfb94154126f670fa0ada62d36fec5f6188bd9.tar.bz2 |
Merge "Add a hyperlink resolved for Android XML files"
Diffstat (limited to 'eclipse/plugins/com.android.ide.eclipse.adt/src')
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; + } +} |