[javadoc] Basic navigation to region definition

IDEA-314507 Support regions in @snippet

GitOrigin-RevId: fb4471da5ed0b3c17cde8824938f0cc99e89aa96
This commit is contained in:
Tagir Valeev
2023-03-13 14:49:41 +01:00
committed by intellij-monorepo-bot
parent 5b956b914e
commit a6960c3e4e
8 changed files with 210 additions and 26 deletions

View File

@@ -8,6 +8,7 @@ import com.intellij.openapi.util.text.StringUtil;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.psi.PsiReference;
import com.intellij.psi.impl.FakePsiElement;
import com.intellij.psi.javadoc.*;
import com.intellij.psi.util.CachedValueProvider;
import com.intellij.psi.util.CachedValuesManager;
@@ -17,6 +18,7 @@ import one.util.streamex.StreamEx;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.TestOnly;
import java.util.*;
import java.util.function.Function;
@@ -27,6 +29,7 @@ import java.util.stream.Stream;
public class SnippetMarkup {
private final @NotNull List<@NotNull MarkupNode> myNodes;
private final @NotNull Map<@NotNull String, @NotNull MarkupNode> myRegionStarts;
private final @NotNull PsiElement myContext;
private final @NotNull BitSet myTextOffsets = new BitSet();
// \\S+ = language-dependent comment start token, like "//" or "#"
@@ -43,20 +46,30 @@ public class SnippetMarkup {
"link", Set.of("substring", "regex", "region", "target", "type")
);
private SnippetMarkup(@NotNull List<@NotNull MarkupNode> nodes) {
private SnippetMarkup(@NotNull List<@NotNull MarkupNode> nodes, @NotNull PsiElement context) {
myNodes = nodes;
myRegionStarts = StreamEx.of(nodes)
.mapToEntry(n -> n instanceof StartRegion start ? start.region() :
n instanceof LocationMarkupNode node ? node.region() :
null, Function.identity())
n instanceof LocationMarkupNode node ? node.region() :
null, Function.identity())
.removeKeys(k -> k == null || k.isEmpty())
.distinctKeys()
.toImmutableMap();
myContext = context;
StreamEx.of(nodes)
.select(PlainText.class)
.forEach(text -> myTextOffsets.set(text.range().getStartOffset(), text.range().getEndOffset()));
}
public @NotNull PsiElement getContext() {
return myContext;
}
public int getRegionOffset(@NotNull String region) {
MarkupNode node = myRegionStarts.get(region);
return node == null ? -1 : node.range().getStartOffset();
}
/**
* @param region region to test; null for the whole snippet
* @return true if any of {@code @replacement}, {@code @highlight}, or {@code @link} tags exist within the region
@@ -64,7 +77,7 @@ public class SnippetMarkup {
public boolean hasMarkup(@Nullable String region) {
var visitor = new SnippetVisitor() {
boolean hasMarkup = false;
@Override
public void visitPlainText(@NotNull PlainText plainText, @NotNull List<@NotNull LocationMarkupNode> activeNodes) {
hasMarkup |= !activeNodes.isEmpty();
@@ -231,8 +244,14 @@ public class SnippetMarkup {
@NotNull LinkType linkType) implements LocationMarkupNode {
}
@TestOnly
public static @NotNull SnippetMarkup parse(@NotNull String text) {
return parse(preparse(text));
return parse(preparse(text), new FakePsiElement() {
@Override
public PsiElement getParent() {
return null;
}
});
}
/**
@@ -244,7 +263,8 @@ public class SnippetMarkup {
throw new IllegalArgumentException();
}
return CachedValuesManager.getCachedValue(element, () -> {
SnippetMarkup markup = element instanceof PsiSnippetDocTagBody body ? parse(body) : parse(element.getText());
SnippetMarkup markup = element instanceof PsiSnippetDocTagBody body ? parse(preparse(body), body) :
parse(preparse(element.getText()), element);
return new CachedValueProvider.Result<>(markup, element);
});
}
@@ -267,10 +287,6 @@ public class SnippetMarkup {
return fromElement(file);
}
private static @NotNull SnippetMarkup parse(@NotNull PsiSnippetDocTagBody body) {
return parse(preparse(body));
}
private static @NotNull List<@NotNull PlainText> preparse(@NotNull String text) {
List<PlainText> output = new ArrayList<>();
int pos = 0;
@@ -296,8 +312,8 @@ public class SnippetMarkup {
return output;
}
private static @NotNull SnippetMarkup parse(@NotNull List<@NotNull PlainText> preparsed) {
return new SnippetMarkup(preparsed.stream().flatMap(SnippetMarkup::parseLine).toList());
private static @NotNull SnippetMarkup parse(@NotNull List<@NotNull PlainText> preparsed, @NotNull PsiElement context) {
return new SnippetMarkup(preparsed.stream().flatMap(SnippetMarkup::parseLine).toList(), context);
}
private static Stream<MarkupNode> parseLine(@NotNull PlainText text) {

View File

@@ -1,14 +1,18 @@
// Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
// Copyright 2000-2023 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.SnippetMarkup;
import com.intellij.ide.fileTemplates.FileTemplate;
import com.intellij.ide.fileTemplates.FileTemplateManager;
import com.intellij.ide.fileTemplates.JavaTemplateUtil;
import com.intellij.lang.ASTNode;
import com.intellij.lang.java.JavaLanguage;
import com.intellij.navigation.NavigationRequest;
import com.intellij.navigation.NavigationService;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.fileEditor.OpenFileDescriptor;
import com.intellij.openapi.fileTypes.FileTypeRegistry;
import com.intellij.openapi.fileTypes.StdFileTypes;
import com.intellij.openapi.module.LanguageLevelUtil;
@@ -32,6 +36,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.javadoc.PsiSnippetDocTagValue;
import com.intellij.psi.search.GlobalSearchScope;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.psi.util.PsiUtil;
@@ -336,4 +341,71 @@ public class JavaPsiImplementationHelperImpl extends JavaPsiImplementationHelper
throw new IncorrectOperationException("Incorrect file template", (Throwable)e);
}
}
@Override
public @Nullable PsiElement resolveSnippetRegion(@NotNull PsiElement context,
@NotNull PsiSnippetDocTagValue snippet,
@NotNull String region) {
SnippetMarkup markup = SnippetMarkup.fromSnippet(snippet);
if (markup == null) return null;
int offset = markup.getRegionOffset(region);
if (offset == -1) return null;
PsiElement markupContext = markup.getContext();
PsiFile file = markupContext.getContainingFile();
if (file == null) return null;
int fileOffset = markupContext.getTextRange().getStartOffset() + offset - 1;
return new FakeElement(file, fileOffset);
}
private static final class FakeElement extends FakePsiElement implements SyntheticElement {
private final PsiFile myFile;
private final int myOffset;
private final Project myProject;
private final VirtualFile myVirtualFile;
private FakeElement(PsiFile file, int fileOffset) {
myFile = file;
myOffset = fileOffset;
myProject = file.getProject();
myVirtualFile = file.getVirtualFile();
}
@Override
public PsiElement getParent() {
return myFile;
}
@Override
public void navigate(boolean requestFocus) {
new OpenFileDescriptor(myProject, myVirtualFile, myOffset).navigate(requestFocus);
}
@Override
public @Nullable NavigationRequest navigationRequest() {
return NavigationService.getInstance().sourceNavigationRequest(myVirtualFile, myOffset);
}
@Override
public boolean canNavigate() {
return true;
}
@Override
public boolean canNavigateToSource() {
return true;
}
@Override
public boolean equals(Object object) {
if (this == object) return true;
if (object == null || getClass() != object.getClass()) return false;
FakeElement element = (FakeElement)object;
return myOffset == element.myOffset && Objects.equals(myVirtualFile, element.myVirtualFile);
}
@Override
public int hashCode() {
return Objects.hash(myVirtualFile, myOffset);
}
}
}

View File

@@ -1,4 +1,4 @@
// Copyright 2000-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
// Copyright 2000-2023 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;
@@ -7,6 +7,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.javadoc.PsiSnippetDocTagValue;
import com.intellij.psi.util.PsiUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -58,6 +59,13 @@ public class CoreJavaPsiImplementationHelper extends JavaPsiImplementationHelper
throw new UnsupportedOperationException("TODO");
}
@Override
public @Nullable PsiElement resolveSnippetRegion(@NotNull PsiElement context,
@NotNull PsiSnippetDocTagValue snippet,
@NotNull String region) {
return null;
}
@NotNull
public Project getProject() {
return myProject;

View File

@@ -1,4 +1,4 @@
// Copyright 2000-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
// Copyright 2000-2023 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;
@@ -6,6 +6,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.javadoc.PsiSnippetDocTagValue;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -37,4 +38,8 @@ public abstract class JavaPsiImplementationHelper {
@NotNull PsiType exceptionType,
@Nullable PsiElement context,
@NotNull PsiCatchSection element);
public abstract @Nullable PsiElement resolveSnippetRegion(@NotNull PsiElement context,
@NotNull PsiSnippetDocTagValue snippet,
@NotNull String region);
}

View File

@@ -8,6 +8,7 @@ import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.VfsUtilCore;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.*;
import com.intellij.psi.impl.JavaPsiImplementationHelper;
import com.intellij.psi.impl.source.resolve.reference.impl.providers.PsiFileReference;
import com.intellij.psi.impl.source.tree.ChangeUtil;
import com.intellij.psi.impl.source.tree.JavaDocElementType;
@@ -15,6 +16,8 @@ import com.intellij.psi.impl.source.tree.LeafElement;
import com.intellij.psi.impl.source.tree.LeafPsiElement;
import com.intellij.psi.javadoc.PsiSnippetAttribute;
import com.intellij.psi.javadoc.PsiSnippetAttributeValue;
import com.intellij.psi.javadoc.PsiSnippetDocTagValue;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.util.ArrayUtilRt;
import com.intellij.util.IncorrectOperationException;
import com.intellij.util.containers.ContainerUtil;
@@ -44,14 +47,15 @@ public class PsiSnippetAttributeValueImpl extends LeafPsiElement implements PsiS
PsiElement parent = getParent();
if (parent instanceof PsiSnippetAttribute) {
PsiSnippetAttribute attribute = (PsiSnippetAttribute)parent;
if (attribute.getValue() == this) {
String name = attribute.getName();
if (name.equals(PsiSnippetAttribute.CLASS_ATTRIBUTE)) {
return new SnippetFileReference(false);
}
else if (name.equals(PsiSnippetAttribute.FILE_ATTRIBUTE)) {
return new SnippetFileReference(true);
}
String name = attribute.getName();
if (name.equals(PsiSnippetAttribute.CLASS_ATTRIBUTE)) {
return new SnippetFileReference(false);
}
else if (name.equals(PsiSnippetAttribute.FILE_ATTRIBUTE)) {
return new SnippetFileReference(true);
}
else if (name.equals(PsiSnippetAttribute.REGION_ATTRIBUTE)) {
return new SnippetRegionReference();
}
}
return null;
@@ -240,4 +244,54 @@ public class PsiSnippetAttributeValueImpl extends LeafPsiElement implements PsiS
return getClass().getName() + "(" + getCanonicalText() + ")";
}
}
private class SnippetRegionReference implements PsiReference {
private PsiElement myTarget;
@Override
public @NotNull PsiElement getElement() {
return PsiSnippetAttributeValueImpl.this;
}
@Override
public @NotNull TextRange getRangeInElement() {
return getValueRange();
}
@Override
public @Nullable PsiElement resolve() {
if (myTarget == null) {
PsiSnippetDocTagValue snippet = PsiTreeUtil.getParentOfType(PsiSnippetAttributeValueImpl.this, PsiSnippetDocTagValue.class);
if (snippet == null) return null;
myTarget = JavaPsiImplementationHelper.getInstance(getProject())
.resolveSnippetRegion(PsiSnippetAttributeValueImpl.this, snippet, getValue());
}
return myTarget;
}
@Override
public @NotNull String getCanonicalText() {
return getValue();
}
@Override
public PsiElement handleElementRename(@NotNull String newElementName) throws IncorrectOperationException {
throw new IncorrectOperationException();
}
@Override
public PsiElement bindToElement(@NotNull PsiElement element) throws IncorrectOperationException {
throw new IncorrectOperationException();
}
@Override
public boolean isReferenceTo(@NotNull PsiElement element) {
return element.equals(myTarget);
}
@Override
public boolean isSoft() {
return false;
}
}
}

View File

@@ -6,7 +6,7 @@ package pkg;
* {@snippet class=<error descr="Snippet file is not found">Test1</error>}
* {@snippet class="Test"}
* {@snippet class='Test''}
* {@snippet class='sub.Test'}
* {@snippet class='sub.Test' region="reg"}
* {@snippet class=<error descr="Snippet file is not found">'sub.Test2'</error>}
* {@snippet file="sub/test.txt"}
*/

View File

@@ -1,4 +1,6 @@
package sub;
// @start region=reg
public class Test {
}
}
// @end

View File

@@ -4,9 +4,19 @@ package com.intellij.java.codeInsight.daemon;
import com.intellij.codeInsight.daemon.DaemonAnalyzerTestCase;
import com.intellij.codeInspection.javaDoc.JavaDocReferenceInspection;
import com.intellij.codeInspection.javaDoc.JavadocDeclarationInspection;
import com.intellij.navigation.NavigationRequest;
import com.intellij.navigation.impl.SourceNavigationRequest;
import com.intellij.openapi.application.ReadAction;
import com.intellij.openapi.projectRoots.Sdk;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.pom.Navigatable;
import com.intellij.pom.java.LanguageLevel;
import com.intellij.psi.*;
import com.intellij.psi.javadoc.PsiSnippetAttribute;
import com.intellij.testFramework.IdeaTestUtil;
import com.intellij.util.concurrency.AppExecutorUtil;
import java.util.concurrent.ExecutionException;
public class JavadocResolveTest extends DaemonAnalyzerTestCase {
private static final String BASE_PATH = "/codeInsight/daemonCodeAnalyzer/javaDoc/resolve";
@@ -23,8 +33,25 @@ public class JavadocResolveTest extends DaemonAnalyzerTestCase {
myJavaDocReferenceInspection.REPORT_INACCESSIBLE = false;
doTest();
}
public void testSnippetRefs() {
public void testSnippetRefs() throws ExecutionException, InterruptedException {
doTest();
PsiSnippetAttribute attribute = SyntaxTraverser.psiTraverser(myFile).filter(PsiSnippetAttribute.class)
.filter(attr -> attr.getName().equals(PsiSnippetAttribute.REGION_ATTRIBUTE))
.first();
assertNotNull(attribute);
PsiReference ref = attribute.getValue().getReference();
assertNotNull(ref);
PsiElement resolved = ref.resolve();
assertTrue(resolved instanceof Navigatable);
NavigationRequest request = ReadAction
.nonBlocking(() -> ((Navigatable)resolved).navigationRequest()).submit(AppExecutorUtil.getAppExecutorService())
.get();
assertTrue(request instanceof SourceNavigationRequest);
SourceNavigationRequest snr = (SourceNavigationRequest)request;
VirtualFile file = snr.getFile();
assertEquals(file.getName(), "Test.java");
PsiFile snippetFile = PsiManager.getInstance(myProject).findFile(file);
assertTrue(snippetFile.getText().startsWith("@start region=reg", snr.getOffset()));
}
private void doTest() {