IDEA-144356 Recognize relative HTML links with anchors when parsing external Javadoc
authorDmitry Batrak <Dmitry.Batrak@jetbrains.com>
Wed, 2 Sep 2015 16:56:31 +0000 (19:56 +0300)
committerDmitry Batrak <Dmitry.Batrak@jetbrains.com>
Wed, 2 Sep 2015 16:58:05 +0000 (19:58 +0300)
java/java-impl/src/com/intellij/codeInsight/javadoc/JavaDocExternalFilter.java
java/java-impl/src/com/intellij/codeInsight/javadoc/JavaDocInfoGenerator.java
java/java-tests/testData/codeInsight/documentation/LinkWithReference.html [new file with mode: 0644]
java/java-tests/testData/codeInsight/documentation/library-javadoc.jar
java/java-tests/testData/codeInsight/documentation/library-src.jar
java/java-tests/testData/codeInsight/documentation/library.jar
java/java-tests/testSrc/com/intellij/codeInsight/JavaExternalDocumentationTest.java

index aa5437d076d52994da7c8e83f1837cd772bfb7b3..159becfc8a8ac3fc4595b285710257de3da36257 100644 (file)
@@ -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("<A.*?HREF=\"([^>\"]*)\"", 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('#');
index 95a1258f6d22c7265edd672719645286384b41b7..905bba345c720e9b30892141da90f9257d3c8919 100644 (file)
@@ -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 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 = href.indexOf('#');
+    int hashPosition = relativeLink.indexOf('#');
     if (hashPosition >= 0) {
-      fragment = href.substring(hashPosition + 1);
-      href = href.substring(0, hashPosition);
+      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<String> 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 (href.isEmpty()) {
-      PsiElement containingClass = myElement instanceof PsiMember ? ((PsiMember)myElement).getContainingClass() : null;
-      PsiElement rootElement = containingClass == null ? myElement : containingClass;
-      return createLinkWithRef(rootElement, fragment);
+    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;
+          }
+        }
+      }
     }
-    if (!href.toLowerCase().endsWith(".htm") && !href.toLowerCase().endsWith(".html")) {
-      return originalReference;
+    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 <code>null</code> if there are more ../ elements than package
+   * components.
+   */
+  @Nullable
+  static Couple<String> removeParentReferences(Couple<String> 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);
     }
-    href = href.substring(0, href.lastIndexOf('.'));
+    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 (file)
index 0000000..d84a994
--- /dev/null
@@ -0,0 +1,37 @@
+<HTML><base href="placeholder"><style type="text/css">  ul.inheritance {
+      margin:0;
+      padding:0;
+  }
+  ul.inheritance li {
+       display:inline;
+       list-style:none;
+  }
+  ul.inheritance li ul.inheritance {
+    margin-left:15px;
+    padding-left:15px;
+    padding-top:1px;
+  }
+</style><!-- ======== START OF CLASS DATA ======== -->
+<div class="header">
+<div class="subTitle">com.jetbrains</div>
+<h2 title="Class ClassWithRefLink" class="title">Class ClassWithRefLink</h2>
+</div>
+<div class="contentContainer">
+<ul class="inheritance">
+<li>java.lang.Object</li>
+<li>
+<ul class="inheritance">
+<li>com.jetbrains.ClassWithRefLink</li>
+</ul>
+</li>
+</ul>
+<DL><br>
+<pre>public class <span class="typeNameLabel">ClassWithRefLink</span>
+extends java.lang.Object</pre>
+<div class="block"><a href="psi_element://com.jetbrains.Test###someRef">link</a></div>
+</li>
+</ul>
+</div>
+<ul class="blockList">
+<li class="blockList">
+</HTML>
\ No newline at end of file
index bfdf48dd071c700d63df7271877841148be732bc..33dd5f2de6e57c7fa9feb38cbfe3748fe1c52392 100644 (file)
Binary files a/java/java-tests/testData/codeInsight/documentation/library-javadoc.jar and b/java/java-tests/testData/codeInsight/documentation/library-javadoc.jar differ
index 423bc77ce8d71f5c63d685433b383b5bd7a85df1..4b511794a4b012895e9b0db9995ffd2219062c01 100644 (file)
Binary files a/java/java-tests/testData/codeInsight/documentation/library-src.jar and b/java/java-tests/testData/codeInsight/documentation/library-src.jar differ
index 3a251a0a5fa300f132e8f122db91a056d89fd32d..fcd0040d5d1756ab1601c112ba9bbc5a15f0b7ea 100644 (file)
Binary files a/java/java-tests/testData/codeInsight/documentation/library.jar and b/java/java-tests/testData/codeInsight/documentation/library.jar differ
index 0deb48ca69670dcdf69e302574e762c5d51a537a..009983ab6a4b9210462a9561545640d8b1382ea2 100644 (file)
@@ -94,7 +94,15 @@ public class JavaExternalDocumentationTest extends PlatformTestCase {
   // We're guessing style of references in javadoc by bytecode version of library class file
   // but displaying quick doc should work even if javadoc was generated using a JDK not corresponding to bytecode version
   public void testReferenceStyleDoesntMatchBytecodeVersion() throws Exception {
-    String actualText = getDocumentationText("@com.jetbrains.TestAnnotation(<caret>param = \"foo\") class Foo {}");
+    doTest("@com.jetbrains.TestAnnotation(<caret>param = \"foo\") class Foo {}");
+  }
+
+  public void testLinkWithReference() throws Exception {
+    doTest("class Foo { com.jetbrains.<caret>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));
   }