From 7affe68244725c40daf91eb01848e0bfbd9c11e4 Mon Sep 17 00:00:00 2001 From: Mikhail Golubev Date: Sun, 9 Aug 2020 18:47:22 +0300 Subject: [PATCH] PY-15608 Handle converting quotes on "glued" string literal with varying quotes Namely, if such composed literal has nodes with different quotes, convert only the one directly under the caret. This way we both preserve the old behavior, when quotes of all individual strings of the literal were converted, and allow to convert quotes of at least one element if that is not possible. I've also migrated the intention to newer PyStringElement API that appeared with the new f-string support. GitOrigin-RevId: a8592a46e2991b172a205f041cbc3b668e242e8a --- .../intentions/PyQuotedStringIntention.java | 102 ++++++++++-------- ...OfGluedStringWithDifferentElementQuotes.py | 3 + ...dStringWithDifferentElementQuotes_after.py | 3 + .../python/intentions/PyIntentionTest.java | 5 + 4 files changed, 68 insertions(+), 45 deletions(-) create mode 100644 python/testData/intentions/convertingQuotesOfGluedStringWithDifferentElementQuotes.py create mode 100644 python/testData/intentions/convertingQuotesOfGluedStringWithDifferentElementQuotes_after.py 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")); }