From bfe0d574ab9032e70b7442d6281b1b50d9d0da75 Mon Sep 17 00:00:00 2001 From: Louis Vignier Date: Thu, 4 Sep 2025 15:11:36 +0200 Subject: [PATCH] [javadoc] Add navigation for fragment references #IDEA-376337 Fixed GitOrigin-RevId: e56328231c555d8cf4a75a93b107a1e284f15c73 --- .../impl/JavaPsiImplementationHelperImpl.java | 38 ++++++++++++++++++- .../index/JavaDocFragmentAnchorCache.kt | 15 ++++++++ .../core/CoreJavaPsiImplementationHelper.java | 8 +++- .../psi/impl/JavaPsiImplementationHelper.java | 8 +++- .../source/javadoc/PsiDocFragmentName.java | 23 +++++++++++ 5 files changed, 89 insertions(+), 3 deletions(-) diff --git a/java/java-impl/src/com/intellij/psi/impl/JavaPsiImplementationHelperImpl.java b/java/java-impl/src/com/intellij/psi/impl/JavaPsiImplementationHelperImpl.java index 77d9391d5d85..10de0db907f6 100644 --- a/java/java-impl/src/com/intellij/psi/impl/JavaPsiImplementationHelperImpl.java +++ b/java/java-impl/src/com/intellij/psi/impl/JavaPsiImplementationHelperImpl.java @@ -1,10 +1,12 @@ -// 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 com.intellij.psi.impl; import com.intellij.application.options.CodeStyle; import com.intellij.codeInsight.javadoc.JavaDocInfoGenerator; import com.intellij.codeInsight.javadoc.JavaSuperTypeSearchUtil; import com.intellij.codeInsight.javadoc.SnippetMarkup; +import com.intellij.codeInsight.javadoc.index.JavaDocFragmentAnchorCacheKt; +import com.intellij.codeInsight.javadoc.index.JavaDocFragmentData; import com.intellij.ide.fileTemplates.FileTemplate; import com.intellij.ide.fileTemplates.FileTemplateManager; import com.intellij.ide.fileTemplates.JavaTemplateUtil; @@ -45,6 +47,7 @@ import com.intellij.psi.codeStyle.arrangement.MemberOrderService; import com.intellij.psi.impl.compiled.ClsClassImpl; import com.intellij.psi.impl.compiled.ClsElementImpl; import com.intellij.psi.impl.source.codeStyle.ImportHelper; +import com.intellij.psi.impl.source.javadoc.PsiDocFragmentName; import com.intellij.psi.impl.source.javadoc.PsiSnippetAttributeValueImpl; import com.intellij.psi.javadoc.*; import com.intellij.psi.search.GlobalSearchScope; @@ -53,9 +56,11 @@ import com.intellij.psi.util.PsiTreeUtil; import com.intellij.psi.util.PsiUtil; import com.intellij.testFramework.LightVirtualFile; import com.intellij.util.IncorrectOperationException; +import kotlin.Pair; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.Unmodifiable; import java.util.*; import java.util.function.Function; @@ -445,6 +450,37 @@ public class JavaPsiImplementationHelperImpl extends JavaPsiImplementationHelper }; } + @Override + public @Nullable PsiSymbolReference getFragmentNameSymbol(@NotNull PsiDocFragmentName fragmentName) { + if (DumbService.getInstance(myProject).isDumb()) return null; + + final Pair fragmentData = JavaDocFragmentAnchorCacheKt.getAnchor(myProject, fragmentName); + if (fragmentData == null) return null; + final int offset = fragmentData.getSecond().getOffset(); + + final PsiElement target = fragmentData.getFirst().getContainingFile().getViewProvider().findElementAt(offset); + if (!(target instanceof PsiDocToken)) return null; + + return new PsiSymbolReference() { + @Override + public @NotNull PsiElement getElement() { + return fragmentName; + } + + @Override + public @NotNull TextRange getRangeInElement() { + return new TextRange(0, fragmentName.getText().length()); + } + + @Override + public @NotNull @Unmodifiable Collection resolveReference() { + return List.of( + new SnippetRegionSymbol(target.getContainingFile(), new TextRange(offset, offset + fragmentName.getText().length())) + ); + } + }; + } + public static final class SnippetRegionSymbol implements Symbol, DocumentationTarget, NavigationTarget { private final @NotNull PsiFile myFile; diff --git a/java/java-indexing-impl/src/com/intellij/codeInsight/javadoc/index/JavaDocFragmentAnchorCache.kt b/java/java-indexing-impl/src/com/intellij/codeInsight/javadoc/index/JavaDocFragmentAnchorCache.kt index 1db21ca5fefb..ccfa54a00503 100644 --- a/java/java-indexing-impl/src/com/intellij/codeInsight/javadoc/index/JavaDocFragmentAnchorCache.kt +++ b/java/java-indexing-impl/src/com/intellij/codeInsight/javadoc/index/JavaDocFragmentAnchorCache.kt @@ -3,8 +3,11 @@ package com.intellij.codeInsight.javadoc.index import com.intellij.openapi.components.Service import com.intellij.openapi.components.service +import com.intellij.openapi.project.DumbService import com.intellij.openapi.project.Project import com.intellij.psi.JavaPsiFacade +import com.intellij.psi.PsiClass +import com.intellij.psi.impl.source.javadoc.PsiDocFragmentName import com.intellij.psi.javadoc.PsiDocComment import com.intellij.psi.search.GlobalSearchScope import com.intellij.util.indexing.ID @@ -64,4 +67,16 @@ private class JavaDocFragmentCacheService { fun getAnchors(project: Project, fqnOrPackage: String): LinkedHashSet { return project.service().getAnchors(project, fqnOrPackage) +} + +fun getAnchor(project: Project, fragmentName: PsiDocFragmentName): Pair? { + if (DumbService.getInstance(project).isDumb) return null + + val psiClass = fragmentName.getScope() + val fqn = psiClass?.qualifiedName ?: return null + + val data = getAnchors(project, fqn).firstOrNull { data: JavaDocFragmentData -> data.name == fragmentName.text } + ?: return null + + return psiClass to data } \ No newline at end of file diff --git a/java/java-psi-impl/src/com/intellij/core/CoreJavaPsiImplementationHelper.java b/java/java-psi-impl/src/com/intellij/core/CoreJavaPsiImplementationHelper.java index a0d9d0e02989..782db3ce36b6 100644 --- a/java/java-psi-impl/src/com/intellij/core/CoreJavaPsiImplementationHelper.java +++ b/java/java-psi-impl/src/com/intellij/core/CoreJavaPsiImplementationHelper.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 com.intellij.core; import com.intellij.lang.ASTNode; @@ -9,6 +9,7 @@ import com.intellij.openapi.vfs.VirtualFile; import com.intellij.pom.java.LanguageLevel; import com.intellij.psi.*; import com.intellij.psi.impl.JavaPsiImplementationHelper; +import com.intellij.psi.impl.source.javadoc.PsiDocFragmentName; import com.intellij.psi.javadoc.PsiDocToken; import com.intellij.psi.javadoc.PsiSnippetAttributeValue; import com.intellij.psi.util.PsiUtil; @@ -85,6 +86,11 @@ public class CoreJavaPsiImplementationHelper extends JavaPsiImplementationHelper throw new UnsupportedOperationException(); } + @Override + public @Nullable PsiSymbolReference getFragmentNameSymbol(@NotNull PsiDocFragmentName token) { + throw new UnsupportedOperationException(); + } + public @NotNull Project getProject() { return myProject; } diff --git a/java/java-psi-impl/src/com/intellij/psi/impl/JavaPsiImplementationHelper.java b/java/java-psi-impl/src/com/intellij/psi/impl/JavaPsiImplementationHelper.java index 249887633303..e8a5d71d41e1 100644 --- a/java/java-psi-impl/src/com/intellij/psi/impl/JavaPsiImplementationHelper.java +++ b/java/java-psi-impl/src/com/intellij/psi/impl/JavaPsiImplementationHelper.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 com.intellij.psi.impl; import com.intellij.lang.ASTNode; @@ -7,6 +7,7 @@ import com.intellij.openapi.project.Project; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.pom.java.LanguageLevel; import com.intellij.psi.*; +import com.intellij.psi.impl.source.javadoc.PsiDocFragmentName; import com.intellij.psi.javadoc.PsiDocToken; import com.intellij.psi.javadoc.PsiSnippetAttributeValue; import org.jetbrains.annotations.NotNull; @@ -42,4 +43,9 @@ public abstract class JavaPsiImplementationHelper { * This makes it possible to navigate to the inherited doc. */ public abstract @NotNull PsiSymbolReference getInheritDocSymbol(@NotNull PsiDocToken token); + + /** + * Returns a PsiSymbolReference mapping a fragment reference ({@code ##fragment-id}) to its source ({@code

}). + */ + public abstract @Nullable PsiSymbolReference getFragmentNameSymbol(@NotNull PsiDocFragmentName token); } \ No newline at end of file diff --git a/java/java-psi-impl/src/com/intellij/psi/impl/source/javadoc/PsiDocFragmentName.java b/java/java-psi-impl/src/com/intellij/psi/impl/source/javadoc/PsiDocFragmentName.java index 3483337a574c..c4484c93dbdc 100644 --- a/java/java-psi-impl/src/com/intellij/psi/impl/source/javadoc/PsiDocFragmentName.java +++ b/java/java-psi-impl/src/com/intellij/psi/impl/source/javadoc/PsiDocFragmentName.java @@ -1,12 +1,20 @@ // Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. package com.intellij.psi.impl.source.javadoc; +import com.intellij.model.psi.PsiSymbolReference; import com.intellij.psi.JavaElementVisitor; +import com.intellij.psi.PsiClass; +import com.intellij.psi.PsiElement; import com.intellij.psi.PsiElementVisitor; +import com.intellij.psi.impl.JavaPsiImplementationHelper; import com.intellij.psi.impl.source.Constants; import com.intellij.psi.impl.source.tree.CompositePsiElement; import com.intellij.psi.javadoc.PsiDocTagValue; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Collection; +import java.util.Collections; public class PsiDocFragmentName extends CompositePsiElement implements PsiDocTagValue, Constants { public PsiDocFragmentName() { @@ -22,4 +30,19 @@ public class PsiDocFragmentName extends CompositePsiElement implements PsiDocTag visitor.visitElement(this); } } + + public @Nullable PsiClass getScope() { + final PsiElement parent = getParent(); + return parent instanceof PsiDocFragmentRef ? ((PsiDocFragmentRef)parent).getScope() : null; + } + + @Override + public @NotNull Collection getOwnReferences() { + final PsiSymbolReference symbolReference = JavaPsiImplementationHelper + .getInstance(getProject()) + .getFragmentNameSymbol(this); + + if (symbolReference != null) return Collections.singleton(symbolReference); + return super.getOwnReferences(); + } }