[javadoc] IDEA-285556 Support language injection into snippet

Implement updating the content of a snippet tag with the help from a method implementer. A snippet might invoke overriding a method though a completion and when a parent method is inserted it might contain javadoc. In this case it's impossible to recreate a snippet tag because the new text for the tag contains "/**" and "*/" that come from the parent method's javadoc. The implemented method implementer helps to strip a method's javadoc if it's present.

Signed-off-by: Nikita Eshkeev <nikita.eshkeev@jetbrains.com>

GitOrigin-RevId: 42b315ed2670701e50574a383f0442df69accd35
This commit is contained in:
Nikita Eshkeev
2022-01-18 05:41:10 +03:00
committed by intellij-monorepo-bot
parent 715e914e30
commit 436eee6cf4
16 changed files with 190 additions and 16 deletions

View File

@@ -163,7 +163,7 @@ public final class OverrideImplementUtil extends OverrideImplementExploreUtil {
}
@NotNull
private static PsiMethod decorateMethod(@NotNull PsiClass aClass,
public static PsiMethod decorateMethod(@NotNull PsiClass aClass,
@NotNull PsiMethod method,
boolean toCopyJavaDoc,
boolean insertOverrideIfPossible,
@@ -292,7 +292,7 @@ public final class OverrideImplementUtil extends OverrideImplementExploreUtil {
for (MethodImplementor implementor : getImplementors()) {
final GenerationInfo info = implementor.createGenerationInfo(s, mergeIfExists);
if (info instanceof PsiGenerationInfo) {
@SuppressWarnings({"unchecked"}) final PsiGenerationInfo<PsiMethod> psiGenerationInfo = (PsiGenerationInfo<PsiMethod>)info;
@SuppressWarnings("unchecked") final PsiGenerationInfo<PsiMethod> psiGenerationInfo = (PsiGenerationInfo<PsiMethod>)info;
return psiGenerationInfo;
}
}

View File

@@ -0,0 +1,61 @@
// Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.codeInsight.javadoc;
import com.intellij.codeInsight.MethodImplementor;
import com.intellij.codeInsight.generation.GenerateMembersUtil;
import com.intellij.codeInsight.generation.GenerationInfo;
import com.intellij.codeInsight.generation.OverrideImplementUtil;
import com.intellij.codeInsight.generation.PsiGenerationInfo;
import com.intellij.lang.injection.InjectedLanguageManager;
import com.intellij.psi.PsiClass;
import com.intellij.psi.PsiLanguageInjectionHost;
import com.intellij.psi.PsiMethod;
import com.intellij.psi.PsiSubstitutor;
import com.intellij.psi.javadoc.PsiSnippetDocTag;
import com.intellij.psi.util.TypeConversionUtil;
import com.intellij.util.Consumer;
import com.intellij.util.IncorrectOperationException;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
public class SnippetDocTagMethodImplementor implements MethodImplementor {
@Override
public PsiMethod @NotNull [] getMethodsToImplement(PsiClass aClass) {
return PsiMethod.EMPTY_ARRAY;
}
@Override
public PsiMethod @NotNull [] createImplementationPrototypes(PsiClass inClass,
PsiMethod method) throws IncorrectOperationException {
final PsiLanguageInjectionHost injectionHost = InjectedLanguageManager.getInstance(inClass.getProject()).getInjectionHost(inClass);
if (!(injectionHost instanceof PsiSnippetDocTag)) {
return PsiMethod.EMPTY_ARRAY;
}
final PsiClass containingClass = method.getContainingClass();
if (containingClass == null) {
return PsiMethod.EMPTY_ARRAY;
}
final PsiSubstitutor substitutor = inClass.isInheritor(containingClass, true)
? TypeConversionUtil.getSuperClassSubstitutor(containingClass, inClass, PsiSubstitutor.EMPTY)
: PsiSubstitutor.EMPTY;
final PsiMethod result = GenerateMembersUtil.substituteGenericMethod(method, substitutor, inClass);
return new PsiMethod[]{result};
}
@Override
public @Nullable GenerationInfo createGenerationInfo(PsiMethod method,
boolean mergeIfExists) {
return null;
}
@Override
public @NotNull Consumer<PsiMethod> createDecorator(PsiClass targetClass,
final PsiMethod baseMethod,
boolean toCopyJavaDoc,
boolean insertOverrideIfPossible) {
return result -> OverrideImplementUtil.decorateMethod(targetClass, baseMethod, false, insertOverrideIfPossible, result);
}
}

View File

@@ -22,6 +22,8 @@ import java.util.List;
public class JavadocInjector implements MultiHostInjector {
private static final String LANG_ATTR_KEY = "lang";
private static final String SNIPPET_INJECTION_JAVA_HEADER = "class ___JavadocSnippetPlaceholder {\n" +
" void ___JavadocSnippetPlaceholderMethod() throws Throwable {\n";
@Override
public void getLanguagesToInject(@NotNull MultiHostRegistrar registrar,
@@ -32,8 +34,8 @@ public class JavadocInjector implements MultiHostInjector {
final Language language = getLanguage(snippet);
final String prefix = language == JavaLanguage.INSTANCE ? "class ___JavadocSnippetPlaceholder { " : null;
final String suffix = language == JavaLanguage.INSTANCE ? " }" : null;
final String prefix = language == JavaLanguage.INSTANCE ? SNIPPET_INJECTION_JAVA_HEADER : null;
final String suffix = language == JavaLanguage.INSTANCE ? " }}" : null;
registrar.startInjecting(language)
.addPlace(prefix, suffix, snippet, innerRangeStrippingQuotes(snippet))

View File

@@ -121,6 +121,7 @@
implementationClass="com.intellij.psi.impl.source.resolve.reference.impl.manipulators.StringLiteralManipulator"/>
<lang.elementManipulator forClass="com.intellij.psi.impl.source.javadoc.PsiSnippetDocTagImpl"
implementationClass="com.intellij.psi.impl.source.javadoc.SnippetDocTagManipulator"/>
<methodImplementor implementation="com.intellij.codeInsight.javadoc.SnippetDocTagMethodImplementor"/>
<superMethodsSearch implementation="com.intellij.psi.impl.search.MethodSuperSearcher"/>
<lang.psiAugmentProvider implementation="com.intellij.psi.impl.source.JShellPsiAugmentProvider"/>
<codeInsight.containerProvider implementation="com.intellij.codeInsight.JavaContainerProvider" id="JAVA"/>

View File

@@ -7,9 +7,15 @@ import com.intellij.psi.AbstractElementManipulator;
import com.intellij.psi.JavaDocTokenType;
import com.intellij.psi.PsiElement;
import com.intellij.psi.impl.source.tree.TreeUtil;
import com.intellij.psi.*;
import com.intellij.psi.codeStyle.JavaFileCodeStyleFacade;
import com.intellij.psi.javadoc.PsiDocComment;
import com.intellij.psi.javadoc.PsiSnippetDocTag;
import com.intellij.psi.javadoc.PsiSnippetDocTagBody;
import com.intellij.psi.javadoc.PsiSnippetDocTagValue;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.util.IncorrectOperationException;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NotNull;
public final class SnippetDocTagManipulator extends AbstractElementManipulator<PsiSnippetDocTagImpl> {
@@ -18,7 +24,41 @@ public final class SnippetDocTagManipulator extends AbstractElementManipulator<P
public PsiSnippetDocTagImpl handleContentChange(@NotNull PsiSnippetDocTagImpl element,
@NotNull TextRange range,
String newContent) throws IncorrectOperationException {
throw new UnsupportedOperationException("Not implemented");
final PsiElementFactory factory = JavaPsiFacade.getElementFactory(element.getProject());
final JavaFileCodeStyleFacade codeStyleFacade = JavaFileCodeStyleFacade.forContext(element.getContainingFile());
final String newSnippetTagContent = codeStyleFacade.isJavaDocLeadingAsterisksEnabled()
? prependAbsentAsterisks(newContent)
: newContent;
final PsiDocComment text = factory.createDocCommentFromText("/**\n" + newSnippetTagContent + "\n*/");
final PsiSnippetDocTag snippet = PsiTreeUtil.findChildOfType(text, PsiSnippetDocTag.class);
if (snippet == null) {
return element;
}
return (PsiSnippetDocTagImpl)element.replace(snippet);
}
@Contract(pure = true)
private static @NotNull String prependAbsentAsterisks(@NotNull String input) {
final StringBuilder builder = new StringBuilder();
boolean afterNewLine = false;
for (char c : input.toCharArray()) {
if (c == '\n') {
afterNewLine = true;
}
else if (afterNewLine) {
if (c == '*') {
afterNewLine = false;
}
else if (!Character.isWhitespace(c)) {
builder.append("* ");
afterNewLine = false;
}
}
builder.append(c);
}
return builder.toString();
}
@Override

View File

@@ -1,5 +1,5 @@
/** {<warning descr="'@snippet' tag is not available at this language level">@snippet</warning> :
* Body<EOLError descr="Identifier expected"></EOLError>
* Body
* }
*/
class A {

View File

@@ -1,11 +1,9 @@
/** {<warning descr="'@snippet' tag is not available at this language level">@snippet</warning> :
* {
* Optional<Integer> v = null;
* if (v.isPresent()) {
* System.out.println("v: " + v.get());
* }
* }
* }
*/
class A {
}

View File

@@ -0,0 +1,7 @@
/** {<warning descr="'@snippet' tag is not available at this language level">@snippet</warning> :
* FileInputStream is = new FileInputStream("hello.world");
* int r = is.read();
* }
*/
class A {
}

View File

@@ -1,5 +1,5 @@
/** {<warning descr="'@snippet' tag is not available at this language level">@snippet</warning> :
* void f() {}
* void f<error descr="';' expected">(</error><error descr="Expression expected">)</error> {}
* }
*/
class A {

View File

@@ -0,0 +1,14 @@
class Main {
/**
* A simple program.
* {@snippet:
* class HelloWorld {
* @Override
* public boolean equals(Object obj) {
* return super.equals(obj);
* }void f() {}
* }
*}
*/
void f() {}
}

View File

@@ -0,0 +1,12 @@
class Main {
/**
* A simple program.
* {@snippet :
* class HelloWorld {
* equal<caret>
* void f() {}
* }
*}
*/
void f() {}
}

View File

@@ -5,7 +5,7 @@ class Main {
* A simple program.
* {@snippet :
* class HelloWorld {
* <caret>
* <caret>
* }
* }
*/

View File

@@ -131,7 +131,7 @@ public class JavadocHighlightingTest extends LightDaemonAnalyzerTestCase {
public void testSnippetInstructions() { doTest(); }
public void testEmptySnippet() { doTest(); }
public void testOnlyEmptyLinesInSnippet() { doTest(); }
public void testSnippetInstructionsWithUnhandledThrowable() { doTest(); }
public void testIssueLinksInJavaDoc() {
IssueNavigationConfiguration navigationConfiguration = IssueNavigationConfiguration.getInstance(getProject());
@@ -182,4 +182,4 @@ public class JavadocHighlightingTest extends LightDaemonAnalyzerTestCase {
private void doTest(String testFileName) {
doTest(testFileName, true, false);
}
}
}

View File

@@ -0,0 +1,35 @@
// Copyright 2000-2021 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.java.codeInsight.javadoc;
import com.intellij.testFramework.LightProjectDescriptor;
import com.intellij.testFramework.PlatformTestUtil;
import com.intellij.testFramework.fixtures.BasePlatformTestCase;
import org.jetbrains.annotations.NotNull;
import java.io.File;
import static com.intellij.testFramework.fixtures.LightJavaCodeInsightFixtureTestCase.JAVA_17;
public class JavadocCompletionInSnippetTest extends BasePlatformTestCase {
public void testOverrideMethod() {
doTest();
}
private void doTest() {
myFixture.configureByFile("before" + getTestName(false) + ".java");
myFixture.completeBasic();
myFixture.checkResultByFile("after" + getTestName(false) + ".java");
}
@Override
protected String getTestDataPath() {
return PlatformTestUtil.getCommunityPath().replace(File.separatorChar, '/') + "/java/java-tests/testData/codeInsight/javadoc/snippet/completion/";
}
@NotNull
@Override
protected LightProjectDescriptor getProjectDescriptor() {
return JAVA_17;
}
}

View File

@@ -6,7 +6,6 @@ import com.intellij.codeInsight.daemon.quickFix.LightQuickFixParameterizedTestCa
import com.intellij.lang.Language;
import com.intellij.lang.injection.InjectedLanguageManager;
import com.intellij.psi.PsiElement;
import com.intellij.psi.impl.source.tree.injected.MyTestInjector;
import com.intellij.psi.javadoc.PsiSnippetDocTag;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.psi.util.PsiUtilCore;

View File

@@ -142,11 +142,16 @@ class SmartPsiElementPointerImpl<E extends PsiElement> implements SmartPointerEx
boolean forInjected) {
SmartPointerElementInfo elementInfo = doCreateElementInfo(manager.getProject(), element, containingFile, forInjected);
if (ApplicationManager.getApplication().isUnitTestMode() && !ApplicationManagerEx.isInStressTest()) {
PsiElement restored = elementInfo.restoreElement(manager);
if (!element.equals(restored)) {
PsiElement restoredElement = elementInfo.restoreElement(manager);
if (restoredElement == null) {
// The problem might be with injection. It's a questionable solution, requires more discussion.
elementInfo = doCreateElementInfo(manager.getProject(), element, containingFile, !forInjected);
restoredElement = elementInfo.restoreElement(manager);
}
if (!element.equals(restoredElement)) {
// likely cause: PSI having isPhysical==true, but which can't be restored by containing file and range. To fix, make isPhysical return false
LOG.error("Cannot restore " + element + " of " + element.getClass() + " from " + elementInfo +
"; restored=" + restored + (restored == null ? "" : " of "+restored.getClass())+" in " + element.getProject());
"; restored=" + restoredElement + (restoredElement == null ? "" : " of " + restoredElement.getClass()) + " in " + element.getProject());
}
}
return elementInfo;