| package org.jetbrains.android.dom; |
| |
| import com.android.SdkConstants; |
| import com.android.ide.common.resources.ResourceUrl; |
| import com.android.resources.ResourceType; |
| import com.android.tools.idea.javadoc.AndroidJavaDocRenderer; |
| import com.android.utils.Pair; |
| import com.intellij.lang.documentation.DocumentationProvider; |
| import com.intellij.openapi.module.Module; |
| import com.intellij.openapi.module.ModuleUtilCore; |
| import com.intellij.openapi.util.Key; |
| import com.intellij.openapi.util.Ref; |
| import com.intellij.openapi.vfs.VirtualFile; |
| import com.intellij.pom.PomTarget; |
| import com.intellij.pom.PomTargetPsiElement; |
| import com.intellij.psi.PsiElement; |
| import com.intellij.psi.PsiManager; |
| import com.intellij.psi.impl.FakePsiElement; |
| import com.intellij.psi.util.*; |
| import com.intellij.psi.xml.XmlAttribute; |
| import com.intellij.psi.xml.XmlAttributeValue; |
| import com.intellij.psi.xml.XmlTag; |
| import com.intellij.psi.xml.XmlToken; |
| import com.intellij.reference.SoftReference; |
| import com.intellij.util.xml.*; |
| import com.intellij.util.xml.reflect.DomAttributeChildDescription; |
| import com.intellij.util.xml.reflect.DomExtension; |
| import org.jetbrains.android.dom.attrs.AttributeDefinition; |
| import org.jetbrains.android.dom.attrs.AttributeDefinitions; |
| import org.jetbrains.android.dom.attrs.AttributeFormat; |
| import org.jetbrains.android.dom.converters.AttributeValueDocumentationProvider; |
| import org.jetbrains.android.dom.wrappers.LazyValueResourceElementWrapper; |
| import org.jetbrains.android.facet.AndroidFacet; |
| import org.jetbrains.android.resourceManagers.ResourceManager; |
| import org.jetbrains.android.resourceManagers.SystemResourceManager; |
| import org.jetbrains.android.resourceManagers.ValueResourceInfo; |
| import org.jetbrains.android.util.AndroidUtils; |
| import org.jetbrains.annotations.NotNull; |
| import org.jetbrains.annotations.Nullable; |
| |
| import java.util.*; |
| |
| import static com.android.SdkConstants.*; |
| import static com.intellij.psi.xml.XmlTokenType.*; |
| |
| /** |
| * @author Eugene.Kudelevsky |
| */ |
| public class AndroidXmlDocumentationProvider implements DocumentationProvider { |
| private static final Key<SoftReference<Map<XmlName, CachedValue<String>>>> ANDROID_ATTRIBUTE_DOCUMENTATION_CACHE_KEY = |
| Key.create("ANDROID_ATTRIBUTE_DOCUMENTATION_CACHE"); |
| |
| @Override |
| public String getQuickNavigateInfo(PsiElement element, PsiElement originalElement) { |
| if (element instanceof LazyValueResourceElementWrapper) { |
| final ValueResourceInfo info = ((LazyValueResourceElementWrapper)element).getResourceInfo(); |
| return "value resource '" + info.getName() + "' [" + info.getContainingFile().getName() + "]"; |
| } |
| return null; |
| } |
| |
| @Override |
| public List<String> getUrlFor(PsiElement element, PsiElement originalElement) { |
| return null; |
| } |
| |
| @Override |
| public String generateDoc(PsiElement element, @Nullable PsiElement originalElement) { |
| if (element instanceof LazyValueResourceElementWrapper) { |
| LazyValueResourceElementWrapper wrapper = (LazyValueResourceElementWrapper)element; |
| ValueResourceInfo resourceInfo = wrapper.getResourceInfo(); |
| ResourceType type = resourceInfo.getType(); |
| String name = resourceInfo.getName(); |
| |
| Module module = ModuleUtilCore.findModuleForPsiElement(element); |
| if (module == null) { |
| return null; |
| } |
| AndroidFacet facet = AndroidFacet.getInstance(element); |
| if (facet == null) { |
| return null; |
| } |
| |
| ResourceUrl url; |
| ResourceUrl originalUrl = originalElement != null ? ResourceUrl.parse(originalElement.getText()) : null; |
| if (originalUrl != null && name.equals(originalUrl.name)) { |
| url = originalUrl; |
| } else { |
| boolean isFramework = false; |
| if (originalUrl != null) { |
| isFramework = originalUrl.framework; |
| } else { |
| // Figure out if this resource is a framework file. |
| // We really should store that info in the ValueResourceInfo instances themselves. |
| // For now, attempt to figure it out |
| SystemResourceManager systemResourceManager = facet.getSystemResourceManager(); |
| VirtualFile containingFile = resourceInfo.getContainingFile(); |
| if (systemResourceManager != null) { |
| VirtualFile parent = containingFile.getParent(); |
| if (parent != null) { |
| VirtualFile resDir = parent.getParent(); |
| if (resDir != null) { |
| isFramework = systemResourceManager.isResourceDir(resDir); |
| } |
| } |
| } |
| } |
| |
| url = ResourceUrl.create(type, name, isFramework, false); |
| } |
| return generateDoc(element, url); |
| } else if (element instanceof MyResourceElement) { |
| return getResourceDocumentation(element, ((MyResourceElement)element).myResource); |
| } else if (element instanceof XmlAttributeValue) { |
| return getResourceDocumentation(element, ((XmlAttributeValue)element).getValue()); |
| } |
| if (originalElement instanceof XmlToken) { |
| XmlToken token = (XmlToken)originalElement; |
| if (token.getTokenType() == XML_ATTRIBUTE_VALUE_START_DELIMITER) { |
| PsiElement next = token.getNextSibling(); |
| if (next instanceof XmlToken) { |
| token = (XmlToken)next; |
| } |
| } else if (token.getTokenType() == XML_ATTRIBUTE_VALUE_END_DELIMITER) { |
| PsiElement prev = token.getPrevSibling(); |
| if (prev instanceof XmlToken) { |
| token = (XmlToken)prev; |
| } |
| } |
| if (token.getTokenType() == XML_ATTRIBUTE_VALUE_TOKEN) { |
| String documentation = getResourceDocumentation(originalElement, token.getText()); |
| if (documentation != null) { |
| return documentation; |
| } |
| } else if (token.getTokenType() == XML_DATA_CHARACTERS) { |
| String text = token.getText().trim(); |
| String documentation = getResourceDocumentation(originalElement, text); |
| if (documentation != null) { |
| return documentation; |
| } |
| } |
| } |
| |
| if (element instanceof PomTargetPsiElement && originalElement != null) { |
| final PomTarget target = ((PomTargetPsiElement)element).getTarget(); |
| |
| if (target instanceof DomAttributeChildDescription) { |
| synchronized (ANDROID_ATTRIBUTE_DOCUMENTATION_CACHE_KEY) { |
| return generateDocForXmlAttribute((DomAttributeChildDescription)target, originalElement); |
| } |
| } |
| } |
| |
| if (element instanceof MyDocElement) { |
| return ((MyDocElement)element).myDocumentation; |
| } |
| return null; |
| } |
| |
| @Nullable |
| private static String getResourceDocumentation(PsiElement element, String value) { |
| ResourceUrl url = ResourceUrl.parse(value); |
| if (url != null) { |
| return generateDoc(element, url); |
| } else { |
| // See if it's in a resource file definition: This allows you to invoke |
| // documentation on <string name="cursor_here">...</string> |
| // and see the various translations etc of the string |
| XmlAttribute attribute = PsiTreeUtil.getParentOfType(element, XmlAttribute.class, false); |
| if (attribute != null && ATTR_NAME.equals(attribute.getName())) { |
| XmlTag tag = attribute.getParent(); |
| String typeName = tag.getName(); |
| if (TAG_ITEM.equals(typeName)) { |
| typeName = tag.getAttributeValue(ATTR_TYPE); |
| if (typeName == null) { |
| return null; |
| } |
| } |
| ResourceType type = ResourceType.getEnum(typeName); |
| if (type != null) { |
| return generateDoc(element, type, value, false); |
| } |
| } |
| } |
| return null; |
| } |
| |
| @Nullable |
| private static String generateDocForXmlAttribute(@NotNull DomAttributeChildDescription description, @NotNull final PsiElement originalElement) { |
| final XmlName xmlName = description.getXmlName(); |
| |
| Map<XmlName, CachedValue<String>> cachedDocsMap = SoftReference.dereference( |
| originalElement.getUserData(ANDROID_ATTRIBUTE_DOCUMENTATION_CACHE_KEY)); |
| |
| if (cachedDocsMap != null) { |
| final CachedValue<String> cachedDoc = cachedDocsMap.get(xmlName); |
| |
| if (cachedDoc != null) { |
| return cachedDoc.getValue(); |
| } |
| } |
| final AndroidFacet facet = AndroidFacet.getInstance(originalElement); |
| |
| if (facet == null) { |
| return null; |
| } |
| final String localName = xmlName.getLocalName(); |
| String namespace = xmlName.getNamespaceKey(); |
| |
| if (namespace == null) { |
| return null; |
| } |
| if (AndroidUtils.NAMESPACE_KEY.equals(namespace)) { |
| namespace = ANDROID_URI; |
| } |
| |
| if (namespace.startsWith(URI_PREFIX)) { |
| final String finalNamespace = namespace; |
| |
| final CachedValue<String> cachedValue = CachedValuesManager.getManager(originalElement.getProject()).createCachedValue( |
| new CachedValueProvider<String>() { |
| @Nullable |
| @Override |
| public Result<String> compute() { |
| final Pair<AttributeDefinition, String> pair = findAttributeDefinition(originalElement, facet, finalNamespace, localName); |
| final String doc = pair != null ? generateDocForXmlAttribute(pair.getFirst(), pair.getSecond()) : null; |
| return Result.create(doc, PsiModificationTracker.MODIFICATION_COUNT); |
| } |
| }, false); |
| if (cachedDocsMap == null) { |
| cachedDocsMap = new HashMap<XmlName, CachedValue<String>>(); |
| originalElement.putUserData(ANDROID_ATTRIBUTE_DOCUMENTATION_CACHE_KEY, |
| new SoftReference<Map<XmlName, CachedValue<String>>>(cachedDocsMap)); |
| } |
| cachedDocsMap.put(xmlName, cachedValue); |
| return cachedValue.getValue(); |
| } |
| return null; |
| } |
| |
| @Nullable |
| private static Pair<AttributeDefinition, String> findAttributeDefinition(@NotNull PsiElement originalElement, |
| @NotNull AndroidFacet facet, |
| @NotNull final String namespace, |
| @NotNull final String localName) { |
| if (!originalElement.isValid()) { |
| return null; |
| } |
| final XmlTag parentTag = PsiTreeUtil.getParentOfType(originalElement, XmlTag.class); |
| |
| if (parentTag == null) { |
| return null; |
| } |
| final DomElement parentDomElement = DomManager.getDomManager(parentTag.getProject()).getDomElement(parentTag); |
| |
| if (!(parentDomElement instanceof AndroidDomElement)) { |
| return null; |
| } |
| final Ref<Pair<AttributeDefinition, String>> result = Ref.create(); |
| AndroidDomExtender.processAttrsAndSubtags((AndroidDomElement)parentDomElement, new AndroidDomExtender.MyCallback() { |
| |
| @Nullable |
| @Override |
| DomExtension processAttribute(@NotNull XmlName xn, @NotNull AttributeDefinition attrDef, @Nullable String parentStyleableName) { |
| if (xn.getLocalName().equals(localName) && |
| namespace.equals(xn.getNamespaceKey())) { |
| result.set(Pair.of(attrDef, parentStyleableName)); |
| stop(); |
| } |
| return null; |
| } |
| }, facet, false, true); |
| |
| final Pair<AttributeDefinition, String> pair = result.get(); |
| |
| if (pair != null) { |
| return pair; |
| } |
| final AttributeDefinition attrDef = findAttributeDefinitionGlobally(facet, namespace, localName); |
| return attrDef != null ? Pair.of(attrDef, (String)null) : null; |
| } |
| |
| @Nullable |
| private static AttributeDefinition findAttributeDefinitionGlobally(@NotNull AndroidFacet facet, |
| @NotNull String namespace, |
| @NotNull String localName) { |
| ResourceManager resourceManager; |
| if (ANDROID_URI.equals(namespace) || TOOLS_URI.equals(namespace)) { |
| resourceManager = facet.getSystemResourceManager(); |
| } |
| else if (namespace.equals(AUTO_URI) || namespace.startsWith(URI_PREFIX)) { |
| resourceManager = facet.getLocalResourceManager(); |
| } |
| else { |
| resourceManager = facet.getSystemResourceManager(); |
| } |
| |
| if (resourceManager != null) { |
| final AttributeDefinitions attrDefs = resourceManager.getAttributeDefinitions(); |
| |
| if (attrDefs != null) { |
| return attrDefs.getAttrDefByName(localName); |
| } |
| } |
| return null; |
| } |
| |
| private static String generateDocForXmlAttribute(@NotNull AttributeDefinition definition, @Nullable String parentStyleable) { |
| final StringBuilder builder = new StringBuilder("<html><body>"); |
| final Set<AttributeFormat> formats = definition.getFormats(); |
| |
| if (formats.size() > 0) { |
| builder.append("Formats: "); |
| final List<String> formatLabels = new ArrayList<String>(formats.size()); |
| |
| for (AttributeFormat format : formats) { |
| formatLabels.add(format.name().toLowerCase()); |
| } |
| Collections.sort(formatLabels); |
| |
| for (int i = 0, n = formatLabels.size(); i < n; i++) { |
| builder.append(formatLabels.get(i)); |
| |
| if (i < n - 1) { |
| builder.append(", "); |
| } |
| } |
| } |
| final String[] values = definition.getValues(); |
| |
| if (values.length > 0) { |
| if (builder.length() > 0) { |
| builder.append("<br>"); |
| } |
| builder.append("Values: "); |
| final String[] sortedValues = new String[values.length]; |
| System.arraycopy(values, 0, sortedValues, 0, values.length); |
| Arrays.sort(sortedValues); |
| |
| for (int i = 0; i < sortedValues.length; i++) { |
| builder.append(sortedValues[i]); |
| |
| if (i < sortedValues.length - 1) { |
| builder.append(", "); |
| } |
| } |
| } |
| final String docValue = definition.getDocValue(parentStyleable); |
| |
| if (docValue != null && docValue.length() > 0) { |
| if (builder.length() > 0) { |
| builder.append("<br><br>"); |
| } |
| builder.append(docValue); |
| } |
| builder.append("</body></html>"); |
| return builder.toString(); |
| } |
| |
| @Nullable |
| private static String generateDoc(PsiElement originalElement, ResourceType type, String name, boolean framework) { |
| Module module = ModuleUtilCore.findModuleForPsiElement(originalElement); |
| if (module == null) { |
| return null; |
| } |
| |
| return AndroidJavaDocRenderer.render(module, type, name, framework); |
| } |
| |
| @Nullable |
| private static String generateDoc(PsiElement originalElement, ResourceUrl url) { |
| Module module = ModuleUtilCore.findModuleForPsiElement(originalElement); |
| if (module == null) { |
| return null; |
| } |
| |
| return AndroidJavaDocRenderer.render(module, url); |
| } |
| |
| @Override |
| public PsiElement getDocumentationElementForLookupItem(PsiManager psiManager, Object object, PsiElement element) { |
| if (!(element instanceof XmlAttributeValue) || !(object instanceof String)) { |
| return null; |
| } |
| final String value = (String)object; |
| final PsiElement parent = element.getParent(); |
| |
| if (!(parent instanceof XmlAttribute)) { |
| return null; |
| } |
| final GenericAttributeValue domValue = DomManager.getDomManager( |
| parent.getProject()).getDomElement((XmlAttribute)parent); |
| |
| if (domValue == null) { |
| return null; |
| } |
| final Converter converter = domValue.getConverter(); |
| |
| if (converter instanceof AttributeValueDocumentationProvider) { |
| final String doc = ((AttributeValueDocumentationProvider)converter).getDocumentation(value); |
| |
| if (doc != null) { |
| return new MyDocElement(element, doc); |
| } |
| } |
| |
| if ((value.startsWith(PREFIX_RESOURCE_REF) || value.startsWith(PREFIX_THEME_REF)) && !value.startsWith(PREFIX_BINDING_EXPR)) { |
| return new MyResourceElement(element, value); |
| } |
| |
| return null; |
| } |
| |
| @Override |
| public PsiElement getDocumentationElementForLink(PsiManager psiManager, String link, PsiElement context) { |
| return null; |
| } |
| |
| private static class MyDocElement extends FakePsiElement { |
| final PsiElement myParent; |
| final String myDocumentation; |
| |
| private MyDocElement(@NotNull PsiElement parent, @NotNull String documentation) { |
| myParent = parent; |
| myDocumentation = documentation; |
| } |
| |
| @Override |
| public PsiElement getParent() { |
| return myParent; |
| } |
| } |
| |
| private static class MyResourceElement extends FakePsiElement { |
| final PsiElement myParent; |
| final String myResource; |
| |
| private MyResourceElement(@NotNull PsiElement parent, @NotNull String resource) { |
| myParent = parent; |
| myResource = resource; |
| } |
| |
| @Override |
| public PsiElement getParent() { |
| return myParent; |
| } |
| } |
| } |