diff --git a/plugins/groovy/resources/intentionDescriptions/ConvertStringToMultilineIntention/after.groovy.template b/plugins/groovy/resources/intentionDescriptions/ConvertStringToMultilineIntention/after.groovy.template new file mode 100644 index 000000000000..d39ed06ecad8 --- /dev/null +++ b/plugins/groovy/resources/intentionDescriptions/ConvertStringToMultilineIntention/after.groovy.template @@ -0,0 +1,2 @@ +print '''first + second''' \ No newline at end of file diff --git a/plugins/groovy/resources/intentionDescriptions/ConvertStringToMultilineIntention/before.groovy.template b/plugins/groovy/resources/intentionDescriptions/ConvertStringToMultilineIntention/before.groovy.template new file mode 100644 index 000000000000..588872062d48 --- /dev/null +++ b/plugins/groovy/resources/intentionDescriptions/ConvertStringToMultilineIntention/before.groovy.template @@ -0,0 +1 @@ +print 'first\n second' \ No newline at end of file diff --git a/plugins/groovy/resources/intentionDescriptions/ConvertStringToMultilineIntention/description.html b/plugins/groovy/resources/intentionDescriptions/ConvertStringToMultilineIntention/description.html new file mode 100644 index 000000000000..5bc803413371 --- /dev/null +++ b/plugins/groovy/resources/intentionDescriptions/ConvertStringToMultilineIntention/description.html @@ -0,0 +1,5 @@ + + +This intention converts singleline string literal to multiline literal. + + \ No newline at end of file diff --git a/plugins/groovy/src/META-INF/plugin.xml b/plugins/groovy/src/META-INF/plugin.xml index 65d5a32f4603..84ec2b08e529 100644 --- a/plugins/groovy/src/META-INF/plugin.xml +++ b/plugins/groovy/src/META-INF/plugin.xml @@ -1106,6 +1106,11 @@ intention.category.groovy/intention.category.conversions org.jetbrains.plugins.groovy.intentions.conversions.ConvertMethodToClosureIntention + + org.jetbrains.plugins.groovy.intentions.GroovyIntentionsBundle + intention.category.groovy/intention.category.conversions + org.jetbrains.plugins.groovy.intentions.conversions.ConvertStringToMultilineIntention + org.jetbrains.plugins.groovy.intentions.GroovyIntentionsBundle intention.category.groovy/intention.category.conversions diff --git a/plugins/groovy/src/org/jetbrains/plugins/groovy/intentions/GroovyIntentionsBundle.properties b/plugins/groovy/src/org/jetbrains/plugins/groovy/intentions/GroovyIntentionsBundle.properties index 41a0e471bd6c..6bcd52cb3ec8 100644 --- a/plugins/groovy/src/org/jetbrains/plugins/groovy/intentions/GroovyIntentionsBundle.properties +++ b/plugins/groovy/src/org/jetbrains/plugins/groovy/intentions/GroovyIntentionsBundle.properties @@ -81,6 +81,8 @@ convert.closure.to.method.intention.name=Convert to method convert.closure.to.method.intention.family.name=Convert property to method convert.method.to.closure.intention.name=Convert to property convert.method.to.closure.intention.family.name=Convert method to property of closure type +convert.string.to.multiline.intention.name=Convert to Multiline +convert.string.to.multiline.intention.family.name=Convert literal to multiline literal convert.junit.assertion.to.assert.statement.intention.name=Convert JUnit assertion to assert statement convert.junit.assertion.to.assert.statement.intention.family.name=Convert JUnit assertion to assert statement ########################################################################################################### diff --git a/plugins/groovy/src/org/jetbrains/plugins/groovy/intentions/conversions/ConvertStringToMultilineIntention.java b/plugins/groovy/src/org/jetbrains/plugins/groovy/intentions/conversions/ConvertStringToMultilineIntention.java new file mode 100644 index 000000000000..62b5b8d43e03 --- /dev/null +++ b/plugins/groovy/src/org/jetbrains/plugins/groovy/intentions/conversions/ConvertStringToMultilineIntention.java @@ -0,0 +1,135 @@ +/* + * Copyright 2000-2012 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jetbrains.plugins.groovy.intentions.conversions; + +import com.intellij.lang.ASTNode; +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.editor.Editor; +import com.intellij.openapi.project.Project; +import com.intellij.psi.PsiElement; +import com.intellij.psi.tree.IElementType; +import com.intellij.psi.util.PsiTreeUtil; +import com.intellij.util.IncorrectOperationException; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.plugins.groovy.intentions.base.Intention; +import org.jetbrains.plugins.groovy.intentions.base.PsiElementPredicate; +import org.jetbrains.plugins.groovy.lang.editor.actions.GroovyEditorActionUtil; +import org.jetbrains.plugins.groovy.lang.lexer.GroovyTokenTypes; +import org.jetbrains.plugins.groovy.lang.parser.GroovyElementTypes; +import org.jetbrains.plugins.groovy.lang.psi.GroovyFile; +import org.jetbrains.plugins.groovy.lang.psi.GroovyPsiElementFactory; +import org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.GrExpression; +import org.jetbrains.plugins.groovy.lang.psi.api.statements.typedef.members.GrMember; +import org.jetbrains.plugins.groovy.lang.psi.impl.statements.expressions.literals.GrLiteralImpl; +import org.jetbrains.plugins.groovy.lang.psi.impl.statements.expressions.literals.GrStringImpl; +import org.jetbrains.plugins.groovy.lang.psi.util.GrStringUtil; + +/** + * @author Max Medvedev + */ +public class ConvertStringToMultilineIntention extends Intention { + private static final Logger LOG = Logger.getInstance(ConvertStringToMultilineIntention.class); + + public static final String hint = "Convert to Multiline"; + + @Override + protected void processIntention(@NotNull PsiElement element, Project project, Editor editor) throws IncorrectOperationException { + String quote = element.getText().startsWith("'") ? "'''" : "\"\"\""; + + StringBuilder buffer = new StringBuilder(); + buffer.append(quote); + + GrExpression old; + + if (element instanceof GrLiteralImpl) { + appendSimpleStringValue(element, buffer, quote); + old = (GrExpression)element; + } + else { + final GrStringImpl gstring = PsiTreeUtil.getParentOfType(element, GrStringImpl.class); + for (ASTNode child = gstring.getNode().getFirstChildNode(); child != null; child = child.getTreeNext()) { + if (child.getElementType() == GroovyTokenTypes.mGSTRING_CONTENT) { + appendGStringContent(child, buffer); + } + else if (child.getElementType() == GroovyElementTypes.GSTRING_INJECTION) { + buffer.append(child.getText()); + } + } + old = gstring; + } + + buffer.append(quote); + try { + final GrExpression newLiteral = GroovyPsiElementFactory.getInstance(project).createExpressionFromText(buffer.toString()); + old.replaceWithExpression(newLiteral, true); + } + catch (IncorrectOperationException e) { + LOG.error(e); + } + } + + private static void appendGStringContent(ASTNode child, StringBuilder buffer) { + final String text = child.getText(); + final StringBuilder parsed = new StringBuilder(); + if (GrStringUtil.parseStringCharacters(text, parsed, null)) { + buffer.append(parsed); + } + else { + buffer.append(text); + } + } + + private static void appendSimpleStringValue(PsiElement element, StringBuilder buffer, String quote) { + final Object value = ((GrLiteralImpl)element).getValue(); + if (value instanceof String) { + if ("'''".equals(quote)) { + GrStringUtil.escapeStringCharacters(((String)value).length(), (String)value, "", false, true, buffer); + } + else { + GrStringUtil.escapeSymbolsForGString((CharSequence)value, false, false, buffer); + } + } + else { + buffer.append(GrStringUtil.removeQuotes(element.getText())); + } + } + + @NotNull + @Override + protected PsiElementPredicate getElementPredicate() { + return new PsiElementPredicate() { + @Override + public boolean satisfiedBy(PsiElement element) { + if (element instanceof GrLiteralImpl) { + final ASTNode node = element.getFirstChild().getNode(); + final IElementType type = node.getElementType(); + if (type == GroovyTokenTypes.mSTRING_LITERAL) { + return GroovyEditorActionUtil.isPlainStringLiteral(node); + } + if (type == GroovyTokenTypes.mGSTRING_LITERAL) { + return GroovyEditorActionUtil.isPlainGString(node); + } + } + else { + final GrStringImpl gstring = PsiTreeUtil.getParentOfType(element, GrStringImpl.class, false, GrMember.class, GroovyFile.class); + if (gstring == null) return false; + return gstring.isPlainString(); + } + return false; + } + }; + } +} diff --git a/plugins/groovy/src/org/jetbrains/plugins/groovy/lang/psi/util/GrStringUtil.java b/plugins/groovy/src/org/jetbrains/plugins/groovy/lang/psi/util/GrStringUtil.java index 7e942d05510d..8def3caf30ad 100644 --- a/plugins/groovy/src/org/jetbrains/plugins/groovy/lang/psi/util/GrStringUtil.java +++ b/plugins/groovy/src/org/jetbrains/plugins/groovy/lang/psi/util/GrStringUtil.java @@ -248,7 +248,7 @@ public class GrStringUtil { return b.toString(); } - private static void escapeSymbolsForGString(CharSequence s, boolean isSingleLine, boolean forInjection, StringBuilder b) { + public static void escapeSymbolsForGString(CharSequence s, boolean isSingleLine, boolean forInjection, StringBuilder b) { escapeStringCharacters(s.length(), s, isSingleLine ? "$\"" : "$", isSingleLine, true, b); if (!forInjection) { unescapeCharacters(b, isSingleLine ? "'" : "'\"", true); @@ -828,4 +828,28 @@ public class GrStringUtil { return false; } + + public static void getOperandText(@NotNull GrLiteral operand, @NotNull StringBuilder builder) { + if (operand instanceof GrRegex) { + StringBuilder b = new StringBuilder(); + parseRegexCharacters(removeQuotes(operand.getText()), b, null, operand.getText().startsWith("/")); + escapeSymbolsForGString(b, false, false); + } + else if (operand instanceof GrString) { + builder.append(removeQuotes(operand.getText())); + } + else { + Object value = operand.getValue(); + if (value == null) { + value = removeQuotes(operand.getText()); + } + + String text = value.toString(); + StringBuilder buffer = new StringBuilder(text.length()); + boolean containsLineFeeds = text.indexOf('\n') >= 0 || text.indexOf('\r') >= 0; + escapeStringCharacters(text.length(), text, "$", false, true, buffer); + unescapeCharacters(buffer, containsLineFeeds?"'\"":"'", containsLineFeeds); + builder.append(buffer); + } + } } diff --git a/plugins/groovy/test/org/jetbrains/plugins/groovy/intentions/ConvertStringToMultilineTest.groovy b/plugins/groovy/test/org/jetbrains/plugins/groovy/intentions/ConvertStringToMultilineTest.groovy new file mode 100644 index 000000000000..144dc9884b40 --- /dev/null +++ b/plugins/groovy/test/org/jetbrains/plugins/groovy/intentions/ConvertStringToMultilineTest.groovy @@ -0,0 +1,74 @@ +/* + * Copyright 2000-2012 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jetbrains.plugins.groovy.intentions + +import com.intellij.openapi.application.WriteAction +import org.jetbrains.annotations.Nullable +import org.jetbrains.plugins.groovy.LightGroovyTestCase +import org.jetbrains.plugins.groovy.intentions.conversions.ConvertStringToMultilineIntention + +/** + * @author Max Medvedev + */ +public class ConvertStringToMultilineTest extends LightGroovyTestCase { + @Override + protected String getBasePath() {''} + + void testPlainString() { + doTest("print 'abc'", "print '''abc'''") + } + + void testGString() { + doTest('print "abc"', 'print """abc"""') + } + + void testPlainString2() { + doTest("print 'a\\nbc'", "print '''a\nbc'''") + } + + void testGString2() { + doTest('print "a\\nbc"', 'print """a\nbc"""') + } + + void testGString3() { + doTest('print "a\\nb${a}c"', 'print """a\nb${a}c"""') + } + + void testAlreadyMultiline() { + doTest('print """abc"""', null) + } + + private doTest(String before, @Nullable String after) { + myFixture.with { + configureByText('_.groovy', before) + def intentions = filterAvailableIntentions(ConvertStringToMultilineIntention.hint) + if (!after) { + assertEmpty(intentions) + return + } + + assertOneElement(intentions) + def accessToken = WriteAction.start() + try { + intentions[0].invoke(project, editor, file) + } + finally { + accessToken.finish() + } + checkResult(after) + } + } +} diff --git a/plugins/groovy/test/org/jetbrains/plugins/groovy/intentions/CreateFieldFromParameterTest.groovy b/plugins/groovy/test/org/jetbrains/plugins/groovy/intentions/CreateFieldFromParameterTest.groovy index 516ae9c842a7..8156066ce4b5 100644 --- a/plugins/groovy/test/org/jetbrains/plugins/groovy/intentions/CreateFieldFromParameterTest.groovy +++ b/plugins/groovy/test/org/jetbrains/plugins/groovy/intentions/CreateFieldFromParameterTest.groovy @@ -51,7 +51,7 @@ class CreateFieldFromParameterTest extends LightCodeInsightFixtureTestCase { void testTypeParameter() {doTest()} private void doTest() { - myFixture.configureByFile("before${getTestName(true)}.groovy") + myFixture.configureByFile("before${getTestName(false)}.groovy") def intentions = myFixture.availableIntentions for (intention in intentions) { if (intention instanceof IntentionActionWrapper) intention = intention.delegate @@ -61,6 +61,6 @@ class CreateFieldFromParameterTest extends LightCodeInsightFixtureTestCase { break } } - myFixture.checkResultByFile("after${getTestName(true)}.groovy") + myFixture.checkResultByFile("after${getTestName(false)}.groovy") } }