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 bfdf48dd071c..33dd5f2de6e5 100644 Binary files a/java/java-tests/testData/codeInsight/documentation/library-javadoc.jar and b/java/java-tests/testData/codeInsight/documentation/library-javadoc.jar differ diff --git a/java/java-tests/testData/codeInsight/documentation/library-src.jar b/java/java-tests/testData/codeInsight/documentation/library-src.jar index 423bc77ce8d7..4b511794a4b0 100644 Binary files a/java/java-tests/testData/codeInsight/documentation/library-src.jar and b/java/java-tests/testData/codeInsight/documentation/library-src.jar differ diff --git a/java/java-tests/testData/codeInsight/documentation/library.jar b/java/java-tests/testData/codeInsight/documentation/library.jar index 3a251a0a5fa3..fcd0040d5d17 100644 Binary files a/java/java-tests/testData/codeInsight/documentation/library.jar and b/java/java-tests/testData/codeInsight/documentation/library.jar differ diff --git a/java/java-tests/testSrc/com/intellij/codeInsight/JavaExternalDocumentationTest.java b/java/java-tests/testSrc/com/intellij/codeInsight/JavaExternalDocumentationTest.java index 0deb48ca6967..009983ab6a4b 100644 --- a/java/java-tests/testSrc/com/intellij/codeInsight/JavaExternalDocumentationTest.java +++ b/java/java-tests/testSrc/com/intellij/codeInsight/JavaExternalDocumentationTest.java @@ -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(param = \"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)); }