diff --git a/python/src/com/jetbrains/python/refactoring/PyReplaceExpressionUtil.java b/python/src/com/jetbrains/python/refactoring/PyReplaceExpressionUtil.java index 77cb8d769f06..b3fb0836cf3b 100644 --- a/python/src/com/jetbrains/python/refactoring/PyReplaceExpressionUtil.java +++ b/python/src/com/jetbrains/python/refactoring/PyReplaceExpressionUtil.java @@ -4,8 +4,10 @@ import com.intellij.lang.ASTNode; import com.intellij.openapi.util.Key; import com.intellij.openapi.util.Pair; import com.intellij.openapi.util.TextRange; +import com.intellij.openapi.util.text.StringUtil; import com.intellij.psi.PsiElement; import com.intellij.psi.tree.IElementType; +import com.intellij.util.Function; import com.jetbrains.python.PyElementTypes; import com.jetbrains.python.PythonStringUtil; import com.jetbrains.python.inspections.PyStringFormatParser; @@ -77,9 +79,12 @@ public class PyReplaceExpressionUtil implements PyElementTypes { @NotNull PsiElement newExpression, @NotNull TextRange textRange) { final String fullText = oldExpression.getText(); + final Pair detectedQuotes = PythonStringUtil.getQuotes(fullText); + final Pair quotes = detectedQuotes != null ? detectedQuotes : Pair.create("'", "'"); final String prefix = fullText.substring(0, textRange.getStartOffset()); final String suffix = fullText.substring(textRange.getEndOffset(), oldExpression.getTextLength()); final PyExpression valueExpression = PyStringFormatParser.getFormatValueExpression(oldExpression); + final String newText = newExpression.getText(); final PyElementGenerator generator = PyElementGenerator.getInstance(oldExpression.getProject()); final LanguageLevel languageLevel = LanguageLevel.forElement(oldExpression); @@ -88,8 +93,6 @@ public class PyReplaceExpressionUtil implements PyElementTypes { if (valueExpression != null && !containsStringFormatting(fullText, textRange)) { if (valueExpression instanceof PyTupleExpression) { // 'foo%s' % (x,) -> '%s%s' % (s, x) - // TODO: Support dict literals and dict() function - // TODO: It is possible to resolve to a tuple or dict literal and modify them final String newLiteralText = prefix + "%s" + suffix; final PyStringLiteralExpression newLiteralExpression = generator.createStringLiteralAlreadyEscaped(newLiteralText); oldExpression.replace(newLiteralExpression); @@ -110,14 +113,43 @@ public class PyReplaceExpressionUtil implements PyElementTypes { return newExpression; } } - return null; + else if (valueExpression instanceof PyDictLiteralExpression) { + // 'foo%(x)s' % {'x': x} -> '%(s)s%(x)s' % {'x': x, 's': s} + // TODO: Support the dict() function + final String newLiteralText = prefix + "%(" + newText + ")s" + suffix; + final PyStringLiteralExpression newLiteralExpression = generator.createStringLiteralAlreadyEscaped(newLiteralText); + oldExpression.replace(newLiteralExpression); + + final PyDictLiteralExpression dict = (PyDictLiteralExpression)valueExpression; + final StringBuilder builder = new StringBuilder(); + builder.append("{"); + final PyKeyValueExpression[] elements = dict.getElements(); + builder.append(StringUtil.join(elements, new Function() { + @Override + public String fun(PyKeyValueExpression expression) { + return expression.getText(); + } + }, ",")); + if (elements.length > 0) { + builder.append(","); + } + builder.append(quotes.getSecond()); + builder.append(newText); + builder.append(quotes.getSecond()); + builder.append(":"); + final int pos = builder.toString().length(); + builder.append(newText); + builder.append("}"); + final PyExpression newDictLiteral = generator.createExpressionFromText(languageLevel, builder.toString()); + final PsiElement newElement = valueExpression.replace(newDictLiteral); + return newElement.findElementAt(pos); + } } - else if (isConcatFormatting(oldExpression) || substitutions.size() > 0) { + + if (isConcatFormatting(oldExpression) || substitutions.size() > 0) { // 'foobar' + 'baz' -> s + 'bar' + 'baz' // 'foobar%s' -> s + 'bar%s' // 'f%soobar' % x -> (s + 'bar') % x - final Pair detectedQuotes = PythonStringUtil.getQuotes(fullText); - final Pair quotes = detectedQuotes != null ? detectedQuotes : Pair.create("'", "'"); final String leftQuote = quotes.getFirst(); final String rightQuote = quotes.getSecond(); final StringBuilder builder = new StringBuilder(); @@ -128,7 +160,7 @@ public class PyReplaceExpressionUtil implements PyElementTypes { builder.append(prefix + rightQuote + " + "); } final int pos = builder.toString().length(); - builder.append(newExpression.getText()); + builder.append(newText); if (!rightQuote.startsWith(suffix)) { builder.append(" + " + leftQuote + suffix); } @@ -153,7 +185,7 @@ public class PyReplaceExpressionUtil implements PyElementTypes { builder.append(suffix); builder.append(" % "); final int pos = builder.toString().length(); - builder.append(newExpression.getText()); + builder.append(newText); if (parensNeeded) { builder.append(")"); } diff --git a/python/testData/refactoring/introduceVariable/substringFromFormatDict.after.py b/python/testData/refactoring/introduceVariable/substringFromFormatDict.after.py new file mode 100644 index 000000000000..4579894ce34c --- /dev/null +++ b/python/testData/refactoring/introduceVariable/substringFromFormatDict.after.py @@ -0,0 +1,2 @@ +a = "Hello" +print("%(a)s %(name)s" % {"name": "World", "a": a}) \ No newline at end of file diff --git a/python/testData/refactoring/introduceVariable/substringFromFormatDict.py b/python/testData/refactoring/introduceVariable/substringFromFormatDict.py new file mode 100644 index 000000000000..8ca9637af1ee --- /dev/null +++ b/python/testData/refactoring/introduceVariable/substringFromFormatDict.py @@ -0,0 +1 @@ +print("Hello %(name)s" % {"name": "World"}) \ No newline at end of file diff --git a/python/testSrc/com/jetbrains/python/refactoring/PyIntroduceVariableTest.java b/python/testSrc/com/jetbrains/python/refactoring/PyIntroduceVariableTest.java index 7cb8e3613b04..eb631714e6c7 100644 --- a/python/testSrc/com/jetbrains/python/refactoring/PyIntroduceVariableTest.java +++ b/python/testSrc/com/jetbrains/python/refactoring/PyIntroduceVariableTest.java @@ -178,6 +178,11 @@ public class PyIntroduceVariableTest extends PyIntroduceTestCase { doTest(); } + // PY-3654 + public void testSubstringFromFormatDict() { + doTest(); + } + private void doTestCannotPerform() { boolean thrownExpectedException = false; try {