From ff8d993f4f2e521a7e0a1fa76f0362d5e2b95d91 Mon Sep 17 00:00:00 2001 From: Dmitry Jemerov Date: Tue, 18 Dec 2012 19:49:39 +0100 Subject: [PATCH 01/10] select Python 2 interpreter when selecting a project type that doesn't support Python 3; report Python 3 as incompatible when creating App Engine project --- .../python/newProject/PyFrameworkProjectGenerator.java | 2 ++ .../com/jetbrains/python/sdk/PreferredSdkComparator.java | 7 +++++++ python/src/com/jetbrains/python/sdk/PythonSdkType.java | 1 + 3 files changed, 10 insertions(+) diff --git a/python/openapi/src/com/jetbrains/python/newProject/PyFrameworkProjectGenerator.java b/python/openapi/src/com/jetbrains/python/newProject/PyFrameworkProjectGenerator.java index 8873232265c4..9dad403e5e01 100644 --- a/python/openapi/src/com/jetbrains/python/newProject/PyFrameworkProjectGenerator.java +++ b/python/openapi/src/com/jetbrains/python/newProject/PyFrameworkProjectGenerator.java @@ -13,4 +13,6 @@ public interface PyFrameworkProjectGenerator extends DirectoryProjectGenerato boolean isFrameworkInstalled(Project project, Sdk sdk); boolean acceptsRemoteSdk(); + + boolean supportsPython3(); } diff --git a/python/src/com/jetbrains/python/sdk/PreferredSdkComparator.java b/python/src/com/jetbrains/python/sdk/PreferredSdkComparator.java index c3e0a0d21b97..26d166314d00 100644 --- a/python/src/com/jetbrains/python/sdk/PreferredSdkComparator.java +++ b/python/src/com/jetbrains/python/sdk/PreferredSdkComparator.java @@ -11,10 +11,17 @@ import java.util.Comparator; * @author yole */ public class PreferredSdkComparator implements Comparator { + public static PreferredSdkComparator INSTANCE = new PreferredSdkComparator(); + @Override public int compare(Sdk o1, Sdk o2) { final PythonSdkFlavor flavor1 = PythonSdkFlavor.getFlavor(o1); final PythonSdkFlavor flavor2 = PythonSdkFlavor.getFlavor(o2); + int venv1weight = PythonSdkType.isVirtualEnv(o1) ? 0 : 1; + int venv2weight = PythonSdkType.isVirtualEnv(o2) ? 0 : 1; + if (venv1weight != venv2weight) { + return venv2weight - venv1weight; + } int flavor1weight = flavor1 instanceof CPythonSdkFlavor ? 1 : 0; int flavor2weight = flavor2 instanceof CPythonSdkFlavor ? 1 : 0; if (flavor1weight != flavor2weight) { diff --git a/python/src/com/jetbrains/python/sdk/PythonSdkType.java b/python/src/com/jetbrains/python/sdk/PythonSdkType.java index e30b3cae9ff1..c42eb430951f 100644 --- a/python/src/com/jetbrains/python/sdk/PythonSdkType.java +++ b/python/src/com/jetbrains/python/sdk/PythonSdkType.java @@ -801,6 +801,7 @@ public class PythonSdkType extends SdkType { return moduleSDK; } List allSdks = getAllSdks(); + Collections.sort(allSdks, PreferredSdkComparator.INSTANCE); for (Sdk sdk : allSdks) { if (!getLanguageLevelForSdk(sdk).isPy3K()) { return sdk; From 724dccdeb0d3eef7dd297d4885a1d0ff79d18f3e Mon Sep 17 00:00:00 2001 From: Dmitry Jemerov Date: Tue, 18 Dec 2012 20:13:22 +0100 Subject: [PATCH 02/10] resolve references to WSGI apps from app.yaml (PY-5186) --- .../psi/resolve/QualifiedNameResolver.java | 2 ++ .../resolve/QualifiedNameResolverImpl.java | 22 ++++++++++++------- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/python/psi-api/src/com/jetbrains/python/psi/resolve/QualifiedNameResolver.java b/python/psi-api/src/com/jetbrains/python/psi/resolve/QualifiedNameResolver.java index a53e21143944..1b5e39acbdc9 100644 --- a/python/psi-api/src/com/jetbrains/python/psi/resolve/QualifiedNameResolver.java +++ b/python/psi-api/src/com/jetbrains/python/psi/resolve/QualifiedNameResolver.java @@ -46,4 +46,6 @@ public interface QualifiedNameResolver { QualifiedNameResolver withoutForeign(); Module getModule(); + + QualifiedNameResolver withMembers(); } diff --git a/python/src/com/jetbrains/python/psi/resolve/QualifiedNameResolverImpl.java b/python/src/com/jetbrains/python/psi/resolve/QualifiedNameResolverImpl.java index 31c59c848bf2..4b786de3e974 100644 --- a/python/src/com/jetbrains/python/psi/resolve/QualifiedNameResolverImpl.java +++ b/python/src/com/jetbrains/python/psi/resolve/QualifiedNameResolverImpl.java @@ -12,7 +12,6 @@ import com.intellij.openapi.vfs.VirtualFile; import com.intellij.psi.PsiDirectory; import com.intellij.psi.PsiElement; import com.intellij.psi.PsiFile; -import com.intellij.psi.PsiFileSystemItem; import com.jetbrains.django.facet.DjangoFacetType; import com.jetbrains.python.console.PydevConsoleRunner; import com.jetbrains.python.psi.impl.PyBuiltinCache; @@ -42,6 +41,7 @@ public class QualifiedNameResolverImpl implements RootVisitor, QualifiedNameReso private int myRelativeLevel = -1; private boolean myWithoutRoots; private boolean myWithoutForeign; + private boolean myWithMembers; public QualifiedNameResolverImpl(@NotNull String qNameString) { myQualifiedName = PyQualifiedName.fromDottedString(qNameString); @@ -130,6 +130,12 @@ public class QualifiedNameResolverImpl implements RootVisitor, QualifiedNameReso return this; } + @Override + public QualifiedNameResolver withMembers() { + myWithMembers = true; + return this; + } + /** * Specifies that we're looking for a file in a directory hierarchy, not a module in the Python package hierarchy * (so we don't need to check for existence of __init__.py) @@ -146,7 +152,7 @@ public class QualifiedNameResolverImpl implements RootVisitor, QualifiedNameReso if (!root.isValid()) { return true; } - PsiFileSystemItem resolveResult = resolveInRoot(root); + PsiElement resolveResult = resolveInRoot(root); if (resolveResult != null) { addRoot(resolveResult, isModuleSource); } @@ -161,7 +167,7 @@ public class QualifiedNameResolverImpl implements RootVisitor, QualifiedNameReso return true; } - private void addRoot(PsiFileSystemItem resolveResult, boolean isModuleSource) { + private void addRoot(PsiElement resolveResult, boolean isModuleSource) { if (isModuleSource) { mySourceResults.add(resolveResult); } @@ -185,7 +191,7 @@ public class QualifiedNameResolverImpl implements RootVisitor, QualifiedNameReso dir = ResolveImportUtil.stepBackFrom(footholdFile, myRelativeLevel); } - PsiFileSystemItem module = resolveModuleAt(dir); + PsiElement module = resolveModuleAt(dir); if (module != null) { addRoot(module, true); } @@ -302,7 +308,7 @@ public class QualifiedNameResolverImpl implements RootVisitor, QualifiedNameReso } @Nullable - private PsiFileSystemItem resolveInRoot(VirtualFile root) { + private PsiElement resolveInRoot(VirtualFile root) { if (!root.isDirectory()) { // if we have added a file as a root, it's unlikely that we'll be able to resolve anything under it in 'files only' resolve mode return null; @@ -316,16 +322,16 @@ public class QualifiedNameResolverImpl implements RootVisitor, QualifiedNameReso * @param directory where to start from; top qualifier will be searched for here. */ @Nullable - private PsiFileSystemItem resolveModuleAt(@Nullable PsiDirectory directory) { + private PsiElement resolveModuleAt(@Nullable PsiDirectory directory) { // prerequisites if (directory == null || !directory.isValid()) return null; - PsiFileSystemItem seeker = directory; + PsiElement seeker = directory; for (String name : myQualifiedName.getComponents()) { if (name == null) { return null; } - seeker = (PsiFileSystemItem)ResolveImportUtil.resolveChild(seeker, name, myContext.getFootholdFile(), true, myCheckForPackage); + seeker = ResolveImportUtil.resolveChild(seeker, name, myContext.getFootholdFile(), !myWithMembers, myCheckForPackage); } return seeker; } From 4163e6241acf2a602b4ec04041b66624c432fa22 Mon Sep 17 00:00:00 2001 From: Dmitry Jemerov Date: Wed, 19 Dec 2012 13:21:40 +0100 Subject: [PATCH 03/10] rename run configuration group (PY-8310) --- python/src/com/jetbrains/python/PyBundle.properties | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/python/src/com/jetbrains/python/PyBundle.properties b/python/src/com/jetbrains/python/PyBundle.properties index 8289d9646d05..cfd37a7f6c02 100644 --- a/python/src/com/jetbrains/python/PyBundle.properties +++ b/python/src/com/jetbrains/python/PyBundle.properties @@ -611,8 +611,8 @@ runcfg.attest.display_name=Attests runcfg.attest.description=Python's attests run configuration ### test run configuration -runcfg.test.display_name=Python's test -runcfg.test.description=Python's test run configuration +runcfg.test.display_name=Python tests +runcfg.test.description=Python frameworks supported by PyCharm ### unittest run configuration runcfg.unittest.display_name=Unittests From c9e423dcaccaa78d3c97a781f8afec4be50e4f2a Mon Sep 17 00:00:00 2001 From: Andrey Vlasovskikh Date: Wed, 19 Dec 2012 19:42:41 +0400 Subject: [PATCH 04/10] Fixed Python string utils for triple-quoted and triple-double quoted string and bytes literals --- .../jetbrains/python/PythonStringUtil.java | 81 ++++++++++--------- 1 file changed, 41 insertions(+), 40 deletions(-) diff --git a/python/psi-api/src/com/jetbrains/python/PythonStringUtil.java b/python/psi-api/src/com/jetbrains/python/PythonStringUtil.java index 1702ed7a2d0f..215362bcc22e 100644 --- a/python/psi-api/src/com/jetbrains/python/PythonStringUtil.java +++ b/python/psi-api/src/com/jetbrains/python/PythonStringUtil.java @@ -1,5 +1,6 @@ package com.jetbrains.python; +import com.google.common.collect.ImmutableList; import com.intellij.openapi.util.Pair; import com.intellij.openapi.util.SystemInfo; import com.intellij.openapi.util.TextRange; @@ -23,9 +24,11 @@ import static com.intellij.openapi.util.text.StringUtil.startsWith; /** * @author Alexei Orischenko - * Date: Nov 26, 2009 + * @author vlan */ public class PythonStringUtil { + private static final ImmutableList QUOTES = ImmutableList.of("'''", "\"\"\"", "'", "\""); + private PythonStringUtil() { } @@ -44,12 +47,11 @@ public class PythonStringUtil { public static TextRange getStringValueTextRange(@NotNull String s) { - if ((s.charAt(0) == '\'' || s.charAt(0) == '"') && (s.charAt(0) == s.charAt(s.length() - 1)) && s.length() > 1) { - return TextRange.create(1, s.length() - 1); - } - else { - return TextRange.create(0, s.length()); + final Pair quotes = getQuotes(s); + if (quotes != null) { + return TextRange.create(quotes.getFirst().length(), s.length() - quotes.getSecond().length()); } + return TextRange.allOf(s); } @NotNull @@ -169,23 +171,7 @@ public class PythonStringUtil { */ public static boolean isQuoted(@Nullable String text) { - if (text == null) { - return false; - } - if (text.toLowerCase().startsWith("u")) { - text = text.substring(1); - } - - assert text != null; - if (text.toLowerCase().startsWith("r")) { - text = text.substring(1); - } - - assert text != null; - if (text.length() > 1 && text.charAt(0) == text.charAt(text.length() - 1) && (text.charAt(0) == '\'' || text.charAt(0) == '"')) { - return true; - } - return false; + return text != null && getQuotes(text) != null; } /** @@ -197,27 +183,42 @@ public class PythonStringUtil { * UR"unicode raw string" -> (UR", ") */ @Nullable - public static Pair getQuotes(@NotNull String text) { - String first = ""; - - if (text.toLowerCase().startsWith("u")) { - first += text.substring(0, 1); - text = text.substring(1); + public static Pair getQuotes(@NotNull final String text) { + boolean start = true; + int pos = 0; + for (int i = 0; i < text.length(); i++) { + final char c = Character.toLowerCase(text.charAt(i)); + if (start) { + if (c == 'u' || c == 'r' || c == 'b') { + pos = i + 1; + } + else { + start = false; + } + } + else { + break; + } } - - if (text.toLowerCase().startsWith("r")) { - first += text.substring(0, 1); - text = text.substring(1); + final String prefix = text.substring(0, pos); + final String mainText = text.substring(pos); + for (String quote : QUOTES) { + final Pair quotes = getQuotes(mainText, prefix, quote); + if (quotes != null) { + return quotes; + } } + return null; + } - int last = text.length() - 1; - - if (text.length() > 2 && (text.charAt(0) == '\'' || text.charAt(0) == '"') && (text.charAt(last) == '\'' || text.charAt(last) == '"')) { - return Pair.create(first + text.substring(0, 1), text.substring(last)); - } - else { - return null; + @Nullable + private static Pair getQuotes(@NotNull String text, @NotNull String prefix, @NotNull String quote) { + final int length = text.length(); + final int n = quote.length(); + if (length >= 2 * n && text.startsWith(quote) && text.endsWith(quote)) { + return Pair.create(prefix + text.substring(0, n), text.substring(length - n)); } + return null; } @Nullable From 9d80d6f98c4c1732fc28c1d6cf18f28fb924a765 Mon Sep 17 00:00:00 2001 From: Andrey Vlasovskikh Date: Wed, 19 Dec 2012 19:44:10 +0400 Subject: [PATCH 05/10] Initial implementation of introduce refactoring for substrings (PY-3654) --- .../jetbrains/python/psi/impl/PyPsiUtils.java | 38 ++++++- .../python/refactoring/PyRefactoringUtil.java | 16 +++ .../introduce/IntroduceHandler.java | 99 ++++++++++++------- .../introduceVariable/bytesSubstring.after.py | 2 + .../introduceVariable/bytesSubstring.py | 1 + .../leftQuoteSubstring.after.py | 2 + .../introduceVariable/leftQuoteSubstring.py | 1 + .../introduceVariable/leftSubstring.after.py | 2 + .../introduceVariable/leftSubstring.py | 1 + .../middleSubstring.after.py | 2 + .../introduceVariable/middleSubstring.py | 1 + .../introduceVariable/rightSubstring.after.py | 2 + .../introduceVariable/rightSubstring.py | 1 + .../substringInExpression.after.py | 2 + .../substringInExpression.py | 1 + .../substringInExpressionStatement.after.py | 2 + .../substringInExpressionStatement.py | 1 + .../substringInStatement.after.py | 2 + .../introduceVariable/substringInStatement.py | 1 + .../tripleQuotedSubstring.after.py | 6 ++ .../tripleQuotedSubstring.py | 5 + .../refactoring/PyIntroduceVariableTest.java | 45 +++++++++ 22 files changed, 191 insertions(+), 42 deletions(-) create mode 100644 python/testData/refactoring/introduceVariable/bytesSubstring.after.py create mode 100644 python/testData/refactoring/introduceVariable/bytesSubstring.py create mode 100644 python/testData/refactoring/introduceVariable/leftQuoteSubstring.after.py create mode 100644 python/testData/refactoring/introduceVariable/leftQuoteSubstring.py create mode 100644 python/testData/refactoring/introduceVariable/leftSubstring.after.py create mode 100644 python/testData/refactoring/introduceVariable/leftSubstring.py create mode 100644 python/testData/refactoring/introduceVariable/middleSubstring.after.py create mode 100644 python/testData/refactoring/introduceVariable/middleSubstring.py create mode 100644 python/testData/refactoring/introduceVariable/rightSubstring.after.py create mode 100644 python/testData/refactoring/introduceVariable/rightSubstring.py create mode 100644 python/testData/refactoring/introduceVariable/substringInExpression.after.py create mode 100644 python/testData/refactoring/introduceVariable/substringInExpression.py create mode 100644 python/testData/refactoring/introduceVariable/substringInExpressionStatement.after.py create mode 100644 python/testData/refactoring/introduceVariable/substringInExpressionStatement.py create mode 100644 python/testData/refactoring/introduceVariable/substringInStatement.after.py create mode 100644 python/testData/refactoring/introduceVariable/substringInStatement.py create mode 100644 python/testData/refactoring/introduceVariable/tripleQuotedSubstring.after.py create mode 100644 python/testData/refactoring/introduceVariable/tripleQuotedSubstring.py 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 { From 41eadd1c8cddc25499205736e50db64595036b87 Mon Sep 17 00:00:00 2001 From: Dmitry Jemerov Date: Wed, 19 Dec 2012 20:01:05 +0100 Subject: [PATCH 06/10] include restClient plugin in pycharm build --- python/build/plugin-list.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/python/build/plugin-list.txt b/python/build/plugin-list.txt index a3a10f858e8c..f2b7f745fa40 100644 --- a/python/build/plugin-list.txt +++ b/python/build/plugin-list.txt @@ -48,3 +48,4 @@ python-rest pycharm-flask pycharm-numpy textmate +restClient From 82727c3e938ce9f361b0083c788a986f145169a8 Mon Sep 17 00:00:00 2001 From: Dmitry Trofimov Date: Thu, 20 Dec 2012 14:52:04 +0100 Subject: [PATCH 07/10] Longer timeout for ports waiting. --- .../src/com/jetbrains/python/console/PydevConsoleRunner.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/python/src/com/jetbrains/python/console/PydevConsoleRunner.java b/python/src/com/jetbrains/python/console/PydevConsoleRunner.java index f82054d3a8cb..c62dcea3ef69 100644 --- a/python/src/com/jetbrains/python/console/PydevConsoleRunner.java +++ b/python/src/com/jetbrains/python/console/PydevConsoleRunner.java @@ -72,6 +72,7 @@ import static com.jetbrains.python.sdk.PythonEnvUtil.setPythonUnbuffered; public class PydevConsoleRunner extends AbstractConsoleRunnerWithHistory { private static final Logger LOG = Logger.getInstance(PydevConsoleRunner.class.getName()); public static final String PYDEV_PYDEVCONSOLE_PY = "pydev/pydevconsole.py"; + public static final int PORTS_WAITING_TIMEOUT = 20000; private Sdk mySdk; @NotNull private final CommandLineArgumentsProvider myCommandLineArgumentsProvider; @@ -267,7 +268,7 @@ public class PydevConsoleRunner extends AbstractConsoleRunnerWithHistory Date: Thu, 20 Dec 2012 17:48:17 +0100 Subject: [PATCH 08/10] Don't try to kill remote process by means of local OS (PY-8171). --- python/src/com/jetbrains/python/run/PythonProcessHandler.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/python/src/com/jetbrains/python/run/PythonProcessHandler.java b/python/src/com/jetbrains/python/run/PythonProcessHandler.java index 1458081fdee1..f401cd0da78e 100644 --- a/python/src/com/jetbrains/python/run/PythonProcessHandler.java +++ b/python/src/com/jetbrains/python/run/PythonProcessHandler.java @@ -26,8 +26,8 @@ public class PythonProcessHandler extends KillableColoredProcessHandler { } @Override - public boolean canKillProcess() { - return super.canKillProcess() && myShouldTryToKillSoftly; + protected boolean shouldKillProcessSoftly() { + return myShouldTryToKillSoftly; } @Override From c7848c251fec638086383b4b673aee7497b764b3 Mon Sep 17 00:00:00 2001 From: Dmitry Trofimov Date: Thu, 20 Dec 2012 18:21:50 +0100 Subject: [PATCH 09/10] Fix reload console action for remote interpreter (PY-8170). --- .../python/console/PydevConsoleRunner.java | 80 ++++++++++++------- .../console/RunPythonConsoleAction.java | 2 +- 2 files changed, 52 insertions(+), 30 deletions(-) diff --git a/python/src/com/jetbrains/python/console/PydevConsoleRunner.java b/python/src/com/jetbrains/python/console/PydevConsoleRunner.java index c62dcea3ef69..497ed1d3e92d 100644 --- a/python/src/com/jetbrains/python/console/PydevConsoleRunner.java +++ b/python/src/com/jetbrains/python/console/PydevConsoleRunner.java @@ -30,6 +30,7 @@ import com.intellij.openapi.project.DumbAwareAction; import com.intellij.openapi.project.Project; import com.intellij.openapi.projectRoots.Sdk; import com.intellij.openapi.util.Key; +import com.intellij.openapi.util.Pair; import com.intellij.openapi.util.io.FileUtil; import com.intellij.openapi.vfs.CharsetToolkit; import com.intellij.openapi.vfs.VirtualFile; @@ -75,13 +76,14 @@ public class PydevConsoleRunner extends AbstractConsoleRunnerWithHistory myConsoleListeners = Lists.newArrayList(); private final PyConsoleType myConsoleType; + private Map myEnvironmentVariables; private String myCommandLine; private String[] myStatementsToExecute = ArrayUtil.EMPTY_STRING_ARRAY; @@ -93,14 +95,12 @@ public class PydevConsoleRunner extends AbstractConsoleRunnerWithHistory environmentVariables) { super(project, consoleType.getTitle(), workingDir); mySdk = sdk; myConsoleType = consoleType; - myCommandLineArgumentsProvider = commandLineArgumentsProvider; - myPorts = ports; + myEnvironmentVariables = environmentVariables; } public void setStatementsToExecute(String... statementsToExecute) { @@ -133,7 +133,7 @@ public class PydevConsoleRunner extends AbstractConsoleRunnerWithHistory environmentVariables, final String... statements2execute) { final PydevConsoleRunner consoleRunner = create(project, sdk, consoleType, workingDirectory, environmentVariables); - if (consoleRunner == null) return null; consoleRunner.setStatementsToExecute(statements2execute); consoleRunner.run(); return consoleRunner; } public void run() { + myPorts = findAvailablePorts(getProject(), myConsoleType); + + assert myPorts != null; + + myCommandLineArgumentsProvider = createCommandLineArgumentsProvider(mySdk, myEnvironmentVariables, myPorts); + ProgressManager.getInstance().run(new Task.Backgroundable(getProject(), "Connecting to console", false) { public void run(@NotNull final ProgressIndicator indicator) { indicator.setText("Connecting to console..."); @@ -169,11 +174,16 @@ public class PydevConsoleRunner extends AbstractConsoleRunnerWithHistorynewHashMap()); } + @NotNull private static PydevConsoleRunner create(@NotNull final Project project, @NotNull final Sdk sdk, @NotNull final PyConsoleType consoleType, @Nullable final String workingDirectory, @NotNull final Map environmentVariables) { + return new PydevConsoleRunner(project, sdk, consoleType, workingDirectory, environmentVariables); + } + + private static int[] findAvailablePorts(Project project, PyConsoleType consoleType) { final int[] ports; try { // File "pydev/console/pydevconsole.py", line 223, in @@ -184,6 +194,12 @@ public class PydevConsoleRunner extends AbstractConsoleRunnerWithHistoryasList(e), consoleType.getTitle(), null); return null; } + return ports; + } + + private static CommandLineArgumentsProvider createCommandLineArgumentsProvider(final Sdk sdk, + final Map environmentVariables, + int[] ports) { final ArrayList args = new ArrayList(); args.add(sdk.getHomePath()); final String versionString = sdk.getVersionString(); @@ -194,7 +210,7 @@ public class PydevConsoleRunner extends AbstractConsoleRunnerWithHistory remotePorts = getRemotePortsFromProcess(remoteProcess); + + remoteProcess.addLocalTunnel(myPorts[0], data.getHost(), remotePorts.first); + remoteProcess.addRemoteTunnel(remotePorts.second, "localhost", myPorts[1]); + + + try { + myPydevConsoleCommunication = new PydevConsoleCommunication(getProject(), myPorts[0], remoteProcess, myPorts[1]); + return remoteProcess; + } + catch (Exception e) { + throw new ExecutionException(e.getMessage()); + } + } + + private static Pair getRemotePortsFromProcess(RemoteSshProcess process) throws ExecutionException { + Scanner s = new Scanner(process.getInputStream()); + long started = System.currentTimeMillis(); - while (!received && (System.currentTimeMillis() - started < PORTS_WAITING_TIMEOUT)) { + + while (System.currentTimeMillis() - started < PORTS_WAITING_TIMEOUT) { try { int port = s.nextInt(); int port2 = s.nextInt(); - received = true; - remoteProcess.addLocalTunnel(myPorts[0], data.getHost(), port); - remoteProcess.addRemoteTunnel(port2, "localhost", myPorts[1]); + + return Pair.create(port, port2); } catch (Exception e) { try { @@ -284,16 +314,8 @@ public class PydevConsoleRunner extends AbstractConsoleRunnerWithHistory Date: Thu, 20 Dec 2012 18:49:38 +0100 Subject: [PATCH 10/10] Added error handling to remote ports obtaining (PY-8298). --- .../python/console/PydevConsoleRunner.java | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/python/src/com/jetbrains/python/console/PydevConsoleRunner.java b/python/src/com/jetbrains/python/console/PydevConsoleRunner.java index 497ed1d3e92d..b55852641e6d 100644 --- a/python/src/com/jetbrains/python/console/PydevConsoleRunner.java +++ b/python/src/com/jetbrains/python/console/PydevConsoleRunner.java @@ -32,6 +32,7 @@ import com.intellij.openapi.projectRoots.Sdk; import com.intellij.openapi.util.Key; import com.intellij.openapi.util.Pair; import com.intellij.openapi.util.io.FileUtil; +import com.intellij.openapi.util.io.StreamUtil; import com.intellij.openapi.vfs.CharsetToolkit; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.psi.PsiElement; @@ -40,6 +41,7 @@ import com.intellij.psi.impl.source.tree.FileElement; import com.intellij.testFramework.LightVirtualFile; import com.intellij.util.ArrayUtil; import com.intellij.util.IJSwingUtilities; +import com.intellij.util.io.IOUtil; import com.intellij.util.net.NetUtils; import com.intellij.util.ui.UIUtil; import com.jetbrains.django.run.Runner; @@ -297,6 +299,7 @@ public class PydevConsoleRunner extends AbstractConsoleRunnerWithHistory getRemotePortsFromProcess(RemoteSshProcess process) throws ExecutionException { Scanner s = new Scanner(process.getInputStream()); + long started = System.currentTimeMillis(); while (System.currentTimeMillis() - started < PORTS_WAITING_TIMEOUT) { @@ -313,6 +316,24 @@ public class PydevConsoleRunner extends AbstractConsoleRunnerWithHistory