mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-04-21 22:11:40 +07:00
[javadoc] Basic navigation to region definition
IDEA-314507 Support regions in @snippet GitOrigin-RevId: fb4471da5ed0b3c17cde8824938f0cc99e89aa96
This commit is contained in:
committed by
intellij-monorepo-bot
parent
5b956b914e
commit
a6960c3e4e
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"}
|
||||
*/
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
package sub;
|
||||
|
||||
// @start region=reg
|
||||
public class Test {
|
||||
}
|
||||
}
|
||||
// @end
|
||||
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user