Skip to content

Commit 9c70117

Browse files
revarbatclaude
andcommitted
Coalesce bare ) / [ onto one line in pretty-printer output
When a closing ) sits alone on a line and the next line starts with [, they are joined as ) [ — most visible when block-form let() is followed by a vector or list comprehension body. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 40aeae0 commit 9c70117

3 files changed

Lines changed: 97 additions & 3 deletions

File tree

docs/PRETTY_PRINTING.md

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,7 @@ function f(x) =
163163
y + 1;
164164
```
165165

166-
**Block** — two or more assignments, or any single assignment whose value is multiline (e.g. a ternary): `let(` opens, each assignment on its own line indented one level, `)` closes at the original indent:
166+
**Block** — two or more assignments, or any single assignment whose value is multiline (e.g. a ternary): `let(` opens, each assignment on its own line indented one level, `)` closes at the original indent. When the body is a plain expression, it follows on the next line:
167167

168168
```
169169
function f(a, b) =
@@ -182,6 +182,15 @@ function f(x) =
182182
y + 1;
183183
```
184184

185+
When the body is a vector or list comprehension, the closing `)` and opening `[` are coalesced onto one line (see [§5 `) / [` Coalescing](#--coalescing)):
186+
187+
```
188+
x = let(
189+
a = 1,
190+
b = 2
191+
) [a, b, a + b];
192+
```
193+
185194
This same inline/block rule applies to `let()` inside `let()` module calls and inside list comprehensions.
186195

187196
### Long Function Call Expressions
@@ -341,6 +350,29 @@ The precedence table (low → high):
341350
| 80 | unary `-`, `!`, `~` |
342351
| 90 | `^` (right-associative) |
343352

353+
### `)` / `[` Coalescing
354+
355+
When a closing `)` lands on a line by itself and the very next line starts with `[`, the two are joined onto one line as `) [`. This most commonly occurs when a block-form `let(` is followed by a vector or list comprehension body:
356+
357+
```
358+
// two or more assignments → block let, body is a vector
359+
x = let(
360+
a = 1,
361+
b = 2
362+
) [a, b, a + b];
363+
364+
// block let followed by an expanding list comprehension
365+
x = let(
366+
a = 1,
367+
b = 2
368+
) [
369+
for (i = [0:5])
370+
i + a
371+
];
372+
```
373+
374+
The indentation of the resulting `) [` line is taken from the `)` line. A `)` with any trailing content (e.g. `);` or `) {`) is not coalesced.
375+
344376
### Other Expressions
345377

346378
All other expressions (ranges, identifiers, etc.) are rendered on a single line.

src/openscad_parser/ast/pretty_print.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,27 @@ def to_openscad(nodes: list[ASTNode], indent_width: int = 4) -> str:
4747
parts.append("")
4848
parts.append(_fmt_node(node, 0, indent_width))
4949
prev_complex = is_complex
50-
return "\n".join(parts)
50+
return _coalesce_paren_bracket("\n".join(parts))
51+
52+
53+
def _coalesce_paren_bracket(text: str) -> str:
54+
"""Join consecutive lines where one is a bare ')' and the next starts with '['."""
55+
lines = text.split("\n")
56+
result = []
57+
i = 0
58+
while i < len(lines):
59+
if (
60+
i + 1 < len(lines)
61+
and lines[i].strip() == ")"
62+
and lines[i + 1].lstrip().startswith("[")
63+
):
64+
indent = len(lines[i]) - len(lines[i].lstrip())
65+
result.append(" " * indent + ") " + lines[i + 1].lstrip())
66+
i += 2
67+
else:
68+
result.append(lines[i])
69+
i += 1
70+
return "\n".join(result)
5171

5272

5373
# Line length beyond which call arguments are formatted one-per-line.

tests/test_pretty_print.py

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
Assignment, FunctionDeclaration, ModuleDeclaration,
77
ModularCall, ModularFor, ModularIf, ModularIfElse,
88
)
9-
from openscad_parser.ast.pretty_print import _as_list
9+
from openscad_parser.ast.pretty_print import _as_list, _coalesce_paren_bracket
1010

1111

1212
def _roundtrip(code: str) -> list:
@@ -644,3 +644,45 @@ def test_none_returns_empty(self):
644644
def test_single_value_wraps(self):
645645
assert _as_list("x") == ["x"]
646646
assert _as_list(42) == [42]
647+
648+
649+
class TestCoalesceParenBracket:
650+
"""Unit tests for the ) / [ line-coalescing post-processor."""
651+
652+
def test_bare_paren_bracket_joined(self):
653+
inp = " )\n ["
654+
assert _coalesce_paren_bracket(inp) == " ) ["
655+
656+
def test_bracket_content_preserved(self):
657+
inp = " )\n [1, 2, 3]"
658+
assert _coalesce_paren_bracket(inp) == " ) [1, 2, 3]"
659+
660+
def test_indentation_from_paren_line(self):
661+
# The coalesced line uses the indent of the ')' line, not the '[' line.
662+
inp = " )\n [x, y]"
663+
assert _coalesce_paren_bracket(inp) == " ) [x, y]"
664+
665+
def test_no_coalesce_when_paren_has_trailing_content(self):
666+
inp = " ) {\n ["
667+
assert _coalesce_paren_bracket(inp) == " ) {\n ["
668+
669+
def test_no_coalesce_when_bracket_preceded_by_non_paren(self):
670+
inp = " x = 1\n [1, 2]"
671+
assert _coalesce_paren_bracket(inp) == " x = 1\n [1, 2]"
672+
673+
def test_multiple_occurrences(self):
674+
inp = ")\n[\n)\n["
675+
assert _coalesce_paren_bracket(inp) == ") [\n) ["
676+
677+
def test_let_block_with_vector_body(self):
678+
out = _fmt("x = let(a = 1, b = 2) [a, b];")
679+
assert ") [a, b];" in out
680+
assert "\n)" not in out.split(") [")[0].split("\n")[-1] # no bare ) before the join
681+
682+
def test_let_block_with_list_comp_body(self):
683+
out = _fmt("x = let(a = 1, b = 2) [for (i = [0:5]) i + a];")
684+
assert ") [\n" in out
685+
# no consecutive lines of bare ) then [
686+
lines = out.split("\n")
687+
for i in range(len(lines) - 1):
688+
assert not (lines[i].strip() == ")" and lines[i + 1].lstrip().startswith("["))

0 commit comments

Comments
 (0)