[javadoc] Fix label for links

Links that come from {@link} or @see without label should be rendered with a monospace font (!plainLink).

#IDEA-372168 Fixed

GitOrigin-RevId: 8040e6b0ea850bc9f043a8a72bb4a5bef8a2739e
This commit is contained in:
Louis Vignier
2025-06-13 15:12:12 +00:00
committed by intellij-monorepo-bot
parent cb3d9aeb39
commit d2c974c282
12 changed files with 136 additions and 43 deletions
@@ -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("</span>");
}
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);
}
}
@@ -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 <a href="psi_element://java.lang.Character"><code><span style="color:#0000ff;">Character</span></code></a> class.
specified by the <a href="psi_element://java.lang.Character"><code>Character</code></a> class.
<p>
The Java language provides special support for the string
concatenation operator (&nbsp;+&nbsp;), and for conversion of
@@ -1,4 +1,4 @@
<html><head><base href="placeholder"></head><body><div class='definition'><pre><span style="color:#000080;font-weight:bold;">class</span> <span style="color:#000000;">Test</span></pre></div><div class='content'>
Link to class <a href="psi_element://java.lang.String"><code><span style="color:#0000ff;">String</span></code></a>. <br>
Link to class <a href="psi_element://java.lang.String"><code><span style="color:#0000ff;">myLink</span></code></a>. <br>
Link to class <a href="psi_element://java.lang.String"><code>myLink</code></a>. <br>
</div><table class='sections'><p></table>
@@ -2,5 +2,5 @@
<code><span style="">Hello world</span></code></p><p>No tags are interpreted inside them
<code><span style="">{@link java.lang.String niceLink}</span></code></p><p>No markdown markup is interpreted inside them
<code><span style="">_Hello_ &lt;code&gt;</span></code></p><p>Code span inside a link
<a href="psi_element://java.lang.String"><span style="color:#0000ff;">my text with <code>a code span</code>!</span></a></p><p>This is a broken inline code span
<a href="psi_element://java.lang.String">my text with <code>a code span</code>!</a></p><p>This is a broken inline code span
`Start of broken code span</p>end of broken code span`</div><table class='sections'><p></table>
@@ -6,10 +6,10 @@
used in <code><span style="">equals</span></code> 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.</li><li>If two objects are equal according to the
<a href="psi_element://java.lang.Object#equals(java.lang.Object)"><span style="color:#0000ff;">equals</span></a> method, then calling the
<a href="psi_element://java.lang.Object#equals(java.lang.Object)">equals</a> method, then calling the
<code><span style="">hashCode</span></code> method on each of the two objects must produce the
same integer result.</li><li>It is <em>not</em> required that if two objects are unequal
according to the <a href="psi_element://java.lang.Object#equals(java.lang.Object)"><span style="color:#0000ff;">equals</span></a> method, then
according to the <a href="psi_element://java.lang.Object#equals(java.lang.Object)">equals</a> method, then
calling the <code><span style="">hashCode</span></code> method on each of the two objects
must produce distinct integer results. However, the programmer
should be aware that producing distinct integer results for
@@ -1 +1 @@
<html><head><base href="placeholder"></head><body><div class='definition'><pre><span style="color:#000080;font-weight:bold;">class</span> <span style="color:#000000;">MarkdownCodeBlock</span></pre></div><div class='content'><p><a href="psi_element://java.util.HashMap"><code><span style="color:#0000ff;">java.util.HashMap</span></code></a></p><a href="psi_element://java.util.Collections#EMPTY_LIST"><span style="color:#660e7a;">The default empty collection</span></a></div><table class='sections'><p></table>
<html><head><base href="placeholder"></head><body><div class='definition'><pre><span style="color:#000080;font-weight:bold;">class</span> <span style="color:#000000;">MarkdownCodeBlock</span></pre></div><div class='content'><p><a href="psi_element://java.util.HashMap"><code><span style="color:#0000ff;">java.util.HashMap</span></code></a></p><a href="psi_element://java.util.Collections#EMPTY_LIST">The default empty collection</a></div><table class='sections'><p></table>
@@ -0,0 +1 @@
<html><head><base href="placeholder"></head><body><div class='definition'><pre><span style="color:#000080;font-weight:bold;">class</span> <span style="color:#000000;">Test</span></pre></div><table class='sections'><p><tr><td valign='top' class='section'><p>See Also:</td><td valign='top'><p>"plain",<br><a href="java.lang.Math#E">plain</a>,<br><a href="psi_element://java.lang.Math#E"><code><span style="color:#0000ff;">Math</span><span style="">.</span><span style="color:#660e7a;">E</span></code></a>,<br><a href="psi_element://java.base/"><code><span style="">java.base/</span></code></a>,<br><a href="psi_element://java.lang.Math#E"><code><span style="color:#0000ff;">Math</span><span style="">.</span><span style="color:#660e7a;">E</span></code></a>,<br><a href="psi_element://java.lang.Math#E">plain</a>,<br><a href="psi_element://java.lang.Math#E">myLabel.Math#E</a></td></table>
@@ -0,0 +1,14 @@
import java.lang.String;
/**
* @see "plain"
* @see <a href="java.lang.Math#E">plain</a>
* @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;
}
@@ -0,0 +1 @@
<html><head><base href="placeholder"></head><body><div class='definition'><pre><span style="color:#000080;font-weight:bold;">class</span> <span style="color:#000000;">Test</span></pre></div><table class='sections'><p><tr><td valign='top' class='section'><p>See Also:</td><td valign='top'><p>"plain",<br><a href="java.lang.Math#E">plain</a>,<br><a href="psi_element://java.lang.Math#E"><code><span style="color:#0000ff;">Math</span><span style="">.</span><span style="color:#660e7a;">E</span></code></a>,<br><a href="psi_element://java.base/"><code><span style="">java.base/</span></code></a>,<br><a href="psi_element://java.lang.Math#E"><code><span style="color:#0000ff;">Math</span><span style="">.</span><span style="color:#660e7a;">E</span></code></a>,<br><a href="psi_element://java.lang.Math#E">plain</a>,<br><a href="psi_element://java.lang.Math#E">myLabel.Math#E</a></td></table>
@@ -0,0 +1,12 @@
import java.lang.String;
/// @see "plain"
/// @see <a href="java.lang.Math#E">plain</a>
/// @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;
}
@@ -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();
@@ -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
""",
"""
<div class="bottom"><icon src="AllIcons.Nodes.Class">&nbsp;<a href="psi_element://Gr"><code><span style="color:#000000;">Gr</span></code></a></div><div class='definition'><pre><span style="color:#000043;font-weight:bold;">void</span>&nbsp;<span style="color:#000000;">foo</span><span style="">(</span><span style="">)</span></pre></div><div class='content'>
Use <a href="psi_element://Gr#bar()"><code><span style="color:#0000ff;">bar</span><span style="">()</span></code></a> from class <a href="psi_element://Gr"><code><span style="color:#0000ff;">Gr</span></code></a> instead
Use <a href="psi_element://Gr#bar()"><code>#bar()</code></a> from class <a href="psi_element://Gr"><code>Gr</code></a> instead
</div><table class='sections'></table>""");
}