[java] IDEA-349062 Allow "Join lines" for text blocks to convert to a regular string

GitOrigin-RevId: d435698b23f56ef9ce05305266118c0e75bdb9ef
This commit is contained in:
Tagir Valeev
2024-03-13 13:44:19 +01:00
committed by intellij-monorepo-bot
parent 40006e5d42
commit 1fd43a5df9
14 changed files with 146 additions and 8 deletions

View File

@@ -8,8 +8,12 @@ public final class BasicLiteralUtil {
private BasicLiteralUtil() {
}
/**
* @param expression text block expression to calculate indent for
* @return the indent of text block lines; may return -1 if text block is heavily malformed
*/
public static int getTextBlockIndent(@NotNull PsiElement expression) {
String[] lines = getTextBlockLines(expression);
String[] lines = getTextBlockLines(expression.getText(), true);
if (lines == null) return -1;
return getTextBlockIndent(lines);
}
@@ -27,14 +31,26 @@ public final class BasicLiteralUtil {
return getTextBlockLines(rawText);
}
/**
* @param rawText text block text, including triple quotes at the start and at the end
* @return array of textblock content lines, including indent; null if text block is malformed
*/
public static String @Nullable [] getTextBlockLines(String rawText) {
if (rawText.length() < 7 || !rawText.endsWith("\"\"\"")) return null;
return getTextBlockLines(rawText, false);
}
/**
* @param rawText text block text, including triple quotes at the start and at the end
* @param skipFirstLine if true, skip invalid content in the first line after triple quotes
* @return array of textblock content lines, including indent; null if text block is malformed
*/
private static String @Nullable [] getTextBlockLines(String rawText, boolean skipFirstLine) {
if (rawText.length() < 7 || !rawText.startsWith("\"\"\"") || !rawText.endsWith("\"\"\"")) return null;
int start = 3;
while (true) {
char c = rawText.charAt(start++);
if (c == '\n') break;
if (!isTextBlockWhiteSpace(c) || start == rawText.length()) return null;
if (!skipFirstLine && !isTextBlockWhiteSpace(c) || start == rawText.length()) return null;
}
return rawText.substring(start, rawText.length() - 3).split("\n", -1);
}

View File

@@ -7,10 +7,15 @@ import com.intellij.openapi.util.TextRange;
import com.intellij.psi.*;
import com.intellij.psi.impl.source.tree.java.PsiFragmentImpl;
import com.intellij.psi.tree.IElementType;
import com.intellij.psi.util.PsiLiteralUtil;
import com.intellij.util.ObjectUtils;
import org.jetbrains.annotations.NotNull;
import java.util.regex.Pattern;
public final class TextBlockJoinLinesHandler implements JoinRawLinesHandlerDelegate {
private static final Pattern TEXT_BLOCK_START = Pattern.compile("^\"\"\"[ \t\f]*\n", Pattern.MULTILINE);
@Override
public int tryJoinRawLines(@NotNull Document doc, @NotNull PsiFile file, int start, int endWithSpaces) {
CharSequence text = doc.getCharsSequence();
@@ -26,10 +31,14 @@ public final class TextBlockJoinLinesHandler implements JoinRawLinesHandlerDeleg
!tokenType.equals(JavaTokenType.TEXT_BLOCK_TEMPLATE_MID)) {
return CANNOT_JOIN;
}
TextRange tokenRange = token.getTextRange();
int lineNumber = doc.getLineNumber(start);
boolean atStartLine = (tokenType.equals(JavaTokenType.TEXT_BLOCK_LITERAL) || tokenType.equals(JavaTokenType.TEXT_BLOCK_TEMPLATE_BEGIN))
&& lineNumber == doc.getLineNumber(tokenRange.getStartOffset());
boolean atEmptyStartLine = atStartLine && TEXT_BLOCK_START.matcher(token.getText()).find();
boolean singleSlash = false;
if (text.charAt(start) == '\\') {
int lineNumber = doc.getLineNumber(start);
int startOffset = Math.max(token.getTextRange().getStartOffset(), doc.getLineStartOffset(lineNumber));
int startOffset = Math.max(tokenRange.getStartOffset(), doc.getLineStartOffset(lineNumber));
String substring = doc.getText(TextRange.create(startOffset, start)) + "\\\n";
CharSequence parsed = CodeInsightUtilCore.parseStringCharacters(substring, null);
singleSlash = parsed != null && parsed.charAt(parsed.length() - 1) != '\n';
@@ -44,14 +53,51 @@ public final class TextBlockJoinLinesHandler implements JoinRawLinesHandlerDeleg
indent--;
end++;
}
if (singleSlash) {
boolean fromStartTillEnd = atStartLine && tokenType.equals(JavaTokenType.TEXT_BLOCK_LITERAL) &&
doc.getLineNumber(tokenRange.getEndOffset()) == lineNumber + 1 &&
token.getText().endsWith("\"\"\"");
if (singleSlash || atEmptyStartLine) {
doc.deleteString(start, end);
end = start;
} else {
doc.replaceString(start, end, "\\n");
end = start + 2;
}
if (fromStartTillEnd) {
doc.replaceString(tokenRange.getStartOffset(), end + 3,
convertToRegular(doc.getText().substring(tokenRange.getStartOffset(), end + 3)));
}
return start;
}
private static @NotNull String convertToRegular(@NotNull String literal) {
if (literal.length() < 6 || !literal.startsWith("\"\"\"") || !literal.endsWith("\"\"\"")) {
return literal;
}
int end = literal.length() - 3;
StringBuilder sb = null;
for (int i = 3; i < end; i++) {
char ch = literal.charAt(i);
if (ch == '"') {
if (sb == null) {
sb = new StringBuilder(literal.substring(3, i));
}
sb.append("\\\"");
} else if (sb != null) {
sb.append(ch);
}
int nextI = PsiLiteralUtil.parseBackSlash(literal, i);
if (nextI != -1) {
if (sb != null) {
sb.append(literal, i + 1, nextI + 1);
}
//noinspection AssignmentToForLoopParameter
i = nextI;
}
}
return sb == null ? literal.substring(2, end + 1) : '"' + sb.toString() + '"';
}
private static int getNextLineStart(int start, CharSequence text) {
int end = start;
while (text.charAt(end) != '\n') {

View File

@@ -0,0 +1,8 @@
class A {
void test() {
String s = """<caret>
Line1
Line2
""";
}
}

View File

@@ -0,0 +1,8 @@
class A {
void test() {
String s = <selection>"""
Line1
Line2
"""</selection>;
}
}

View File

@@ -0,0 +1,12 @@
class A {
void test() {
String s = <selection>"""
Line1
Line2
Line3\
ContinueLine3
"Quoted"
\040""\"TripleQuoted"\""
"""</selection>;
}
}

View File

@@ -0,0 +1,5 @@
class A {
void test() {
String s = "Line1\nLine2\nLine3ContinueLine3\n\"Quoted\"\n\040\"\"\"TripleQuoted\"\"\"\n";
}
}

View File

@@ -0,0 +1,5 @@
class A {
void test() {
String s = "Line1\nLine2\n";
}
}

View File

@@ -0,0 +1,6 @@
class A {
void test() {
String s = <caret>"""Line1\nLine2
""";
}
}

View File

@@ -0,0 +1,5 @@
class A {
void test() {
String s = "Line1\nLine2\n";
}
}

View File

@@ -0,0 +1,8 @@
class A {
void test() {
String s = """<caret>Line0
Line1
Line2
""";
}
}

View File

@@ -0,0 +1,7 @@
class A {
void test() {
String s = """Line0<caret>\nLine1
Line2
""";
}
}

View File

@@ -0,0 +1,7 @@
class A {
void test() {
String s = """<caret>Line1
Line2
""";
}
}

View File

@@ -2,7 +2,7 @@ class BadTextBlock {
void x() {
String s = <error descr="Illegal text block start: missing new line after opening quotes">"""</error>a
bad bad bad
bad bad bad<warning descr="Trailing whitespace characters inside text block"> </warning>
""";
}
}

View File

@@ -299,6 +299,11 @@ public class JoinLinesTest extends LightJavaCodeInsightTestCase {
public void testCaseLabels3() {doTest();}
public void testJoinTextBlock() {doTest();}
public void testJoinTextBlockAtStartLine() {doTest();}
public void testJoinTextBlockAtStartLineNonEmpty() {doTest();}
public void testJoinTextBlockAtStartLineFinalStep() {doTest();}
public void testJoinTextBlockAtStartLineComplete() {doTest();}
public void testJoinTextBlockAtStartLineCompleteWithEscapes() {doTest();}
public void testJoinTextBlockBackSlash() {doTest();}
public void testJoinTextBlockBackSlash2() {doTest();}
public void testJoinTextBlockBackDoubleSlash() {doTest();}