[javadoc] IDEA-285556 Support language injection into snippet

JEP-413 says that by default a snippet tag's body is in JAVA language, so JavadocInjector injects JAVA into a snippet tag automatically. The injector relies on the lang attribute to inject a language, if the attribute is not present, then JAVA is used.
In order to make a user's live easier the injector doesn't make user guess the correct language name, instead if the injector didn't find a language by the value from the `lang` attribute it traverses throughout all the registered languages and looks for the one the name of which matches the specified ignoring case. That is the case for java: in our code base the language goes by the `JAVA` id, but users tend to write the language name in lowercase ("java") or with only the first letter in the capital case ("Java")

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

GitOrigin-RevId: cca8c90bb5ad04485f1bf4119b9936114e5492e4
This commit is contained in:
Nikita Eshkeev
2021-12-28 19:55:49 +03:00
committed by intellij-monorepo-bot
parent 6df3ee6111
commit 6ffd9bc4b6
10 changed files with 161 additions and 18 deletions

View File

@@ -1276,6 +1276,7 @@
<postStartupActivity implementation="com.intellij.pom.java.AcceptedLanguageLevelsSettings"/>
<projectModelModifier implementation="com.intellij.openapi.roots.impl.IdeaProjectModelModifier" order="last"/>
<multiHostInjector implementation="com.intellij.psi.impl.source.tree.injected.JavaConcatenationToInjectorAdapter" order="first"/>
<multiHostInjector implementation="com.intellij.psi.impl.source.tree.injected.JavadocInjector" />
<changeSignatureDetector language="JAVA" implementationClass="com.intellij.refactoring.changeSignature.JavaChangeSignatureDetector"/>
<lookup.charFilter implementation="com.intellij.codeInsight.completion.JavaCharFilter" id="java"/>
<completion.contributor language="JAVA" id="javaMethodHandle" order="last, before javaLegacy"

View File

@@ -0,0 +1,86 @@
// 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.psi.impl.source.tree.injected;
import com.intellij.lang.Language;
import com.intellij.lang.injection.MultiHostInjector;
import com.intellij.lang.injection.MultiHostRegistrar;
import com.intellij.lang.java.JavaLanguage;
import com.intellij.openapi.util.TextRange;
import com.intellij.psi.PsiElement;
import com.intellij.psi.impl.source.javadoc.PsiSnippetDocTagImpl;
import com.intellij.psi.impl.source.javadoc.SnippetDocTagManipulator;
import com.intellij.psi.javadoc.PsiSnippetAttribute;
import com.intellij.psi.javadoc.PsiSnippetAttributeList;
import com.intellij.psi.javadoc.PsiSnippetDocTag;
import com.intellij.psi.javadoc.PsiSnippetDocTagValue;
import com.intellij.util.containers.ContainerUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.List;
public class JavadocInjector implements MultiHostInjector {
private static final String LANG_ATTR_KEY = "lang";
@Override
public void getLanguagesToInject(@NotNull MultiHostRegistrar registrar,
@NotNull PsiElement context) {
if (!(context instanceof PsiSnippetDocTagImpl)) return;
final PsiSnippetDocTagImpl snippet = (PsiSnippetDocTagImpl)context;
registrar.startInjecting(getLanguage(snippet))
.addPlace(null, null, snippet, innerRangeStrippingQuotes(snippet))
.doneInjecting();
}
private static @NotNull Language getLanguage(@NotNull PsiSnippetDocTagImpl snippet) {
PsiSnippetDocTagValue valueElement = snippet.getValueElement();
if (valueElement == null) return JavaLanguage.INSTANCE;
final PsiSnippetAttributeList attributeList = valueElement.getAttributeList();
for (PsiSnippetAttribute attribute : attributeList.getAttributes()) {
if (!LANG_ATTR_KEY.equals(attribute.getName())) continue;
final PsiElement langValue = attribute.getValue();
if (langValue == null) break;
final String langValueText = stripPossibleLeadingAndTrailingQuotes(langValue);
final Language language = findRegisteredLanguage(langValueText);
if (language == null) break;
return language;
}
return JavaLanguage.INSTANCE;
}
private static @Nullable Language findRegisteredLanguage(@NotNull String langValueText) {
final Language language = Language.findLanguageByID(langValueText);
if (language != null) return language;
return ContainerUtil.find(Language.getRegisteredLanguages(),
e -> e.getID().equalsIgnoreCase(langValueText));
}
private static @NotNull String stripPossibleLeadingAndTrailingQuotes(@NotNull PsiElement langValue) {
String langValueText = langValue.getText();
if (langValueText.charAt(0) == '"') {
langValueText = langValueText.substring(1);
}
if (langValueText.charAt(langValueText.length() - 1) == '"') {
langValueText = langValueText.substring(0, langValueText.length() - 1);
}
return langValueText;
}
private static @NotNull TextRange innerRangeStrippingQuotes(@NotNull PsiSnippetDocTagImpl context) {
return new SnippetDocTagManipulator().getRangeInElement(context);
}
@Override
public @NotNull List<? extends Class<? extends PsiElement>> elementsToInjectIn() {
return List.of(PsiSnippetDocTag.class);
}
}

View File

@@ -84,9 +84,19 @@ public class PsiSnippetDocTagImpl extends CompositePsiElement implements PsiSnip
int off = 0;
int len = subText.length();
boolean afterNewline = false;
while (off < len) {
final char aChar = subText.charAt(off++);
if (aChar == '*') continue;
if (afterNewline && Character.isWhitespace(aChar) ) {
continue;
}
if (afterNewline && aChar == '*') {
afterNewline = false;
continue;
}
if (aChar == '\n') {
afterNewline = true;
}
outChars.append(aChar);
outSourceOffsets[outChars.length() - outOffset] = off;
}

View File

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

View File

@@ -1,7 +1,6 @@
// "Inject language or reference" "true"
// "JAVA" "true"
/**
* The following code shows how to use {@code Optional.isPresent}:
* {@snippet<caret> :
* class Main {
* void f(Optional<Object> e) {

View File

@@ -1,9 +1,8 @@
// "Inject language or reference" "true"
// "JAVA" "true"
/**
* The following code shows how to use {@code Optional.isPresent}:
* {@snippet :
* class Main<caret> {
* {@snippet<caret> lang = "java" :
* class Main {
* void f(Optional<Object> e) {
* if (v.isPresent()) {
* System.out.println("v: " + v.get());

View File

@@ -0,0 +1,8 @@
// "JAVA" "true"
/**
* {@snippet<caret> lang=_NONEXISTING_LANGUAGE_:
* class Main { }
* }
*/
class InjectJava {}

View File

@@ -0,0 +1,9 @@
// "Properties" "true"
/**
* {@snippet<caret> lang = properties:
* greeting = hello
* what = world
* }
*/
class InjectJava {}

View File

@@ -3,15 +3,51 @@ package com.intellij.java.codeInsight.javadoc;
import com.intellij.codeInsight.daemon.quickFix.ActionHint;
import com.intellij.codeInsight.daemon.quickFix.LightQuickFixParameterizedTestCase;
import com.intellij.codeInsight.intention.IntentionAction;
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.PsiUtilCore;
import com.intellij.testFramework.LightProjectDescriptor;
import org.jetbrains.annotations.NotNull;
import java.util.concurrent.atomic.AtomicReference;
import static com.intellij.testFramework.assertions.Assertions.assertThat;
import static com.intellij.testFramework.fixtures.LightJavaCodeInsightFixtureTestCase.JAVA_17;
public class JavadocSnippetInjectionTest extends LightQuickFixParameterizedTestCase {
@Override
protected void setUp() throws Exception {
super.setUp();
MyTestInjector testInjector = new MyTestInjector(getPsiManager());
testInjector.injectAll(getTestRootDisposable());
}
@Override
protected void doAction(@NotNull ActionHint actionHint, @NotNull String testFullPath, @NotNull String testName) {
final Language language = getInjectedLanguage();
final Language expectedLang = Language.findLanguageByID(actionHint.getExpectedText());
assertThat(language)
.withFailMessage(String.format("Language '%s' should be injected, but found '%s'", actionHint.getExpectedText(), language.getID()))
.isEqualTo(expectedLang);
}
@NotNull
private Language getInjectedLanguage() {
int offset = getEditor().getCaretModel().getPrimaryCaret().getOffset();
final PsiSnippetDocTag snippet = (PsiSnippetDocTag) PsiUtilCore.getElementAtOffset(getFile(), offset).getParent();
final AtomicReference<PsiElement> injected = new AtomicReference<>();
final InjectedLanguageManager injectionManager = InjectedLanguageManager.getInstance(getProject());
injectionManager.enumerate(snippet, (injectedPsi, places) -> { injected.set(injectedPsi); });
return injected.get().getLanguage();
}
@Override
protected String getBasePath() {
return "/codeInsight/javadoc/snippet";
@@ -22,12 +58,4 @@ public class JavadocSnippetInjectionTest extends LightQuickFixParameterizedTestC
protected LightProjectDescriptor getProjectDescriptor() {
return JAVA_17;
}
@Override
protected void doAction(@NotNull ActionHint actionHint, @NotNull String testFullPath, @NotNull String testName) {
final IntentionAction injectionAction = findActionAndCheck(actionHint, testFullPath);
assertThat(injectionAction)
.withFailMessage("Injecting a language or a reference should be possible, but the action not found")
.isNotNull();
}
}

View File

@@ -35,7 +35,10 @@ public interface MultiHostInjector {
* class MyRegExpToJavaInjector implements MultiHostInjector {
* void getLanguagesToInject(MultiHostRegistrar registrar, PsiElement context) {
* if (context instanceof PsiLiteralExpression && looksLikeAGoodPlaceToInject(context)) {
* registrar.startInjecting(REGEXP_LANG).addPlace(null,null,context,innerRangeStrippingQuotes(context));
* registrar
* .startInjecting(REGEXP_LANG)
* .addPlace(null,null,context,innerRangeStrippingQuotes(context))
* .doneInjecting();
* }
* }
* }