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
This commit is contained in:
Mikhail Golubev
2020-08-09 18:47:22 +03:00
committed by intellij-monorepo-bot
parent f001a2eb53
commit 7affe68244
4 changed files with 68 additions and 45 deletions

View File

@@ -3,18 +3,26 @@ package com.jetbrains.python.codeInsight.intentions;
import com.intellij.openapi.editor.Editor; import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.project.Project; import com.intellij.openapi.project.Project;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile; import com.intellij.psi.PsiFile;
import com.intellij.psi.SmartPointerManager;
import com.intellij.psi.SmartPsiElementPointer;
import com.intellij.psi.util.PsiTreeUtil; import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.util.IncorrectOperationException; import com.intellij.util.IncorrectOperationException;
import com.intellij.util.containers.ContainerUtil;
import com.jetbrains.python.PyPsiBundle; import com.jetbrains.python.PyPsiBundle;
import com.jetbrains.python.psi.*; import com.jetbrains.python.psi.*;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import static com.jetbrains.python.psi.PyUtil.as;
/** /**
* User: catherine * User: catherine
* Intention to convert between single-quoted and double-quoted strings * Intention to convert between single-quoted and double-quoted strings
*/ */
public class PyQuotedStringIntention extends PyBaseIntentionAction { public class PyQuotedStringIntention extends PyBaseIntentionAction {
private SmartPsiElementPointer<PsiElement> myConversionTarget;
@Override @Override
@NotNull @NotNull
@@ -28,54 +36,58 @@ public class PyQuotedStringIntention extends PyBaseIntentionAction {
return false; return false;
} }
PyStringLiteralExpression string = PsiTreeUtil.getParentOfType(file.findElementAt(editor.getCaretModel().getOffset()), PyStringLiteralExpression.class); PyStringElement stringElement = findConvertibleStringElementUnderCaret(editor, file);
if (string != null) { if (stringElement == null) return false;
final PyDocStringOwner docStringOwner = PsiTreeUtil.getParentOfType(string, PyDocStringOwner.class); PyStringLiteralExpression stringLiteral = as(stringElement.getParent(), PyStringLiteralExpression.class);
if (docStringOwner != null) { if (stringLiteral == null) return false;
if (docStringOwner.getDocStringExpression() == string) return false;
}
String stringText = string.getText();
int prefixLength = PyStringLiteralUtil.getPrefixLength(stringText);
stringText = stringText.substring(prefixLength);
if (stringText.length() >= 6) { final PyDocStringOwner docStringOwner = PsiTreeUtil.getParentOfType(stringLiteral, PyDocStringOwner.class);
if (stringText.startsWith("'''") && stringText.endsWith("'''") || if (docStringOwner != null && docStringOwner.getDocStringExpression() == stringLiteral) return false;
stringText.startsWith("\"\"\"") && stringText.endsWith("\"\"\"")) return false;
} String currentQuote = stringElement.getQuote();
if (stringText.length() > 2) { boolean allComponentsCanBeConverted = ContainerUtil.all(stringLiteral.getStringElements(),
if (stringText.startsWith("'") && stringText.endsWith("'")) { 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")); setText(PyPsiBundle.message("INTN.quoted.string.single.to.double"));
return true;
} }
if (stringText.startsWith("\"") && stringText.endsWith("\"")) { else {
setText(PyPsiBundle.message("INTN.quoted.string.double.to.single")); setText(PyPsiBundle.message("INTN.quoted.string.double.to.single"));
}
return true; 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;
} }
}
return false; public boolean canBeConverted(@NotNull PyStringElement stringElement) {
return !stringElement.isTripleQuoted() && stringElement.isTerminated() && !stringElement.getContentRange().isEmpty();
} }
@Override @Override
public void doInvoke(@NotNull Project project, Editor editor, PsiFile file) throws IncorrectOperationException { public void doInvoke(@NotNull Project project, Editor editor, PsiFile file) throws IncorrectOperationException {
PyStringLiteralExpression string = PsiTreeUtil.getParentOfType(file.findElementAt(editor.getCaretModel().getOffset()), PyStringLiteralExpression.class); @Nullable PsiElement target = myConversionTarget.getElement();
PyElementGenerator elementGenerator = PyElementGenerator.getInstance(project); if (target instanceof PyStringLiteralExpression) {
if (string != null) { ((PyStringLiteralExpression)target).getStringElements().forEach(this::convertStringElement);
final String stringText = string.getText(); }
int prefixLength = PyStringLiteralUtil.getPrefixLength(stringText); else if (target instanceof PyStringElement) {
final String text = stringText.substring(prefixLength); convertStringElement((PyStringElement)target);
}
}
if (text.startsWith("'") && text.endsWith("'")) { public void convertStringElement(@NotNull PyStringElement stringElement) {
String result = convertSingleToDoubleQuoted(stringText); 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); PyStringLiteralExpression st = elementGenerator.createStringLiteralAlreadyEscaped(result);
string.replace(st); stringElement.replace(st.getFirstChild());
}
if (text.startsWith("\"") && text.endsWith("\"")) {
String result = convertDoubleToSingleQuoted(stringText);
PyStringLiteralExpression st = elementGenerator.createStringLiteralAlreadyEscaped(result);
string.replace(st);
}
}
} }
private static String convertDoubleToSingleQuoted(String stringText) { private static String convertDoubleToSingleQuoted(String stringText) {

View File

@@ -0,0 +1,3 @@
s = ('f<caret>oo'
"bar"
'baz')

View File

@@ -0,0 +1,3 @@
s = ("foo"
"bar"
'baz')

View File

@@ -206,6 +206,11 @@ public class PyIntentionTest extends PyTestCase {
doTest(PyPsiBundle.message("INTN.quoted.string.double.to.single")); 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() { public void testConvertLambdaToFunction() {
doTest(PyPsiBundle.message("INTN.convert.lambda.to.function")); doTest(PyPsiBundle.message("INTN.convert.lambda.to.function"));
} }