diff --git a/python/psi-api/src/com/jetbrains/python/psi/impl/PyPsiUtils.java b/python/psi-api/src/com/jetbrains/python/psi/impl/PyPsiUtils.java index 598501ef7617..4b398df5e41a 100644 --- a/python/psi-api/src/com/jetbrains/python/psi/impl/PyPsiUtils.java +++ b/python/psi-api/src/com/jetbrains/python/psi/impl/PyPsiUtils.java @@ -15,6 +15,7 @@ import com.intellij.psi.util.PsiTreeUtil; import com.intellij.util.ArrayUtil; import com.intellij.util.containers.ContainerUtil; import com.jetbrains.python.PyTokenTypes; +import com.jetbrains.python.PythonStringUtil; import com.jetbrains.python.psi.*; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -71,15 +72,42 @@ public class PyPsiUtils { @NotNull final PsiElement newExpression) { final Pair data = oldExpression.getUserData(SELECTION_BREAKS_AST_NODE); if (data != null) { - final PsiElement parent = data.first; + final PsiElement element = data.first; final TextRange textRange = data.second; - final String parentText = parent.getText(); + final String parentText = element.getText(); final String prefix = parentText.substring(0, textRange.getStartOffset()); - final String suffix = parentText.substring(textRange.getEndOffset(), parent.getTextLength()); + final String suffix = parentText.substring(textRange.getEndOffset(), element.getTextLength()); final PyElementGenerator generator = PyElementGenerator.getInstance(oldExpression.getProject()); final LanguageLevel languageLevel = LanguageLevel.forElement(oldExpression); - final PsiElement expression = generator.createFromText(languageLevel, parent.getClass(), prefix + newExpression.getText() + suffix); - return parent.replace(expression); + if (element instanceof PyStringLiteralExpression) { + final Pair quotes = PythonStringUtil.getQuotes(parentText); + final PsiElement parent = element.getParent(); + final boolean parensNeeded = parent instanceof PyExpression && !(parent instanceof PyParenthesizedExpression); + if (quotes != null) { + final String leftQuote = quotes.getFirst(); + final String rightQuote = quotes.getSecond(); + final StringBuilder builder = new StringBuilder(); + if (parensNeeded) { + builder.append("("); + } + if (!leftQuote.endsWith(prefix)) { + builder.append(prefix + rightQuote + " + "); + } + final int pos = builder.toString().length(); + builder.append(newExpression.getText()); + if (!rightQuote.startsWith(suffix)) { + builder.append(" + " + leftQuote + suffix); + } + if (parensNeeded) { + builder.append(")"); + } + final PsiElement expression = generator.createExpressionFromText(languageLevel, builder.toString()); + final PsiElement newElement = element.replace(expression); + return newElement.findElementAt(pos); + } + } + final PsiElement expression = generator.createFromText(languageLevel, element.getClass(), prefix + newExpression.getText() + suffix); + return element.replace(expression); } else { return oldExpression.replace(newExpression); diff --git a/python/src/com/jetbrains/python/refactoring/PyRefactoringUtil.java b/python/src/com/jetbrains/python/refactoring/PyRefactoringUtil.java index ff946a310991..03a61fa3817f 100644 --- a/python/src/com/jetbrains/python/refactoring/PyRefactoringUtil.java +++ b/python/src/com/jetbrains/python/refactoring/PyRefactoringUtil.java @@ -42,6 +42,22 @@ public class PyRefactoringUtil { occurrences.add(element); return; } + if (element instanceof PyStringLiteralExpression) { + final Pair selection = pattern.getUserData(PyPsiUtils.SELECTION_BREAKS_AST_NODE); + if (selection != null) { + final String substring = selection.getSecond().substring(pattern.getText()); + final PyStringLiteralExpression expr = (PyStringLiteralExpression)element; + final String text = element.getText(); + if (text != null && expr.getStringNodes().size() == 1) { + final int start = text.indexOf(substring); + if (start >= 0) { + element.putUserData(PyPsiUtils.SELECTION_BREAKS_AST_NODE, Pair.create(element, TextRange.from(start, substring.length()))); + occurrences.add(element); + return; + } + } + } + } element.acceptChildren(this); } }; diff --git a/python/src/com/jetbrains/python/refactoring/introduce/IntroduceHandler.java b/python/src/com/jetbrains/python/refactoring/introduce/IntroduceHandler.java index a330bb006be8..0e5a53ee7f27 100644 --- a/python/src/com/jetbrains/python/refactoring/introduce/IntroduceHandler.java +++ b/python/src/com/jetbrains/python/refactoring/introduce/IntroduceHandler.java @@ -29,6 +29,8 @@ import com.intellij.refactoring.util.CommonRefactoringUtil; import com.intellij.util.Function; import com.jetbrains.python.PyBundle; import com.jetbrains.python.PyNames; +import com.jetbrains.python.PythonStringUtil; +import com.jetbrains.python.codeInsight.dataflow.scope.ScopeUtil; import com.jetbrains.python.psi.*; import com.jetbrains.python.psi.impl.PyPsiUtils; import com.jetbrains.python.psi.resolve.PyResolveContext; @@ -44,22 +46,23 @@ import java.util.*; /** * @author Alexey.Ivanov + * @author vlan */ abstract public class IntroduceHandler implements RefactoringActionHandler { protected static PsiElement findAnchor(List occurrences) { PsiElement anchor = occurrences.get(0); next: do { - PyStatement statement = PsiTreeUtil.getParentOfType(anchor, PyStatement.class); - - final PsiElement parent = statement.getParent(); - for (PsiElement element : occurrences) { - if (!PsiTreeUtil.isAncestor(parent, element, true)) { - anchor = statement; - continue next; + final PyStatement statement = PsiTreeUtil.getParentOfType(anchor, PyStatement.class); + if (statement != null) { + final PsiElement parent = statement.getParent(); + for (PsiElement element : occurrences) { + if (!PsiTreeUtil.isAncestor(parent, element, true)) { + anchor = statement; + continue next; + } } } - return statement; } while (true); @@ -112,7 +115,7 @@ abstract public class IntroduceHandler implements RefactoringActionHandler { protected PsiElement replaceExpression(PsiElement expression, PyExpression newExpression, IntroduceOperation operation) { PyExpressionStatement statement = PsiTreeUtil.getParentOfType(expression, PyExpressionStatement.class); if (statement != null) { - if (statement.getExpression() == expression) { + if (statement.getExpression() == expression && expression.getUserData(PyPsiUtils.SELECTION_BREAKS_AST_NODE) == null) { statement.delete(); return null; } @@ -169,6 +172,10 @@ abstract public class IntroduceHandler implements RefactoringActionHandler { } }; String text = expression.getText(); + final Pair selection = expression.getUserData(PyPsiUtils.SELECTION_BREAKS_AST_NODE); + if (selection != null) { + text = selection.getSecond().substring(text); + } if (expression instanceof PyCallExpression) { final PyExpression callee = ((PyCallExpression)expression).getCallee(); if (callee != null) { @@ -263,13 +270,26 @@ abstract public class IntroduceHandler implements RefactoringActionHandler { return; } - // Introduce refactoring for substrings is not supported yet - TextRange r = element1.getTextRange(); - if (singleElementSelection && element1 instanceof PyStringLiteralExpression && - (r.getStartOffset() < selectionModel.getSelectionStart() || r.getEndOffset() > selectionModel.getSelectionEnd())) { - showCannotPerformError(project, editor); - return; + if (singleElementSelection && element1 instanceof PyStringLiteralExpression) { + // TODO: Protect against escapes + // TODO: Protect against substrings with format characters + // TODO: Handle extracting substring from a string with formatting + final PyStringLiteralExpression literal = (PyStringLiteralExpression)element1; + if (literal.getStringNodes().size() > 1) { + showCannotPerformError(project, editor); + return; + } + final int offset = element1.getTextOffset(); + final TextRange selectionRange = TextRange.create(selectionModel.getSelectionStart(), selectionModel.getSelectionEnd()); + final TextRange elementRange = element1.getTextRange(); + if (!elementRange.equals(selectionRange) && elementRange.contains(selectionRange)) { + final TextRange innerRange = literal.getStringValueTextRange(); + final TextRange intersection = selectionRange.shiftRight(-offset).intersection(innerRange); + final TextRange finalRange = intersection != null ? intersection : selectionRange; + element1.putUserData(PyPsiUtils.SELECTION_BREAKS_AST_NODE, Pair.create(element1, finalRange)); + } } + if (!checkIntroduceContext(file, editor, element1)) { return; } @@ -357,10 +377,10 @@ abstract public class IntroduceHandler implements RefactoringActionHandler { (PyExpression)element; operation.setInitializer(initializer); - if (initializer.getUserData(PyPsiUtils.SELECTION_BREAKS_AST_NODE) == null) { + if (initializer != null) { operation.setOccurrences(getOccurrences(element, initializer)); + operation.setSuggestedNames(getSuggestedNames(initializer)); } - operation.setSuggestedNames(getSuggestedNames(initializer)); if (operation.getOccurrences().size() == 0) { operation.setReplaceAll(false); } @@ -453,21 +473,33 @@ abstract public class IntroduceHandler implements RefactoringActionHandler { @Override public void visitPyStringLiteralExpression(PyStringLiteralExpression node) { - ASTNode child = node.getNode().getFirstChildNode(); - while (child != null) { - String text = child.getText(); - if (child.getElementType() == TokenType.WHITE_SPACE) { - if (text.contains("\n")) { - if (!text.contains("\\")) { - myResult.append("\\"); + final Pair data = node.getUserData(PyPsiUtils.SELECTION_BREAKS_AST_NODE); + if (data != null) { + final PsiElement parent = data.getFirst(); + final String text = parent.getText(); + final Pair detectedQuotes = PythonStringUtil.getQuotes(text); + final Pair quotes = detectedQuotes != null ? detectedQuotes : Pair.create("'", "'"); + final TextRange range = data.getSecond(); + final String substring = range.substring(text); + myResult.append(quotes.getFirst() + substring + quotes.getSecond()); + } + else { + ASTNode child = node.getNode().getFirstChildNode(); + while (child != null) { + String text = child.getText(); + if (child.getElementType() == TokenType.WHITE_SPACE) { + if (text.contains("\n")) { + if (!text.contains("\\")) { + myResult.append("\\"); + } + myResult.append(text); } + } + else { myResult.append(text); } + child = child.getTreeNext(); } - else { - myResult.append(text); - } - child = child.getTreeNext(); } } @@ -498,14 +530,7 @@ abstract public class IntroduceHandler implements RefactoringActionHandler { } protected List getOccurrences(PsiElement element, @NotNull final PyExpression expression) { - PsiElement context = PsiTreeUtil.getParentOfType(expression, PyFunction.class); - if (context == null) { - context = PsiTreeUtil.getParentOfType(expression, PyClass.class); - } - if (context == null) { - context = expression.getContainingFile(); - } - return PyRefactoringUtil.getOccurrences(expression, context); + return PyRefactoringUtil.getOccurrences(expression, ScopeUtil.getScopeOwner(expression)); } private PsiElement performReplace(@NotNull final PsiElement declaration, @@ -551,7 +576,7 @@ abstract public class IntroduceHandler implements RefactoringActionHandler { } protected PyExpression createExpression(Project project, String name, PsiElement declaration) { - return PyElementGenerator.getInstance(project).createExpressionFromText(name); + return PyElementGenerator.getInstance(project).createExpressionFromText(LanguageLevel.forElement(declaration), name); } @Nullable diff --git a/python/testData/refactoring/introduceVariable/bytesSubstring.after.py b/python/testData/refactoring/introduceVariable/bytesSubstring.after.py new file mode 100644 index 000000000000..67a86c8749cf --- /dev/null +++ b/python/testData/refactoring/introduceVariable/bytesSubstring.after.py @@ -0,0 +1,2 @@ +a = b'bar' +b'foo' + a + b'baz' \ No newline at end of file diff --git a/python/testData/refactoring/introduceVariable/bytesSubstring.py b/python/testData/refactoring/introduceVariable/bytesSubstring.py new file mode 100644 index 000000000000..6f98337f4121 --- /dev/null +++ b/python/testData/refactoring/introduceVariable/bytesSubstring.py @@ -0,0 +1 @@ +b'foobarbaz' \ No newline at end of file diff --git a/python/testData/refactoring/introduceVariable/leftQuoteSubstring.after.py b/python/testData/refactoring/introduceVariable/leftQuoteSubstring.after.py new file mode 100644 index 000000000000..66921e9c1aa1 --- /dev/null +++ b/python/testData/refactoring/introduceVariable/leftQuoteSubstring.after.py @@ -0,0 +1,2 @@ +a = "hello" +print(a + " world") diff --git a/python/testData/refactoring/introduceVariable/leftQuoteSubstring.py b/python/testData/refactoring/introduceVariable/leftQuoteSubstring.py new file mode 100644 index 000000000000..4c4486bf7ccd --- /dev/null +++ b/python/testData/refactoring/introduceVariable/leftQuoteSubstring.py @@ -0,0 +1 @@ +print("hello world") diff --git a/python/testData/refactoring/introduceVariable/leftSubstring.after.py b/python/testData/refactoring/introduceVariable/leftSubstring.after.py new file mode 100644 index 000000000000..d21668605427 --- /dev/null +++ b/python/testData/refactoring/introduceVariable/leftSubstring.after.py @@ -0,0 +1,2 @@ +a = "hello" +print(a + " world") \ No newline at end of file diff --git a/python/testData/refactoring/introduceVariable/leftSubstring.py b/python/testData/refactoring/introduceVariable/leftSubstring.py new file mode 100644 index 000000000000..e981bd05c293 --- /dev/null +++ b/python/testData/refactoring/introduceVariable/leftSubstring.py @@ -0,0 +1 @@ +print("hello world") \ No newline at end of file diff --git a/python/testData/refactoring/introduceVariable/middleSubstring.after.py b/python/testData/refactoring/introduceVariable/middleSubstring.after.py new file mode 100644 index 000000000000..148c9c774e8e --- /dev/null +++ b/python/testData/refactoring/introduceVariable/middleSubstring.after.py @@ -0,0 +1,2 @@ +a = "lo wor" +print("hel" + a + "ld") \ No newline at end of file diff --git a/python/testData/refactoring/introduceVariable/middleSubstring.py b/python/testData/refactoring/introduceVariable/middleSubstring.py new file mode 100644 index 000000000000..938db7bd1c1e --- /dev/null +++ b/python/testData/refactoring/introduceVariable/middleSubstring.py @@ -0,0 +1 @@ +print("hello world") \ No newline at end of file diff --git a/python/testData/refactoring/introduceVariable/rightSubstring.after.py b/python/testData/refactoring/introduceVariable/rightSubstring.after.py new file mode 100644 index 000000000000..fa468b6341c7 --- /dev/null +++ b/python/testData/refactoring/introduceVariable/rightSubstring.after.py @@ -0,0 +1,2 @@ +a = "world" +print("hello " + a) \ No newline at end of file diff --git a/python/testData/refactoring/introduceVariable/rightSubstring.py b/python/testData/refactoring/introduceVariable/rightSubstring.py new file mode 100644 index 000000000000..faaede5b7ddb --- /dev/null +++ b/python/testData/refactoring/introduceVariable/rightSubstring.py @@ -0,0 +1 @@ +print("hello world") \ No newline at end of file diff --git a/python/testData/refactoring/introduceVariable/substringInExpression.after.py b/python/testData/refactoring/introduceVariable/substringInExpression.after.py new file mode 100644 index 000000000000..3420cf9bf534 --- /dev/null +++ b/python/testData/refactoring/introduceVariable/substringInExpression.after.py @@ -0,0 +1,2 @@ +a = 'foo' +print((a + 'bar').upper()) \ No newline at end of file diff --git a/python/testData/refactoring/introduceVariable/substringInExpression.py b/python/testData/refactoring/introduceVariable/substringInExpression.py new file mode 100644 index 000000000000..ef56fc02a263 --- /dev/null +++ b/python/testData/refactoring/introduceVariable/substringInExpression.py @@ -0,0 +1 @@ +print('foobar'.upper()) \ No newline at end of file diff --git a/python/testData/refactoring/introduceVariable/substringInExpressionStatement.after.py b/python/testData/refactoring/introduceVariable/substringInExpressionStatement.after.py new file mode 100644 index 000000000000..c4b048f0ab3d --- /dev/null +++ b/python/testData/refactoring/introduceVariable/substringInExpressionStatement.after.py @@ -0,0 +1,2 @@ +a = "two" +"one " + a + " three" \ No newline at end of file diff --git a/python/testData/refactoring/introduceVariable/substringInExpressionStatement.py b/python/testData/refactoring/introduceVariable/substringInExpressionStatement.py new file mode 100644 index 000000000000..e81c165f4255 --- /dev/null +++ b/python/testData/refactoring/introduceVariable/substringInExpressionStatement.py @@ -0,0 +1 @@ +"one two three" \ No newline at end of file diff --git a/python/testData/refactoring/introduceVariable/substringInStatement.after.py b/python/testData/refactoring/introduceVariable/substringInStatement.after.py new file mode 100644 index 000000000000..4027559cab28 --- /dev/null +++ b/python/testData/refactoring/introduceVariable/substringInStatement.after.py @@ -0,0 +1,2 @@ +a = 'foo' +x = a + 'bar' \ No newline at end of file diff --git a/python/testData/refactoring/introduceVariable/substringInStatement.py b/python/testData/refactoring/introduceVariable/substringInStatement.py new file mode 100644 index 000000000000..526ad2c55ad5 --- /dev/null +++ b/python/testData/refactoring/introduceVariable/substringInStatement.py @@ -0,0 +1 @@ +x = 'foobar' \ No newline at end of file diff --git a/python/testData/refactoring/introduceVariable/tripleQuotedSubstring.after.py b/python/testData/refactoring/introduceVariable/tripleQuotedSubstring.after.py new file mode 100644 index 000000000000..5389de87a910 --- /dev/null +++ b/python/testData/refactoring/introduceVariable/tripleQuotedSubstring.after.py @@ -0,0 +1,6 @@ +a = """Three""" +print(""""One two + +* """ + a + """ +* Four +* Five""") \ No newline at end of file diff --git a/python/testData/refactoring/introduceVariable/tripleQuotedSubstring.py b/python/testData/refactoring/introduceVariable/tripleQuotedSubstring.py new file mode 100644 index 000000000000..5972d20c474c --- /dev/null +++ b/python/testData/refactoring/introduceVariable/tripleQuotedSubstring.py @@ -0,0 +1,5 @@ +print(""""One two + +* Three +* Four +* Five""") \ 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 1ce1af50e248..5a28cd82f9f5 100644 --- a/python/testSrc/com/jetbrains/python/refactoring/PyIntroduceVariableTest.java +++ b/python/testSrc/com/jetbrains/python/refactoring/PyIntroduceVariableTest.java @@ -88,6 +88,51 @@ public class PyIntroduceVariableTest extends PyIntroduceTestCase { doTest(); } + // PY-3654 + public void testLeftSubstring() { + doTest(); + } + + // PY-3654 + public void testRightSubstring() { + doTest(); + } + + // PY-3654 + public void testMiddleSubstring() { + doTest(); + } + + // PY-3654 + public void testLeftQuoteSubstring() { + doTest(); + } + + // PY-3654 + public void testSubstringInExpression() { + doTest(); + } + + // PY-3654 + public void testSubstringInStatement() { + doTest(); + } + + // PY-3654 + public void testTripleQuotedSubstring() { + doTest(); + } + + // PY-3654 + public void testSubstringInExpressionStatement() { + doTest(); + } + + // PY-6354 + public void testBytesSubstring() { + doTest(); + } + private void doTestCannotPerform() { boolean thrownExpectedException = false; try {