FileDocCategorySizeDatePackage
AndroidContentAssist.javaAPI DocAndroid 1.5 API35020Wed May 06 22:41:10 BST 2009com.android.ide.eclipse.editors

AndroidContentAssist

public abstract class AndroidContentAssist extends Object implements org.eclipse.jface.text.contentassist.IContentAssistProcessor
Content Assist Processor for Android XML files

Fields Summary
private static Pattern
sFirstAttribute
Regexp to detect a full attribute after an element tag.
Syntax:
name = "..." quoted string with all but < and "
or:
name = '...' quoted string with all but < and '
private static Pattern
sFirstElementWord
Regexp to detect an element tag name
private static Pattern
sWhitespace
Regexp to detect whitespace
protected static final String
ROOT_ELEMENT
private com.android.ide.eclipse.editors.descriptors.ElementDescriptor
mRootDescriptor
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 final int
mDescriptorId
private AndroidEditor
mEditor
Constructors Summary
public AndroidContentAssist(int descriptorId)
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.


                                                                       
       
        mDescriptorId = descriptorId;
    
Methods Summary
public org.eclipse.jface.text.contentassist.ICompletionProposal[]computeCompletionProposals(org.eclipse.jface.text.ITextViewer viewer, int offset)
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 null if no proposals are possible
see
org.eclipse.jface.text.contentassist.IContentAssistProcessor#computeCompletionProposals(org.eclipse.jface.text.ITextViewer, int)

      
        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);
    
public org.eclipse.jface.text.contentassist.IContextInformation[]computeContextInformation(org.eclipse.jface.text.ITextViewer viewer, int offset)

        return null;
    
private org.eclipse.jface.text.contentassist.ICompletionProposal[]computeProposals(int offset, org.w3c.dom.Node currentNode, java.lang.Object[] choices, java.lang.String wordPrefix, char need_tag, boolean is_attribute, int selectionLength)
Given a list of choices found, generates the proposals to be displayed to the user.

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.

        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()]);
    
private booleanelementCanHaveChildren(java.lang.Object descriptor)
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.

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

        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;
    
protected charextractChar(org.eclipse.jface.text.ITextViewer viewer, int offset)
Extracts the character at the given offset. Returns 0 if the offset is invalid.

        IDocument document = viewer.getDocument();
        if (offset > document.getLength()) return 0;

        try {
            return document.getChar(offset);
        } catch (BadLocationException e) {
            return 0;
        }
    
protected java.lang.StringextractElementPrefix(org.eclipse.jface.text.ITextViewer viewer, int offset)
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

        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$
        }
    
private AndroidEditorgetAndroidEditor(org.eclipse.jface.text.ITextViewer viewer)
Returns the active {@link AndroidEditor} matching this source 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;
    
private java.lang.Object[]getChoicesForAttribute(java.lang.String parent, org.w3c.dom.Node currentNode, com.android.ide.eclipse.editors.uimodel.UiElementNode currentUiNode, com.android.ide.eclipse.editors.AndroidContentAssist$AttribInfo attrInfo)
Gets the choices when the user is editing an XML attribute.

In input, attrInfo contains details on the analyzed context, namely whether the user is editing an attribute value (isInValue) or an attribute name.

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.

        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;
    
private java.lang.Object[]getChoicesForElement(java.lang.String parent, org.w3c.dom.Node current_node)
Gets the choices when the user is editing the name of an XML element.

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.

Example: returns the list of all elements that can be found under , of which is one of the choices.

return
an ElementDescriptor[] or null if no valid element was found.

        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;
    
private java.lang.Object[]getChoicesForTextNode(org.w3c.dom.Node currentNode)
Gets the choices when the user is editing an XML text node.

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.

        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;
    
public char[]getCompletionProposalAutoActivationCharacters()
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 null if no auto activation is desired

        return new char[]{ '<", ':", '=" };
    
public char[]getContextInformationAutoActivationCharacters()

        return null;
    
public org.eclipse.jface.text.contentassist.IContextInformationValidatorgetContextInformationValidator()

        return null;
    
private com.android.ide.eclipse.editors.descriptors.ElementDescriptorgetDescriptor(java.lang.String nodeName)
Returns the element descriptor matching a given XML node name or null if it can't be found.

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.

        return getRootDescriptor().findChildrenDescriptor(nodeName, true /* recursive */);
    
public java.lang.StringgetErrorMessage()

        return null;
    
protected org.w3c.dom.NodegetNode(org.eclipse.jface.text.ITextViewer viewer, int offset)
Returns the XML DOM node corresponding to the given offset of the given document.

        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;
    
private com.android.ide.eclipse.editors.descriptors.ElementDescriptorgetRootDescriptor()
Computes (if needed) and returns the root descriptor.

        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;
    
private java.lang.StringlookupNamespacePrefix(org.w3c.dom.Node node, java.lang.String nsUri)
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.

        // 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;
    
private com.android.ide.eclipse.editors.AndroidContentAssist$AttribInfoparseAttributeInfo(org.eclipse.jface.text.ITextViewer viewer, int offset)
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.
This is currently *only* called when we know the cursor is after a complete element tag name, so it should never return null.
Reference for XML syntax: http://www.w3.org/TR/2006/REC-xml-20060816/#sec-starttags

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).

    


                                                                                                              
          
        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;