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 444204d8b010..b633f1691cdc 100644 --- a/java/java-impl/src/com/intellij/codeInsight/javadoc/JavaDocInfoGenerator.java +++ b/java/java-impl/src/com/intellij/codeInsight/javadoc/JavaDocInfoGenerator.java @@ -47,10 +47,12 @@ import com.intellij.openapi.vfs.VirtualFile; import com.intellij.pom.java.LanguageLevel; import com.intellij.psi.*; import com.intellij.psi.impl.JavaConstantExpressionEvaluator; +import com.intellij.psi.impl.source.javadoc.PsiDocMethodOrFieldRef; import com.intellij.psi.impl.source.javadoc.PsiSnippetDocTagImpl; import com.intellij.psi.impl.source.tree.ElementType; import com.intellij.psi.impl.source.tree.JavaDocElementType; import com.intellij.psi.impl.source.tree.JavaElementType; +import com.intellij.psi.impl.source.tree.TreeElement; import com.intellij.psi.javadoc.*; import com.intellij.psi.search.GlobalSearchScope; import com.intellij.psi.tree.IElementType; @@ -2303,10 +2305,11 @@ public class JavaDocInfoGenerator { private void generateLinkValue(PsiInlineDocTag tag, StringBuilder buffer, boolean plainLink) { PsiElement[] tagElements = tag.getDataElements(); - String text = createLinkText(tagElements); - if (!text.isEmpty()) { - generateLink(buffer, text, tagElements[0], plainLink); - } + PsiElement ref = getRefElement(tagElements); + String label = getLinkLabel(tagElements, ref); + StringBuilder b = new StringBuilder(); + collectElementText(b, ref != null ? ref : tag); + generateLink(buffer, b.toString(), label, tag, plainLink, !hasLinkLabel(tagElements, ref)); } private void generateMarkdownLinkValue(PsiMarkdownReferenceLink referenceLink, StringBuilder buffer) { @@ -2322,7 +2325,7 @@ public class JavaDocInfoGenerator { } private void generateValueValue(PsiInlineDocTag tag, StringBuilder buffer, PsiElement element) { - String text = createLinkText(tag.getDataElements()); + String text = getRefText(tag.getDataElements()); PsiField valueField = null; if (text.isEmpty()) { if (myElement instanceof PsiField) valueField = (PsiField)myElement; @@ -2364,18 +2367,53 @@ public class JavaDocInfoGenerator { } } - protected String createLinkText(PsiElement[] tagElements) { - int predictOffset = tagElements.length > 0 ? tagElements[0].getTextOffset() + tagElements[0].getText().length() : 0; - StringBuilder buffer = new StringBuilder(); - for (PsiElement tagElement : tagElements) { - if (tagElement.getTextOffset() > predictOffset) buffer.append(' '); - predictOffset = tagElement.getTextOffset() + tagElement.getText().length(); + protected String getLinkLabel(PsiElement[] tagElements, @Nullable PsiElement ref) { + if (!hasLinkLabel(tagElements, ref)) return null; - collectElementText(buffer, tagElement); - } + StringBuilder buffer = new StringBuilder(); + Arrays.stream(tagElements) + .skip(ref == null ? 0 : ContainerUtil.indexOf(tagElements, e -> e == ref) + 1) + .forEach(element -> collectElementText(buffer, element)); return buffer.toString().trim(); } + private String getRefText(PsiElement[] tagElements) { + StringBuilder buffer = new StringBuilder(); + PsiElement ref = getRefElement(tagElements); + if (ref != null) collectElementText(buffer, ref); + return buffer.toString().trim(); + } + + private static @Nullable PsiElement getRefElement(PsiElement[] tagElements) { + for (PsiElement element : tagElements) { + if (element instanceof PsiWhiteSpace) { continue; } + + if (element instanceof PsiDocMethodOrFieldRef) { + return element; + } + // JavaDoc references + if (element instanceof TreeElement treeElement && treeElement.getTokenType() == JavaDocElementType.DOC_REFERENCE_HOLDER) { + return element; + } + // JavaDoc module references + if (element instanceof PsiDocTagValue docTagValue) { + PsiElement firstChild = docTagValue.getFirstChild(); + if (firstChild instanceof PsiJavaModuleReferenceElement || firstChild instanceof PsiJavaModuleReference) { + return element; + } + } + break; + } + return null; + } + + /** + * Returns true if the link has a custom label. + */ + private static boolean hasLinkLabel(PsiElement[] tagElements, PsiElement ref) { + return !(ContainerUtil.and(tagElements, element -> element == ref || element.getText().isBlank())); + } + protected void collectElementText(StringBuilder buffer, PsiElement element) { element.accept(new PsiRecursiveElementWalkingVisitor() { @Override @@ -2426,15 +2464,17 @@ public class JavaDocInfoGenerator { PsiDocTag tag = tags[i]; PsiElement[] elements = tag.getDataElements(); if (elements.length > 0) { - String text = createLinkText(elements); - if (StringUtil.startsWithChar(text, '<')) { - buffer.append(text); + PsiElement ref = getRefElement(elements); + String linkLabel = getLinkLabel(elements, ref); + if (StringUtil.startsWithChar(linkLabel, '<')) { + buffer.append(linkLabel); } - else if (StringUtil.startsWithChar(text, '"')) { - appendPlainText(buffer, text); + else if (StringUtil.startsWithChar(linkLabel, '"')) { + appendPlainText(buffer, linkLabel); } else { - generateLink(buffer, text, comment, false); + boolean plain = hasLinkLabel(elements, ref); + generateLink(buffer, ref != null ? ref.getText() : tag.getText(), plain ? linkLabel : null, tag, plain); } } if (i < tags.length - 1) { @@ -2706,13 +2746,6 @@ public class JavaDocInfoGenerator { } } - private void generateLink(StringBuilder buffer, String linkText, @NotNull PsiElement context, boolean plainLink) { - int index = JavaDocUtil.extractReference(linkText); - String refText = linkText.substring(0, index).trim(); - String label = StringUtil.nullize(linkText.substring(index).trim()); - generateLink(buffer, refText, label, context, plainLink); - } - void generateLink(StringBuilder buffer, PsiElement element, String label, boolean plainLink) { String refText = JavaDocUtil.getReferenceText(element.getProject(), element); if (refText != null) { @@ -2721,13 +2754,22 @@ public class JavaDocInfoGenerator { } /** + * @param refText The text of the target element. + * @param label An optional user-defined label. When not null, it will always be used as the link text. * @return Length of the generated label. */ int generateLink(StringBuilder buffer, String refText, String label, @NotNull PsiElement context, boolean plainLink) { - if (label == null) { - PsiManager manager = context.getManager(); - label = JavaDocUtil.getLabelText(manager.getProject(), manager, refText, context); - } + return generateLink(buffer, refText, label, context, plainLink, !plainLink); + } + + /** + * @param refText The text of the target element. + * @param label An optional user-defined label. When not null, it will always be used as the link text. + * @param shouldHighlightLabel True if syntax highlighting should be applied to the link (package, class, …). + * @return Length of the generated label. + */ + int generateLink(StringBuilder buffer, String refText, String label, @NotNull PsiElement context, boolean plainLink, boolean shouldHighlightLabel) { + // Resolve link target LOG.assertTrue(refText != null, "refText appears to be null."); PsiElement target = null; try { @@ -2736,8 +2778,16 @@ public class JavaDocInfoGenerator { catch (IndexNotReadyException e) { LOG.debug(e); } - appendMaybeUnresolvedLink(buffer, target, label, context.getProject(), plainLink); - return StringUtil.stripHtml(label, true).length(); + + // Resolve link text + String linkLabel = label; + if (label == null) { + PsiManager manager = context.getManager(); + linkLabel = JavaDocUtil.getLabelText(manager.getProject(), manager, refText, context); + } + + appendMaybeUnresolvedLink(buffer, target, linkLabel, context.getProject(), plainLink, shouldHighlightLabel); + return StringUtil.stripHtml(linkLabel, true).length(); } public void appendMaybeUnresolvedLink( @@ -2746,6 +2796,20 @@ public class JavaDocInfoGenerator { String label, @NotNull Project project, boolean plainLink + ) { + appendMaybeUnresolvedLink(buffer, target, label, project, plainLink, true); + } + + /** + * @param canHighlightLink false if the link should not receive syntax highlighting (e.g., it has a custom label) + */ + public void appendMaybeUnresolvedLink( + StringBuilder buffer, + @Nullable PsiElement target, + String label, + @NotNull Project project, + boolean plainLink, + boolean canHighlightLink ) { if (target == null && DumbService.isDumb(project)) { buffer.append(label); @@ -2754,9 +2818,8 @@ public class JavaDocInfoGenerator { buffer.append(getSpanForUnresolvedItem()).append(label).append(""); } else { - String highlightedLabel = myIsSignatureGenerationInProgress && doHighlightSignatures() || doSemanticHighlightingOfLinks() - ? tryHighlightLinkLabel(target, label) - : label; + boolean doHighlight = canHighlightLink && ((myIsSignatureGenerationInProgress && doHighlightSignatures() || doSemanticHighlightingOfLinks())); + String highlightedLabel = doHighlight ? tryHighlightLinkLabel(target, label) : label; generateLink(buffer, target, highlightedLabel, plainLink); } } diff --git a/java/java-tests/testData/codeInsight/javadocIG/documentationForJdkClassWhenExternalDocIsNotAvailable.html b/java/java-tests/testData/codeInsight/javadocIG/documentationForJdkClassWhenExternalDocIsNotAvailable.html index 11e9958c34e5..bb3fabe89895 100644 --- a/java/java-tests/testData/codeInsight/javadocIG/documentationForJdkClassWhenExternalDocIsNotAvailable.html +++ b/java/java-tests/testData/codeInsight/javadocIG/documentationForJdkClassWhenExternalDocIsNotAvailable.html @@ -18,7 +18,7 @@ searching strings, for extracting substrings, and for creating a copy of a string with all characters translated to uppercase or to lowercase. Case mapping is based on the Unicode Standard version - specified by the Character class. + specified by the Character class.

The Java language provides special support for the string concatenation operator ( + ), and for conversion of diff --git a/java/java-tests/testData/codeInsight/javadocIG/linkWithModule.html b/java/java-tests/testData/codeInsight/javadocIG/linkWithModule.html index 5f2b91806eec..acc44ee8d866 100644 --- a/java/java-tests/testData/codeInsight/javadocIG/linkWithModule.html +++ b/java/java-tests/testData/codeInsight/javadocIG/linkWithModule.html @@ -1,4 +1,4 @@

class Test
Link to class String.
- Link to class myLink.
+ Link to class myLink.

\ No newline at end of file diff --git a/java/java-tests/testData/codeInsight/javadocIG/markdownInlineCodeBlock.html b/java/java-tests/testData/codeInsight/javadocIG/markdownInlineCodeBlock.html index 393e9abe1d1b..fb7253be6bec 100644 --- a/java/java-tests/testData/codeInsight/javadocIG/markdownInlineCodeBlock.html +++ b/java/java-tests/testData/codeInsight/javadocIG/markdownInlineCodeBlock.html @@ -2,5 +2,5 @@ Hello world

No tags are interpreted inside them {@link java.lang.String niceLink}

No markdown markup is interpreted inside them _Hello_ <code>

Code span inside a link - my text with a code span!

This is a broken inline code span + my text with a code span!

This is a broken inline code span `Start of broken code span

end of broken code span`

\ No newline at end of file diff --git a/java/java-tests/testData/codeInsight/javadocIG/markdownJepExample.html b/java/java-tests/testData/codeInsight/javadocIG/markdownJepExample.html index cb8020bfd3ef..fec8368d323f 100644 --- a/java/java-tests/testData/codeInsight/javadocIG/markdownJepExample.html +++ b/java/java-tests/testData/codeInsight/javadocIG/markdownJepExample.html @@ -6,10 +6,10 @@ used in equals comparisons on the object is modified. This integer need not remain consistent from one execution of an application to another execution of the same application.
  • If two objects are equal according to the - equals method, then calling the + equals method, then calling the hashCode method on each of the two objects must produce the same integer result.
  • It is not required that if two objects are unequal - according to the equals method, then + according to the equals method, then calling the hashCode method on each of the two objects must produce distinct integer results. However, the programmer should be aware that producing distinct integer results for diff --git a/java/java-tests/testData/codeInsight/javadocIG/markdownReferenceLink.html b/java/java-tests/testData/codeInsight/javadocIG/markdownReferenceLink.html index 558b170cb8a4..cd682aa27bdb 100644 --- a/java/java-tests/testData/codeInsight/javadocIG/markdownReferenceLink.html +++ b/java/java-tests/testData/codeInsight/javadocIG/markdownReferenceLink.html @@ -1 +1 @@ -
    class MarkdownCodeBlock

    java.util.HashMap

    The default empty collection

    \ No newline at end of file +
    class MarkdownCodeBlock

    java.util.HashMap

    The default empty collection

    \ No newline at end of file diff --git a/java/java-tests/testData/codeInsight/javadocIG/seeTagWithLabel.html b/java/java-tests/testData/codeInsight/javadocIG/seeTagWithLabel.html new file mode 100644 index 000000000000..e2f45cb8c4b3 --- /dev/null +++ b/java/java-tests/testData/codeInsight/javadocIG/seeTagWithLabel.html @@ -0,0 +1 @@ +
    class Test

    See Also:

    "plain",
    plain,
    Math.E,
    java.base/,
    Math.E,
    plain,
    myLabel.Math#E

    \ No newline at end of file diff --git a/java/java-tests/testData/codeInsight/javadocIG/seeTagWithLabel.java b/java/java-tests/testData/codeInsight/javadocIG/seeTagWithLabel.java new file mode 100644 index 000000000000..24334e3bdbdc --- /dev/null +++ b/java/java-tests/testData/codeInsight/javadocIG/seeTagWithLabel.java @@ -0,0 +1,14 @@ +import java.lang.String; + +/** + * @see "plain" + * @see plain + * @see java.lang.Math#E + * @see java.base/ + * @see java.base/java.lang.Math#E + * @see java.lang.Math#E plain + * @see java.lang.Math#E myLabel.Math#E + */ +class Test { + public String field = null; +} \ No newline at end of file diff --git a/java/java-tests/testData/codeInsight/javadocIG/seeTagWithLabelMarkdown.html b/java/java-tests/testData/codeInsight/javadocIG/seeTagWithLabelMarkdown.html new file mode 100644 index 000000000000..e2f45cb8c4b3 --- /dev/null +++ b/java/java-tests/testData/codeInsight/javadocIG/seeTagWithLabelMarkdown.html @@ -0,0 +1 @@ +
    class Test

    See Also:

    "plain",
    plain,
    Math.E,
    java.base/,
    Math.E,
    plain,
    myLabel.Math#E

    \ No newline at end of file diff --git a/java/java-tests/testData/codeInsight/javadocIG/seeTagWithLabelMarkdown.java b/java/java-tests/testData/codeInsight/javadocIG/seeTagWithLabelMarkdown.java new file mode 100644 index 000000000000..fe5b1cda1ca2 --- /dev/null +++ b/java/java-tests/testData/codeInsight/javadocIG/seeTagWithLabelMarkdown.java @@ -0,0 +1,12 @@ +import java.lang.String; + +/// @see "plain" +/// @see plain +/// @see java.lang.Math#E +/// @see java.base/ +/// @see java.base/java.lang.Math#E +/// @see java.lang.Math#E plain +/// @see java.lang.Math#E myLabel.Math#E +class Test { + public String field = null; +} \ No newline at end of file diff --git a/java/java-tests/testSrc/com/intellij/java/codeInsight/javadoc/JavaDocInfoGeneratorTest.java b/java/java-tests/testSrc/com/intellij/java/codeInsight/javadoc/JavaDocInfoGeneratorTest.java index 7e2953d370dd..1406db1109bd 100644 --- a/java/java-tests/testSrc/com/intellij/java/codeInsight/javadoc/JavaDocInfoGeneratorTest.java +++ b/java/java-tests/testSrc/com/intellij/java/codeInsight/javadoc/JavaDocInfoGeneratorTest.java @@ -209,6 +209,8 @@ public class JavaDocInfoGeneratorTest extends JavaCodeInsightTestCase { public void testEscapeHtmlCodesInCodeBlock() { doTestClass(); } public void testPreTagLeakBeforeCode() { doTestClass(); } public void testPreTagStrictBeforeCode(){ doTestClass(); } + public void testSeeTagWithLabel() { doTestClass(); } + public void testSeeTagWithLabelMarkdown() { doTestClass(); } public void testRepeatableAnnotations() { useJava8(); diff --git a/plugins/groovy/test/org/jetbrains/plugins/groovy/GroovyDocumentationTest.java b/plugins/groovy/test/org/jetbrains/plugins/groovy/GroovyDocumentationTest.java index ed05436a845c..48bbb020e1ed 100644 --- a/plugins/groovy/test/org/jetbrains/plugins/groovy/GroovyDocumentationTest.java +++ b/plugins/groovy/test/org/jetbrains/plugins/groovy/GroovyDocumentationTest.java @@ -1,4 +1,4 @@ -// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. package org.jetbrains.plugins.groovy; import com.intellij.codeInsight.navigation.CtrlMouseHandler; @@ -47,7 +47,7 @@ public class GroovyDocumentationTest extends LightJavaCodeInsightFixtureTestCase """, """
     Gr
    void foo()
    - Use bar() from class Gr instead + Use #bar() from class Gr instead
    """); }