diff --git a/python/src/META-INF/python-plugin-common.xml b/python/src/META-INF/python-plugin-common.xml index e96104e2fdce..816b7bb09d77 100644 --- a/python/src/META-INF/python-plugin-common.xml +++ b/python/src/META-INF/python-plugin-common.xml @@ -138,6 +138,11 @@ Python + + com.jetbrains.python.codeInsight.intentions.PyQuotedStringIntention + Python + + diff --git a/python/src/com/jetbrains/python/PyBundle.properties b/python/src/com/jetbrains/python/PyBundle.properties index 7fb2f7ddd794..2060bdeb2944 100644 --- a/python/src/com/jetbrains/python/PyBundle.properties +++ b/python/src/com/jetbrains/python/PyBundle.properties @@ -129,6 +129,10 @@ INTN.convert.dict.constructor.to.dict.literal=Convert dict constructor to dict l INTN.convert.dict.literal.to.dict.constructor=Convert dict literal to dict constructor INTN.convert.dict=Convert dict back and forth between literal dict and dict(**kwargs) +INTN.quoted.string=Convert between single-quoted and double-quoted strings +INTN.quoted.string.single.to.double=Convert single-quoted string to double-quoted string +INTN.quoted.string.double.to.single=Convert double-quoted string to sungle-quoted string + # Conflict checker CONFLICT.name.$0.obscured=Name ''{0}'' obscured by local definitions CONFLICT.name.$0.obscured.cannot.convert=Name ''{0}'' obscured. Cannot convert. diff --git a/python/src/com/jetbrains/python/codeInsight/intentions/PyDictConstructorIntention.java b/python/src/com/jetbrains/python/codeInsight/intentions/PyDictConstructorIntention.java index a73f31bf8e5b..92f2bd40d238 100644 --- a/python/src/com/jetbrains/python/codeInsight/intentions/PyDictConstructorIntention.java +++ b/python/src/com/jetbrains/python/codeInsight/intentions/PyDictConstructorIntention.java @@ -4,9 +4,6 @@ import com.intellij.codeInsight.intention.impl.BaseIntentionAction; import com.intellij.openapi.editor.Editor; import com.intellij.openapi.project.Project; import com.intellij.psi.PsiFile; -import com.intellij.psi.PsiPolyVariantReference; -import com.intellij.psi.PsiReference; -import com.intellij.psi.ResolveResult; import com.intellij.psi.util.PsiTreeUtil; import com.intellij.util.IncorrectOperationException; import com.jetbrains.python.PyBundle; @@ -14,7 +11,6 @@ import com.jetbrains.python.psi.*; import com.jetbrains.python.psi.types.PyType; import com.jetbrains.python.psi.types.TypeEvalContext; import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; /** * User: catherine diff --git a/python/src/com/jetbrains/python/codeInsight/intentions/PyQuotedStringIntention.java b/python/src/com/jetbrains/python/codeInsight/intentions/PyQuotedStringIntention.java new file mode 100644 index 000000000000..8c67574df525 --- /dev/null +++ b/python/src/com/jetbrains/python/codeInsight/intentions/PyQuotedStringIntention.java @@ -0,0 +1,106 @@ +package com.jetbrains.python.codeInsight.intentions; + +import com.intellij.codeInsight.intention.impl.BaseIntentionAction; +import com.intellij.openapi.editor.Editor; +import com.intellij.openapi.project.Project; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiFile; +import com.intellij.psi.util.PsiTreeUtil; +import com.intellij.util.IncorrectOperationException; +import com.jetbrains.python.PyBundle; +import com.jetbrains.python.PyTokenTypes; +import com.jetbrains.python.psi.*; +import com.jetbrains.python.psi.types.PyType; +import com.jetbrains.python.psi.types.TypeEvalContext; +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +/** + * User: catherine + */ +public class PyQuotedStringIntention extends BaseIntentionAction { + + @NotNull + public String getFamilyName() { + return PyBundle.message("INTN.quoted.string"); + } + + public boolean isAvailable(@NotNull Project project, Editor editor, PsiFile file) { + PyStringLiteralExpression string = PsiTreeUtil.getParentOfType(file.findElementAt(editor.getCaretModel().getOffset()), PyStringLiteralExpression.class); + if (string != null) { + final PyDocStringOwner docStringOwner = PsiTreeUtil.getParentOfType(string, PyDocStringOwner.class); + if (docStringOwner != null) { + if (docStringOwner.getDocStringExpression() == string) return false; + } + String stringText = string.getText(); + if (stringText.length() >= 6) { + if (stringText.startsWith("'''") && stringText.endsWith("'''") || + stringText.startsWith("\"\"\"") && stringText.endsWith("\"\"\"")) return false; + } + if (stringText.length() > 2) { + if (stringText.startsWith("'") && stringText.endsWith("'")) { + setText(PyBundle.message("INTN.quoted.string.single.to.double")); + return true; + } + if (stringText.startsWith("\"") && stringText.endsWith("\"")) { + setText(PyBundle.message("INTN.quoted.string.double.to.single")); + return true; + } + } + } + return false; + } + + public void invoke(@NotNull Project project, Editor editor, PsiFile file) throws IncorrectOperationException { + PyStringLiteralExpression string = PsiTreeUtil.getParentOfType(file.findElementAt(editor.getCaretModel().getOffset()), PyStringLiteralExpression.class); + PyElementGenerator elementGenerator = PyElementGenerator.getInstance(project); + if (string != null) { + String stringText = string.getText(); + if (stringText.startsWith("'") && stringText.endsWith("'")) { + String result = convertSingleToDoubleQuoted(stringText); + PyStringLiteralExpression st = elementGenerator.createStringLiteralAlreadyEscaped(result); + string.replace(st); + } + if (stringText.startsWith("\"") && stringText.endsWith("\"")) { + String result = convertDoubleToSingleQuoted(stringText); + PyStringLiteralExpression st = elementGenerator.createStringLiteralAlreadyEscaped(result); + string.replace(st); + } + } + } + + private static String convertDoubleToSingleQuoted(String stringText) { + StringBuilder stringBuilder = new StringBuilder(); + for (char ch : stringText.toCharArray()) { + if (ch == '"') { + stringBuilder.append('\''); + } + else if (ch == '\'') { + stringBuilder.append("\\\'"); + } + else { + stringBuilder.append(ch); + } + } + return stringBuilder.toString(); + } + + private static String convertSingleToDoubleQuoted(String stringText) { + StringBuilder stringBuilder = new StringBuilder(); + for (char ch : stringText.toCharArray()) { + if (ch == '\'') { + stringBuilder.append('"'); + } + else if (ch == '"') { + stringBuilder.append("\\\""); + } + else { + stringBuilder.append(ch); + } + } + return stringBuilder.toString(); + } +} diff --git a/python/testData/intentions/afterQuotedString.py b/python/testData/intentions/afterQuotedString.py new file mode 100644 index 000000000000..895ef0f6d0cd --- /dev/null +++ b/python/testData/intentions/afterQuotedString.py @@ -0,0 +1 @@ +a = '\'Hello\' - said man.' \ No newline at end of file diff --git a/python/testData/intentions/beforeQuotedString.py b/python/testData/intentions/beforeQuotedString.py new file mode 100644 index 000000000000..08104a0704cb --- /dev/null +++ b/python/testData/intentions/beforeQuotedString.py @@ -0,0 +1 @@ +a = "'Hello' - said man." \ No newline at end of file diff --git a/python/testSrc/com/jetbrains/python/PyIntentionTest.java b/python/testSrc/com/jetbrains/python/PyIntentionTest.java index ac57562a1604..75b53fcd7970 100644 --- a/python/testSrc/com/jetbrains/python/PyIntentionTest.java +++ b/python/testSrc/com/jetbrains/python/PyIntentionTest.java @@ -116,4 +116,8 @@ public class PyIntentionTest extends PyLightFixtureTestCase { public void testDictConstructor() { doTest(PyBundle.message("INTN.convert.dict.constructor.to.dict.literal")); } + + public void testQuotedString() { + doTest(PyBundle.message("INTN.quoted.string.double.to.single")); + } }