blob: f8dbabe299e0d08c3e3f5e7070e696cfcc86e45d [file] [log] [blame]
// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package org.jetbrains.android;
import com.android.SdkConstants;
import com.google.common.annotations.VisibleForTesting;
import com.android.resources.ResourceType;
import com.android.tools.idea.AndroidPsiUtils;
import com.android.tools.idea.javadoc.AndroidJavaDocRenderer;
import com.intellij.codeInsight.javadoc.JavaDocExternalFilter;
import com.intellij.facet.ProjectFacetManager;
import com.intellij.lang.documentation.DocumentationProvider;
import com.intellij.lang.documentation.ExternalDocumentationProvider;
import com.intellij.lang.java.JavaDocumentationProvider;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.module.ModuleManager;
import com.intellij.openapi.module.ModuleUtilCore;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Computable;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.vfs.JarFileSystem;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.*;
import org.jetbrains.android.facet.AndroidFacet;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.Reader;
import java.util.List;
import java.util.Locale;
import static com.android.SdkConstants.CLASS_R;
import static com.android.tools.idea.AndroidPsiUtils.ResourceReferenceType;
/**
* @author Eugene.Kudelevsky
*/
public class AndroidDocumentationProvider implements DocumentationProvider, ExternalDocumentationProvider {
private static final Logger LOG = Logger.getInstance("#org.jetbrains.android.AndroidDocumentationProvider");
@Override
public String getQuickNavigateInfo(PsiElement element, PsiElement originalElement) {
return null;
}
@Override
public List<String> getUrlFor(PsiElement element, PsiElement originalElement) {
return null;
}
@Override
public String generateDoc(PsiElement element, @Nullable PsiElement originalElement) {
if (originalElement == null) {
return null;
}
ResourceReferenceType referenceType = AndroidPsiUtils.getResourceReferenceType(originalElement);
if (referenceType == ResourceReferenceType.NONE) {
return null;
}
Module module = ModuleUtilCore.findModuleForPsiElement(originalElement);
if (module == null) {
return null;
}
ResourceType type = AndroidPsiUtils.getResourceType(originalElement);
if (type == null) {
return null;
}
String name = AndroidPsiUtils.getResourceName(originalElement);
boolean isFrameworkResource = referenceType == ResourceReferenceType.FRAMEWORK;
return AndroidJavaDocRenderer.render(module, type, name, isFrameworkResource);
}
@Override
public PsiElement getDocumentationElementForLookupItem(@NotNull PsiManager psiManager, @NotNull Object object, @NotNull PsiElement element) {
return null;
}
@Override
public PsiElement getDocumentationElementForLink(PsiManager psiManager, String link, PsiElement context) {
return null;
}
@Override
public String fetchExternalDocumentation(final Project project, final PsiElement element, final List<String> docUrls) {
// Workaround: When you invoke completion on an android.R.type.name field in a Java class, we
// never get a chance to provide documentation for it via generateDoc, presumably because the
// field is recognized by an earlier documentation provider (the generic Java javadoc one?) as
// something we have documentation for. We do however get a chance to fetch documentation for it;
// that's this call, so in that case we insert our javadoc rendering into the fetched documentation.
String doc = ApplicationManager.getApplication().runReadAction(new Computable<String>() {
@Override
public String compute() {
if (isFrameworkFieldDeclaration(element)) {
// We don't have the original module, so just find one of the Android modules in the project.
// It's theoretically possible that this will point to a different Android version than the one
// module used by the original request.
Module module = guessAndroidModule(project, element);
PsiField field = (PsiField)element;
PsiClass containingClass = field.getContainingClass();
assert containingClass != null; // because isFrameworkFieldDeclaration returned true
ResourceType type = ResourceType.fromClassName(containingClass.getName());
if (module != null && type != null && field.getName() != null) {
String name = field.getName();
String render = AndroidJavaDocRenderer.render(module, type, name, true);
String external = JavaDocumentationProvider.fetchExternalJavadoc(element, docUrls, new MyDocExternalFilter(project));
return AndroidJavaDocRenderer.injectExternalDocumentation(render, external);
}
}
return null;
}
});
if (doc != null) return null;
return isMyContext(element, project) ?
JavaDocumentationProvider.fetchExternalJavadoc(element, docUrls, new MyDocExternalFilter(project)) :
null;
}
@Nullable
private static Module guessAndroidModule(Project project, PsiElement element) {
Module module = ModuleUtilCore.findModuleForPsiElement(element);
if (module == null) {
Module[] modules = ModuleManager.getInstance(project).getModules();
for (Module m : modules) {
if (AndroidFacet.getInstance(m) != null) {
module = m;
break;
}
}
if (module == null) {
return null;
}
}
return module;
}
private static boolean isFrameworkFieldDeclaration(PsiElement element) {
if (element instanceof PsiField) {
PsiField field = (PsiField) element;
PsiClass typeClass = field.getContainingClass();
if (typeClass != null) {
PsiClass rClass = typeClass.getContainingClass();
return rClass != null && CLASS_R.equals(AndroidPsiUtils.getQualifiedNameSafely(rClass));
}
}
return false;
}
@Override
public boolean hasDocumentationFor(PsiElement element, PsiElement originalElement) {
return false;
}
@Override
public boolean canPromptToConfigureDocumentation(PsiElement element) {
return false;
}
@Override
public void promptToConfigureDocumentation(PsiElement element) {
}
private static boolean isMyContext(@NotNull final PsiElement element, @NotNull final Project project) {
if (element instanceof PsiClass) {
return ApplicationManager.getApplication().runReadAction(new Computable<Boolean>() {
@Override
public Boolean compute() {
PsiFile file = element.getContainingFile();
if (file == null) {
return false;
}
VirtualFile vFile = file.getVirtualFile();
if (vFile == null) {
return false;
}
String path = FileUtil.toSystemIndependentName(vFile.getPath());
if (path.toLowerCase(Locale.US).contains("/" + SdkConstants.FN_FRAMEWORK_LIBRARY + "!/")) {
if (!ProjectFacetManager.getInstance(project).getFacets(AndroidFacet.ID).isEmpty()) {
VirtualFile jarFile = JarFileSystem.getInstance().getVirtualFileForJar(vFile);
return jarFile != null && SdkConstants.FN_FRAMEWORK_LIBRARY.equals(jarFile.getName());
}
}
return false;
}
});
}
return false;
}
@VisibleForTesting
static class MyDocExternalFilter extends JavaDocExternalFilter {
public MyDocExternalFilter(Project project) {
super(project);
}
@Override
protected void doBuildFromStream(String url, Reader input, StringBuilder data) throws IOException {
try {
// Looking up a method, field or constructor? If so we can use the
// builtin support -- it works.
if (ourAnchorSuffix.matcher(url).find()) {
super.doBuildFromStream(url, input, data);
return;
}
// For classes however we'll need to do our own filtering; the Android SDK
// docs are quite different from the JDK docs so IntelliJ's built in javadoc
// support doesn't work.
try (BufferedReader buf = new BufferedReader(input)) {
// Pull out the javadoc section.
// The format has changed over time, so we need to look for different formats.
// The document begins with a bunch of stuff we don't want to include (e.g.
// page navigation etc); in all formats this seems to end with the following marker:
@NonNls String startSection = "<!-- ======== START OF CLASS DATA ======== -->";
// This doesn't appear anywhere in recent documentation,
// but presumably was needed at one point; left for now
// for users who have old documentation installed locally.
@NonNls String skipHeader = "<!-- END HEADER -->";
data.append(HTML);
String read;
do {
read = buf.readLine();
}
while (read != null && !read.contains(startSection));
if (read == null) {
data.delete(0, data.length());
return;
}
data.append(read).append("\n");
// Read until we reach the class overview (if present); copy everything until we see the
// optional marker skipHeader.
boolean skip = false;
while (((read = buf.readLine()) != null) &&
// Old format: class description follows <h2>Class Overview</h2>
!read.startsWith("<h2>Class Overview") &&
// New format: class description follows just a <br><hr>. These
// are luckily not present in the older docs.
!read.equals("<br><hr>")) {
if (read.contains("<table class=")) {
// Skip all tables until the beginning of the class description
skip = true;
}
else if (read.startsWith("<h2 class=\"api-section\"")) {
// Done; we've reached the section after the class description already.
// Newer docs have no marker section or class attribute marking the
// beginning of the class doc.
read = null;
break;
}
if (!skip && !read.isEmpty()) {
data.append(read).append("\n");
}
if (read.contains(skipHeader)) {
skip = true;
}
}
// Now copy lines until the next <h2> section.
// In older versions of the docs format, this was a "<h2>", but in recent
// revisions (N+) it's <h2 class="api-section">
if (read != null) {
data.append("<br><div>\n");
while (((read = buf.readLine()) != null) && (!read.startsWith("<h2>") && !read.startsWith("<h2 "))) {
data.append(read).append("\n");
}
data.append("</div>\n");
}
data.append(HTML_CLOSE);
}
}
catch (Exception e) {
LOG.error(e.getMessage(), e, "URL: " + url);
}
}
}
}