gdscript-fmt 0.5.2

GDScript source formatter (whitespace + indentation normalization, safe-by-construction) — gdscript-analyzer.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588
1589
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
1610
1611
1612
1613
1614
1615
1616
1617
1618
1619
1620
1621
1622
1623
1624
1625
1626
1627
1628
1629
1630
1631
1632
1633
1634
1635
1636
1637
1638
1639
1640
1641
1642
1643
1644
1645
1646
1647
1648
1649
1650
1651
1652
1653
1654
1655
1656
1657
1658
1659
1660
1661
1662
1663
1664
1665
1666
1667
1668
1669
1670
1671
1672
1673
1674
1675
1676
1677
1678
1679
1680
1681
1682
1683
1684
1685
1686
1687
1688
1689
1690
1691
1692
1693
1694
1695
1696
1697
1698
1699
1700
1701
1702
1703
1704
1705
1706
1707
1708
1709
1710
1711
1712
1713
1714
1715
1716
1717
1718
1719
1720
1721
1722
1723
1724
1725
1726
1727
1728
1729
1730
1731
1732
1733
1734
1735
1736
1737
1738
1739
1740
1741
1742
1743
1744
1745
1746
1747
1748
1749
1750
1751
1752
1753
1754
1755
1756
1757
1758
1759
1760
1761
1762
1763
1764
1765
1766
1767
1768
1769
1770
1771
1772
1773
1774
1775
1776
1777
1778
1779
1780
1781
1782
1783
1784
1785
1786
1787
1788
1789
1790
1791
1792
1793
1794
1795
1796
1797
1798
1799
1800
1801
1802
1803
1804
1805
1806
1807
1808
1809
1810
1811
1812
1813
1814
1815
1816
1817
1818
1819
1820
1821
1822
1823
1824
1825
1826
1827
1828
1829
1830
1831
1832
1833
1834
1835
1836
1837
1838
1839
1840
1841
1842
1843
1844
1845
1846
1847
1848
1849
1850
1851
1852
1853
1854
1855
1856
1857
1858
1859
1860
1861
1862
1863
1864
1865
1866
1867
1868
1869
1870
1871
1872
1873
1874
1875
1876
1877
1878
1879
1880
1881
1882
1883
1884
1885
1886
1887
1888
1889
1890
1891
1892
1893
1894
1895
1896
1897
1898
1899
1900
1901
1902
1903
1904
1905
1906
1907
1908
1909
1910
1911
1912
1913
1914
1915
1916
1917
1918
1919
1920
1921
1922
1923
1924
1925
1926
1927
1928
1929
1930
1931
1932
1933
1934
1935
1936
1937
1938
1939
1940
1941
1942
1943
1944
1945
1946
1947
1948
1949
1950
1951
1952
1953
1954
1955
1956
1957
1958
1959
1960
1961
1962
1963
1964
1965
1966
1967
1968
1969
1970
1971
1972
1973
1974
1975
1976
1977
1978
1979
1980
1981
1982
1983
1984
1985
1986
1987
1988
1989
1990
1991
1992
1993
1994
1995
1996
1997
1998
1999
2000
2001
2002
2003
2004
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
2027
2028
2029
2030
2031
2032
2033
2034
2035
2036
2037
2038
2039
2040
2041
2042
2043
2044
2045
2046
2047
2048
2049
2050
2051
2052
2053
2054
2055
2056
2057
2058
2059
2060
2061
2062
2063
2064
2065
2066
2067
2068
2069
2070
2071
2072
2073
2074
2075
2076
2077
2078
2079
2080
2081
2082
2083
2084
2085
2086
2087
2088
2089
2090
2091
2092
2093
2094
2095
2096
2097
2098
2099
2100
2101
2102
2103
2104
2105
2106
2107
2108
2109
2110
2111
2112
2113
2114
2115
2116
2117
2118
2119
2120
2121
2122
2123
2124
2125
2126
2127
2128
2129
2130
2131
2132
2133
2134
2135
2136
2137
2138
2139
2140
2141
2142
2143
2144
2145
2146
2147
2148
2149
2150
2151
2152
2153
2154
2155
2156
2157
2158
2159
2160
2161
2162
2163
2164
2165
2166
2167
2168
2169
2170
2171
2172
2173
2174
2175
2176
2177
2178
2179
2180
2181
2182
2183
2184
2185
2186
2187
2188
2189
2190
2191
2192
2193
2194
2195
2196
2197
2198
2199
2200
2201
2202
2203
2204
2205
2206
2207
2208
2209
2210
2211
2212
2213
2214
2215
2216
2217
2218
2219
2220
2221
2222
2223
2224
2225
2226
2227
2228
2229
2230
2231
2232
2233
2234
2235
2236
2237
2238
2239
2240
2241
2242
2243
2244
2245
2246
2247
2248
2249
2250
2251
2252
2253
2254
2255
2256
2257
2258
2259
2260
2261
2262
2263
2264
2265
2266
2267
2268
2269
2270
2271
2272
2273
2274
2275
2276
2277
2278
2279
2280
2281
2282
2283
2284
2285
2286
2287
2288
2289
2290
2291
2292
2293
2294
2295
2296
2297
2298
2299
2300
2301
2302
2303
2304
2305
2306
2307
2308
2309
2310
2311
2312
2313
2314
2315
2316
2317
2318
2319
2320
2321
2322
2323
2324
2325
2326
2327
2328
2329
2330
2331
2332
2333
2334
2335
2336
2337
2338
2339
2340
2341
2342
2343
2344
2345
2346
2347
2348
2349
2350
2351
2352
2353
2354
2355
2356
2357
2358
2359
2360
2361
2362
2363
2364
2365
2366
2367
2368
2369
2370
2371
2372
2373
2374
2375
2376
2377
2378
2379
2380
2381
2382
2383
2384
2385
2386
2387
2388
2389
2390
2391
2392
2393
2394
2395
2396
2397
2398
2399
2400
2401
2402
2403
2404
2405
2406
2407
2408
2409
2410
2411
2412
2413
2414
2415
2416
2417
2418
2419
2420
2421
2422
2423
2424
2425
2426
2427
2428
2429
2430
2431
2432
2433
2434
2435
2436
2437
2438
2439
2440
2441
2442
2443
2444
2445
2446
2447
2448
2449
2450
2451
2452
2453
2454
2455
2456
2457
2458
2459
2460
2461
2462
2463
2464
2465
2466
2467
2468
2469
2470
2471
2472
2473
2474
2475
2476
2477
2478
2479
2480
2481
2482
2483
2484
2485
2486
2487
2488
2489
2490
2491
2492
2493
2494
2495
2496
2497
2498
2499
2500
2501
2502
2503
2504
2505
2506
2507
2508
2509
2510
2511
2512
2513
2514
2515
2516
2517
2518
2519
2520
2521
2522
2523
2524
2525
2526
2527
2528
2529
2530
2531
2532
2533
2534
2535
2536
2537
2538
2539
2540
2541
2542
2543
2544
2545
2546
2547
2548
2549
2550
2551
2552
2553
2554
2555
2556
2557
2558
2559
2560
2561
2562
2563
2564
2565
2566
2567
2568
2569
2570
2571
2572
2573
2574
2575
2576
2577
2578
2579
2580
2581
2582
2583
2584
2585
2586
2587
2588
2589
2590
2591
2592
2593
2594
2595
2596
2597
2598
2599
2600
2601
2602
2603
2604
2605
2606
2607
2608
2609
2610
2611
2612
2613
2614
2615
2616
2617
2618
2619
2620
2621
2622
2623
2624
2625
2626
2627
2628
2629
2630
2631
2632
2633
2634
2635
2636
2637
2638
2639
2640
2641
2642
2643
2644
2645
2646
2647
2648
2649
2650
2651
2652
2653
2654
2655
2656
2657
2658
2659
2660
2661
2662
2663
2664
2665
2666
2667
2668
2669
2670
2671
2672
2673
2674
2675
2676
2677
2678
2679
2680
2681
2682
2683
2684
2685
2686
2687
2688
2689
2690
2691
2692
2693
2694
2695
2696
2697
2698
2699
2700
2701
2702
2703
2704
2705
2706
2707
2708
2709
2710
2711
2712
2713
2714
2715
2716
2717
2718
2719
2720
2721
2722
2723
2724
2725
2726
2727
2728
2729
2730
2731
2732
2733
2734
2735
2736
2737
2738
2739
2740
2741
2742
2743
2744
2745
2746
2747
2748
2749
2750
2751
2752
2753
2754
2755
2756
2757
2758
2759
2760
2761
2762
2763
2764
2765
2766
2767
2768
2769
2770
2771
2772
2773
2774
2775
2776
2777
2778
2779
2780
2781
2782
2783
2784
2785
2786
2787
2788
2789
2790
2791
2792
2793
2794
2795
2796
2797
2798
2799
2800
2801
2802
2803
2804
2805
2806
2807
2808
2809
2810
2811
2812
2813
2814
2815
2816
2817
2818
2819
2820
2821
2822
2823
2824
2825
2826
2827
2828
2829
2830
2831
2832
2833
2834
2835
2836
2837
2838
2839
2840
2841
2842
2843
2844
2845
2846
2847
2848
2849
2850
2851
2852
2853
2854
2855
2856
2857
2858
2859
2860
2861
2862
2863
2864
2865
2866
2867
2868
2869
2870
2871
2872
2873
2874
2875
2876
2877
2878
2879
2880
2881
2882
2883
2884
2885
2886
2887
2888
2889
2890
2891
2892
2893
2894
2895
2896
2897
2898
2899
2900
2901
2902
2903
2904
2905
2906
2907
2908
2909
2910
2911
2912
2913
2914
2915
2916
2917
2918
2919
2920
2921
2922
2923
2924
2925
2926
2927
2928
2929
2930
2931
2932
2933
2934
2935
2936
2937
2938
2939
2940
2941
2942
2943
2944
2945
2946
2947
2948
2949
2950
2951
2952
2953
2954
2955
2956
2957
2958
2959
2960
2961
2962
2963
2964
2965
2966
2967
2968
2969
2970
2971
2972
2973
2974
2975
2976
2977
2978
2979
2980
2981
2982
2983
2984
2985
2986
2987
2988
2989
2990
2991
2992
2993
2994
2995
2996
2997
2998
2999
3000
3001
3002
3003
3004
3005
3006
3007
3008
3009
3010
3011
3012
3013
3014
3015
3016
3017
3018
3019
3020
3021
3022
3023
3024
3025
3026
3027
3028
3029
3030
3031
3032
3033
3034
3035
3036
3037
3038
3039
3040
3041
3042
3043
3044
3045
3046
3047
3048
3049
3050
3051
3052
3053
3054
3055
3056
3057
3058
3059
3060
3061
3062
3063
3064
3065
3066
3067
3068
3069
3070
3071
3072
3073
3074
3075
3076
3077
3078
3079
3080
3081
3082
3083
3084
3085
3086
3087
3088
3089
3090
3091
3092
3093
3094
3095
3096
3097
3098
3099
3100
3101
3102
3103
3104
3105
3106
3107
3108
3109
3110
3111
3112
3113
3114
3115
3116
3117
3118
3119
3120
3121
3122
3123
3124
3125
3126
3127
3128
3129
3130
3131
3132
3133
3134
3135
3136
3137
3138
3139
3140
3141
3142
3143
3144
3145
3146
3147
3148
3149
3150
3151
3152
3153
3154
3155
3156
3157
3158
3159
3160
3161
3162
3163
3164
3165
3166
3167
3168
3169
3170
3171
3172
3173
3174
3175
3176
3177
3178
3179
3180
3181
3182
3183
3184
3185
3186
3187
3188
3189
3190
3191
3192
3193
3194
3195
3196
3197
3198
3199
3200
3201
3202
3203
3204
3205
3206
3207
3208
3209
3210
3211
3212
3213
3214
3215
3216
3217
3218
3219
3220
3221
3222
3223
3224
3225
3226
3227
3228
3229
3230
3231
3232
3233
3234
3235
3236
3237
3238
3239
3240
3241
3242
3243
3244
3245
3246
3247
3248
3249
3250
3251
3252
3253
3254
3255
3256
3257
3258
3259
3260
3261
3262
3263
3264
3265
3266
3267
3268
3269
3270
3271
3272
3273
3274
3275
3276
3277
3278
3279
3280
3281
3282
3283
3284
3285
3286
3287
3288
3289
3290
3291
3292
3293
3294
3295
3296
3297
3298
3299
3300
3301
3302
3303
3304
3305
3306
3307
3308
3309
3310
3311
3312
3313
3314
3315
3316
3317
3318
3319
3320
3321
3322
3323
3324
3325
3326
3327
3328
3329
3330
3331
3332
3333
3334
3335
3336
3337
3338
3339
3340
3341
3342
3343
3344
3345
3346
3347
3348
3349
3350
3351
3352
3353
3354
3355
3356
3357
3358
3359
3360
3361
3362
3363
3364
3365
3366
3367
3368
3369
3370
3371
3372
3373
3374
3375
3376
3377
3378
3379
3380
3381
3382
3383
3384
3385
3386
3387
3388
3389
3390
3391
3392
3393
3394
3395
3396
3397
3398
3399
3400
3401
3402
3403
3404
3405
3406
3407
3408
3409
3410
3411
3412
3413
3414
3415
3416
3417
3418
3419
3420
3421
3422
3423
3424
3425
3426
3427
3428
3429
3430
3431
3432
3433
3434
3435
3436
3437
3438
3439
3440
3441
3442
3443
3444
3445
3446
3447
3448
3449
3450
3451
3452
3453
3454
3455
3456
3457
3458
3459
3460
3461
3462
3463
3464
3465
3466
3467
3468
3469
3470
3471
3472
3473
3474
3475
3476
3477
3478
3479
3480
3481
3482
3483
3484
3485
3486
3487
3488
3489
3490
3491
3492
3493
3494
3495
3496
3497
3498
3499
3500
3501
3502
3503
3504
3505
3506
3507
3508
3509
3510
3511
3512
3513
3514
3515
3516
3517
3518
3519
3520
3521
3522
3523
3524
3525
3526
3527
3528
3529
3530
3531
3532
3533
3534
3535
3536
3537
3538
3539
3540
3541
3542
3543
3544
3545
3546
3547
3548
3549
3550
3551
3552
3553
3554
3555
3556
3557
3558
3559
3560
3561
3562
3563
3564
3565
3566
3567
3568
3569
3570
3571
3572
3573
3574
3575
3576
3577
3578
3579
3580
3581
3582
3583
3584
3585
3586
3587
3588
3589
3590
3591
3592
3593
3594
3595
3596
3597
3598
3599
3600
3601
3602
3603
3604
3605
3606
3607
3608
3609
3610
3611
3612
3613
3614
3615
3616
3617
3618
3619
3620
3621
3622
3623
3624
3625
3626
3627
3628
3629
3630
3631
3632
3633
3634
3635
3636
3637
3638
3639
3640
3641
3642
3643
3644
3645
3646
3647
3648
3649
3650
3651
3652
3653
3654
3655
3656
3657
3658
3659
3660
3661
3662
3663
3664
3665
3666
3667
3668
3669
3670
3671
3672
3673
3674
3675
3676
3677
3678
3679
3680
3681
3682
3683
3684
3685
3686
3687
3688
3689
3690
3691
3692
3693
3694
3695
3696
3697
3698
3699
3700
3701
3702
3703
3704
3705
3706
3707
3708
3709
3710
3711
3712
3713
3714
3715
3716
3717
3718
3719
3720
3721
3722
3723
3724
3725
3726
3727
3728
3729
3730
3731
3732
3733
3734
3735
3736
3737
3738
3739
3740
3741
3742
3743
3744
3745
3746
3747
3748
3749
3750
3751
3752
3753
3754
3755
3756
3757
3758
3759
3760
3761
3762
3763
3764
3765
3766
3767
3768
3769
3770
3771
3772
3773
3774
3775
3776
3777
3778
3779
3780
3781
3782
3783
3784
3785
3786
3787
3788
3789
3790
3791
3792
3793
3794
3795
3796
3797
3798
3799
3800
3801
3802
3803
3804
3805
3806
3807
3808
3809
3810
3811
3812
3813
3814
3815
3816
3817
3818
3819
3820
3821
3822
3823
3824
3825
3826
3827
3828
3829
3830
3831
3832
3833
3834
3835
3836
3837
3838
3839
3840
3841
3842
3843
3844
3845
3846
3847
3848
3849
3850
3851
3852
3853
3854
3855
3856
3857
3858
3859
3860
3861
3862
3863
3864
3865
3866
3867
3868
3869
3870
3871
3872
3873
3874
3875
3876
3877
3878
3879
3880
3881
3882
3883
3884
3885
3886
3887
3888
3889
3890
3891
3892
3893
3894
3895
3896
3897
3898
3899
3900
3901
3902
3903
3904
3905
3906
3907
3908
3909
3910
3911
3912
3913
3914
3915
3916
3917
3918
3919
3920
3921
3922
3923
3924
3925
3926
3927
3928
3929
3930
3931
3932
3933
3934
3935
3936
3937
3938
3939
3940
3941
3942
3943
3944
3945
3946
3947
3948
3949
3950
3951
3952
3953
3954
3955
3956
3957
3958
3959
3960
3961
3962
3963
3964
3965
3966
3967
3968
3969
3970
3971
3972
3973
3974
3975
3976
3977
3978
3979
3980
3981
3982
3983
3984
3985
3986
3987
3988
3989
3990
3991
3992
3993
3994
3995
3996
3997
3998
3999
4000
4001
4002
4003
4004
4005
4006
4007
4008
4009
4010
4011
4012
4013
4014
4015
4016
4017
4018
4019
4020
4021
4022
4023
4024
4025
4026
4027
4028
4029
4030
4031
4032
4033
4034
4035
4036
4037
4038
4039
4040
4041
4042
4043
4044
4045
4046
4047
4048
4049
4050
4051
4052
4053
4054
4055
//! `gdscript-fmt` — the GDScript source formatter (Phase-6 Workstream 3).
//!
//! > **Internal layer (not a stable API).** Depend on [`gdscript-ide`](https://docs.rs/gdscript-ide) (the public surface); the items here
//! > may change between releases.
//!
//! A pure `fn(source, &FmtConfig) -> String`: no engine model, no filesystem, `wasm32`-safe.
//! It re-emits the lexer/pre-pass token stream, normalizing **block indentation** (to the
//! configured unit), **trailing whitespace**, and the **final newline** — every *significant*
//! token (keywords, identifiers, literals — including multi-line strings, which are single tokens)
//! is emitted **verbatim**, so meaning cannot change.
//!
//! It also normalizes **intra-line spacing** (Phase-4 increment A): one space around binary
//! operators / assignments / `->` / `:=`, after `,` and `:` (in type-annotation and dict contexts),
//! hugged brackets (`f(x, y)`, `[1, 2]`), tight member access (`a.b`), and tight unary `-x`. The
//! decision is purely local (previous significant token + innermost bracket), and the genuinely
//! ambiguous contexts — slice colons `arr[a:b]`, and node-path sigils `$Node/Path` / `%Unique`
//! (where a stray space around `/` would silently change meaning **without** changing the token
//! sequence) — are left **verbatim** / kept tight by a small node-path state machine.
//!
//! **Safe by construction.** In `safe_mode` (the default) the formatter (a) refuses to touch a
//! file with syntax errors, and (b) re-lexes its own output and **falls back to the original** if
//! the significant token sequence changed. So it never corrupts code, even input it doesn't fully
//! understand. The result is idempotent: `format(format(x)) == format(x)`.
//!
//! It also performs **length-driven line reflow** (Phase-4 increment C): a single-line statement
//! that exceeds `line_width` and contains a bracketed group is wrapped flat → compact → exploded via
//! a small `Doc`-IR, matching gdformat and preserving the token sequence. The remaining gdformat
//! behaviours (magic trailing comma, operator-chain paren injection, quote normalization) are
//! token-mutating and documented in `DEVIATIONS.md`.
#![cfg_attr(docsrs, feature(doc_cfg))]
#![deny(missing_docs)]

use gdscript_syntax::SyntaxKind;

mod wrap;

/// Formatter options. Defaults match the Godot convention (tabs) and keep the safety net on.
#[derive(Debug, Clone, PartialEq, Eq)]
#[allow(
    clippy::struct_excessive_bools,
    reason = "a plain user-facing options bag; each bool is an independent formatter toggle, not a state machine"
)]
pub struct FmtConfig {
    /// Indent with tabs (the Godot convention). `false` indents with [`indent_size`](Self::indent_size) spaces.
    pub use_tabs: bool,
    /// Spaces per indent level when `use_tabs` is `false`.
    pub indent_size: usize,
    /// The target line width for [`reflow`](Self::reflow) (default 100).
    pub line_width: usize,
    /// Normalize intra-line spacing between tokens (one space around binary operators, after
    /// `,`/`:`, hugged brackets, tight member access + unary). On by default. Turn off to format
    /// **indentation only** (the pre-increment-A behavior).
    pub normalize_spacing: bool,
    /// Collapse runs of blank lines (max 2 at top level, max 1 inside a block) and strip leading
    /// blank lines. On by default.
    pub collapse_blank_lines: bool,
    /// Insert blank lines around definitions to match gdformat (2 around top-level `func`/`class`/
    /// `static func`, 1 around nested ones; comments/annotations attached to a def move with it). On
    /// by default. Purely additive — never changes the significant token sequence.
    pub insert_blank_lines: bool,
    /// Wrap a single-line statement that exceeds [`line_width`](Self::line_width) and contains a
    /// bracketed group (call / array / dict / parameter list) — flat → compact → exploded, matching
    /// gdformat's length-driven layout. On by default. Token-preserving (no trailing comma added).
    pub reflow: bool,
    /// Normalize string-literal quotes to gdformat's style: prefer `"`, fall back to `'` only when
    /// the body has more `"` than `'`. On by default. Preserves the string's value (a token-mutating
    /// rewrite guarded by the meaning-equivalence net).
    pub normalize_strings: bool,
    /// Split an inline suite body onto its own indented line (`if c: x` → `if c:` / `x`,
    /// `func f(): return` → split, `a; b` → two lines), matching gdformat. An inline *lambda* body
    /// (`func(): x`) is preserved. On by default. Token-preserving (adds only newlines/indent).
    pub expand_inline_blocks: bool,
    /// Remove redundant grouping parens from a *standalone-expression* position — a var/const/return
    /// value, a `for` iterable, an `if`/`while` condition, a call argument, an array/dict element, a
    /// nested `(…)` — matching gdformat's `remove_outer_parentheses`. Precedence-significant parens
    /// (`(a + b) * c`) are kept. On by default. Token-mutating; guarded by the meaning-equivalence net.
    pub strip_parens: bool,
    /// Collapse a multi-line lambda whose body is a single simple statement onto one line
    /// (`func():\n\tbody` → `func(): body`), matching gdformat, so the surrounding statement can re-flow
    /// (and often fit on one line). On by default. Token-preserving (removes only a newline/indent).
    pub collapse_lambdas: bool,
    /// Re-parse + significant-token-equality fallback to verbatim. Keep on unless you have a
    /// reason not to: it is the guarantee the formatter never changes meaning.
    pub safe_mode: bool,
}

impl Default for FmtConfig {
    fn default() -> Self {
        Self {
            use_tabs: true,
            indent_size: 4,
            line_width: 100,
            normalize_spacing: true,
            collapse_blank_lines: true,
            insert_blank_lines: true,
            reflow: true,
            normalize_strings: true,
            expand_inline_blocks: true,
            strip_parens: true,
            collapse_lambdas: true,
            safe_mode: true,
        }
    }
}

impl FmtConfig {
    /// One level of indentation as a string.
    #[must_use]
    fn indent_unit(&self) -> String {
        if self.use_tabs {
            "\t".to_owned()
        } else {
            " ".repeat(self.indent_size)
        }
    }
}

/// Format `source`, returning the tidied text. In `safe_mode` (the default) this returns `source`
/// unchanged rather than risk a meaning-changing edit (a syntax error in the input, or output whose
/// significant tokens differ from the input's).
///
/// The source's line-ending style is preserved: a file using `\r\n` (a Windows checkout) is
/// formatted internally in `\n` and re-emitted with `\r\n`, so the formatter never churns every line
/// by flipping CRLF to LF (matching gdformat, which preserves line endings).
#[must_use]
pub fn format(source: &str, config: &FmtConfig) -> String {
    // A leading byte-order mark is preserved (gdformat keeps it): strip it, format the rest, re-add it.
    // Otherwise the reflow — which re-emits each statement from its *significant* tokens — would drop
    // the BOM (a trivia token) when it re-renders the first line.
    if let Some(rest) = source.strip_prefix('\u{feff}') {
        return format!("\u{feff}{}", format(rest, config));
    }
    if source.contains("\r\n") {
        let lf = source.replace("\r\n", "\n");
        return format_lf(&lf, config).replace('\n', "\r\n");
    }
    format_lf(source, config)
}

/// A single whole-line replacement edit produced by [`format_range`].
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RangeEdit {
    /// The byte range in the original `source` to replace (snapped to whole lines).
    pub range: core::ops::Range<usize>,
    /// The replacement text.
    pub new_text: String,
}

/// Format only the part of `source` overlapping the byte range `sel` (for editor "format selection"
/// / LSP `textDocument/rangeFormatting`). The whole document is formatted for correct structure and
/// indentation; the result is the **minimal changed line-hunk** that intersects `sel`, or `None` if
/// nothing in the selection's lines changes. Applying the edit yields the same bytes the whole-file
/// [`format`] would have produced for that region.
#[must_use]
pub fn format_range(
    source: &str,
    config: &FmtConfig,
    sel: core::ops::Range<usize>,
) -> Option<RangeEdit> {
    let formatted = format(source, config);
    if formatted == source {
        return None;
    }
    let src: Vec<&str> = source.split_inclusive('\n').collect();
    let out: Vec<&str> = formatted.split_inclusive('\n').collect();
    // Trim the common prefix and suffix lines: the change is the span between them.
    let mut p = 0;
    while p < src.len() && p < out.len() && src[p] == out[p] {
        p += 1;
    }
    let mut s = 0;
    while s < src.len() - p && s < out.len() - p && src[src.len() - 1 - s] == out[out.len() - 1 - s]
    {
        s += 1;
    }
    let (changed_start, changed_end) = (p, src.len() - s); // source line span [start, end)

    // Byte offset of each source line start (so `starts[i]` is line `i`'s first byte).
    let mut starts = Vec::with_capacity(src.len() + 1);
    let mut acc = 0;
    for l in &src {
        starts.push(acc);
        acc += l.len();
    }
    starts.push(acc);
    let line_of = |off: usize| starts.partition_point(|&b| b <= off).saturating_sub(1);

    let last_byte = source.len().saturating_sub(1);
    let sel_first = line_of(sel.start.min(last_byte));
    let sel_last = line_of(sel.end.saturating_sub(1).max(sel.start).min(last_byte));
    if changed_end <= sel_first || changed_start > sel_last {
        return None; // the selection's lines are unchanged
    }
    Some(RangeEdit {
        range: starts[changed_start]..starts[changed_end],
        new_text: out[p..out.len() - s].concat(),
    })
}

/// `format` working purely in LF (the caller handles CRLF round-tripping).
fn format_lf(source: &str, config: &FmtConfig) -> String {
    let input_parses = gdscript_syntax::parse(source).errors().is_empty();
    // Safe mode: never reformat around a syntax error — we'd risk mis-indenting a mis-parsed block.
    if config.safe_mode && !input_parses {
        return source.to_owned();
    }
    // Move an inner class's `extends` onto its own body line (`class C extends B:` → `class C:` /
    // `extends B`), matching gdformat. Self-validating; a no-op when there is no such class.
    let unextended = split_inner_class_extends(source);
    let source = unextended.as_deref().unwrap_or(source);
    // Collapse single-statement lambda bodies, strip redundant grouping parens, then de-inline suite
    // bodies, so the rest of the pipeline sees a paren-clean, one-statement-per-line tree with inline
    // lambdas. Each pre-pass self-validates and is a no-op otherwise.
    let collapsed = if config.collapse_lambdas {
        collapse_inline_lambdas(source, config)
    } else {
        None
    };
    let source = collapsed.as_deref().unwrap_or(source);
    let stripped = if config.strip_parens {
        strip_outer_parens(source)
    } else {
        None
    };
    let source = stripped.as_deref().unwrap_or(source);
    let expanded = if config.expand_inline_blocks {
        expand_inline_blocks(source, config)
    } else {
        None
    };
    let source = expanded.as_deref().unwrap_or(source);
    let mut out = reindent(source, config);
    if config.insert_blank_lines {
        // Purely additive (only inserts blank lines), so the significant-token net still holds.
        out = insert_def_blanks(&out, config);
    }
    if config.reflow {
        // Length-driven wrapping; token-preserving (no trailing comma added).
        out = reflow(&out, config);
    }
    if config.safe_mode {
        // The safety net is two-layered, because each catches what the other cannot:
        // (1) meaning-equivalence catches a dropped / reordered / corrupted *token* — while
        //     normalising away the rewrites the formatter is allowed to make (string-quote style,
        //     trailing commas);
        if !meaning_preserved(source, &out) {
            return source.to_owned();
        }
        // (2) a parse-validity recheck catches a meaning-changing *indentation* edit — indentation
        //     lives entirely in trivia/synthetic layout, so it is invisible to (1). If the input
        //     parsed clean, the output must too, else we fall back to the verbatim source.
        if input_parses && !gdscript_syntax::parse(&out).errors().is_empty() {
            return source.to_owned();
        }
    }
    out
}

/// gdformat splits an inner class header's `extends` clause onto the first body line
/// (`class C extends B:` → `class C:` then `\textends B`). Source pre-pass, parse-tree-driven and
/// self-validating; returns `None` (leave untouched) if it does not parse, has no such class, or the
/// rewrite changes meaning or fails to parse.
fn split_inner_class_extends(source: &str) -> Option<String> {
    let parse = gdscript_syntax::parse(source);
    if !parse.errors().is_empty() {
        return None;
    }
    // (remove_start, remove_end, insert_pos, insert_text)
    let mut edits: Vec<(usize, usize, usize, String)> = Vec::new();
    collect_inner_class_extends(&parse.syntax_node(), source, &mut edits);
    if edits.is_empty() {
        return None;
    }
    // Apply highest-offset edit first so earlier byte spans stay valid.
    edits.sort_unstable_by_key(|e| std::cmp::Reverse(e.2));
    let mut out = source.to_owned();
    for (rs, re, ins_pos, text) in &edits {
        out.insert_str(*ins_pos, text);
        out.replace_range(*rs..*re, "");
    }
    // This transform deliberately *moves* the `extends` clause (header → body), which the structural
    // meaning net would flag as a reorder. It only relocates existing tokens, so validate that the
    // significant-token multiset is unchanged and the result parses clean.
    if !same_token_multiset(source, &out) || !gdscript_syntax::parse(&out).errors().is_empty() {
        return None;
    }
    Some(out)
}

/// Whether two sources contain the same multiset of significant (non-trivia) `(kind, text)` tokens —
/// order-insensitive. Used to validate a transform that only *relocates* existing tokens.
fn same_token_multiset(a: &str, b: &str) -> bool {
    fn bag(s: &str) -> Vec<(String, String)> {
        let mut v: Vec<(String, String)> = gdscript_syntax::tokenize(s)
            .into_iter()
            .filter(|t| !t.kind.is_trivia() && t.kind != SyntaxKind::Semicolon)
            .map(|t| (format!("{:?}", t.kind), s[t.range].to_owned()))
            .collect();
        v.sort();
        v
    }
    bag(a) == bag(b)
}

/// Collect the edits that split each inner class's `extends` clause onto its first body line.
fn collect_inner_class_extends(
    node: &gdscript_syntax::GdNode,
    src: &str,
    edits: &mut Vec<(usize, usize, usize, String)>,
) {
    use SyntaxKind as S;
    for child in node.children() {
        let edit = (child.kind() == S::InnerClassDecl)
            .then(|| inner_class_extends_edit(child, src))
            .flatten();
        if let Some(edit) = edit {
            edits.push(edit);
        }
        collect_inner_class_extends(child, src, edits);
    }
}

/// For an inner class whose header carries `extends T`, build the edit that removes ` extends T` from
/// the header and inserts `<body-indent>extends T\n` before the first body statement. `None` when the
/// class has no `extends`, no body statement, or any required token is missing.
fn inner_class_extends_edit(
    class: &gdscript_syntax::GdNode,
    src: &str,
) -> Option<(usize, usize, usize, String)> {
    use SyntaxKind as S;
    use cstree::util::NodeOrToken;
    let name = class.children().find(|c| c.kind() == S::Name)?;
    let body = class.children().find(|c| c.kind() == S::ClassBody)?;
    // The header tokens (direct children) hold `extends`, the type spelling, and the `:`.
    let mut extends_start = None;
    let mut colon_start = None;
    for c in class.children_with_tokens() {
        if let NodeOrToken::Token(t) = c {
            match t.kind() {
                S::ExtendsKw if extends_start.is_none() => {
                    extends_start = Some(usize::from(t.text_range().start()));
                }
                S::Colon if extends_start.is_some() && colon_start.is_none() => {
                    colon_start = Some(usize::from(t.text_range().start()));
                }
                _ => {}
            }
        }
    }
    let extends_start = extends_start?;
    let colon_start = colon_start?;
    // The type spelling sits between `extends` and `:`.
    let ty = src.get(extends_start..colon_start)?.trim();
    let ty = ty.strip_prefix("extends")?.trim();
    if ty.is_empty() {
        return None;
    }
    // Remove ` extends T` — from the name's end up to the colon (drops the joining space too).
    let remove_start = usize::from(name.text_range().end());
    let remove_end = colon_start;
    // Insert the `extends` line at the first body statement's indentation (its node range starts at
    // the leading whitespace, so read it forward from there).
    let first_stmt = body.children().next()?;
    let stmt_start = usize::from(first_stmt.text_range().start());
    let indent: String = src[stmt_start..]
        .chars()
        .take_while(|&c| c == '\t' || c == ' ')
        .collect();
    let insert = format!("{indent}extends {ty}\n");
    Some((remove_start, remove_end, stmt_start, insert))
}

/// gdformat moves an inline suite body to its own indented line (`if c: x` → two lines, `func f():
/// return` → split), while keeping an inline *lambda* body (`func(): x`). We do it as a source
/// pre-pass driven by the parse tree: find each block whose first statement shares its header's line
/// — and whose parent is a real statement/declaration, not a `LambdaExpr` — and insert a newline +
/// indentation before the body. Returns `None` (leave the source untouched) if it does not parse,
/// nothing is inline, or the rewrite would drop a significant token / fail to parse.
fn expand_inline_blocks(source: &str, config: &FmtConfig) -> Option<String> {
    let parse = gdscript_syntax::parse(source);
    if !parse.errors().is_empty() {
        return None;
    }
    let unit = config.indent_unit();
    // Each split is `(start, replace_len, text)`: `replace_len` bytes at `start` become `text`.
    let mut splits: Vec<(usize, usize, String)> = Vec::new();
    collect_inline_splits(&parse.syntax_node(), source, 0, &unit, &mut splits);
    if splits.is_empty() {
        return None;
    }
    splits.sort_by_key(|(off, ..)| std::cmp::Reverse(*off)); // apply right-to-left so offsets stay valid
    let mut out = source.to_owned();
    for (off, len, text) in &splits {
        out.replace_range(*off..*off + *len, text);
    }
    // The rewrite only adds layout (newlines/indent), so it must keep every significant token and the
    // structure intact — verified by a token-equality + parse-validity check.
    if !same_significant_tokens(source, &out) || !gdscript_syntax::parse(&out).errors().is_empty() {
        return None;
    }
    Some(out)
}

/// Collect `(offset, inserted_text)` for each inline suite body, recursing with `depth` = the
/// indentation level of `node`'s own line (incremented when descending into a block).
fn collect_inline_splits(
    node: &gdscript_syntax::GdNode,
    src: &str,
    depth: usize,
    unit: &str,
    splits: &mut Vec<(usize, usize, String)>,
) {
    use SyntaxKind as S;
    use cstree::util::NodeOrToken;
    for elem in node.children_with_tokens() {
        let child = match elem {
            NodeOrToken::Node(n) => n,
            NodeOrToken::Token(t) => {
                // A statement-level `;` separates two statements — replace it (and any following
                // spaces) with a newline + this block's indent so each lands on its own line; a
                // *trailing* `;` (nothing after it on the line) is dropped.
                if t.kind() == S::Semicolon {
                    let r = t.text_range();
                    let (s, e) = (usize::from(r.start()), usize::from(r.end()));
                    let trail = src[e..].len() - src[e..].trim_start_matches([' ', '\t']).len();
                    let rest = &src[e + trail..];
                    let text = if rest.starts_with('\n') || rest.is_empty() {
                        String::new()
                    } else {
                        format!("\n{}", unit.repeat(depth))
                    };
                    splits.push((s, e + trail - s, text));
                }
                continue;
            }
        };
        // A block-introducing child: a statement/declaration `Block` or a property body
        // (`var x: T: set = f`). Its *inline-body split* (moving the body onto its own indented line)
        // is skipped for a `LambdaExpr`'s block — a lambda's body stays inline; the wrapper owns its
        // multi-line layout — but the block still counts as a depth increment so any `;`-separated
        // statements *inside* the lambda body land at the correct indentation.
        let is_block = matches!(child.kind(), S::Block | S::PropertyBody);
        let suite = is_block && node.kind() != S::LambdaExpr;
        if suite {
            // The body offset: a `Block`'s first token, or — for a property body, whose own first
            // token is the `:` — its first getter/setter node (the content after the `:`).
            let body_offset = if child.kind() == S::PropertyBody {
                child.children().next().and_then(first_sig_offset)
            } else {
                first_sig_offset(child)
            };
            let inline_body =
                body_offset.filter(|&bs| src[..bs].trim_end_matches([' ', '\t']).ends_with(':'));
            if let Some(bs) = inline_body {
                splits.push((bs, 0, format!("\n{}", unit.repeat(depth + 1))));
            }
        }
        // A child is one indent level deeper when it is a suite, an inner class's `ClassBody` (whose
        // members sit a level below the `class` header), or a `match` arm (arms sit a level below the
        // `match` with no intervening `Block` node).
        let deeper = is_block
            || child.kind() == S::ClassBody
            || (node.kind() == S::MatchStmt && child.kind() == S::MatchArm);
        collect_inline_splits(child, src, depth + usize::from(deeper), unit, splits);
    }
}

/// Collapse a multi-line lambda whose body is a single simple statement onto one line, matching
/// gdformat (`func():\n\tbody` → `func(): body`), so the surrounding statement can re-flow. Restricted
/// to a body that is a single statement, not a compound (`if`/`for`/…), on a single physical line, with
/// only whitespace between the lambda's `:` and the body — so the join is a pure newline/indent removal.
/// Returns `None` (leave the source) if nothing collapses or the result is not meaning-equivalent.
fn collapse_inline_lambdas(source: &str, config: &FmtConfig) -> Option<String> {
    let parse = gdscript_syntax::parse(source);
    if !parse.errors().is_empty() {
        return None;
    }
    let mut edits: Vec<(usize, usize)> = Vec::new(); // (start, end) byte ranges to replace with " "
    collect_lambda_collapses(&parse.syntax_node(), source, config, &mut edits);
    if edits.is_empty() {
        return None;
    }
    edits.sort_unstable_by_key(|(s, _)| std::cmp::Reverse(*s));
    let mut out = source.to_owned();
    for (s, e) in &edits {
        out.replace_range(*s..*e, " ");
    }
    if !meaning_preserved(source, &out) || !gdscript_syntax::parse(&out).errors().is_empty() {
        return None;
    }
    Some(out)
}

/// Collect `(start, end)` ranges (the whitespace/newline between a collapsible lambda's `:` and its
/// body) to replace with a single space.
fn collect_lambda_collapses(
    node: &gdscript_syntax::GdNode,
    src: &str,
    config: &FmtConfig,
    edits: &mut Vec<(usize, usize)>,
) {
    for child in node.children() {
        let collapse = (child.kind() == SyntaxKind::LambdaExpr)
            .then(|| lambda_collapse_range(child, src, config))
            .flatten();
        if let Some((s, e)) = collapse {
            edits.push((s, e));
        }
        collect_lambda_collapses(child, src, config, edits);
    }
}

/// The byte range between a lambda's `:` and its body to collapse, when the body is a single simple
/// statement spanning one physical line, the gap is whitespace only (no comment), and the *enclosing*
/// statement would fit on one line once collapsed (gdformat keeps a lambda multi-line otherwise).
fn lambda_collapse_range(
    lambda: &gdscript_syntax::GdNode,
    src: &str,
    config: &FmtConfig,
) -> Option<(usize, usize)> {
    use SyntaxKind as S;
    let block = lambda.children().find(|c| c.kind() == S::Block)?;
    let stmts: Vec<_> = block.children().collect(); // a block's child nodes are all statements
    let [stmt] = stmts.as_slice() else {
        return None;
    };
    // A compound body (`func(): if c: …`) stays multi-line, as does a body that itself spans lines.
    if matches!(
        stmt.kind(),
        S::IfStmt | S::ElifClause | S::ElseClause | S::ForStmt | S::WhileStmt | S::MatchStmt
    ) {
        return None;
    }
    let bs = first_sig_offset(stmt)?; // skip the body's own leading indentation
    let be = usize::from(stmt.text_range().end());
    if src[bs..be].contains('\n') {
        return None; // body wrapped over lines — leave it
    }
    let pre = src[..bs].trim_end_matches([' ', '\t', '\n', '\r']);
    if !pre.ends_with(':') {
        return None;
    }
    let gap = &src[pre.len()..bs];
    if !gap.contains('\n') || gap.contains('#') {
        return None; // already inline, or a comment sits in the gap
    }
    // gdformat only inlines a lambda when the *enclosing* statement then fits on one line; otherwise it
    // keeps the lambda multi-line. Estimate the collapsed length of the enclosing top-level statement
    // (the ancestor directly inside a `Block`): its content with every whitespace run squeezed to one
    // space, plus its indentation. (The estimate matches our canonical spacing closely enough to decide.)
    let mut top = lambda.clone();
    while let Some(p) = top.parent() {
        if p.kind() == S::Block {
            break;
        }
        top = p.clone();
    }
    let r = top.text_range();
    let (ts, te) = (usize::from(r.start()), usize::from(r.end()));
    let indent_cols = src[..ts]
        .bytes()
        .rev()
        .take_while(|&b| b == b'\t' || b == b' ')
        .map(|b| if b == b'\t' { 4 } else { 1 })
        .sum::<usize>();
    let content: usize = src[ts..te].split_whitespace().map(str::len).sum::<usize>()
        + src[ts..te].split_whitespace().count().saturating_sub(1); // joined by single spaces
    if indent_cols + content > config.line_width {
        return None;
    }
    Some((pre.len(), bs))
}

/// Remove redundant grouping parens from standalone-expression positions, matching gdformat's
/// `remove_outer_parentheses`. A `ParenExpr` is redundant when its parent uses it as a *whole
/// expression* (a value / condition / iterable / argument / element / nested paren) rather than as an
/// *operand* of a larger expression — `(a + b) * c` keeps its parens because the parent is the `*`
/// `BinExpr`. Returns `None` (leave the source) if it does not parse, has no such parens, or the
/// result is not meaning-equivalent / does not parse (the net catches a precedence-changing strip).
fn strip_outer_parens(source: &str) -> Option<String> {
    let parse = gdscript_syntax::parse(source);
    if !parse.errors().is_empty() {
        return None;
    }
    let mut dels: Vec<usize> = Vec::new(); // byte offsets of `(`/`)` tokens to delete (1 byte each)
    collect_redundant_parens(&parse.syntax_node(), &mut dels);
    collect_empty_signal_parens(&parse.syntax_node(), &mut dels);
    if dels.is_empty() {
        return None;
    }
    dels.sort_unstable_by(|a, b| b.cmp(a)); // delete right-to-left
    let mut out = source.to_owned();
    for off in &dels {
        out.replace_range(*off..=*off, "");
    }
    if !meaning_preserved(source, &out) || !gdscript_syntax::parse(&out).errors().is_empty() {
        return None;
    }
    Some(out)
}

/// Whether a `ParenExpr` child of a node of `parent` is a redundant grouping paren (a standalone
/// expression), not a precedence-bearing operand.
fn paren_parent_strips(parent: SyntaxKind) -> bool {
    use SyntaxKind as S;
    matches!(
        parent,
        S::VarDecl
            | S::ConstDecl
            | S::ReturnStmt
            | S::ForStmt
            | S::IfStmt
            | S::ElifClause
            | S::WhileStmt
            | S::MatchStmt
            | S::ExprStmt
            | S::ArgList
            | S::ArrayLit
            | S::DictEntry
            | S::ParenExpr
    )
}

/// Collect the byte offsets of the `(`/`)` tokens of every redundant `ParenExpr`.
fn collect_redundant_parens(node: &gdscript_syntax::GdNode, dels: &mut Vec<usize>) {
    use cstree::util::NodeOrToken;
    for child in node.children() {
        if child.kind() == SyntaxKind::ParenExpr && paren_parent_strips(node.kind()) {
            for c in child.children_with_tokens() {
                let NodeOrToken::Token(t) = c else { continue };
                if matches!(t.kind(), SyntaxKind::LParen | SyntaxKind::RParen) {
                    dels.push(usize::from(t.text_range().start()));
                }
            }
        }
        collect_redundant_parens(child, dels);
    }
}

/// Collect the `(`/`)` of a signal's *empty* parameter list — gdformat writes `signal s` not
/// `signal s()`. (A `func` always keeps its `()`, so this is restricted to signals.)
fn collect_empty_signal_parens(node: &gdscript_syntax::GdNode, dels: &mut Vec<usize>) {
    use cstree::util::NodeOrToken;
    for child in node.children() {
        let empty_params = (child.kind() == SyntaxKind::SignalDecl)
            .then(|| child.children().find(|c| c.kind() == SyntaxKind::ParamList))
            .flatten()
            .filter(|p| p.children().next().is_none());
        if let Some(params) = empty_params {
            for c in params.children_with_tokens() {
                let NodeOrToken::Token(t) = c else { continue };
                if matches!(t.kind(), SyntaxKind::LParen | SyntaxKind::RParen) {
                    dels.push(usize::from(t.text_range().start()));
                }
            }
        }
        collect_empty_signal_parens(child, dels);
    }
}

/// The byte offset of the first significant (non-trivia, non-synthetic) token within `node`.
fn first_sig_offset(node: &gdscript_syntax::GdNode) -> Option<usize> {
    use cstree::util::NodeOrToken;
    for c in node.children_with_tokens() {
        match c {
            NodeOrToken::Token(t) => {
                let k = t.kind();
                if !k.is_trivia() && !k.is_synthetic_layout() {
                    return Some(usize::from(t.text_range().start()));
                }
            }
            NodeOrToken::Node(n) => {
                if let Some(o) = first_sig_offset(n) {
                    return Some(o);
                }
            }
        }
    }
    None
}

/// Inter-token spacing: how to join two adjacent significant tokens on one logical line.
#[derive(Clone, Copy, PartialEq, Eq)]
enum Spacing {
    /// Tight — no space (`a.b`, `f(`, before `,`/`)`).
    None,
    /// Exactly one space (` + `, `, ` after a comma, ` = `).
    Single,
    /// Keep the original inter-token whitespace — the ambiguous contexts we refuse to normalize
    /// (a slice colon `arr[a:b]`). Emits nothing when there was no original whitespace.
    Verbatim,
}

/// A value-completing token: a following `+`/`-`/`~` is **binary** (not unary), and a following
/// `(`/`[` is a **call**/**subscript** (not a grouping paren / array literal).
fn is_operand_end(k: SyntaxKind) -> bool {
    use SyntaxKind as S;
    matches!(
        k,
        S::Int
            | S::Float
            | S::String
            | S::StringName
            | S::NodePath
            | S::Ident
            | S::True
            | S::False
            | S::Null
            | S::ConstPi
            | S::ConstTau
            | S::ConstInf
            | S::ConstNan
            | S::SelfKw
            | S::SuperKw
            | S::RParen
            | S::RBrack
            | S::RBrace
    )
}

fn is_open_bracket(k: SyntaxKind) -> bool {
    matches!(
        k,
        SyntaxKind::LParen | SyntaxKind::LBrack | SyntaxKind::LBrace
    )
}

fn is_close_bracket(k: SyntaxKind) -> bool {
    matches!(
        k,
        SyntaxKind::RParen | SyntaxKind::RBrack | SyntaxKind::RBrace
    )
}

/// The spacing to insert *before* `cur`, given the previous significant token `prev` on the same
/// logical line, the innermost open-bracket kind `top`, and whether `prev` was a **unary** prefix
/// operator. Node-path runs (`$Node/Path`, `%Unique`) are forced tight by the caller and never
/// reach here, so a bare `Slash`/`Percent` here is always division/modulo.
fn space_before(
    prev: SyntaxKind,
    cur: SyntaxKind,
    top: Option<SyntaxKind>,
    prev_unary: bool,
    top_enum: bool,
) -> Spacing {
    use SyntaxKind as S;
    // An enum body is spaced inside (`{ A, B }`) — unlike a dict (`{"k": v}`) — but an empty `{}`
    // stays tight. This overrides the bracket-hug rule below for the enum braces only.
    if top_enum {
        if prev == S::LBrace && cur != S::RBrace {
            return Spacing::Single;
        }
        if cur == S::RBrace && prev != S::LBrace {
            return Spacing::Single;
        }
    }
    // --- tight-forcing rules (these carve out every no-space case; the default is one space) ---
    // Hug the inside of brackets: `(x`, `x)`, `[1`, `1]`, `{k`, `v}`.
    if is_open_bracket(prev) || is_close_bracket(cur) {
        return Spacing::None;
    }
    // Member access is tight both sides (`.5` / `1.0` are single Float tokens, so a bare `Dot` is
    // always member access).
    if cur == S::Dot || prev == S::Dot {
        return Spacing::None;
    }
    // No space before a separator; `@export` is tight after the `@`.
    if cur == S::Comma || cur == S::Semicolon || prev == S::At {
        return Spacing::None;
    }
    // Tight after a unary prefix operator: `-x`, `+x`, `~x`, `!x`.
    if prev == S::Tilde || prev == S::Bang || ((prev == S::Minus || prev == S::Plus) && prev_unary)
    {
        return Spacing::None;
    }
    // Colon: never a space *before* it; *after* it, a dict/type-annotation colon gets one space,
    // while a slice colon inside `[ ]` is left verbatim (a subscript and a slice are not
    // distinguishable from local context). `:=` is its own token, handled by the default.
    if cur == S::Colon {
        return if top == Some(S::LBrack) {
            Spacing::Verbatim
        } else {
            Spacing::None
        };
    }
    if prev == S::Colon {
        return if top == Some(S::LBrack) {
            Spacing::Verbatim
        } else {
            Spacing::Single
        };
    }
    // After a separator: one space (`a, b`).
    if prev == S::Comma || prev == S::Semicolon {
        return Spacing::Single;
    }
    // Open paren: a call hugs an operand callee (`f(`, `a.b(`, `preload(`, `assert(`), and a lambda
    // header hugs its `func` (`func(x):` — a bare `func(` is always a lambda; a named func has
    // `func name(`). A grouping paren after a value-keyword keeps its space (`return (x)`, `if (c)`).
    if cur == S::LParen {
        return if is_operand_end(prev)
            || prev == S::PreloadKw
            || prev == S::AssertKw
            || prev == S::FuncKw
        {
            Spacing::None
        } else {
            Spacing::Single
        };
    }
    // Open bracket: a subscript / typed-collection hugs an operand (`arr[i]`, `Array[int]`); an
    // array literal after an operator/keyword/`,` is spaced.
    if cur == S::LBrack {
        return if is_operand_end(prev) {
            Spacing::None
        } else {
            Spacing::Single
        };
    }
    // Everything else — binary/keyword operators, assignments, `->`, `:=`, `{`, atoms, words, and a
    // unary prefix *before* its operand — takes exactly one space.
    Spacing::Single
}

/// Emit a logical-line break: a real `\n` for a content line, or — when collapsing blank lines —
/// buffer a blank line into `pending_blanks` (flushed, capped, before the next content line).
fn emit_break(
    out: &mut String,
    collapse_on: bool,
    line_had_content: &mut bool,
    pending_blanks: &mut usize,
) {
    if collapse_on && !*line_had_content {
        *pending_blanks += 1; // a blank line — capped + flushed when the next content line starts.
    } else {
        out.push('\n');
    }
    *line_had_content = false;
}

/// The depth and leading-indentation length of the next code line after token index `idx` (skipping
/// comment / blank lines), given the current raw depth `cur`. Used to place a block-boundary comment.
fn next_code_info(toks: &[gdscript_syntax::RawToken], idx: usize, cur: usize) -> (usize, usize) {
    use SyntaxKind as S;
    let mut delta: i32 = 0;
    let mut indent_len = 0usize;
    for t in &toks[idx + 1..] {
        match t.kind {
            S::Indent => delta += 1,
            S::Dedent => delta -= 1,
            S::NewlinePhys | S::Newline => indent_len = 0,
            S::Whitespace => indent_len = usize::from(t.range.len()),
            k if k.is_trivia() => {} // comments / line-continuation / BOM
            _ => {
                let d = usize::try_from(i32::try_from(cur).unwrap_or(0) + delta).unwrap_or(0);
                return (d, indent_len);
            }
        }
    }
    (cur, 0)
}

/// The intended depth of a block-boundary comment, matching gdformat's
/// `reconstruct_blank_lines_in_range`: a standalone comment joins the deepest block whose line-range
/// contains it, which is its own authored indentation level *clamped down* to the deepest open block
/// around it. `comment_levels` is the comment's authored indentation measured in indent units; the
/// deepest open block is the deeper of the previous code line's depth (`prev_depth`) and the next
/// code line's (`next_depth`) — the next statement can reveal a block the prepass has not yet opened
/// with an `Indent` (a comment that is the first line of a body). So an under-indented comment keeps
/// its own (shallower) level, while an over-indented one snaps to the deepest real block. A column-0
/// `#` comment is special-cased by the caller (kept at column 0).
fn comment_depth(comment_levels: usize, prev_depth: usize, next_depth: usize) -> usize {
    comment_levels.min(prev_depth.max(next_depth))
}

/// Re-emit the pre-pass token stream with normalized **indentation**, **intra-line spacing** (when
/// `config.normalize_spacing`), trailing whitespace, and a single final newline. Significant token
/// *text* is always emitted verbatim (so meaning is preserved); only the whitespace *between*
/// tokens — and a logical line's leading indentation — is rewritten. Bracketed-continuation
/// interiors and node-path runs are kept verbatim / tight (see the module docs).
#[allow(
    clippy::too_many_lines,
    reason = "one cohesive token-stream state machine; the indentation, spacing, bracket-stack and node-path transitions are interdependent and clearer kept together than split across helpers"
)]
fn reindent(source: &str, config: &FmtConfig) -> String {
    let raw = gdscript_syntax::tokenize(source);
    let (toks, _diags) = gdscript_syntax::run_prepass(&raw, source);
    let unit = config.indent_unit();
    let spacing_on = config.normalize_spacing;
    let collapse_on = config.collapse_blank_lines;

    let mut out = String::with_capacity(source.len() + 16);
    let mut depth: usize = 0;
    // --- blank-line state (only used when `collapse_on`) ---
    // Blank lines are buffered, not emitted, until the next content line: we then flush at most
    // `cap` of them (2 at top level, 1 nested) — knowing the *next* line's depth, since the prepass
    // emits the `Dedent` between a block and a following top-level line AFTER the blank lines.
    let mut pending_blanks: usize = 0;
    // Whether the current logical line emitted any content (a significant token or a comment) — a
    // line that did not is a blank line.
    let mut line_had_content = false;
    // Whether any content has been emitted yet (leading blank lines are stripped entirely).
    let mut seen_content = false;
    // `true` at the start of a logical line, before its first significant token (when we re-emit
    // the indentation, `depth` being final by then).
    let mut line_start = true;
    // A synthetic `Newline` precedes the `NewlinePhys` carrying the line's bytes; this swallows that
    // one `NewlinePhys` so the break is emitted once. See the original notes below.
    let mut just_broke = false;
    // Innermost-first stack of open-bracket kinds: `.len()` is the old `bracket_depth` (drives the
    // continuation logic), `.last()` is the colon-context discriminator for spacing.
    let mut stack: Vec<SyntaxKind> = Vec::new();
    // Parallel to the `{` entries in `stack`: whether each brace is an **enum body** (spaced inside,
    // `{ A, B }`) rather than a dict (tight, `{"k": v}`). `pending_enum` is armed by an `enum` keyword
    // and consumed by the brace it opens.
    let mut brace_is_enum: Vec<bool> = Vec::new();
    let mut pending_enum = false;
    // --- intra-line spacing state (all reset at a logical-line break) ---
    let mut prev_sig: Option<SyntaxKind> = None;
    let mut prev_unary = false;
    let mut node_path = false;
    // Original inter-token whitespace, buffered so the next token can keep it (`Verbatim`) or drop
    // it (and re-synthesize). Only used when `spacing_on`.
    let mut pending_ws: Option<&str> = None;
    // Set after a physical newline *inside* brackets: the next line's leading whitespace (alignment)
    // is kept verbatim — increment A does not reflow bracketed continuations.
    let mut cont_line_start = false;
    // The leading whitespace of the current logical line (used to recover a *comment*'s authored
    // indentation, since the prepass attributes a block-boundary comment to the wrong raw `depth`).
    let mut line_indent_ws: Option<&str> = None;
    // The depth of the most recent *code* line, and whether the most recent content line was a
    // comment. Used to detect the start of a block: a code statement deeper than the previous code
    // line, with no comment in between, is a suite's first statement — gdformat strips a blank between
    // a compound header and its body. A comment is the block's "first content" (its trailing blanks
    // are kept), so an intervening comment suppresses the strip.
    let mut prev_content_depth: usize = 0;
    let mut prev_was_comment = false;

    for (idx, t) in toks.iter().enumerate() {
        let text = &source[t.range];
        match t.kind {
            SyntaxKind::Indent => depth += 1,
            SyntaxKind::Dedent => depth = depth.saturating_sub(1),
            // A synthetic line break: ends the logical line; the next one is re-indented. A synthetic
            // `Newline` only follows a statement, so it always terminates a content line.
            SyntaxKind::Newline => {
                trim_trailing_inline_ws(&mut out);
                if stack.is_empty() {
                    // A normal logical-line break: the next line is re-indented from `depth`.
                    emit_break(
                        &mut out,
                        collapse_on,
                        &mut line_had_content,
                        &mut pending_blanks,
                    );
                    line_start = true;
                    prev_sig = None;
                } else {
                    // A synthetic break *inside brackets* is a multi-line lambda-body line break:
                    // the prepass suppresses synthetic layout inside brackets EXCEPT for a lambda
                    // body, for which it re-emits `Newline`/`Indent`/`Dedent`. Treat it like a
                    // `NewlinePhys` continuation and keep the bracketed interior verbatim — do NOT
                    // re-indent the body from `depth`. The body's verbatim-aligned *header* arrives
                    // via `NewlinePhys` (kept verbatim), so depth-re-indenting only the body makes
                    // the two disagree and produces non-parsing output. Canonical reflow of a
                    // bracketed lambda block is the increment-C reflow's job, not the indenter's.
                    out.push('\n');
                    cont_line_start = true;
                }
                just_broke = true;
                node_path = false;
                pending_ws = None;
                line_indent_ws = None;
                pending_enum = false; // an `enum` keyword is consumed by its `{` within one line
            }
            SyntaxKind::NewlinePhys => {
                if just_broke {
                    just_broke = false; // its bytes belong to the synthetic break already emitted
                } else {
                    trim_trailing_inline_ws(&mut out);
                    node_path = false;
                    pending_ws = None;
                    // Outside brackets this ends a comment-only line (content) or a blank line, and
                    // the next line is re-indented. Inside brackets it is a real continuation — keep
                    // the next line's alignment verbatim and never collapse it.
                    if stack.is_empty() {
                        emit_break(
                            &mut out,
                            collapse_on,
                            &mut line_had_content,
                            &mut pending_blanks,
                        );
                        line_start = true;
                        prev_sig = None;
                        line_indent_ws = None;
                    } else {
                        out.push('\n');
                        cont_line_start = true;
                    }
                }
            }
            SyntaxKind::Whitespace => {
                if line_start {
                    // Leading indentation — dropped; re-synthesized at the first significant token.
                    // Remember it: a *comment*'s authored indentation is recovered from it below.
                    line_indent_ws = Some(text);
                } else if spacing_on {
                    pending_ws = Some(text); // buffered; the next token decides whether to keep it.
                } else {
                    out.push_str(text); // indentation-only mode: original verbatim behavior.
                }
            }
            // A significant token or a comment.
            _ => {
                if line_start {
                    // A block-boundary *comment* is attributed by the prepass to the wrong raw
                    // `depth` (the `Indent`/`Dedent` lands on the next *code* line, not the comment).
                    // Recover its intended depth the way gdformat does: its authored indentation level
                    // clamped down to the deepest open block (`min(authored, max(prev, next))`).
                    let is_comment = matches!(
                        t.kind,
                        SyntaxKind::LineComment
                            | SyntaxKind::DocComment
                            | SyntaxKind::RegionComment
                            | SyntaxKind::EndRegionComment
                    );
                    let comment_len = line_indent_ws.map_or(0, str::len);
                    let emit_depth = if is_comment {
                        if comment_len == 0 {
                            // gdformat keeps a column-0 comment (`line.startswith("#")`) at column 0 —
                            // e.g. file-spanning `#region`/`#endregion` markers — rather than indenting
                            // it to the enclosing block.
                            0
                        } else {
                            let (next_depth, _) = next_code_info(&toks, idx, depth);
                            let unit_len = unit.len().max(1);
                            comment_depth(comment_len / unit_len, depth, next_depth)
                        }
                    } else {
                        depth
                    };
                    // Flush the buffered blank lines. gdformat squeezes *every* run of blank lines to a
                    // single blank, then re-inserts the 2nd (top-level) / 1st (nested) blank only around
                    // definitions (done later by `insert_def_blanks`). So the cap here is 1 everywhere,
                    // 0 before the first content (leading blanks gone), and 0 at the **start of a block**
                    // (a suite's first statement directly under its header — gdformat's
                    // `_remove_empty_strings_from_begin` strips a blank between a header and its body).
                    // The strip fires only for a *code* statement deeper than the previous code line
                    // with **no comment in between**: a comment is the block's first content, so its
                    // trailing blanks are kept (and a column-0 comment, whose raw `depth` lags, never
                    // looks like a header). This mirrors gdformat's `previous_statement_name is None`.
                    if collapse_on {
                        let at_block_start =
                            !prev_was_comment && !is_comment && depth > prev_content_depth;
                        let cap = if at_block_start {
                            0
                        } else {
                            usize::from(seen_content)
                        };
                        for _ in 0..pending_blanks.min(cap) {
                            out.push('\n');
                        }
                        pending_blanks = 0;
                    }
                    prev_was_comment = is_comment;
                    if !is_comment {
                        prev_content_depth = depth;
                    }
                    for _ in 0..emit_depth {
                        out.push_str(&unit);
                    }
                    line_start = false;
                    cont_line_start = false;
                } else if spacing_on {
                    if cont_line_start {
                        // First token of a bracketed continuation: keep its leading alignment.
                        if let Some(ws) = pending_ws {
                            out.push_str(ws);
                        }
                        cont_line_start = false;
                    } else if matches!(
                        t.kind,
                        SyntaxKind::LineComment
                            | SyntaxKind::DocComment
                            | SyntaxKind::RegionComment
                            | SyntaxKind::EndRegionComment
                    ) {
                        // An inline (trailing) comment is offset by exactly two spaces (gdformat's
                        // `INLINE_COMMENT_OFFSET`), regardless of the original spacing.
                        out.push_str("  ");
                        node_path = false;
                    } else if t.kind.is_trivia() {
                        // A line-continuation / BOM: keep the original spacing before it.
                        if let Some(ws) = pending_ws {
                            out.push_str(ws);
                        }
                        node_path = false;
                    } else {
                        // A significant token: synthesize the spacing. Inside a node-path run we
                        // keep the *original* spacing verbatim — a real `$Node/Path` is already
                        // tight (so it stays tight), and we must never *collapse* a genuinely-spaced
                        // `$A / b` (a division) into a node path, which would silently change meaning
                        // with an identical token sequence (the safety net cannot catch it).
                        let path_continue = node_path
                            && (matches!(
                                t.kind,
                                SyntaxKind::Ident | SyntaxKind::Slash | SyntaxKind::String
                            ) || (t.kind == SyntaxKind::Percent
                                && matches!(
                                    prev_sig,
                                    Some(SyntaxKind::Dollar | SyntaxKind::Slash)
                                )));
                        let spacing = if path_continue {
                            // A `%` in sigil position (`$%Unique`, `$A/%Unique`) continues the path; a
                            // `%` after an identifier is modulo and falls through to `space_before`.
                            Spacing::Verbatim
                        } else {
                            node_path = false; // any non-path token ends the run.
                            match prev_sig {
                                Some(p) => {
                                    let top_enum = stack.last() == Some(&SyntaxKind::LBrace)
                                        && brace_is_enum.last() == Some(&true);
                                    space_before(
                                        p,
                                        t.kind,
                                        stack.last().copied(),
                                        prev_unary,
                                        top_enum,
                                    )
                                }
                                None => Spacing::None,
                            }
                        };
                        match spacing {
                            Spacing::None => {}
                            Spacing::Single => out.push(' '),
                            Spacing::Verbatim => {
                                if let Some(ws) = pending_ws {
                                    out.push_str(ws);
                                }
                            }
                        }
                    }
                }
                pending_ws = None;
                just_broke = false;
                // String literals are normalized to gdformat's canonical quote style (value-
                // preserving; guarded by the meaning-equivalence net). Everything else is verbatim.
                if config.normalize_strings
                    && matches!(
                        t.kind,
                        SyntaxKind::String | SyntaxKind::StringName | SyntaxKind::NodePath
                    )
                {
                    out.push_str(&canonical_string(text));
                } else {
                    out.push_str(text);
                }
                // Any token reaching here (a significant token or a comment) is line content.
                line_had_content = true;
                seen_content = true;
                match t.kind {
                    SyntaxKind::LBrace => {
                        stack.push(t.kind);
                        brace_is_enum.push(pending_enum);
                        pending_enum = false;
                    }
                    SyntaxKind::LParen | SyntaxKind::LBrack => {
                        stack.push(t.kind);
                    }
                    SyntaxKind::RBrace => {
                        stack.pop();
                        brace_is_enum.pop();
                    }
                    SyntaxKind::RParen | SyntaxKind::RBrack => {
                        stack.pop();
                    }
                    _ => {}
                }
                // Spacing state — significant tokens only (a comment is never an operand).
                if !t.kind.is_trivia() {
                    if t.kind == SyntaxKind::EnumKw {
                        pending_enum = true; // the next `{` opens an enum body
                    }
                    let unary_ctx = prev_sig.is_none_or(|p| !is_operand_end(p));
                    // Enter node-path mode after a `$` (always) or a `%` in sigil position (a `%`
                    // after an operand is modulo, and stays a normal binary operator).
                    if t.kind == SyntaxKind::Dollar || (t.kind == SyntaxKind::Percent && unary_ctx)
                    {
                        node_path = true;
                    }
                    prev_unary = match t.kind {
                        SyntaxKind::Minus | SyntaxKind::Plus => unary_ctx,
                        SyntaxKind::Tilde | SyntaxKind::Bang => true,
                        _ => false,
                    };
                    // A soft keyword (`match`/`when`) used as a *member* (`obj.match`) is an ordinary
                    // identifier operand — record it as `Ident` so a following `(` hugs it
                    // (`obj.match(x)`), while a leading `match (x):` statement keeps its space.
                    let member_soft_kw = matches!(t.kind, SyntaxKind::MatchKw | SyntaxKind::WhenKw)
                        && prev_sig == Some(SyntaxKind::Dot);
                    prev_sig = Some(if member_soft_kw {
                        SyntaxKind::Ident
                    } else {
                        t.kind
                    });
                }
            }
        }
    }
    // Trim a trailing blank/whitespace run and guarantee exactly one final newline.
    let trimmed = out.trim_end();
    let mut result = String::with_capacity(trimmed.len() + 1);
    result.push_str(trimmed);
    if !result.is_empty() {
        result.push('\n');
    }
    result
}

/// The role a statement (logical) line plays in the blank-line-insertion policy.
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
enum LineRole {
    /// A `func` / `static func` / `class` declaration — gdformat surrounds these with blank lines.
    Def,
    /// A standalone comment line (`#` / `##`) — attaches to a following statement.
    Comment,
    /// A standalone annotation line (`@foo` with nothing else on the line) — attaches to a following
    /// statement.
    Annotation,
    /// Any other statement (`var` / `const` / `signal` / `class_name` / an expression / …).
    Other,
}

/// A statement-level "head" line discovered in already-formatted text: its 0-based physical line
/// number, its block depth, and its [`LineRole`]. Bracket-continuation and lambda-body lines are
/// not heads.
#[derive(Clone, Copy)]
struct HeadLine {
    line: usize,
    depth: usize,
    role: LineRole,
}

/// One logical unit for the blank-line edge rule: a definition (or other statement) together with
/// any comment/annotation prefix that attaches to it. `head_line` is the unit's first physical line.
#[derive(Clone, Copy)]
struct Unit {
    head_line: usize,
    depth: usize,
    is_def: bool,
    /// A *standalone* comment (not a prefix that attaches to a following def/statement). gdformat does
    /// not force blank lines around such a comment (e.g. a trailing `#endregion`), and it is
    /// transparent to the surrounding defs' blank-line spacing.
    is_comment: bool,
    /// The unit is a definition reached through an **annotation** prefix (`@rpc func …`). gdformat
    /// forces blanks before such a unit only when the *previous* sibling was itself a def — not on the
    /// unit's own def-ness (the annotation line, not the def, owns the leading blanks, and an
    /// annotation is absent from the surrounding-empty-lines table). A *comment*-prefixed def forces
    /// like a plain def.
    ann_prefixed: bool,
}

/// Classify the statement line whose first significant-or-comment token is `toks[start]`. Scans the
/// rest of the logical line (across bracketed continuations) to look past an annotation prefix.
fn classify_line(toks: &[gdscript_syntax::RawToken], start: usize) -> LineRole {
    use SyntaxKind as S;
    if matches!(
        toks[start].kind,
        S::LineComment | S::DocComment | S::RegionComment | S::EndRegionComment
    ) {
        return LineRole::Comment;
    }
    // Collect the line's significant token kinds (skip trivia; stop at the logical line end).
    let mut kinds: Vec<S> = Vec::new();
    let mut local_stack = 0usize;
    for t in &toks[start..] {
        match t.kind {
            S::Newline => break,
            S::NewlinePhys if local_stack == 0 => break,
            S::LParen | S::LBrack | S::LBrace => {
                local_stack += 1;
                kinds.push(t.kind);
            }
            S::RParen | S::RBrack | S::RBrace => {
                local_stack = local_stack.saturating_sub(1);
                kinds.push(t.kind);
            }
            k if k.is_trivia() || k.is_synthetic_layout() => {}
            k => kinds.push(k),
        }
    }
    // Skip a leading annotation prefix: `@ Ident (balanced parens)?`, repeated.
    let mut i = 0;
    while kinds.get(i) == Some(&S::At) {
        i += 1; // `@`
        if kinds.get(i) == Some(&S::Ident) {
            i += 1; // annotation name
        }
        if kinds.get(i) == Some(&S::LParen) {
            // skip the balanced argument list
            let mut d = 0usize;
            while let Some(&k) = kinds.get(i) {
                i += 1;
                match k {
                    S::LParen => d += 1,
                    S::RParen => {
                        d -= 1;
                        if d == 0 {
                            break;
                        }
                    }
                    _ => {}
                }
            }
        }
    }
    match kinds.get(i) {
        None => LineRole::Annotation, // an annotation alone on its line
        Some(S::FuncKw | S::ClassKw) => LineRole::Def,
        Some(S::StaticKw) if kinds.get(i + 1) == Some(&S::FuncKw) => LineRole::Def,
        _ => LineRole::Other,
    }
}

/// Insert blank lines around definitions to match gdformat's policy (2 around top-level defs, 1
/// around nested defs), operating on already-formatted, already-collapsed text. Purely additive —
/// it only inserts blank lines, so it never changes the significant token sequence. Idempotent: the
/// blanks it would add are already present on a second pass.
#[allow(
    clippy::too_many_lines,
    reason = "three cohesive sequential passes (find heads, group units, apply the edge rule) over the same token stream; clearer kept together than split"
)]
fn insert_def_blanks(formatted: &str, config: &FmtConfig) -> String {
    use SyntaxKind as S;
    let raw = gdscript_syntax::tokenize(formatted);
    let (toks, _diags) = gdscript_syntax::run_prepass(&raw, formatted);

    let lines: Vec<&str> = formatted.lines().collect();
    let is_blank: Vec<bool> = lines.iter().map(|l| l.trim().is_empty()).collect();
    let blank_above = |l: usize| l > 0 && is_blank.get(l - 1).copied().unwrap_or(false);

    // Byte offset -> 0-based physical line number. Built from the raw bytes (so newlines *inside*
    // multi-line string tokens count too) — `formatted.lines()` indices must align with this.
    let line_starts: Vec<usize> = std::iter::once(0)
        .chain(
            formatted
                .bytes()
                .enumerate()
                .filter_map(|(i, b)| (b == b'\n').then_some(i + 1)),
        )
        .collect();
    let line_of = |offset: usize| line_starts.partition_point(|&s| s <= offset) - 1;
    // The (already-normalized) leading-indentation depth of a formatted line.
    let line_depth = |l: usize| -> usize {
        let s = lines.get(l).copied().unwrap_or("");
        if config.use_tabs {
            s.bytes().take_while(|&b| b == b'\t').count()
        } else {
            s.bytes()
                .take_while(|&b| b == b' ')
                .count()
                .checked_div(config.indent_size)
                .unwrap_or(0)
        }
    };

    // --- pass 1: find statement-level head lines (skip bracket / lambda continuations) ---
    let mut heads: Vec<HeadLine> = Vec::new();
    let mut depth = 0usize;
    let mut stack = 0usize;
    let mut this_line_continues = false; // a trailing `\` continues onto the next physical line
    let mut next_line_is_cont = false; // the logical line we are about to start is a continuation
    let mut seen_first_on_line = false;
    for (idx, t) in toks.iter().enumerate() {
        match t.kind {
            S::Indent => depth += 1,
            S::Dedent => depth = depth.saturating_sub(1),
            S::NewlinePhys => {
                next_line_is_cont = stack > 0 || this_line_continues;
                this_line_continues = false;
                seen_first_on_line = false;
            }
            S::LineContinuation => this_line_continues = true,
            S::Newline | S::Whitespace | S::Bom => {}
            _ => {
                if !seen_first_on_line {
                    seen_first_on_line = true;
                    if stack == 0 && !next_line_is_cont {
                        let line = line_of(usize::from(t.range.start()));
                        let role = classify_line(&toks, idx);
                        // A comment's *structural* block (which one it belongs to, for the edge
                        // rule) is not its visual indentation: a comment whose *next* code is deeper
                        // than the comment's own column belongs to that deeper block — even a column-0
                        // comment sitting amid a function body (commented-out code) is inside the
                        // function, not a sibling of the preceding def. Otherwise it sits at its own
                        // (visual) depth, which correctly attaches a column-0 doc-comment to a
                        // following *dedented* def (whose next code is at column 0, not deeper).
                        let head_depth = if role == LineRole::Comment {
                            let visual = line_depth(line);
                            let next_depth = next_code_info(&toks, idx, depth).0;
                            if next_depth > visual {
                                next_depth
                            } else {
                                visual
                            }
                        } else {
                            depth
                        };
                        heads.push(HeadLine {
                            line,
                            depth: head_depth,
                            role,
                        });
                    }
                }
                match t.kind {
                    S::LParen | S::LBrack | S::LBrace => stack += 1,
                    S::RParen | S::RBrack | S::RBrace => stack = stack.saturating_sub(1),
                    _ => {}
                }
            }
        }
    }

    // --- pass 2: group heads into units (a def/statement + its attached comment/annotation prefix) ---
    let mut units: Vec<Unit> = Vec::new();
    let mut i = 0;
    while i < heads.len() {
        let h = heads[i];
        if matches!(h.role, LineRole::Comment | LineRole::Annotation) {
            // Accumulate a contiguous, same-depth prefix run of comments/annotations.
            let mut j = i + 1;
            while j < heads.len()
                && matches!(heads[j].role, LineRole::Comment | LineRole::Annotation)
                && heads[j].depth == h.depth
                && !blank_above(heads[j].line)
            {
                j += 1;
            }
            // Does a same-depth statement follow contiguously (no blank gap)? Then the run attaches.
            if j < heads.len()
                && heads[j].depth == h.depth
                && !blank_above(heads[j].line)
                && matches!(heads[j].role, LineRole::Def | LineRole::Other)
            {
                units.push(Unit {
                    head_line: h.line,
                    depth: h.depth,
                    is_def: heads[j].role == LineRole::Def,
                    is_comment: false,
                    ann_prefixed: h.role == LineRole::Annotation,
                });
                i = j + 1;
            } else {
                // Standalone comment/annotation lines (each its own non-def unit).
                for h in &heads[i..j] {
                    units.push(Unit {
                        head_line: h.line,
                        depth: h.depth,
                        is_def: false,
                        is_comment: h.role == LineRole::Comment,
                        ann_prefixed: false,
                    });
                }
                i = j;
            }
        } else {
            units.push(Unit {
                head_line: h.line,
                depth: h.depth,
                is_def: h.role == LineRole::Def,
                is_comment: false,
                ann_prefixed: false,
            });
            i += 1;
        }
    }

    // --- pass 3: the edge rule — N blanks before a unit whose own or previous sibling is a def ---
    let mut required: std::collections::HashMap<usize, usize> = std::collections::HashMap::new();
    let mut last_is_def: Vec<Option<bool>> = Vec::new();
    for (k, u) in units.iter().enumerate() {
        while last_is_def.len() <= u.depth {
            last_is_def.push(None);
        }
        let prev = last_is_def[u.depth];
        let first_in_block = prev.is_none();
        let n = if u.depth == 0 { 2 } else { 1 };
        // A *trailing* standalone comment — one that ends its block (no following unit at its depth or
        // shallower-after) such as a closing `#endregion` — keeps its source blanks unforced, matching
        // gdformat's end-of-block reconstruction. A standalone comment *between* statements still
        // resets def-adjacency (a following statement's spacing is measured from it, not the def above).
        let trailing_comment = u.is_comment && units.get(k + 1).is_none_or(|nx| nx.depth < u.depth);
        // An annotation-prefixed def forces blanks only after a previous def; a plain or
        // comment-prefixed def also forces on its own def-ness.
        let needs = if u.ann_prefixed {
            prev == Some(true)
        } else {
            u.is_def || prev == Some(true)
        };
        if !trailing_comment && !first_in_block && needs {
            required.insert(u.head_line, n);
        }
        last_is_def[u.depth] = Some(u.is_def);
        last_is_def.truncate(u.depth + 1); // entering a deeper block starts its siblings fresh
    }
    if required.is_empty() {
        return formatted.to_owned();
    }

    // --- rebuild: top up the blank run before each required head to exactly N ---
    let mut out = String::with_capacity(formatted.len() + required.len() * 2);
    let mut trailing_blanks = 0usize;
    for (lno, content) in lines.iter().enumerate() {
        if let Some(&k) = required.get(&lno) {
            for _ in trailing_blanks..k {
                out.push('\n');
            }
        }
        out.push_str(content);
        out.push('\n');
        trailing_blanks = if is_blank[lno] {
            trailing_blanks + 1
        } else {
            0
        };
    }
    out
}

// ===================== line reflow (length-driven wrapping) =====================
//
// A statement that does not fit in `line_width` and contains a bracketed group is wrapped the way
// gdformat does: try the group **flat**; if it does not fit, **compact** (all elements on one
// indented continuation line, close bracket on its own line); if even that does not fit, **exploded**
// (one element per line, recursively). A **magic trailing comma** in the source forces a group
// exploded-with-comma even when it would fit (and forces every enclosing group multi-line) — the one
// case that mutates the token sequence (a trailing comma), guarded by the meaning-equivalence net.
// Only statements that occupy a single physical line are reflowed (so the pass is trivially
// idempotent — a wrapped statement spans bracket-continuation lines that are skipped on the next run).

/// One rendered token: its kind, text, and whether a single space precedes it in the flat form.
struct Atom {
    kind: SyntaxKind,
    text: String,
    space: bool,
}

/// A reflow document node: a text run, or a bracketed group of comma-separated element sequences.
enum ReflowDoc {
    Text {
        text: String,
        space: bool,
    },
    Group {
        space: bool,
        open: String,
        elems: Vec<Vec<ReflowDoc>>,
        close: &'static str,
        /// The source had a **magic trailing comma** — gdformat forces this group exploded one per
        /// line *with* the trailing comma, even when it would fit.
        magic: bool,
    },
}

/// Whether a group must be broken across lines: it has a magic trailing comma, or a descendant does
/// (a magic comma anywhere forces every enclosing group multi-line, matching gdformat).
fn group_forced(elems: &[Vec<ReflowDoc>], magic: bool) -> bool {
    magic
        || elems.iter().any(|e| {
            e.iter().any(|d| match d {
                ReflowDoc::Group { elems, magic, .. } => group_forced(elems, *magic),
                ReflowDoc::Text { .. } => false,
            })
        })
}

fn open_close(text: &str) -> Option<&'static str> {
    match text {
        "(" => Some(")"),
        "[" => Some("]"),
        "{" => Some("}"),
        _ => None,
    }
}

/// Display columns of a content string (no tabs — those only appear in leading indentation).
fn cols(s: &str) -> usize {
    s.chars().count()
}

/// Display width of a full line, counting a leading tab as `tw` columns.
fn display_cols(line: &str, tw: usize) -> usize {
    line.chars().map(|c| if c == '\t' { tw } else { 1 }).sum()
}

/// Build the comma-separated element sequences of a group whose contents start at `atoms[*i]`,
/// consuming through the matching `close` (or to the end when `close` is `None`, for the top level).
/// Returns the elements and whether the group ended with a magic trailing comma.
fn build_elems(atoms: &[Atom], i: &mut usize, close: Option<&str>) -> (Vec<Vec<ReflowDoc>>, bool) {
    let mut elems: Vec<Vec<ReflowDoc>> = Vec::new();
    let mut cur: Vec<ReflowDoc> = Vec::new();
    let mut elem_start = true;
    while *i < atoms.len() {
        let a = &atoms[*i];
        if close == Some(a.text.as_str()) {
            *i += 1;
            break;
        }
        if a.text == "," {
            elems.push(std::mem::take(&mut cur));
            elem_start = true;
            *i += 1;
            continue;
        }
        let space = !elem_start && a.space;
        if let Some(cl) = open_close(&a.text) {
            let open = a.text.clone();
            *i += 1;
            let (inner, magic) = build_elems(atoms, i, Some(cl));
            cur.push(ReflowDoc::Group {
                space,
                open,
                elems: inner,
                close: cl,
                magic,
            });
        } else {
            cur.push(ReflowDoc::Text {
                text: a.text.clone(),
                space,
            });
            *i += 1;
        }
        elem_start = false;
    }
    // A trailing comma leaves `elem_start` set with `cur` empty after at least one real element.
    let magic = elem_start && !elems.is_empty();
    if !cur.is_empty() {
        elems.push(cur);
    }
    (elems, magic)
}

fn flat_seq(docs: &[ReflowDoc]) -> String {
    let mut out = String::new();
    for d in docs {
        match d {
            ReflowDoc::Text { text, space } => {
                if *space {
                    out.push(' ');
                }
                out.push_str(text);
            }
            ReflowDoc::Group {
                space,
                open,
                elems,
                close,
                magic,
            } => {
                if *space {
                    out.push(' ');
                }
                out.push_str(open);
                out.push_str(&flat_group_contents(elems, *magic));
                out.push_str(close);
            }
        }
    }
    out
}

fn flat_group_contents(elems: &[Vec<ReflowDoc>], magic: bool) -> String {
    let mut s = elems
        .iter()
        .map(|e| flat_seq(e))
        .collect::<Vec<_>>()
        .join(", ");
    if magic && !elems.is_empty() {
        s.push(',');
    }
    s
}

/// Render a sequence (one element) at base `indent`, starting at column `col`, wrapping any group
/// that does not fit. Returns the text and the ending column.
fn render_seq(
    docs: &[ReflowDoc],
    indent: usize,
    mut col: usize,
    cfg: &FmtConfig,
    tw: usize,
    unit: &str,
) -> (String, usize) {
    let mut out = String::new();
    // Flat width of each doc, so a group knows the width of the content that *follows* it on the line
    // (its "tail") — a group must wrap when the group + its tail overflow, even if the group alone fits.
    let widths: Vec<usize> = docs
        .iter()
        .map(|d| cols(&flat_seq(std::slice::from_ref(d))))
        .collect();
    let total: usize = widths.iter().sum();
    // gdformat wraps the *outermost* (last) bracket group — except a `func`/`static func` definition,
    // whose **parameter list** (the first group) is the one that wraps, not a `-> Array[T]` return
    // type. An earlier group otherwise stays flat unless it is itself forced (a magic comma).
    let first_text = match docs.first() {
        Some(ReflowDoc::Text { text, .. }) => Some(text.as_str()),
        _ => None,
    };
    let is_def = first_text == Some("func")
        || (first_text == Some("static")
            && matches!(docs.get(1), Some(ReflowDoc::Text { text, .. }) if text == "func"));
    let wrap_target = if is_def {
        docs.iter()
            .position(|d| matches!(d, ReflowDoc::Group { .. }))
    } else {
        docs.iter()
            .rposition(|d| matches!(d, ReflowDoc::Group { .. }))
    };
    let mut before = 0usize;
    for (i, d) in docs.iter().enumerate() {
        let tail = total - before - widths[i];
        match d {
            ReflowDoc::Text { text, space } => {
                if *space {
                    out.push(' ');
                    col += 1;
                }
                out.push_str(text);
                col += cols(text);
            }
            ReflowDoc::Group {
                space,
                open,
                elems,
                close,
                magic,
            } => {
                if *space {
                    out.push(' ');
                    col += 1;
                }
                let (g, end) = if Some(i) == wrap_target || group_forced(elems, *magic) {
                    render_group(open, elems, close, *magic, indent, col, tail, cfg, tw, unit)
                } else {
                    let flat = format!("{open}{}{close}", flat_group_contents(elems, *magic));
                    let end = col + cols(&flat);
                    (flat, end)
                };
                out.push_str(&g);
                col = end;
            }
        }
        before += widths[i];
    }
    (out, col)
}

#[allow(
    clippy::too_many_arguments,
    reason = "a focused internal renderer threading layout state"
)]
fn render_group(
    open: &str,
    elems: &[Vec<ReflowDoc>],
    close: &str,
    magic: bool,
    indent: usize,
    col: usize,
    tail: usize,
    cfg: &FmtConfig,
    tw: usize,
    unit: &str,
) -> (String, usize) {
    let forced = group_forced(elems, magic);
    // flat: the whole group on the current line (only when it — plus the content that follows it on
    // the line — fits, and it is not forced multi-line).
    let flat = format!("{open}{}{close}", flat_group_contents(elems, magic));
    if !forced && (col + cols(&flat) + tail <= cfg.line_width || elems.is_empty()) {
        let end = col + cols(&flat);
        return (flat, end);
    }
    let inner = indent + 1;
    let inner_col = inner * tw;
    let close_end = indent * tw + cols(close);
    // compact: all elements on one indented continuation line (not when forced exploded).
    if !forced {
        let contents = flat_group_contents(elems, magic);
        if inner_col + cols(&contents) <= cfg.line_width {
            let s = format!(
                "{open}\n{ind}{contents}\n{base}{close}",
                ind = unit.repeat(inner),
                base = unit.repeat(indent),
            );
            return (s, close_end);
        }
    }
    // exploded: one element per line (each rendered recursively). A comma follows every element
    // except the last — unless this group is magic, when it follows the last one too.
    let mut s = String::from(open);
    s.push('\n');
    for (k, elem) in elems.iter().enumerate() {
        s.push_str(&unit.repeat(inner));
        let (es, _) = render_seq(elem, inner, inner_col, cfg, tw, unit);
        s.push_str(&es);
        if magic || k + 1 < elems.len() {
            s.push(',');
        }
        s.push('\n');
    }
    s.push_str(&unit.repeat(indent));
    s.push_str(close);
    (s, close_end)
}

/// Parse a single physical line's content into reflow atoms, or `None` if it is not a clean,
/// reflowable single-line statement (has a comment, an error/continuation token, or unbalanced /
/// negative bracket nesting). A magic trailing comma is kept (it drives gdformat's exploded mode).
fn line_atoms(body: &str) -> Option<Vec<Atom>> {
    use SyntaxKind as S;
    let toks = gdscript_syntax::tokenize(body);
    let mut atoms: Vec<Atom> = Vec::new();
    let mut space = false;
    let mut depth: i32 = 0;
    for t in &toks {
        match t.kind {
            S::Whitespace => space = true,
            S::Bom => {}
            S::LineComment
            | S::DocComment
            | S::RegionComment
            | S::EndRegionComment
            | S::LineContinuation => return None,
            k => {
                if matches!(k, S::LParen | S::LBrack | S::LBrace) {
                    depth += 1;
                } else if matches!(k, S::RParen | S::RBrack | S::RBrace) {
                    depth -= 1;
                    if depth < 0 {
                        return None;
                    }
                }
                atoms.push(Atom {
                    kind: k,
                    text: body[t.range].to_string(),
                    space,
                });
                space = false;
            }
        }
    }
    if depth != 0 {
        return None;
    }
    Some(atoms)
}

/// Binary-operator precedence (lower binds looser → breaks first); `None` for non-binary tokens.
/// Mirrors the parser's `infix_prec`.
pub(crate) fn infix_prec(kind: SyntaxKind) -> Option<u8> {
    use SyntaxKind as S;
    Some(match kind {
        S::OrKw | S::PipePipe => 4,
        S::AndKw | S::AmpAmp => 5,
        S::InKw => 7,
        S::EqEq | S::Neq | S::Lt | S::Gt | S::Le | S::Ge => 8,
        S::Pipe => 9,
        S::Caret => 10,
        S::Amp => 11,
        S::Shl | S::Shr => 12,
        S::Plus | S::Minus => 13,
        S::Star | S::Slash | S::Percent => 14,
        S::StarStar => 17,
        _ => return None,
    })
}

fn is_assign_op(kind: SyntaxKind) -> bool {
    use SyntaxKind as S;
    matches!(
        kind,
        S::Eq
            | S::PlusEq
            | S::MinusEq
            | S::StarEq
            | S::SlashEq
            | S::StarStarEq
            | S::PercentEq
            | S::AmpEq
            | S::PipeEq
            | S::CaretEq
            | S::ShlEq
            | S::ShrEq
            | S::ColonEq
    )
}

/// Flat-render a run of atoms (the first atom never gets a leading space).
fn flat_atoms(atoms: &[Atom]) -> String {
    let mut s = String::new();
    for (i, a) in atoms.iter().enumerate() {
        if i > 0 && a.space {
            s.push(' ');
        }
        s.push_str(&a.text);
    }
    s
}

/// The `[prefix_end, suffix_start)` boundaries of the wrappable expression inside a statement's
/// atoms: the condition of `if`/`elif`/`while` (excluding the trailing `:`), the value of `return`,
/// or the right-hand side of a top-level assignment. `None` if there is no such expression.
fn expression_span(atoms: &[Atom]) -> Option<(usize, usize)> {
    use SyntaxKind as S;
    let first = atoms.first()?;
    match first.kind {
        S::IfKw | S::ElifKw | S::WhileKw => {
            let suffix = if atoms.last()?.kind == S::Colon {
                atoms.len() - 1
            } else {
                return None;
            };
            Some((1, suffix))
        }
        S::ReturnKw => Some((1, atoms.len())),
        _ => {
            let mut depth = 0i32;
            for (i, a) in atoms.iter().enumerate() {
                match a.kind {
                    S::LParen | S::LBrack | S::LBrace => depth += 1,
                    S::RParen | S::RBrack | S::RBrace => depth -= 1,
                    k if depth == 0 && is_assign_op(k) => return Some((i + 1, atoms.len())),
                    _ => {}
                }
            }
            None
        }
    }
}

/// Strip a redundant grouping paren that wraps the *entire* expression (e.g. the `(...)` this very
/// pass injected on a previous run) so re-flowing is idempotent. `(a and b)` → `a and b`, but
/// `(a + b) * c` is left alone (the paren is not the whole expression).
fn strip_redundant_parens(mut expr: &[Atom]) -> &[Atom] {
    while expr.len() >= 2 && expr[0].text == "(" {
        let mut depth = 0i32;
        let mut close = None;
        for (i, a) in expr.iter().enumerate() {
            if open_close(&a.text).is_some() {
                depth += 1;
            } else if matches!(a.text.as_str(), ")" | "]" | "}") {
                depth -= 1;
                if depth == 0 {
                    close = Some(i);
                    break;
                }
            }
        }
        if close == Some(expr.len() - 1) {
            expr = &expr[1..expr.len() - 1];
        } else {
            break;
        }
    }
    expr
}

/// The indices (within `expr`) of top-level **binary** operators, paired with their precedence. A
/// `+`/`-` etc. is binary only when it follows an operand (not at a unary position); and a `/`/`%`
/// inside a **node-path run** (`$Node/Path`, `%Unique/Child`) is a path separator, not an operator —
/// the parser reads `$A / B` as the same node-path as `$A/B`, so such a chain must never be split.
fn top_level_binary_ops(expr: &[Atom]) -> Vec<(usize, u8)> {
    use SyntaxKind as S;
    let mut out = Vec::new();
    let mut depth = 0i32;
    let mut node_path = false;
    let mut prev_operand_end = false;
    for (i, a) in expr.iter().enumerate() {
        match a.kind {
            S::LParen | S::LBrack | S::LBrace => depth += 1,
            S::RParen | S::RBrack | S::RBrace => depth -= 1,
            _ => {}
        }
        let in_path = node_path && matches!(a.kind, S::Ident | S::Slash | S::String);
        let binary_here = depth == 0 && !in_path && i > 0 && prev_operand_end;
        if let Some(p) = infix_prec(a.kind).filter(|_| binary_here) {
            out.push((i, p));
        }
        if node_path && !matches!(a.kind, S::Ident | S::Slash | S::String) {
            node_path = false;
        }
        if a.kind == S::Dollar || (a.kind == S::Percent && !prev_operand_end) {
            node_path = true;
        }
        prev_operand_end = is_operand_end(a.kind);
    }
    out
}

/// Wrap a too-long statement whose wrappable expression is a top-level **binary-operator chain** the
/// way gdformat does: inject parens and break at the lowest-precedence top-level operator,
/// operator-leading (each operand rendered recursively, so its own brackets reflow). `None` when
/// there is no wrappable binary-operator expression.
fn operator_chain_wrap(
    atoms: &[Atom],
    indent: usize,
    cfg: &FmtConfig,
    tw: usize,
    unit: &str,
) -> Option<String> {
    let (pre_end, suf_start) = expression_span(atoms)?;
    if pre_end >= suf_start {
        return None;
    }
    let expr = strip_redundant_parens(&atoms[pre_end..suf_start]);
    let ops = top_level_binary_ops(expr);
    let min_prec = ops.iter().map(|&(_, p)| p).min()?;
    let prefix = flat_atoms(&atoms[..pre_end]);
    let suffix = flat_atoms(&atoms[suf_start..]);
    let lead = |out: &mut String| {
        out.push_str(&unit.repeat(indent));
        out.push_str(&prefix);
        if !prefix.is_empty() {
            out.push(' ');
        }
        out.push_str("(\n");
    };
    // Compact first: the whole expression on one indented continuation line (gdformat tries this
    // before breaking — e.g. `(...).normalized() * power` that fits stays on one line).
    let expr_flat = flat_atoms(expr);
    if (indent + 1) * tw + cols(&expr_flat) <= cfg.line_width {
        let mut out = String::new();
        lead(&mut out);
        out.push_str(&unit.repeat(indent + 1));
        out.push_str(&expr_flat);
        out.push('\n');
        out.push_str(&unit.repeat(indent));
        out.push(')');
        out.push_str(&suffix);
        return Some(out);
    }

    let split: Vec<usize> = ops
        .iter()
        .filter(|&&(_, p)| p == min_prec)
        .map(|&(i, _)| i)
        .collect();

    // Build (leading-operator, operand-atoms) segments; the operator leads its following operand.
    let mut segs: Vec<(Option<&str>, &[Atom])> = Vec::new();
    let mut start = 0;
    let mut prev_op: Option<&str> = None;
    for &oi in &split {
        segs.push((prev_op, &expr[start..oi]));
        prev_op = Some(expr[oi].text.as_str());
        start = oi + 1;
    }
    segs.push((prev_op, &expr[start..]));

    let mut out = String::new();
    lead(&mut out);
    for (op, operand) in &segs {
        out.push_str(&unit.repeat(indent + 1));
        let mut col = (indent + 1) * tw;
        if let Some(op) = op {
            out.push_str(op);
            out.push(' ');
            col += cols(op) + 1;
        }
        let (od, _) = build_elems(operand, &mut 0, None);
        if let Some(seq) = od.first() {
            let (r, _) = render_seq(seq, indent + 1, col, cfg, tw, unit);
            out.push_str(&r);
        }
        out.push('\n');
    }
    out.push_str(&unit.repeat(indent));
    out.push(')');
    out.push_str(&suffix);
    Some(out)
}

/// Wrap the statement's expression in injected parens on a single indented continuation line (the
/// **compact** form gdformat uses for a too-long expression with no top-level binary operator — e.g.
/// a long method chain). `None` if there is no such expression, it has a top-level operator (handled
/// by [`operator_chain_wrap`]), or even the compact line would still overflow.
fn compact_paren_wrap(
    atoms: &[Atom],
    indent: usize,
    cfg: &FmtConfig,
    tw: usize,
    unit: &str,
) -> Option<String> {
    let (pre_end, suf_start) = expression_span(atoms)?;
    let expr = strip_redundant_parens(atoms.get(pre_end..suf_start)?);
    // Only wrap an expression that has a bracketed group (e.g. a method chain) and no top-level
    // binary operator: a bare expression / node-path is left on one line, as gdformat does.
    if expr.is_empty()
        || !top_level_binary_ops(expr).is_empty()
        || !expr.iter().any(|a| open_close(&a.text).is_some())
    {
        return None;
    }
    let expr_flat = flat_atoms(expr);
    if (indent + 1) * tw + cols(&expr_flat) > cfg.line_width {
        return None;
    }
    let prefix = flat_atoms(&atoms[..pre_end]);
    let suffix = flat_atoms(&atoms[suf_start..]);
    let mut out = String::new();
    out.push_str(&unit.repeat(indent));
    out.push_str(&prefix);
    if !prefix.is_empty() {
        out.push(' ');
    }
    out.push_str("(\n");
    out.push_str(&unit.repeat(indent + 1));
    out.push_str(&expr_flat);
    out.push('\n');
    out.push_str(&unit.repeat(indent));
    out.push(')');
    out.push_str(&suffix);
    Some(out)
}

/// Collapse a (possibly multi-line) statement to its canonical single-line body — re-synthesising the
/// inter-token spacing from scratch (so an already-wrapped statement is flattened). Returns `None`
/// when the statement must be kept **verbatim**: it contains a comment, a multi-line lambda body, a
/// backslash continuation, or a multi-line string (whose own newlines must be preserved).
/// Whether a multi-line statement that could not be flattened to one line is safe to re-lay-out via
/// the CST wrapper. It must (a) carry no comment — the wrapper does not thread comments through — and
/// (b) contain a lambda, the one legitimate reason a statement cannot collapse to a single line (so we
/// leave a multi-line string or an already-wrapped plain call verbatim). A leading `func`/`static func`
/// is a function *declaration* header (its `func` is not a lambda), so it is excluded.
fn stmt_is_rewrappable_multiline(stmt: &str) -> bool {
    let toks = gdscript_syntax::tokenize(stmt);
    let lead = stmt.trim_start();
    let lead = lead.strip_prefix("static ").unwrap_or(lead);
    if lead.starts_with("func ") || lead.starts_with("func(") {
        return false; // a function *declaration* header, not a lambda expression
    }
    // The three legitimate reasons a statement cannot collapse onto one physical line and is still
    // worth re-laying-out: it carries a lambda, a comment, or a multi-line string (the CST wrapper
    // now threads all three; the meaning / comment-multiset nets fall back to verbatim otherwise).
    let has_lambda = toks.iter().any(|t| t.kind == SyntaxKind::FuncKw);
    let has_comment = toks.iter().any(|t| {
        matches!(
            t.kind,
            SyntaxKind::LineComment
                | SyntaxKind::DocComment
                | SyntaxKind::RegionComment
                | SyntaxKind::EndRegionComment
        )
    });
    let has_multiline_string = toks
        .iter()
        .any(|t| t.kind == SyntaxKind::String && stmt[t.range].contains('\n'));
    has_lambda || has_comment || has_multiline_string
}

fn flatten_statement(stmt: &str) -> Option<String> {
    use SyntaxKind as S;
    let raw = gdscript_syntax::tokenize(stmt);
    let (toks, _diags) = gdscript_syntax::run_prepass(&raw, stmt);
    let mut out = String::with_capacity(stmt.len());
    let mut stack: Vec<S> = Vec::new();
    let mut brace_enum: Vec<bool> = Vec::new();
    let mut pending_enum = false;
    let mut prev_sig: Option<S> = None;
    let mut prev_unary = false;
    let mut node_path = false;
    let mut pending_break = false; // a line break was seen; the next token re-spaces across it
    for t in &toks {
        match t.kind {
            S::NewlinePhys => pending_break = true,
            // A synthetic newline inside brackets is a multi-line lambda body — cannot collapse.
            S::Newline if !stack.is_empty() => return None,
            S::Indent | S::Dedent | S::Bom | S::Newline => {}
            // A backslash line continuation is collapsed away: trim the space before the `\` so the
            // join is not double-spaced; the following physical newline then sets `pending_break` and
            // the next token re-spaces once (and the statement is later re-wrapped) like gdformat,
            // which converts `\`-continued statements to paren-wrapped multi-line form.
            S::LineContinuation => trim_trailing_inline_ws(&mut out),
            S::Whitespace => {
                // Keep intra-line spacing verbatim (it is already canonical / the user's, when
                // spacing-normalisation is off); drop a line's leading indentation.
                if !pending_break && prev_sig.is_some() {
                    out.push_str(&stmt[t.range]);
                }
            }
            S::LineComment | S::DocComment | S::RegionComment | S::EndRegionComment => return None,
            k => {
                let text = &stmt[t.range];
                if matches!(k, S::String | S::StringName | S::NodePath) && text.contains('\n') {
                    return None; // a multi-line string — keep the statement verbatim
                }
                // Replace a line break by the spacing the intra-line rules would synthesize.
                if pending_break {
                    let sp = match prev_sig {
                        Some(_) if node_path && matches!(k, S::Ident | S::Slash | S::String) => {
                            Spacing::Verbatim
                        }
                        Some(p) => {
                            let top_enum = stack.last() == Some(&S::LBrace)
                                && brace_enum.last() == Some(&true);
                            space_before(p, k, stack.last().copied(), prev_unary, top_enum)
                        }
                        None => Spacing::None,
                    };
                    if matches!(sp, Spacing::Single) {
                        out.push(' ');
                    }
                    pending_break = false;
                }
                out.push_str(text);
                match k {
                    S::LBrace => {
                        stack.push(k);
                        brace_enum.push(pending_enum);
                        pending_enum = false;
                    }
                    S::LParen | S::LBrack => stack.push(k),
                    S::RBrace => {
                        stack.pop();
                        brace_enum.pop();
                    }
                    S::RParen | S::RBrack => {
                        stack.pop();
                    }
                    _ => {}
                }
                if k == S::EnumKw {
                    pending_enum = true;
                }
                let unary_ctx = prev_sig.is_none_or(|p| !is_operand_end(p));
                if k == S::Dollar || (k == S::Percent && unary_ctx) {
                    node_path = true;
                } else if node_path && !matches!(k, S::Ident | S::Slash | S::String) {
                    node_path = false;
                }
                prev_unary = match k {
                    S::Minus | S::Plus => unary_ctx,
                    S::Tilde | S::Bang => true,
                    _ => false,
                };
                prev_sig = Some(k);
            }
        }
    }
    Some(out)
}

/// Render a flattened statement `body` at `indent` into its canonical layout — flat if it fits, else
/// wrapped (operator-chain → bracketed compact/exploded → compact-paren). Always indented.
fn render_statement(body: &str, indent: usize, cfg: &FmtConfig, tw: usize, unit: &str) -> String {
    // Primary path: gdformat's own algorithm, driven from the CST (see `wrap`). It owns the layout and
    // is faithful to gdformat. `wrap::render` self-validates that its output is meaning-equivalent to
    // the body (allowing exactly gdformat's legitimate rewrites — redundant grouping parens, trailing
    // commas, string quotes); it returns `None` otherwise, and we fall back to the heuristic below.
    if let Some(out) = wrap::render(body, indent, cfg) {
        return out;
    }
    let flat = || format!("{}{body}", unit.repeat(indent));
    let Some(atoms) = line_atoms(body) else {
        return flat();
    };
    let width = indent * tw + cols(body);
    // A too-long top-level binary-operator chain is wrapped in injected parens (highest priority).
    if width > cfg.line_width {
        let oc = operator_chain_wrap(&atoms, indent, cfg, tw, unit);
        if let Some(oc) = oc {
            return oc;
        }
    }
    if !atoms.iter().any(|a| open_close(&a.text).is_some()) {
        return flat();
    }
    let (docs, _) = build_elems(&atoms, &mut 0, None);
    if docs.len() != 1 {
        return flat();
    }
    let top = &docs[0];
    // Faithfulness: the atom tree must round-trip the body's *tokens* (spacing may differ — e.g. enum
    // braces / type-annotation colons — which `flat_seq` does not reproduce).
    if !same_significant_tokens(&flat_seq(top), body) {
        return flat();
    }
    // Stay flat unless the line is too long or a magic trailing comma forces it exploded.
    let forced = top
        .iter()
        .any(|d| matches!(d, ReflowDoc::Group { elems, magic, .. } if group_forced(elems, *magic)));
    if width <= cfg.line_width && !forced {
        return flat();
    }
    let (rendered, _) = render_seq(top, indent, indent * tw, cfg, tw, unit);
    let result = format!("{}{rendered}", unit.repeat(indent));
    // If bracket reflow still overflows (e.g. a long method chain), wrap the expression compact.
    let overflows = result.lines().any(|l| display_cols(l, tw) > cfg.line_width);
    if overflows {
        let cp = compact_paren_wrap(&atoms, indent, cfg, tw, unit);
        if let Some(cp) = cp {
            return cp;
        }
    }
    result
}

/// Re-flow the layout of every statement: a statement that now fits is collapsed onto one line, one
/// that does not is re-wrapped to its canonical form — gdformat-style layout ownership. Statements
/// that cannot be safely collapsed (comments, multi-line lambdas, multi-line strings) are preserved.
/// Token-preserving (modulo the trailing commas / parens the meaning-equivalence net allows).
#[allow(
    clippy::too_many_lines,
    reason = "one cohesive pass: per-line bracket/straddle bookkeeping then logical-statement grouping"
)]
fn reflow(formatted: &str, config: &FmtConfig) -> String {
    use SyntaxKind as S;
    if config.line_width == 0 {
        return formatted.to_owned();
    }
    let tw = if config.use_tabs {
        4
    } else {
        config.indent_size.max(1)
    };
    let unit = config.indent_unit();
    let lines: Vec<&str> = formatted.split('\n').collect();

    let line_starts: Vec<usize> = std::iter::once(0)
        .chain(
            formatted
                .bytes()
                .enumerate()
                .filter_map(|(i, b)| (b == b'\n').then_some(i + 1)),
        )
        .collect();
    let line_of = |off: usize| line_starts.partition_point(|&s| s <= off).saturating_sub(1);

    // Per line: bracket depth at its start, whether a multi-line token (e.g. a `"""..."""` string)
    // covers it, and whether it ends in a `\` continuation — used to group physical lines into the
    // logical statement they belong to.
    let raw = gdscript_syntax::tokenize(formatted);
    let mut start_depth = vec![i32::MIN; lines.len()];
    let mut straddled = vec![false; lines.len()];
    let mut ends_cont = vec![false; lines.len()];
    let mut depth: i32 = 0;
    for t in &raw {
        let s = usize::from(t.range.start());
        let e = usize::from(t.range.end()).saturating_sub(1).max(s);
        let (sl, el) = (line_of(s), line_of(e));
        if sl < start_depth.len() && start_depth[sl] == i32::MIN {
            start_depth[sl] = depth;
        }
        if el > sl {
            let end = el.min(lines.len().saturating_sub(1));
            straddled[sl..=end].fill(true);
        }
        match t.kind {
            S::LParen | S::LBrack | S::LBrace => depth += 1,
            S::RParen | S::RBrack | S::RBrace => depth -= 1,
            S::LineContinuation if sl < ends_cont.len() => ends_cont[sl] = true,
            // The next line starts at the current depth — covers blank lines (which have no token of
            // their own to set `start_depth`, so they would otherwise stay unset and be absorbed).
            S::NewlinePhys if sl + 1 < start_depth.len() && start_depth[sl + 1] == i32::MIN => {
                start_depth[sl + 1] = depth;
            }
            _ => {}
        }
    }
    let sd = |i: usize| {
        let d = start_depth.get(i).copied().unwrap_or(0);
        if d == i32::MIN { 0 } else { d }
    };
    let strad = |i: usize| straddled.get(i).copied().unwrap_or(false);
    let cont = |i: usize| ends_cont.get(i).copied().unwrap_or(false);

    let mut out = String::with_capacity(formatted.len());
    let mut li = 0;
    while li < lines.len() {
        let line = lines[li];
        // A statement head: a non-blank line at bracket depth 0 that is not the continuation of the
        // previous line (a `\` or a multi-line token spanning the boundary).
        let head = !line.trim().is_empty()
            && sd(li) == 0
            && (li == 0 || !cont(li - 1))
            && !(li > 0 && strad(li) && strad(li - 1));
        if !head {
            out.push_str(line);
            if li + 1 < lines.len() {
                out.push('\n');
            }
            li += 1;
            continue;
        }
        // Extend to the end of the logical statement (brackets open, a multi-line token spanning the
        // boundary, or a backslash continuation).
        let mut j = li;
        while j + 1 < lines.len() && (sd(j + 1) != 0 || (strad(j) && strad(j + 1)) || cont(j)) {
            j += 1;
        }
        let stmt = lines[li..=j].join("\n");
        let indent = if config.use_tabs {
            line.bytes().take_while(|&b| b == b'\t').count()
        } else {
            line.bytes()
                .take_while(|&b| b == b' ')
                .count()
                .checked_div(config.indent_size)
                .unwrap_or(0)
        };
        let rendered = match flatten_statement(&stmt) {
            Some(body) => Some(render_statement(&body, indent, config, tw, &unit)),
            // A statement that cannot be collapsed onto one physical line *because it carries a
            // multi-line lambda body* (a compound- or multi-statement-bodied lambda argument) is still
            // re-laid-out by the CST wrapper directly, from its dedented multi-line form — gdformat
            // reformats *every* statement, not just single-line ones. We only attempt this for a
            // comment-free lambda-bearing statement (the wrapper does not carry comments through, and a
            // plain multi-line string / already-wrapped statement is better left verbatim); the wrapper
            // still self-validates meaning-equivalence and returns `None` (→ verbatim) otherwise.
            None if stmt_is_rewrappable_multiline(&stmt) => {
                wrap::render(&wrap::dedent(&stmt), indent, config)
            }
            None => None,
        };
        out.push_str(rendered.as_deref().unwrap_or(&stmt));
        if j + 1 < lines.len() {
            out.push('\n');
        }
        li = j + 1;
    }
    out
}

/// Trim trailing spaces/tabs from the end of `out` (the current line).
fn trim_trailing_inline_ws(out: &mut String) {
    while out.ends_with(' ') || out.ends_with('\t') {
        out.pop();
    }
}

/// Whether two sources lex to the same sequence of significant (non-trivia) tokens — a
/// spacing-insensitive equality used by the reflow faithfulness check and the token-preserving tests.
fn same_significant_tokens(a: &str, b: &str) -> bool {
    fn sig(s: &str) -> Vec<(SyntaxKind, &str)> {
        gdscript_syntax::tokenize(s)
            .into_iter()
            // A `;` is a statement separator equivalent to a newline — ignore it, so splitting
            // `a; b` onto two lines is recognised as token-preserving.
            .filter(|t| !t.kind.is_trivia() && t.kind != SyntaxKind::Semicolon)
            .map(|t| (t.kind, &s[t.range]))
            .collect()
    }
    sig(a) == sig(b)
}

/// Re-emit a string-literal token's text in gdformat's canonical quote style: prefer `"`, fall back
/// to `'` only when the body has more `"` than `'` (fewer escapes), keeping the prefix (`r`/`&`/`^`)
/// and the decoded value. Idempotent. Triple-quoted strings are left verbatim (rare; not normalized).
pub(crate) fn canonical_string(text: &str) -> String {
    let Some(qpos) = text.find(['"', '\'']) else {
        return text.to_owned(); // defensive: not actually a string literal
    };
    let prefix = &text[..qpos];
    let rest = &text[qpos..];
    let rb = rest.as_bytes();
    let quote = rb[0];
    // Triple-quoted. gdformat converts a *single-line* triple-**single**-quoted string (`'''…'''`)
    // to a regular string (strip the outer `''` and apply the regular quote rule to `'…'`); a
    // triple-**double** (`"""…"""`) and any *multi-line* triple-quoted string are left verbatim.
    if rb.len() >= 6 && rb[1] == quote && rb[2] == quote {
        if quote == b'\'' && rest.ends_with("'''") {
            let body = &rest[3..rest.len() - 3];
            if !body.contains('\n') {
                return canonical_string(&format!("{prefix}'{body}'"));
            }
        }
        return text.to_owned();
    }
    if rest.len() < 2 || rb[rest.len() - 1] != quote {
        return text.to_owned(); // unterminated / malformed: don't touch
    }
    let body = &rest[1..rest.len() - 1];
    // Raw strings (`r"..."`) cannot escape — only switch quotes if the body lacks the target.
    if prefix.contains('r') {
        let target = if !body.contains('"') {
            '"'
        } else if !body.contains('\'') {
            '\''
        } else {
            quote as char
        };
        return format!("{prefix}{target}{body}{target}");
    }
    // Parse the body into units (escaped or literal) and count the value's quote characters.
    let mut units: Vec<(bool, char)> = Vec::new();
    let (mut dq, mut sq) = (0usize, 0usize);
    let mut chars = body.chars();
    while let Some(c) = chars.next() {
        if c == '\\' {
            if let Some(n) = chars.next() {
                units.push((true, n));
                match n {
                    '"' => dq += 1,
                    '\'' => sq += 1,
                    _ => {}
                }
            } else {
                units.push((false, '\\'));
            }
        } else {
            units.push((false, c));
            match c {
                '"' => dq += 1,
                '\'' => sq += 1,
                _ => {}
            }
        }
    }
    let target = if dq > sq { '\'' } else { '"' };
    let mut out = String::with_capacity(text.len());
    out.push_str(prefix);
    out.push(target);
    for (esc, c) in units {
        if c == '"' || c == '\'' {
            if c == target {
                out.push('\\');
            }
            out.push(c);
        } else {
            if esc {
                out.push('\\');
            }
            out.push(c);
        }
    }
    out.push(target);
    out
}

/// A normalised parse-tree event used by [`meaning_preserved`].
#[derive(Clone, PartialEq, Eq)]
enum TreeEvent {
    Open(SyntaxKind),
    Close,
    Token(SyntaxKind, String),
}

/// Walk a parse-tree node, appending normalised events: trivia is dropped, a `ParenExpr` is
/// **unwrapped** (its node + `(`/`)` tokens removed, its inner expression spliced in — so a
/// *redundant* grouping paren is invisible while a precedence-changing one still differs, because the
/// surrounding `BinExpr` nesting changes), and string literals are recorded by canonical quote form.
fn emit_tree_events(node: &gdscript_syntax::GdNode, out: &mut Vec<TreeEvent>) {
    use cstree::util::NodeOrToken;
    if node.kind() == SyntaxKind::ParenExpr {
        for child in node.children() {
            emit_tree_events(child, out);
        }
        return;
    }
    out.push(TreeEvent::Open(node.kind()));
    for child in node.children_with_tokens() {
        match child {
            // An *empty* parameter list carries no meaning — `signal s()` ≡ `signal s` (a func always
            // has `()`, so dropping the empty list uniformly leaves func headers unaffected). Skipping
            // it here lets the formatter remove a signal's empty `()` without the net objecting.
            NodeOrToken::Node(n)
                if n.kind() == SyntaxKind::ParamList && n.children().next().is_none() => {}
            NodeOrToken::Node(n) => emit_tree_events(n, out),
            NodeOrToken::Token(t) => {
                let kind = t.kind();
                // Skip trivia, the synthetic block-structure markers (`Newline`/`Indent`/`Dedent` —
                // the block nesting they encode is already captured by the surrounding node Open/Close
                // events, and they differ between an inline and a multi-line form of the *same*
                // construct, e.g. `func(): x` vs a wrapped lambda body), and a `;` statement separator.
                if kind.is_trivia() || kind.is_synthetic_layout() || kind == SyntaxKind::Semicolon {
                    continue;
                }
                let text = if matches!(
                    kind,
                    SyntaxKind::String | SyntaxKind::StringName | SyntaxKind::NodePath
                ) {
                    canonical_string(t.text())
                } else {
                    t.text().to_owned()
                };
                out.push(TreeEvent::Token(kind, text));
            }
        }
    }
    out.push(TreeEvent::Close);
}

/// Whether `a` and `b` are **meaning-equivalent** — the relaxed safety net used once the formatter
/// performs token-*mutating* rewrites. It compares the **parse-tree structure** (so token order,
/// nesting and operator precedence must all match), normalising away exactly the differences gdformat
/// is allowed to introduce: **redundant grouping parens** are unwrapped, a **trailing comma** before a
/// closing bracket is dropped, and **string literals are compared by canonical quote form**. A
/// dropped/added/reordered token, a changed string *value*, or a precedence change is still caught.
pub(crate) fn meaning_preserved(a: &str, b: &str) -> bool {
    fn events(s: &str) -> Vec<TreeEvent> {
        let mut raw = Vec::new();
        emit_tree_events(&gdscript_syntax::parse(s).syntax_node(), &mut raw);
        let mut out = Vec::with_capacity(raw.len());
        for i in 0..raw.len() {
            // Drop a trailing comma (a `,` immediately before a closing-bracket token).
            let trailing_comma = matches!(&raw[i], TreeEvent::Token(SyntaxKind::Comma, _))
                && matches!(
                    raw.get(i + 1),
                    Some(TreeEvent::Token(
                        SyntaxKind::RParen | SyntaxKind::RBrack | SyntaxKind::RBrace,
                        _
                    ))
                );
            if !trailing_comma {
                out.push(raw[i].clone());
            }
        }
        out
    }
    events(a) == events(b)
}

#[cfg(test)]
mod tests {
    use super::*;

    fn fmt(src: &str) -> String {
        format(src, &FmtConfig::default())
    }

    #[test]
    fn normalizes_indentation_to_tabs() {
        // Four-space indentation becomes one tab per level.
        let src = "func f():\n    if true:\n        return 1\n";
        assert_eq!(fmt(src), "func f():\n\tif true:\n\t\treturn 1\n");
    }

    #[test]
    fn trims_trailing_whitespace_and_adds_final_newline() {
        let src = "var x = 1   \nvar y = 2"; // trailing spaces + no final newline
        assert_eq!(fmt(src), "var x = 1\nvar y = 2\n");
    }

    #[test]
    fn is_idempotent() {
        let src = "func f():\n  var a = 1\n  if a:\n      return a\n";
        let once = fmt(src);
        assert_eq!(fmt(&once), once, "formatting must be idempotent");
    }

    #[test]
    fn already_formatted_is_unchanged() {
        let src = "func f():\n\tvar a = 1\n\treturn a\n";
        assert_eq!(fmt(src), src);
    }

    #[test]
    fn indexed_array_literal_explodes_the_array_keeps_index_on_close_line() {
        // A subscript on a too-wide array literal (`[…][i]`): explode the ARRAY one element per line
        // (with a trailing comma) and keep `[index]` compact on the close-bracket line — gdformat
        // parity (the os_test.gd byte-exact miss, burndown Stage 6.29), NOT the reverse (a compact
        // over-width array with the index dropped to its own line).
        let src = "func f():\n\tvar x = [\"Landscape\", \"Portrait\", \"Landscape (reverse)\", \"Portrait (reverse)\", \"Defined by sensor\"][get_orientation()]\n";
        let out = fmt(src);
        // The array opens/explodes and the `[index]` stays compact on the close-bracket line.
        assert!(
            out.contains("var x = [\n"),
            "the array must open/explode:\n{out}"
        );
        assert!(
            out.contains("][get_orientation()]"),
            "the index must stay on the close-bracket line:\n{out}"
        );
        // The OLD bug kept the array a compact over-width line and dropped the index onto its own
        // line (`…"][` then a newline) — never again.
        assert!(
            !out.contains("][\n"),
            "the index must not be exploded onto its own line:\n{out}"
        );
        assert!(super::same_significant_tokens(src, &out));
        assert_eq!(fmt(&out), out, "idempotent");
    }

    #[test]
    fn operator_chain_paren_operand_with_leading_comment_compacts_and_hoists() {
        // gdformat parity (town_scene.gd, burndown Stage 6.28): a parenthesized `and`-chain operand of
        // an `or` chain, with a comment leading the operand *inside* its parens, renders the operand
        // compact and hoists the comment to its own next line — not the source's hand-wrapped paren
        // block. (Our paren-retention already matched gdformat; this closes the comment placement.)
        let src = "func f():\n\tif (\n\t\t\t# on\n\t\t\taaa and bbb\n\t) or (\n\t\t\t# off\n\t\t\tccc and ddd\n\t):\n\t\tpass\n";
        let out = fmt(src);
        assert!(out.contains("(aaa and bbb)"), "operand 0 compacts:\n{out}");
        assert!(
            out.contains("or (ccc and ddd)"),
            "operator leads the compacted operand 1:\n{out}"
        );
        assert!(
            out.contains("# on") && out.contains("# off"),
            "comments preserved:\n{out}"
        );
        // (No raw `same_significant_tokens` here: a multi-line operator chain is wrapped in an
        // injected `(…)` — gdformat does the same, and the formatter's own net accounts for it, but a
        // raw token compare would not. The byte-exact gdformat corpus parity is the real guard.)
        assert_eq!(fmt(&out), out, "idempotent");
    }

    #[test]
    fn preserves_significant_tokens_including_strings() {
        let src = "func f():\n\tvar s = \"a + b\"\n\treturn s\n";
        let out = fmt(src);
        assert!(super::same_significant_tokens(src, &out));
        assert!(out.contains("\"a + b\""));
    }

    #[test]
    fn multiline_string_content_is_untouched() {
        // The interior of a multi-line string must survive verbatim (it is a single token).
        let src = "func f():\n\tvar s = \"\"\"line1\n        keep   \nline2\"\"\"\n\treturn s\n";
        let out = fmt(src);
        assert!(
            out.contains("line1\n        keep   \nline2"),
            "got: {out:?}"
        );
    }

    #[test]
    fn safe_mode_returns_input_on_syntax_error() {
        let src = "func f(:\n\treturn"; // malformed
        assert_eq!(fmt(src), src);
    }

    #[test]
    fn empty_input_stays_empty() {
        assert_eq!(fmt(""), "");
        assert_eq!(fmt("\n\n\n"), "");
    }

    #[test]
    fn spaces_option_indents_with_spaces() {
        let cfg = FmtConfig {
            use_tabs: false,
            indent_size: 2,
            ..FmtConfig::default()
        };
        let src = "func f():\n\treturn 1\n";
        assert_eq!(format(src, &cfg), "func f():\n  return 1\n");
    }

    /// `parse(src).errors()` must be empty — the formatter must never emit code that fails to parse.
    fn parses_clean(src: &str) -> bool {
        gdscript_syntax::parse(src).errors().is_empty()
    }

    #[test]
    fn comment_between_statements_does_not_corrupt_the_next_line() {
        // A comment-only line is copied verbatim by the prepass (no synthetic Newline); the line
        // AFTER it must still be re-indented to the block depth, not left at its original spacing.
        let src = "func g():\n  var a = 1\n  # c\n  var x = 1\n  var y = 2\n";
        let out = fmt(src);
        assert_eq!(
            out,
            "func g():\n\tvar a = 1\n\t# c\n\tvar x = 1\n\tvar y = 2\n"
        );
        assert!(
            parses_clean(&out),
            "formatter must not emit mixed indent: {out:?}"
        );
        assert_eq!(fmt(&out), out, "must be idempotent");
    }

    #[test]
    fn leading_body_comment_is_indented_to_the_block() {
        // A comment that is the FIRST line of a block: the prepass emits `Indent` only at the first
        // *code* line, so the comment's raw depth is wrong — but it is re-indented to its intended
        // block depth by comparing its authored indentation against the surrounding code lines
        // (gdformat's rule). Works for the space-indented input here too (length comparison is
        // indent-width agnostic).
        let src = "func g():\n  # c\n  var x = 1\n  var y = 2\n";
        let out = fmt(src);
        assert_eq!(out, "func g():\n\t# c\n\tvar x = 1\n\tvar y = 2\n");
        assert!(parses_clean(&out), "{out:?}");
        assert_eq!(fmt(&out), out, "must be idempotent");
    }

    #[test]
    fn doc_comment_between_statements_is_reindented_and_does_not_corrupt() {
        // A doc comment AFTER a code line (depth known) is re-indented like any line, and the line
        // following it must not be mis-indented.
        let src = "func g():\n  var a = 1\n  ## doc\n  var x = 1\n";
        let out = fmt(src);
        assert_eq!(out, "func g():\n\tvar a = 1\n\t## doc\n\tvar x = 1\n");
        assert!(parses_clean(&out), "{out:?}");
    }

    #[test]
    fn bracketed_continuation_interior_is_preserved() {
        // A physical newline INSIDE brackets is a real continuation — its interior spacing must be
        // kept verbatim (not treated like a comment-line terminator that re-indents the next line).
        let src = "func f():\n\tvar a = [\n\t\t1,\n\t\t2,\n\t]\n\treturn a\n";
        let out = fmt(src);
        assert!(parses_clean(&out), "{out:?}");
        assert!(super::same_significant_tokens(src, &out));
        assert_eq!(fmt(&out), out, "must be idempotent");
    }

    // ---- Phase-4 increment A: intra-line spacing ----

    /// Format a single statement inside a function body, returning just the (de-indented) body line.
    /// Wrapping keeps `safe_mode` happy (a bare statement is not a valid top-level form).
    fn fmt_stmt(stmt: &str) -> String {
        let src = format!("func _f():\n\t{stmt}\n");
        let out = fmt(&src);
        out.strip_prefix("func _f():\n\t")
            .and_then(|s| s.strip_suffix('\n'))
            .unwrap_or(&out)
            .to_owned()
    }

    #[test]
    fn spacing_operators_and_assignment() {
        assert_eq!(fmt_stmt("var x=a+b"), "var x = a + b");
        assert_eq!(fmt_stmt("var x = a-b"), "var x = a - b"); // binary minus
        assert_eq!(fmt_stmt("var t = a   *   b"), "var t = a * b"); // collapse runs
        assert_eq!(
            fmt_stmt("var z = a==b and c!=d"),
            "var z = a == b and c != d"
        );
        assert_eq!(fmt_stmt("x+=1"), "x += 1");
    }

    #[test]
    fn spacing_brackets_and_commas() {
        assert_eq!(fmt_stmt("foo( x ,y )"), "foo(x, y)");
        assert_eq!(fmt_stmt("var a = [1,2]"), "var a = [1, 2]");
        assert_eq!(
            fmt_stmt("var d = {\"x\":1,\"y\":2}"),
            "var d = {\"x\": 1, \"y\": 2}"
        );
        assert_eq!(
            fmt_stmt("var n = obj . field . method ( )"),
            "var n = obj.field.method()"
        );
    }

    #[test]
    fn spacing_type_annotation_and_default_args() {
        assert_eq!(fmt_stmt("var x:int=1"), "var x: int = 1");
        // Typed default `=` is spaced (we do not replicate Black's untyped `x=1`); arrow + header colon.
        assert_eq!(
            fmt("func f(a,b:int=1)->int:\n\treturn 0\n"),
            "func f(a, b: int = 1) -> int:\n\treturn 0\n"
        );
        assert_eq!(fmt_stmt("var a:Array[int]=[]"), "var a: Array[int] = []");
    }

    #[test]
    fn spacing_unary_minus() {
        assert_eq!(fmt_stmt("var x = -1"), "var x = -1"); // unary after `=`
        assert_eq!(fmt_stmt("foo( -1 , -2 )"), "foo(-1, -2)"); // unary in call
        assert_eq!(fmt_stmt("var a = [-1,-2]"), "var a = [-1, -2]"); // unary in array
        assert_eq!(fmt_stmt("var n = -2**2"), "var n = -2 ** 2"); // unary then power
        assert_eq!(fmt_stmt("var d = a - -b"), "var d = a - -b"); // binary then unary
    }

    #[test]
    fn spacing_percent_is_modulo_or_format_when_after_an_operand() {
        assert_eq!(fmt_stmt("var r = a%b"), "var r = a % b"); // modulo
        assert_eq!(fmt_stmt("var s = \"%d\"%n"), "var s = \"%d\" % n"); // format operator
    }

    #[test]
    fn spacing_node_paths_stay_tight() {
        // The correctness-critical cases: a node path must NOT gain spaces around `/` (that would
        // turn `$Player/Bone` into a division with an identical token sequence).
        assert_eq!(
            fmt_stmt("var n = get_node($Player/Bone)"),
            "var n = get_node($Player/Bone)"
        );
        assert_eq!(fmt_stmt("var u = %Unique/Child"), "var u = %Unique/Child");
        assert_eq!(
            fmt_stmt("var p = $\"Player\".position"),
            "var p = $\"Player\".position"
        );
        // StringName / NodePath literals are single tokens — untouched atoms.
        assert_eq!(fmt_stmt("var v = &\"Name\""), "var v = &\"Name\"");
        assert_eq!(fmt_stmt("var q = ^\"a/b\""), "var q = ^\"a/b\"");
    }

    #[test]
    fn spacing_keywords_paren_callee_and_grouping() {
        assert_eq!(
            fmt_stmt("var p = preload ( \"res://x.gd\" )"),
            "var p = preload(\"res://x.gd\")"
        );
        assert_eq!(fmt_stmt("var x = a if c else b"), "var x = a if c else b"); // ternary
        assert_eq!(fmt_stmt("var y = not  flag"), "var y = not flag");
        assert_eq!(fmt_stmt("var z = n is int"), "var z = n is int");
        // a redundant grouping paren after a value-keyword is stripped; a precedence-significant one
        // is kept (with its space after the keyword), and a call paren hugs.
        assert_eq!(fmt_stmt("return ( x )"), "return x");
        assert_eq!(fmt_stmt("return ( a + b ) * c"), "return (a + b) * c");
    }

    #[test]
    fn spacing_lambda_func_paren_is_tight() {
        // Corpus regression: a lambda `func(...)` must hug its `func` — `func (` does not parse.
        // (A named function declaration has `func name(`, which is unaffected.) A single-statement
        // lambda body that fits is collapsed onto one line, matching gdformat.
        assert_eq!(
            fmt_stmt("var cb = func( ) -> void:\n\t\tpass"),
            "var cb = func() -> void: pass"
        );
        assert_eq!(
            fmt_stmt("var g = func(_text:String)->void:\n\t\tpass"),
            "var g = func(_text: String) -> void: pass"
        );
        assert_eq!(
            fmt("func named(a,b):\n\tpass\n"),
            "func named(a, b):\n\tpass\n"
        );
    }

    #[test]
    fn multiline_lambda_collapses_when_single_statement_else_kept() {
        // gdformat inlines a multi-line lambda whose body is a single simple statement once the
        // enclosing statement fits — but keeps a multi-statement (or non-fitting) lambda body.
        assert_eq!(
            fmt("func _r():\n\tx.connect(func() -> void:\n\t\tdo_thing()\n\t)\n"),
            "func _r():\n\tx.connect(func() -> void: do_thing())\n"
        );
        // a two-statement lambda body stays multi-line; the lambda argument explodes onto its own
        // line (gdformat's `_format_lambda_to_multiple_lines`, reached via the multi-line re-wrap).
        let multi = "func _r():\n\tx.connect(func() -> void:\n\t\ta()\n\t\tb()\n\t)\n";
        let out = fmt(multi);
        assert_eq!(
            out,
            "func _r():\n\tx.connect(\n\t\tfunc() -> void:\n\t\t\ta()\n\t\t\tb()\n\t)\n"
        );
        assert!(parses_clean(&out), "{out:?}");
        assert_eq!(fmt(&out), out, "idempotent");
    }

    #[test]
    fn inline_lambda_value_in_exploded_dict_is_rendered() {
        // A dict whose values include inline lambdas must wrap (the dict overflows) with each lambda
        // kept inline — the CST wrapper renders `LambdaExpr` rather than bailing to the heuristic.
        let src = "func r():\n\tvar n = V.line_edit({\"text\": t, \"on_changed\": func(x): cb.call(x), \"on_submit\": func(): add.call(), \"placeholder\": \"type a fairly long placeholder here\"})\n";
        let out = fmt(src);
        assert!(
            out.contains("\"on_changed\": func(x): cb.call(x),"),
            "{out:?}"
        );
        assert!(
            out.contains("\"on_submit\": func(): add.call(),"),
            "{out:?}"
        );
        assert!(parses_clean(&out), "{out:?}");
        assert_eq!(fmt(&out), out, "idempotent");
    }

    #[test]
    fn colon_dict_value_drops_below_key_when_entry_overflows() {
        // A dict entry whose value pushes the line past the width splits `"key":` / value-below
        // (gdformat's `_format_kv_pair_to_multiple_lines`); a fitting sibling stays inline.
        let src = "func r():\n\tvar d = V.label({\"text\": \"a moderately long string value that does not fit on the line at all here for sure\" % n, \"style\": {\"c\": 1}})\n";
        let out = fmt(src);
        assert!(
            out.contains("\t\t\t\"text\":\n"),
            "value should drop below key: {out:?}"
        );
        assert!(
            out.contains("\"style\": {\"c\": 1}"),
            "fitting entry stays inline: {out:?}"
        );
        assert!(parses_clean(&out), "{out:?}");
        assert_eq!(fmt(&out), out, "idempotent");
    }

    #[test]
    fn dot_chain_with_lambda_wraps_bottom_up_not_leading_dot() {
        // gdformat always bottom-ups a dot-chain that contains a lambda (a Godot-parser-bug
        // workaround), even when a magic comma deep inside would otherwise force leading-dot.
        let src = "func r():\n\treturn V.fc(Demo.render, {\"title\": \"a fairly long title here for the box widget yes\"}, [V.button({\"on_pressed\": func(): go()})])\n";
        let out = fmt(src);
        assert!(out.contains("\treturn V.fc("), "should bottom-up: {out:?}");
        assert!(!out.contains(". fc"), "should not leading-dot: {out:?}");
        assert!(parses_clean(&out), "{out:?}");
        assert_eq!(fmt(&out), out, "idempotent");
    }

    #[test]
    fn over_indented_lambda_in_brackets_does_not_corrupt() {
        // The exact corpus shapes (godot-demo-projects rhythm_game) that used to format to
        // non-parsing code: the lambda HEADER sits on its own bracket-continuation line at an
        // author-chosen over-indent, and the BODY (synthetic-Newline) must stay aligned with it,
        // not snap back to block depth. Both are now kept verbatim and parse.
        let note_manager = "func _ready() -> void:\n\t_play_stats.changed.connect(\n\t\t\tfunc() -> void:\n\t\t\t\tplay_stats_updated.emit(_play_stats)\n\t\t\t\t)\n";
        let main_gd = "func _r() -> void:\n\tlatency_line_edit.text_submitted.connect(\n\t\tfunc(_text: String) -> void:\n\t\t\tlatency_line_edit.release_focus())\n";
        for src in [note_manager, main_gd] {
            // safe_mode OFF so a regression would surface as a real assert, not a silent fallback.
            let cfg = FmtConfig {
                safe_mode: false,
                ..FmtConfig::default()
            };
            let out = format(src, &cfg);
            assert!(parses_clean(&out), "lambda-in-brackets must parse: {out:?}");
            assert!(
                super::same_significant_tokens(src, &out),
                "tokens changed: {out:?}"
            );
            assert_eq!(format(&out, &cfg), out, "idempotent: {out:?}");
        }
    }

    #[test]
    fn spacing_annotation_is_tight() {
        assert_eq!(
            fmt("@export_range(0,100)\nvar speed = 1\n"),
            "@export_range(0, 100)\nvar speed = 1\n"
        );
        assert_eq!(fmt("@export var hp=100\n"), "@export var hp = 100\n");
    }

    #[test]
    fn spacing_colon_in_brackets_left_verbatim() {
        // GDScript has no Python-style slice syntax, so a colon inside `[ ]` never appears in valid
        // code — but defensively we leave its spacing verbatim (not locally distinguishable from
        // other colon roles). Exercised with safe_mode OFF, since the construct does not parse; the
        // rest of the line is still normalized while the `[a:b]` colon spacing is preserved.
        let cfg = FmtConfig {
            safe_mode: false,
            ..FmtConfig::default()
        };
        assert_eq!(
            format("func _f():\n\tvar d = data[a:b]\n", &cfg),
            "func _f():\n\tvar d = data[a:b]\n"
        );
        assert_eq!(
            format("func _f():\n\tvar e=data[a : b]\n", &cfg),
            "func _f():\n\tvar e = data[a : b]\n"
        );
    }

    #[test]
    fn spacing_is_idempotent_across_cases() {
        let cases = [
            "var x = a+b*c-d",
            "func f(a,b:int=1)->int:\n\treturn a",
            "var n = get_node($Player/Bone).position",
            "var d = {\"k\": foo(-1, 2), \"m\": items.slice(i, j)}",
            "var z = a if b<c else -d",
        ];
        for c in cases {
            let src = format!("func _w():\n\t{c}\n");
            let once = fmt(&src);
            assert_eq!(fmt(&once), once, "not idempotent for {c:?}: {once:?}");
            assert!(parses_clean(&once), "did not parse: {once:?}");
            assert!(
                super::same_significant_tokens(&src, &once),
                "tokens changed for {c:?}"
            );
        }
    }

    // ---- Phase-4 increment B: blank-line policy ----

    #[test]
    fn blank_lines_collapsed_top_level_to_two() {
        // 4 blank lines between two top-level functions collapse to 2 (the cap uses the *next*
        // line's depth — 0 here — even though the Dedent lands after the blanks).
        let src = "func a():\n\tpass\n\n\n\n\nfunc b():\n\tpass\n";
        assert_eq!(fmt(src), "func a():\n\tpass\n\n\nfunc b():\n\tpass\n");
    }

    #[test]
    fn blank_lines_collapsed_inside_block_to_one() {
        let src = "func a():\n\tvar x = 1\n\n\n\n\tvar y = 2\n";
        assert_eq!(fmt(src), "func a():\n\tvar x = 1\n\n\tvar y = 2\n");
    }

    #[test]
    fn leading_blank_lines_stripped() {
        assert_eq!(fmt("\n\n\nfunc a():\n\tpass\n"), "func a():\n\tpass\n");
    }

    #[test]
    fn single_blank_between_top_defs_is_grown_to_two() {
        // gdformat enforces exactly 2 blank lines around top-level defs; a single blank is grown.
        let src = "func a():\n\tpass\n\nfunc b():\n\tpass\n";
        assert_eq!(fmt(src), "func a():\n\tpass\n\n\nfunc b():\n\tpass\n");
    }

    #[test]
    fn blank_lines_inside_a_multiline_string_are_untouched() {
        // The blank line lives inside a `"""..."""` token, NOT between logical lines — the
        // token-level pass must never see it as a collapsible blank.
        let src = "func a():\n\tvar s = \"\"\"x\n\n\n\ny\"\"\"\n\treturn s\n";
        let out = fmt(src);
        assert!(
            out.contains("x\n\n\n\ny"),
            "string interior collapsed: {out:?}"
        );
        assert!(super::same_significant_tokens(src, &out));
    }

    #[test]
    fn blank_lines_off_preserved() {
        let cfg = FmtConfig {
            collapse_blank_lines: false,
            ..FmtConfig::default()
        };
        let src = "func a():\n\tpass\n\n\n\n\nfunc b():\n\tpass\n";
        assert_eq!(format(src, &cfg), src);
    }

    #[test]
    fn spacing_off_is_indentation_only() {
        // With `normalize_spacing` off, the formatter touches indentation only (the old behavior):
        // intra-line spacing is left exactly as written.
        let cfg = FmtConfig {
            normalize_spacing: false,
            ..FmtConfig::default()
        };
        let src = "func f():\n    var x=a+b\n";
        assert_eq!(format(src, &cfg), "func f():\n\tvar x=a+b\n");
    }

    // ---- Phase-4 increment C: block-boundary comment indentation ----

    #[test]
    fn trailing_body_comment_stays_at_block_depth() {
        // A comment after the last body statement, before a dedented def, stays at the body depth
        // (gdformat keeps it attached to the block it was written in, not the following def).
        let src = "func foo():\n\tpass\n\t# trailing\n\n\nfunc bar():\n\tpass\n";
        let out = fmt(src);
        assert_eq!(
            out,
            "func foo():\n\tpass\n\t# trailing\n\n\nfunc bar():\n\tpass\n"
        );
        assert_eq!(fmt(&out), out, "idempotent");
    }

    #[test]
    fn comment_after_nested_block_stays_at_outer_body_depth() {
        // The "after-if" case: a comment authored at body depth, sitting between a deeper block and
        // a following body statement, keeps the body depth (not the deeper raw prepass depth).
        let src = "func f():\n\tif x:\n\t\tpass\n\t# back at body level\n\treturn\n";
        let out = fmt(src);
        assert_eq!(
            out,
            "func f():\n\tif x:\n\t\tpass\n\t# back at body level\n\treturn\n"
        );
        assert_eq!(fmt(&out), out, "idempotent");
    }

    #[test]
    fn over_indented_comment_snaps_to_block_and_col0_stays() {
        // An over-indented comment snaps to the surrounding block; a column-0 comment stays at 0.
        assert_eq!(
            fmt("func f():\n\t\t\t# over\n\tpass\n"),
            "func f():\n\t# over\n\tpass\n"
        );
        assert_eq!(
            fmt("func f():\n# at col 0\n\tpass\n"),
            "func f():\n# at col 0\n\tpass\n"
        );
    }

    // ---- Phase-4 increment C: blank-line insertion around definitions ----

    #[test]
    fn two_blanks_inserted_around_top_level_defs() {
        let src = "extends Node\nfunc a():\n\tpass\nfunc b():\n\tpass\n";
        assert_eq!(
            fmt(src),
            "extends Node\n\n\nfunc a():\n\tpass\n\n\nfunc b():\n\tpass\n"
        );
    }

    #[test]
    fn one_blank_inserted_between_methods_in_a_class() {
        // Inside a class: 1 blank between methods; none before the first member (after the header).
        let src = "class C:\n\tfunc a():\n\t\tpass\n\tfunc b():\n\t\tpass\n";
        assert_eq!(
            fmt(src),
            "class C:\n\tfunc a():\n\t\tpass\n\n\tfunc b():\n\t\tpass\n"
        );
    }

    #[test]
    fn blanks_go_before_an_attached_comment_or_annotation_prefix() {
        // The 2 blanks land before the doc-comment/annotation that belongs to the def, not between.
        let src = "func a():\n\tpass\n## docs for b\n@warning_ignore(\"x\")\nfunc b():\n\tpass\n";
        assert_eq!(
            fmt(src),
            "func a():\n\tpass\n\n\n## docs for b\n@warning_ignore(\"x\")\nfunc b():\n\tpass\n"
        );
    }

    #[test]
    fn blanks_inserted_after_a_def_before_a_following_non_def() {
        // gdformat surrounds a def: a top-level statement after a func body gets 2 blanks too.
        let src = "func a():\n\tpass\nvar x = 1\n";
        assert_eq!(fmt(src), "func a():\n\tpass\n\n\nvar x = 1\n");
    }

    #[test]
    fn static_var_is_not_a_def_but_static_func_is() {
        // `static var` is an ordinary member (no surrounding blanks); `static func` is a def.
        let src = "var a = 1\nstatic var b = 2\nstatic func c():\n\tpass\n";
        assert_eq!(
            fmt(src),
            "var a = 1\nstatic var b = 2\n\n\nstatic func c():\n\tpass\n"
        );
    }

    #[test]
    fn no_blank_before_the_first_def_in_the_file() {
        let src = "func a():\n\tpass\n";
        assert_eq!(fmt(src), src);
    }

    #[test]
    fn blank_insertion_off_leaves_blanks_alone() {
        let cfg = FmtConfig {
            insert_blank_lines: false,
            ..FmtConfig::default()
        };
        let src = "func a():\n\tpass\nfunc b():\n\tpass\n";
        assert_eq!(format(src, &cfg), src);
    }

    // ---- Phase-4 increment C: line-ending preservation ----

    #[test]
    fn crlf_line_endings_are_preserved() {
        // A CRLF file is formatted (spacing + blank policy applied) but stays CRLF — never churned
        // to LF.
        let src = "func a():\r\n\tvar x=1\r\nfunc b():\r\n\tpass\r\n";
        assert_eq!(
            fmt(src),
            "func a():\r\n\tvar x = 1\r\n\r\n\r\nfunc b():\r\n\tpass\r\n"
        );
    }

    #[test]
    fn crlf_preserved_including_multiline_string_interior() {
        // A `\r\n` inside a multi-line string round-trips (normalize to LF, format, restore CRLF).
        let src = "var s = \"\"\"a\r\nb\"\"\"\r\n";
        let out = fmt(src);
        assert_eq!(out, "var s = \"\"\"a\r\nb\"\"\"\r\n");
        assert!(super::same_significant_tokens(src, &out));
    }

    #[test]
    fn lf_files_stay_lf() {
        let src = "func a():\n\tpass\n";
        assert!(!fmt(src).contains('\r'));
    }

    // ---- Phase-4 increment C: length-driven reflow ----

    #[test]
    fn reflow_compact_call() {
        let src = "func f():\n\tvar long_call = some_function(argument_one, argument_two, argument_three, argument_four, arg_five)\n";
        assert_eq!(
            fmt(src),
            "func f():\n\tvar long_call = some_function(\n\t\targument_one, argument_two, argument_three, argument_four, arg_five\n\t)\n"
        );
        assert_eq!(fmt(&fmt(src)), fmt(src), "idempotent");
    }

    #[test]
    fn reflow_compact_array_and_dict() {
        let arr = "func f():\n\tvar arr = [element_one, element_two, element_three, element_four, element_five, element_six, seven]\n";
        assert_eq!(
            fmt(arr),
            "func f():\n\tvar arr = [\n\t\telement_one, element_two, element_three, element_four, element_five, element_six, seven\n\t]\n"
        );
        let dct = "func f():\n\tvar d = {\"key_one\": value_one, \"key_two\": value_two, \"key_three\": value_three, \"key4\": value_four}\n";
        assert_eq!(
            fmt(dct),
            "func f():\n\tvar d = {\n\t\t\"key_one\": value_one, \"key_two\": value_two, \"key_three\": value_three, \"key4\": value_four\n\t}\n"
        );
    }

    #[test]
    fn reflow_exploded_when_compact_too_long() {
        // When even the single compact continuation line exceeds the width, explode one per line —
        // with NO trailing comma (length-driven). Byte-identical to gdformat.
        let src = "func f():\n\tvar x = process_data(first_long_argument_name_here, second_long_argument_name_here, third_long_argument_name_here, fourth_argument)\n";
        assert_eq!(
            fmt(src),
            "func f():\n\tvar x = process_data(\n\t\tfirst_long_argument_name_here,\n\t\tsecond_long_argument_name_here,\n\t\tthird_long_argument_name_here,\n\t\tfourth_argument\n\t)\n"
        );
        assert_eq!(fmt(&fmt(src)), fmt(src), "idempotent");
    }

    #[test]
    fn reflow_nested_outer_explodes_inner_stays_inline() {
        let src = "func f():\n\tvar n = outermost_call(inner_first(aaaa, bbbb, cccc, dddd), inner_second(eeee, ffff, gggg, hhhh), inner_third(iiii, jjjj, kkkk))\n";
        assert_eq!(
            fmt(src),
            "func f():\n\tvar n = outermost_call(\n\t\tinner_first(aaaa, bbbb, cccc, dddd),\n\t\tinner_second(eeee, ffff, gggg, hhhh),\n\t\tinner_third(iiii, jjjj, kkkk)\n\t)\n"
        );
    }

    #[test]
    fn reflow_short_lines_stay_flat() {
        let src = "func f():\n\tvar short = call(a, b, c)\n";
        assert_eq!(fmt(src), src);
    }

    #[test]
    fn reflow_off_leaves_long_lines() {
        let cfg = FmtConfig {
            reflow: false,
            ..FmtConfig::default()
        };
        let src = "func f():\n\tvar long_call = some_function(argument_one, argument_two, argument_three, argument_four, arg_five)\n";
        // spacing still normalized, but no wrapping
        assert_eq!(format(src, &cfg), src);
    }

    #[test]
    fn reflow_keeps_an_already_canonical_wrapped_statement() {
        // A wrapped statement that is too long to collapse stays in its canonical exploded form.
        let src = "func f():\n\tvar n = outermost_call(\n\t\tinner_first(aaaa, bbbb, cccc, dddd),\n\t\tinner_second(eeee, ffff, gggg, hhhh),\n\t\tinner_third(iiii, jjjj, kkkk)\n\t)\n";
        assert_eq!(fmt(src), src);
    }

    // ---- Phase-4: layout ownership (re-flow already-multi-line statements) ----

    #[test]
    fn reflow_collapses_a_short_hand_wrapped_statement() {
        // A statement the author wrapped that now fits is collapsed back onto one line.
        let src = "func f():\n\tvar x = call(\n\t\ta,\n\t\tb,\n\t\tc\n\t)\n";
        assert_eq!(fmt(src), "func f():\n\tvar x = call(a, b, c)\n");
    }

    #[test]
    fn reflow_rewraps_a_still_too_long_wrapped_statement_idempotently() {
        // A wrapped statement still over the limit is re-laid-out to canonical form, idempotently.
        let src = "func f():\n\tvar x = some_long_function_name(argument_number_one, argument_number_two,\n\t\targument_number_three, argument_number_four, argument_number_five)\n";
        let out = fmt(src);
        assert!(
            out.contains("some_long_function_name(\n"),
            "should wrap: {out:?}"
        );
        assert_eq!(fmt(&out), out, "idempotent");
    }

    #[test]
    fn reflow_keeps_a_statement_with_an_inner_comment_verbatim() {
        // A comment inside the brackets blocks a safe collapse — the statement is preserved.
        let src = "func f():\n\tvar x = call(\n\t\ta,  # first\n\t\tb,\n\t)\n";
        let out = fmt(src);
        assert!(out.contains("# first"), "{out:?}");
        assert!(super::same_significant_tokens(src, &out));
        assert_eq!(fmt(&out), out, "idempotent");
    }

    // ---- Phase-4: CST-driven wrapping (gdformat parity — see `wrap`) ----

    #[test]
    fn wrap_func_param_list_explodes_with_return_type_on_close_line() {
        // A func header over the limit wraps its *parameter list*; the `-> void:` stays a suffix on the
        // closing-paren line (it is never itself wrapped), matching gdformat.
        let src = "func process(first_argument: int, second_argument: String, third_argument: float, fourth: bool) -> void:\n\tpass\n";
        let out = fmt(src);
        assert_eq!(
            out,
            "func process(\n\tfirst_argument: int, second_argument: String, third_argument: float, fourth: bool\n) -> void:\n\tpass\n"
        );
        assert_eq!(fmt(&out), out, "idempotent");
    }

    #[test]
    fn wrap_method_chain_bottom_up_wraps_final_call_args() {
        // When the chain prefix fits, gdformat wraps only the final call's arguments (bottom-up).
        let src = "func f():\n\tobject.method_one(argument).method_two(argument).method_three(argument).method_four(argument_xxxx)\n";
        let out = fmt(src);
        assert_eq!(
            out,
            "func f():\n\tobject.method_one(argument).method_two(argument).method_three(argument).method_four(\n\t\targument_xxxx\n\t)\n"
        );
        assert_eq!(fmt(&out), out, "idempotent");
    }

    #[test]
    fn wrap_method_chain_explodes_leading_dot_when_compact_overflows() {
        // When even the compact chain overflows, gdformat wraps it in parens and breaks at each `.`,
        // leading-dot style (`. method`).
        let src = "func _ready():\n\ttween.tween_property(self, ^\"modulate:a\", 0.0, fade_out_duration).set_trans(Tween.TRANS_LINEAR).set_ease(Tween.EASE_OUT)\n";
        let out = fmt(src);
        assert!(
            out.contains("\t(\n\t\ttween\n\t\t. tween_property("),
            "{out}"
        );
        assert!(
            out.contains("\n\t\t. set_ease(Tween.EASE_OUT)\n\t)\n"),
            "{out}"
        );
        assert_eq!(fmt(&out), out, "idempotent");
    }

    #[test]
    fn wrap_assignment_operator_chain_wraps_in_parens_compact_first() {
        // A too-long assignment RHS that is an operator chain is wrapped in injected parens; the chain
        // stays on one continuation line while it fits there (gdformat's compact-first), exploding at
        // the operator only when even that overflows.
        let src = "func f():\n\tgravity_value = first_long_operand_value_xx * second_long_operand_value_yy * third_long_operand_value_zz\n";
        let out = fmt(src);
        assert_eq!(
            out,
            "func f():\n\tgravity_value = (\n\t\tfirst_long_operand_value_xx * second_long_operand_value_yy * third_long_operand_value_zz\n\t)\n"
        );
        assert_eq!(fmt(&out), out, "idempotent");
    }

    #[test]
    fn wrap_dict_entry_drops_multiline_value_below_the_key() {
        // gdformat's kv-pair rule: a multi-line dict-entry value drops to its own line(s) below the
        // `key =`, and a magic trailing comma forces the whole nest exploded.
        let src = "func f():\n\tvar d := {player = {position = a, health = b,}, enemies = [],}\n";
        let out = fmt(src);
        assert_eq!(
            out,
            "func f():\n\tvar d := {\n\t\tplayer =\n\t\t{\n\t\t\tposition = a,\n\t\t\thealth = b,\n\t\t},\n\t\tenemies = [],\n\t}\n"
        );
        assert_eq!(fmt(&out), out, "idempotent");
    }

    #[test]
    fn wrap_magic_comma_chain_explodes_leading_dot() {
        // A method chain forced multi-line by a magic comma inside it goes straight to leading-dot.
        let src = "func f():\n\treturn obj.method({\"a\": 1, \"b\": 2,})\n";
        let out = fmt(src);
        assert_eq!(
            out,
            "func f():\n\treturn (\n\t\tobj\n\t\t. method(\n\t\t\t{\n\t\t\t\t\"a\": 1,\n\t\t\t\t\"b\": 2,\n\t\t\t}\n\t\t)\n\t)\n"
        );
        assert_eq!(fmt(&out), out, "idempotent");
    }

    #[test]
    fn column_zero_trailing_region_comment_stays_put_without_forced_blanks() {
        // gdformat keeps a column-0 `#endregion` at column 0 and forces no blank lines before a
        // *trailing* comment (one that ends its block).
        let src = "func f():\n\tvar x = 1\n#endregion\n";
        assert_eq!(fmt(src), src);
        let src2 = "#region Section\nvar a = 1\nvar b = 2\n#endregion\n";
        assert_eq!(fmt(src2), src2);
    }

    #[test]
    fn inline_suite_bodies_split_but_lambdas_stay_inline() {
        // gdformat moves an inline suite body to its own indented line; an inline *lambda* body stays.
        assert_eq!(
            fmt("func f():\n\tif cond: do_thing()\n"),
            "func f():\n\tif cond:\n\t\tdo_thing()\n"
        );
        assert_eq!(fmt("func g(): return 1\n"), "func g():\n\treturn 1\n");
        assert_eq!(
            fmt("func i():\n\tif a: b()\n\telse: c()\n"),
            "func i():\n\tif a:\n\t\tb()\n\telse:\n\t\tc()\n"
        );
        // an inline lambda body is preserved
        assert_eq!(
            fmt("func h():\n\tvar a := func(): return 1\n"),
            "func h():\n\tvar a := func(): return 1\n"
        );
    }

    #[test]
    fn backslash_line_continuations_collapse_and_rewrap() {
        // gdformat collapses a `\`-continued statement and re-lays it out — onto one line when it now
        // fits, or re-wrapped (an operator chain becomes paren-wrapped) when it does not.
        assert_eq!(
            fmt("func f():\n\tvar x = a + \\\n\t\tb\n"),
            "func f():\n\tvar x = a + b\n"
        );
        let out = fmt(
            "func f():\n\tif long_condition_name_one == 1 or \\\n\t\t\tlong_condition_name_two == 2 or long_condition_name_three == 3:\n\t\tpass\n",
        );
        assert!(out.contains("\tif (\n"), "{out}");
        assert!(!out.contains('\\'), "backslash removed: {out}");
        assert_eq!(fmt(&out), out, "idempotent");
    }

    #[test]
    fn redundant_grouping_parens_are_stripped() {
        // gdformat strips parens that merely group a standalone expression (value / condition /
        // iterable / argument / element / nested), but keeps precedence-significant ones.
        assert_eq!(fmt("var x = (y)\n"), "var x = y\n");
        assert_eq!(
            fmt("func f():\n\treturn (g(a))\n"),
            "func f():\n\treturn g(a)\n"
        );
        assert_eq!(
            fmt("func f():\n\tfor i in (a * b):\n\t\tpass\n"),
            "func f():\n\tfor i in a * b:\n\t\tpass\n"
        );
        assert_eq!(fmt("var a = g((x))\n"), "var a = g(x)\n"); // call arg + nested
        assert_eq!(fmt("var a = {(k): (v)}\n"), "var a = {k: v}\n");
        // precedence parens kept; an expr-statement assignment RHS keeps its parens (gdformat does too)
        assert_eq!(fmt("var a = (b + c) * d\n"), "var a = (b + c) * d\n");
        assert_eq!(fmt("func f():\n\tx = (y)\n"), "func f():\n\tx = (y)\n");
    }

    #[test]
    fn nested_multiline_bracket_inside_lambda_body_parses_and_wraps() {
        // A lambda body that contains a call which itself wraps (`return new(\n …\n)`) must keep the
        // nested bracket's lines indentation-suppressed (they sit a level *deeper* than the lambda
        // body), then re-lay-out to gdformat's exact shape.
        let src = "func f():\n\tvar v = use_memo(func():\n\t\tif a == null:\n\t\t\treturn make.parse(\"/\")\n\t\treturn make.build(strip_basename(location.path, basename), location.query, location.state)\n\t, [a, base])\n";
        let out = fmt(src);
        assert!(parses_clean(&out), "{out:?}");
        assert!(
            out.contains("\tvar v = use_memo(\n\t\tfunc():"),
            "lambda explodes: {out:?}"
        );
        assert!(
            out.contains("\t\t\treturn make.build(\n"),
            "nested call wraps: {out:?}"
        );
        assert_eq!(fmt(&out), out, "idempotent");
    }

    #[test]
    fn subscript_on_a_long_call_chain_wraps_leading_dot() {
        // `Fmt.format(…)["text"]` is a dot-chain ending in a subscript; when the flat subscriptee
        // overflows, gdformat wraps leading-dot, keeping `. format(…)["text"]` (call then index) on
        // one segment line. (Routes `IndexExpr`-on-a-chain through the dot-chain formatter.)
        let src = "func f():\n\tvar no_space: String = Fmt.format(src, {\"singleAttributePerLine\": true, \"insertSpaceBeforeSelfClose\": false})[\"text\"]\n";
        let out = fmt(src);
        assert!(
            out.contains("\tvar no_space: String = (\n\t\tFmt\n"),
            "leading-dot: {out:?}"
        );
        assert!(
            out.contains("\n\t\t. format(src, {") && out.contains("})[\"text\"]\n"),
            "call+index segment: {out:?}"
        );
        assert!(parses_clean(&out), "{out:?}");
        assert_eq!(fmt(&out), out, "idempotent");
    }

    #[test]
    fn dot_chain_with_lambda_in_earlier_segment_uses_leading_dot() {
        // gdformat bottom-ups a chain whose lambda is in the *final* call's args, but falls back to
        // leading-dot when the lambda sits in an earlier segment (`a.m(func…).n(…)`).
        let src = "func f():\n\tvar t := obj.tween_method(func(x: float) -> void: node.position = origin + curve.sample(x), 0.0, 100.0).set_delay(5.0)\n";
        let out = fmt(src);
        assert!(
            out.contains("\tvar t := (\n"),
            "leading-dot paren wrap: {out:?}"
        );
        assert!(
            out.contains("\n\t\t. set_delay("),
            "explodes at each dot: {out:?}"
        );
        assert!(parses_clean(&out), "{out:?}");
        assert_eq!(fmt(&out), out, "idempotent");
    }

    #[test]
    fn lambda_body_ending_in_arg_separator_comma_parses_and_is_preserved() {
        // gdformat emits `func(): … return X,` where the `,` closes the lambda body mid-line and
        // separates the enclosing call's arguments. The prepass must suppress the lambda body's now
        // -stale trailing newline so the `,` and the next argument parse — and the statement (with an
        // inner comment, so it stays verbatim) round-trips unchanged.
        let src = "func f():\n\tHooks.use_effect(\n\t\tfunc():\n\t\t\tsfx.call(null)  # c1\n\t\t\treturn null,\n\t\t[]\n\t)\n";
        let out = fmt(src);
        assert!(out.contains("\t\t\treturn null,\n"), "{out:?}");
        assert!(out.contains("# c1"), "comment preserved: {out:?}");
        assert!(parses_clean(&out), "{out:?}");
        assert_eq!(fmt(&out), out, "idempotent");
    }

    #[test]
    fn comments_in_a_reshaped_lambda_body_are_threaded_through() {
        // A statement reshaped by the CST wrapper carries its block comments: a trailing inline
        // comment stays on its statement's line (two-space offset), a standalone comment keeps its own
        // line — both at the reshaped indent. (The comment-multiset net falls back to verbatim if any
        // comment can't be placed, so this can only improve correctness.)
        assert_eq!(
            fmt(
                "func r():\n\tx.connect(func():\n\t\ta()  # trailing\n\t\t# standalone\n\t\tb()\n\t)\n"
            ),
            "func r():\n\tx.connect(\n\t\tfunc():\n\t\t\ta()  # trailing\n\t\t\t# standalone\n\t\t\tb()\n\t)\n"
        );
    }

    #[test]
    fn trailing_comment_on_a_reshaped_statement_survives() {
        // A comment trailing the whole statement (`const X := {…}  # note`) lands on the rendered last
        // line of the reshaped statement.
        let src = "const RESERVED := {\"a\": one_long_key_value_that_forces_a_wrap, \"b\": another_long_one_here}  # note\n";
        let out = fmt(src);
        assert!(out.contains("}  # note\n"), "{out:?}");
        assert!(parses_clean(&out), "{out:?}");
        assert_eq!(fmt(&out), out, "idempotent");
    }

    #[test]
    fn lambda_arg_trailing_comment_keeps_separator_before_it() {
        // A lambda argument whose last body statement carries a trailing comment: the enclosing call's
        // argument separator `,` lands before the comment (`return when,  # block`), not after it
        // (which would swallow the `,` into the comment text).
        let src = "func p():\n\tg(func():\n\t\tif c:\n\t\t\twork()\n\t\treturn when  # block\n\t, other)\n";
        assert_eq!(
            fmt(src),
            "func p():\n\tg(\n\t\tfunc():\n\t\t\tif c:\n\t\t\t\twork()\n\t\t\treturn when,  # block\n\t\tother\n\t)\n"
        );
        assert_eq!(fmt(&fmt(src)), fmt(src), "idempotent");
    }

    #[test]
    fn multiline_string_operator_chain_paren_wraps_verbatim() {
        // gdformat paren-wraps `x = """…""" % [args]`: the string's interior lines stay verbatim
        // (literal content, not re-indented), the `% [` goes on its own line, the array explodes.
        let src =
            "func p():\n\t$X.text = \"\"\"%d FPS\n\nObjects:\n%d\n\"\"\" % [\nfps,\nobjs,\n]\n";
        assert_eq!(
            fmt(src),
            "func p():\n\t$X.text = (\n\t\t\"\"\"%d FPS\n\nObjects:\n%d\n\"\"\"\n\t\t% [\n\t\t\tfps,\n\t\t\tobjs,\n\t\t]\n\t)\n"
        );
        assert_eq!(fmt(&fmt(src)), fmt(src), "idempotent");
    }

    #[test]
    fn comments_in_a_collection_are_threaded_through() {
        // A bracket list carries its comments when it explodes: one trailing the open bracket, one
        // trailing an element, and a standalone one keeping its own line — all at the element indent.
        assert_eq!(
            fmt("var x = [  # head\n\t1,  # one\n\t# mid\n\t2,\n]\n"),
            "var x = [  # head\n\t1,  # one\n\t# mid\n\t2,\n]\n"
        );
        // a standalone comment forces the collection multi-line even when it would otherwise fit
        let out = fmt("var d = {\n\t# note\n\t\"a\": 1,\n}\n");
        assert!(out.contains("\t# note\n\t\"a\": 1,"), "{out:?}");
        assert!(parses_clean(&out), "{out:?}");
        assert_eq!(fmt(&out), out, "idempotent");
    }

    #[test]
    fn semicolons_inside_lambda_body_expand_at_correct_depth() {
        // A multi-line lambda body's `;`-separated statements expand one-per-line at the body depth
        // (the lambda's block counts as a depth increment even though its own body is not split inline).
        assert_eq!(
            fmt("func r():\n\tvar cb = func():\n\t\ta(); b(); c()\n"),
            "func r():\n\tvar cb = func():\n\t\ta()\n\t\tb()\n\t\tc()\n"
        );
    }

    #[test]
    fn inline_blocks_inside_inner_class_methods_expand_at_correct_depth() {
        // An inner class's `ClassBody` is one indent level deep, so `;`-separated statements and an
        // inline suite body inside its methods must expand at the method-body depth, not one short.
        assert_eq!(
            fmt("class P:\n\tfunc release():\n\t\ta = 1; b = 2\n"),
            "class P:\n\tfunc release():\n\t\ta = 1\n\t\tb = 2\n"
        );
        assert_eq!(
            fmt("class P:\n\tfunc release():\n\t\tif not x: return\n"),
            "class P:\n\tfunc release():\n\t\tif not x:\n\t\t\treturn\n"
        );
    }

    #[test]
    fn inner_class_extends_moves_to_its_own_body_line() {
        // gdformat splits `class C extends B:` into `class C:` + a leading `extends B` body line.
        assert_eq!(
            fmt("class CRProps extends RefCounted:\n\tvar x = 1\n"),
            "class CRProps:\n\textends RefCounted\n\tvar x = 1\n"
        );
        // a file-level `extends` (not an inner class) is left alone
        assert_eq!(
            fmt("extends Node\n\nvar x = 1\n"),
            "extends Node\n\nvar x = 1\n"
        );
        // idempotent
        let once = fmt("class C extends B:\n\tpass\n");
        assert_eq!(fmt(&once), once);
    }

    #[test]
    fn long_annotated_var_splits_annotation_to_its_own_line() {
        // A short annotated var stays prepended; a long one whose value cannot wrap moves the
        // annotation to its own line (gdformat's `indent + len(ann) + len(line) <= max`, no space).
        assert_eq!(fmt("@onready var x = $Path\n"), "@onready var x = $Path\n");
        let long = "@onready var pelvis: PhysicalBone3D = $\"root/root_001/Skeleton3D/PhysicalBoneSimulator3D/Physical Bone pelvis\"\n";
        let out = fmt(long);
        assert!(out.starts_with("@onready\nvar pelvis:"), "{out:?}");
        assert_eq!(fmt(&out), out, "idempotent");
    }

    #[test]
    fn empty_signal_parens_are_removed() {
        // gdformat writes `signal s` not `signal s()`; a signal with parameters keeps its list.
        assert_eq!(fmt("signal done()\n"), "signal done\n");
        assert_eq!(fmt("signal hit(x, y)\n"), "signal hit(x, y)\n");
        assert_eq!(fmt("signal a\n"), "signal a\n");
    }

    #[test]
    fn semicolon_separated_statements_split() {
        // gdformat splits `;`-separated statements onto their own lines and drops a trailing `;`.
        assert_eq!(
            fmt("func f():\n\ta = 1; b = 2\n"),
            "func f():\n\ta = 1\n\tb = 2\n"
        );
        assert_eq!(fmt("func g():\n\tpass;\n"), "func g():\n\tpass\n");
        assert_eq!(
            fmt("func h():\n\tif c: a(); b()\n"),
            "func h():\n\tif c:\n\t\ta()\n\t\tb()\n"
        );
    }

    #[test]
    fn inline_match_arm_and_property_bodies_split() {
        // A match arm sits one level below `match`, so its inline body splits to two deeper levels.
        assert_eq!(
            fmt("func f():\n\tmatch x:\n\t\t\"inc\": return state + 1\n"),
            "func f():\n\tmatch x:\n\t\t\"inc\":\n\t\t\treturn state + 1\n"
        );
        // A property setter shorthand splits below the `var`.
        assert_eq!(
            fmt("var active: bool = false: set = set_active\n"),
            "var active: bool = false:\n\tset = set_active\n"
        );
        // Inline getter/setter bodies split too.
        assert_eq!(
            fmt("var q: int = 0:\n\tget: return _q\n\tset(v): _q = v\n"),
            "var q: int = 0:\n\tget:\n\t\treturn _q\n\tset(v):\n\t\t_q = v\n"
        );
    }

    #[test]
    fn blank_runs_collapse_to_one_then_defs_restore_two() {
        // gdformat squeezes every blank run to one, then re-adds the 2nd blank only around defs.
        // Between two non-defs, that means a single blank regardless of how many were authored.
        assert_eq!(
            fmt("var a = 1\n\n\nvar b = 2\n"),
            "var a = 1\n\nvar b = 2\n"
        );
        assert_eq!(
            fmt("extends Node\n\n\nvar b = 2\n"),
            "extends Node\n\nvar b = 2\n"
        );
        // Two top-level funcs still get two blanks (def-forced), no matter the authored count.
        assert_eq!(
            fmt("func a():\n\tpass\n\n\n\nfunc b():\n\tpass\n"),
            "func a():\n\tpass\n\n\nfunc b():\n\tpass\n"
        );
    }

    #[test]
    fn annotation_prefixed_def_forces_blanks_only_after_a_def() {
        // An `@rpc func` after a non-def (`extends`/`var`) keeps its source blanks; after a def it gets
        // the usual 2; a plain func always forces 2.
        assert_eq!(
            fmt("extends Node\n\n@rpc(\"x\")\nfunc f():\n\tpass\n"),
            "extends Node\n\n@rpc(\"x\")\nfunc f():\n\tpass\n"
        );
        assert_eq!(
            fmt("func a():\n\tpass\n@rpc(\"x\")\nfunc b():\n\tpass\n"),
            "func a():\n\tpass\n\n\n@rpc(\"x\")\nfunc b():\n\tpass\n"
        );
        assert_eq!(
            fmt("extends Node\nfunc f():\n\tpass\n"),
            "extends Node\n\n\nfunc f():\n\tpass\n"
        );
    }

    #[test]
    fn leading_bom_is_preserved() {
        // gdformat keeps a leading byte-order mark; the reflow must not drop it when it re-renders the
        // first statement from its significant tokens.
        let src = "\u{feff}class_name Foo\nvar x = 1\n";
        let out = fmt(src);
        assert!(out.starts_with('\u{feff}'), "{out:?}");
        assert_eq!(&out[3..], "class_name Foo\nvar x = 1\n");
        assert_eq!(fmt(&out), out, "idempotent");
    }

    #[test]
    fn soft_keyword_member_call_hugs_paren_but_statement_keeps_space() {
        // `obj.match(x)` is a member call (tight `(`), while a `match (x):` statement keeps its space.
        assert_eq!(
            fmt("func f():\n\tvar m = obj.match(\"a\", \"b\")\n"),
            "func f():\n\tvar m = obj.match(\"a\", \"b\")\n"
        );
        assert_eq!(
            fmt("func f():\n\tmatch (x):\n\t\tpass\n"),
            "func f():\n\tmatch (x):\n\t\tpass\n"
        );
    }

    // ---- Phase-4: string-quote normalization (gdformat / Black rule) ----

    #[test]
    fn canonical_string_rules() {
        use super::canonical_string as c;
        assert_eq!(c("'simple'"), "\"simple\""); // prefer double
        assert_eq!(c("\"already\""), "\"already\""); // unchanged
        assert_eq!(c("'has \"x\" in'"), "'has \"x\" in'"); // more " than ' -> keep single
        assert_eq!(c("'a\\'b'"), "\"a'b\""); // escaped ' -> double, unescaped
        assert_eq!(c("'both \" and \\' x'"), "\"both \\\" and ' x\""); // tie -> double, re-escape
        assert_eq!(c("&'name'"), "&\"name\""); // StringName prefix kept
        assert_eq!(c("^'a/b'"), "^\"a/b\""); // NodePath prefix kept
        assert_eq!(c("r'raw\\n'"), "r\"raw\\n\""); // raw: body verbatim
        assert_eq!(c("r'has \"x\"'"), "r'has \"x\"'"); // raw with " -> keep single (cannot escape)
        // gdformat collapses a single-line triple-SINGLE to a regular string; triple-double + any
        // multi-line triple are left verbatim.
        assert_eq!(c("'''triple'''"), "\"triple\"");
        assert_eq!(c("'''say \"hi\"'''"), "'say \"hi\"'"); // body has " -> regular single
        assert_eq!(c("\"\"\"triple\"\"\""), "\"\"\"triple\"\"\""); // triple-double verbatim
        assert_eq!(c("'''line1\nline2'''"), "'''line1\nline2'''"); // multi-line verbatim
        assert_eq!(c("'\\t\\n'"), "\"\\t\\n\""); // non-quote escapes preserved
    }

    #[test]
    fn quote_normalization_in_format() {
        assert_eq!(fmt("var a = 'simple'\n"), "var a = \"simple\"\n");
        assert_eq!(fmt("var b = 'has \"x\" in'\n"), "var b = 'has \"x\" in'\n");
        assert_eq!(fmt("var f = &'n'\n"), "var f = &\"n\"\n");
        // idempotent
        assert_eq!(fmt("var a = \"simple\"\n"), "var a = \"simple\"\n");
    }

    // ---- Phase-4: magic trailing comma ----

    #[test]
    fn magic_trailing_comma_explodes_with_comma() {
        // A magic trailing comma forces exploded-one-per-line WITH the comma, even though it fits.
        assert_eq!(
            fmt("var a = call(x, y,)\n"),
            "var a = call(\n\tx,\n\ty,\n)\n"
        );
        assert_eq!(fmt("var b = [1, 2,]\n"), "var b = [\n\t1,\n\t2,\n]\n");
        assert_eq!(fmt("var g = call(only,)\n"), "var g = call(\n\tonly,\n)\n");
    }

    #[test]
    fn magic_trailing_comma_nested_forces_outer_without_own_comma() {
        // The inner magic group explodes WITH its comma; the outer is forced multi-line by the
        // descendant but, not being magic itself, has no trailing comma after its last element.
        assert_eq!(
            fmt("var d = outer(inner(a, b,), c)\n"),
            "var d = outer(\n\tinner(\n\t\ta,\n\t\tb,\n\t),\n\tc\n)\n"
        );
    }

    #[test]
    fn magic_trailing_comma_is_idempotent() {
        let once = fmt("var a = call(x, y,)\n");
        assert_eq!(fmt(&once), once);
    }

    // ---- Phase-4: inline-comment offset ----

    #[test]
    fn inline_comments_get_two_spaces() {
        assert_eq!(fmt("var x = 1 # one\n"), "var x = 1  # one\n");
        assert_eq!(fmt("var y = 2     # many\n"), "var y = 2  # many\n");
        assert_eq!(
            fmt("func f(): # c\n\tpass ## doc\n"),
            "func f():  # c\n\tpass  ## doc\n"
        );
        // a standalone comment (its own line) is unaffected — it is indentation, not an offset
        assert_eq!(
            fmt("func f():\n\t# standalone\n\tpass\n"),
            "func f():\n\t# standalone\n\tpass\n"
        );
    }

    // ---- Phase-4: format_range ----

    #[test]
    fn format_range_edits_only_the_changed_lines_overlapping_the_selection() {
        let src = "func f():\n\tvar x = 1\n\tvar y=2\n";
        // line 2 (bytes 21..30) needs spacing; selecting it returns just that line's edit
        let e = super::format_range(src, &FmtConfig::default(), 21..30).unwrap();
        assert_eq!(e.range, 21..30);
        assert_eq!(e.new_text, "\tvar y = 2\n");
        // selecting the already-formatted line 1 → no edit
        assert!(super::format_range(src, &FmtConfig::default(), 10..21).is_none());
        // a fully-formatted document → no edit
        assert!(
            super::format_range("func f():\n\tvar y = 2\n", &FmtConfig::default(), 0..20).is_none()
        );
    }

    // ---- Phase-4: enum-brace spacing ----

    #[test]
    fn enum_braces_are_spaced_dicts_are_not() {
        assert_eq!(fmt("enum E {A, B, C}\n"), "enum E { A, B, C }\n");
        assert_eq!(fmt("enum {A, B}\n"), "enum { A, B }\n"); // anonymous
        assert_eq!(
            fmt("enum Named {RED = 1, GREEN = 2}\n"),
            "enum Named { RED = 1, GREEN = 2 }\n"
        );
        assert_eq!(fmt("enum Empty {}\n"), "enum Empty {}\n"); // empty stays tight
        // a dict literal stays tight even right after an enum on the previous line
        assert_eq!(
            fmt("enum E {A}\nvar d = {\"k\": 1}\n"),
            "enum E { A }\nvar d = {\"k\": 1}\n"
        );
    }

    // ---- Phase-4: operator-chain wrapping ----

    #[test]
    fn operator_chain_if_condition_breaks_operator_leading() {
        let src = "func f():\n\tif condition_number_one and condition_number_two and condition_number_three and condition_number_four:\n\t\tpass\n";
        assert_eq!(
            fmt(src),
            "func f():\n\tif (\n\t\tcondition_number_one\n\t\tand condition_number_two\n\t\tand condition_number_three\n\t\tand condition_number_four\n\t):\n\t\tpass\n"
        );
        assert_eq!(fmt(&fmt(src)), fmt(src), "idempotent");
    }

    #[test]
    fn operator_chain_breaks_at_lowest_precedence_only() {
        // `a and b or c and d`: break at the lower-precedence `or`, keeping the `and` groups inline.
        let src = "func f():\n\tif aaaaaaaaaaaaaaaaaaaaaaaa and bbbbbbbbbbbbbbbbbbbbbbbb or cccccccccccccccccccccccc and dddddddd:\n\t\tpass\n";
        assert_eq!(
            fmt(src),
            "func f():\n\tif (\n\t\taaaaaaaaaaaaaaaaaaaaaaaa and bbbbbbbbbbbbbbbbbbbbbbbb\n\t\tor cccccccccccccccccccccccc and dddddddd\n\t):\n\t\tpass\n"
        );
    }

    #[test]
    fn operator_chain_dot_chain_wraps_compact() {
        let src = "func f():\n\tvar chain = some_object.first_method().second_method().third_method().fourth_method().fifth_method_x()\n";
        assert_eq!(
            fmt(src),
            "func f():\n\tvar chain = (\n\t\tsome_object.first_method().second_method().third_method().fourth_method().fifth_method_x()\n\t)\n"
        );
    }

    #[test]
    fn operator_chain_never_splits_a_node_path() {
        // A node-path's `/` are path separators, not division — an over-long node-path must stay on
        // one line (the parser reads `$A / B` as the same path as `$A/B`, so splitting it would be a
        // silent meaning change in disguise — and gdformat leaves it alone anyway).
        let src = "func f():\n\tvar n = $LongContainerNameHere/AnotherLongChildNode/YetAnotherChildNode/AndOneMoreChildNodeHere/FinalNode\n";
        assert_eq!(fmt(src), src);
    }

    #[test]
    fn quote_normalization_off() {
        let cfg = FmtConfig {
            normalize_strings: false,
            ..FmtConfig::default()
        };
        assert_eq!(format("var a = 'simple'\n", &cfg), "var a = 'simple'\n");
    }

    #[test]
    fn meaning_preserved_accepts_quote_trailing_comma_and_redundant_parens() {
        use super::meaning_preserved as mp;
        // string quotes, trailing commas, and *redundant* grouping parens are all accepted
        assert!(mp("var a = 'x'\n", "var a = \"x\"\n"));
        assert!(mp("func f():\n\tg(a, b,)\n", "func f():\n\tg(a, b)\n"));
        assert!(mp(
            "func f():\n\tvar x = [1, 2,]\n",
            "func f():\n\tvar x = [1, 2]\n"
        ));
        assert!(mp("var x = (a + b)\n", "var x = a + b\n"));
        assert!(mp(
            "func f():\n\tif (a and b):\n\t\tpass\n",
            "func f():\n\tif a and b:\n\t\tpass\n"
        ));
        // but real changes — value, dropped token, and PRECEDENCE — are still caught
        assert!(!mp("var a = 'x'\n", "var a = \"y\"\n"));
        assert!(!mp("func f():\n\tg(a, b)\n", "func f():\n\tg(a)\n"));
        assert!(!mp("var x = (a + b) * c\n", "var x = a + b * c\n"));
    }

    #[test]
    fn blank_at_the_start_of_a_block_is_stripped() {
        // gdformat removes a blank line between a compound header and its body's first statement.
        let src = "func f():\n\tfor i in range(3):\n\n\t\tprint(i)\n";
        assert_eq!(fmt(src), "func f():\n\tfor i in range(3):\n\t\tprint(i)\n");
    }

    #[test]
    fn blank_after_a_leading_block_comment_is_kept() {
        // A comment is the block's first content, so a blank between it and the first statement is
        // preserved (the comment is not a header — the statement is not a new block's first line).
        let src = "func f():\n\t# note\n\n\tvar x = 1\n";
        assert_eq!(fmt(src), src);
    }

    #[test]
    fn a_column0_comment_amid_a_function_body_forces_no_def_blanks() {
        // Commented-out code dedented to column 0 in the middle of a function body is structurally
        // inside the function (its next code is deeper), so it must not be treated as a class-level
        // sibling of the function and get 2 blank lines forced before it.
        let src = "func f():\n\tvar a = 1\n# commented out\n#\tvar b = 2\n\tvar c = 3\n";
        assert_eq!(fmt(src), src);
    }

    #[test]
    fn a_trailing_comment_does_not_force_a_fitting_statement_to_wrap() {
        // gdformat measures line width without the trailing comment (appended in a post-pass), so an
        // author-wrapped statement whose code fits on one line is collapsed even though the comment
        // pushes the rendered line past the width.
        let src = "const C := [aaaaaaaaaa, bbbbbbbbbb, cccccccccc, dddddddddd, eeeeeeeeee, ffffffffff]  # note\n";
        let multi = "const C := [\n\taaaaaaaaaa,\n\tbbbbbbbbbb,\n\tcccccccccc,\n\tdddddddddd,\n\teeeeeeeeee,\n\tffffffffff\n]  # note\n";
        // code-without-comment is < 100, so both the already-flat and the author-wrapped forms collapse.
        assert_eq!(fmt(src), src);
        assert_eq!(fmt(multi), src);
    }

    #[test]
    fn standalone_comments_after_a_lambda_arg_hang_off_the_body() {
        // A call with a multi-line lambda argument followed by another argument explodes its arg list;
        // a standalone comment dedented between the lambda and the next arg is re-emitted at the lambda
        // *body's* indent (gdformat's `_get_greater_indent`), and the arg separator `,` lands on the
        // lambda's last body line before its trailing comment.
        let src =
            "func f():\n\tcall(func():\n\t\ta()\n\t\treturn b  # t\n\t# between\n\t, [x, y])\n";
        let want = "func f():\n\tcall(\n\t\tfunc():\n\t\t\ta()\n\t\t\treturn b,  # t\n\t\t\t# between\n\t\t[x, y]\n\t)\n";
        assert_eq!(fmt(src), want);
    }

    #[test]
    fn comments_thread_through_an_operator_chain_wrap() {
        // A standalone comment between operands of a paren-wrapped operator chain is re-emitted on its
        // own line at the operand indent (one level past the assignment), matching gdformat — and the
        // chain is forced multi-line by the comment even though it would otherwise fit on one line.
        let src = "func f():\n\tx = (\n\t\t\ta\n\t\t\t# note\n\t\t\t- b\n\t)\n";
        let want = "func f():\n\tx = (\n\t\ta\n\t\t# note\n\t\t- b\n\t)\n";
        assert_eq!(fmt(src), want);
    }

    #[test]
    fn a_block_trailing_comment_keeps_its_shallower_indent() {
        // A comment after a nested block, indented to the enclosing function body, stays at the body's
        // depth (gdformat places it in the block whose range contains it), not snapped to column 0.
        let src = "func f():\n\tif c:\n\t\tx()\n\t# trailing\n\n\nfunc g():\n\tpass\n";
        let out = fmt(src);
        assert!(
            out.contains("\n\t# trailing\n"),
            "comment kept at one tab:\n{out}"
        );
    }
}