java text blocks: split text block; ensure no trailing spaces introduced (IDEA-217012)

GitOrigin-RevId: 500fdf647c90dd8877e39cdbef4e88d0b24806eb
This commit is contained in:
Anna Kozlova
2019-07-02 06:52:16 +03:00
committed by intellij-monorepo-bot
parent 0ee7900e3a
commit 37116e8226
11 changed files with 218 additions and 18 deletions
@@ -1685,6 +1685,10 @@
<className>com.intellij.codeInsight.intention.impl.SplitRawStringIntentionAction</className>
<category>Java/Strings</category>
</intentionAction>
<intentionAction>
<className>com.intellij.codeInsight.intention.impl.SplitTextBlockIntentionAction</className>
<category>Java/Strings</category>
</intentionAction>
<intentionAction>
<className>com.intellij.codeInsight.intention.impl.ReplaceCastWithVariableAction</className>
<category>Java/Imports</category>
@@ -0,0 +1,100 @@
// Copyright 2000-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.intellij.codeInsight.intention.impl;
import com.intellij.codeInsight.intention.PsiElementBaseIntentionAction;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.psi.*;
import com.intellij.psi.impl.source.tree.java.PsiLiteralExpressionImpl;
import com.intellij.util.ArrayUtil;
import com.intellij.util.IncorrectOperationException;
import com.siyeh.ig.psiutils.ExpressionUtils;
import org.jetbrains.annotations.NotNull;
import java.util.Objects;
public class SplitTextBlockIntentionAction extends PsiElementBaseIntentionAction {
@Override
public boolean isAvailable(@NotNull Project project, Editor editor, @NotNull PsiElement element) {
if (!(element instanceof PsiJavaToken)) {
return false;
}
final PsiJavaToken token = (PsiJavaToken)element;
if (token.getTokenType() != JavaTokenType.TEXT_BLOCK_LITERAL) {
return false;
}
return token.getText() != null;
}
@Override
public void invoke(@NotNull Project project, Editor editor, @NotNull PsiElement element) throws IncorrectOperationException {
if (!(element instanceof PsiJavaToken)) {
return;
}
final PsiJavaToken token = (PsiJavaToken)element;
if (token.getTokenType() != JavaTokenType.TEXT_BLOCK_LITERAL) {
return;
}
final String text = token.getText();
if (text == null) {
return;
}
int offset = editor.getCaretModel().getOffset();
int splitIdx = offset - token.getTextOffset();
String firstBlock = StringUtil.trimTrailing(text.substring(0, splitIdx));
String lastBlock = StringUtil.trimLeading(text.substring(splitIdx));
int indent = ((PsiLiteralExpressionImpl)element.getParent()).getTextBlockIndent();
int trimmedSpaces = text.length() - firstBlock.length() - lastBlock.length();
int newLineIdx = text.lastIndexOf('\n', splitIdx);
if (StringUtil.isEmptyOrSpaces(text.substring(newLineIdx, splitIdx))) {
trimmedSpaces -= indent + 1;
}
String trailingSpaces = trimmedSpaces > 0 ? " + \"" + StringUtil.repeat(" ", trimmedSpaces) + "\""
: "";
PsiPolyadicExpression replacement = (PsiPolyadicExpression)JavaPsiFacade.getElementFactory(project)
.createExpressionFromText(firstBlock + "\"\"\"" +
trailingSpaces +
" + \"\"\"" + '\n' +
StringUtil.repeat(" ", indent) + lastBlock, element);
PsiPolyadicExpression replacedExpression = (PsiPolyadicExpression)ExpressionUtils.replacePolyadicWithParent((PsiExpression)token.getParent(), replacement);
if (replacedExpression != null) {
PsiElement leftOperand = replacedExpression.findElementAt(splitIdx);
PsiExpression[] operands = replacedExpression.getOperands();
int idx = ArrayUtil.find(operands, leftOperand);
if (idx < operands.length - 1) {
PsiJavaToken tokenBeforeOperand = replacedExpression.getTokenBeforeOperand(operands[idx + 1]);
if (tokenBeforeOperand != null) {
editor.getCaretModel().moveToOffset(tokenBeforeOperand.getTextOffset());
}
}
}
else {
PsiPolyadicExpression replaced = (PsiPolyadicExpression)token.getParent().replace(replacement);
PsiExpression[] operands = replaced.getOperands();
assert operands.length > 1;
editor.getCaretModel().moveToOffset(Objects.requireNonNull(replaced.getTokenBeforeOperand(operands[1])).getTextOffset());
}
}
@NotNull
@Override
public String getText() {
return getFamilyName();
}
@NotNull
@Override
public String getFamilyName() {
return "Split text block";
}
}
@@ -0,0 +1,4 @@
String s = <spot>"""
Hello, """ + """
world!
"""</spot>;
@@ -0,0 +1,3 @@
String s = <spot>"""
Hello, <caret>world!
"""</spot>;
@@ -0,0 +1,5 @@
<html>
<body>
This intention allows to break text block into 2 parts at caret position.
</body>
</html>
@@ -159,25 +159,10 @@ public class PsiLiteralExpressionImpl
}
private String getTextBlockText() {
String rawText = getText();
if (rawText.length() < 7 || !rawText.endsWith("\"\"\"")) return null;
int start = 3;
while (true) {
char c = rawText.charAt(start++);
if (c == '\n') break;
if (!Character.isWhitespace(c) || start == rawText.length()) return null;
}
String innerText = rawText.substring(start, rawText.length() - 3);
String[] lines = StringUtil.splitByLinesDontTrim(innerText);
String[] lines = getTextBlockLines();
if (lines == null) return null;
int prefix = Integer.MAX_VALUE;
for (int i = 0; i < lines.length; i++) {
String line = lines[i];
int indent = 0;
while (indent < line.length() && Character.isWhitespace(line.charAt(indent))) indent++;
if (indent == line.length() && i < lines.length - 1) lines[i] = "";
else if (indent < prefix) prefix = indent;
}
int prefix = getTextBlockIndent(lines);
StringBuilder sb = new StringBuilder();
for (int i = 0; i < lines.length; i++) {
@@ -192,6 +177,37 @@ public class PsiLiteralExpressionImpl
return sb.toString();
}
public int getTextBlockIndent() {
String[] lines = getTextBlockLines();
if (lines == null) return -1;
return getTextBlockIndent(lines);
}
private static int getTextBlockIndent(String[] lines) {
int prefix = Integer.MAX_VALUE;
for (int i = 0; i < lines.length; i++) {
String line = lines[i];
int indent = 0;
while (indent < line.length() && Character.isWhitespace(line.charAt(indent))) indent++;
if (indent == line.length() && i < lines.length - 1) lines[i] = "";
else if (indent < prefix) prefix = indent;
}
return prefix;
}
private String[] getTextBlockLines() {
String rawText = getText();
if (rawText.length() < 7 || !rawText.endsWith("\"\"\"")) return null;
int start = 3;
while (true) {
char c = rawText.charAt(start++);
if (c == '\n') break;
if (!Character.isWhitespace(c) || start == rawText.length()) return null;
}
String innerText = rawText.substring(start, rawText.length() - 3);
return StringUtil.splitByLinesDontTrim(innerText);
}
public String getRawString() {
return StringUtil.nullize(StringUtil.trimLeading(StringUtil.trimTrailing(getCanonicalText(), '`'), '`'));
}
@@ -0,0 +1,7 @@
class MyTest {
String text = """
Lorem
ipsum""" <caret>+ " " + """
dolor sit amet
""";
}
@@ -0,0 +1,6 @@
class MyTest {
String text = """
Lorem
ipsum<caret> dolor sit amet
""";
}
@@ -0,0 +1,6 @@
class MyTest {
String text = """
Lorem ipsum""" <caret>+ " " + """
dolor sit amet
""";
}
@@ -0,0 +1,5 @@
class MyTest {
String text = """
Lorem ipsum <caret>dolor sit amet
""";
}
@@ -0,0 +1,44 @@
// Copyright 2000-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.intellij.java.codeInsight.daemon;
import com.intellij.JavaTestUtil;
import com.intellij.codeInsight.intention.IntentionAction;
import com.intellij.openapi.actionSystem.IdeActions;
import com.intellij.testFramework.LightProjectDescriptor;
import com.intellij.testFramework.fixtures.LightJavaCodeInsightFixtureTestCase;
import org.jetbrains.annotations.NotNull;
import java.util.List;
public class LightAdvTextBlocksTest extends LightJavaCodeInsightFixtureTestCase {
@Override
protected String getBasePath() {
return JavaTestUtil.getRelativeJavaTestDataPath() + "/codeInsight/daemonCodeAnalyzer/advTextBlock";
}
@NotNull
@Override
protected LightProjectDescriptor getProjectDescriptor() {
return JAVA_13;
}
public void testSplitTextBlockOnWhitespace() { doTestSplitIntention(); }
public void testSplitTextBlock() { doTestSplitIntention(); }
private void doTestSplitIntention() {
myFixture.configureByFile(getTestName(false) + ".java");
List<IntentionAction> actions = myFixture.filterAvailableIntentions("Split text block");
assertNotEmpty(actions);
myFixture.launchAction(actions.get(0));
myFixture.checkResultByFile(getTestName(false) + ".after.java");
}
private void performPaste() {
myFixture.performEditorAction(IdeActions.ACTION_EDITOR_PASTE);
}
private void performCopy() {
myFixture.performEditorAction(IdeActions.ACTION_EDITOR_COPY);
}
}