[javadoc] IDEA-285556 Support language injection into snippet

Usually leading asterisks of a javadoc are aligned so the common indent for the lines in a snippet's body is obvious,
but nevertheless javadoc can have multiple leading asterisks, and they don't have to be aligned.
This patch fixes the indent stripping: if the indent is too short, which will result in leaving some leading
asterisks after stripping the indent from the line, the indent gets increased, so it goes after the last leading asterisk in the line.

GitOrigin-RevId: c79bcb3e25b96b5b1ff52da350f926673d155199
This commit is contained in:
Nikita Eshkeev
2022-02-10 04:12:07 +03:00
committed by intellij-monorepo-bot
parent 50e17a9f98
commit fd0a6dfb94
18 changed files with 238 additions and 40 deletions

View File

@@ -35,7 +35,7 @@ public class JavadocInjector implements MultiHostInjector {
final Language language = getLanguage(snippet);
final String prefix = language == JavaLanguage.INSTANCE ? SNIPPET_INJECTION_JAVA_HEADER : null;
final String suffix = language == JavaLanguage.INSTANCE ? " }}" : null;
final String suffix = language == JavaLanguage.INSTANCE ? "\n}}" : null;
final List<TextRange> ranges = snippet.getContentRanges();
if (ranges.isEmpty()) return;

View File

@@ -13,6 +13,7 @@ import com.intellij.psi.javadoc.PsiSnippetDocTagBody;
import com.intellij.psi.javadoc.PsiSnippetDocTagValue;
import com.intellij.psi.tree.TokenSet;
import com.intellij.util.IncorrectOperationException;
import com.intellij.util.text.CharArrayUtil;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -84,7 +85,7 @@ public class PsiSnippetDocTagImpl extends CompositePsiElement implements PsiSnip
}
@Contract(pure = true)
public @NotNull List<TextRange> getContentRanges() {
public @NotNull List<@NotNull TextRange> getContentRanges() {
final PsiSnippetDocTagValue valueElement = getValueElement();
if (valueElement == null) return Collections.emptyList();
@@ -108,12 +109,11 @@ public class PsiSnippetDocTagImpl extends CompositePsiElement implements PsiSnip
}
@Contract(pure = true)
private static @NotNull List<TextRange> getRanges(@NotNull TextRange snippetBodyTextRangeRelativeToSnippet, String@NotNull [] lines) {
private static @NotNull List<@NotNull TextRange> getRanges(@NotNull TextRange snippetBodyTextRangeRelativeToSnippet, String@NotNull [] lines) {
final int firstLine = getFirstNonEmptyLine(lines);
final int lastLine = getLastNonEmptyLine(lines);
int indent = getIndent(lines, firstLine, lastLine);
if (indent == Integer.MAX_VALUE) indent = 0;
int totalMinIndent = getIndent(lines, firstLine, lastLine);
int startOffset = getStartOffsetOfFirstNonEmptyLine(snippetBodyTextRangeRelativeToSnippet, lines, firstLine);
@@ -121,20 +121,44 @@ public class PsiSnippetDocTagImpl extends CompositePsiElement implements PsiSnip
for (int i = firstLine; i < Math.min(lastLine, lines.length); i++) {
final String line = lines[i];
final int size = line.length() + 1;
ranges.add(TextRange.create(size - indent).shiftRight(startOffset + indent));
final int indentSize = getIndentSize(line, totalMinIndent);
ranges.add(TextRange.create(size - indentSize).shiftRight(startOffset + indentSize));
startOffset += size;
}
final String line = lines[lastLine];
final int indentSize = getIndentSize(line, totalMinIndent);
final int endOffset = snippetBodyTextRangeRelativeToSnippet.getEndOffset();
final int lastLineStartOffset = Math.min(endOffset, startOffset + indent);
final int lastLineEndOffset = startOffset + lines[lastLine].length();
final int lastLineStartOffset = Math.min(endOffset, startOffset + indentSize);
final int lastLineEndOffset = startOffset + line.length();
ranges.add(TextRange.create(lastLineStartOffset, Math.min(endOffset, lastLineEndOffset)));
return ranges;
}
/**
* Usually leading asterisks of a javadoc are aligned so the common indent for lines in snippet body is obvious,
* but nevertheless javadoc can have multiple leading asterisks, and they don't have to be aligned.
* This method either returns the passed indent or, if the passed indent is too short, which will result in leaving some leading
* asterisks after stripping the indent from the line, the indent that goes after the last leading asterisk.
* @param line a line to calculate the indent size for
* @param indent an indent that is minimal across all the lines in the snippet body
* @return the indent that is either the passed indent, or a new indent that goes after the last leading asterisk.
*/
@Contract(pure = true)
@Range(from = 0, to = Integer.MAX_VALUE)
private static int getStartOffsetOfFirstNonEmptyLine(@NotNull TextRange snippetBodyTextRangeRelativeToSnippet, String@NotNull [] lines, int firstLine) {
private static @Range(from = 0, to = Integer.MAX_VALUE) int getIndentSize(@NotNull final String line, int indent) {
final int ownLineIndent = CharArrayUtil.shiftForward(line, 0, " *");
final String maxPossibleIndent = line.substring(0, ownLineIndent);
final int lastAsteriskInIndent = maxPossibleIndent.lastIndexOf('*', ownLineIndent);
return lastAsteriskInIndent >= indent ? lastAsteriskInIndent + 1 : indent;
}
@Contract(pure = true)
private static @Range(from = 0, to = Integer.MAX_VALUE) int getStartOffsetOfFirstNonEmptyLine(@NotNull TextRange snippetBodyTextRangeRelativeToSnippet, String@NotNull [] lines, int firstLine) {
int start = snippetBodyTextRangeRelativeToSnippet.getStartOffset();
for (int i = 0; i < Math.min(firstLine, lines.length); i++) {
start += lines[i].length() + 1;
@@ -143,8 +167,7 @@ public class PsiSnippetDocTagImpl extends CompositePsiElement implements PsiSnip
}
@Contract(pure = true)
@Range(from = 0, to = Integer.MAX_VALUE)
private static int getIndent(String@NotNull [] lines, int firstLine, int lastLine) {
private static @Range(from = 0, to = Integer.MAX_VALUE) int getIndent(String@NotNull [] lines, int firstLine, int lastLine) {
int minIndent = Integer.MAX_VALUE;
for (int i = firstLine; i <= lastLine && i < lines.length; i++) {
String line = lines[i];
@@ -157,12 +180,12 @@ public class PsiSnippetDocTagImpl extends CompositePsiElement implements PsiSnip
}
if (minIndent > indentLength) minIndent = indentLength;
}
if (minIndent == Integer.MAX_VALUE) minIndent = 0;
return minIndent;
}
@Contract(pure = true)
@Range(from = 0, to = Integer.MAX_VALUE)
private static int getLastNonEmptyLine(String@NotNull[] lines) {
private static @Range(from = 0, to = Integer.MAX_VALUE) int getLastNonEmptyLine(String@NotNull[] lines) {
int lastLine = lines.length - 1;
while (lastLine > 0 && isEmptyOrSpacesWithLeadingAsterisksOnly(lines[lastLine])) {
lastLine --;
@@ -171,8 +194,7 @@ public class PsiSnippetDocTagImpl extends CompositePsiElement implements PsiSnip
}
@Contract(pure = true)
@Range(from = 0, to = Integer.MAX_VALUE)
private static int getFirstNonEmptyLine(String@NotNull[] lines) {
private static @Range(from = 0, to = Integer.MAX_VALUE) int getFirstNonEmptyLine(String@NotNull[] lines) {
int firstLine = 0;
while (firstLine < lines.length && isEmptyOrSpacesWithLeadingAsterisksOnly(lines[firstLine])) {
firstLine ++;
@@ -187,8 +209,7 @@ public class PsiSnippetDocTagImpl extends CompositePsiElement implements PsiSnip
}
@Contract(pure = true)
@Range(from = 0, to = Integer.MAX_VALUE)
private static int calculateIndent(@NotNull String content) {
private static @Range(from = 0, to = Integer.MAX_VALUE) int calculateIndent(@NotNull String content) {
if (content.isEmpty()) return 0;
final String noIndent = content.replaceAll("^\\s*\\*\\s*", "");
return content.length() - noIndent.length();

View File

@@ -39,24 +39,7 @@ public final class SnippetDocTagManipulator extends AbstractElementManipulator<P
@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();
return input.replaceAll("(\\n\\s*)([^*\\s])", "$1 * $2");
}
@Override

View File

@@ -0,0 +1,15 @@
class A {
/**
* A simple program.
* {<warning descr="'@snippet' tag is not available at this language level">@snippet</warning> :
* class HelloWorld {
* void
* f(
* ) {
}
* }
* }
*/
void g() { }
}

View File

@@ -0,0 +1,6 @@
// "JAVA" "true"
class ___JavadocSnippetPlaceholder {
void ___JavadocSnippetPlaceholderMethod() throws Throwable {
}}

View File

@@ -0,0 +1,6 @@
// "JAVA" "true"
class ___JavadocSnippetPlaceholder {
void ___JavadocSnippetPlaceholderMethod() throws Throwable {
}}

View File

@@ -0,0 +1,12 @@
// "JAVA" "true"
class ___JavadocSnippetPlaceholder {
void ___JavadocSnippetPlaceholderMethod() throws Throwable {
class Main {
void f(Optional<Object> e) {
if (v.isPresent()) {
System.out.println("v: " + v.get());
}
}
}
}}

View File

@@ -0,0 +1,11 @@
// "_ignore" "true"
class ___JavadocSnippetPlaceholder {
void ___JavadocSnippetPlaceholderMethod() throws Throwable {
class HelloWorld {
void
f(
) {
}
}
}}

View File

@@ -0,0 +1,11 @@
// "_ignore" "true"
class ___JavadocSnippetPlaceholder {
void ___JavadocSnippetPlaceholderMethod() throws Throwable {
class HelloWorld {
void
f(
) {
}
}
}}

View File

@@ -0,0 +1,6 @@
// "JAVA" "true"
/**
* {@snippet :<caret>}
*/
class InjectJava {}

View File

@@ -0,0 +1,9 @@
// "JAVA" "true"
/**
* A simple program.
* {@snippet :
* <caret>
* }
*/
class InjectJava {}

View File

@@ -0,0 +1,16 @@
// "JAVA" "true"
class InjectJava {
/**
* {@snippet<caret> :
* class Main {
* void f(Optional<Object> e) {
* if (v.isPresent()) {
* System.out.println("v: " + v.get());
* }
* }
* }
* }
*/
void g() {}
}

View File

@@ -0,0 +1,17 @@
// "_ignore" "true"
class A {
/**
* A simple program.
* {@snippet :<caret>
class HelloWorld {
void
f(
) {
* }
}
}
*/
void g() { }
}

View File

@@ -0,0 +1,17 @@
// "_ignore" "true"
class A {
/**
* A simple program.
* {@snippet :<caret>
* class HelloWorld {
* void
* f(
* ) {
}
* }
* }
*/
void g() { }
}

View File

@@ -132,6 +132,7 @@ public class JavadocHighlightingTest extends LightDaemonAnalyzerTestCase {
public void testEmptySnippet() { doTest(); }
public void testOnlyEmptyLinesInSnippet() { doTest(); }
public void testSnippetInstructionsWithUnhandledThrowable() { doTest(); }
public void testUnalignedLeadingAsterisks() { doTest(); }
public void testIssueLinksInJavaDoc() {
IssueNavigationConfiguration navigationConfiguration = IssueNavigationConfiguration.getInstance(getProject());

View File

@@ -11,7 +11,7 @@ import org.jetbrains.annotations.NotNull;
import java.io.File;
import static com.intellij.testFramework.fixtures.LightJavaCodeInsightFixtureTestCase.JAVA_17;
import static com.intellij.testFramework.fixtures.LightJavaCodeInsightFixtureTestCase.JAVA_18;
public class JavadocCompletionInSnippetTest extends BasePlatformTestCase implements TestIndexingModeSupporter {
private @NotNull IndexingMode myIndexingMode = IndexingMode.SMART;
@@ -36,7 +36,7 @@ public class JavadocCompletionInSnippetTest extends BasePlatformTestCase impleme
@NotNull
@Override
protected LightProjectDescriptor getProjectDescriptor() {
return JAVA_17;
return JAVA_18;
}
@Override

View File

@@ -0,0 +1,67 @@
// 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.codeInsight.daemon.quickFix.ActionHint;
import com.intellij.codeInsight.daemon.quickFix.LightQuickFixParameterizedTestCase;
import com.intellij.lang.injection.InjectedLanguageManager;
import com.intellij.openapi.application.WriteAction;
import com.intellij.psi.PsiClass;
import com.intellij.psi.PsiElement;
import com.intellij.psi.javadoc.PsiSnippetDocTag;
import com.intellij.psi.util.PsiTreeUtil;
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.fixtures.LightJavaCodeInsightFixtureTestCase.JAVA_18;
public class JavadocSnippetInjectionFileTest extends LightQuickFixParameterizedTestCase {
@Override
protected void setUp() throws Exception {
super.setUp();
}
@Override
protected void doAction(@NotNull ActionHint actionHint, @NotNull String testFullPath, @NotNull String testName) {
final int offset = getEditor().getCaretModel().getPrimaryCaret().getOffset();
final PsiElement snippet = PsiUtilCore.getElementAtOffset(getFile(), offset);
final PsiClass enclosingClass = getEnclosingClass(snippet);
final PsiClass injectedClass = getInjectedClass(snippet);
WriteAction.run(() -> enclosingClass.replace(injectedClass));
checkResult(testName);
}
private void checkResult(@NotNull final String testName) {
final String expectedFilePath = getBasePath() + "/after" + testName;
checkResultByFile(expectedFilePath);
}
private static @NotNull PsiClass getEnclosingClass(PsiElement element) {
return PsiTreeUtil.getParentOfType(element, PsiClass.class);
}
private @NotNull PsiClass getInjectedClass(PsiElement element) {
final PsiSnippetDocTag snippet = PsiTreeUtil.getParentOfType(element, PsiSnippetDocTag.class);
final AtomicReference<PsiElement> injected = new AtomicReference<>();
final InjectedLanguageManager injectionManager = InjectedLanguageManager.getInstance(getProject());
injectionManager.enumerate(snippet, (injectedPsi, places) -> { injected.set(injectedPsi); });
return PsiTreeUtil.findChildOfType(injected.get(), PsiClass.class);
}
@Override
protected String getBasePath() {
return "/codeInsight/javadoc/snippet/file";
}
@NotNull
@Override
protected LightProjectDescriptor getProjectDescriptor() {
return JAVA_18;
}
}

View File

@@ -15,7 +15,7 @@ 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;
import static com.intellij.testFramework.fixtures.LightJavaCodeInsightFixtureTestCase.JAVA_18;
public class JavadocSnippetInjectionTest extends LightQuickFixParameterizedTestCase {
@@ -55,6 +55,6 @@ public class JavadocSnippetInjectionTest extends LightQuickFixParameterizedTestC
@NotNull
@Override
protected LightProjectDescriptor getProjectDescriptor() {
return JAVA_17;
return JAVA_18;
}
}