/* * Copyright (C) 2007 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.editors; import com.android.ide.eclipse.adt.sdk.AndroidTargetData; import com.android.ide.eclipse.editors.descriptors.AttributeDescriptor; import com.android.ide.eclipse.editors.descriptors.DescriptorsUtils; import com.android.ide.eclipse.editors.descriptors.ElementDescriptor; import com.android.ide.eclipse.editors.descriptors.IDescriptorProvider; import com.android.ide.eclipse.editors.descriptors.SeparatorAttributeDescriptor; import com.android.ide.eclipse.editors.descriptors.TextAttributeDescriptor; import com.android.ide.eclipse.editors.descriptors.TextValueDescriptor; import com.android.ide.eclipse.editors.descriptors.XmlnsAttributeDescriptor; import com.android.ide.eclipse.editors.uimodel.UiAttributeNode; import com.android.ide.eclipse.editors.uimodel.UiElementNode; import com.android.ide.eclipse.editors.uimodel.UiFlagAttributeNode; import com.android.sdklib.SdkConstants; import org.eclipse.jface.text.BadLocationException; import org.eclipse.jface.text.IDocument; import org.eclipse.jface.text.ITextViewer; import org.eclipse.jface.text.TextSelection; import org.eclipse.jface.text.contentassist.CompletionProposal; import org.eclipse.jface.text.contentassist.ICompletionProposal; import org.eclipse.jface.text.contentassist.IContentAssistProcessor; import org.eclipse.jface.text.contentassist.IContextInformation; import org.eclipse.jface.text.contentassist.IContextInformationValidator; import org.eclipse.jface.text.source.ISourceViewer; import org.eclipse.jface.viewers.ISelection; import org.eclipse.swt.graphics.Image; import org.eclipse.ui.IEditorPart; import org.eclipse.ui.IWorkbenchPage; import org.eclipse.ui.IWorkbenchWindow; import org.eclipse.ui.PlatformUI; import org.eclipse.wst.sse.core.StructuredModelManager; import org.eclipse.wst.sse.core.internal.provisional.IModelManager; import org.eclipse.wst.sse.core.internal.provisional.IStructuredModel; import org.w3c.dom.NamedNodeMap; import org.w3c.dom.Node; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.regex.Pattern; /** * Content Assist Processor for Android XML files */ public abstract class AndroidContentAssist implements IContentAssistProcessor { /** Regexp to detect a full attribute after an element tag. * <pre>Syntax: * name = "..." quoted string with all but < and " * or: * name = '...' quoted string with all but < and ' * </pre> */ private static Pattern sFirstAttribute = Pattern.compile( "^ *[a-zA-Z_:]+ *= *(?:\"[^<\"]*\"|'[^<']*')"); //$NON-NLS-1$ /** Regexp to detect an element tag name */ private static Pattern sFirstElementWord = Pattern.compile("^[a-zA-Z0-9_:]+"); //$NON-NLS-1$ /** Regexp to detect whitespace */ private static Pattern sWhitespace = Pattern.compile("\\s+"); //$NON-NLS-1$ protected final static String ROOT_ELEMENT = ""; /** Descriptor of the root of the XML hierarchy. This a "fake" ElementDescriptor which * is used to list all the possible roots given by actual implementations. * DO NOT USE DIRECTLY. Call {@link #getRootDescriptor()} instead. */ private ElementDescriptor mRootDescriptor; private final int mDescriptorId; private AndroidEditor mEditor; /** * Constructor for AndroidContentAssist * @param descriptorId An id for {@link AndroidTargetData#getDescriptorProvider(int)}. * The Id can be one of {@link AndroidTargetData#DESCRIPTOR_MANIFEST}, * {@link AndroidTargetData#DESCRIPTOR_LAYOUT}, * {@link AndroidTargetData#DESCRIPTOR_MENU}, * or {@link AndroidTargetData#DESCRIPTOR_XML}. * All other values will throw an {@link IllegalArgumentException} later at runtime. */ public AndroidContentAssist(int descriptorId) { mDescriptorId = descriptorId; } /** * Returns a list of completion proposals based on the * specified location within the document that corresponds * to the current cursor position within the text viewer. * * @param viewer the viewer whose document is used to compute the proposals * @param offset an offset within the document for which completions should be computed * @return an array of completion proposals or <code>null</code> if no proposals are possible * * @see org.eclipse.jface.text.contentassist.IContentAssistProcessor#computeCompletionProposals(org.eclipse.jface.text.ITextViewer, int) */ public ICompletionProposal[] computeCompletionProposals(ITextViewer viewer, int offset) { if (mEditor == null) { mEditor = getAndroidEditor(viewer); } UiElementNode rootUiNode = mEditor.getUiRootNode(); Object[] choices = null; /* An array of ElementDescriptor, or AttributeDescriptor or String or null */ String parent = ""; //$NON-NLS-1$ String wordPrefix = extractElementPrefix(viewer, offset); char needTag = 0; boolean isElement = false; boolean isAttribute = false; Node currentNode = getNode(viewer, offset); if (currentNode == null) return null; // check to see if we can find a UiElementNode matching this XML node UiElementNode currentUiNode = rootUiNode == null ? null : rootUiNode.findXmlNode(currentNode); if (currentNode.getNodeType() == Node.ELEMENT_NODE) { parent = currentNode.getNodeName(); if (wordPrefix.equals(parent)) { // We are still editing the element's tag name, not the attributes // (the element's tag name may not even be complete) isElement = true; choices = getChoicesForElement(parent, currentNode); } else { // We're not editing the current node name, so we might be editing its // attributes instead... isAttribute = true; AttribInfo info = parseAttributeInfo(viewer, offset); if (info != null) { // We're editing attributes in an element node (either the attributes' names // or their values). choices = getChoicesForAttribute(parent, currentNode, currentUiNode, info); if (info.correctedPrefix != null) { wordPrefix = info.correctedPrefix; } needTag = info.needTag; } } } else if (currentNode.getNodeType() == Node.TEXT_NODE) { isElement = true; // Examine the parent of the text node. choices = getChoicesForTextNode(currentNode); } // Abort if we can't recognize the context or there are no completion choices if (choices == null || choices.length == 0) return null; if (isElement) { // If we found some suggestions, do we need to add an opening "<" bracket // for the element? We don't if the cursor is right after "<" or "</". // Per XML Spec, there's no whitespace between "<" or "</" and the tag name. int offset2 = offset - wordPrefix.length() - 1; int c1 = extractChar(viewer, offset2); if (!((c1 == '<') || (c1 == '/' && extractChar(viewer, offset2 - 1) == '<'))) { needTag = '<'; } } // get the selection length int selectionLength = 0; ISelection selection = viewer.getSelectionProvider().getSelection(); if (selection instanceof TextSelection) { TextSelection textSelection = (TextSelection)selection; selectionLength = textSelection.getLength(); } return computeProposals(offset, currentNode, choices, wordPrefix, needTag, isAttribute, selectionLength); } /** * Returns the namespace prefix matching the Android Resource URI. * If no such declaration is found, returns the default "android" prefix. * * @param node The current node. Must not be null. * @param nsUri The namespace URI of which the prefix is to be found, * e.g. {@link SdkConstants#NS_RESOURCES} * @return The first prefix declared or the default "android" prefix. */ private String lookupNamespacePrefix(Node node, String nsUri) { // Note: Node.lookupPrefix is not implemented in wst/xml/core NodeImpl.java // The following emulates this: // String prefix = node.lookupPrefix(SdkConstants.NS_RESOURCES); if (XmlnsAttributeDescriptor.XMLNS_URI.equals(nsUri)) { return "xmlns"; //$NON-NLS-1$ } HashSet<String> visited = new HashSet<String>(); String prefix = null; for (; prefix == null && node != null && node.getNodeType() == Node.ELEMENT_NODE; node = node.getParentNode()) { NamedNodeMap attrs = node.getAttributes(); for (int n = attrs.getLength() - 1; n >= 0; --n) { Node attr = attrs.item(n); if ("xmlns".equals(attr.getPrefix())) { //$NON-NLS-1$ String uri = attr.getNodeValue(); if (SdkConstants.NS_RESOURCES.equals(uri)) { return attr.getLocalName(); } visited.add(uri); } } } // Use a sensible default prefix if we can't find one. // We need to make sure the prefix is not one that was declared in the scope // visited above. prefix = SdkConstants.NS_RESOURCES.equals(nsUri) ? "android" : "ns"; //$NON-NLS-1$ //$NON-NLS-2$ String base = prefix; for (int i = 1; visited.contains(prefix); i++) { prefix = base + Integer.toString(i); } return prefix; } /** * Gets the choices when the user is editing the name of an XML element. * <p/> * The user is editing the name of an element (the "parent"). * Find the grand-parent and if one is found, return its children element list. * The name which is being edited should be one of those. * <p/> * Example: <manifest><applic*cursor* => returns the list of all elements that * can be found under <manifest>, of which <application> is one of the choices. * * @return an ElementDescriptor[] or null if no valid element was found. */ private Object[] getChoicesForElement(String parent, Node current_node) { ElementDescriptor grandparent = null; if (current_node.getParentNode().getNodeType() == Node.ELEMENT_NODE) { grandparent = getDescriptor(current_node.getParentNode().getNodeName()); } else if (current_node.getParentNode().getNodeType() == Node.DOCUMENT_NODE) { grandparent = getRootDescriptor(); } if (grandparent != null) { for (ElementDescriptor e : grandparent.getChildren()) { if (e.getXmlName().startsWith(parent)) { return grandparent.getChildren(); } } } return null; } /** * Gets the choices when the user is editing an XML attribute. * <p/> * In input, attrInfo contains details on the analyzed context, namely whether the * user is editing an attribute value (isInValue) or an attribute name. * <p/> * In output, attrInfo also contains two possible new values (this is a hack to circumvent * the lack of out-parameters in Java): * - AttribInfo.correctedPrefix if the user has been editing an attribute value and it has * been detected that what the user typed is different from what extractElementPrefix() * predicted. This happens because extractElementPrefix() stops when a character that * cannot be an element name appears whereas parseAttributeInfo() uses a grammar more * lenient as suitable for attribute values. * - AttribInfo.needTag will be non-zero if we find that the attribute completion proposal * must be double-quoted. * @param currentUiNode * * @return an AttributeDescriptor[] if the user is editing an attribute name. * a String[] if the user is editing an attribute value with some known values, * or null if nothing is known about the context. */ private Object[] getChoicesForAttribute(String parent, Node currentNode, UiElementNode currentUiNode, AttribInfo attrInfo) { Object[] choices = null; if (attrInfo.isInValue) { // Editing an attribute's value... Get the attribute name and then the // possible choice for the tuple(parent,attribute) String value = attrInfo.value; if (value.startsWith("'") || value.startsWith("\"")) { //$NON-NLS-1$ //$NON-NLS-2$ value = value.substring(1); // The prefix that was found at the beginning only scan for characters // valid of tag name. We now know the real prefix for this attribute's // value, which is needed to generate the completion choices below. attrInfo.correctedPrefix = value; } else { attrInfo.needTag = '"'; } if (currentUiNode != null) { // look for an UI attribute matching the current attribute name String attrName = attrInfo.name; // remove any namespace prefix from the attribute name int pos = attrName.indexOf(':'); if (pos >= 0) { attrName = attrName.substring(pos + 1); } UiAttributeNode currAttrNode = null; for (UiAttributeNode attrNode : currentUiNode.getUiAttributes()) { if (attrNode.getDescriptor().getXmlLocalName().equals(attrName)) { currAttrNode = attrNode; break; } } if (currAttrNode != null) { choices = currAttrNode.getPossibleValues(value); if (currAttrNode instanceof UiFlagAttributeNode) { // A "flag" can consist of several values separated by "or" (|). // If the correct prefix contains such a pipe character, we change // it so that only the currently edited value is completed. pos = value.indexOf('|'); if (pos >= 0) { attrInfo.correctedPrefix = value = value.substring(pos + 1); attrInfo.needTag = 0; } } } } if (choices == null) { // fallback on the older descriptor-only based lookup. // in order to properly handle the special case of the name attribute in // the action tag, we need the grandparent of the action node, to know // what type of actions we need. // e.g. activity -> intent-filter -> action[@name] String greatGrandParentName = null; Node grandParent = currentNode.getParentNode(); if (grandParent != null) { Node greatGrandParent = grandParent.getParentNode(); if (greatGrandParent != null) { greatGrandParentName = greatGrandParent.getLocalName(); } } AndroidTargetData data = mEditor.getTargetData(); if (data != null) { choices = data.getAttributeValues(parent, attrInfo.name, greatGrandParentName); } } } else { // Editing an attribute's name... Get attributes valid for the parent node. if (currentUiNode != null) { choices = currentUiNode.getAttributeDescriptors(); } else { ElementDescriptor parent_desc = getDescriptor(parent); choices = parent_desc.getAttributes(); } } return choices; } /** * Gets the choices when the user is editing an XML text node. * <p/> * This means the user is editing outside of any XML element or attribute. * Simply return the list of XML elements that can be present there, based on the * parent of the current node. * * @return An ElementDescriptor[] or null. */ private Object[] getChoicesForTextNode(Node currentNode) { Object[] choices = null; String parent; Node parent_node = currentNode.getParentNode(); if (parent_node.getNodeType() == Node.ELEMENT_NODE) { // We're editing a text node which parent is an element node. Limit // content assist to elements valid for the parent. parent = parent_node.getNodeName(); ElementDescriptor desc = getDescriptor(parent); if (desc != null) { choices = desc.getChildren(); } } else if (parent_node.getNodeType() == Node.DOCUMENT_NODE) { // We're editing a text node at the first level (i.e. root node). // Limit content assist to the only valid root elements. choices = getRootDescriptor().getChildren(); } return choices; } /** * Given a list of choices found, generates the proposals to be displayed to the user. * <p/> * Choices is an object array. Items of the array can be: * - ElementDescriptor: a possible element descriptor which XML name should be completed. * - AttributeDescriptor: a possible attribute descriptor which XML name should be completed. * - String: string values to display as-is to the user. Typically those are possible * values for a given attribute. * * @return The ICompletionProposal[] to display to the user. */ private ICompletionProposal[] computeProposals(int offset, Node currentNode, Object[] choices, String wordPrefix, char need_tag, boolean is_attribute, int selectionLength) { ArrayList<CompletionProposal> proposals = new ArrayList<CompletionProposal>(); HashMap<String, String> nsUriMap = new HashMap<String, String>(); for (Object choice : choices) { String keyword = null; String nsPrefix = null; Image icon = null; String tooltip = null; if (choice instanceof ElementDescriptor) { keyword = ((ElementDescriptor)choice).getXmlName(); icon = ((ElementDescriptor)choice).getIcon(); tooltip = DescriptorsUtils.formatTooltip(((ElementDescriptor)choice).getTooltip()); } else if (choice instanceof TextValueDescriptor) { continue; // Value nodes are not part of the completion choices } else if (choice instanceof SeparatorAttributeDescriptor) { continue; // not real attribute descriptors } else if (choice instanceof AttributeDescriptor) { keyword = ((AttributeDescriptor)choice).getXmlLocalName(); icon = ((AttributeDescriptor)choice).getIcon(); if (choice instanceof TextAttributeDescriptor) { tooltip = ((TextAttributeDescriptor) choice).getTooltip(); } // Get the namespace URI for the attribute. Note that some attributes // do not have a namespace and thus return null here. String nsUri = ((AttributeDescriptor)choice).getNamespaceUri(); if (nsUri != null) { nsPrefix = nsUriMap.get(nsUri); if (nsPrefix == null) { nsPrefix = lookupNamespacePrefix(currentNode, nsUri); nsUriMap.put(nsUri, nsPrefix); } } if (nsPrefix != null) { nsPrefix += ":"; //$NON-NLS-1$ } } else if (choice instanceof String) { keyword = (String) choice; } else { continue; // discard unknown choice } String nsKeyword = nsPrefix == null ? keyword : (nsPrefix + keyword); if (keyword.startsWith(wordPrefix) || (nsPrefix != null && keyword.startsWith(nsPrefix)) || (nsPrefix != null && nsKeyword.startsWith(wordPrefix))) { if (nsPrefix != null) { keyword = nsPrefix + keyword; } String end_tag = ""; //$NON-NLS-1$ if (need_tag != 0) { if (need_tag == '"') { keyword = need_tag + keyword; end_tag = String.valueOf(need_tag); } else if (need_tag == '<') { if (elementCanHaveChildren(choice)) { end_tag = String.format("></%1$s>", keyword); //$NON-NLS-1$ keyword = need_tag + keyword; } else { keyword = need_tag + keyword; end_tag = "/>"; //$NON-NLS-1$ } } } CompletionProposal proposal = new CompletionProposal( keyword + end_tag, // String replacementString offset - wordPrefix.length(), // int replacementOffset wordPrefix.length() + selectionLength, // int replacementLength keyword.length(), // int cursorPosition (rel. to rplcmntOffset) icon, // Image image null, // String displayString null, // IContextInformation contextInformation tooltip // String additionalProposalInfo ); proposals.add(proposal); } } return proposals.toArray(new ICompletionProposal[proposals.size()]); } /** * Indicates whether this descriptor describes an element that can potentially * have children (either sub-elements or text value). If an element can have children, * we want to explicitly write an opening and a separate closing tag. * <p/> * Elements can have children if the descriptor has children element descriptors * or if one of the attributes is a TextValueDescriptor. * * @param descriptor An ElementDescriptor or an AttributeDescriptor * @return True if the descriptor is an ElementDescriptor that can have children or a text value */ private boolean elementCanHaveChildren(Object descriptor) { if (descriptor instanceof ElementDescriptor) { ElementDescriptor desc = (ElementDescriptor) descriptor; if (desc.hasChildren()) { return true; } for (AttributeDescriptor attr_desc : desc.getAttributes()) { if (attr_desc instanceof TextValueDescriptor) { return true; } } } return false; } /** * Returns the element descriptor matching a given XML node name or null if it can't be * found. * <p/> * This is simplistic; ideally we should consider the parent's chain to make sure we * can differentiate between different hierarchy trees. Right now the first match found * is returned. */ private ElementDescriptor getDescriptor(String nodeName) { return getRootDescriptor().findChildrenDescriptor(nodeName, true /* recursive */); } public IContextInformation[] computeContextInformation(ITextViewer viewer, int offset) { return null; } /** * Returns the characters which when entered by the user should * automatically trigger the presentation of possible completions. * * In our case, we auto-activate on opening tags and attributes namespace. * * @return the auto activation characters for completion proposal or <code>null</code> * if no auto activation is desired */ public char[] getCompletionProposalAutoActivationCharacters() { return new char[]{ '<', ':', '=' }; } public char[] getContextInformationAutoActivationCharacters() { return null; } public IContextInformationValidator getContextInformationValidator() { return null; } public String getErrorMessage() { return null; } /** * Heuristically extracts the prefix used for determining template relevance * from the viewer's document. The default implementation returns the String from * offset backwards that forms a potential XML element name, attribute name or * attribute value. * * The part were we access the docment was extracted from * org.eclipse.jface.text.templatesTemplateCompletionProcessor and adapted to our needs. * * @param viewer the viewer * @param offset offset into document * @return the prefix to consider */ protected String extractElementPrefix(ITextViewer viewer, int offset) { int i = offset; IDocument document = viewer.getDocument(); if (i > document.getLength()) return ""; //$NON-NLS-1$ try { for (; i > 0; --i) { char ch = document.getChar(i - 1); // We want all characters that can form a valid: // - element name, e.g. anything that is a valid Java class/variable literal. // - attribute name, including : for the namespace // - attribute value. // Before we were inclusive and that made the code fragile. So now we're // going to be exclusive: take everything till we get one of: // - any form of whitespace // - any xml separator, e.g. < > ' " and = if (Character.isWhitespace(ch) || ch == '<' || ch == '>' || ch == '\'' || ch == '"' || ch == '=') { break; } } return document.get(i, offset - i); } catch (BadLocationException e) { return ""; //$NON-NLS-1$ } } /** * Extracts the character at the given offset. * Returns 0 if the offset is invalid. */ protected char extractChar(ITextViewer viewer, int offset) { IDocument document = viewer.getDocument(); if (offset > document.getLength()) return 0; try { return document.getChar(offset); } catch (BadLocationException e) { return 0; } } /** * Information about the current edit of an attribute as reported by parseAttributeInfo. */ private class AttribInfo { /** True if the cursor is located in an attribute's value, false if in an attribute name */ public boolean isInValue = false; /** The attribute name. Null when not set. */ public String name = null; /** The attribute value. Null when not set. The value *may* start with a quote * (' or "), in which case we know we don't need to quote the string for the user */ public String value = null; /** String typed by the user so far (i.e. right before requesting code completion), * which will be corrected if we find a possible completion for an attribute value. * See the long comment in getChoicesForAttribute(). */ public String correctedPrefix = null; /** Non-zero if an attribute value need a start/end tag (i.e. quotes or brackets) */ public char needTag = 0; } /** * Try to guess if the cursor is editing an element's name or an attribute following an * element. If it's an attribute, try to find if an attribute name is being defined or * its value. * <br/> * This is currently *only* called when we know the cursor is after a complete element * tag name, so it should never return null. * <br/> * Reference for XML syntax: http://www.w3.org/TR/2006/REC-xml-20060816/#sec-starttags * <br/> * @return An AttribInfo describing which attribute is being edited or null if the cursor is * not editing an attribute (in which case it must be an element's name). */ private AttribInfo parseAttributeInfo(ITextViewer viewer, int offset) { AttribInfo info = new AttribInfo(); IDocument document = viewer.getDocument(); int n = document.getLength(); if (offset <= n) { try { n = offset; for (;offset > 0; --offset) { char ch = document.getChar(offset - 1); if (ch == '<') break; } // text will contain the full string of the current element, // i.e. whatever is after the "<" to the current cursor String text = document.get(offset, n - offset); // Normalize whitespace to single spaces text = sWhitespace.matcher(text).replaceAll(" "); //$NON-NLS-1$ // Remove the leading element name. By spec, it must be after the < without // any whitespace. If there's nothing left, no attribute has been defined yet. // Be sure to keep any whitespace after the initial word if any, as it matters. text = sFirstElementWord.matcher(text).replaceFirst(""); //$NON-NLS-1$ // There MUST be space after the element name. If not, the cursor is still // defining the element name. if (!text.startsWith(" ")) { //$NON-NLS-1$ return null; } // Remove full attributes: // Syntax: // name = "..." quoted string with all but < and " // or: // name = '...' quoted string with all but < and ' String temp; do { temp = text; text = sFirstAttribute.matcher(temp).replaceFirst(""); //$NON-NLS-1$ } while(!temp.equals(text)); // Now we're left with 3 cases: // - nothing: either there is no attribute definition or the cursor located after // a completed attribute definition. // - a string with no =: the user is writing an attribute name. This case can be // merged with the previous one. // - string with an = sign, optionally followed by a quote (' or "): the user is // writing the value of the attribute. int pos_equal = text.indexOf('='); if (pos_equal == -1) { info.isInValue = false; info.name = text.trim(); } else { info.isInValue = true; info.name = text.substring(0, pos_equal).trim(); info.value = text.substring(pos_equal + 1).trim(); } return info; } catch (BadLocationException e) { // pass } } return null; } /** * Returns the XML DOM node corresponding to the given offset of the given document. */ protected Node getNode(ITextViewer viewer, int offset) { Node node = null; try { IModelManager mm = StructuredModelManager.getModelManager(); if (mm != null) { IStructuredModel model = mm.getExistingModelForRead(viewer.getDocument()); if (model != null) { for(; offset >= 0 && node == null; --offset) { node = (Node) model.getIndexedRegion(offset); } } } } catch (Exception e) { // Ignore exceptions. } return node; } /** * Computes (if needed) and returns the root descriptor. */ private ElementDescriptor getRootDescriptor() { if (mRootDescriptor == null) { AndroidTargetData data = mEditor.getTargetData(); if (data != null) { IDescriptorProvider descriptorProvider = data.getDescriptorProvider(mDescriptorId); if (descriptorProvider != null) { mRootDescriptor = new ElementDescriptor("", descriptorProvider.getRootElementDescriptors()); } } } return mRootDescriptor; } /** * Returns the active {@link AndroidEditor} matching this source viewer. */ private AndroidEditor getAndroidEditor(ITextViewer viewer) { IWorkbenchWindow wwin = PlatformUI.getWorkbench().getActiveWorkbenchWindow(); if (wwin != null) { IWorkbenchPage page = wwin.getActivePage(); if (page != null) { IEditorPart editor = page.getActiveEditor(); if (editor instanceof AndroidEditor) { ISourceViewer ssviewer = ((AndroidEditor) editor).getStructuredSourceViewer(); if (ssviewer == viewer) { return (AndroidEditor) editor; } } } } return null; } }