From 0e9810168aae6fd48344821caaec257ea06de1ed Mon Sep 17 00:00:00 2001 From: Mikhail Golubev Date: Mon, 2 Apr 2018 13:52:26 +0300 Subject: [PATCH] PY-10972 Properly align elements of expressions without closing bracket on enter instead of using artificially built indentation. It also covers all collection literals, comprehensions and glued string literals in parentheses most of which were overlooked in the previous implementation. --- .../com/jetbrains/python/PyElementTypes.java | 2 - .../jetbrains/python/formatter/PyBlock.java | 79 ++++++++----------- ...nterInIncompleteDictComprehension.after.py | 2 + .../enterInIncompleteDictComprehension.py | 1 + .../enterInIncompleteDictLiteral.after.py | 2 + .../editing/enterInIncompleteDictLiteral.py | 1 + ...teGluedStringLiteralInParentheses.after.py | 2 + ...completeGluedStringLiteralInParentheses.py | 1 + ...nterInIncompleteListComprehension.after.py | 2 + .../enterInIncompleteListComprehension.py | 1 + .../enterInIncompleteListLiteral.after.py | 2 + .../editing/enterInIncompleteListLiteral.py | 1 + ...nIncompleteParenthesizedGenerator.after.py | 2 + ...enterInIncompleteParenthesizedGenerator.py | 1 + ...enterInIncompleteSetComprehension.after.py | 2 + .../enterInIncompleteSetComprehension.py | 1 + .../enterInIncompleteSetLiteral.after.py | 2 + .../editing/enterInIncompleteSetLiteral.py | 1 + .../enterInIncompleteTupleLiteral.after.py | 2 + .../editing/enterInIncompleteTupleLiteral.py | 1 + .../com/jetbrains/python/PyEditingTest.java | 48 ++++++++++- 21 files changed, 109 insertions(+), 47 deletions(-) create mode 100644 python/testData/editing/enterInIncompleteDictComprehension.after.py create mode 100644 python/testData/editing/enterInIncompleteDictComprehension.py create mode 100644 python/testData/editing/enterInIncompleteDictLiteral.after.py create mode 100644 python/testData/editing/enterInIncompleteDictLiteral.py create mode 100644 python/testData/editing/enterInIncompleteGluedStringLiteralInParentheses.after.py create mode 100644 python/testData/editing/enterInIncompleteGluedStringLiteralInParentheses.py create mode 100644 python/testData/editing/enterInIncompleteListComprehension.after.py create mode 100644 python/testData/editing/enterInIncompleteListComprehension.py create mode 100644 python/testData/editing/enterInIncompleteListLiteral.after.py create mode 100644 python/testData/editing/enterInIncompleteListLiteral.py create mode 100644 python/testData/editing/enterInIncompleteParenthesizedGenerator.after.py create mode 100644 python/testData/editing/enterInIncompleteParenthesizedGenerator.py create mode 100644 python/testData/editing/enterInIncompleteSetComprehension.after.py create mode 100644 python/testData/editing/enterInIncompleteSetComprehension.py create mode 100644 python/testData/editing/enterInIncompleteSetLiteral.after.py create mode 100644 python/testData/editing/enterInIncompleteSetLiteral.py create mode 100644 python/testData/editing/enterInIncompleteTupleLiteral.after.py create mode 100644 python/testData/editing/enterInIncompleteTupleLiteral.py diff --git a/python/src/com/jetbrains/python/PyElementTypes.java b/python/src/com/jetbrains/python/PyElementTypes.java index 09f7bd64178e..82292eb0d50b 100644 --- a/python/src/com/jetbrains/python/PyElementTypes.java +++ b/python/src/com/jetbrains/python/PyElementTypes.java @@ -121,8 +121,6 @@ public interface PyElementTypes { PyElementType SET_COMP_EXPRESSION = new PyElementType("SET_COMP_EXPRESSION", PySetCompExpressionImpl.class); PyElementType DICT_COMP_EXPRESSION = new PyElementType("DICT_COMP_EXPRESSION", PyDictCompExpressionImpl.class); - TokenSet LIST_LIKE_EXPRESSIONS = TokenSet.create(LIST_LITERAL_EXPRESSION, LIST_COMP_EXPRESSION, TUPLE_EXPRESSION); - TokenSet STATEMENT_LISTS = TokenSet.create(STATEMENT_LIST); TokenSet BINARY_OPS = TokenSet.create(PyTokenTypes.OR_KEYWORD, PyTokenTypes.AND_KEYWORD, PyTokenTypes.LT, PyTokenTypes.GT, diff --git a/python/src/com/jetbrains/python/formatter/PyBlock.java b/python/src/com/jetbrains/python/formatter/PyBlock.java index 0d7847e94890..c0545c46776a 100644 --- a/python/src/com/jetbrains/python/formatter/PyBlock.java +++ b/python/src/com/jetbrains/python/formatter/PyBlock.java @@ -17,7 +17,6 @@ package com.jetbrains.python.formatter; import com.intellij.formatting.*; import com.intellij.lang.ASTNode; -import com.intellij.openapi.editor.Document; import com.intellij.openapi.util.Key; import com.intellij.openapi.util.TextRange; import com.intellij.psi.*; @@ -892,6 +891,8 @@ public class PyBlock implements ASTBlock { @NotNull public ChildAttributes getChildAttributes(int newChildIndex) { int statementListsBelow = 0; + PyBlock incompleteAlignedBlock = null; + if (newChildIndex > 0) { // always pass decision to a sane block from top level from file or definition if (myNode.getPsi() instanceof PyFile || myNode.getElementType() == PyTokenTypes.COLON) { @@ -935,6 +936,19 @@ public class PyBlock implements ASTBlock { } lastChild = getLastNonSpaceChild(lastChild, true); } + + if ((prevElt instanceof PySequenceExpression || prevElt instanceof PyComprehensionElement) && + prevElt.getLastChild() instanceof PsiErrorElement) { + incompleteAlignedBlock = insertAfterBlock; + } + else if (prevElt instanceof PyParenthesizedExpression && prevElt.getLastChild() instanceof PsiErrorElement) { + final PyExpression contained = ((PyParenthesizedExpression)prevElt).getContainedExpression(); + // In case of parenthesized string literal we can use both the alignment of the containing parenthesized expression + // and the literal itself, since it's not clear whether user is going to insert another string node or the closing parenthesis + if (contained instanceof PyTupleExpression || contained instanceof PyStringLiteralExpression) { + incompleteAlignedBlock = insertAfterBlock.getSubBlockByNode(contained.getNode()); + } + } } // HACKETY-HACK @@ -959,7 +973,7 @@ public class PyBlock implements ASTBlock { final Indent childIndent = getChildIndent(newChildIndex); - final Alignment childAlignment = getChildAlignment(); + final Alignment childAlignment = incompleteAlignedBlock != null ? incompleteAlignedBlock.getChildAlignment() : getChildAlignment(); return new ChildAttributes(childIndent, childAlignment); } @@ -985,15 +999,30 @@ public class PyBlock implements ASTBlock { @Nullable private Alignment getChildAlignment() { - if (ourListElementTypes.contains(myNode.getElementType()) || myNode.getElementType() == PyElementTypes.SLICE_ITEM) { + // TODO merge it with needListAlignment(ASTNode) + final IElementType nodeType = myNode.getElementType(); + if (ourListElementTypes.contains(nodeType) || + nodeType == PyElementTypes.SLICE_ITEM || + nodeType == PyElementTypes.STRING_LITERAL_EXPRESSION) { if (isInControlStatement()) { return null; } - if (myNode.getPsi() instanceof PyParameterList && !myContext.getSettings().ALIGN_MULTILINE_PARAMETERS) { + final PsiElement elem = myNode.getPsi(); + if (elem instanceof PyParameterList && !myContext.getSettings().ALIGN_MULTILINE_PARAMETERS) { return null; } - if (myNode.getPsi() instanceof PyDictLiteralExpression) { - final PyKeyValueExpression lastElement = ArrayUtil.getLastElement(((PyDictLiteralExpression)myNode.getPsi()).getElements()); + if ((elem instanceof PySequenceExpression || elem instanceof PyComprehensionElement) && + !myContext.getPySettings().ALIGN_COLLECTIONS_AND_COMPREHENSIONS) { + return null; + } + if (elem instanceof PyParenthesizedExpression) { + final PyExpression parenthesized = ((PyParenthesizedExpression)elem).getContainedExpression(); + if (parenthesized instanceof PyTupleExpression && !myContext.getPySettings().ALIGN_COLLECTIONS_AND_COMPREHENSIONS) { + return null; + } + } + if (elem instanceof PyDictLiteralExpression) { + final PyKeyValueExpression lastElement = ArrayUtil.getLastElement(((PyDictLiteralExpression)elem).getElements()); if (lastElement == null || lastElement.getValue() == null /* incomplete */) { return null; } @@ -1025,44 +1054,6 @@ public class PyBlock implements ASTBlock { return Indent.getNormalIndent(); } } - else if (lastChild != null && PyElementTypes.LIST_LIKE_EXPRESSIONS.contains(lastChild.getElementType())) { - // handle pressing enter at the end of a list literal when there's no closing paren or bracket - final ASTNode lastLastChild = lastChild.getLastChildNode(); - if (lastLastChild != null && lastLastChild.getPsi() instanceof PsiErrorElement) { - // we're at a place like this: [foo, ... bar, - // we'd rather align to foo. this may be not a multiple of tabs. - final PsiElement expr = lastChild.getPsi(); - PsiElement exprItem = expr.getFirstChild(); - boolean found = false; - while (exprItem != null) { // find a worthy element to align to - if (exprItem instanceof PyElement) { - found = true; // align to foo in "[foo," - break; - } - if (exprItem instanceof PsiComment) { - found = true; // align to foo in "[ # foo," - break; - } - exprItem = exprItem.getNextSibling(); - } - if (found) { - final PsiDocumentManager docMgr = PsiDocumentManager.getInstance(exprItem.getProject()); - final Document doc = docMgr.getDocument(exprItem.getContainingFile()); - if (doc != null) { - int lineNum = doc.getLineNumber(exprItem.getTextOffset()); - final int itemCol = exprItem.getTextOffset() - doc.getLineStartOffset(lineNum); - final PsiElement hereElt = getNode().getPsi(); - lineNum = doc.getLineNumber(hereElt.getTextOffset()); - final int nodeCol = hereElt.getTextOffset() - doc.getLineStartOffset(lineNum); - final int padding = itemCol - nodeCol; - if (padding > 0) { // negative is a syntax error, but possible - return Indent.getSpaceIndent(padding); - } - } - } - return Indent.getContinuationIndent(); // a fallback - } - } if (afterNode != null && afterNode.getElementType() == PyElementTypes.KEY_VALUE_EXPRESSION) { final PyKeyValueExpression keyValue = (PyKeyValueExpression)afterNode.getPsi(); diff --git a/python/testData/editing/enterInIncompleteDictComprehension.after.py b/python/testData/editing/enterInIncompleteDictComprehension.after.py new file mode 100644 index 000000000000..695e9f836db8 --- /dev/null +++ b/python/testData/editing/enterInIncompleteDictComprehension.after.py @@ -0,0 +1,2 @@ +mapping = {foo: bar for + foo \ No newline at end of file diff --git a/python/testData/editing/enterInIncompleteDictComprehension.py b/python/testData/editing/enterInIncompleteDictComprehension.py new file mode 100644 index 000000000000..0adeda094a3b --- /dev/null +++ b/python/testData/editing/enterInIncompleteDictComprehension.py @@ -0,0 +1 @@ +mapping = {foo: bar for \ No newline at end of file diff --git a/python/testData/editing/enterInIncompleteDictLiteral.after.py b/python/testData/editing/enterInIncompleteDictLiteral.after.py new file mode 100644 index 000000000000..cf0f47f1e826 --- /dev/null +++ b/python/testData/editing/enterInIncompleteDictLiteral.after.py @@ -0,0 +1,2 @@ +d = {'foo': 'bar', + 'baz' \ No newline at end of file diff --git a/python/testData/editing/enterInIncompleteDictLiteral.py b/python/testData/editing/enterInIncompleteDictLiteral.py new file mode 100644 index 000000000000..bd725d628b3e --- /dev/null +++ b/python/testData/editing/enterInIncompleteDictLiteral.py @@ -0,0 +1 @@ +d = {'foo': 'bar', \ No newline at end of file diff --git a/python/testData/editing/enterInIncompleteGluedStringLiteralInParentheses.after.py b/python/testData/editing/enterInIncompleteGluedStringLiteralInParentheses.after.py new file mode 100644 index 000000000000..c883b96bc959 --- /dev/null +++ b/python/testData/editing/enterInIncompleteGluedStringLiteralInParentheses.after.py @@ -0,0 +1,2 @@ +xs = ('foo' + 'bar' \ No newline at end of file diff --git a/python/testData/editing/enterInIncompleteGluedStringLiteralInParentheses.py b/python/testData/editing/enterInIncompleteGluedStringLiteralInParentheses.py new file mode 100644 index 000000000000..45717f760366 --- /dev/null +++ b/python/testData/editing/enterInIncompleteGluedStringLiteralInParentheses.py @@ -0,0 +1 @@ +xs = ('foo' \ No newline at end of file diff --git a/python/testData/editing/enterInIncompleteListComprehension.after.py b/python/testData/editing/enterInIncompleteListComprehension.after.py new file mode 100644 index 000000000000..d8cd34743d7d --- /dev/null +++ b/python/testData/editing/enterInIncompleteListComprehension.after.py @@ -0,0 +1,2 @@ +xs = [foo for + foo \ No newline at end of file diff --git a/python/testData/editing/enterInIncompleteListComprehension.py b/python/testData/editing/enterInIncompleteListComprehension.py new file mode 100644 index 000000000000..3a7f9588366f --- /dev/null +++ b/python/testData/editing/enterInIncompleteListComprehension.py @@ -0,0 +1 @@ +xs = [foo for \ No newline at end of file diff --git a/python/testData/editing/enterInIncompleteListLiteral.after.py b/python/testData/editing/enterInIncompleteListLiteral.after.py new file mode 100644 index 000000000000..9dcabfeb7a6b --- /dev/null +++ b/python/testData/editing/enterInIncompleteListLiteral.after.py @@ -0,0 +1,2 @@ +xs = ['foo', 'bar', + 'baz' \ No newline at end of file diff --git a/python/testData/editing/enterInIncompleteListLiteral.py b/python/testData/editing/enterInIncompleteListLiteral.py new file mode 100644 index 000000000000..c78cd1da3549 --- /dev/null +++ b/python/testData/editing/enterInIncompleteListLiteral.py @@ -0,0 +1 @@ +xs = ['foo', 'bar', \ No newline at end of file diff --git a/python/testData/editing/enterInIncompleteParenthesizedGenerator.after.py b/python/testData/editing/enterInIncompleteParenthesizedGenerator.after.py new file mode 100644 index 000000000000..83d932b6fd2a --- /dev/null +++ b/python/testData/editing/enterInIncompleteParenthesizedGenerator.after.py @@ -0,0 +1,2 @@ +xs = (foo for + foo \ No newline at end of file diff --git a/python/testData/editing/enterInIncompleteParenthesizedGenerator.py b/python/testData/editing/enterInIncompleteParenthesizedGenerator.py new file mode 100644 index 000000000000..916c8c81e01f --- /dev/null +++ b/python/testData/editing/enterInIncompleteParenthesizedGenerator.py @@ -0,0 +1 @@ +xs = (foo for \ No newline at end of file diff --git a/python/testData/editing/enterInIncompleteSetComprehension.after.py b/python/testData/editing/enterInIncompleteSetComprehension.after.py new file mode 100644 index 000000000000..79c63c865bdf --- /dev/null +++ b/python/testData/editing/enterInIncompleteSetComprehension.after.py @@ -0,0 +1,2 @@ +xs = {foo for + foo \ No newline at end of file diff --git a/python/testData/editing/enterInIncompleteSetComprehension.py b/python/testData/editing/enterInIncompleteSetComprehension.py new file mode 100644 index 000000000000..1aaf3a39563c --- /dev/null +++ b/python/testData/editing/enterInIncompleteSetComprehension.py @@ -0,0 +1 @@ +xs = {foo for \ No newline at end of file diff --git a/python/testData/editing/enterInIncompleteSetLiteral.after.py b/python/testData/editing/enterInIncompleteSetLiteral.after.py new file mode 100644 index 000000000000..826687d9202b --- /dev/null +++ b/python/testData/editing/enterInIncompleteSetLiteral.after.py @@ -0,0 +1,2 @@ +xs = {'foo', 'bar', + 'baz' \ No newline at end of file diff --git a/python/testData/editing/enterInIncompleteSetLiteral.py b/python/testData/editing/enterInIncompleteSetLiteral.py new file mode 100644 index 000000000000..deae075a14f7 --- /dev/null +++ b/python/testData/editing/enterInIncompleteSetLiteral.py @@ -0,0 +1 @@ +xs = {'foo', 'bar', \ No newline at end of file diff --git a/python/testData/editing/enterInIncompleteTupleLiteral.after.py b/python/testData/editing/enterInIncompleteTupleLiteral.after.py new file mode 100644 index 000000000000..07315bfc850b --- /dev/null +++ b/python/testData/editing/enterInIncompleteTupleLiteral.after.py @@ -0,0 +1,2 @@ +xs = ('foo', 'bar', + 'baz' \ No newline at end of file diff --git a/python/testData/editing/enterInIncompleteTupleLiteral.py b/python/testData/editing/enterInIncompleteTupleLiteral.py new file mode 100644 index 000000000000..630317d348b4 --- /dev/null +++ b/python/testData/editing/enterInIncompleteTupleLiteral.py @@ -0,0 +1 @@ +xs = ('foo', 'bar', \ No newline at end of file diff --git a/python/testSrc/com/jetbrains/python/PyEditingTest.java b/python/testSrc/com/jetbrains/python/PyEditingTest.java index 027c26c09731..2484bbdda8e6 100644 --- a/python/testSrc/com/jetbrains/python/PyEditingTest.java +++ b/python/testSrc/com/jetbrains/python/PyEditingTest.java @@ -365,7 +365,8 @@ public class PyEditingTest extends PyTestCase { public void testEndOfStringInParenth() { doTestEnter("print (\"foo\"\n" + " \"bar\")", - "print (\"foo\"\n\n" + + "print (\"foo\"\n" + + " \n" + " \"bar\")"); } @@ -596,4 +597,49 @@ public class PyEditingTest extends PyTestCase { public void testIncompleteFunctionTypeComment() { doTypingTest('.'); } + + // PY-10972 + public void testEnterInIncompleteTupleLiteral() { + doTypingTest("\n'baz'"); + } + + // PY-10972 + public void testEnterInIncompleteListLiteral() { + doTypingTest("\n'baz'"); + } + + // PY-10972 + public void testEnterInIncompleteSetLiteral() { + doTypingTest("\n'baz'"); + } + + // PY-10972 + public void testEnterInIncompleteDictLiteral() { + doTypingTest("\n'baz'"); + } + + // PY-10972 + public void testEnterInIncompleteGluedStringLiteralInParentheses() { + doTypingTest("\n'bar'"); + } + + // PY-10972 + public void testEnterInIncompleteListComprehension() { + doTypingTest("\nfoo"); + } + + // PY-10972 + public void testEnterInIncompleteSetComprehension() { + doTypingTest("\nfoo"); + } + + // PY-10972 + public void testEnterInIncompleteDictComprehension() { + doTypingTest("\nfoo"); + } + + // PY-10972 + public void testEnterInIncompleteParenthesizedGenerator() { + doTypingTest("\nfoo"); + } }