mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-01-04 17:20:55 +07:00
[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:
committed by
intellij-monorepo-bot
parent
6df3ee6111
commit
6ffd9bc4b6
@@ -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"
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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) {
|
||||
@@ -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());
|
||||
@@ -0,0 +1,8 @@
|
||||
// "JAVA" "true"
|
||||
|
||||
/**
|
||||
* {@snippet<caret> lang=_NONEXISTING_LANGUAGE_:
|
||||
* class Main { }
|
||||
* }
|
||||
*/
|
||||
class InjectJava {}
|
||||
@@ -0,0 +1,9 @@
|
||||
// "Properties" "true"
|
||||
|
||||
/**
|
||||
* {@snippet<caret> lang = properties:
|
||||
* greeting = hello
|
||||
* what = world
|
||||
* }
|
||||
*/
|
||||
class InjectJava {}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
|
||||
Reference in New Issue
Block a user