From f7c123e3281272c108c0349c66b5822afa5e71f9 Mon Sep 17 00:00:00 2001 From: Dmitry Batrak Date: Wed, 2 Sep 2015 19:56:31 +0300 Subject: [PATCH] IDEA-144356 Recognize relative HTML links with anchors when parsing external Javadoc --- .../javadoc/JavaDocExternalFilter.java | 40 +++--- .../javadoc/JavaDocInfoGenerator.java | 127 ++++++++++++------ .../documentation/LinkWithReference.html | 37 +++++ .../documentation/library-javadoc.jar | Bin 22163 -> 24051 bytes .../codeInsight/documentation/library-src.jar | Bin 1465 -> 1761 bytes .../codeInsight/documentation/library.jar | Bin 911 -> 1311 bytes .../JavaExternalDocumentationTest.java | 10 +- 7 files changed, 147 insertions(+), 67 deletions(-) create mode 100644 java/java-tests/testData/codeInsight/documentation/LinkWithReference.html diff --git a/java/java-impl/src/com/intellij/codeInsight/javadoc/JavaDocExternalFilter.java b/java/java-impl/src/com/intellij/codeInsight/javadoc/JavaDocExternalFilter.java index aa5437d076d5..159becfc8a8a 100644 --- a/java/java-impl/src/com/intellij/codeInsight/javadoc/JavaDocExternalFilter.java +++ b/java/java-impl/src/com/intellij/codeInsight/javadoc/JavaDocExternalFilter.java @@ -17,21 +17,17 @@ package com.intellij.codeInsight.javadoc; import com.intellij.codeInsight.documentation.AbstractExternalFilter; import com.intellij.codeInsight.documentation.DocumentationManager; -import com.intellij.codeInsight.documentation.DocumentationManagerProtocol; import com.intellij.codeInsight.documentation.PlatformDocumentationUtil; import com.intellij.ide.BrowserUtil; import com.intellij.lang.java.JavaDocumentationProvider; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.project.Project; import com.intellij.openapi.util.NullableComputable; -import com.intellij.openapi.util.text.StringUtil; import com.intellij.openapi.vfs.CharsetToolkit; import com.intellij.openapi.vfs.VirtualFile; -import com.intellij.psi.JavaPsiFacade; import com.intellij.psi.PsiClass; import com.intellij.psi.PsiElement; import com.intellij.psi.PsiMethod; -import com.intellij.psi.search.GlobalSearchScope; import org.jetbrains.annotations.NonNls; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -51,7 +47,8 @@ import java.util.regex.Pattern; */ public class JavaDocExternalFilter extends AbstractExternalFilter { - private final Project myProject; + private final Project myProject; + private PsiElement myElement; private static final ParseSettings ourPackageInfoSettings = new ParseSettings( Pattern.compile("package\\s+[^\\s]+\\s+description", Pattern.CASE_INSENSITIVE), @@ -59,8 +56,6 @@ public class JavaDocExternalFilter extends AbstractExternalFilter { true, false ); - protected static @NonNls final Pattern ourHTMLsuffix = Pattern.compile("[.][hH][tT][mM][lL]?"); - protected static @NonNls final Pattern ourParentFolderprefix = Pattern.compile("^[.][.]/"); protected static @NonNls final Pattern ourAnchorsuffix = Pattern.compile("#(.*)$"); protected static @NonNls final Pattern ourHTMLFilesuffix = Pattern.compile("/([^/]*[.][hH][tT][mM][lL]?)$"); private static @NonNls final Pattern ourHREFselector = Pattern.compile("\"]*)\"", Pattern.CASE_INSENSITIVE|Pattern.DOTALL); @@ -76,25 +71,19 @@ public class JavaDocExternalFilter extends AbstractExternalFilter { if (BrowserUtil.isAbsoluteURL(href)) { return href; } - - if (StringUtil.startsWithChar(href, '#')) { - return root + href; + String reference = JavaDocInfoGenerator.createReferenceForRelativeLink(href, myElement); + if (reference == null) { + if (href.startsWith("#")) { + return root + href; + } + else { + String nakedRoot = ourHTMLFilesuffix.matcher(root).replaceAll("/"); + return doAnnihilate(nakedRoot + href); + } + } + else { + return reference; } - - String nakedRoot = ourHTMLFilesuffix.matcher(root).replaceAll("/"); - - String stripped = ourHTMLsuffix.matcher(href).replaceAll(""); - int len = stripped.length(); - - do stripped = ourParentFolderprefix.matcher(stripped).replaceAll(""); while (len > (len = stripped.length())); - - final String elementRef = stripped.replaceAll("/", "."); - final String classRef = ourAnchorsuffix.matcher(elementRef).replaceAll(""); - - return - (JavaPsiFacade.getInstance(myProject).findClass(classRef, GlobalSearchScope.allScope(myProject)) != null) - ? DocumentationManagerProtocol.PSI_ELEMENT_PROTOCOL + elementRef - : doAnnihilate(nakedRoot + href); } } }; @@ -121,6 +110,7 @@ public class JavaDocExternalFilter extends AbstractExternalFilter { @Nullable public String getExternalDocInfoForElement(@NotNull String docURL, final PsiElement element) throws Exception { String externalDoc = null; + myElement = element; String builtInServer = "http://localhost:" + BuiltInServerOptions.getInstance().getEffectiveBuiltInServerPort() + "/" + myProject.getName() + "/"; if (docURL.startsWith(builtInServer)) { int refPosition = docURL.lastIndexOf('#'); diff --git a/java/java-impl/src/com/intellij/codeInsight/javadoc/JavaDocInfoGenerator.java b/java/java-impl/src/com/intellij/codeInsight/javadoc/JavaDocInfoGenerator.java index 95a1258f6d22..905bba345c72 100644 --- a/java/java-impl/src/com/intellij/codeInsight/javadoc/JavaDocInfoGenerator.java +++ b/java/java-impl/src/com/intellij/codeInsight/javadoc/JavaDocInfoGenerator.java @@ -31,6 +31,7 @@ import com.intellij.openapi.projectRoots.JavaSdkVersion; import com.intellij.openapi.projectRoots.Sdk; import com.intellij.openapi.roots.ProjectFileIndex; import com.intellij.openapi.util.Comparing; +import com.intellij.openapi.util.Couple; import com.intellij.openapi.util.JDOMUtil; import com.intellij.openapi.util.Pair; import com.intellij.openapi.util.text.StringUtil; @@ -282,57 +283,101 @@ public class JavaDocInfoGenerator { } protected String convertReference(@NonNls String href) { - final String originalReference = href; - - String fragment = null; - int hashPosition = href.indexOf('#'); - if (hashPosition >= 0) { - fragment = href.substring(hashPosition + 1); - href = href.substring(0, hashPosition); - } - if (href.isEmpty()) { - PsiElement containingClass = myElement instanceof PsiMember ? ((PsiMember)myElement).getContainingClass() : null; - PsiElement rootElement = containingClass == null ? myElement : containingClass; - return createLinkWithRef(rootElement, fragment); - } - if (!href.toLowerCase().endsWith(".htm") && !href.toLowerCase().endsWith(".html")) { - return originalReference; - } - href = href.substring(0, href.lastIndexOf('.')); + String reference = createReferenceForRelativeLink(href, myElement); + return reference == null ? href : reference; + } + /** + * Converts a relative link into {@link DocumentationManagerProtocol#PSI_ELEMENT_PROTOCOL PSI_ELEMENT_PROTOCOL}-type link if possible + */ + @Nullable + static String createReferenceForRelativeLink(@NotNull @NonNls String relativeLink, @NotNull PsiElement contextElement) { + String fragment = null; + int hashPosition = relativeLink.indexOf('#'); + if (hashPosition >= 0) { + fragment = relativeLink.substring(hashPosition + 1); + relativeLink = relativeLink.substring(0, hashPosition); + } + PsiElement targetElement; + if (relativeLink.isEmpty()) { + targetElement = (contextElement instanceof PsiField || contextElement instanceof PsiMethod) ? + ((PsiMember)contextElement).getContainingClass() : contextElement; + } + else { + if (!relativeLink.toLowerCase().endsWith(".htm") && !relativeLink.toLowerCase().endsWith(".html")) { + return null; + } + relativeLink = relativeLink.substring(0, relativeLink.lastIndexOf('.')); + + String packageName = getPackageName(contextElement); + if (packageName == null) return null; + + Couple pathWithPackage = removeParentReferences(Couple.of(relativeLink, packageName)); + if (pathWithPackage == null) return null; + relativeLink = pathWithPackage.first; + packageName = pathWithPackage.second; + + relativeLink = relativeLink.replace('/', '.'); + + String qualifiedTargetClassName = packageName.isEmpty() ? relativeLink : packageName + "." + relativeLink; + targetElement = JavaPsiFacade.getInstance(contextElement.getProject()).findClass(qualifiedTargetClassName, + contextElement.getResolveScope()); + } + if (targetElement == null) return null; + + String rawFragment = null; + if (fragment != null && targetElement instanceof PsiClass) { + if (fragment.contains("-") || fragment.contains("(")) { + rawFragment = fragment; + fragment = null; // reference to a method + } + else { + for (PsiField field : ((PsiClass)targetElement).getFields()) { + if (field.getName().equals(fragment)) { + rawFragment = fragment; + fragment = null; // reference to a field + break; + } + } + } + } + return DocumentationManagerProtocol.PSI_ELEMENT_PROTOCOL + JavaDocUtil.getReferenceText(targetElement.getProject(), targetElement) + + (rawFragment == null ? "" : ('#' + rawFragment)) + + (fragment == null ? "" : DocumentationManagerProtocol.PSI_ELEMENT_PROTOCOL_REF_SEPARATOR + fragment); + } + + /** + * Takes a pair of strings representing a relative path and a package name, and returns corresponding pair, where path is stripped of + * leading ../ elements, and package name adjusted correspondingly. Returns null if there are more ../ elements than package + * components. + */ + @Nullable + static Couple removeParentReferences(Couple pathWithContextPackage) { + String path = pathWithContextPackage.first; + String packageName = pathWithContextPackage.second; + while (path.startsWith("../")) { + if (packageName.isEmpty()) return null; + int dotPos = packageName.lastIndexOf('.'); + packageName = dotPos < 0 ? "" : packageName.substring(0, dotPos); + path = path.substring(3); + } + return Couple.of(path, packageName); + } + + static String getPackageName(PsiElement element) { String packageName = null; - if (myElement instanceof PsiPackage) { - packageName = ((PsiPackage)myElement).getQualifiedName(); + if (element instanceof PsiPackage) { + packageName = ((PsiPackage)element).getQualifiedName(); } else { - PsiFile file = myElement.getContainingFile(); + PsiFile file = element.getContainingFile(); if (file instanceof PsiClassOwner) { packageName = ((PsiClassOwner)file).getPackageName(); } } - if (packageName == null) return originalReference; - - while (href.startsWith("../")) { - if (packageName.isEmpty()) return originalReference; - int dotPos = packageName.lastIndexOf('.'); - packageName = dotPos < 0 ? "" : packageName.substring(0, dotPos); - href = href.substring(3); - } - - href = href.replace('/', '.'); - - String qualifiedName = packageName.isEmpty() ? href : packageName + "." + href; - PsiClass target = JavaPsiFacade.getInstance(myProject).findClass(qualifiedName, myElement.getResolveScope()); - if (target == null) return originalReference; - - return createLinkWithRef(target, fragment); + return packageName; } - private String createLinkWithRef(PsiElement psiElement, String ref) { - return DocumentationManagerProtocol.PSI_ELEMENT_PROTOCOL + JavaDocUtil.getReferenceText(myProject, psiElement) + - (ref == null ? "" : DocumentationManagerProtocol.PSI_ELEMENT_PROTOCOL_REF_SEPARATOR + ref); - } - public boolean generateDocInfoCore (final StringBuilder buffer, final boolean generatePrologueAndEpilogue) { if (myElement instanceof PsiClass) { generateClassJavaDoc(buffer, (PsiClass)myElement, generatePrologueAndEpilogue); diff --git a/java/java-tests/testData/codeInsight/documentation/LinkWithReference.html b/java/java-tests/testData/codeInsight/documentation/LinkWithReference.html new file mode 100644 index 000000000000..d84a994294b9 --- /dev/null +++ b/java/java-tests/testData/codeInsight/documentation/LinkWithReference.html @@ -0,0 +1,37 @@ + +
+
com.jetbrains
+

Class ClassWithRefLink

+
+
+
    +
  • java.lang.Object
  • +
  • +
      +
    • com.jetbrains.ClassWithRefLink
    • +
    +
  • +
+

+
public class ClassWithRefLink
+extends java.lang.Object
+ + + +
+
    +
  • + \ No newline at end of file diff --git a/java/java-tests/testData/codeInsight/documentation/library-javadoc.jar b/java/java-tests/testData/codeInsight/documentation/library-javadoc.jar index bfdf48dd071c700d63df7271877841148be732bc..33dd5f2de6e57c7fa9feb38cbfe3748fe1c52392 100644 GIT binary patch delta 2063 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 = \"foo\") class Foo {}"); + doTest("@com.jetbrains.TestAnnotation(param = \"foo\") class Foo {}"); + } + + public void testLinkWithReference() throws Exception { + doTest("class Foo { com.jetbrains.ClassWithRefLink field;}"); + } + + private void doTest(String text) throws Exception { + String actualText = getDocumentationText(text); String expectedText = StringUtil.convertLineSeparators(FileUtil.loadFile(getDataFile(getTestName(false) + ".html"))); assertEquals(expectedText, replaceBaseUrlWithPlaceholder(actualText)); }