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"));
+ }
}