blob: 75d33b3e37fa6dd99f103f9bf99d3429001a7102 [file] [log] [blame]
/*
* 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.adt.internal.editors;
import com.android.ide.eclipse.adt.AdtPlugin;
import com.android.ide.eclipse.adt.internal.editors.descriptors.AttributeDescriptor;
import com.android.ide.eclipse.adt.internal.editors.descriptors.DescriptorsUtils;
import com.android.ide.eclipse.adt.internal.editors.descriptors.ElementDescriptor;
import com.android.ide.eclipse.adt.internal.editors.descriptors.IDescriptorProvider;
import com.android.ide.eclipse.adt.internal.editors.descriptors.SeparatorAttributeDescriptor;
import com.android.ide.eclipse.adt.internal.editors.descriptors.TextAttributeDescriptor;
import com.android.ide.eclipse.adt.internal.editors.descriptors.TextValueDescriptor;
import com.android.ide.eclipse.adt.internal.editors.descriptors.XmlnsAttributeDescriptor;
import com.android.ide.eclipse.adt.internal.editors.uimodel.UiAttributeNode;
import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode;
import com.android.ide.eclipse.adt.internal.editors.uimodel.UiFlagAttributeNode;
import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData;
import com.android.sdklib.SdkConstants;
import org.eclipse.core.runtime.IStatus;
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);
if (mEditor == null) {
// This should not happen. Duck and forget.
AdtPlugin.log(IStatus.ERROR, "Editor not found during completion");
return null;
}
}
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 == null) {
// Should not happen (an XML doc always has at least a doc node). Just give up.
return null;
}
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 choices 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 for 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("", //$NON-NLS-1$
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;
}
}