Repository: psf/black

Issue:
Line too long: Many cases in case block
**Describe the bug**

Match patterns with lots of `|`-delimited alternatives are not split into multiple lines.

**To Reproduce**

```python
% cat longmatch.py 
match x:
    case "abcd" | "abcd" | "abcd" | "abcd" | "abcd" | "abcd" | "abcd" | "abcd" | "abcd" | "abcd" | "abcd" | "abcd" | "abcd" | "abcd" | "abcd":
        pass
% black -t py311 longmatch.py --diff
All done! ✨ 🍰 ✨
1 file would be left unchanged.
```

(Note the line is 142 characters long)

**Expected behavior**

I want it to reformat to:

```python
match x:
    case (
        "abcd"
        | "abcd"
        | "abcd"
        | "abcd"
        | "abcd"
        | "abcd"
        | "abcd"
        | "abcd"
        | "abcd"
        | "abcd"
        | "abcd"
        | "abcd"
        | "abcd"
        | "abcd"
        | "abcd"
    ):
        pass
```

Black does this already if you manually add parentheses around the whole sequence of alternatives.

**Environment**

- Black's version: 23.7.0
- OS and Python version: macOS



Discussion:
While I was trying to fix this issue, I noticed that the rules for `case` is a bit different than the others: the [trailing comma rules ](https://black.readthedocs.io/en/stable/the_black_code_style/current_style.html#trailing-commas) will not be activated on `case`

E.g. Black will currently leave this piece of code unchanged despite it contains trailing comma and each of its item is not in seperate lines (see [here](https://black.vercel.app/?version=stable&state=_Td6WFoAAATm1rRGAgAhARYAAAB0L-Wj4ACPAGBdAD2IimZxl1N_WmBasjhgIpJRm9D2G66SDLhhnFrSNeMykF1T7_XvdV1ILVbFVrh2v9AAXw2gdeUx-8Jo-bDKXZ1ooMdzNtm7kIEA1AsAChaiMeoJ3hg3MQVQGY_oHvffAABtJu4QBJOyRAABfJABAAAAElkP1rHEZ_sCAAAAAARZWg==))
```
match x:
    case 1, 2,:
        pass
```

Yet, an unintentional side effect of my current attempt would enable the trailing comma on `case` as well, hence the above code will be converted into:
```
match x:
    case (
        1, 
        2,
    ):
        pass
```
I think it is beneficial to have our trailing comma rules consistent across different syntax in general, but I do want to double check if this is indeed the preferable result here and there is no particular reason to do trailing comma for `case` differently.
I agree we should reformat this case as you suggest.

Resolving patch:
diff --git a/src/black/linegen.py b/src/black/linegen.py
--- a/src/black/linegen.py
+++ b/src/black/linegen.py
@@ -1229,7 +1229,7 @@ def normalize_prefix(leaf: Leaf, *, inside_brackets: bool) -> None:
     leaf.prefix = ""
 
 
-def normalize_invisible_parens(
+def normalize_invisible_parens(  # noqa: C901
     node: Node, parens_after: Set[str], *, mode: Mode, features: Collection[Feature]
 ) -> None:
     """Make existing optional parentheses invisible or create new ones.
@@ -1260,6 +1260,17 @@ def normalize_invisible_parens(
                 child, parens_after=parens_after, mode=mode, features=features
             )
 
+        # Fixes a bug where invisible parens are not properly wrapped around
+        # case blocks.
+        if (
+            isinstance(child, Node)
+            and child.type == syms.case_block
+            and Preview.long_case_block_line_splitting in mode
+        ):
+            normalize_invisible_parens(
+                child, parens_after={"case"}, mode=mode, features=features
+            )
+
         # Add parentheses around long tuple unpacking in assignments.
         if (
             index == 0
@@ -1305,6 +1316,17 @@ def normalize_invisible_parens(
                 # invisible parentheses to work more precisely.
                 continue
 
+            elif (
+                isinstance(child, Leaf)
+                and child.next_sibling is not None
+                and child.next_sibling.type == token.COLON
+                and child.value == "case"
+                and Preview.long_case_block_line_splitting in mode
+            ):
+                # A special patch for "case case:" scenario, the second occurrence
+                # of case will be not parsed as a Python keyword.
+                break
+
             elif not (isinstance(child, Leaf) and is_multiline_string(child)):
                 wrap_in_parentheses(node, child, visible=False)
 
diff --git a/src/black/mode.py b/src/black/mode.py
--- a/src/black/mode.py
+++ b/src/black/mode.py
@@ -193,6 +193,7 @@ class Preview(Enum):
     hug_parens_with_braces_and_square_brackets = auto()
     allow_empty_first_line_before_new_block_or_comment = auto()
     single_line_format_skip_with_multiple_comments = auto()
+    long_case_block_line_splitting = auto()
 
 
 class Deprecated(UserWarning):


Test patch:
diff --git a/tests/data/cases/pattern_matching_extras.py b/tests/data/cases/pattern_matching_extras.py
--- a/tests/data/cases/pattern_matching_extras.py
+++ b/tests/data/cases/pattern_matching_extras.py
@@ -30,22 +30,6 @@ def func(match: case, case: match) -> case:
             ...
 
 
-match maybe, multiple:
-    case perhaps, 5:
-        pass
-    case perhaps, 6,:
-        pass
-
-
-match more := (than, one), indeed,:
-    case _, (5, 6):
-        pass
-    case [[5], (6)], [7],:
-        pass
-    case _:
-        pass
-
-
 match a, *b, c:
     case [*_]:
         assert "seq" == _
@@ -67,12 +51,12 @@ def func(match: case, case: match) -> case:
         ),
     ):
         pass
-
     case [a as match]:
         pass
-
     case case:
         pass
+    case something:
+        pass
 
 
 match match:
@@ -98,10 +82,8 @@ def func(match: case, case: match) -> case:
 match something:
     case 1 as a:
         pass
-
     case 2 as b, 3 as c:
         pass
-
     case 4 as d, (5 as e), (6 | 7 as g), *h:
         pass
 
diff --git a/tests/data/cases/preview_pattern_matching_long.py b/tests/data/cases/preview_pattern_matching_long.py
new file mode 100644
--- /dev/null
+++ b/tests/data/cases/preview_pattern_matching_long.py
@@ -0,0 +1,34 @@
+# flags: --preview --minimum-version=3.10
+match x:
+    case "abcd" | "abcd" | "abcd" :
+        pass
+    case "abcd" | "abcd" | "abcd" | "abcd" | "abcd" | "abcd" | "abcd" | "abcd" | "abcd" | "abcd" | "abcd" | "abcd" | "abcd" | "abcd" | "abcd":
+        pass
+    case xxxxxxxxxxxxxxxxxxxxxxx:
+        pass
+
+# output
+
+match x:
+    case "abcd" | "abcd" | "abcd":
+        pass
+    case (
+        "abcd"
+        | "abcd"
+        | "abcd"
+        | "abcd"
+        | "abcd"
+        | "abcd"
+        | "abcd"
+        | "abcd"
+        | "abcd"
+        | "abcd"
+        | "abcd"
+        | "abcd"
+        | "abcd"
+        | "abcd"
+        | "abcd"
+    ):
+        pass
+    case xxxxxxxxxxxxxxxxxxxxxxx:
+        pass
diff --git a/tests/data/cases/preview_pattern_matching_trailing_comma.py b/tests/data/cases/preview_pattern_matching_trailing_comma.py
new file mode 100644
--- /dev/null
+++ b/tests/data/cases/preview_pattern_matching_trailing_comma.py
@@ -0,0 +1,39 @@
+# flags: --preview --minimum-version=3.10
+match maybe, multiple:
+    case perhaps, 5:
+        pass
+    case perhaps, 6,:
+        pass
+
+
+match more := (than, one), indeed,:
+    case _, (5, 6):
+        pass
+    case [[5], (6)], [7],:
+        pass
+    case _:
+        pass
+
+
+# output
+
+match maybe, multiple:
+    case perhaps, 5:
+        pass
+    case (
+        perhaps,
+        6,
+    ):
+        pass
+
+
+match more := (than, one), indeed,:
+    case _, (5, 6):
+        pass
+    case (
+        [[5], (6)],
+        [7],
+    ):
+        pass
+    case _:
+        pass
\ No newline at end of file