Repository: tox-dev/pyproject-fmt-rust (language: python)

Issue:
column_width is not respected everywhere
I see this:

```toml
[tool.ruff]
...
lint.allowed-confusables = [
  "×",
  "’",
]
lint.flake8-type-checking.exempt-modules = [
]
lint.per-file-ignores."*/__init__.py" = [
  "F401",
]
lint.per-file-ignores."docs/*" = [
  "I",
]
lint.per-file-ignores."tests/*" = [
  "D",
]
```

and would rather see

```toml
[tool.ruff]
...
lint.allowed-confusables = [ "×", "’" ]
lint.flake8-type-checking.exempt-modules = []
lint.per-file-ignores."*/__init__.py" = [ "F401" ]
lint.per-file-ignores."docs/*" = [ "I" ]
lint.per-file-ignores."tests/*" = [ "D" ]
```

- `allowed-confusables` is like `tool.hatch.envs.*.features` in that it always contains a short list of strings 
- `lint.per-file-ignores.*` too, those lists are always very short
- I think empty lists should just be on one line always, it looks super weird to expand them.


Discussion / hints:
Set https://pyproject-fmt.readthedocs.io/en/latest/#pyproject-fmt---indent accordingly.
That option doesn’t seem in any way related to what I’m talking about, are you sure?
The reason those go over multiple lines is that the default indent is 2 (my preferred way, makes the code diff friendly). If you increase it to like 80 will not split it across lines.
I’m so confused, what does the indent have to do with putting things on multiple lines or not?

And why would `indent = 80` ever make sense?

And why do I see this in the same file?

```toml
[tool.hatch.envs.hatch-test]
features = [ "test" ]
```

----

The way every formatter I’ve ever used works is (simplified)

1. check if current line is longer than `max_columns`
2. if yes, and last item on line is a list, split it up by only leaving `[` on the current line and indenting each item by `indent` spaces

your docs say

```toml
# how many spaces use for indentation
indent = 2
```

and

```restructuredtext
--indent COUNT - number of spaces to use for indentation (default: 2)
```

so that seems to agree with my intuition?

where is what you say described? and why is it necessary for `lint.allowed-confusables` but not `tool.hatch.envs.hatch-test.features`?
Sorry I meant https://pyproject-fmt.readthedocs.io/en/latest/#pyproject-fmt---column-width, also we do not roll our own formatter but defer to https://taplo.tamasfe.dev/configuration/formatter-options.html; so any bugs you might see will need to be reported there I am afraid.
> Sorry I meant [pyproject-fmt.readthedocs.io/en/latest#pyproject-fmt---column-width](https://pyproject-fmt.readthedocs.io/en/latest/#pyproject-fmt---column-width)

haha OK! now that makes more sense.

> we do not roll our own formatter but defer to [taplo.tamasfe.dev/configuration/formatter-options.html](https://taplo.tamasfe.dev/configuration/formatter-options.html); so any bugs you might see will need to be reported there I am afraid.

Nope, taplo formats the example above as I want it when configured with your defaults

Can you reopen this?
Likely some bug, PR welcome.
The issue is (maybe among other things) `helpers::array::sort` which expands arrays indiscriminately.

Fix patch:
diff --git a/Cargo.lock b/Cargo.lock
index 13c18a9..63ca0bb 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -520,7 +520,7 @@ dependencies = [
 
 [[package]]
 name = "pyproject-fmt-rust"
-version = "1.1.4"
+version = "1.1.5"
 dependencies = [
  "indoc",
  "lexical-sort",
diff --git a/rust/src/helpers/array.rs b/rust/src/helpers/array.rs
index 3cf4814..bc96672 100644
--- a/rust/src/helpers/array.rs
+++ b/rust/src/helpers/array.rs
@@ -37,6 +37,10 @@ where
                 .filter(|x| *x == COMMA || *x == VALUE)
                 .last()
                 == Some(COMMA);
+            let multiline = array_node
+                .children_with_tokens()
+                .find(|e| e.kind() == NEWLINE)
+                .is_some();
             let mut value_set = Vec::<Vec<SyntaxElement>>::new();
             let entry_set = RefCell::new(Vec::<SyntaxElement>::new());
             let mut key_to_pos = HashMap::<String, usize>::new();
@@ -67,7 +71,9 @@ where
                 match &entry.kind() {
                     SyntaxKind::BRACKET_START => {
                         entries.push(entry);
-                        entries.push(make_newline());
+                        if multiline {
+                            entries.push(make_newline());
+                        }
                         previous_is_bracket_open = true;
                     }
                     SyntaxKind::BRACKET_END => {
@@ -80,7 +86,9 @@ where
                     }
                     VALUE => {
                         if has_value {
-                            entry_set.borrow_mut().push(make_newline());
+                            if multiline {
+                                entry_set.borrow_mut().push(make_newline());
+                            }
                             add_to_value_set(entry_value.clone());
                         }
                         has_value = true;
@@ -117,7 +125,7 @@ where
 
             let mut order: Vec<String> = key_to_pos.clone().into_keys().collect();
             order.string_sort_unstable(natural_lexical_cmp);
-            let end = entries.split_off(2);
+            let end = entries.split_off(if multiline { 2 } else { 1 });
             for key in order {
                 entries.extend(value_set[key_to_pos[&key]].clone());
             }
diff --git a/rust/src/main.rs b/rust/src/main.rs
index 1920e95..93d7e3a 100644
--- a/rust/src/main.rs
+++ b/rust/src/main.rs
@@ -63,16 +63,16 @@ pub fn format_toml(content: &str, opt: &Settings) -> String {
     reorder_tables(&root_ast, &mut tables);
 
     let options = Options {
-        align_entries: false,           // do not align by =
-        align_comments: true,           // align inline comments
-        align_single_comments: true,    // align comments after entries
-        array_trailing_comma: true,     // ensure arrays finish with trailing comma
-        array_auto_expand: true,        // arrays go to multi line for easier diffs
-        array_auto_collapse: false,     // do not collapse for easier diffs
-        compact_arrays: false,          // do not compact for easier diffs
-        compact_inline_tables: false,   // do not compact for easier diffs
-        compact_entries: false,         // do not compact for easier diffs
-        column_width: opt.column_width, // always expand arrays per https://github.com/tamasfe/taplo/issues/390
+        align_entries: false,         // do not align by =
+        align_comments: true,         // align inline comments
+        align_single_comments: true,  // align comments after entries
+        array_trailing_comma: true,   // ensure arrays finish with trailing comma
+        array_auto_expand: true,      // arrays go to multi line when too long
+        array_auto_collapse: false,   // do not collapse for easier diffs
+        compact_arrays: false,        // leave whitespace
+        compact_inline_tables: false, // leave whitespace
+        compact_entries: false,       // leave whitespace
+        column_width: opt.column_width,
         indent_tables: false,
         indent_entries: false,
         inline_table_expand: true,
@@ -316,4 +316,47 @@ mod tests {
         let second = format_toml(got.as_str(), &settings);
         assert_eq!(second, got);
     }
+
+    /// Test that the column width is respected,
+    /// and that arrays are neither exploded nor collapsed without reason
+    #[rstest]
+    fn test_column_width() {
+        let start = indoc! {r#"
+        [build-system]
+        build-backend = "backend"
+        requires = ["c>=1.5", "d == 2" ]
+
+        [project]
+        name = "beta"
+        dependencies = [
+        "e>=1.5",
+        ]
+        "#};
+        let settings = Settings {
+            column_width: 80,
+            indent: 4,
+            keep_full_version: false,
+            max_supported_python: (3, 12),
+            min_supported_python: (3, 12),
+        };
+        let got = format_toml(start, &settings);
+        let expected = indoc! {r#"
+        [build-system]
+        build-backend = "backend"
+        requires = [ "c>=1.5", "d==2" ]
+
+        [project]
+        name = "beta"
+        classifiers = [
+            "Programming Language :: Python :: 3 :: Only",
+            "Programming Language :: Python :: 3.12",
+        ]
+        dependencies = [
+            "e>=1.5",
+        ]
+        "#};
+        assert_eq!(got, expected);
+        let second = format_toml(got.as_str(), &settings);
+        assert_eq!(second, got);
+    }
 }


Test patch:
diff --git a/tests/test_main.py b/tests/test_main.py
index 468bdee..8216701 100644
--- a/tests/test_main.py
+++ b/tests/test_main.py
@@ -2,26 +2,73 @@
 
 from textwrap import dedent
 
-from pyproject_fmt_rust import Settings, format_toml
+import pytest
 
+from pyproject_fmt_rust import Settings, format_toml
 
-def test_format_toml() -> None:
-    txt = """
-    [project]
-    keywords = [
-      "A",
-    ]
-    classifiers = [
-      "Programming Language :: Python :: 3 :: Only",
-    ]
-    dynamic = [
-      "B",
-    ]
-    dependencies = [
-      "requests>=2.0",
-    ]
-    """
 
+@pytest.mark.parametrize(
+    ("start", "expected"),
+    [
+        pytest.param(
+            """
+            [project]
+            keywords = [
+              "A",
+            ]
+            classifiers = [
+              "Programming Language :: Python :: 3 :: Only",
+            ]
+            dynamic = [
+              "B",
+            ]
+            dependencies = [
+              "requests>=2.0",
+            ]
+            """,
+            """\
+            [project]
+            keywords = [
+                "A",
+            ]
+            classifiers = [
+                "Programming Language :: Python :: 3 :: Only",
+                "Programming Language :: Python :: 3.7",
+                "Programming Language :: Python :: 3.8",
+            ]
+            dynamic = [
+                "B",
+            ]
+            dependencies = [
+                "requests>=2.0",
+            ]
+            """,
+            id="expanded",
+        ),
+        pytest.param(
+            """
+            [project]
+            keywords = ["A"]
+            classifiers = ["Programming Language :: Python :: 3 :: Only"]
+            dynamic = ["B"]
+            dependencies = ["requests>=2.0"]
+            """,
+            """\
+            [project]
+            keywords = [ "A" ]
+            classifiers = [
+                "Programming Language :: Python :: 3 :: Only",
+                "Programming Language :: Python :: 3.7",
+                "Programming Language :: Python :: 3.8",
+            ]
+            dynamic = [ "B" ]
+            dependencies = [ "requests>=2.0" ]
+            """,
+            id="collapsed",
+        ),
+    ],
+)
+def test_format_toml(start: str, expected: str) -> None:
     settings = Settings(
         column_width=120,
         indent=4,
@@ -29,23 +76,5 @@ def test_format_toml() -> None:
         min_supported_python=(3, 7),
         max_supported_python=(3, 8),
     )
-    res = format_toml(dedent(txt), settings)
-
-    expected = """\
-    [project]
-    keywords = [
-        "A",
-    ]
-    classifiers = [
-        "Programming Language :: Python :: 3 :: Only",
-        "Programming Language :: Python :: 3.7",
-        "Programming Language :: Python :: 3.8",
-    ]
-    dynamic = [
-        "B",
-    ]
-    dependencies = [
-        "requests>=2.0",
-    ]
-    """
+    res = format_toml(dedent(start), settings)
     assert res == dedent(expected)