diff --git a/python/python-psi-impl/src/com/jetbrains/python/codeInsight/intentions/PyQuotedStringIntention.java b/python/python-psi-impl/src/com/jetbrains/python/codeInsight/intentions/PyQuotedStringIntention.java index 4bc7621b2367..6d7894b6e25e 100644 --- a/python/python-psi-impl/src/com/jetbrains/python/codeInsight/intentions/PyQuotedStringIntention.java +++ b/python/python-psi-impl/src/com/jetbrains/python/codeInsight/intentions/PyQuotedStringIntention.java @@ -3,18 +3,26 @@ package com.jetbrains.python.codeInsight.intentions; 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.SmartPointerManager; +import com.intellij.psi.SmartPsiElementPointer; import com.intellij.psi.util.PsiTreeUtil; import com.intellij.util.IncorrectOperationException; +import com.intellij.util.containers.ContainerUtil; import com.jetbrains.python.PyPsiBundle; import com.jetbrains.python.psi.*; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import static com.jetbrains.python.psi.PyUtil.as; /** * User: catherine * Intention to convert between single-quoted and double-quoted strings */ public class PyQuotedStringIntention extends PyBaseIntentionAction { + private SmartPsiElementPointer myConversionTarget; @Override @NotNull @@ -28,54 +36,58 @@ public class PyQuotedStringIntention extends PyBaseIntentionAction { return false; } - 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(); - int prefixLength = PyStringLiteralUtil.getPrefixLength(stringText); - stringText = stringText.substring(prefixLength); + PyStringElement stringElement = findConvertibleStringElementUnderCaret(editor, file); + if (stringElement == null) return false; + PyStringLiteralExpression stringLiteral = as(stringElement.getParent(), PyStringLiteralExpression.class); + if (stringLiteral == null) return false; - 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(PyPsiBundle.message("INTN.quoted.string.single.to.double")); - return true; - } - if (stringText.startsWith("\"") && stringText.endsWith("\"")) { - setText(PyPsiBundle.message("INTN.quoted.string.double.to.single")); - return true; - } - } + final PyDocStringOwner docStringOwner = PsiTreeUtil.getParentOfType(stringLiteral, PyDocStringOwner.class); + if (docStringOwner != null && docStringOwner.getDocStringExpression() == stringLiteral) return false; + + String currentQuote = stringElement.getQuote(); + boolean allComponentsCanBeConverted = ContainerUtil.all(stringLiteral.getStringElements(), + s -> s.getQuote().equals(currentQuote) && canBeConverted(s)); + + myConversionTarget = SmartPointerManager.createPointer(allComponentsCanBeConverted ? stringLiteral : stringElement); + + if (currentQuote.equals("'")) { + setText(PyPsiBundle.message("INTN.quoted.string.single.to.double")); } - return false; + else { + setText(PyPsiBundle.message("INTN.quoted.string.double.to.single")); + } + return true; + } + + public @Nullable PyStringElement findConvertibleStringElementUnderCaret(@NotNull Editor editor, @NotNull PsiFile file) { + PsiElement elemUnderCaret = file.findElementAt(editor.getCaretModel().getOffset()); + PyStringElement stringElement = PsiTreeUtil.getParentOfType(elemUnderCaret, PyStringElement.class, false); + return stringElement != null && canBeConverted(stringElement) ? stringElement : null; + } + + public boolean canBeConverted(@NotNull PyStringElement stringElement) { + return !stringElement.isTripleQuoted() && stringElement.isTerminated() && !stringElement.getContentRange().isEmpty(); } @Override public void doInvoke(@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) { - final String stringText = string.getText(); - int prefixLength = PyStringLiteralUtil.getPrefixLength(stringText); - final String text = stringText.substring(prefixLength); - - if (text.startsWith("'") && text.endsWith("'")) { - String result = convertSingleToDoubleQuoted(stringText); - PyStringLiteralExpression st = elementGenerator.createStringLiteralAlreadyEscaped(result); - string.replace(st); - } - if (text.startsWith("\"") && text.endsWith("\"")) { - String result = convertDoubleToSingleQuoted(stringText); - PyStringLiteralExpression st = elementGenerator.createStringLiteralAlreadyEscaped(result); - string.replace(st); - } + @Nullable PsiElement target = myConversionTarget.getElement(); + if (target instanceof PyStringLiteralExpression) { + ((PyStringLiteralExpression)target).getStringElements().forEach(this::convertStringElement); } + else if (target instanceof PyStringElement) { + convertStringElement((PyStringElement)target); + } + } + + public void convertStringElement(@NotNull PyStringElement stringElement) { + Project project = stringElement.getProject(); + String result = stringElement.getQuote().equals("'") + ? convertSingleToDoubleQuoted(stringElement.getText()) + : convertDoubleToSingleQuoted(stringElement.getText()); + PyElementGenerator elementGenerator = PyElementGenerator.getInstance(project); + PyStringLiteralExpression st = elementGenerator.createStringLiteralAlreadyEscaped(result); + stringElement.replace(st.getFirstChild()); } private static String convertDoubleToSingleQuoted(String stringText) { @@ -95,9 +107,9 @@ public class PyQuotedStringIntention extends PyBaseIntentionAction { else if (ch == '\'') { stringBuilder.append("\\'"); } - else if (ch == '\\' && charArr[i+1] == '\"' && !(i+2 == charArr.length)) { + else if (ch == '\\' && charArr[i + 1] == '\"' && !(i + 2 == charArr.length)) { skipNext = true; - stringBuilder.append(charArr[i+1]); + stringBuilder.append(charArr[i + 1]); } else { stringBuilder.append(ch); @@ -123,9 +135,9 @@ public class PyQuotedStringIntention extends PyBaseIntentionAction { else if (ch == '"') { stringBuilder.append("\\\""); } - else if (ch == '\\' && charArr[i+1] == '\'' && !(i+2 == charArr.length)) { + else if (ch == '\\' && charArr[i + 1] == '\'' && !(i + 2 == charArr.length)) { skipNext = true; - stringBuilder.append(charArr[i+1]); + stringBuilder.append(charArr[i + 1]); } else { stringBuilder.append(ch); diff --git a/python/testData/intentions/convertingQuotesOfGluedStringWithDifferentElementQuotes.py b/python/testData/intentions/convertingQuotesOfGluedStringWithDifferentElementQuotes.py new file mode 100644 index 000000000000..5f17ebdb0c90 --- /dev/null +++ b/python/testData/intentions/convertingQuotesOfGluedStringWithDifferentElementQuotes.py @@ -0,0 +1,3 @@ +s = ('foo' + "bar" + 'baz') diff --git a/python/testData/intentions/convertingQuotesOfGluedStringWithDifferentElementQuotes_after.py b/python/testData/intentions/convertingQuotesOfGluedStringWithDifferentElementQuotes_after.py new file mode 100644 index 000000000000..3c9164576b95 --- /dev/null +++ b/python/testData/intentions/convertingQuotesOfGluedStringWithDifferentElementQuotes_after.py @@ -0,0 +1,3 @@ +s = ("foo" + "bar" + 'baz') diff --git a/python/testSrc/com/jetbrains/python/intentions/PyIntentionTest.java b/python/testSrc/com/jetbrains/python/intentions/PyIntentionTest.java index 4ed66d9aa36b..bf5cec0a23af 100644 --- a/python/testSrc/com/jetbrains/python/intentions/PyIntentionTest.java +++ b/python/testSrc/com/jetbrains/python/intentions/PyIntentionTest.java @@ -206,6 +206,11 @@ public class PyIntentionTest extends PyTestCase { doTest(PyPsiBundle.message("INTN.quoted.string.double.to.single")); } + // PY-15608 + public void testConvertingQuotesOfGluedStringWithDifferentElementQuotes() { + doTest(PyPsiBundle.message("INTN.quoted.string.single.to.double")); + } + public void testConvertLambdaToFunction() { doTest(PyPsiBundle.message("INTN.convert.lambda.to.function")); }