diff --git a/python/src/META-INF/python-plugin-common.xml b/python/src/META-INF/python-plugin-common.xml
index d587328ebc2a..8f2edb44912a 100644
--- a/python/src/META-INF/python-plugin-common.xml
+++ b/python/src/META-INF/python-plugin-common.xml
@@ -23,6 +23,7 @@
+
diff --git a/python/src/com/jetbrains/python/editor/PythonEnterHandler.java b/python/src/com/jetbrains/python/editor/PythonEnterHandler.java
new file mode 100644
index 000000000000..266bbdc3dc17
--- /dev/null
+++ b/python/src/com/jetbrains/python/editor/PythonEnterHandler.java
@@ -0,0 +1,121 @@
+package com.jetbrains.python.editor;
+
+import com.intellij.codeInsight.editorActions.TypedHandler;
+import com.intellij.codeInsight.editorActions.enter.EnterHandlerDelegate;
+import com.intellij.ide.DataManager;
+import com.intellij.openapi.actionSystem.DataContext;
+import com.intellij.openapi.editor.Document;
+import com.intellij.openapi.editor.Editor;
+import com.intellij.openapi.editor.actionSystem.EditorActionHandler;
+import com.intellij.openapi.util.Ref;
+import com.intellij.psi.*;
+import com.intellij.psi.util.PsiTreeUtil;
+import com.jetbrains.python.psi.*;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * @author yole
+ */
+public class PythonEnterHandler implements EnterHandlerDelegate {
+ private static final Class[] IMPLICIT_WRAP_CLASSES = new Class[]{
+ PsiComment.class,
+ PyStringLiteralExpression.class,
+ PyParenthesizedExpression.class,
+ PyListCompExpression.class,
+ PyDictCompExpression.class,
+ PySetCompExpression.class,
+ PyDictLiteralExpression.class,
+ PySetLiteralExpression.class,
+ PyListLiteralExpression.class,
+ PyArgumentList.class,
+ PyParameterList.class};
+
+ @Override
+ public Result preprocessEnter(PsiFile file,
+ Editor editor,
+ Ref caretOffset,
+ Ref caretAdvance,
+ DataContext dataContext,
+ EditorActionHandler originalHandler) {
+ if (!(file instanceof PyFile)) {
+ return Result.Continue;
+ }
+ Document doc = editor.getDocument();
+ PsiDocumentManager.getInstance(file.getProject()).commitDocument(doc);
+ final int offset = caretOffset.get();
+ final PsiElement element = file.findElementAt(offset);
+ if (element == null) {
+ return Result.Continue;
+ }
+ PsiElement statementBefore = findStatementBeforeCaret(file, offset);
+ PsiElement statementAfter = findStatementAfterCaret(file, offset);
+ if (statementBefore != statementAfter) { // Enter pressed at statement break
+ return Result.Continue;
+ }
+
+ if (statementBefore != null && PsiTreeUtil.hasErrorElements(statementBefore)) {
+ final Boolean autoWrapping = DataManager.getInstance().loadFromDataContext(dataContext, TypedHandler.AUTO_WRAP_LINE_IN_PROGRESS_KEY);
+ if (autoWrapping == null) {
+ // code is already bad, don't mess it up even further
+ return Result.Continue;
+ }
+ // if we're in middle of typing, it's expected that we will have error elements
+ }
+
+ PsiElement wrappableBefore = findBeforeCaret(file, offset, IMPLICIT_WRAP_CLASSES);
+ PsiElement wrappableAfter = findAfterCaret(file, offset, IMPLICIT_WRAP_CLASSES);
+ while (wrappableBefore != null) {
+ PsiElement next = PsiTreeUtil.getParentOfType(wrappableBefore, IMPLICIT_WRAP_CLASSES);
+ if (next == null) {
+ break;
+ }
+ wrappableBefore = next;
+ }
+ while (wrappableAfter != null) {
+ PsiElement next = PsiTreeUtil.getParentOfType(wrappableAfter, IMPLICIT_WRAP_CLASSES);
+ if (next == null) {
+ break;
+ }
+ wrappableAfter = next;
+ }
+ if (wrappableAfter == null || wrappableBefore != wrappableAfter) {
+ doc.insertString(offset, "\\");
+ caretOffset.set(offset+1);
+ }
+ return Result.Continue;
+ }
+
+ @Nullable
+ private static PsiElement findStatementBeforeCaret(PsiFile file, int offset) {
+ return findBeforeCaret(file, offset, PyStatement.class);
+ }
+
+ @Nullable
+ private static PsiElement findStatementAfterCaret(PsiFile file, int offset) {
+ return findAfterCaret(file, offset, PyStatement.class);
+ }
+
+ @Nullable
+ private static PsiElement findBeforeCaret(PsiFile file, int offset, Class extends PsiElement>... classes) {
+ while(offset > 0) {
+ offset--;
+ final PsiElement element = file.findElementAt(offset);
+ if (!(element instanceof PsiWhiteSpace)) {
+ return PsiTreeUtil.getParentOfType(element, classes);
+ }
+ }
+ return null;
+ }
+
+ @Nullable
+ private static PsiElement findAfterCaret(PsiFile file, int offset, Class extends PsiElement>... classes) {
+ while(offset < file.getTextLength()) {
+ final PsiElement element = file.findElementAt(offset);
+ if (!(element instanceof PsiWhiteSpace)) {
+ return PsiTreeUtil.getParentOfType(element, classes);
+ }
+ offset++;
+ }
+ return null;
+ }
+}
diff --git a/python/testData/wrap/BackslashOnWrap.after.py b/python/testData/wrap/BackslashOnWrap.after.py
new file mode 100644
index 000000000000..279565e6743f
--- /dev/null
+++ b/python/testData/wrap/BackslashOnWrap.after.py
@@ -0,0 +1,4 @@
+def copy_location(new_node, old_node):
+ for attr in 'lineno', 'col_offset':
+ if attr in old_node._attributes and attr in new_node._attributes and \
+ hasattr(old_node, attr):
\ No newline at end of file
diff --git a/python/testData/wrap/BackslashOnWrap.py b/python/testData/wrap/BackslashOnWrap.py
new file mode 100644
index 000000000000..4361dc9e8247
--- /dev/null
+++ b/python/testData/wrap/BackslashOnWrap.py
@@ -0,0 +1,3 @@
+def copy_location(new_node, old_node):
+ for attr in 'lineno', 'col_offset':
+ if attr in old_node._attributes and attr in new_node._attributes
\ No newline at end of file
diff --git a/python/testData/wrap/WrapInComment.after.py b/python/testData/wrap/WrapInComment.after.py
new file mode 100644
index 000000000000..f444f557dfb0
--- /dev/null
+++ b/python/testData/wrap/WrapInComment.after.py
@@ -0,0 +1,2 @@
+# Gallia est omnis divisa in partes tres, quarum unam incolumnt Belgae, aliam
+# Aquitani
diff --git a/python/testData/wrap/WrapInComment.py b/python/testData/wrap/WrapInComment.py
new file mode 100644
index 000000000000..7ec5072cc13f
--- /dev/null
+++ b/python/testData/wrap/WrapInComment.py
@@ -0,0 +1 @@
+# Gallia est omnis divisa in partes tres, quarum unam incolumnt Belgae, aliam
diff --git a/python/testSrc/com/jetbrains/python/PyEditingTest.java b/python/testSrc/com/jetbrains/python/PyEditingTest.java
index 194fe23bb744..44e64f3ae60e 100644
--- a/python/testSrc/com/jetbrains/python/PyEditingTest.java
+++ b/python/testSrc/com/jetbrains/python/PyEditingTest.java
@@ -97,7 +97,33 @@ public class PyEditingTest extends PyLightFixtureTestCase {
}
public void testEnterInLineComment() { // PY-1739
- assertEquals("# foo\n# bar", doTestTyping("# foo bar", 5, '\n'));
+ doTestEnter("# foo bar", "# foo \n# bar");
+ }
+
+ public void testEnterInStatement() {
+ doTestEnter("if a and b: pass", "if a \\\nand b: pass");
+ }
+
+ public void testEnterBeforeStatement() {
+ doTestEnter("def foo(): pass", "def foo(): \n pass");
+ }
+
+ public void testEnterInParameterList() {
+ doTestEnter("def foo(a,b): pass", "def foo(a,\n b): pass");
+ }
+
+ public void testEnterInTuple() {
+ doTestEnter("for x in 'a', 'b': pass", "for x in 'a', \\\n 'b': pass");
+ }
+
+ public void testEnterInCodeWithErrorElements() {
+ doTestEnter("z=1 2", "z=1 \n2");
+ }
+
+ private void doTestEnter(String before, final String after) {
+ int pos = before.indexOf("");
+ before = before.replace("", "");
+ assertEquals(after, doTestTyping(before, pos, '\n'));
}
private String doTestTyping(final String text, final int offset, final char character) {
diff --git a/python/testSrc/com/jetbrains/python/PyWrapTest.java b/python/testSrc/com/jetbrains/python/PyWrapTest.java
new file mode 100644
index 000000000000..4105ffb6e111
--- /dev/null
+++ b/python/testSrc/com/jetbrains/python/PyWrapTest.java
@@ -0,0 +1,45 @@
+package com.jetbrains.python;
+
+import com.intellij.psi.codeStyle.CodeStyleSettings;
+import com.intellij.psi.codeStyle.CodeStyleSettingsManager;
+import com.jetbrains.python.fixtures.PyLightFixtureTestCase;
+
+/**
+ * @author yole
+ */
+public class PyWrapTest extends PyLightFixtureTestCase {
+ private boolean myOldWrap;
+ private int myOldMargin;
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ final CodeStyleSettings settings = CodeStyleSettingsManager.getInstance(myFixture.getProject()).getCurrentSettings();
+ myOldWrap = settings.WRAP_WHEN_TYPING_REACHES_RIGHT_MARGIN;
+ myOldMargin = settings.RIGHT_MARGIN;
+ settings.WRAP_WHEN_TYPING_REACHES_RIGHT_MARGIN = true;
+ settings.RIGHT_MARGIN = 80;
+ }
+
+ @Override
+ protected void tearDown() throws Exception {
+ final CodeStyleSettings settings = CodeStyleSettingsManager.getInstance(myFixture.getProject()).getCurrentSettings();
+ settings.WRAP_WHEN_TYPING_REACHES_RIGHT_MARGIN = myOldWrap;
+ settings.RIGHT_MARGIN = myOldMargin;
+ super.tearDown();
+ }
+
+ public void testBackslashOnWrap() {
+ doTest("and hasattr(old_node, attr):");
+ }
+
+ public void testWrapInComment() {
+ doTest("Aquitani");
+ }
+
+ private void doTest(final String textToType) {
+ myFixture.configureByFile("wrap/" + getTestName(false) + ".py");
+ myFixture.type(textToType);
+ myFixture.checkResultByFile("wrap/" + getTestName(false) + ".after.py");
+ }
+}
diff --git a/python/testSrc/com/jetbrains/python/PythonAllTestsSuite.java b/python/testSrc/com/jetbrains/python/PythonAllTestsSuite.java
index 51eb66b3725b..4e31b5437626 100644
--- a/python/testSrc/com/jetbrains/python/PythonAllTestsSuite.java
+++ b/python/testSrc/com/jetbrains/python/PythonAllTestsSuite.java
@@ -20,6 +20,7 @@ public class PythonAllTestsSuite {
PythonParsingTest.class,
PyStringLiteralTest.class,
PyIndentTest.class,
+ PyWrapTest.class,
PyStatementPartsTest.class,
PythonHighlightingTest.class,
PyStubsTest.class,