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.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<PsiElement> 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);

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