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... 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... 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,