From 37116e8226b188a3fc16d6089134c6738dfa5e42 Mon Sep 17 00:00:00 2001 From: Anna Kozlova Date: Wed, 26 Jun 2019 14:02:41 +0200 Subject: [PATCH] java text blocks: split text block; ensure no trailing spaces introduced (IDEA-217012) GitOrigin-RevId: 500fdf647c90dd8877e39cdbef4e88d0b24806eb --- java/java-impl/src/META-INF/JavaPlugin.xml | 4 + .../impl/SplitTextBlockIntentionAction.java | 100 ++++++++++++++++++ .../after.java.template | 4 + .../before.java.template | 3 + .../description.html | 5 + .../tree/java/PsiLiteralExpressionImpl.java | 52 +++++---- .../advTextBlock/SplitTextBlock.after.java | 7 ++ .../advTextBlock/SplitTextBlock.java | 6 ++ .../SplitTextBlockOnWhitespace.after.java | 6 ++ .../SplitTextBlockOnWhitespace.java | 5 + .../daemon/LightAdvTextBlocksTest.java | 44 ++++++++ 11 files changed, 218 insertions(+), 18 deletions(-) create mode 100644 java/java-impl/src/com/intellij/codeInsight/intention/impl/SplitTextBlockIntentionAction.java create mode 100644 java/java-impl/src/intentionDescriptions/SplitTextBlockIntentionAction/after.java.template create mode 100644 java/java-impl/src/intentionDescriptions/SplitTextBlockIntentionAction/before.java.template create mode 100644 java/java-impl/src/intentionDescriptions/SplitTextBlockIntentionAction/description.html create mode 100644 java/java-tests/testData/codeInsight/daemonCodeAnalyzer/advTextBlock/SplitTextBlock.after.java create mode 100644 java/java-tests/testData/codeInsight/daemonCodeAnalyzer/advTextBlock/SplitTextBlock.java create mode 100644 java/java-tests/testData/codeInsight/daemonCodeAnalyzer/advTextBlock/SplitTextBlockOnWhitespace.after.java create mode 100644 java/java-tests/testData/codeInsight/daemonCodeAnalyzer/advTextBlock/SplitTextBlockOnWhitespace.java create mode 100644 java/java-tests/testSrc/com/intellij/java/codeInsight/daemon/LightAdvTextBlocksTest.java diff --git a/java/java-impl/src/META-INF/JavaPlugin.xml b/java/java-impl/src/META-INF/JavaPlugin.xml index 4b0745ff20b0..a183fc9b0aad 100644 --- a/java/java-impl/src/META-INF/JavaPlugin.xml +++ b/java/java-impl/src/META-INF/JavaPlugin.xml @@ -1685,6 +1685,10 @@ com.intellij.codeInsight.intention.impl.SplitRawStringIntentionAction Java/Strings + + com.intellij.codeInsight.intention.impl.SplitTextBlockIntentionAction + Java/Strings + com.intellij.codeInsight.intention.impl.ReplaceCastWithVariableAction Java/Imports diff --git a/java/java-impl/src/com/intellij/codeInsight/intention/impl/SplitTextBlockIntentionAction.java b/java/java-impl/src/com/intellij/codeInsight/intention/impl/SplitTextBlockIntentionAction.java new file mode 100644 index 000000000000..5474cc45781c --- /dev/null +++ b/java/java-impl/src/com/intellij/codeInsight/intention/impl/SplitTextBlockIntentionAction.java @@ -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"; + } +} diff --git a/java/java-impl/src/intentionDescriptions/SplitTextBlockIntentionAction/after.java.template b/java/java-impl/src/intentionDescriptions/SplitTextBlockIntentionAction/after.java.template new file mode 100644 index 000000000000..025dd57a4cc5 --- /dev/null +++ b/java/java-impl/src/intentionDescriptions/SplitTextBlockIntentionAction/after.java.template @@ -0,0 +1,4 @@ +String s = """ + Hello, """ + """ + world! + """; \ No newline at end of file diff --git a/java/java-impl/src/intentionDescriptions/SplitTextBlockIntentionAction/before.java.template b/java/java-impl/src/intentionDescriptions/SplitTextBlockIntentionAction/before.java.template new file mode 100644 index 000000000000..7aeaf63b8b3e --- /dev/null +++ b/java/java-impl/src/intentionDescriptions/SplitTextBlockIntentionAction/before.java.template @@ -0,0 +1,3 @@ +String s = """ + Hello, world! + """; \ No newline at end of file diff --git a/java/java-impl/src/intentionDescriptions/SplitTextBlockIntentionAction/description.html b/java/java-impl/src/intentionDescriptions/SplitTextBlockIntentionAction/description.html new file mode 100644 index 000000000000..ee7873740830 --- /dev/null +++ b/java/java-impl/src/intentionDescriptions/SplitTextBlockIntentionAction/description.html @@ -0,0 +1,5 @@ + + +This intention allows to break text block into 2 parts at caret position. + + diff --git a/java/java-psi-impl/src/com/intellij/psi/impl/source/tree/java/PsiLiteralExpressionImpl.java b/java/java-psi-impl/src/com/intellij/psi/impl/source/tree/java/PsiLiteralExpressionImpl.java index 9b8805dceb0d..2a35ef4b4c66 100644 --- a/java/java-psi-impl/src/com/intellij/psi/impl/source/tree/java/PsiLiteralExpressionImpl.java +++ b/java/java-psi-impl/src/com/intellij/psi/impl/source/tree/java/PsiLiteralExpressionImpl.java @@ -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(), '`'), '`')); } diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/advTextBlock/SplitTextBlock.after.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/advTextBlock/SplitTextBlock.after.java new file mode 100644 index 000000000000..f89511134e96 --- /dev/null +++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/advTextBlock/SplitTextBlock.after.java @@ -0,0 +1,7 @@ +class MyTest { + String text = """ + Lorem + ipsum""" + " " + """ + dolor sit amet + """; +} \ No newline at end of file diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/advTextBlock/SplitTextBlock.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/advTextBlock/SplitTextBlock.java new file mode 100644 index 000000000000..d43001f8d78f --- /dev/null +++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/advTextBlock/SplitTextBlock.java @@ -0,0 +1,6 @@ +class MyTest { + String text = """ + Lorem + ipsum dolor sit amet + """; +} \ No newline at end of file diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/advTextBlock/SplitTextBlockOnWhitespace.after.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/advTextBlock/SplitTextBlockOnWhitespace.after.java new file mode 100644 index 000000000000..73d56597eced --- /dev/null +++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/advTextBlock/SplitTextBlockOnWhitespace.after.java @@ -0,0 +1,6 @@ +class MyTest { + String text = """ + Lorem ipsum""" + " " + """ + dolor sit amet + """; +} \ No newline at end of file diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/advTextBlock/SplitTextBlockOnWhitespace.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/advTextBlock/SplitTextBlockOnWhitespace.java new file mode 100644 index 000000000000..f9500b98a34c --- /dev/null +++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/advTextBlock/SplitTextBlockOnWhitespace.java @@ -0,0 +1,5 @@ +class MyTest { + String text = """ + Lorem ipsum dolor sit amet + """; +} \ No newline at end of file diff --git a/java/java-tests/testSrc/com/intellij/java/codeInsight/daemon/LightAdvTextBlocksTest.java b/java/java-tests/testSrc/com/intellij/java/codeInsight/daemon/LightAdvTextBlocksTest.java new file mode 100644 index 000000000000..b7e1a3aa4861 --- /dev/null +++ b/java/java-tests/testSrc/com/intellij/java/codeInsight/daemon/LightAdvTextBlocksTest.java @@ -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 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); + } +} \ No newline at end of file