From 98d196aec628ddb1f4b25c12905e2125519e5db9 Mon Sep 17 00:00:00 2001 From: Mikhail Golubev Date: Wed, 5 May 2021 15:28:50 +0300 Subject: [PATCH] PY-48009 Add automatic indent inside match statements Namely, for their case clauses and inner comments. Right now, each of them is indented on its own, as a separate formatting block. It's still not entirely clear whether we should have a dedicated indented container element for case clauses, similar to PyStatementList for statements. It might simplify the formatter and some editing actions, but cause confusion between the two container elements. GitOrigin-RevId: 69184d2f8f78e2e113e8f40a310bb13ac0b5e71a --- .../jetbrains/python/formatter/PyBlock.java | 11 ++++++-- .../python/editor/PythonEnterHandler.java | 4 +-- ...terAfterColonOfCaseClauseWithBody.after.py | 4 +++ .../enterAfterColonOfCaseClauseWithBody.py | 3 +++ ...AfterColonOfCaseClauseWithoutBody.after.py | 3 +++ .../enterAfterColonOfCaseClauseWithoutBody.py | 2 ++ ...rColonOfMatchStatementWithClauses.after.py | 4 +++ ...erAfterColonOfMatchStatementWithClauses.py | 3 +++ ...lonOfMatchStatementWithoutClauses.after.py | 2 ++ ...fterColonOfMatchStatementWithoutClauses.py | 1 + ...indentOfCaseClausesInsideMatchStatement.py | 5 ++++ ...OfCaseClausesInsideMatchStatement_after.py | 5 ++++ .../indentOfCommentsInsideMatchStatement.py | 12 +++++++++ ...entOfCommentsInsideMatchStatement_after.py | 12 +++++++++ ...tternMatchingLeadingAndTrailingComments.py | 7 +++++ ...ternMatchingLeadingAndTrailingComments.txt | 27 +++++++++++++++++++ .../com/jetbrains/python/PyEditingTest.java | 20 ++++++++++++++ .../com/jetbrains/python/PyFormatterTest.java | 10 +++++++ .../python/parsing/PythonParsingTest.java | 4 +++ 19 files changed, 135 insertions(+), 4 deletions(-) create mode 100644 python/testData/editing/enterAfterColonOfCaseClauseWithBody.after.py create mode 100644 python/testData/editing/enterAfterColonOfCaseClauseWithBody.py create mode 100644 python/testData/editing/enterAfterColonOfCaseClauseWithoutBody.after.py create mode 100644 python/testData/editing/enterAfterColonOfCaseClauseWithoutBody.py create mode 100644 python/testData/editing/enterAfterColonOfMatchStatementWithClauses.after.py create mode 100644 python/testData/editing/enterAfterColonOfMatchStatementWithClauses.py create mode 100644 python/testData/editing/enterAfterColonOfMatchStatementWithoutClauses.after.py create mode 100644 python/testData/editing/enterAfterColonOfMatchStatementWithoutClauses.py create mode 100644 python/testData/formatter/indentOfCaseClausesInsideMatchStatement.py create mode 100644 python/testData/formatter/indentOfCaseClausesInsideMatchStatement_after.py create mode 100644 python/testData/formatter/indentOfCommentsInsideMatchStatement.py create mode 100644 python/testData/formatter/indentOfCommentsInsideMatchStatement_after.py create mode 100644 python/testData/psi/PatternMatchingLeadingAndTrailingComments.py create mode 100644 python/testData/psi/PatternMatchingLeadingAndTrailingComments.txt diff --git a/python/python-psi-impl/src/com/jetbrains/python/formatter/PyBlock.java b/python/python-psi-impl/src/com/jetbrains/python/formatter/PyBlock.java index c8b7235bec8f..0a9776bb9ea3 100644 --- a/python/python-psi-impl/src/com/jetbrains/python/formatter/PyBlock.java +++ b/python/python-psi-impl/src/com/jetbrains/python/formatter/PyBlock.java @@ -217,6 +217,9 @@ public class PyBlock implements ASTBlock { childIndent = Indent.getNormalIndent(); } } + else if (childType == PyElementTypes.CASE_CLAUSE) { + childIndent = Indent.getNormalIndent(); + } else if (childType == PyElementTypes.IMPORT_ELEMENT) { if (parentType == PyElementTypes.FROM_IMPORT_STATEMENT) { childWrap = myFromImportWrapping; @@ -226,7 +229,8 @@ public class PyBlock implements ASTBlock { } childIndent = Indent.getNormalIndent(); } - if (childType == PyTokenTypes.END_OF_LINE_COMMENT && parentType == PyElementTypes.FROM_IMPORT_STATEMENT) { + if (childType == PyTokenTypes.END_OF_LINE_COMMENT && (parentType == PyElementTypes.FROM_IMPORT_STATEMENT || + parentType == PyElementTypes.MATCH_STATEMENT)) { childIndent = Indent.getNormalIndent(); } @@ -1058,6 +1062,7 @@ public class PyBlock implements ASTBlock { @NotNull private Indent getChildIndent(int newChildIndex) { + final IElementType parentType = myNode.getElementType(); final ASTNode afterNode = getAfterNode(newChildIndex); final ASTNode lastChild = getLastNonSpaceChild(myNode, false); if (lastChild != null && lastChild.getElementType() == PyElementTypes.STATEMENT_LIST && mySubBlocks.size() >= newChildIndex) { @@ -1078,6 +1083,9 @@ public class PyBlock implements ASTBlock { return Indent.getNormalIndent(); } } + if (parentType == PyElementTypes.MATCH_STATEMENT && afterNode != null && afterNode.getElementType() == PyTokenTypes.COLON) { + return Indent.getNormalIndent(); + } if (afterNode != null && afterNode.getElementType() == PyElementTypes.KEY_VALUE_EXPRESSION) { final PyKeyValueExpression keyValue = (PyKeyValueExpression)afterNode.getPsi(); @@ -1086,7 +1094,6 @@ public class PyBlock implements ASTBlock { } } - final IElementType parentType = myNode.getElementType(); // constructs that imply indent for their children final PyCodeStyleSettings settings = myContext.getPySettings(); if ((parentType == PyElementTypes.PARAMETER_LIST && settings.USE_CONTINUATION_INDENT_FOR_PARAMETERS) || diff --git a/python/src/com/jetbrains/python/editor/PythonEnterHandler.java b/python/src/com/jetbrains/python/editor/PythonEnterHandler.java index 823d6b84969e..a69b701513f5 100644 --- a/python/src/com/jetbrains/python/editor/PythonEnterHandler.java +++ b/python/src/com/jetbrains/python/editor/PythonEnterHandler.java @@ -260,12 +260,12 @@ public class PythonEnterHandler extends EnterHandlerDelegateAdapter { @Nullable private static PsiElement findStatementBeforeCaret(ASTNode node) { - return findBeforeCaret(node, PyStatement.class); + return findBeforeCaret(node, PyStatement.class, PyStatementPart.class); } @Nullable private static PsiElement findStatementAfterCaret(ASTNode node) { - return findAfterCaret(node, PyStatement.class); + return findAfterCaret(node, PyStatement.class, PyStatementPart.class); } private static PsiElement findBeforeCaret(ASTNode atCaret, Class... classes) { diff --git a/python/testData/editing/enterAfterColonOfCaseClauseWithBody.after.py b/python/testData/editing/enterAfterColonOfCaseClauseWithBody.after.py new file mode 100644 index 000000000000..1d83a0b16831 --- /dev/null +++ b/python/testData/editing/enterAfterColonOfCaseClauseWithBody.after.py @@ -0,0 +1,4 @@ +match x: + case 42: + + pass \ No newline at end of file diff --git a/python/testData/editing/enterAfterColonOfCaseClauseWithBody.py b/python/testData/editing/enterAfterColonOfCaseClauseWithBody.py new file mode 100644 index 000000000000..5a9efb7d28a4 --- /dev/null +++ b/python/testData/editing/enterAfterColonOfCaseClauseWithBody.py @@ -0,0 +1,3 @@ +match x: + case 42: + pass \ No newline at end of file diff --git a/python/testData/editing/enterAfterColonOfCaseClauseWithoutBody.after.py b/python/testData/editing/enterAfterColonOfCaseClauseWithoutBody.after.py new file mode 100644 index 000000000000..43a3dd0d72ed --- /dev/null +++ b/python/testData/editing/enterAfterColonOfCaseClauseWithoutBody.after.py @@ -0,0 +1,3 @@ +match x: + case 42: + \ No newline at end of file diff --git a/python/testData/editing/enterAfterColonOfCaseClauseWithoutBody.py b/python/testData/editing/enterAfterColonOfCaseClauseWithoutBody.py new file mode 100644 index 000000000000..2fd2bfab59ea --- /dev/null +++ b/python/testData/editing/enterAfterColonOfCaseClauseWithoutBody.py @@ -0,0 +1,2 @@ +match x: + case 42: \ No newline at end of file diff --git a/python/testData/editing/enterAfterColonOfMatchStatementWithClauses.after.py b/python/testData/editing/enterAfterColonOfMatchStatementWithClauses.after.py new file mode 100644 index 000000000000..503cc23f0dab --- /dev/null +++ b/python/testData/editing/enterAfterColonOfMatchStatementWithClauses.after.py @@ -0,0 +1,4 @@ +match x: + + case 42: + pass \ No newline at end of file diff --git a/python/testData/editing/enterAfterColonOfMatchStatementWithClauses.py b/python/testData/editing/enterAfterColonOfMatchStatementWithClauses.py new file mode 100644 index 000000000000..baab0af55d4f --- /dev/null +++ b/python/testData/editing/enterAfterColonOfMatchStatementWithClauses.py @@ -0,0 +1,3 @@ +match x: + case 42: + pass \ No newline at end of file diff --git a/python/testData/editing/enterAfterColonOfMatchStatementWithoutClauses.after.py b/python/testData/editing/enterAfterColonOfMatchStatementWithoutClauses.after.py new file mode 100644 index 000000000000..6b6c3a19a558 --- /dev/null +++ b/python/testData/editing/enterAfterColonOfMatchStatementWithoutClauses.after.py @@ -0,0 +1,2 @@ +match x: + \ No newline at end of file diff --git a/python/testData/editing/enterAfterColonOfMatchStatementWithoutClauses.py b/python/testData/editing/enterAfterColonOfMatchStatementWithoutClauses.py new file mode 100644 index 000000000000..6d9420f3b0f4 --- /dev/null +++ b/python/testData/editing/enterAfterColonOfMatchStatementWithoutClauses.py @@ -0,0 +1 @@ +match x: \ No newline at end of file diff --git a/python/testData/formatter/indentOfCaseClausesInsideMatchStatement.py b/python/testData/formatter/indentOfCaseClausesInsideMatchStatement.py new file mode 100644 index 000000000000..c26401adeb78 --- /dev/null +++ b/python/testData/formatter/indentOfCaseClausesInsideMatchStatement.py @@ -0,0 +1,5 @@ +match x: + case 1: + pass + case 2: + pass \ No newline at end of file diff --git a/python/testData/formatter/indentOfCaseClausesInsideMatchStatement_after.py b/python/testData/formatter/indentOfCaseClausesInsideMatchStatement_after.py new file mode 100644 index 000000000000..344853a80f7b --- /dev/null +++ b/python/testData/formatter/indentOfCaseClausesInsideMatchStatement_after.py @@ -0,0 +1,5 @@ +match x: + case 1: + pass + case 2: + pass diff --git a/python/testData/formatter/indentOfCommentsInsideMatchStatement.py b/python/testData/formatter/indentOfCommentsInsideMatchStatement.py new file mode 100644 index 000000000000..27a921ad6e65 --- /dev/null +++ b/python/testData/formatter/indentOfCommentsInsideMatchStatement.py @@ -0,0 +1,12 @@ +match x: + # first 1 + # first 2 + case 1: + pass + # intermediate 1 + # intermediate 2 + case 2: + pass + # trailing 1 + # trailing 2 +# unrelated \ No newline at end of file diff --git a/python/testData/formatter/indentOfCommentsInsideMatchStatement_after.py b/python/testData/formatter/indentOfCommentsInsideMatchStatement_after.py new file mode 100644 index 000000000000..82e8ee4a48ee --- /dev/null +++ b/python/testData/formatter/indentOfCommentsInsideMatchStatement_after.py @@ -0,0 +1,12 @@ +match x: + # first 1 + # first 2 + case 1: + pass + # intermediate 1 + # intermediate 2 + case 2: + pass + # trailing 1 + # trailing 2 +# unrelated diff --git a/python/testData/psi/PatternMatchingLeadingAndTrailingComments.py b/python/testData/psi/PatternMatchingLeadingAndTrailingComments.py new file mode 100644 index 000000000000..7851e6e7d005 --- /dev/null +++ b/python/testData/psi/PatternMatchingLeadingAndTrailingComments.py @@ -0,0 +1,7 @@ +match x: + # match leading comment + case 1: + pass + # case trailing comment + # match trailing comment +# unrelated comment \ No newline at end of file diff --git a/python/testData/psi/PatternMatchingLeadingAndTrailingComments.txt b/python/testData/psi/PatternMatchingLeadingAndTrailingComments.txt new file mode 100644 index 000000000000..8e0cd651b27c --- /dev/null +++ b/python/testData/psi/PatternMatchingLeadingAndTrailingComments.txt @@ -0,0 +1,27 @@ +PyFile:PatternMatchingLeadingAndTrailingComments.py + PyMatchStatement + PsiElement(Py:MATCH_KEYWORD)('match') + PsiWhiteSpace(' ') + PyReferenceExpression: x + PsiElement(Py:IDENTIFIER)('x') + PsiElement(Py:COLON)(':') + PsiWhiteSpace('\n ') + PsiComment(Py:END_OF_LINE_COMMENT)('# match leading comment') + PsiWhiteSpace('\n ') + PyCaseClause + PsiElement(Py:CASE_KEYWORD)('case') + PsiWhiteSpace(' ') + PyLiteralPattern + PyNumericLiteralExpression + PsiElement(Py:INTEGER_LITERAL)('1') + PsiElement(Py:COLON)(':') + PsiWhiteSpace('\n ') + PyStatementList + PyPassStatement + PsiElement(Py:PASS_KEYWORD)('pass') + PsiWhiteSpace('\n ') + PsiComment(Py:END_OF_LINE_COMMENT)('# case trailing comment') + PsiWhiteSpace('\n ') + PsiComment(Py:END_OF_LINE_COMMENT)('# match trailing comment') + PsiWhiteSpace('\n') + PsiComment(Py:END_OF_LINE_COMMENT)('# unrelated comment') \ 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 9fe720f35e00..304f44344757 100644 --- a/python/testSrc/com/jetbrains/python/PyEditingTest.java +++ b/python/testSrc/com/jetbrains/python/PyEditingTest.java @@ -832,6 +832,26 @@ public class PyEditingTest extends PyTestCase { "s = f'{42::3d}'"); } + // PY-48009 + public void testEnterAfterColonOfMatchStatementWithoutClauses() { + doTypingTest('\n'); + } + + // PY-48009 + public void testEnterAfterColonOfMatchStatementWithClauses() { + doTypingTest('\n'); + } + + // PY-48009 + public void testEnterAfterColonOfCaseClauseWithoutBody() { + doTypingTest('\n'); + } + + // PY-48009 + public void testEnterAfterColonOfCaseClauseWithBody() { + doTypingTest('\n'); + } + @NotNull private PyCodeStyleSettings getPythonCodeStyleSettings() { return getCodeStyleSettings().getCustomSettings(PyCodeStyleSettings.class); diff --git a/python/testSrc/com/jetbrains/python/PyFormatterTest.java b/python/testSrc/com/jetbrains/python/PyFormatterTest.java index 202a00821c7c..b789dc25f2dc 100644 --- a/python/testSrc/com/jetbrains/python/PyFormatterTest.java +++ b/python/testSrc/com/jetbrains/python/PyFormatterTest.java @@ -1073,4 +1073,14 @@ public class PyFormatterTest extends PyTestCase { public void testModuleLevelDunderWithImports() { doTest(); } + + // PY-48009 + public void testIndentOfCaseClausesInsideMatchStatement() { + doTest(); + } + + // PY-48009 + public void testIndentOfCommentsInsideMatchStatement() { + doTest(); + } } diff --git a/python/testSrc/com/jetbrains/python/parsing/PythonParsingTest.java b/python/testSrc/com/jetbrains/python/parsing/PythonParsingTest.java index 7886f9a2cac8..a6ff4d308f99 100644 --- a/python/testSrc/com/jetbrains/python/parsing/PythonParsingTest.java +++ b/python/testSrc/com/jetbrains/python/parsing/PythonParsingTest.java @@ -1139,6 +1139,10 @@ public class PythonParsingTest extends ParsingTestCase { doTest(LanguageLevel.PYTHON310); } + public void testPatternMatchingLeadingAndTrailingComments() { + doTest(LanguageLevel.PYTHON310); + } + public void doTest() { doTest(LanguageLevel.PYTHON26); }