| /* |
| * 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 static com.android.SdkConstants.ANDROID_URI; |
| import static com.android.SdkConstants.ATTR_LAYOUT_RESOURCE_PREFIX; |
| import static com.android.SdkConstants.PREFIX_ANDROID; |
| import static com.android.SdkConstants.PREFIX_RESOURCE_REF; |
| import static com.android.SdkConstants.PREFIX_THEME_REF; |
| import static com.android.SdkConstants.UNIT_DP; |
| import static com.android.SdkConstants.UNIT_IN; |
| import static com.android.SdkConstants.UNIT_MM; |
| import static com.android.SdkConstants.UNIT_PT; |
| import static com.android.SdkConstants.UNIT_PX; |
| import static com.android.SdkConstants.UNIT_SP; |
| import static com.android.ide.eclipse.adt.internal.editors.descriptors.AttributeDescriptor.ATTRIBUTE_ICON_FILENAME; |
| |
| import com.android.ide.common.api.IAttributeInfo; |
| import com.android.ide.common.api.IAttributeInfo.Format; |
| 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.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.TextValueDescriptor; |
| import com.android.ide.eclipse.adt.internal.editors.layout.gle2.DomUtilities; |
| 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.editors.uimodel.UiResourceAttributeNode; |
| import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData; |
| import com.android.utils.Pair; |
| import com.android.utils.XmlUtils; |
| |
| import org.eclipse.core.runtime.IStatus; |
| import org.eclipse.jdt.core.IType; |
| import org.eclipse.jdt.ui.ISharedImages; |
| import org.eclipse.jdt.ui.JavaUI; |
| import org.eclipse.jface.text.BadLocationException; |
| import org.eclipse.jface.text.IDocument; |
| import org.eclipse.jface.text.IRegion; |
| import org.eclipse.jface.text.ITextViewer; |
| 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.swt.graphics.Image; |
| import org.eclipse.wst.sse.core.internal.provisional.IndexedRegion; |
| import org.w3c.dom.Node; |
| |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.Comparator; |
| import java.util.EnumSet; |
| import java.util.HashMap; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.regex.Pattern; |
| |
| /** |
| * Content Assist Processor for Android XML files |
| * <p> |
| * Remaining corner cases: |
| * <ul> |
| * <li>Completion does not work right if there is a space between the = and the opening |
| * quote. |
| * <li>Replacement completion does not work right if the caret is to the left of the |
| * opening quote, where the opening quote is a single quote, and the replacement items use |
| * double quotes. |
| * </ul> |
| */ |
| @SuppressWarnings("restriction") // XML model |
| 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; |
| |
| protected AndroidXmlEditor 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_OTHER_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) |
| */ |
| @Override |
| public ICompletionProposal[] computeCompletionProposals(ITextViewer viewer, int offset) { |
| String wordPrefix = extractElementPrefix(viewer, offset); |
| |
| if (mEditor == null) { |
| mEditor = AndroidXmlEditor.fromTextViewer(viewer); |
| if (mEditor == null) { |
| // This should not happen. Duck and forget. |
| AdtPlugin.log(IStatus.ERROR, "Editor not found during completion"); |
| return null; |
| } |
| } |
| |
| // List of proposals, in the order presented to the user. |
| List<ICompletionProposal> proposals = new ArrayList<ICompletionProposal>(80); |
| |
| // Look up the caret context - where in an element, or between elements, or |
| // within an element's children, is the given caret offset located? |
| Pair<Node, Node> context = DomUtilities.getNodeContext(viewer.getDocument(), offset); |
| if (context == null) { |
| return null; |
| } |
| Node parentNode = context.getFirst(); |
| Node currentNode = context.getSecond(); |
| assert parentNode != null || currentNode != null; |
| |
| UiElementNode rootUiNode = mEditor.getUiRootNode(); |
| if (currentNode == null || currentNode.getNodeType() == Node.TEXT_NODE) { |
| UiElementNode parentUiNode = |
| rootUiNode == null ? null : rootUiNode.findXmlNode(parentNode); |
| computeTextValues(proposals, offset, parentNode, currentNode, parentUiNode, |
| wordPrefix); |
| } else if (currentNode.getNodeType() == Node.ELEMENT_NODE) { |
| String parent = currentNode.getNodeName(); |
| AttribInfo info = parseAttributeInfo(viewer, offset, offset - wordPrefix.length()); |
| char nextChar = extractChar(viewer, offset); |
| if (info != null) { |
| // check to see if we can find a UiElementNode matching this XML node |
| UiElementNode currentUiNode = rootUiNode == null |
| ? null : rootUiNode.findXmlNode(currentNode); |
| computeAttributeProposals(proposals, viewer, offset, wordPrefix, currentUiNode, |
| parentNode, currentNode, parent, info, nextChar); |
| } else { |
| computeNonAttributeProposals(viewer, offset, wordPrefix, proposals, parentNode, |
| currentNode, parent, nextChar); |
| } |
| } |
| |
| return proposals.toArray(new ICompletionProposal[proposals.size()]); |
| } |
| |
| private void computeNonAttributeProposals(ITextViewer viewer, int offset, String wordPrefix, |
| List<ICompletionProposal> proposals, Node parentNode, Node currentNode, String parent, |
| char nextChar) { |
| if (startsWith(parent, wordPrefix)) { |
| // We are still editing the element's tag name, not the attributes |
| // (the element's tag name may not even be complete) |
| |
| Object[] choices = getChoicesForElement(parent, currentNode); |
| if (choices == null || choices.length == 0) { |
| return; |
| } |
| |
| int replaceLength = parent.length() - wordPrefix.length(); |
| boolean isNew = replaceLength == 0 && nextNonspaceChar(viewer, offset) == '<'; |
| // Special case: if we are right before the beginning of a new |
| // element, wipe out the replace length such that we insert before it, |
| // we don't edit the current element. |
| if (wordPrefix.length() == 0 && nextChar == '<') { |
| replaceLength = 0; |
| isNew = true; |
| } |
| |
| // 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. |
| char needTag = computeElementNeedTag(viewer, offset, wordPrefix); |
| |
| addMatchingProposals(proposals, choices, offset, |
| parentNode != null ? parentNode : null, wordPrefix, needTag, |
| false /* isAttribute */, isNew, false /*isComplete*/, |
| replaceLength); |
| } |
| } |
| |
| private void computeAttributeProposals(List<ICompletionProposal> proposals, ITextViewer viewer, |
| int offset, String wordPrefix, UiElementNode currentUiNode, Node parentNode, |
| Node currentNode, String parent, AttribInfo info, char nextChar) { |
| // We're editing attributes in an element node (either the attributes' names |
| // or their values). |
| |
| if (info.isInValue) { |
| if (computeAttributeValues(proposals, offset, parent, info.name, currentNode, |
| wordPrefix, info.skipEndTag, info.replaceLength)) { |
| return; |
| } |
| } |
| |
| // Look up attribute proposals based on descriptors |
| Object[] choices = getChoicesForAttribute(parent, currentNode, currentUiNode, |
| info, wordPrefix); |
| if (choices == null || choices.length == 0) { |
| return; |
| } |
| |
| int replaceLength = info.replaceLength; |
| if (info.correctedPrefix != null) { |
| wordPrefix = info.correctedPrefix; |
| } |
| char needTag = info.needTag; |
| // Look to the right and see if we're followed by whitespace |
| boolean isNew = replaceLength == 0 |
| && (Character.isWhitespace(nextChar) || nextChar == '>' || nextChar == '/'); |
| |
| addMatchingProposals(proposals, choices, offset, parentNode != null ? parentNode : null, |
| wordPrefix, needTag, true /* isAttribute */, isNew, info.skipEndTag, |
| replaceLength); |
| } |
| |
| private char computeElementNeedTag(ITextViewer viewer, int offset, String wordPrefix) { |
| char needTag = 0; |
| int offset2 = offset - wordPrefix.length() - 1; |
| char c1 = extractChar(viewer, offset2); |
| if (!((c1 == '<') || (c1 == '/' && extractChar(viewer, offset2 - 1) == '<'))) { |
| needTag = '<'; |
| } |
| return needTag; |
| } |
| |
| protected int computeTextReplaceLength(Node currentNode, int offset) { |
| if (currentNode == null) { |
| return 0; |
| } |
| |
| assert currentNode != null && currentNode.getNodeType() == Node.TEXT_NODE; |
| |
| String nodeValue = currentNode.getNodeValue(); |
| int relativeOffset = offset - ((IndexedRegion) currentNode).getStartOffset(); |
| int lineEnd = nodeValue.indexOf('\n', relativeOffset); |
| if (lineEnd == -1) { |
| lineEnd = nodeValue.length(); |
| } |
| return lineEnd - relativeOffset; |
| } |
| |
| /** |
| * 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. |
| */ |
| protected Object[] getChoicesForElement(String parent, Node currentNode) { |
| ElementDescriptor grandparent = null; |
| if (currentNode.getParentNode().getNodeType() == Node.ELEMENT_NODE) { |
| grandparent = getDescriptor(currentNode.getParentNode().getNodeName()); |
| } else if (currentNode.getParentNode().getNodeType() == Node.DOCUMENT_NODE) { |
| grandparent = getRootDescriptor(); |
| } |
| if (grandparent != null) { |
| for (ElementDescriptor e : grandparent.getChildren()) { |
| if (e.getXmlName().startsWith(parent)) { |
| return sort(grandparent.getChildren()); |
| } |
| } |
| } |
| |
| return null; |
| } |
| |
| /** Non-destructively sort a list of ElementDescriptors and return the result */ |
| protected static ElementDescriptor[] sort(ElementDescriptor[] elements) { |
| if (elements != null && elements.length > 1) { |
| // Sort alphabetically. Must make copy to not destroy original. |
| ElementDescriptor[] copy = new ElementDescriptor[elements.length]; |
| System.arraycopy(elements, 0, copy, 0, elements.length); |
| |
| Arrays.sort(copy, new Comparator<ElementDescriptor>() { |
| @Override |
| public int compare(ElementDescriptor e1, ElementDescriptor e2) { |
| return e1.getXmlLocalName().compareTo(e2.getXmlLocalName()); |
| } |
| }); |
| |
| return copy; |
| } |
| |
| return elements; |
| } |
| |
| /** |
| * 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, |
| String wordPrefix) { |
| 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.valuePrefix; |
| 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.getAllUiAttributes()) { |
| if (attrNode.getDescriptor().getXmlLocalName().equals(attrName)) { |
| currAttrNode = attrNode; |
| break; |
| } |
| } |
| |
| if (currAttrNode != null) { |
| choices = getAttributeValueChoices(currAttrNode, attrInfo, value); |
| } |
| } |
| |
| 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 parentDesc = getDescriptor(parent); |
| if (parentDesc != null) { |
| choices = parentDesc.getAttributes(); |
| } |
| } |
| } |
| return choices; |
| } |
| |
| protected Object[] getAttributeValueChoices(UiAttributeNode currAttrNode, AttribInfo attrInfo, |
| String value) { |
| Object[] choices; |
| int pos; |
| choices = currAttrNode.getPossibleValues(value); |
| if (choices != null && currAttrNode instanceof UiResourceAttributeNode) { |
| attrInfo.skipEndTag = false; |
| } |
| |
| 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.lastIndexOf('|'); |
| if (pos >= 0) { |
| attrInfo.correctedPrefix = value = value.substring(pos + 1); |
| attrInfo.needTag = 0; |
| } |
| |
| attrInfo.skipEndTag = false; |
| } |
| |
| // Should we do suffix completion on dimension units etc? |
| choices = completeSuffix(choices, value, currAttrNode); |
| |
| // Check to see if the user is attempting resource completion |
| AttributeDescriptor attributeDescriptor = currAttrNode.getDescriptor(); |
| IAttributeInfo attributeInfo = attributeDescriptor.getAttributeInfo(); |
| if (value.startsWith(PREFIX_RESOURCE_REF) |
| && !attributeInfo.getFormats().contains(Format.REFERENCE)) { |
| // Special case: If the attribute value looks like a reference to a |
| // resource, offer to complete it, since in many cases our metadata |
| // does not correctly state whether a resource value is allowed. We don't |
| // offer these for an empty completion context, but if the user has |
| // actually typed "@", in that case list resource matches. |
| // For example, for android:minHeight this makes completion on @dimen/ |
| // possible. |
| choices = UiResourceAttributeNode.computeResourceStringMatches( |
| mEditor, attributeDescriptor, value); |
| attrInfo.skipEndTag = false; |
| } else if (value.startsWith(PREFIX_THEME_REF) |
| && !attributeInfo.getFormats().contains(Format.REFERENCE)) { |
| choices = UiResourceAttributeNode.computeResourceStringMatches( |
| mEditor, attributeDescriptor, value); |
| attrInfo.skipEndTag = false; |
| } |
| |
| return choices; |
| } |
| |
| /** |
| * Compute attribute values. Return true if the complete set of values was |
| * added, so addition descriptor information should not be added. |
| */ |
| protected boolean computeAttributeValues(List<ICompletionProposal> proposals, int offset, |
| String parentTagName, String attributeName, Node node, String wordPrefix, |
| boolean skipEndTag, int replaceLength) { |
| return false; |
| } |
| |
| protected void computeTextValues(List<ICompletionProposal> proposals, int offset, |
| Node parentNode, Node currentNode, UiElementNode uiParent, |
| String wordPrefix) { |
| |
| if (parentNode != null) { |
| // Examine the parent of the text node. |
| Object[] choices = getElementChoicesForTextNode(parentNode); |
| if (choices != null && choices.length > 0) { |
| ISourceViewer viewer = mEditor.getStructuredSourceViewer(); |
| char needTag = computeElementNeedTag(viewer, offset, wordPrefix); |
| |
| int replaceLength = 0; |
| addMatchingProposals(proposals, choices, |
| offset, parentNode, wordPrefix, needTag, |
| false /* isAttribute */, |
| false /*isNew*/, |
| false /*isComplete*/, |
| replaceLength); |
| } |
| } |
| } |
| |
| /** |
| * 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. |
| */ |
| protected ElementDescriptor[] getElementChoicesForTextNode(Node parentNode) { |
| ElementDescriptor[] choices = null; |
| String parent; |
| if (parentNode.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 = parentNode.getNodeName(); |
| ElementDescriptor desc = getDescriptor(parent); |
| if (desc == null && parent.indexOf('.') != -1) { |
| // The parent is a custom view and we don't have metadata about its |
| // allowable children, so just assume any normal layout tag is |
| // legal |
| desc = mRootDescriptor; |
| } |
| |
| if (desc != null) { |
| choices = sort(desc.getChildren()); |
| } |
| } else if (parentNode.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 = sort(getRootDescriptor().getChildren()); |
| } |
| |
| return choices; |
| } |
| |
| /** |
| * Given a list of choices, adds in any that match the current prefix into the |
| * proposals list. |
| * <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. |
| * - Pair of Strings: the first value is the keyword to insert, and the second value |
| * is the tooltip/help for the value to be displayed in the documentation popup. |
| */ |
| protected void addMatchingProposals(List<ICompletionProposal> proposals, Object[] choices, |
| int offset, Node currentNode, String wordPrefix, char needTag, |
| boolean isAttribute, boolean isNew, boolean skipEndTag, int replaceLength) { |
| if (choices == null) { |
| return; |
| } |
| |
| Map<String, String> nsUriMap = new HashMap<String, String>(); |
| boolean haveLayoutParams = false; |
| |
| for (Object choice : choices) { |
| String keyword = null; |
| String nsPrefix = null; |
| String nsUri = null; |
| Image icon = null; |
| String tooltip = null; |
| if (choice instanceof ElementDescriptor) { |
| keyword = ((ElementDescriptor)choice).getXmlName(); |
| icon = ((ElementDescriptor)choice).getGenericIcon(); |
| // Tooltip computed lazily in {@link CompletionProposal} |
| } 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).getGenericIcon(); |
| // Tooltip computed lazily in {@link CompletionProposal} |
| |
| // Get the namespace URI for the attribute. Note that some attributes |
| // do not have a namespace and thus return null here. |
| nsUri = ((AttributeDescriptor)choice).getNamespaceUri(); |
| if (nsUri != null) { |
| nsPrefix = nsUriMap.get(nsUri); |
| if (nsPrefix == null) { |
| nsPrefix = XmlUtils.lookupNamespacePrefix(currentNode, nsUri, false); |
| nsUriMap.put(nsUri, nsPrefix); |
| } |
| } |
| if (nsPrefix != null) { |
| nsPrefix += ":"; //$NON-NLS-1$ |
| } |
| |
| } else if (choice instanceof String) { |
| keyword = (String) choice; |
| if (isAttribute) { |
| icon = IconFactory.getInstance().getIcon(ATTRIBUTE_ICON_FILENAME); |
| } |
| } else if (choice instanceof Pair<?, ?>) { |
| @SuppressWarnings("unchecked") |
| Pair<String, String> pair = (Pair<String, String>) choice; |
| keyword = pair.getFirst(); |
| tooltip = pair.getSecond(); |
| if (isAttribute) { |
| icon = IconFactory.getInstance().getIcon(ATTRIBUTE_ICON_FILENAME); |
| } |
| } else if (choice instanceof IType) { |
| IType type = (IType) choice; |
| keyword = type.getFullyQualifiedName(); |
| icon = JavaUI.getSharedImages().getImage(ISharedImages.IMG_OBJS_CUNIT); |
| } else { |
| continue; // discard unknown choice |
| } |
| |
| String nsKeyword = nsPrefix == null ? keyword : (nsPrefix + keyword); |
| |
| if (nameStartsWith(nsKeyword, wordPrefix, nsPrefix)) { |
| keyword = nsKeyword; |
| String endTag = ""; //$NON-NLS-1$ |
| if (needTag != 0) { |
| if (needTag == '"') { |
| keyword = needTag + keyword; |
| endTag = String.valueOf(needTag); |
| } else if (needTag == '<') { |
| if (elementCanHaveChildren(choice)) { |
| endTag = String.format("></%1$s>", keyword); //$NON-NLS-1$ |
| } else { |
| endTag = "/>"; //$NON-NLS-1$ |
| } |
| keyword = needTag + keyword + ' '; |
| } else if (needTag == ' ') { |
| keyword = needTag + keyword; |
| } |
| } else if (!isAttribute && isNew) { |
| if (elementCanHaveChildren(choice)) { |
| endTag = String.format("></%1$s>", keyword); //$NON-NLS-1$ |
| } else { |
| endTag = "/>"; //$NON-NLS-1$ |
| } |
| keyword = keyword + ' '; |
| } |
| |
| final String suffix; |
| int cursorPosition; |
| final String displayString; |
| if (choice instanceof AttributeDescriptor && isNew) { |
| // Special case for attributes: insert ="" stuff and locate caret inside "" |
| suffix = "=\"\""; //$NON-NLS-1$ |
| cursorPosition = keyword.length() + suffix.length() - 1; |
| displayString = keyword + endTag; // don't include suffix; |
| } else { |
| suffix = endTag; |
| cursorPosition = keyword.length(); |
| displayString = null; |
| } |
| |
| if (skipEndTag) { |
| assert isAttribute; |
| cursorPosition++; |
| } |
| |
| if (nsPrefix != null && |
| keyword.startsWith(ATTR_LAYOUT_RESOURCE_PREFIX, nsPrefix.length())) { |
| haveLayoutParams = true; |
| } |
| |
| // For attributes, automatically insert ns:attribute="" and place the cursor |
| // inside the quotes. |
| // Special case for attributes: insert ="" stuff and locate caret inside "" |
| proposals.add(new CompletionProposal( |
| this, |
| choice, |
| keyword + suffix, // String replacementString |
| offset - wordPrefix.length(), // int replacementOffset |
| wordPrefix.length() + replaceLength,// int replacementLength |
| cursorPosition, // cursorPosition |
| icon, // Image image |
| displayString, // displayString |
| null, // IContextInformation contextInformation |
| tooltip, // String additionalProposalInfo |
| nsPrefix, |
| nsUri |
| )); |
| } |
| } |
| |
| if (wordPrefix.length() > 0 && haveLayoutParams |
| && !wordPrefix.startsWith(ATTR_LAYOUT_RESOURCE_PREFIX)) { |
| // Sort layout parameters to the front if we automatically inserted some |
| // that you didn't request. For example, you typed "width" and we match both |
| // "width" and "layout_width" - should match layout_width. |
| String nsPrefix = nsUriMap.get(ANDROID_URI); |
| if (nsPrefix == null) { |
| nsPrefix = PREFIX_ANDROID; |
| } else { |
| nsPrefix += ':'; |
| } |
| if (!(wordPrefix.startsWith(nsPrefix) |
| && wordPrefix.startsWith(ATTR_LAYOUT_RESOURCE_PREFIX, nsPrefix.length()))) { |
| int nextLayoutIndex = 0; |
| for (int i = 0, n = proposals.size(); i < n; i++) { |
| ICompletionProposal proposal = proposals.get(i); |
| String keyword = proposal.getDisplayString(); |
| if (keyword.startsWith(nsPrefix) && |
| keyword.startsWith(ATTR_LAYOUT_RESOURCE_PREFIX, nsPrefix.length()) |
| && i != nextLayoutIndex) { |
| // Swap to front |
| ICompletionProposal temp = proposals.get(nextLayoutIndex); |
| proposals.set(nextLayoutIndex, proposal); |
| proposals.set(i, temp); |
| nextLayoutIndex++; |
| } |
| } |
| } |
| } |
| } |
| |
| /** |
| * Returns true if the given word starts with the given prefix. The comparison is not |
| * case sensitive. |
| * |
| * @param word the word to test |
| * @param prefix the prefix the word should start with |
| * @return true if the given word starts with the given prefix |
| */ |
| protected static boolean startsWith(String word, String prefix) { |
| int prefixLength = prefix.length(); |
| int wordLength = word.length(); |
| if (wordLength < prefixLength) { |
| return false; |
| } |
| |
| for (int i = 0; i < prefixLength; i++) { |
| if (Character.toLowerCase(prefix.charAt(i)) |
| != Character.toLowerCase(word.charAt(i))) { |
| return false; |
| } |
| } |
| |
| return true; |
| } |
| |
| /** @return the editor associated with this content assist */ |
| AndroidXmlEditor getEditor() { |
| return mEditor; |
| } |
| |
| /** |
| * This method performs a prefix match for the given word and prefix, with a couple of |
| * Android code completion specific twists: |
| * <ol> |
| * <li> The match is not case sensitive, so {word="fOo",prefix="FoO"} is a match. |
| * <li>If the word to be matched has a namespace prefix, the typed prefix doesn't have |
| * to match it. So {word="android:foo", prefix="foo"} is a match. |
| * <li>If the attribute name part starts with "layout_" it can be omitted. So |
| * {word="android:layout_marginTop",prefix="margin"} is a match, as is |
| * {word="android:layout_marginTop",prefix="android:margin"}. |
| * </ol> |
| * |
| * @param word the full word to be matched, including namespace if any |
| * @param prefix the prefix to check |
| * @param nsPrefix the namespace prefix (android: or local definition of android |
| * namespace prefix) |
| * @return true if the prefix matches for code completion |
| */ |
| protected static boolean nameStartsWith(String word, String prefix, String nsPrefix) { |
| if (nsPrefix == null) { |
| nsPrefix = ""; //$NON-NLS-1$ |
| } |
| |
| int wordStart = nsPrefix.length(); |
| int prefixStart = 0; |
| |
| if (startsWith(prefix, nsPrefix)) { |
| // Already matches up through the namespace prefix: |
| prefixStart = wordStart; |
| } else if (startsWith(nsPrefix, prefix)) { |
| return true; |
| } |
| |
| int prefixLength = prefix.length(); |
| int wordLength = word.length(); |
| |
| if (wordLength - wordStart < prefixLength - prefixStart) { |
| return false; |
| } |
| |
| boolean matches = true; |
| for (int i = prefixStart, j = wordStart; i < prefixLength; i++, j++) { |
| char c1 = Character.toLowerCase(prefix.charAt(i)); |
| char c2 = Character.toLowerCase(word.charAt(j)); |
| if (c1 != c2) { |
| matches = false; |
| break; |
| } |
| } |
| |
| if (!matches && word.startsWith(ATTR_LAYOUT_RESOURCE_PREFIX, wordStart) |
| && !prefix.startsWith(ATTR_LAYOUT_RESOURCE_PREFIX, prefixStart)) { |
| wordStart += ATTR_LAYOUT_RESOURCE_PREFIX.length(); |
| |
| if (wordLength - wordStart < prefixLength - prefixStart) { |
| return false; |
| } |
| |
| for (int i = prefixStart, j = wordStart; i < prefixLength; i++, j++) { |
| char c1 = Character.toLowerCase(prefix.charAt(i)); |
| char c2 = Character.toLowerCase(word.charAt(j)); |
| if (c1 != c2) { |
| return false; |
| } |
| } |
| |
| return true; |
| } |
| |
| return matches; |
| } |
| |
| /** |
| * 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 attrDesc : desc.getAttributes()) { |
| if (attrDesc 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 */); |
| } |
| |
| @Override |
| 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 |
| */ |
| @Override |
| public char[] getCompletionProposalAutoActivationCharacters() { |
| return new char[]{ '<', ':', '=' }; |
| } |
| |
| @Override |
| public char[] getContextInformationAutoActivationCharacters() { |
| return null; |
| } |
| |
| @Override |
| public IContextInformationValidator getContextInformationValidator() { |
| return null; |
| } |
| |
| @Override |
| 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 document 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; |
| } |
| } |
| |
| /** |
| * Search forward and find the first non-space character and return it. Returns 0 if no |
| * such character was found. |
| */ |
| private char nextNonspaceChar(ITextViewer viewer, int offset) { |
| IDocument document = viewer.getDocument(); |
| int length = document.getLength(); |
| for (; offset < length; offset++) { |
| try { |
| char c = document.getChar(offset); |
| if (!Character.isWhitespace(c)) { |
| return c; |
| } |
| } catch (BadLocationException e) { |
| return 0; |
| } |
| } |
| |
| return 0; |
| } |
| |
| /** |
| * Information about the current edit of an attribute as reported by parseAttributeInfo. |
| */ |
| protected static class AttribInfo { |
| public 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 top the left of the cursor. 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 valuePrefix = 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; |
| /** Number of characters to replace after the prefix */ |
| public int replaceLength = 0; |
| /** Should the cursor advance through the end tag when inserted? */ |
| public boolean skipEndTag = false; |
| } |
| |
| /** |
| * 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, int prefixStartOffset) { |
| AttribInfo info = new AttribInfo(); |
| int originalOffset = offset; |
| |
| IDocument document = viewer.getDocument(); |
| int n = document.getLength(); |
| if (offset <= n) { |
| try { |
| // Look to the right to make sure we aren't sitting on the boundary of the |
| // beginning of a new element with whitespace before it |
| if (offset < n && document.getChar(offset) == '<') { |
| return null; |
| } |
| |
| n = offset; |
| for (;offset > 0; --offset) { |
| char ch = document.getChar(offset - 1); |
| if (ch == '>') break; |
| 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)); |
| |
| IRegion lineInfo = document.getLineInformationOfOffset(originalOffset); |
| int lineStart = lineInfo.getOffset(); |
| String line = document.get(lineStart, lineInfo.getLength()); |
| int cursorColumn = originalOffset - lineStart; |
| int prefixLength = originalOffset - prefixStartOffset; |
| |
| // 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 posEqual = text.indexOf('='); |
| if (posEqual == -1) { |
| info.isInValue = false; |
| info.name = text.trim(); |
| |
| // info.name is currently just the prefix of the attribute name. |
| // Look at the text buffer to find the complete name (since we need |
| // to know its bounds in order to replace it when a different attribute |
| // that matches this prefix is chosen) |
| int nameStart = cursorColumn; |
| for (int nameEnd = nameStart; nameEnd < line.length(); nameEnd++) { |
| char c = line.charAt(nameEnd); |
| if (!(Character.isLetter(c) || c == ':' || c == '_')) { |
| String nameSuffix = line.substring(nameStart, nameEnd); |
| info.name = text.trim() + nameSuffix; |
| break; |
| } |
| } |
| |
| info.replaceLength = info.name.length() - prefixLength; |
| |
| if (info.name.length() == 0 && originalOffset > 0) { |
| // Ensure that attribute names are properly separated |
| char prevChar = extractChar(viewer, originalOffset - 1); |
| if (prevChar == '"' || prevChar == '\'') { |
| // Ensure that the attribute is properly separated from the |
| // previous element |
| info.needTag = ' '; |
| } |
| } |
| info.skipEndTag = false; |
| } else { |
| info.isInValue = true; |
| info.name = text.substring(0, posEqual).trim(); |
| info.valuePrefix = text.substring(posEqual + 1); |
| |
| char quoteChar = '"'; // Does " or ' surround the XML value? |
| for (int i = posEqual + 1; i < text.length(); i++) { |
| if (!Character.isWhitespace(text.charAt(i))) { |
| quoteChar = text.charAt(i); |
| break; |
| } |
| } |
| |
| // Must compute the complete value |
| int valueStart = cursorColumn; |
| int valueEnd = valueStart; |
| for (; valueEnd < line.length(); valueEnd++) { |
| char c = line.charAt(valueEnd); |
| if (c == quoteChar) { |
| // Make sure this isn't the *opening* quote of the value, |
| // which is the case if we invoke code completion with the |
| // caret between the = and the opening quote; in that case |
| // we consider it value completion, and offer items including |
| // the quotes, but we shouldn't bail here thinking we have found |
| // the end of the value. |
| // Look backwards to make sure we find another " before |
| // we find a = |
| boolean isFirst = false; |
| for (int j = valueEnd - 1; j >= 0; j--) { |
| char pc = line.charAt(j); |
| if (pc == '=') { |
| isFirst = true; |
| break; |
| } else if (pc == quoteChar) { |
| valueStart = j; |
| break; |
| } |
| } |
| if (!isFirst) { |
| info.skipEndTag = true; |
| break; |
| } |
| } |
| } |
| int valueEndOffset = valueEnd + lineStart; |
| info.replaceLength = valueEndOffset - (prefixStartOffset + prefixLength); |
| // Is the caret to the left of the value quote? If so, include it in |
| // the replace length. |
| int valueStartOffset = valueStart + lineStart; |
| if (valueStartOffset == prefixStartOffset && valueEnd > valueStart) { |
| info.replaceLength++; |
| } |
| } |
| return info; |
| } catch (BadLocationException e) { |
| // pass |
| } |
| } |
| |
| return null; |
| } |
| |
| /** Returns the root descriptor id to use */ |
| protected int getRootDescriptorId() { |
| return mDescriptorId; |
| } |
| |
| /** |
| * Computes (if needed) and returns the root descriptor. |
| */ |
| protected ElementDescriptor getRootDescriptor() { |
| if (mRootDescriptor == null) { |
| AndroidTargetData data = mEditor.getTargetData(); |
| if (data != null) { |
| IDescriptorProvider descriptorProvider = |
| data.getDescriptorProvider(getRootDescriptorId()); |
| |
| if (descriptorProvider != null) { |
| mRootDescriptor = new ElementDescriptor("", //$NON-NLS-1$ |
| descriptorProvider.getRootElementDescriptors()); |
| } |
| } |
| } |
| |
| return mRootDescriptor; |
| } |
| |
| /** |
| * Fixed list of dimension units, along with user documentation, for use by |
| * {@link #completeSuffix}. |
| */ |
| private static final String[] sDimensionUnits = new String[] { |
| UNIT_DP, |
| "<b>Density-independent Pixels</b> - an abstract unit that is based on the physical " |
| + "density of the screen.", |
| |
| UNIT_SP, |
| "<b>Scale-independent Pixels</b> - this is like the dp unit, but it is also scaled by " |
| + "the user's font size preference.", |
| |
| UNIT_PT, |
| "<b>Points</b> - 1/72 of an inch based on the physical size of the screen.", |
| |
| UNIT_MM, |
| "<b>Millimeters</b> - based on the physical size of the screen.", |
| |
| UNIT_IN, |
| "<b>Inches</b> - based on the physical size of the screen.", |
| |
| UNIT_PX, |
| "<b>Pixels</b> - corresponds to actual pixels on the screen. Not recommended.", |
| }; |
| |
| /** |
| * Fixed list of fractional units, along with user documentation, for use by |
| * {@link #completeSuffix} |
| */ |
| private static final String[] sFractionUnits = new String[] { |
| "%", //$NON-NLS-1$ |
| "<b>Fraction</b> - a percentage of the base size", |
| |
| "%p", //$NON-NLS-1$ |
| "<b>Fraction</b> - a percentage relative to parent container", |
| }; |
| |
| /** |
| * Completes suffixes for applicable types (like dimensions and fractions) such that |
| * after a dimension number you get completion on unit types like "px". |
| */ |
| private Object[] completeSuffix(Object[] choices, String value, UiAttributeNode currAttrNode) { |
| IAttributeInfo attributeInfo = currAttrNode.getDescriptor().getAttributeInfo(); |
| EnumSet<Format> formats = attributeInfo.getFormats(); |
| List<Object> suffixes = new ArrayList<Object>(); |
| |
| if (value.length() > 0 && Character.isDigit(value.charAt(0))) { |
| boolean hasDimension = formats.contains(Format.DIMENSION); |
| boolean hasFraction = formats.contains(Format.FRACTION); |
| |
| if (hasDimension || hasFraction) { |
| // Split up the value into a numeric part (the prefix) and the |
| // unit part (the suffix) |
| int suffixBegin = 0; |
| for (; suffixBegin < value.length(); suffixBegin++) { |
| if (!Character.isDigit(value.charAt(suffixBegin))) { |
| break; |
| } |
| } |
| String number = value.substring(0, suffixBegin); |
| String suffix = value.substring(suffixBegin); |
| |
| // Add in the matching dimension and/or fraction units, if any |
| if (hasDimension) { |
| // Each item has two entries in the array of strings: the first odd numbered |
| // ones are the unit names and the second even numbered ones are the |
| // corresponding descriptions. |
| for (int i = 0; i < sDimensionUnits.length; i += 2) { |
| String unit = sDimensionUnits[i]; |
| if (startsWith(unit, suffix)) { |
| String description = sDimensionUnits[i + 1]; |
| suffixes.add(Pair.of(number + unit, description)); |
| } |
| } |
| |
| // Allow "dip" completion but don't offer it ("dp" is preferred) |
| if (startsWith(suffix, "di") || startsWith(suffix, "dip")) { //$NON-NLS-1$ //$NON-NLS-2$ |
| suffixes.add(Pair.of(number + "dip", "Alternative name for \"dp\"")); //$NON-NLS-1$ |
| } |
| } |
| if (hasFraction) { |
| for (int i = 0; i < sFractionUnits.length; i += 2) { |
| String unit = sFractionUnits[i]; |
| if (startsWith(unit, suffix)) { |
| String description = sFractionUnits[i + 1]; |
| suffixes.add(Pair.of(number + unit, description)); |
| } |
| } |
| } |
| } |
| } |
| |
| boolean hasFlag = formats.contains(Format.FLAG); |
| if (hasFlag) { |
| boolean isDone = false; |
| String[] flagValues = attributeInfo.getFlagValues(); |
| for (String flagValue : flagValues) { |
| if (flagValue.equals(value)) { |
| isDone = true; |
| break; |
| } |
| } |
| if (isDone) { |
| // Add in all the new values with a separator of | |
| String currentValue = currAttrNode.getCurrentValue(); |
| for (String flagValue : flagValues) { |
| if (currentValue == null || !currentValue.contains(flagValue)) { |
| suffixes.add(value + '|' + flagValue); |
| } |
| } |
| } |
| } |
| |
| if (suffixes.size() > 0) { |
| // Merge previously added choices (from attribute enums etc) with the new matches |
| List<Object> all = new ArrayList<Object>(); |
| if (choices != null) { |
| for (Object s : choices) { |
| all.add(s); |
| } |
| } |
| all.addAll(suffixes); |
| choices = all.toArray(); |
| } |
| |
| return choices; |
| } |
| } |