Skip to main content

gdscript_fmt/
lib.rs

1//! `gdscript-fmt` — the GDScript source formatter (Phase-6 Workstream 3).
2//!
3//! > **Internal layer (not a stable API).** Depend on [`gdscript-ide`](https://docs.rs/gdscript-ide) (the public surface); the items here
4//! > may change between releases.
5//!
6//! A pure `fn(source, &FmtConfig) -> String`: no engine model, no filesystem, `wasm32`-safe.
7//! It re-emits the lexer/pre-pass token stream, normalizing **block indentation** (to the
8//! configured unit), **trailing whitespace**, and the **final newline** — every *significant*
9//! token (keywords, identifiers, literals — including multi-line strings, which are single tokens)
10//! is emitted **verbatim**, so meaning cannot change.
11//!
12//! It also normalizes **intra-line spacing** (Phase-4 increment A): one space around binary
13//! operators / assignments / `->` / `:=`, after `,` and `:` (in type-annotation and dict contexts),
14//! hugged brackets (`f(x, y)`, `[1, 2]`), tight member access (`a.b`), and tight unary `-x`. The
15//! decision is purely local (previous significant token + innermost bracket), and the genuinely
16//! ambiguous contexts — slice colons `arr[a:b]`, and node-path sigils `$Node/Path` / `%Unique`
17//! (where a stray space around `/` would silently change meaning **without** changing the token
18//! sequence) — are left **verbatim** / kept tight by a small node-path state machine.
19//!
20//! **Safe by construction.** In `safe_mode` (the default) the formatter (a) refuses to touch a
21//! file with syntax errors, and (b) re-lexes its own output and **falls back to the original** if
22//! the significant token sequence changed. So it never corrupts code, even input it doesn't fully
23//! understand. The result is idempotent: `format(format(x)) == format(x)`.
24//!
25//! It also performs **length-driven line reflow** (Phase-4 increment C): a single-line statement
26//! that exceeds `line_width` and contains a bracketed group is wrapped flat → compact → exploded via
27//! a small `Doc`-IR, matching gdformat and preserving the token sequence. The remaining gdformat
28//! behaviours (magic trailing comma, operator-chain paren injection, quote normalization) are
29//! token-mutating and documented in `DEVIATIONS.md`.
30#![cfg_attr(docsrs, feature(doc_cfg))]
31#![deny(missing_docs)]
32
33use gdscript_syntax::SyntaxKind;
34
35mod wrap;
36
37/// Formatter options. Defaults match the Godot convention (tabs) and keep the safety net on.
38#[derive(Debug, Clone, PartialEq, Eq)]
39#[allow(
40    clippy::struct_excessive_bools,
41    reason = "a plain user-facing options bag; each bool is an independent formatter toggle, not a state machine"
42)]
43pub struct FmtConfig {
44    /// Indent with tabs (the Godot convention). `false` indents with [`indent_size`](Self::indent_size) spaces.
45    pub use_tabs: bool,
46    /// Spaces per indent level when `use_tabs` is `false`.
47    pub indent_size: usize,
48    /// The target line width for [`reflow`](Self::reflow) (default 100).
49    pub line_width: usize,
50    /// Normalize intra-line spacing between tokens (one space around binary operators, after
51    /// `,`/`:`, hugged brackets, tight member access + unary). On by default. Turn off to format
52    /// **indentation only** (the pre-increment-A behavior).
53    pub normalize_spacing: bool,
54    /// Collapse runs of blank lines (max 2 at top level, max 1 inside a block) and strip leading
55    /// blank lines. On by default.
56    pub collapse_blank_lines: bool,
57    /// Insert blank lines around definitions to match gdformat (2 around top-level `func`/`class`/
58    /// `static func`, 1 around nested ones; comments/annotations attached to a def move with it). On
59    /// by default. Purely additive — never changes the significant token sequence.
60    pub insert_blank_lines: bool,
61    /// Wrap a single-line statement that exceeds [`line_width`](Self::line_width) and contains a
62    /// bracketed group (call / array / dict / parameter list) — flat → compact → exploded, matching
63    /// gdformat's length-driven layout. On by default. Token-preserving (no trailing comma added).
64    pub reflow: bool,
65    /// Normalize string-literal quotes to gdformat's style: prefer `"`, fall back to `'` only when
66    /// the body has more `"` than `'`. On by default. Preserves the string's value (a token-mutating
67    /// rewrite guarded by the meaning-equivalence net).
68    pub normalize_strings: bool,
69    /// Split an inline suite body onto its own indented line (`if c: x` → `if c:` / `x`,
70    /// `func f(): return` → split, `a; b` → two lines), matching gdformat. An inline *lambda* body
71    /// (`func(): x`) is preserved. On by default. Token-preserving (adds only newlines/indent).
72    pub expand_inline_blocks: bool,
73    /// Remove redundant grouping parens from a *standalone-expression* position — a var/const/return
74    /// value, a `for` iterable, an `if`/`while` condition, a call argument, an array/dict element, a
75    /// nested `(…)` — matching gdformat's `remove_outer_parentheses`. Precedence-significant parens
76    /// (`(a + b) * c`) are kept. On by default. Token-mutating; guarded by the meaning-equivalence net.
77    pub strip_parens: bool,
78    /// Collapse a multi-line lambda whose body is a single simple statement onto one line
79    /// (`func():\n\tbody` → `func(): body`), matching gdformat, so the surrounding statement can re-flow
80    /// (and often fit on one line). On by default. Token-preserving (removes only a newline/indent).
81    pub collapse_lambdas: bool,
82    /// Re-parse + significant-token-equality fallback to verbatim. Keep on unless you have a
83    /// reason not to: it is the guarantee the formatter never changes meaning.
84    pub safe_mode: bool,
85}
86
87impl Default for FmtConfig {
88    fn default() -> Self {
89        Self {
90            use_tabs: true,
91            indent_size: 4,
92            line_width: 100,
93            normalize_spacing: true,
94            collapse_blank_lines: true,
95            insert_blank_lines: true,
96            reflow: true,
97            normalize_strings: true,
98            expand_inline_blocks: true,
99            strip_parens: true,
100            collapse_lambdas: true,
101            safe_mode: true,
102        }
103    }
104}
105
106impl FmtConfig {
107    /// One level of indentation as a string.
108    #[must_use]
109    fn indent_unit(&self) -> String {
110        if self.use_tabs {
111            "\t".to_owned()
112        } else {
113            " ".repeat(self.indent_size)
114        }
115    }
116}
117
118/// Format `source`, returning the tidied text. In `safe_mode` (the default) this returns `source`
119/// unchanged rather than risk a meaning-changing edit (a syntax error in the input, or output whose
120/// significant tokens differ from the input's).
121///
122/// The source's line-ending style is preserved: a file using `\r\n` (a Windows checkout) is
123/// formatted internally in `\n` and re-emitted with `\r\n`, so the formatter never churns every line
124/// by flipping CRLF to LF (matching gdformat, which preserves line endings).
125#[must_use]
126pub fn format(source: &str, config: &FmtConfig) -> String {
127    // A leading byte-order mark is preserved (gdformat keeps it): strip it, format the rest, re-add it.
128    // Otherwise the reflow — which re-emits each statement from its *significant* tokens — would drop
129    // the BOM (a trivia token) when it re-renders the first line.
130    if let Some(rest) = source.strip_prefix('\u{feff}') {
131        return format!("\u{feff}{}", format(rest, config));
132    }
133    if source.contains("\r\n") {
134        let lf = source.replace("\r\n", "\n");
135        return format_lf(&lf, config).replace('\n', "\r\n");
136    }
137    format_lf(source, config)
138}
139
140/// A single whole-line replacement edit produced by [`format_range`].
141#[derive(Debug, Clone, PartialEq, Eq)]
142pub struct RangeEdit {
143    /// The byte range in the original `source` to replace (snapped to whole lines).
144    pub range: core::ops::Range<usize>,
145    /// The replacement text.
146    pub new_text: String,
147}
148
149/// Format only the part of `source` overlapping the byte range `sel` (for editor "format selection"
150/// / LSP `textDocument/rangeFormatting`). The whole document is formatted for correct structure and
151/// indentation; the result is the **minimal changed line-hunk** that intersects `sel`, or `None` if
152/// nothing in the selection's lines changes. Applying the edit yields the same bytes the whole-file
153/// [`format`] would have produced for that region.
154#[must_use]
155pub fn format_range(
156    source: &str,
157    config: &FmtConfig,
158    sel: core::ops::Range<usize>,
159) -> Option<RangeEdit> {
160    let formatted = format(source, config);
161    if formatted == source {
162        return None;
163    }
164    let src: Vec<&str> = source.split_inclusive('\n').collect();
165    let out: Vec<&str> = formatted.split_inclusive('\n').collect();
166    // Trim the common prefix and suffix lines: the change is the span between them.
167    let mut p = 0;
168    while p < src.len() && p < out.len() && src[p] == out[p] {
169        p += 1;
170    }
171    let mut s = 0;
172    while s < src.len() - p && s < out.len() - p && src[src.len() - 1 - s] == out[out.len() - 1 - s]
173    {
174        s += 1;
175    }
176    let (changed_start, changed_end) = (p, src.len() - s); // source line span [start, end)
177
178    // Byte offset of each source line start (so `starts[i]` is line `i`'s first byte).
179    let mut starts = Vec::with_capacity(src.len() + 1);
180    let mut acc = 0;
181    for l in &src {
182        starts.push(acc);
183        acc += l.len();
184    }
185    starts.push(acc);
186    let line_of = |off: usize| starts.partition_point(|&b| b <= off).saturating_sub(1);
187
188    let last_byte = source.len().saturating_sub(1);
189    let sel_first = line_of(sel.start.min(last_byte));
190    let sel_last = line_of(sel.end.saturating_sub(1).max(sel.start).min(last_byte));
191    if changed_end <= sel_first || changed_start > sel_last {
192        return None; // the selection's lines are unchanged
193    }
194    Some(RangeEdit {
195        range: starts[changed_start]..starts[changed_end],
196        new_text: out[p..out.len() - s].concat(),
197    })
198}
199
200/// `format` working purely in LF (the caller handles CRLF round-tripping).
201fn format_lf(source: &str, config: &FmtConfig) -> String {
202    let input_parses = gdscript_syntax::parse(source).errors().is_empty();
203    // Safe mode: never reformat around a syntax error — we'd risk mis-indenting a mis-parsed block.
204    if config.safe_mode && !input_parses {
205        return source.to_owned();
206    }
207    // Move an inner class's `extends` onto its own body line (`class C extends B:` → `class C:` /
208    // `extends B`), matching gdformat. Self-validating; a no-op when there is no such class.
209    let unextended = split_inner_class_extends(source);
210    let source = unextended.as_deref().unwrap_or(source);
211    // Collapse single-statement lambda bodies, strip redundant grouping parens, then de-inline suite
212    // bodies, so the rest of the pipeline sees a paren-clean, one-statement-per-line tree with inline
213    // lambdas. Each pre-pass self-validates and is a no-op otherwise.
214    let collapsed = if config.collapse_lambdas {
215        collapse_inline_lambdas(source, config)
216    } else {
217        None
218    };
219    let source = collapsed.as_deref().unwrap_or(source);
220    let stripped = if config.strip_parens {
221        strip_outer_parens(source)
222    } else {
223        None
224    };
225    let source = stripped.as_deref().unwrap_or(source);
226    let expanded = if config.expand_inline_blocks {
227        expand_inline_blocks(source, config)
228    } else {
229        None
230    };
231    let source = expanded.as_deref().unwrap_or(source);
232    let mut out = reindent(source, config);
233    if config.insert_blank_lines {
234        // Purely additive (only inserts blank lines), so the significant-token net still holds.
235        out = insert_def_blanks(&out, config);
236    }
237    if config.reflow {
238        // Length-driven wrapping; token-preserving (no trailing comma added).
239        out = reflow(&out, config);
240    }
241    if config.safe_mode {
242        // The safety net is two-layered, because each catches what the other cannot:
243        // (1) meaning-equivalence catches a dropped / reordered / corrupted *token* — while
244        //     normalising away the rewrites the formatter is allowed to make (string-quote style,
245        //     trailing commas);
246        if !meaning_preserved(source, &out) {
247            return source.to_owned();
248        }
249        // (2) a parse-validity recheck catches a meaning-changing *indentation* edit — indentation
250        //     lives entirely in trivia/synthetic layout, so it is invisible to (1). If the input
251        //     parsed clean, the output must too, else we fall back to the verbatim source.
252        if input_parses && !gdscript_syntax::parse(&out).errors().is_empty() {
253            return source.to_owned();
254        }
255    }
256    out
257}
258
259/// gdformat splits an inner class header's `extends` clause onto the first body line
260/// (`class C extends B:` → `class C:` then `\textends B`). Source pre-pass, parse-tree-driven and
261/// self-validating; returns `None` (leave untouched) if it does not parse, has no such class, or the
262/// rewrite changes meaning or fails to parse.
263fn split_inner_class_extends(source: &str) -> Option<String> {
264    let parse = gdscript_syntax::parse(source);
265    if !parse.errors().is_empty() {
266        return None;
267    }
268    // (remove_start, remove_end, insert_pos, insert_text)
269    let mut edits: Vec<(usize, usize, usize, String)> = Vec::new();
270    collect_inner_class_extends(&parse.syntax_node(), source, &mut edits);
271    if edits.is_empty() {
272        return None;
273    }
274    // Apply highest-offset edit first so earlier byte spans stay valid.
275    edits.sort_unstable_by_key(|e| std::cmp::Reverse(e.2));
276    let mut out = source.to_owned();
277    for (rs, re, ins_pos, text) in &edits {
278        out.insert_str(*ins_pos, text);
279        out.replace_range(*rs..*re, "");
280    }
281    // This transform deliberately *moves* the `extends` clause (header → body), which the structural
282    // meaning net would flag as a reorder. It only relocates existing tokens, so validate that the
283    // significant-token multiset is unchanged and the result parses clean.
284    if !same_token_multiset(source, &out) || !gdscript_syntax::parse(&out).errors().is_empty() {
285        return None;
286    }
287    Some(out)
288}
289
290/// Whether two sources contain the same multiset of significant (non-trivia) `(kind, text)` tokens —
291/// order-insensitive. Used to validate a transform that only *relocates* existing tokens.
292fn same_token_multiset(a: &str, b: &str) -> bool {
293    fn bag(s: &str) -> Vec<(String, String)> {
294        let mut v: Vec<(String, String)> = gdscript_syntax::tokenize(s)
295            .into_iter()
296            .filter(|t| !t.kind.is_trivia() && t.kind != SyntaxKind::Semicolon)
297            .map(|t| (format!("{:?}", t.kind), s[t.range].to_owned()))
298            .collect();
299        v.sort();
300        v
301    }
302    bag(a) == bag(b)
303}
304
305/// Collect the edits that split each inner class's `extends` clause onto its first body line.
306fn collect_inner_class_extends(
307    node: &gdscript_syntax::GdNode,
308    src: &str,
309    edits: &mut Vec<(usize, usize, usize, String)>,
310) {
311    use SyntaxKind as S;
312    for child in node.children() {
313        let edit = (child.kind() == S::InnerClassDecl)
314            .then(|| inner_class_extends_edit(child, src))
315            .flatten();
316        if let Some(edit) = edit {
317            edits.push(edit);
318        }
319        collect_inner_class_extends(child, src, edits);
320    }
321}
322
323/// For an inner class whose header carries `extends T`, build the edit that removes ` extends T` from
324/// the header and inserts `<body-indent>extends T\n` before the first body statement. `None` when the
325/// class has no `extends`, no body statement, or any required token is missing.
326fn inner_class_extends_edit(
327    class: &gdscript_syntax::GdNode,
328    src: &str,
329) -> Option<(usize, usize, usize, String)> {
330    use SyntaxKind as S;
331    use cstree::util::NodeOrToken;
332    let name = class.children().find(|c| c.kind() == S::Name)?;
333    let body = class.children().find(|c| c.kind() == S::ClassBody)?;
334    // The header tokens (direct children) hold `extends`, the type spelling, and the `:`.
335    let mut extends_start = None;
336    let mut colon_start = None;
337    for c in class.children_with_tokens() {
338        if let NodeOrToken::Token(t) = c {
339            match t.kind() {
340                S::ExtendsKw if extends_start.is_none() => {
341                    extends_start = Some(usize::from(t.text_range().start()));
342                }
343                S::Colon if extends_start.is_some() && colon_start.is_none() => {
344                    colon_start = Some(usize::from(t.text_range().start()));
345                }
346                _ => {}
347            }
348        }
349    }
350    let extends_start = extends_start?;
351    let colon_start = colon_start?;
352    // The type spelling sits between `extends` and `:`.
353    let ty = src.get(extends_start..colon_start)?.trim();
354    let ty = ty.strip_prefix("extends")?.trim();
355    if ty.is_empty() {
356        return None;
357    }
358    // Remove ` extends T` — from the name's end up to the colon (drops the joining space too).
359    let remove_start = usize::from(name.text_range().end());
360    let remove_end = colon_start;
361    // Insert the `extends` line at the first body statement's indentation (its node range starts at
362    // the leading whitespace, so read it forward from there).
363    let first_stmt = body.children().next()?;
364    let stmt_start = usize::from(first_stmt.text_range().start());
365    let indent: String = src[stmt_start..]
366        .chars()
367        .take_while(|&c| c == '\t' || c == ' ')
368        .collect();
369    let insert = format!("{indent}extends {ty}\n");
370    Some((remove_start, remove_end, stmt_start, insert))
371}
372
373/// gdformat moves an inline suite body to its own indented line (`if c: x` → two lines, `func f():
374/// return` → split), while keeping an inline *lambda* body (`func(): x`). We do it as a source
375/// pre-pass driven by the parse tree: find each block whose first statement shares its header's line
376/// — and whose parent is a real statement/declaration, not a `LambdaExpr` — and insert a newline +
377/// indentation before the body. Returns `None` (leave the source untouched) if it does not parse,
378/// nothing is inline, or the rewrite would drop a significant token / fail to parse.
379fn expand_inline_blocks(source: &str, config: &FmtConfig) -> Option<String> {
380    let parse = gdscript_syntax::parse(source);
381    if !parse.errors().is_empty() {
382        return None;
383    }
384    let unit = config.indent_unit();
385    // Each split is `(start, replace_len, text)`: `replace_len` bytes at `start` become `text`.
386    let mut splits: Vec<(usize, usize, String)> = Vec::new();
387    collect_inline_splits(&parse.syntax_node(), source, 0, &unit, &mut splits);
388    if splits.is_empty() {
389        return None;
390    }
391    splits.sort_by_key(|(off, ..)| std::cmp::Reverse(*off)); // apply right-to-left so offsets stay valid
392    let mut out = source.to_owned();
393    for (off, len, text) in &splits {
394        out.replace_range(*off..*off + *len, text);
395    }
396    // The rewrite only adds layout (newlines/indent), so it must keep every significant token and the
397    // structure intact — verified by a token-equality + parse-validity check.
398    if !same_significant_tokens(source, &out) || !gdscript_syntax::parse(&out).errors().is_empty() {
399        return None;
400    }
401    Some(out)
402}
403
404/// Collect `(offset, inserted_text)` for each inline suite body, recursing with `depth` = the
405/// indentation level of `node`'s own line (incremented when descending into a block).
406fn collect_inline_splits(
407    node: &gdscript_syntax::GdNode,
408    src: &str,
409    depth: usize,
410    unit: &str,
411    splits: &mut Vec<(usize, usize, String)>,
412) {
413    use SyntaxKind as S;
414    use cstree::util::NodeOrToken;
415    for elem in node.children_with_tokens() {
416        let child = match elem {
417            NodeOrToken::Node(n) => n,
418            NodeOrToken::Token(t) => {
419                // A statement-level `;` separates two statements — replace it (and any following
420                // spaces) with a newline + this block's indent so each lands on its own line; a
421                // *trailing* `;` (nothing after it on the line) is dropped.
422                if t.kind() == S::Semicolon {
423                    let r = t.text_range();
424                    let (s, e) = (usize::from(r.start()), usize::from(r.end()));
425                    let trail = src[e..].len() - src[e..].trim_start_matches([' ', '\t']).len();
426                    let rest = &src[e + trail..];
427                    let text = if rest.starts_with('\n') || rest.is_empty() {
428                        String::new()
429                    } else {
430                        format!("\n{}", unit.repeat(depth))
431                    };
432                    splits.push((s, e + trail - s, text));
433                }
434                continue;
435            }
436        };
437        // A block-introducing child: a statement/declaration `Block` or a property body
438        // (`var x: T: set = f`). Its *inline-body split* (moving the body onto its own indented line)
439        // is skipped for a `LambdaExpr`'s block — a lambda's body stays inline; the wrapper owns its
440        // multi-line layout — but the block still counts as a depth increment so any `;`-separated
441        // statements *inside* the lambda body land at the correct indentation.
442        let is_block = matches!(child.kind(), S::Block | S::PropertyBody);
443        let suite = is_block && node.kind() != S::LambdaExpr;
444        if suite {
445            // The body offset: a `Block`'s first token, or — for a property body, whose own first
446            // token is the `:` — its first getter/setter node (the content after the `:`).
447            let body_offset = if child.kind() == S::PropertyBody {
448                child.children().next().and_then(first_sig_offset)
449            } else {
450                first_sig_offset(child)
451            };
452            let inline_body =
453                body_offset.filter(|&bs| src[..bs].trim_end_matches([' ', '\t']).ends_with(':'));
454            if let Some(bs) = inline_body {
455                splits.push((bs, 0, format!("\n{}", unit.repeat(depth + 1))));
456            }
457        }
458        // A child is one indent level deeper when it is a suite, an inner class's `ClassBody` (whose
459        // members sit a level below the `class` header), or a `match` arm (arms sit a level below the
460        // `match` with no intervening `Block` node).
461        let deeper = is_block
462            || child.kind() == S::ClassBody
463            || (node.kind() == S::MatchStmt && child.kind() == S::MatchArm);
464        collect_inline_splits(child, src, depth + usize::from(deeper), unit, splits);
465    }
466}
467
468/// Collapse a multi-line lambda whose body is a single simple statement onto one line, matching
469/// gdformat (`func():\n\tbody` → `func(): body`), so the surrounding statement can re-flow. Restricted
470/// to a body that is a single statement, not a compound (`if`/`for`/…), on a single physical line, with
471/// only whitespace between the lambda's `:` and the body — so the join is a pure newline/indent removal.
472/// Returns `None` (leave the source) if nothing collapses or the result is not meaning-equivalent.
473fn collapse_inline_lambdas(source: &str, config: &FmtConfig) -> Option<String> {
474    let parse = gdscript_syntax::parse(source);
475    if !parse.errors().is_empty() {
476        return None;
477    }
478    let mut edits: Vec<(usize, usize)> = Vec::new(); // (start, end) byte ranges to replace with " "
479    collect_lambda_collapses(&parse.syntax_node(), source, config, &mut edits);
480    if edits.is_empty() {
481        return None;
482    }
483    edits.sort_unstable_by_key(|(s, _)| std::cmp::Reverse(*s));
484    let mut out = source.to_owned();
485    for (s, e) in &edits {
486        out.replace_range(*s..*e, " ");
487    }
488    if !meaning_preserved(source, &out) || !gdscript_syntax::parse(&out).errors().is_empty() {
489        return None;
490    }
491    Some(out)
492}
493
494/// Collect `(start, end)` ranges (the whitespace/newline between a collapsible lambda's `:` and its
495/// body) to replace with a single space.
496fn collect_lambda_collapses(
497    node: &gdscript_syntax::GdNode,
498    src: &str,
499    config: &FmtConfig,
500    edits: &mut Vec<(usize, usize)>,
501) {
502    for child in node.children() {
503        let collapse = (child.kind() == SyntaxKind::LambdaExpr)
504            .then(|| lambda_collapse_range(child, src, config))
505            .flatten();
506        if let Some((s, e)) = collapse {
507            edits.push((s, e));
508        }
509        collect_lambda_collapses(child, src, config, edits);
510    }
511}
512
513/// The byte range between a lambda's `:` and its body to collapse, when the body is a single simple
514/// statement spanning one physical line, the gap is whitespace only (no comment), and the *enclosing*
515/// statement would fit on one line once collapsed (gdformat keeps a lambda multi-line otherwise).
516fn lambda_collapse_range(
517    lambda: &gdscript_syntax::GdNode,
518    src: &str,
519    config: &FmtConfig,
520) -> Option<(usize, usize)> {
521    use SyntaxKind as S;
522    let block = lambda.children().find(|c| c.kind() == S::Block)?;
523    let stmts: Vec<_> = block.children().collect(); // a block's child nodes are all statements
524    let [stmt] = stmts.as_slice() else {
525        return None;
526    };
527    // A compound body (`func(): if c: …`) stays multi-line, as does a body that itself spans lines.
528    if matches!(
529        stmt.kind(),
530        S::IfStmt | S::ElifClause | S::ElseClause | S::ForStmt | S::WhileStmt | S::MatchStmt
531    ) {
532        return None;
533    }
534    let bs = first_sig_offset(stmt)?; // skip the body's own leading indentation
535    let be = usize::from(stmt.text_range().end());
536    if src[bs..be].contains('\n') {
537        return None; // body wrapped over lines — leave it
538    }
539    let pre = src[..bs].trim_end_matches([' ', '\t', '\n', '\r']);
540    if !pre.ends_with(':') {
541        return None;
542    }
543    let gap = &src[pre.len()..bs];
544    if !gap.contains('\n') || gap.contains('#') {
545        return None; // already inline, or a comment sits in the gap
546    }
547    // gdformat only inlines a lambda when the *enclosing* statement then fits on one line; otherwise it
548    // keeps the lambda multi-line. Estimate the collapsed length of the enclosing top-level statement
549    // (the ancestor directly inside a `Block`): its content with every whitespace run squeezed to one
550    // space, plus its indentation. (The estimate matches our canonical spacing closely enough to decide.)
551    let mut top = lambda.clone();
552    while let Some(p) = top.parent() {
553        if p.kind() == S::Block {
554            break;
555        }
556        top = p.clone();
557    }
558    let r = top.text_range();
559    let (ts, te) = (usize::from(r.start()), usize::from(r.end()));
560    let indent_cols = src[..ts]
561        .bytes()
562        .rev()
563        .take_while(|&b| b == b'\t' || b == b' ')
564        .map(|b| if b == b'\t' { 4 } else { 1 })
565        .sum::<usize>();
566    let content: usize = src[ts..te].split_whitespace().map(str::len).sum::<usize>()
567        + src[ts..te].split_whitespace().count().saturating_sub(1); // joined by single spaces
568    if indent_cols + content > config.line_width {
569        return None;
570    }
571    Some((pre.len(), bs))
572}
573
574/// Remove redundant grouping parens from standalone-expression positions, matching gdformat's
575/// `remove_outer_parentheses`. A `ParenExpr` is redundant when its parent uses it as a *whole
576/// expression* (a value / condition / iterable / argument / element / nested paren) rather than as an
577/// *operand* of a larger expression — `(a + b) * c` keeps its parens because the parent is the `*`
578/// `BinExpr`. Returns `None` (leave the source) if it does not parse, has no such parens, or the
579/// result is not meaning-equivalent / does not parse (the net catches a precedence-changing strip).
580fn strip_outer_parens(source: &str) -> Option<String> {
581    let parse = gdscript_syntax::parse(source);
582    if !parse.errors().is_empty() {
583        return None;
584    }
585    let mut dels: Vec<usize> = Vec::new(); // byte offsets of `(`/`)` tokens to delete (1 byte each)
586    collect_redundant_parens(&parse.syntax_node(), &mut dels);
587    collect_empty_signal_parens(&parse.syntax_node(), &mut dels);
588    if dels.is_empty() {
589        return None;
590    }
591    dels.sort_unstable_by(|a, b| b.cmp(a)); // delete right-to-left
592    let mut out = source.to_owned();
593    for off in &dels {
594        out.replace_range(*off..=*off, "");
595    }
596    if !meaning_preserved(source, &out) || !gdscript_syntax::parse(&out).errors().is_empty() {
597        return None;
598    }
599    Some(out)
600}
601
602/// Whether a `ParenExpr` child of a node of `parent` is a redundant grouping paren (a standalone
603/// expression), not a precedence-bearing operand.
604fn paren_parent_strips(parent: SyntaxKind) -> bool {
605    use SyntaxKind as S;
606    matches!(
607        parent,
608        S::VarDecl
609            | S::ConstDecl
610            | S::ReturnStmt
611            | S::ForStmt
612            | S::IfStmt
613            | S::ElifClause
614            | S::WhileStmt
615            | S::MatchStmt
616            | S::ExprStmt
617            | S::ArgList
618            | S::ArrayLit
619            | S::DictEntry
620            | S::ParenExpr
621    )
622}
623
624/// Collect the byte offsets of the `(`/`)` tokens of every redundant `ParenExpr`.
625fn collect_redundant_parens(node: &gdscript_syntax::GdNode, dels: &mut Vec<usize>) {
626    use cstree::util::NodeOrToken;
627    for child in node.children() {
628        if child.kind() == SyntaxKind::ParenExpr && paren_parent_strips(node.kind()) {
629            for c in child.children_with_tokens() {
630                let NodeOrToken::Token(t) = c else { continue };
631                if matches!(t.kind(), SyntaxKind::LParen | SyntaxKind::RParen) {
632                    dels.push(usize::from(t.text_range().start()));
633                }
634            }
635        }
636        collect_redundant_parens(child, dels);
637    }
638}
639
640/// Collect the `(`/`)` of a signal's *empty* parameter list — gdformat writes `signal s` not
641/// `signal s()`. (A `func` always keeps its `()`, so this is restricted to signals.)
642fn collect_empty_signal_parens(node: &gdscript_syntax::GdNode, dels: &mut Vec<usize>) {
643    use cstree::util::NodeOrToken;
644    for child in node.children() {
645        let empty_params = (child.kind() == SyntaxKind::SignalDecl)
646            .then(|| child.children().find(|c| c.kind() == SyntaxKind::ParamList))
647            .flatten()
648            .filter(|p| p.children().next().is_none());
649        if let Some(params) = empty_params {
650            for c in params.children_with_tokens() {
651                let NodeOrToken::Token(t) = c else { continue };
652                if matches!(t.kind(), SyntaxKind::LParen | SyntaxKind::RParen) {
653                    dels.push(usize::from(t.text_range().start()));
654                }
655            }
656        }
657        collect_empty_signal_parens(child, dels);
658    }
659}
660
661/// The byte offset of the first significant (non-trivia, non-synthetic) token within `node`.
662fn first_sig_offset(node: &gdscript_syntax::GdNode) -> Option<usize> {
663    use cstree::util::NodeOrToken;
664    for c in node.children_with_tokens() {
665        match c {
666            NodeOrToken::Token(t) => {
667                let k = t.kind();
668                if !k.is_trivia() && !k.is_synthetic_layout() {
669                    return Some(usize::from(t.text_range().start()));
670                }
671            }
672            NodeOrToken::Node(n) => {
673                if let Some(o) = first_sig_offset(n) {
674                    return Some(o);
675                }
676            }
677        }
678    }
679    None
680}
681
682/// Inter-token spacing: how to join two adjacent significant tokens on one logical line.
683#[derive(Clone, Copy, PartialEq, Eq)]
684enum Spacing {
685    /// Tight — no space (`a.b`, `f(`, before `,`/`)`).
686    None,
687    /// Exactly one space (` + `, `, ` after a comma, ` = `).
688    Single,
689    /// Keep the original inter-token whitespace — the ambiguous contexts we refuse to normalize
690    /// (a slice colon `arr[a:b]`). Emits nothing when there was no original whitespace.
691    Verbatim,
692}
693
694/// A value-completing token: a following `+`/`-`/`~` is **binary** (not unary), and a following
695/// `(`/`[` is a **call**/**subscript** (not a grouping paren / array literal).
696fn is_operand_end(k: SyntaxKind) -> bool {
697    use SyntaxKind as S;
698    matches!(
699        k,
700        S::Int
701            | S::Float
702            | S::String
703            | S::StringName
704            | S::NodePath
705            | S::Ident
706            | S::True
707            | S::False
708            | S::Null
709            | S::ConstPi
710            | S::ConstTau
711            | S::ConstInf
712            | S::ConstNan
713            | S::SelfKw
714            | S::SuperKw
715            | S::RParen
716            | S::RBrack
717            | S::RBrace
718    )
719}
720
721fn is_open_bracket(k: SyntaxKind) -> bool {
722    matches!(
723        k,
724        SyntaxKind::LParen | SyntaxKind::LBrack | SyntaxKind::LBrace
725    )
726}
727
728fn is_close_bracket(k: SyntaxKind) -> bool {
729    matches!(
730        k,
731        SyntaxKind::RParen | SyntaxKind::RBrack | SyntaxKind::RBrace
732    )
733}
734
735/// The spacing to insert *before* `cur`, given the previous significant token `prev` on the same
736/// logical line, the innermost open-bracket kind `top`, and whether `prev` was a **unary** prefix
737/// operator. Node-path runs (`$Node/Path`, `%Unique`) are forced tight by the caller and never
738/// reach here, so a bare `Slash`/`Percent` here is always division/modulo.
739fn space_before(
740    prev: SyntaxKind,
741    cur: SyntaxKind,
742    top: Option<SyntaxKind>,
743    prev_unary: bool,
744    top_enum: bool,
745) -> Spacing {
746    use SyntaxKind as S;
747    // An enum body is spaced inside (`{ A, B }`) — unlike a dict (`{"k": v}`) — but an empty `{}`
748    // stays tight. This overrides the bracket-hug rule below for the enum braces only.
749    if top_enum {
750        if prev == S::LBrace && cur != S::RBrace {
751            return Spacing::Single;
752        }
753        if cur == S::RBrace && prev != S::LBrace {
754            return Spacing::Single;
755        }
756    }
757    // --- tight-forcing rules (these carve out every no-space case; the default is one space) ---
758    // Hug the inside of brackets: `(x`, `x)`, `[1`, `1]`, `{k`, `v}`.
759    if is_open_bracket(prev) || is_close_bracket(cur) {
760        return Spacing::None;
761    }
762    // Member access is tight both sides (`.5` / `1.0` are single Float tokens, so a bare `Dot` is
763    // always member access).
764    if cur == S::Dot || prev == S::Dot {
765        return Spacing::None;
766    }
767    // No space before a separator; `@export` is tight after the `@`.
768    if cur == S::Comma || cur == S::Semicolon || prev == S::At {
769        return Spacing::None;
770    }
771    // Tight after a unary prefix operator: `-x`, `+x`, `~x`, `!x`.
772    if prev == S::Tilde || prev == S::Bang || ((prev == S::Minus || prev == S::Plus) && prev_unary)
773    {
774        return Spacing::None;
775    }
776    // Colon: never a space *before* it; *after* it, a dict/type-annotation colon gets one space,
777    // while a slice colon inside `[ ]` is left verbatim (a subscript and a slice are not
778    // distinguishable from local context). `:=` is its own token, handled by the default.
779    if cur == S::Colon {
780        return if top == Some(S::LBrack) {
781            Spacing::Verbatim
782        } else {
783            Spacing::None
784        };
785    }
786    if prev == S::Colon {
787        return if top == Some(S::LBrack) {
788            Spacing::Verbatim
789        } else {
790            Spacing::Single
791        };
792    }
793    // After a separator: one space (`a, b`).
794    if prev == S::Comma || prev == S::Semicolon {
795        return Spacing::Single;
796    }
797    // Open paren: a call hugs an operand callee (`f(`, `a.b(`, `preload(`, `assert(`), and a lambda
798    // header hugs its `func` (`func(x):` — a bare `func(` is always a lambda; a named func has
799    // `func name(`). A grouping paren after a value-keyword keeps its space (`return (x)`, `if (c)`).
800    if cur == S::LParen {
801        return if is_operand_end(prev)
802            || prev == S::PreloadKw
803            || prev == S::AssertKw
804            || prev == S::FuncKw
805        {
806            Spacing::None
807        } else {
808            Spacing::Single
809        };
810    }
811    // Open bracket: a subscript / typed-collection hugs an operand (`arr[i]`, `Array[int]`); an
812    // array literal after an operator/keyword/`,` is spaced.
813    if cur == S::LBrack {
814        return if is_operand_end(prev) {
815            Spacing::None
816        } else {
817            Spacing::Single
818        };
819    }
820    // Everything else — binary/keyword operators, assignments, `->`, `:=`, `{`, atoms, words, and a
821    // unary prefix *before* its operand — takes exactly one space.
822    Spacing::Single
823}
824
825/// Emit a logical-line break: a real `\n` for a content line, or — when collapsing blank lines —
826/// buffer a blank line into `pending_blanks` (flushed, capped, before the next content line).
827fn emit_break(
828    out: &mut String,
829    collapse_on: bool,
830    line_had_content: &mut bool,
831    pending_blanks: &mut usize,
832) {
833    if collapse_on && !*line_had_content {
834        *pending_blanks += 1; // a blank line — capped + flushed when the next content line starts.
835    } else {
836        out.push('\n');
837    }
838    *line_had_content = false;
839}
840
841/// The depth and leading-indentation length of the next code line after token index `idx` (skipping
842/// comment / blank lines), given the current raw depth `cur`. Used to place a block-boundary comment.
843fn next_code_info(toks: &[gdscript_syntax::RawToken], idx: usize, cur: usize) -> (usize, usize) {
844    use SyntaxKind as S;
845    let mut delta: i32 = 0;
846    let mut indent_len = 0usize;
847    for t in &toks[idx + 1..] {
848        match t.kind {
849            S::Indent => delta += 1,
850            S::Dedent => delta -= 1,
851            S::NewlinePhys | S::Newline => indent_len = 0,
852            S::Whitespace => indent_len = usize::from(t.range.len()),
853            k if k.is_trivia() => {} // comments / line-continuation / BOM
854            _ => {
855                let d = usize::try_from(i32::try_from(cur).unwrap_or(0) + delta).unwrap_or(0);
856                return (d, indent_len);
857            }
858        }
859    }
860    (cur, 0)
861}
862
863/// The intended depth of a block-boundary comment, matching gdformat's
864/// `reconstruct_blank_lines_in_range`: a standalone comment joins the deepest block whose line-range
865/// contains it, which is its own authored indentation level *clamped down* to the deepest open block
866/// around it. `comment_levels` is the comment's authored indentation measured in indent units; the
867/// deepest open block is the deeper of the previous code line's depth (`prev_depth`) and the next
868/// code line's (`next_depth`) — the next statement can reveal a block the prepass has not yet opened
869/// with an `Indent` (a comment that is the first line of a body). So an under-indented comment keeps
870/// its own (shallower) level, while an over-indented one snaps to the deepest real block. A column-0
871/// `#` comment is special-cased by the caller (kept at column 0).
872fn comment_depth(comment_levels: usize, prev_depth: usize, next_depth: usize) -> usize {
873    comment_levels.min(prev_depth.max(next_depth))
874}
875
876/// Re-emit the pre-pass token stream with normalized **indentation**, **intra-line spacing** (when
877/// `config.normalize_spacing`), trailing whitespace, and a single final newline. Significant token
878/// *text* is always emitted verbatim (so meaning is preserved); only the whitespace *between*
879/// tokens — and a logical line's leading indentation — is rewritten. Bracketed-continuation
880/// interiors and node-path runs are kept verbatim / tight (see the module docs).
881#[allow(
882    clippy::too_many_lines,
883    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"
884)]
885fn reindent(source: &str, config: &FmtConfig) -> String {
886    let raw = gdscript_syntax::tokenize(source);
887    let (toks, _diags) = gdscript_syntax::run_prepass(&raw, source);
888    let unit = config.indent_unit();
889    let spacing_on = config.normalize_spacing;
890    let collapse_on = config.collapse_blank_lines;
891
892    let mut out = String::with_capacity(source.len() + 16);
893    let mut depth: usize = 0;
894    // --- blank-line state (only used when `collapse_on`) ---
895    // Blank lines are buffered, not emitted, until the next content line: we then flush at most
896    // `cap` of them (2 at top level, 1 nested) — knowing the *next* line's depth, since the prepass
897    // emits the `Dedent` between a block and a following top-level line AFTER the blank lines.
898    let mut pending_blanks: usize = 0;
899    // Whether the current logical line emitted any content (a significant token or a comment) — a
900    // line that did not is a blank line.
901    let mut line_had_content = false;
902    // Whether any content has been emitted yet (leading blank lines are stripped entirely).
903    let mut seen_content = false;
904    // `true` at the start of a logical line, before its first significant token (when we re-emit
905    // the indentation, `depth` being final by then).
906    let mut line_start = true;
907    // A synthetic `Newline` precedes the `NewlinePhys` carrying the line's bytes; this swallows that
908    // one `NewlinePhys` so the break is emitted once. See the original notes below.
909    let mut just_broke = false;
910    // Innermost-first stack of open-bracket kinds: `.len()` is the old `bracket_depth` (drives the
911    // continuation logic), `.last()` is the colon-context discriminator for spacing.
912    let mut stack: Vec<SyntaxKind> = Vec::new();
913    // Parallel to the `{` entries in `stack`: whether each brace is an **enum body** (spaced inside,
914    // `{ A, B }`) rather than a dict (tight, `{"k": v}`). `pending_enum` is armed by an `enum` keyword
915    // and consumed by the brace it opens.
916    let mut brace_is_enum: Vec<bool> = Vec::new();
917    let mut pending_enum = false;
918    // --- intra-line spacing state (all reset at a logical-line break) ---
919    let mut prev_sig: Option<SyntaxKind> = None;
920    let mut prev_unary = false;
921    let mut node_path = false;
922    // Original inter-token whitespace, buffered so the next token can keep it (`Verbatim`) or drop
923    // it (and re-synthesize). Only used when `spacing_on`.
924    let mut pending_ws: Option<&str> = None;
925    // Set after a physical newline *inside* brackets: the next line's leading whitespace (alignment)
926    // is kept verbatim — increment A does not reflow bracketed continuations.
927    let mut cont_line_start = false;
928    // The leading whitespace of the current logical line (used to recover a *comment*'s authored
929    // indentation, since the prepass attributes a block-boundary comment to the wrong raw `depth`).
930    let mut line_indent_ws: Option<&str> = None;
931    // The depth of the most recent *code* line, and whether the most recent content line was a
932    // comment. Used to detect the start of a block: a code statement deeper than the previous code
933    // line, with no comment in between, is a suite's first statement — gdformat strips a blank between
934    // a compound header and its body. A comment is the block's "first content" (its trailing blanks
935    // are kept), so an intervening comment suppresses the strip.
936    let mut prev_content_depth: usize = 0;
937    let mut prev_was_comment = false;
938
939    for (idx, t) in toks.iter().enumerate() {
940        let text = &source[t.range];
941        match t.kind {
942            SyntaxKind::Indent => depth += 1,
943            SyntaxKind::Dedent => depth = depth.saturating_sub(1),
944            // A synthetic line break: ends the logical line; the next one is re-indented. A synthetic
945            // `Newline` only follows a statement, so it always terminates a content line.
946            SyntaxKind::Newline => {
947                trim_trailing_inline_ws(&mut out);
948                if stack.is_empty() {
949                    // A normal logical-line break: the next line is re-indented from `depth`.
950                    emit_break(
951                        &mut out,
952                        collapse_on,
953                        &mut line_had_content,
954                        &mut pending_blanks,
955                    );
956                    line_start = true;
957                    prev_sig = None;
958                } else {
959                    // A synthetic break *inside brackets* is a multi-line lambda-body line break:
960                    // the prepass suppresses synthetic layout inside brackets EXCEPT for a lambda
961                    // body, for which it re-emits `Newline`/`Indent`/`Dedent`. Treat it like a
962                    // `NewlinePhys` continuation and keep the bracketed interior verbatim — do NOT
963                    // re-indent the body from `depth`. The body's verbatim-aligned *header* arrives
964                    // via `NewlinePhys` (kept verbatim), so depth-re-indenting only the body makes
965                    // the two disagree and produces non-parsing output. Canonical reflow of a
966                    // bracketed lambda block is the increment-C reflow's job, not the indenter's.
967                    out.push('\n');
968                    cont_line_start = true;
969                }
970                just_broke = true;
971                node_path = false;
972                pending_ws = None;
973                line_indent_ws = None;
974                pending_enum = false; // an `enum` keyword is consumed by its `{` within one line
975            }
976            SyntaxKind::NewlinePhys => {
977                if just_broke {
978                    just_broke = false; // its bytes belong to the synthetic break already emitted
979                } else {
980                    trim_trailing_inline_ws(&mut out);
981                    node_path = false;
982                    pending_ws = None;
983                    // Outside brackets this ends a comment-only line (content) or a blank line, and
984                    // the next line is re-indented. Inside brackets it is a real continuation — keep
985                    // the next line's alignment verbatim and never collapse it.
986                    if stack.is_empty() {
987                        emit_break(
988                            &mut out,
989                            collapse_on,
990                            &mut line_had_content,
991                            &mut pending_blanks,
992                        );
993                        line_start = true;
994                        prev_sig = None;
995                        line_indent_ws = None;
996                    } else {
997                        out.push('\n');
998                        cont_line_start = true;
999                    }
1000                }
1001            }
1002            SyntaxKind::Whitespace => {
1003                if line_start {
1004                    // Leading indentation — dropped; re-synthesized at the first significant token.
1005                    // Remember it: a *comment*'s authored indentation is recovered from it below.
1006                    line_indent_ws = Some(text);
1007                } else if spacing_on {
1008                    pending_ws = Some(text); // buffered; the next token decides whether to keep it.
1009                } else {
1010                    out.push_str(text); // indentation-only mode: original verbatim behavior.
1011                }
1012            }
1013            // A significant token or a comment.
1014            _ => {
1015                if line_start {
1016                    // A block-boundary *comment* is attributed by the prepass to the wrong raw
1017                    // `depth` (the `Indent`/`Dedent` lands on the next *code* line, not the comment).
1018                    // Recover its intended depth the way gdformat does: its authored indentation level
1019                    // clamped down to the deepest open block (`min(authored, max(prev, next))`).
1020                    let is_comment = matches!(
1021                        t.kind,
1022                        SyntaxKind::LineComment
1023                            | SyntaxKind::DocComment
1024                            | SyntaxKind::RegionComment
1025                            | SyntaxKind::EndRegionComment
1026                    );
1027                    let comment_len = line_indent_ws.map_or(0, str::len);
1028                    let emit_depth = if is_comment {
1029                        if comment_len == 0 {
1030                            // gdformat keeps a column-0 comment (`line.startswith("#")`) at column 0 —
1031                            // e.g. file-spanning `#region`/`#endregion` markers — rather than indenting
1032                            // it to the enclosing block.
1033                            0
1034                        } else {
1035                            let (next_depth, _) = next_code_info(&toks, idx, depth);
1036                            let unit_len = unit.len().max(1);
1037                            comment_depth(comment_len / unit_len, depth, next_depth)
1038                        }
1039                    } else {
1040                        depth
1041                    };
1042                    // Flush the buffered blank lines. gdformat squeezes *every* run of blank lines to a
1043                    // single blank, then re-inserts the 2nd (top-level) / 1st (nested) blank only around
1044                    // definitions (done later by `insert_def_blanks`). So the cap here is 1 everywhere,
1045                    // 0 before the first content (leading blanks gone), and 0 at the **start of a block**
1046                    // (a suite's first statement directly under its header — gdformat's
1047                    // `_remove_empty_strings_from_begin` strips a blank between a header and its body).
1048                    // The strip fires only for a *code* statement deeper than the previous code line
1049                    // with **no comment in between**: a comment is the block's first content, so its
1050                    // trailing blanks are kept (and a column-0 comment, whose raw `depth` lags, never
1051                    // looks like a header). This mirrors gdformat's `previous_statement_name is None`.
1052                    if collapse_on {
1053                        let at_block_start =
1054                            !prev_was_comment && !is_comment && depth > prev_content_depth;
1055                        let cap = if at_block_start {
1056                            0
1057                        } else {
1058                            usize::from(seen_content)
1059                        };
1060                        for _ in 0..pending_blanks.min(cap) {
1061                            out.push('\n');
1062                        }
1063                        pending_blanks = 0;
1064                    }
1065                    prev_was_comment = is_comment;
1066                    if !is_comment {
1067                        prev_content_depth = depth;
1068                    }
1069                    for _ in 0..emit_depth {
1070                        out.push_str(&unit);
1071                    }
1072                    line_start = false;
1073                    cont_line_start = false;
1074                } else if spacing_on {
1075                    if cont_line_start {
1076                        // First token of a bracketed continuation: keep its leading alignment.
1077                        if let Some(ws) = pending_ws {
1078                            out.push_str(ws);
1079                        }
1080                        cont_line_start = false;
1081                    } else if matches!(
1082                        t.kind,
1083                        SyntaxKind::LineComment
1084                            | SyntaxKind::DocComment
1085                            | SyntaxKind::RegionComment
1086                            | SyntaxKind::EndRegionComment
1087                    ) {
1088                        // An inline (trailing) comment is offset by exactly two spaces (gdformat's
1089                        // `INLINE_COMMENT_OFFSET`), regardless of the original spacing.
1090                        out.push_str("  ");
1091                        node_path = false;
1092                    } else if t.kind.is_trivia() {
1093                        // A line-continuation / BOM: keep the original spacing before it.
1094                        if let Some(ws) = pending_ws {
1095                            out.push_str(ws);
1096                        }
1097                        node_path = false;
1098                    } else {
1099                        // A significant token: synthesize the spacing. Inside a node-path run we
1100                        // keep the *original* spacing verbatim — a real `$Node/Path` is already
1101                        // tight (so it stays tight), and we must never *collapse* a genuinely-spaced
1102                        // `$A / b` (a division) into a node path, which would silently change meaning
1103                        // with an identical token sequence (the safety net cannot catch it).
1104                        let path_continue = node_path
1105                            && (matches!(
1106                                t.kind,
1107                                SyntaxKind::Ident | SyntaxKind::Slash | SyntaxKind::String
1108                            ) || (t.kind == SyntaxKind::Percent
1109                                && matches!(
1110                                    prev_sig,
1111                                    Some(SyntaxKind::Dollar | SyntaxKind::Slash)
1112                                )));
1113                        let spacing = if path_continue {
1114                            // A `%` in sigil position (`$%Unique`, `$A/%Unique`) continues the path; a
1115                            // `%` after an identifier is modulo and falls through to `space_before`.
1116                            Spacing::Verbatim
1117                        } else {
1118                            node_path = false; // any non-path token ends the run.
1119                            match prev_sig {
1120                                Some(p) => {
1121                                    let top_enum = stack.last() == Some(&SyntaxKind::LBrace)
1122                                        && brace_is_enum.last() == Some(&true);
1123                                    space_before(
1124                                        p,
1125                                        t.kind,
1126                                        stack.last().copied(),
1127                                        prev_unary,
1128                                        top_enum,
1129                                    )
1130                                }
1131                                None => Spacing::None,
1132                            }
1133                        };
1134                        match spacing {
1135                            Spacing::None => {}
1136                            Spacing::Single => out.push(' '),
1137                            Spacing::Verbatim => {
1138                                if let Some(ws) = pending_ws {
1139                                    out.push_str(ws);
1140                                }
1141                            }
1142                        }
1143                    }
1144                }
1145                pending_ws = None;
1146                just_broke = false;
1147                // String literals are normalized to gdformat's canonical quote style (value-
1148                // preserving; guarded by the meaning-equivalence net). Everything else is verbatim.
1149                if config.normalize_strings
1150                    && matches!(
1151                        t.kind,
1152                        SyntaxKind::String | SyntaxKind::StringName | SyntaxKind::NodePath
1153                    )
1154                {
1155                    out.push_str(&canonical_string(text));
1156                } else {
1157                    out.push_str(text);
1158                }
1159                // Any token reaching here (a significant token or a comment) is line content.
1160                line_had_content = true;
1161                seen_content = true;
1162                match t.kind {
1163                    SyntaxKind::LBrace => {
1164                        stack.push(t.kind);
1165                        brace_is_enum.push(pending_enum);
1166                        pending_enum = false;
1167                    }
1168                    SyntaxKind::LParen | SyntaxKind::LBrack => {
1169                        stack.push(t.kind);
1170                    }
1171                    SyntaxKind::RBrace => {
1172                        stack.pop();
1173                        brace_is_enum.pop();
1174                    }
1175                    SyntaxKind::RParen | SyntaxKind::RBrack => {
1176                        stack.pop();
1177                    }
1178                    _ => {}
1179                }
1180                // Spacing state — significant tokens only (a comment is never an operand).
1181                if !t.kind.is_trivia() {
1182                    if t.kind == SyntaxKind::EnumKw {
1183                        pending_enum = true; // the next `{` opens an enum body
1184                    }
1185                    let unary_ctx = prev_sig.is_none_or(|p| !is_operand_end(p));
1186                    // Enter node-path mode after a `$` (always) or a `%` in sigil position (a `%`
1187                    // after an operand is modulo, and stays a normal binary operator).
1188                    if t.kind == SyntaxKind::Dollar || (t.kind == SyntaxKind::Percent && unary_ctx)
1189                    {
1190                        node_path = true;
1191                    }
1192                    prev_unary = match t.kind {
1193                        SyntaxKind::Minus | SyntaxKind::Plus => unary_ctx,
1194                        SyntaxKind::Tilde | SyntaxKind::Bang => true,
1195                        _ => false,
1196                    };
1197                    // A soft keyword (`match`/`when`) used as a *member* (`obj.match`) is an ordinary
1198                    // identifier operand — record it as `Ident` so a following `(` hugs it
1199                    // (`obj.match(x)`), while a leading `match (x):` statement keeps its space.
1200                    let member_soft_kw = matches!(t.kind, SyntaxKind::MatchKw | SyntaxKind::WhenKw)
1201                        && prev_sig == Some(SyntaxKind::Dot);
1202                    prev_sig = Some(if member_soft_kw {
1203                        SyntaxKind::Ident
1204                    } else {
1205                        t.kind
1206                    });
1207                }
1208            }
1209        }
1210    }
1211    // Trim a trailing blank/whitespace run and guarantee exactly one final newline.
1212    let trimmed = out.trim_end();
1213    let mut result = String::with_capacity(trimmed.len() + 1);
1214    result.push_str(trimmed);
1215    if !result.is_empty() {
1216        result.push('\n');
1217    }
1218    result
1219}
1220
1221/// The role a statement (logical) line plays in the blank-line-insertion policy.
1222#[derive(Clone, Copy, PartialEq, Eq, Debug)]
1223enum LineRole {
1224    /// A `func` / `static func` / `class` declaration — gdformat surrounds these with blank lines.
1225    Def,
1226    /// A standalone comment line (`#` / `##`) — attaches to a following statement.
1227    Comment,
1228    /// A standalone annotation line (`@foo` with nothing else on the line) — attaches to a following
1229    /// statement.
1230    Annotation,
1231    /// Any other statement (`var` / `const` / `signal` / `class_name` / an expression / …).
1232    Other,
1233}
1234
1235/// A statement-level "head" line discovered in already-formatted text: its 0-based physical line
1236/// number, its block depth, and its [`LineRole`]. Bracket-continuation and lambda-body lines are
1237/// not heads.
1238#[derive(Clone, Copy)]
1239struct HeadLine {
1240    line: usize,
1241    depth: usize,
1242    role: LineRole,
1243}
1244
1245/// One logical unit for the blank-line edge rule: a definition (or other statement) together with
1246/// any comment/annotation prefix that attaches to it. `head_line` is the unit's first physical line.
1247#[derive(Clone, Copy)]
1248struct Unit {
1249    head_line: usize,
1250    depth: usize,
1251    is_def: bool,
1252    /// A *standalone* comment (not a prefix that attaches to a following def/statement). gdformat does
1253    /// not force blank lines around such a comment (e.g. a trailing `#endregion`), and it is
1254    /// transparent to the surrounding defs' blank-line spacing.
1255    is_comment: bool,
1256    /// The unit is a definition reached through an **annotation** prefix (`@rpc func …`). gdformat
1257    /// forces blanks before such a unit only when the *previous* sibling was itself a def — not on the
1258    /// unit's own def-ness (the annotation line, not the def, owns the leading blanks, and an
1259    /// annotation is absent from the surrounding-empty-lines table). A *comment*-prefixed def forces
1260    /// like a plain def.
1261    ann_prefixed: bool,
1262}
1263
1264/// Classify the statement line whose first significant-or-comment token is `toks[start]`. Scans the
1265/// rest of the logical line (across bracketed continuations) to look past an annotation prefix.
1266fn classify_line(toks: &[gdscript_syntax::RawToken], start: usize) -> LineRole {
1267    use SyntaxKind as S;
1268    if matches!(
1269        toks[start].kind,
1270        S::LineComment | S::DocComment | S::RegionComment | S::EndRegionComment
1271    ) {
1272        return LineRole::Comment;
1273    }
1274    // Collect the line's significant token kinds (skip trivia; stop at the logical line end).
1275    let mut kinds: Vec<S> = Vec::new();
1276    let mut local_stack = 0usize;
1277    for t in &toks[start..] {
1278        match t.kind {
1279            S::Newline => break,
1280            S::NewlinePhys if local_stack == 0 => break,
1281            S::LParen | S::LBrack | S::LBrace => {
1282                local_stack += 1;
1283                kinds.push(t.kind);
1284            }
1285            S::RParen | S::RBrack | S::RBrace => {
1286                local_stack = local_stack.saturating_sub(1);
1287                kinds.push(t.kind);
1288            }
1289            k if k.is_trivia() || k.is_synthetic_layout() => {}
1290            k => kinds.push(k),
1291        }
1292    }
1293    // Skip a leading annotation prefix: `@ Ident (balanced parens)?`, repeated.
1294    let mut i = 0;
1295    while kinds.get(i) == Some(&S::At) {
1296        i += 1; // `@`
1297        if kinds.get(i) == Some(&S::Ident) {
1298            i += 1; // annotation name
1299        }
1300        if kinds.get(i) == Some(&S::LParen) {
1301            // skip the balanced argument list
1302            let mut d = 0usize;
1303            while let Some(&k) = kinds.get(i) {
1304                i += 1;
1305                match k {
1306                    S::LParen => d += 1,
1307                    S::RParen => {
1308                        d -= 1;
1309                        if d == 0 {
1310                            break;
1311                        }
1312                    }
1313                    _ => {}
1314                }
1315            }
1316        }
1317    }
1318    match kinds.get(i) {
1319        None => LineRole::Annotation, // an annotation alone on its line
1320        Some(S::FuncKw | S::ClassKw) => LineRole::Def,
1321        Some(S::StaticKw) if kinds.get(i + 1) == Some(&S::FuncKw) => LineRole::Def,
1322        _ => LineRole::Other,
1323    }
1324}
1325
1326/// Insert blank lines around definitions to match gdformat's policy (2 around top-level defs, 1
1327/// around nested defs), operating on already-formatted, already-collapsed text. Purely additive —
1328/// it only inserts blank lines, so it never changes the significant token sequence. Idempotent: the
1329/// blanks it would add are already present on a second pass.
1330#[allow(
1331    clippy::too_many_lines,
1332    reason = "three cohesive sequential passes (find heads, group units, apply the edge rule) over the same token stream; clearer kept together than split"
1333)]
1334fn insert_def_blanks(formatted: &str, config: &FmtConfig) -> String {
1335    use SyntaxKind as S;
1336    let raw = gdscript_syntax::tokenize(formatted);
1337    let (toks, _diags) = gdscript_syntax::run_prepass(&raw, formatted);
1338
1339    let lines: Vec<&str> = formatted.lines().collect();
1340    let is_blank: Vec<bool> = lines.iter().map(|l| l.trim().is_empty()).collect();
1341    let blank_above = |l: usize| l > 0 && is_blank.get(l - 1).copied().unwrap_or(false);
1342
1343    // Byte offset -> 0-based physical line number. Built from the raw bytes (so newlines *inside*
1344    // multi-line string tokens count too) — `formatted.lines()` indices must align with this.
1345    let line_starts: Vec<usize> = std::iter::once(0)
1346        .chain(
1347            formatted
1348                .bytes()
1349                .enumerate()
1350                .filter_map(|(i, b)| (b == b'\n').then_some(i + 1)),
1351        )
1352        .collect();
1353    let line_of = |offset: usize| line_starts.partition_point(|&s| s <= offset) - 1;
1354    // The (already-normalized) leading-indentation depth of a formatted line.
1355    let line_depth = |l: usize| -> usize {
1356        let s = lines.get(l).copied().unwrap_or("");
1357        if config.use_tabs {
1358            s.bytes().take_while(|&b| b == b'\t').count()
1359        } else {
1360            s.bytes()
1361                .take_while(|&b| b == b' ')
1362                .count()
1363                .checked_div(config.indent_size)
1364                .unwrap_or(0)
1365        }
1366    };
1367
1368    // --- pass 1: find statement-level head lines (skip bracket / lambda continuations) ---
1369    let mut heads: Vec<HeadLine> = Vec::new();
1370    let mut depth = 0usize;
1371    let mut stack = 0usize;
1372    let mut this_line_continues = false; // a trailing `\` continues onto the next physical line
1373    let mut next_line_is_cont = false; // the logical line we are about to start is a continuation
1374    let mut seen_first_on_line = false;
1375    for (idx, t) in toks.iter().enumerate() {
1376        match t.kind {
1377            S::Indent => depth += 1,
1378            S::Dedent => depth = depth.saturating_sub(1),
1379            S::NewlinePhys => {
1380                next_line_is_cont = stack > 0 || this_line_continues;
1381                this_line_continues = false;
1382                seen_first_on_line = false;
1383            }
1384            S::LineContinuation => this_line_continues = true,
1385            S::Newline | S::Whitespace | S::Bom => {}
1386            _ => {
1387                if !seen_first_on_line {
1388                    seen_first_on_line = true;
1389                    if stack == 0 && !next_line_is_cont {
1390                        let line = line_of(usize::from(t.range.start()));
1391                        let role = classify_line(&toks, idx);
1392                        // A comment's *structural* block (which one it belongs to, for the edge
1393                        // rule) is not its visual indentation: a comment whose *next* code is deeper
1394                        // than the comment's own column belongs to that deeper block — even a column-0
1395                        // comment sitting amid a function body (commented-out code) is inside the
1396                        // function, not a sibling of the preceding def. Otherwise it sits at its own
1397                        // (visual) depth, which correctly attaches a column-0 doc-comment to a
1398                        // following *dedented* def (whose next code is at column 0, not deeper).
1399                        let head_depth = if role == LineRole::Comment {
1400                            let visual = line_depth(line);
1401                            let next_depth = next_code_info(&toks, idx, depth).0;
1402                            if next_depth > visual {
1403                                next_depth
1404                            } else {
1405                                visual
1406                            }
1407                        } else {
1408                            depth
1409                        };
1410                        heads.push(HeadLine {
1411                            line,
1412                            depth: head_depth,
1413                            role,
1414                        });
1415                    }
1416                }
1417                match t.kind {
1418                    S::LParen | S::LBrack | S::LBrace => stack += 1,
1419                    S::RParen | S::RBrack | S::RBrace => stack = stack.saturating_sub(1),
1420                    _ => {}
1421                }
1422            }
1423        }
1424    }
1425
1426    // --- pass 2: group heads into units (a def/statement + its attached comment/annotation prefix) ---
1427    let mut units: Vec<Unit> = Vec::new();
1428    let mut i = 0;
1429    while i < heads.len() {
1430        let h = heads[i];
1431        if matches!(h.role, LineRole::Comment | LineRole::Annotation) {
1432            // Accumulate a contiguous, same-depth prefix run of comments/annotations.
1433            let mut j = i + 1;
1434            while j < heads.len()
1435                && matches!(heads[j].role, LineRole::Comment | LineRole::Annotation)
1436                && heads[j].depth == h.depth
1437                && !blank_above(heads[j].line)
1438            {
1439                j += 1;
1440            }
1441            // Does a same-depth statement follow contiguously (no blank gap)? Then the run attaches.
1442            if j < heads.len()
1443                && heads[j].depth == h.depth
1444                && !blank_above(heads[j].line)
1445                && matches!(heads[j].role, LineRole::Def | LineRole::Other)
1446            {
1447                units.push(Unit {
1448                    head_line: h.line,
1449                    depth: h.depth,
1450                    is_def: heads[j].role == LineRole::Def,
1451                    is_comment: false,
1452                    ann_prefixed: h.role == LineRole::Annotation,
1453                });
1454                i = j + 1;
1455            } else {
1456                // Standalone comment/annotation lines (each its own non-def unit).
1457                for h in &heads[i..j] {
1458                    units.push(Unit {
1459                        head_line: h.line,
1460                        depth: h.depth,
1461                        is_def: false,
1462                        is_comment: h.role == LineRole::Comment,
1463                        ann_prefixed: false,
1464                    });
1465                }
1466                i = j;
1467            }
1468        } else {
1469            units.push(Unit {
1470                head_line: h.line,
1471                depth: h.depth,
1472                is_def: h.role == LineRole::Def,
1473                is_comment: false,
1474                ann_prefixed: false,
1475            });
1476            i += 1;
1477        }
1478    }
1479
1480    // --- pass 3: the edge rule — N blanks before a unit whose own or previous sibling is a def ---
1481    let mut required: std::collections::HashMap<usize, usize> = std::collections::HashMap::new();
1482    let mut last_is_def: Vec<Option<bool>> = Vec::new();
1483    for (k, u) in units.iter().enumerate() {
1484        while last_is_def.len() <= u.depth {
1485            last_is_def.push(None);
1486        }
1487        let prev = last_is_def[u.depth];
1488        let first_in_block = prev.is_none();
1489        let n = if u.depth == 0 { 2 } else { 1 };
1490        // A *trailing* standalone comment — one that ends its block (no following unit at its depth or
1491        // shallower-after) such as a closing `#endregion` — keeps its source blanks unforced, matching
1492        // gdformat's end-of-block reconstruction. A standalone comment *between* statements still
1493        // resets def-adjacency (a following statement's spacing is measured from it, not the def above).
1494        let trailing_comment = u.is_comment && units.get(k + 1).is_none_or(|nx| nx.depth < u.depth);
1495        // An annotation-prefixed def forces blanks only after a previous def; a plain or
1496        // comment-prefixed def also forces on its own def-ness.
1497        let needs = if u.ann_prefixed {
1498            prev == Some(true)
1499        } else {
1500            u.is_def || prev == Some(true)
1501        };
1502        if !trailing_comment && !first_in_block && needs {
1503            required.insert(u.head_line, n);
1504        }
1505        last_is_def[u.depth] = Some(u.is_def);
1506        last_is_def.truncate(u.depth + 1); // entering a deeper block starts its siblings fresh
1507    }
1508    if required.is_empty() {
1509        return formatted.to_owned();
1510    }
1511
1512    // --- rebuild: top up the blank run before each required head to exactly N ---
1513    let mut out = String::with_capacity(formatted.len() + required.len() * 2);
1514    let mut trailing_blanks = 0usize;
1515    for (lno, content) in lines.iter().enumerate() {
1516        if let Some(&k) = required.get(&lno) {
1517            for _ in trailing_blanks..k {
1518                out.push('\n');
1519            }
1520        }
1521        out.push_str(content);
1522        out.push('\n');
1523        trailing_blanks = if is_blank[lno] {
1524            trailing_blanks + 1
1525        } else {
1526            0
1527        };
1528    }
1529    out
1530}
1531
1532// ===================== line reflow (length-driven wrapping) =====================
1533//
1534// A statement that does not fit in `line_width` and contains a bracketed group is wrapped the way
1535// gdformat does: try the group **flat**; if it does not fit, **compact** (all elements on one
1536// indented continuation line, close bracket on its own line); if even that does not fit, **exploded**
1537// (one element per line, recursively). A **magic trailing comma** in the source forces a group
1538// exploded-with-comma even when it would fit (and forces every enclosing group multi-line) — the one
1539// case that mutates the token sequence (a trailing comma), guarded by the meaning-equivalence net.
1540// Only statements that occupy a single physical line are reflowed (so the pass is trivially
1541// idempotent — a wrapped statement spans bracket-continuation lines that are skipped on the next run).
1542
1543/// One rendered token: its kind, text, and whether a single space precedes it in the flat form.
1544struct Atom {
1545    kind: SyntaxKind,
1546    text: String,
1547    space: bool,
1548}
1549
1550/// A reflow document node: a text run, or a bracketed group of comma-separated element sequences.
1551enum ReflowDoc {
1552    Text {
1553        text: String,
1554        space: bool,
1555    },
1556    Group {
1557        space: bool,
1558        open: String,
1559        elems: Vec<Vec<ReflowDoc>>,
1560        close: &'static str,
1561        /// The source had a **magic trailing comma** — gdformat forces this group exploded one per
1562        /// line *with* the trailing comma, even when it would fit.
1563        magic: bool,
1564    },
1565}
1566
1567/// Whether a group must be broken across lines: it has a magic trailing comma, or a descendant does
1568/// (a magic comma anywhere forces every enclosing group multi-line, matching gdformat).
1569fn group_forced(elems: &[Vec<ReflowDoc>], magic: bool) -> bool {
1570    magic
1571        || elems.iter().any(|e| {
1572            e.iter().any(|d| match d {
1573                ReflowDoc::Group { elems, magic, .. } => group_forced(elems, *magic),
1574                ReflowDoc::Text { .. } => false,
1575            })
1576        })
1577}
1578
1579fn open_close(text: &str) -> Option<&'static str> {
1580    match text {
1581        "(" => Some(")"),
1582        "[" => Some("]"),
1583        "{" => Some("}"),
1584        _ => None,
1585    }
1586}
1587
1588/// Display columns of a content string (no tabs — those only appear in leading indentation).
1589fn cols(s: &str) -> usize {
1590    s.chars().count()
1591}
1592
1593/// Display width of a full line, counting a leading tab as `tw` columns.
1594fn display_cols(line: &str, tw: usize) -> usize {
1595    line.chars().map(|c| if c == '\t' { tw } else { 1 }).sum()
1596}
1597
1598/// Build the comma-separated element sequences of a group whose contents start at `atoms[*i]`,
1599/// consuming through the matching `close` (or to the end when `close` is `None`, for the top level).
1600/// Returns the elements and whether the group ended with a magic trailing comma.
1601fn build_elems(atoms: &[Atom], i: &mut usize, close: Option<&str>) -> (Vec<Vec<ReflowDoc>>, bool) {
1602    let mut elems: Vec<Vec<ReflowDoc>> = Vec::new();
1603    let mut cur: Vec<ReflowDoc> = Vec::new();
1604    let mut elem_start = true;
1605    while *i < atoms.len() {
1606        let a = &atoms[*i];
1607        if close == Some(a.text.as_str()) {
1608            *i += 1;
1609            break;
1610        }
1611        if a.text == "," {
1612            elems.push(std::mem::take(&mut cur));
1613            elem_start = true;
1614            *i += 1;
1615            continue;
1616        }
1617        let space = !elem_start && a.space;
1618        if let Some(cl) = open_close(&a.text) {
1619            let open = a.text.clone();
1620            *i += 1;
1621            let (inner, magic) = build_elems(atoms, i, Some(cl));
1622            cur.push(ReflowDoc::Group {
1623                space,
1624                open,
1625                elems: inner,
1626                close: cl,
1627                magic,
1628            });
1629        } else {
1630            cur.push(ReflowDoc::Text {
1631                text: a.text.clone(),
1632                space,
1633            });
1634            *i += 1;
1635        }
1636        elem_start = false;
1637    }
1638    // A trailing comma leaves `elem_start` set with `cur` empty after at least one real element.
1639    let magic = elem_start && !elems.is_empty();
1640    if !cur.is_empty() {
1641        elems.push(cur);
1642    }
1643    (elems, magic)
1644}
1645
1646fn flat_seq(docs: &[ReflowDoc]) -> String {
1647    let mut out = String::new();
1648    for d in docs {
1649        match d {
1650            ReflowDoc::Text { text, space } => {
1651                if *space {
1652                    out.push(' ');
1653                }
1654                out.push_str(text);
1655            }
1656            ReflowDoc::Group {
1657                space,
1658                open,
1659                elems,
1660                close,
1661                magic,
1662            } => {
1663                if *space {
1664                    out.push(' ');
1665                }
1666                out.push_str(open);
1667                out.push_str(&flat_group_contents(elems, *magic));
1668                out.push_str(close);
1669            }
1670        }
1671    }
1672    out
1673}
1674
1675fn flat_group_contents(elems: &[Vec<ReflowDoc>], magic: bool) -> String {
1676    let mut s = elems
1677        .iter()
1678        .map(|e| flat_seq(e))
1679        .collect::<Vec<_>>()
1680        .join(", ");
1681    if magic && !elems.is_empty() {
1682        s.push(',');
1683    }
1684    s
1685}
1686
1687/// Render a sequence (one element) at base `indent`, starting at column `col`, wrapping any group
1688/// that does not fit. Returns the text and the ending column.
1689fn render_seq(
1690    docs: &[ReflowDoc],
1691    indent: usize,
1692    mut col: usize,
1693    cfg: &FmtConfig,
1694    tw: usize,
1695    unit: &str,
1696) -> (String, usize) {
1697    let mut out = String::new();
1698    // Flat width of each doc, so a group knows the width of the content that *follows* it on the line
1699    // (its "tail") — a group must wrap when the group + its tail overflow, even if the group alone fits.
1700    let widths: Vec<usize> = docs
1701        .iter()
1702        .map(|d| cols(&flat_seq(std::slice::from_ref(d))))
1703        .collect();
1704    let total: usize = widths.iter().sum();
1705    // gdformat wraps the *outermost* (last) bracket group — except a `func`/`static func` definition,
1706    // whose **parameter list** (the first group) is the one that wraps, not a `-> Array[T]` return
1707    // type. An earlier group otherwise stays flat unless it is itself forced (a magic comma).
1708    let first_text = match docs.first() {
1709        Some(ReflowDoc::Text { text, .. }) => Some(text.as_str()),
1710        _ => None,
1711    };
1712    let is_def = first_text == Some("func")
1713        || (first_text == Some("static")
1714            && matches!(docs.get(1), Some(ReflowDoc::Text { text, .. }) if text == "func"));
1715    let wrap_target = if is_def {
1716        docs.iter()
1717            .position(|d| matches!(d, ReflowDoc::Group { .. }))
1718    } else {
1719        docs.iter()
1720            .rposition(|d| matches!(d, ReflowDoc::Group { .. }))
1721    };
1722    let mut before = 0usize;
1723    for (i, d) in docs.iter().enumerate() {
1724        let tail = total - before - widths[i];
1725        match d {
1726            ReflowDoc::Text { text, space } => {
1727                if *space {
1728                    out.push(' ');
1729                    col += 1;
1730                }
1731                out.push_str(text);
1732                col += cols(text);
1733            }
1734            ReflowDoc::Group {
1735                space,
1736                open,
1737                elems,
1738                close,
1739                magic,
1740            } => {
1741                if *space {
1742                    out.push(' ');
1743                    col += 1;
1744                }
1745                let (g, end) = if Some(i) == wrap_target || group_forced(elems, *magic) {
1746                    render_group(open, elems, close, *magic, indent, col, tail, cfg, tw, unit)
1747                } else {
1748                    let flat = format!("{open}{}{close}", flat_group_contents(elems, *magic));
1749                    let end = col + cols(&flat);
1750                    (flat, end)
1751                };
1752                out.push_str(&g);
1753                col = end;
1754            }
1755        }
1756        before += widths[i];
1757    }
1758    (out, col)
1759}
1760
1761#[allow(
1762    clippy::too_many_arguments,
1763    reason = "a focused internal renderer threading layout state"
1764)]
1765fn render_group(
1766    open: &str,
1767    elems: &[Vec<ReflowDoc>],
1768    close: &str,
1769    magic: bool,
1770    indent: usize,
1771    col: usize,
1772    tail: usize,
1773    cfg: &FmtConfig,
1774    tw: usize,
1775    unit: &str,
1776) -> (String, usize) {
1777    let forced = group_forced(elems, magic);
1778    // flat: the whole group on the current line (only when it — plus the content that follows it on
1779    // the line — fits, and it is not forced multi-line).
1780    let flat = format!("{open}{}{close}", flat_group_contents(elems, magic));
1781    if !forced && (col + cols(&flat) + tail <= cfg.line_width || elems.is_empty()) {
1782        let end = col + cols(&flat);
1783        return (flat, end);
1784    }
1785    let inner = indent + 1;
1786    let inner_col = inner * tw;
1787    let close_end = indent * tw + cols(close);
1788    // compact: all elements on one indented continuation line (not when forced exploded).
1789    if !forced {
1790        let contents = flat_group_contents(elems, magic);
1791        if inner_col + cols(&contents) <= cfg.line_width {
1792            let s = format!(
1793                "{open}\n{ind}{contents}\n{base}{close}",
1794                ind = unit.repeat(inner),
1795                base = unit.repeat(indent),
1796            );
1797            return (s, close_end);
1798        }
1799    }
1800    // exploded: one element per line (each rendered recursively). A comma follows every element
1801    // except the last — unless this group is magic, when it follows the last one too.
1802    let mut s = String::from(open);
1803    s.push('\n');
1804    for (k, elem) in elems.iter().enumerate() {
1805        s.push_str(&unit.repeat(inner));
1806        let (es, _) = render_seq(elem, inner, inner_col, cfg, tw, unit);
1807        s.push_str(&es);
1808        if magic || k + 1 < elems.len() {
1809            s.push(',');
1810        }
1811        s.push('\n');
1812    }
1813    s.push_str(&unit.repeat(indent));
1814    s.push_str(close);
1815    (s, close_end)
1816}
1817
1818/// Parse a single physical line's content into reflow atoms, or `None` if it is not a clean,
1819/// reflowable single-line statement (has a comment, an error/continuation token, or unbalanced /
1820/// negative bracket nesting). A magic trailing comma is kept (it drives gdformat's exploded mode).
1821fn line_atoms(body: &str) -> Option<Vec<Atom>> {
1822    use SyntaxKind as S;
1823    let toks = gdscript_syntax::tokenize(body);
1824    let mut atoms: Vec<Atom> = Vec::new();
1825    let mut space = false;
1826    let mut depth: i32 = 0;
1827    for t in &toks {
1828        match t.kind {
1829            S::Whitespace => space = true,
1830            S::Bom => {}
1831            S::LineComment
1832            | S::DocComment
1833            | S::RegionComment
1834            | S::EndRegionComment
1835            | S::LineContinuation => return None,
1836            k => {
1837                if matches!(k, S::LParen | S::LBrack | S::LBrace) {
1838                    depth += 1;
1839                } else if matches!(k, S::RParen | S::RBrack | S::RBrace) {
1840                    depth -= 1;
1841                    if depth < 0 {
1842                        return None;
1843                    }
1844                }
1845                atoms.push(Atom {
1846                    kind: k,
1847                    text: body[t.range].to_string(),
1848                    space,
1849                });
1850                space = false;
1851            }
1852        }
1853    }
1854    if depth != 0 {
1855        return None;
1856    }
1857    Some(atoms)
1858}
1859
1860/// Binary-operator precedence (lower binds looser → breaks first); `None` for non-binary tokens.
1861/// Mirrors the parser's `infix_prec`.
1862pub(crate) fn infix_prec(kind: SyntaxKind) -> Option<u8> {
1863    use SyntaxKind as S;
1864    Some(match kind {
1865        S::OrKw | S::PipePipe => 4,
1866        S::AndKw | S::AmpAmp => 5,
1867        S::InKw => 7,
1868        S::EqEq | S::Neq | S::Lt | S::Gt | S::Le | S::Ge => 8,
1869        S::Pipe => 9,
1870        S::Caret => 10,
1871        S::Amp => 11,
1872        S::Shl | S::Shr => 12,
1873        S::Plus | S::Minus => 13,
1874        S::Star | S::Slash | S::Percent => 14,
1875        S::StarStar => 17,
1876        _ => return None,
1877    })
1878}
1879
1880fn is_assign_op(kind: SyntaxKind) -> bool {
1881    use SyntaxKind as S;
1882    matches!(
1883        kind,
1884        S::Eq
1885            | S::PlusEq
1886            | S::MinusEq
1887            | S::StarEq
1888            | S::SlashEq
1889            | S::StarStarEq
1890            | S::PercentEq
1891            | S::AmpEq
1892            | S::PipeEq
1893            | S::CaretEq
1894            | S::ShlEq
1895            | S::ShrEq
1896            | S::ColonEq
1897    )
1898}
1899
1900/// Flat-render a run of atoms (the first atom never gets a leading space).
1901fn flat_atoms(atoms: &[Atom]) -> String {
1902    let mut s = String::new();
1903    for (i, a) in atoms.iter().enumerate() {
1904        if i > 0 && a.space {
1905            s.push(' ');
1906        }
1907        s.push_str(&a.text);
1908    }
1909    s
1910}
1911
1912/// The `[prefix_end, suffix_start)` boundaries of the wrappable expression inside a statement's
1913/// atoms: the condition of `if`/`elif`/`while` (excluding the trailing `:`), the value of `return`,
1914/// or the right-hand side of a top-level assignment. `None` if there is no such expression.
1915fn expression_span(atoms: &[Atom]) -> Option<(usize, usize)> {
1916    use SyntaxKind as S;
1917    let first = atoms.first()?;
1918    match first.kind {
1919        S::IfKw | S::ElifKw | S::WhileKw => {
1920            let suffix = if atoms.last()?.kind == S::Colon {
1921                atoms.len() - 1
1922            } else {
1923                return None;
1924            };
1925            Some((1, suffix))
1926        }
1927        S::ReturnKw => Some((1, atoms.len())),
1928        _ => {
1929            let mut depth = 0i32;
1930            for (i, a) in atoms.iter().enumerate() {
1931                match a.kind {
1932                    S::LParen | S::LBrack | S::LBrace => depth += 1,
1933                    S::RParen | S::RBrack | S::RBrace => depth -= 1,
1934                    k if depth == 0 && is_assign_op(k) => return Some((i + 1, atoms.len())),
1935                    _ => {}
1936                }
1937            }
1938            None
1939        }
1940    }
1941}
1942
1943/// Strip a redundant grouping paren that wraps the *entire* expression (e.g. the `(...)` this very
1944/// pass injected on a previous run) so re-flowing is idempotent. `(a and b)` → `a and b`, but
1945/// `(a + b) * c` is left alone (the paren is not the whole expression).
1946fn strip_redundant_parens(mut expr: &[Atom]) -> &[Atom] {
1947    while expr.len() >= 2 && expr[0].text == "(" {
1948        let mut depth = 0i32;
1949        let mut close = None;
1950        for (i, a) in expr.iter().enumerate() {
1951            if open_close(&a.text).is_some() {
1952                depth += 1;
1953            } else if matches!(a.text.as_str(), ")" | "]" | "}") {
1954                depth -= 1;
1955                if depth == 0 {
1956                    close = Some(i);
1957                    break;
1958                }
1959            }
1960        }
1961        if close == Some(expr.len() - 1) {
1962            expr = &expr[1..expr.len() - 1];
1963        } else {
1964            break;
1965        }
1966    }
1967    expr
1968}
1969
1970/// The indices (within `expr`) of top-level **binary** operators, paired with their precedence. A
1971/// `+`/`-` etc. is binary only when it follows an operand (not at a unary position); and a `/`/`%`
1972/// inside a **node-path run** (`$Node/Path`, `%Unique/Child`) is a path separator, not an operator —
1973/// the parser reads `$A / B` as the same node-path as `$A/B`, so such a chain must never be split.
1974fn top_level_binary_ops(expr: &[Atom]) -> Vec<(usize, u8)> {
1975    use SyntaxKind as S;
1976    let mut out = Vec::new();
1977    let mut depth = 0i32;
1978    let mut node_path = false;
1979    let mut prev_operand_end = false;
1980    for (i, a) in expr.iter().enumerate() {
1981        match a.kind {
1982            S::LParen | S::LBrack | S::LBrace => depth += 1,
1983            S::RParen | S::RBrack | S::RBrace => depth -= 1,
1984            _ => {}
1985        }
1986        let in_path = node_path && matches!(a.kind, S::Ident | S::Slash | S::String);
1987        let binary_here = depth == 0 && !in_path && i > 0 && prev_operand_end;
1988        if let Some(p) = infix_prec(a.kind).filter(|_| binary_here) {
1989            out.push((i, p));
1990        }
1991        if node_path && !matches!(a.kind, S::Ident | S::Slash | S::String) {
1992            node_path = false;
1993        }
1994        if a.kind == S::Dollar || (a.kind == S::Percent && !prev_operand_end) {
1995            node_path = true;
1996        }
1997        prev_operand_end = is_operand_end(a.kind);
1998    }
1999    out
2000}
2001
2002/// Wrap a too-long statement whose wrappable expression is a top-level **binary-operator chain** the
2003/// way gdformat does: inject parens and break at the lowest-precedence top-level operator,
2004/// operator-leading (each operand rendered recursively, so its own brackets reflow). `None` when
2005/// there is no wrappable binary-operator expression.
2006fn operator_chain_wrap(
2007    atoms: &[Atom],
2008    indent: usize,
2009    cfg: &FmtConfig,
2010    tw: usize,
2011    unit: &str,
2012) -> Option<String> {
2013    let (pre_end, suf_start) = expression_span(atoms)?;
2014    if pre_end >= suf_start {
2015        return None;
2016    }
2017    let expr = strip_redundant_parens(&atoms[pre_end..suf_start]);
2018    let ops = top_level_binary_ops(expr);
2019    let min_prec = ops.iter().map(|&(_, p)| p).min()?;
2020    let prefix = flat_atoms(&atoms[..pre_end]);
2021    let suffix = flat_atoms(&atoms[suf_start..]);
2022    let lead = |out: &mut String| {
2023        out.push_str(&unit.repeat(indent));
2024        out.push_str(&prefix);
2025        if !prefix.is_empty() {
2026            out.push(' ');
2027        }
2028        out.push_str("(\n");
2029    };
2030    // Compact first: the whole expression on one indented continuation line (gdformat tries this
2031    // before breaking — e.g. `(...).normalized() * power` that fits stays on one line).
2032    let expr_flat = flat_atoms(expr);
2033    if (indent + 1) * tw + cols(&expr_flat) <= cfg.line_width {
2034        let mut out = String::new();
2035        lead(&mut out);
2036        out.push_str(&unit.repeat(indent + 1));
2037        out.push_str(&expr_flat);
2038        out.push('\n');
2039        out.push_str(&unit.repeat(indent));
2040        out.push(')');
2041        out.push_str(&suffix);
2042        return Some(out);
2043    }
2044
2045    let split: Vec<usize> = ops
2046        .iter()
2047        .filter(|&&(_, p)| p == min_prec)
2048        .map(|&(i, _)| i)
2049        .collect();
2050
2051    // Build (leading-operator, operand-atoms) segments; the operator leads its following operand.
2052    let mut segs: Vec<(Option<&str>, &[Atom])> = Vec::new();
2053    let mut start = 0;
2054    let mut prev_op: Option<&str> = None;
2055    for &oi in &split {
2056        segs.push((prev_op, &expr[start..oi]));
2057        prev_op = Some(expr[oi].text.as_str());
2058        start = oi + 1;
2059    }
2060    segs.push((prev_op, &expr[start..]));
2061
2062    let mut out = String::new();
2063    lead(&mut out);
2064    for (op, operand) in &segs {
2065        out.push_str(&unit.repeat(indent + 1));
2066        let mut col = (indent + 1) * tw;
2067        if let Some(op) = op {
2068            out.push_str(op);
2069            out.push(' ');
2070            col += cols(op) + 1;
2071        }
2072        let (od, _) = build_elems(operand, &mut 0, None);
2073        if let Some(seq) = od.first() {
2074            let (r, _) = render_seq(seq, indent + 1, col, cfg, tw, unit);
2075            out.push_str(&r);
2076        }
2077        out.push('\n');
2078    }
2079    out.push_str(&unit.repeat(indent));
2080    out.push(')');
2081    out.push_str(&suffix);
2082    Some(out)
2083}
2084
2085/// Wrap the statement's expression in injected parens on a single indented continuation line (the
2086/// **compact** form gdformat uses for a too-long expression with no top-level binary operator — e.g.
2087/// a long method chain). `None` if there is no such expression, it has a top-level operator (handled
2088/// by [`operator_chain_wrap`]), or even the compact line would still overflow.
2089fn compact_paren_wrap(
2090    atoms: &[Atom],
2091    indent: usize,
2092    cfg: &FmtConfig,
2093    tw: usize,
2094    unit: &str,
2095) -> Option<String> {
2096    let (pre_end, suf_start) = expression_span(atoms)?;
2097    let expr = strip_redundant_parens(atoms.get(pre_end..suf_start)?);
2098    // Only wrap an expression that has a bracketed group (e.g. a method chain) and no top-level
2099    // binary operator: a bare expression / node-path is left on one line, as gdformat does.
2100    if expr.is_empty()
2101        || !top_level_binary_ops(expr).is_empty()
2102        || !expr.iter().any(|a| open_close(&a.text).is_some())
2103    {
2104        return None;
2105    }
2106    let expr_flat = flat_atoms(expr);
2107    if (indent + 1) * tw + cols(&expr_flat) > cfg.line_width {
2108        return None;
2109    }
2110    let prefix = flat_atoms(&atoms[..pre_end]);
2111    let suffix = flat_atoms(&atoms[suf_start..]);
2112    let mut out = String::new();
2113    out.push_str(&unit.repeat(indent));
2114    out.push_str(&prefix);
2115    if !prefix.is_empty() {
2116        out.push(' ');
2117    }
2118    out.push_str("(\n");
2119    out.push_str(&unit.repeat(indent + 1));
2120    out.push_str(&expr_flat);
2121    out.push('\n');
2122    out.push_str(&unit.repeat(indent));
2123    out.push(')');
2124    out.push_str(&suffix);
2125    Some(out)
2126}
2127
2128/// Collapse a (possibly multi-line) statement to its canonical single-line body — re-synthesising the
2129/// inter-token spacing from scratch (so an already-wrapped statement is flattened). Returns `None`
2130/// when the statement must be kept **verbatim**: it contains a comment, a multi-line lambda body, a
2131/// backslash continuation, or a multi-line string (whose own newlines must be preserved).
2132/// Whether a multi-line statement that could not be flattened to one line is safe to re-lay-out via
2133/// the CST wrapper. It must (a) carry no comment — the wrapper does not thread comments through — and
2134/// (b) contain a lambda, the one legitimate reason a statement cannot collapse to a single line (so we
2135/// leave a multi-line string or an already-wrapped plain call verbatim). A leading `func`/`static func`
2136/// is a function *declaration* header (its `func` is not a lambda), so it is excluded.
2137fn stmt_is_rewrappable_multiline(stmt: &str) -> bool {
2138    let toks = gdscript_syntax::tokenize(stmt);
2139    let lead = stmt.trim_start();
2140    let lead = lead.strip_prefix("static ").unwrap_or(lead);
2141    if lead.starts_with("func ") || lead.starts_with("func(") {
2142        return false; // a function *declaration* header, not a lambda expression
2143    }
2144    // The three legitimate reasons a statement cannot collapse onto one physical line and is still
2145    // worth re-laying-out: it carries a lambda, a comment, or a multi-line string (the CST wrapper
2146    // now threads all three; the meaning / comment-multiset nets fall back to verbatim otherwise).
2147    let has_lambda = toks.iter().any(|t| t.kind == SyntaxKind::FuncKw);
2148    let has_comment = toks.iter().any(|t| {
2149        matches!(
2150            t.kind,
2151            SyntaxKind::LineComment
2152                | SyntaxKind::DocComment
2153                | SyntaxKind::RegionComment
2154                | SyntaxKind::EndRegionComment
2155        )
2156    });
2157    let has_multiline_string = toks
2158        .iter()
2159        .any(|t| t.kind == SyntaxKind::String && stmt[t.range].contains('\n'));
2160    has_lambda || has_comment || has_multiline_string
2161}
2162
2163fn flatten_statement(stmt: &str) -> Option<String> {
2164    use SyntaxKind as S;
2165    let raw = gdscript_syntax::tokenize(stmt);
2166    let (toks, _diags) = gdscript_syntax::run_prepass(&raw, stmt);
2167    let mut out = String::with_capacity(stmt.len());
2168    let mut stack: Vec<S> = Vec::new();
2169    let mut brace_enum: Vec<bool> = Vec::new();
2170    let mut pending_enum = false;
2171    let mut prev_sig: Option<S> = None;
2172    let mut prev_unary = false;
2173    let mut node_path = false;
2174    let mut pending_break = false; // a line break was seen; the next token re-spaces across it
2175    for t in &toks {
2176        match t.kind {
2177            S::NewlinePhys => pending_break = true,
2178            // A synthetic newline inside brackets is a multi-line lambda body — cannot collapse.
2179            S::Newline if !stack.is_empty() => return None,
2180            S::Indent | S::Dedent | S::Bom | S::Newline => {}
2181            // A backslash line continuation is collapsed away: trim the space before the `\` so the
2182            // join is not double-spaced; the following physical newline then sets `pending_break` and
2183            // the next token re-spaces once (and the statement is later re-wrapped) like gdformat,
2184            // which converts `\`-continued statements to paren-wrapped multi-line form.
2185            S::LineContinuation => trim_trailing_inline_ws(&mut out),
2186            S::Whitespace => {
2187                // Keep intra-line spacing verbatim (it is already canonical / the user's, when
2188                // spacing-normalisation is off); drop a line's leading indentation.
2189                if !pending_break && prev_sig.is_some() {
2190                    out.push_str(&stmt[t.range]);
2191                }
2192            }
2193            S::LineComment | S::DocComment | S::RegionComment | S::EndRegionComment => return None,
2194            k => {
2195                let text = &stmt[t.range];
2196                if matches!(k, S::String | S::StringName | S::NodePath) && text.contains('\n') {
2197                    return None; // a multi-line string — keep the statement verbatim
2198                }
2199                // Replace a line break by the spacing the intra-line rules would synthesize.
2200                if pending_break {
2201                    let sp = match prev_sig {
2202                        Some(_) if node_path && matches!(k, S::Ident | S::Slash | S::String) => {
2203                            Spacing::Verbatim
2204                        }
2205                        Some(p) => {
2206                            let top_enum = stack.last() == Some(&S::LBrace)
2207                                && brace_enum.last() == Some(&true);
2208                            space_before(p, k, stack.last().copied(), prev_unary, top_enum)
2209                        }
2210                        None => Spacing::None,
2211                    };
2212                    if matches!(sp, Spacing::Single) {
2213                        out.push(' ');
2214                    }
2215                    pending_break = false;
2216                }
2217                out.push_str(text);
2218                match k {
2219                    S::LBrace => {
2220                        stack.push(k);
2221                        brace_enum.push(pending_enum);
2222                        pending_enum = false;
2223                    }
2224                    S::LParen | S::LBrack => stack.push(k),
2225                    S::RBrace => {
2226                        stack.pop();
2227                        brace_enum.pop();
2228                    }
2229                    S::RParen | S::RBrack => {
2230                        stack.pop();
2231                    }
2232                    _ => {}
2233                }
2234                if k == S::EnumKw {
2235                    pending_enum = true;
2236                }
2237                let unary_ctx = prev_sig.is_none_or(|p| !is_operand_end(p));
2238                if k == S::Dollar || (k == S::Percent && unary_ctx) {
2239                    node_path = true;
2240                } else if node_path && !matches!(k, S::Ident | S::Slash | S::String) {
2241                    node_path = false;
2242                }
2243                prev_unary = match k {
2244                    S::Minus | S::Plus => unary_ctx,
2245                    S::Tilde | S::Bang => true,
2246                    _ => false,
2247                };
2248                prev_sig = Some(k);
2249            }
2250        }
2251    }
2252    Some(out)
2253}
2254
2255/// Render a flattened statement `body` at `indent` into its canonical layout — flat if it fits, else
2256/// wrapped (operator-chain → bracketed compact/exploded → compact-paren). Always indented.
2257fn render_statement(body: &str, indent: usize, cfg: &FmtConfig, tw: usize, unit: &str) -> String {
2258    // Primary path: gdformat's own algorithm, driven from the CST (see `wrap`). It owns the layout and
2259    // is faithful to gdformat. `wrap::render` self-validates that its output is meaning-equivalent to
2260    // the body (allowing exactly gdformat's legitimate rewrites — redundant grouping parens, trailing
2261    // commas, string quotes); it returns `None` otherwise, and we fall back to the heuristic below.
2262    if let Some(out) = wrap::render(body, indent, cfg) {
2263        return out;
2264    }
2265    let flat = || format!("{}{body}", unit.repeat(indent));
2266    let Some(atoms) = line_atoms(body) else {
2267        return flat();
2268    };
2269    let width = indent * tw + cols(body);
2270    // A too-long top-level binary-operator chain is wrapped in injected parens (highest priority).
2271    if width > cfg.line_width {
2272        let oc = operator_chain_wrap(&atoms, indent, cfg, tw, unit);
2273        if let Some(oc) = oc {
2274            return oc;
2275        }
2276    }
2277    if !atoms.iter().any(|a| open_close(&a.text).is_some()) {
2278        return flat();
2279    }
2280    let (docs, _) = build_elems(&atoms, &mut 0, None);
2281    if docs.len() != 1 {
2282        return flat();
2283    }
2284    let top = &docs[0];
2285    // Faithfulness: the atom tree must round-trip the body's *tokens* (spacing may differ — e.g. enum
2286    // braces / type-annotation colons — which `flat_seq` does not reproduce).
2287    if !same_significant_tokens(&flat_seq(top), body) {
2288        return flat();
2289    }
2290    // Stay flat unless the line is too long or a magic trailing comma forces it exploded.
2291    let forced = top
2292        .iter()
2293        .any(|d| matches!(d, ReflowDoc::Group { elems, magic, .. } if group_forced(elems, *magic)));
2294    if width <= cfg.line_width && !forced {
2295        return flat();
2296    }
2297    let (rendered, _) = render_seq(top, indent, indent * tw, cfg, tw, unit);
2298    let result = format!("{}{rendered}", unit.repeat(indent));
2299    // If bracket reflow still overflows (e.g. a long method chain), wrap the expression compact.
2300    let overflows = result.lines().any(|l| display_cols(l, tw) > cfg.line_width);
2301    if overflows {
2302        let cp = compact_paren_wrap(&atoms, indent, cfg, tw, unit);
2303        if let Some(cp) = cp {
2304            return cp;
2305        }
2306    }
2307    result
2308}
2309
2310/// Re-flow the layout of every statement: a statement that now fits is collapsed onto one line, one
2311/// that does not is re-wrapped to its canonical form — gdformat-style layout ownership. Statements
2312/// that cannot be safely collapsed (comments, multi-line lambdas, multi-line strings) are preserved.
2313/// Token-preserving (modulo the trailing commas / parens the meaning-equivalence net allows).
2314#[allow(
2315    clippy::too_many_lines,
2316    reason = "one cohesive pass: per-line bracket/straddle bookkeeping then logical-statement grouping"
2317)]
2318fn reflow(formatted: &str, config: &FmtConfig) -> String {
2319    use SyntaxKind as S;
2320    if config.line_width == 0 {
2321        return formatted.to_owned();
2322    }
2323    let tw = if config.use_tabs {
2324        4
2325    } else {
2326        config.indent_size.max(1)
2327    };
2328    let unit = config.indent_unit();
2329    let lines: Vec<&str> = formatted.split('\n').collect();
2330
2331    let line_starts: Vec<usize> = std::iter::once(0)
2332        .chain(
2333            formatted
2334                .bytes()
2335                .enumerate()
2336                .filter_map(|(i, b)| (b == b'\n').then_some(i + 1)),
2337        )
2338        .collect();
2339    let line_of = |off: usize| line_starts.partition_point(|&s| s <= off).saturating_sub(1);
2340
2341    // Per line: bracket depth at its start, whether a multi-line token (e.g. a `"""..."""` string)
2342    // covers it, and whether it ends in a `\` continuation — used to group physical lines into the
2343    // logical statement they belong to.
2344    let raw = gdscript_syntax::tokenize(formatted);
2345    let mut start_depth = vec![i32::MIN; lines.len()];
2346    let mut straddled = vec![false; lines.len()];
2347    let mut ends_cont = vec![false; lines.len()];
2348    let mut depth: i32 = 0;
2349    for t in &raw {
2350        let s = usize::from(t.range.start());
2351        let e = usize::from(t.range.end()).saturating_sub(1).max(s);
2352        let (sl, el) = (line_of(s), line_of(e));
2353        if sl < start_depth.len() && start_depth[sl] == i32::MIN {
2354            start_depth[sl] = depth;
2355        }
2356        if el > sl {
2357            let end = el.min(lines.len().saturating_sub(1));
2358            straddled[sl..=end].fill(true);
2359        }
2360        match t.kind {
2361            S::LParen | S::LBrack | S::LBrace => depth += 1,
2362            S::RParen | S::RBrack | S::RBrace => depth -= 1,
2363            S::LineContinuation if sl < ends_cont.len() => ends_cont[sl] = true,
2364            // The next line starts at the current depth — covers blank lines (which have no token of
2365            // their own to set `start_depth`, so they would otherwise stay unset and be absorbed).
2366            S::NewlinePhys if sl + 1 < start_depth.len() && start_depth[sl + 1] == i32::MIN => {
2367                start_depth[sl + 1] = depth;
2368            }
2369            _ => {}
2370        }
2371    }
2372    let sd = |i: usize| {
2373        let d = start_depth.get(i).copied().unwrap_or(0);
2374        if d == i32::MIN { 0 } else { d }
2375    };
2376    let strad = |i: usize| straddled.get(i).copied().unwrap_or(false);
2377    let cont = |i: usize| ends_cont.get(i).copied().unwrap_or(false);
2378
2379    let mut out = String::with_capacity(formatted.len());
2380    let mut li = 0;
2381    while li < lines.len() {
2382        let line = lines[li];
2383        // A statement head: a non-blank line at bracket depth 0 that is not the continuation of the
2384        // previous line (a `\` or a multi-line token spanning the boundary).
2385        let head = !line.trim().is_empty()
2386            && sd(li) == 0
2387            && (li == 0 || !cont(li - 1))
2388            && !(li > 0 && strad(li) && strad(li - 1));
2389        if !head {
2390            out.push_str(line);
2391            if li + 1 < lines.len() {
2392                out.push('\n');
2393            }
2394            li += 1;
2395            continue;
2396        }
2397        // Extend to the end of the logical statement (brackets open, a multi-line token spanning the
2398        // boundary, or a backslash continuation).
2399        let mut j = li;
2400        while j + 1 < lines.len() && (sd(j + 1) != 0 || (strad(j) && strad(j + 1)) || cont(j)) {
2401            j += 1;
2402        }
2403        let stmt = lines[li..=j].join("\n");
2404        let indent = if config.use_tabs {
2405            line.bytes().take_while(|&b| b == b'\t').count()
2406        } else {
2407            line.bytes()
2408                .take_while(|&b| b == b' ')
2409                .count()
2410                .checked_div(config.indent_size)
2411                .unwrap_or(0)
2412        };
2413        let rendered = match flatten_statement(&stmt) {
2414            Some(body) => Some(render_statement(&body, indent, config, tw, &unit)),
2415            // A statement that cannot be collapsed onto one physical line *because it carries a
2416            // multi-line lambda body* (a compound- or multi-statement-bodied lambda argument) is still
2417            // re-laid-out by the CST wrapper directly, from its dedented multi-line form — gdformat
2418            // reformats *every* statement, not just single-line ones. We only attempt this for a
2419            // comment-free lambda-bearing statement (the wrapper does not carry comments through, and a
2420            // plain multi-line string / already-wrapped statement is better left verbatim); the wrapper
2421            // still self-validates meaning-equivalence and returns `None` (→ verbatim) otherwise.
2422            None if stmt_is_rewrappable_multiline(&stmt) => {
2423                wrap::render(&wrap::dedent(&stmt), indent, config)
2424            }
2425            None => None,
2426        };
2427        out.push_str(rendered.as_deref().unwrap_or(&stmt));
2428        if j + 1 < lines.len() {
2429            out.push('\n');
2430        }
2431        li = j + 1;
2432    }
2433    out
2434}
2435
2436/// Trim trailing spaces/tabs from the end of `out` (the current line).
2437fn trim_trailing_inline_ws(out: &mut String) {
2438    while out.ends_with(' ') || out.ends_with('\t') {
2439        out.pop();
2440    }
2441}
2442
2443/// Whether two sources lex to the same sequence of significant (non-trivia) tokens — a
2444/// spacing-insensitive equality used by the reflow faithfulness check and the token-preserving tests.
2445fn same_significant_tokens(a: &str, b: &str) -> bool {
2446    fn sig(s: &str) -> Vec<(SyntaxKind, &str)> {
2447        gdscript_syntax::tokenize(s)
2448            .into_iter()
2449            // A `;` is a statement separator equivalent to a newline — ignore it, so splitting
2450            // `a; b` onto two lines is recognised as token-preserving.
2451            .filter(|t| !t.kind.is_trivia() && t.kind != SyntaxKind::Semicolon)
2452            .map(|t| (t.kind, &s[t.range]))
2453            .collect()
2454    }
2455    sig(a) == sig(b)
2456}
2457
2458/// Re-emit a string-literal token's text in gdformat's canonical quote style: prefer `"`, fall back
2459/// to `'` only when the body has more `"` than `'` (fewer escapes), keeping the prefix (`r`/`&`/`^`)
2460/// and the decoded value. Idempotent. Triple-quoted strings are left verbatim (rare; not normalized).
2461pub(crate) fn canonical_string(text: &str) -> String {
2462    let Some(qpos) = text.find(['"', '\'']) else {
2463        return text.to_owned(); // defensive: not actually a string literal
2464    };
2465    let prefix = &text[..qpos];
2466    let rest = &text[qpos..];
2467    let rb = rest.as_bytes();
2468    let quote = rb[0];
2469    // Triple-quoted. gdformat converts a *single-line* triple-**single**-quoted string (`'''…'''`)
2470    // to a regular string (strip the outer `''` and apply the regular quote rule to `'…'`); a
2471    // triple-**double** (`"""…"""`) and any *multi-line* triple-quoted string are left verbatim.
2472    if rb.len() >= 6 && rb[1] == quote && rb[2] == quote {
2473        if quote == b'\'' && rest.ends_with("'''") {
2474            let body = &rest[3..rest.len() - 3];
2475            if !body.contains('\n') {
2476                return canonical_string(&format!("{prefix}'{body}'"));
2477            }
2478        }
2479        return text.to_owned();
2480    }
2481    if rest.len() < 2 || rb[rest.len() - 1] != quote {
2482        return text.to_owned(); // unterminated / malformed: don't touch
2483    }
2484    let body = &rest[1..rest.len() - 1];
2485    // Raw strings (`r"..."`) cannot escape — only switch quotes if the body lacks the target.
2486    if prefix.contains('r') {
2487        let target = if !body.contains('"') {
2488            '"'
2489        } else if !body.contains('\'') {
2490            '\''
2491        } else {
2492            quote as char
2493        };
2494        return format!("{prefix}{target}{body}{target}");
2495    }
2496    // Parse the body into units (escaped or literal) and count the value's quote characters.
2497    let mut units: Vec<(bool, char)> = Vec::new();
2498    let (mut dq, mut sq) = (0usize, 0usize);
2499    let mut chars = body.chars();
2500    while let Some(c) = chars.next() {
2501        if c == '\\' {
2502            if let Some(n) = chars.next() {
2503                units.push((true, n));
2504                match n {
2505                    '"' => dq += 1,
2506                    '\'' => sq += 1,
2507                    _ => {}
2508                }
2509            } else {
2510                units.push((false, '\\'));
2511            }
2512        } else {
2513            units.push((false, c));
2514            match c {
2515                '"' => dq += 1,
2516                '\'' => sq += 1,
2517                _ => {}
2518            }
2519        }
2520    }
2521    let target = if dq > sq { '\'' } else { '"' };
2522    let mut out = String::with_capacity(text.len());
2523    out.push_str(prefix);
2524    out.push(target);
2525    for (esc, c) in units {
2526        if c == '"' || c == '\'' {
2527            if c == target {
2528                out.push('\\');
2529            }
2530            out.push(c);
2531        } else {
2532            if esc {
2533                out.push('\\');
2534            }
2535            out.push(c);
2536        }
2537    }
2538    out.push(target);
2539    out
2540}
2541
2542/// A normalised parse-tree event used by [`meaning_preserved`].
2543#[derive(Clone, PartialEq, Eq)]
2544enum TreeEvent {
2545    Open(SyntaxKind),
2546    Close,
2547    Token(SyntaxKind, String),
2548}
2549
2550/// Walk a parse-tree node, appending normalised events: trivia is dropped, a `ParenExpr` is
2551/// **unwrapped** (its node + `(`/`)` tokens removed, its inner expression spliced in — so a
2552/// *redundant* grouping paren is invisible while a precedence-changing one still differs, because the
2553/// surrounding `BinExpr` nesting changes), and string literals are recorded by canonical quote form.
2554fn emit_tree_events(node: &gdscript_syntax::GdNode, out: &mut Vec<TreeEvent>) {
2555    use cstree::util::NodeOrToken;
2556    if node.kind() == SyntaxKind::ParenExpr {
2557        for child in node.children() {
2558            emit_tree_events(child, out);
2559        }
2560        return;
2561    }
2562    out.push(TreeEvent::Open(node.kind()));
2563    for child in node.children_with_tokens() {
2564        match child {
2565            // An *empty* parameter list carries no meaning — `signal s()` ≡ `signal s` (a func always
2566            // has `()`, so dropping the empty list uniformly leaves func headers unaffected). Skipping
2567            // it here lets the formatter remove a signal's empty `()` without the net objecting.
2568            NodeOrToken::Node(n)
2569                if n.kind() == SyntaxKind::ParamList && n.children().next().is_none() => {}
2570            NodeOrToken::Node(n) => emit_tree_events(n, out),
2571            NodeOrToken::Token(t) => {
2572                let kind = t.kind();
2573                // Skip trivia, the synthetic block-structure markers (`Newline`/`Indent`/`Dedent` —
2574                // the block nesting they encode is already captured by the surrounding node Open/Close
2575                // events, and they differ between an inline and a multi-line form of the *same*
2576                // construct, e.g. `func(): x` vs a wrapped lambda body), and a `;` statement separator.
2577                if kind.is_trivia() || kind.is_synthetic_layout() || kind == SyntaxKind::Semicolon {
2578                    continue;
2579                }
2580                let text = if matches!(
2581                    kind,
2582                    SyntaxKind::String | SyntaxKind::StringName | SyntaxKind::NodePath
2583                ) {
2584                    canonical_string(t.text())
2585                } else {
2586                    t.text().to_owned()
2587                };
2588                out.push(TreeEvent::Token(kind, text));
2589            }
2590        }
2591    }
2592    out.push(TreeEvent::Close);
2593}
2594
2595/// Whether `a` and `b` are **meaning-equivalent** — the relaxed safety net used once the formatter
2596/// performs token-*mutating* rewrites. It compares the **parse-tree structure** (so token order,
2597/// nesting and operator precedence must all match), normalising away exactly the differences gdformat
2598/// is allowed to introduce: **redundant grouping parens** are unwrapped, a **trailing comma** before a
2599/// closing bracket is dropped, and **string literals are compared by canonical quote form**. A
2600/// dropped/added/reordered token, a changed string *value*, or a precedence change is still caught.
2601pub(crate) fn meaning_preserved(a: &str, b: &str) -> bool {
2602    fn events(s: &str) -> Vec<TreeEvent> {
2603        let mut raw = Vec::new();
2604        emit_tree_events(&gdscript_syntax::parse(s).syntax_node(), &mut raw);
2605        let mut out = Vec::with_capacity(raw.len());
2606        for i in 0..raw.len() {
2607            // Drop a trailing comma (a `,` immediately before a closing-bracket token).
2608            let trailing_comma = matches!(&raw[i], TreeEvent::Token(SyntaxKind::Comma, _))
2609                && matches!(
2610                    raw.get(i + 1),
2611                    Some(TreeEvent::Token(
2612                        SyntaxKind::RParen | SyntaxKind::RBrack | SyntaxKind::RBrace,
2613                        _
2614                    ))
2615                );
2616            if !trailing_comma {
2617                out.push(raw[i].clone());
2618            }
2619        }
2620        out
2621    }
2622    events(a) == events(b)
2623}
2624
2625#[cfg(test)]
2626mod tests {
2627    use super::*;
2628
2629    fn fmt(src: &str) -> String {
2630        format(src, &FmtConfig::default())
2631    }
2632
2633    #[test]
2634    fn normalizes_indentation_to_tabs() {
2635        // Four-space indentation becomes one tab per level.
2636        let src = "func f():\n    if true:\n        return 1\n";
2637        assert_eq!(fmt(src), "func f():\n\tif true:\n\t\treturn 1\n");
2638    }
2639
2640    #[test]
2641    fn trims_trailing_whitespace_and_adds_final_newline() {
2642        let src = "var x = 1   \nvar y = 2"; // trailing spaces + no final newline
2643        assert_eq!(fmt(src), "var x = 1\nvar y = 2\n");
2644    }
2645
2646    #[test]
2647    fn is_idempotent() {
2648        let src = "func f():\n  var a = 1\n  if a:\n      return a\n";
2649        let once = fmt(src);
2650        assert_eq!(fmt(&once), once, "formatting must be idempotent");
2651    }
2652
2653    #[test]
2654    fn already_formatted_is_unchanged() {
2655        let src = "func f():\n\tvar a = 1\n\treturn a\n";
2656        assert_eq!(fmt(src), src);
2657    }
2658
2659    #[test]
2660    fn indexed_array_literal_explodes_the_array_keeps_index_on_close_line() {
2661        // A subscript on a too-wide array literal (`[…][i]`): explode the ARRAY one element per line
2662        // (with a trailing comma) and keep `[index]` compact on the close-bracket line — gdformat
2663        // parity (the os_test.gd byte-exact miss, burndown Stage 6.29), NOT the reverse (a compact
2664        // over-width array with the index dropped to its own line).
2665        let src = "func f():\n\tvar x = [\"Landscape\", \"Portrait\", \"Landscape (reverse)\", \"Portrait (reverse)\", \"Defined by sensor\"][get_orientation()]\n";
2666        let out = fmt(src);
2667        // The array opens/explodes and the `[index]` stays compact on the close-bracket line.
2668        assert!(
2669            out.contains("var x = [\n"),
2670            "the array must open/explode:\n{out}"
2671        );
2672        assert!(
2673            out.contains("][get_orientation()]"),
2674            "the index must stay on the close-bracket line:\n{out}"
2675        );
2676        // The OLD bug kept the array a compact over-width line and dropped the index onto its own
2677        // line (`…"][` then a newline) — never again.
2678        assert!(
2679            !out.contains("][\n"),
2680            "the index must not be exploded onto its own line:\n{out}"
2681        );
2682        assert!(super::same_significant_tokens(src, &out));
2683        assert_eq!(fmt(&out), out, "idempotent");
2684    }
2685
2686    #[test]
2687    fn operator_chain_paren_operand_with_leading_comment_compacts_and_hoists() {
2688        // gdformat parity (town_scene.gd, burndown Stage 6.28): a parenthesized `and`-chain operand of
2689        // an `or` chain, with a comment leading the operand *inside* its parens, renders the operand
2690        // compact and hoists the comment to its own next line — not the source's hand-wrapped paren
2691        // block. (Our paren-retention already matched gdformat; this closes the comment placement.)
2692        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";
2693        let out = fmt(src);
2694        assert!(out.contains("(aaa and bbb)"), "operand 0 compacts:\n{out}");
2695        assert!(
2696            out.contains("or (ccc and ddd)"),
2697            "operator leads the compacted operand 1:\n{out}"
2698        );
2699        assert!(
2700            out.contains("# on") && out.contains("# off"),
2701            "comments preserved:\n{out}"
2702        );
2703        // (No raw `same_significant_tokens` here: a multi-line operator chain is wrapped in an
2704        // injected `(…)` — gdformat does the same, and the formatter's own net accounts for it, but a
2705        // raw token compare would not. The byte-exact gdformat corpus parity is the real guard.)
2706        assert_eq!(fmt(&out), out, "idempotent");
2707    }
2708
2709    #[test]
2710    fn preserves_significant_tokens_including_strings() {
2711        let src = "func f():\n\tvar s = \"a + b\"\n\treturn s\n";
2712        let out = fmt(src);
2713        assert!(super::same_significant_tokens(src, &out));
2714        assert!(out.contains("\"a + b\""));
2715    }
2716
2717    #[test]
2718    fn multiline_string_content_is_untouched() {
2719        // The interior of a multi-line string must survive verbatim (it is a single token).
2720        let src = "func f():\n\tvar s = \"\"\"line1\n        keep   \nline2\"\"\"\n\treturn s\n";
2721        let out = fmt(src);
2722        assert!(
2723            out.contains("line1\n        keep   \nline2"),
2724            "got: {out:?}"
2725        );
2726    }
2727
2728    #[test]
2729    fn safe_mode_returns_input_on_syntax_error() {
2730        let src = "func f(:\n\treturn"; // malformed
2731        assert_eq!(fmt(src), src);
2732    }
2733
2734    #[test]
2735    fn empty_input_stays_empty() {
2736        assert_eq!(fmt(""), "");
2737        assert_eq!(fmt("\n\n\n"), "");
2738    }
2739
2740    #[test]
2741    fn spaces_option_indents_with_spaces() {
2742        let cfg = FmtConfig {
2743            use_tabs: false,
2744            indent_size: 2,
2745            ..FmtConfig::default()
2746        };
2747        let src = "func f():\n\treturn 1\n";
2748        assert_eq!(format(src, &cfg), "func f():\n  return 1\n");
2749    }
2750
2751    /// `parse(src).errors()` must be empty — the formatter must never emit code that fails to parse.
2752    fn parses_clean(src: &str) -> bool {
2753        gdscript_syntax::parse(src).errors().is_empty()
2754    }
2755
2756    #[test]
2757    fn comment_between_statements_does_not_corrupt_the_next_line() {
2758        // A comment-only line is copied verbatim by the prepass (no synthetic Newline); the line
2759        // AFTER it must still be re-indented to the block depth, not left at its original spacing.
2760        let src = "func g():\n  var a = 1\n  # c\n  var x = 1\n  var y = 2\n";
2761        let out = fmt(src);
2762        assert_eq!(
2763            out,
2764            "func g():\n\tvar a = 1\n\t# c\n\tvar x = 1\n\tvar y = 2\n"
2765        );
2766        assert!(
2767            parses_clean(&out),
2768            "formatter must not emit mixed indent: {out:?}"
2769        );
2770        assert_eq!(fmt(&out), out, "must be idempotent");
2771    }
2772
2773    #[test]
2774    fn leading_body_comment_is_indented_to_the_block() {
2775        // A comment that is the FIRST line of a block: the prepass emits `Indent` only at the first
2776        // *code* line, so the comment's raw depth is wrong — but it is re-indented to its intended
2777        // block depth by comparing its authored indentation against the surrounding code lines
2778        // (gdformat's rule). Works for the space-indented input here too (length comparison is
2779        // indent-width agnostic).
2780        let src = "func g():\n  # c\n  var x = 1\n  var y = 2\n";
2781        let out = fmt(src);
2782        assert_eq!(out, "func g():\n\t# c\n\tvar x = 1\n\tvar y = 2\n");
2783        assert!(parses_clean(&out), "{out:?}");
2784        assert_eq!(fmt(&out), out, "must be idempotent");
2785    }
2786
2787    #[test]
2788    fn doc_comment_between_statements_is_reindented_and_does_not_corrupt() {
2789        // A doc comment AFTER a code line (depth known) is re-indented like any line, and the line
2790        // following it must not be mis-indented.
2791        let src = "func g():\n  var a = 1\n  ## doc\n  var x = 1\n";
2792        let out = fmt(src);
2793        assert_eq!(out, "func g():\n\tvar a = 1\n\t## doc\n\tvar x = 1\n");
2794        assert!(parses_clean(&out), "{out:?}");
2795    }
2796
2797    #[test]
2798    fn bracketed_continuation_interior_is_preserved() {
2799        // A physical newline INSIDE brackets is a real continuation — its interior spacing must be
2800        // kept verbatim (not treated like a comment-line terminator that re-indents the next line).
2801        let src = "func f():\n\tvar a = [\n\t\t1,\n\t\t2,\n\t]\n\treturn a\n";
2802        let out = fmt(src);
2803        assert!(parses_clean(&out), "{out:?}");
2804        assert!(super::same_significant_tokens(src, &out));
2805        assert_eq!(fmt(&out), out, "must be idempotent");
2806    }
2807
2808    // ---- Phase-4 increment A: intra-line spacing ----
2809
2810    /// Format a single statement inside a function body, returning just the (de-indented) body line.
2811    /// Wrapping keeps `safe_mode` happy (a bare statement is not a valid top-level form).
2812    fn fmt_stmt(stmt: &str) -> String {
2813        let src = format!("func _f():\n\t{stmt}\n");
2814        let out = fmt(&src);
2815        out.strip_prefix("func _f():\n\t")
2816            .and_then(|s| s.strip_suffix('\n'))
2817            .unwrap_or(&out)
2818            .to_owned()
2819    }
2820
2821    #[test]
2822    fn spacing_operators_and_assignment() {
2823        assert_eq!(fmt_stmt("var x=a+b"), "var x = a + b");
2824        assert_eq!(fmt_stmt("var x = a-b"), "var x = a - b"); // binary minus
2825        assert_eq!(fmt_stmt("var t = a   *   b"), "var t = a * b"); // collapse runs
2826        assert_eq!(
2827            fmt_stmt("var z = a==b and c!=d"),
2828            "var z = a == b and c != d"
2829        );
2830        assert_eq!(fmt_stmt("x+=1"), "x += 1");
2831    }
2832
2833    #[test]
2834    fn spacing_brackets_and_commas() {
2835        assert_eq!(fmt_stmt("foo( x ,y )"), "foo(x, y)");
2836        assert_eq!(fmt_stmt("var a = [1,2]"), "var a = [1, 2]");
2837        assert_eq!(
2838            fmt_stmt("var d = {\"x\":1,\"y\":2}"),
2839            "var d = {\"x\": 1, \"y\": 2}"
2840        );
2841        assert_eq!(
2842            fmt_stmt("var n = obj . field . method ( )"),
2843            "var n = obj.field.method()"
2844        );
2845    }
2846
2847    #[test]
2848    fn spacing_type_annotation_and_default_args() {
2849        assert_eq!(fmt_stmt("var x:int=1"), "var x: int = 1");
2850        // Typed default `=` is spaced (we do not replicate Black's untyped `x=1`); arrow + header colon.
2851        assert_eq!(
2852            fmt("func f(a,b:int=1)->int:\n\treturn 0\n"),
2853            "func f(a, b: int = 1) -> int:\n\treturn 0\n"
2854        );
2855        assert_eq!(fmt_stmt("var a:Array[int]=[]"), "var a: Array[int] = []");
2856    }
2857
2858    #[test]
2859    fn spacing_unary_minus() {
2860        assert_eq!(fmt_stmt("var x = -1"), "var x = -1"); // unary after `=`
2861        assert_eq!(fmt_stmt("foo( -1 , -2 )"), "foo(-1, -2)"); // unary in call
2862        assert_eq!(fmt_stmt("var a = [-1,-2]"), "var a = [-1, -2]"); // unary in array
2863        assert_eq!(fmt_stmt("var n = -2**2"), "var n = -2 ** 2"); // unary then power
2864        assert_eq!(fmt_stmt("var d = a - -b"), "var d = a - -b"); // binary then unary
2865    }
2866
2867    #[test]
2868    fn spacing_percent_is_modulo_or_format_when_after_an_operand() {
2869        assert_eq!(fmt_stmt("var r = a%b"), "var r = a % b"); // modulo
2870        assert_eq!(fmt_stmt("var s = \"%d\"%n"), "var s = \"%d\" % n"); // format operator
2871    }
2872
2873    #[test]
2874    fn spacing_node_paths_stay_tight() {
2875        // The correctness-critical cases: a node path must NOT gain spaces around `/` (that would
2876        // turn `$Player/Bone` into a division with an identical token sequence).
2877        assert_eq!(
2878            fmt_stmt("var n = get_node($Player/Bone)"),
2879            "var n = get_node($Player/Bone)"
2880        );
2881        assert_eq!(fmt_stmt("var u = %Unique/Child"), "var u = %Unique/Child");
2882        assert_eq!(
2883            fmt_stmt("var p = $\"Player\".position"),
2884            "var p = $\"Player\".position"
2885        );
2886        // StringName / NodePath literals are single tokens — untouched atoms.
2887        assert_eq!(fmt_stmt("var v = &\"Name\""), "var v = &\"Name\"");
2888        assert_eq!(fmt_stmt("var q = ^\"a/b\""), "var q = ^\"a/b\"");
2889    }
2890
2891    #[test]
2892    fn spacing_keywords_paren_callee_and_grouping() {
2893        assert_eq!(
2894            fmt_stmt("var p = preload ( \"res://x.gd\" )"),
2895            "var p = preload(\"res://x.gd\")"
2896        );
2897        assert_eq!(fmt_stmt("var x = a if c else b"), "var x = a if c else b"); // ternary
2898        assert_eq!(fmt_stmt("var y = not  flag"), "var y = not flag");
2899        assert_eq!(fmt_stmt("var z = n is int"), "var z = n is int");
2900        // a redundant grouping paren after a value-keyword is stripped; a precedence-significant one
2901        // is kept (with its space after the keyword), and a call paren hugs.
2902        assert_eq!(fmt_stmt("return ( x )"), "return x");
2903        assert_eq!(fmt_stmt("return ( a + b ) * c"), "return (a + b) * c");
2904    }
2905
2906    #[test]
2907    fn spacing_lambda_func_paren_is_tight() {
2908        // Corpus regression: a lambda `func(...)` must hug its `func` — `func (` does not parse.
2909        // (A named function declaration has `func name(`, which is unaffected.) A single-statement
2910        // lambda body that fits is collapsed onto one line, matching gdformat.
2911        assert_eq!(
2912            fmt_stmt("var cb = func( ) -> void:\n\t\tpass"),
2913            "var cb = func() -> void: pass"
2914        );
2915        assert_eq!(
2916            fmt_stmt("var g = func(_text:String)->void:\n\t\tpass"),
2917            "var g = func(_text: String) -> void: pass"
2918        );
2919        assert_eq!(
2920            fmt("func named(a,b):\n\tpass\n"),
2921            "func named(a, b):\n\tpass\n"
2922        );
2923    }
2924
2925    #[test]
2926    fn multiline_lambda_collapses_when_single_statement_else_kept() {
2927        // gdformat inlines a multi-line lambda whose body is a single simple statement once the
2928        // enclosing statement fits — but keeps a multi-statement (or non-fitting) lambda body.
2929        assert_eq!(
2930            fmt("func _r():\n\tx.connect(func() -> void:\n\t\tdo_thing()\n\t)\n"),
2931            "func _r():\n\tx.connect(func() -> void: do_thing())\n"
2932        );
2933        // a two-statement lambda body stays multi-line; the lambda argument explodes onto its own
2934        // line (gdformat's `_format_lambda_to_multiple_lines`, reached via the multi-line re-wrap).
2935        let multi = "func _r():\n\tx.connect(func() -> void:\n\t\ta()\n\t\tb()\n\t)\n";
2936        let out = fmt(multi);
2937        assert_eq!(
2938            out,
2939            "func _r():\n\tx.connect(\n\t\tfunc() -> void:\n\t\t\ta()\n\t\t\tb()\n\t)\n"
2940        );
2941        assert!(parses_clean(&out), "{out:?}");
2942        assert_eq!(fmt(&out), out, "idempotent");
2943    }
2944
2945    #[test]
2946    fn inline_lambda_value_in_exploded_dict_is_rendered() {
2947        // A dict whose values include inline lambdas must wrap (the dict overflows) with each lambda
2948        // kept inline — the CST wrapper renders `LambdaExpr` rather than bailing to the heuristic.
2949        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";
2950        let out = fmt(src);
2951        assert!(
2952            out.contains("\"on_changed\": func(x): cb.call(x),"),
2953            "{out:?}"
2954        );
2955        assert!(
2956            out.contains("\"on_submit\": func(): add.call(),"),
2957            "{out:?}"
2958        );
2959        assert!(parses_clean(&out), "{out:?}");
2960        assert_eq!(fmt(&out), out, "idempotent");
2961    }
2962
2963    #[test]
2964    fn colon_dict_value_drops_below_key_when_entry_overflows() {
2965        // A dict entry whose value pushes the line past the width splits `"key":` / value-below
2966        // (gdformat's `_format_kv_pair_to_multiple_lines`); a fitting sibling stays inline.
2967        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";
2968        let out = fmt(src);
2969        assert!(
2970            out.contains("\t\t\t\"text\":\n"),
2971            "value should drop below key: {out:?}"
2972        );
2973        assert!(
2974            out.contains("\"style\": {\"c\": 1}"),
2975            "fitting entry stays inline: {out:?}"
2976        );
2977        assert!(parses_clean(&out), "{out:?}");
2978        assert_eq!(fmt(&out), out, "idempotent");
2979    }
2980
2981    #[test]
2982    fn dot_chain_with_lambda_wraps_bottom_up_not_leading_dot() {
2983        // gdformat always bottom-ups a dot-chain that contains a lambda (a Godot-parser-bug
2984        // workaround), even when a magic comma deep inside would otherwise force leading-dot.
2985        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";
2986        let out = fmt(src);
2987        assert!(out.contains("\treturn V.fc("), "should bottom-up: {out:?}");
2988        assert!(!out.contains(". fc"), "should not leading-dot: {out:?}");
2989        assert!(parses_clean(&out), "{out:?}");
2990        assert_eq!(fmt(&out), out, "idempotent");
2991    }
2992
2993    #[test]
2994    fn over_indented_lambda_in_brackets_does_not_corrupt() {
2995        // The exact corpus shapes (godot-demo-projects rhythm_game) that used to format to
2996        // non-parsing code: the lambda HEADER sits on its own bracket-continuation line at an
2997        // author-chosen over-indent, and the BODY (synthetic-Newline) must stay aligned with it,
2998        // not snap back to block depth. Both are now kept verbatim and parse.
2999        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";
3000        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";
3001        for src in [note_manager, main_gd] {
3002            // safe_mode OFF so a regression would surface as a real assert, not a silent fallback.
3003            let cfg = FmtConfig {
3004                safe_mode: false,
3005                ..FmtConfig::default()
3006            };
3007            let out = format(src, &cfg);
3008            assert!(parses_clean(&out), "lambda-in-brackets must parse: {out:?}");
3009            assert!(
3010                super::same_significant_tokens(src, &out),
3011                "tokens changed: {out:?}"
3012            );
3013            assert_eq!(format(&out, &cfg), out, "idempotent: {out:?}");
3014        }
3015    }
3016
3017    #[test]
3018    fn spacing_annotation_is_tight() {
3019        assert_eq!(
3020            fmt("@export_range(0,100)\nvar speed = 1\n"),
3021            "@export_range(0, 100)\nvar speed = 1\n"
3022        );
3023        assert_eq!(fmt("@export var hp=100\n"), "@export var hp = 100\n");
3024    }
3025
3026    #[test]
3027    fn spacing_colon_in_brackets_left_verbatim() {
3028        // GDScript has no Python-style slice syntax, so a colon inside `[ ]` never appears in valid
3029        // code — but defensively we leave its spacing verbatim (not locally distinguishable from
3030        // other colon roles). Exercised with safe_mode OFF, since the construct does not parse; the
3031        // rest of the line is still normalized while the `[a:b]` colon spacing is preserved.
3032        let cfg = FmtConfig {
3033            safe_mode: false,
3034            ..FmtConfig::default()
3035        };
3036        assert_eq!(
3037            format("func _f():\n\tvar d = data[a:b]\n", &cfg),
3038            "func _f():\n\tvar d = data[a:b]\n"
3039        );
3040        assert_eq!(
3041            format("func _f():\n\tvar e=data[a : b]\n", &cfg),
3042            "func _f():\n\tvar e = data[a : b]\n"
3043        );
3044    }
3045
3046    #[test]
3047    fn spacing_is_idempotent_across_cases() {
3048        let cases = [
3049            "var x = a+b*c-d",
3050            "func f(a,b:int=1)->int:\n\treturn a",
3051            "var n = get_node($Player/Bone).position",
3052            "var d = {\"k\": foo(-1, 2), \"m\": items.slice(i, j)}",
3053            "var z = a if b<c else -d",
3054        ];
3055        for c in cases {
3056            let src = format!("func _w():\n\t{c}\n");
3057            let once = fmt(&src);
3058            assert_eq!(fmt(&once), once, "not idempotent for {c:?}: {once:?}");
3059            assert!(parses_clean(&once), "did not parse: {once:?}");
3060            assert!(
3061                super::same_significant_tokens(&src, &once),
3062                "tokens changed for {c:?}"
3063            );
3064        }
3065    }
3066
3067    // ---- Phase-4 increment B: blank-line policy ----
3068
3069    #[test]
3070    fn blank_lines_collapsed_top_level_to_two() {
3071        // 4 blank lines between two top-level functions collapse to 2 (the cap uses the *next*
3072        // line's depth — 0 here — even though the Dedent lands after the blanks).
3073        let src = "func a():\n\tpass\n\n\n\n\nfunc b():\n\tpass\n";
3074        assert_eq!(fmt(src), "func a():\n\tpass\n\n\nfunc b():\n\tpass\n");
3075    }
3076
3077    #[test]
3078    fn blank_lines_collapsed_inside_block_to_one() {
3079        let src = "func a():\n\tvar x = 1\n\n\n\n\tvar y = 2\n";
3080        assert_eq!(fmt(src), "func a():\n\tvar x = 1\n\n\tvar y = 2\n");
3081    }
3082
3083    #[test]
3084    fn leading_blank_lines_stripped() {
3085        assert_eq!(fmt("\n\n\nfunc a():\n\tpass\n"), "func a():\n\tpass\n");
3086    }
3087
3088    #[test]
3089    fn single_blank_between_top_defs_is_grown_to_two() {
3090        // gdformat enforces exactly 2 blank lines around top-level defs; a single blank is grown.
3091        let src = "func a():\n\tpass\n\nfunc b():\n\tpass\n";
3092        assert_eq!(fmt(src), "func a():\n\tpass\n\n\nfunc b():\n\tpass\n");
3093    }
3094
3095    #[test]
3096    fn blank_lines_inside_a_multiline_string_are_untouched() {
3097        // The blank line lives inside a `"""..."""` token, NOT between logical lines — the
3098        // token-level pass must never see it as a collapsible blank.
3099        let src = "func a():\n\tvar s = \"\"\"x\n\n\n\ny\"\"\"\n\treturn s\n";
3100        let out = fmt(src);
3101        assert!(
3102            out.contains("x\n\n\n\ny"),
3103            "string interior collapsed: {out:?}"
3104        );
3105        assert!(super::same_significant_tokens(src, &out));
3106    }
3107
3108    #[test]
3109    fn blank_lines_off_preserved() {
3110        let cfg = FmtConfig {
3111            collapse_blank_lines: false,
3112            ..FmtConfig::default()
3113        };
3114        let src = "func a():\n\tpass\n\n\n\n\nfunc b():\n\tpass\n";
3115        assert_eq!(format(src, &cfg), src);
3116    }
3117
3118    #[test]
3119    fn spacing_off_is_indentation_only() {
3120        // With `normalize_spacing` off, the formatter touches indentation only (the old behavior):
3121        // intra-line spacing is left exactly as written.
3122        let cfg = FmtConfig {
3123            normalize_spacing: false,
3124            ..FmtConfig::default()
3125        };
3126        let src = "func f():\n    var x=a+b\n";
3127        assert_eq!(format(src, &cfg), "func f():\n\tvar x=a+b\n");
3128    }
3129
3130    // ---- Phase-4 increment C: block-boundary comment indentation ----
3131
3132    #[test]
3133    fn trailing_body_comment_stays_at_block_depth() {
3134        // A comment after the last body statement, before a dedented def, stays at the body depth
3135        // (gdformat keeps it attached to the block it was written in, not the following def).
3136        let src = "func foo():\n\tpass\n\t# trailing\n\n\nfunc bar():\n\tpass\n";
3137        let out = fmt(src);
3138        assert_eq!(
3139            out,
3140            "func foo():\n\tpass\n\t# trailing\n\n\nfunc bar():\n\tpass\n"
3141        );
3142        assert_eq!(fmt(&out), out, "idempotent");
3143    }
3144
3145    #[test]
3146    fn comment_after_nested_block_stays_at_outer_body_depth() {
3147        // The "after-if" case: a comment authored at body depth, sitting between a deeper block and
3148        // a following body statement, keeps the body depth (not the deeper raw prepass depth).
3149        let src = "func f():\n\tif x:\n\t\tpass\n\t# back at body level\n\treturn\n";
3150        let out = fmt(src);
3151        assert_eq!(
3152            out,
3153            "func f():\n\tif x:\n\t\tpass\n\t# back at body level\n\treturn\n"
3154        );
3155        assert_eq!(fmt(&out), out, "idempotent");
3156    }
3157
3158    #[test]
3159    fn over_indented_comment_snaps_to_block_and_col0_stays() {
3160        // An over-indented comment snaps to the surrounding block; a column-0 comment stays at 0.
3161        assert_eq!(
3162            fmt("func f():\n\t\t\t# over\n\tpass\n"),
3163            "func f():\n\t# over\n\tpass\n"
3164        );
3165        assert_eq!(
3166            fmt("func f():\n# at col 0\n\tpass\n"),
3167            "func f():\n# at col 0\n\tpass\n"
3168        );
3169    }
3170
3171    // ---- Phase-4 increment C: blank-line insertion around definitions ----
3172
3173    #[test]
3174    fn two_blanks_inserted_around_top_level_defs() {
3175        let src = "extends Node\nfunc a():\n\tpass\nfunc b():\n\tpass\n";
3176        assert_eq!(
3177            fmt(src),
3178            "extends Node\n\n\nfunc a():\n\tpass\n\n\nfunc b():\n\tpass\n"
3179        );
3180    }
3181
3182    #[test]
3183    fn one_blank_inserted_between_methods_in_a_class() {
3184        // Inside a class: 1 blank between methods; none before the first member (after the header).
3185        let src = "class C:\n\tfunc a():\n\t\tpass\n\tfunc b():\n\t\tpass\n";
3186        assert_eq!(
3187            fmt(src),
3188            "class C:\n\tfunc a():\n\t\tpass\n\n\tfunc b():\n\t\tpass\n"
3189        );
3190    }
3191
3192    #[test]
3193    fn blanks_go_before_an_attached_comment_or_annotation_prefix() {
3194        // The 2 blanks land before the doc-comment/annotation that belongs to the def, not between.
3195        let src = "func a():\n\tpass\n## docs for b\n@warning_ignore(\"x\")\nfunc b():\n\tpass\n";
3196        assert_eq!(
3197            fmt(src),
3198            "func a():\n\tpass\n\n\n## docs for b\n@warning_ignore(\"x\")\nfunc b():\n\tpass\n"
3199        );
3200    }
3201
3202    #[test]
3203    fn blanks_inserted_after_a_def_before_a_following_non_def() {
3204        // gdformat surrounds a def: a top-level statement after a func body gets 2 blanks too.
3205        let src = "func a():\n\tpass\nvar x = 1\n";
3206        assert_eq!(fmt(src), "func a():\n\tpass\n\n\nvar x = 1\n");
3207    }
3208
3209    #[test]
3210    fn static_var_is_not_a_def_but_static_func_is() {
3211        // `static var` is an ordinary member (no surrounding blanks); `static func` is a def.
3212        let src = "var a = 1\nstatic var b = 2\nstatic func c():\n\tpass\n";
3213        assert_eq!(
3214            fmt(src),
3215            "var a = 1\nstatic var b = 2\n\n\nstatic func c():\n\tpass\n"
3216        );
3217    }
3218
3219    #[test]
3220    fn no_blank_before_the_first_def_in_the_file() {
3221        let src = "func a():\n\tpass\n";
3222        assert_eq!(fmt(src), src);
3223    }
3224
3225    #[test]
3226    fn blank_insertion_off_leaves_blanks_alone() {
3227        let cfg = FmtConfig {
3228            insert_blank_lines: false,
3229            ..FmtConfig::default()
3230        };
3231        let src = "func a():\n\tpass\nfunc b():\n\tpass\n";
3232        assert_eq!(format(src, &cfg), src);
3233    }
3234
3235    // ---- Phase-4 increment C: line-ending preservation ----
3236
3237    #[test]
3238    fn crlf_line_endings_are_preserved() {
3239        // A CRLF file is formatted (spacing + blank policy applied) but stays CRLF — never churned
3240        // to LF.
3241        let src = "func a():\r\n\tvar x=1\r\nfunc b():\r\n\tpass\r\n";
3242        assert_eq!(
3243            fmt(src),
3244            "func a():\r\n\tvar x = 1\r\n\r\n\r\nfunc b():\r\n\tpass\r\n"
3245        );
3246    }
3247
3248    #[test]
3249    fn crlf_preserved_including_multiline_string_interior() {
3250        // A `\r\n` inside a multi-line string round-trips (normalize to LF, format, restore CRLF).
3251        let src = "var s = \"\"\"a\r\nb\"\"\"\r\n";
3252        let out = fmt(src);
3253        assert_eq!(out, "var s = \"\"\"a\r\nb\"\"\"\r\n");
3254        assert!(super::same_significant_tokens(src, &out));
3255    }
3256
3257    #[test]
3258    fn lf_files_stay_lf() {
3259        let src = "func a():\n\tpass\n";
3260        assert!(!fmt(src).contains('\r'));
3261    }
3262
3263    // ---- Phase-4 increment C: length-driven reflow ----
3264
3265    #[test]
3266    fn reflow_compact_call() {
3267        let src = "func f():\n\tvar long_call = some_function(argument_one, argument_two, argument_three, argument_four, arg_five)\n";
3268        assert_eq!(
3269            fmt(src),
3270            "func f():\n\tvar long_call = some_function(\n\t\targument_one, argument_two, argument_three, argument_four, arg_five\n\t)\n"
3271        );
3272        assert_eq!(fmt(&fmt(src)), fmt(src), "idempotent");
3273    }
3274
3275    #[test]
3276    fn reflow_compact_array_and_dict() {
3277        let arr = "func f():\n\tvar arr = [element_one, element_two, element_three, element_four, element_five, element_six, seven]\n";
3278        assert_eq!(
3279            fmt(arr),
3280            "func f():\n\tvar arr = [\n\t\telement_one, element_two, element_three, element_four, element_five, element_six, seven\n\t]\n"
3281        );
3282        let dct = "func f():\n\tvar d = {\"key_one\": value_one, \"key_two\": value_two, \"key_three\": value_three, \"key4\": value_four}\n";
3283        assert_eq!(
3284            fmt(dct),
3285            "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"
3286        );
3287    }
3288
3289    #[test]
3290    fn reflow_exploded_when_compact_too_long() {
3291        // When even the single compact continuation line exceeds the width, explode one per line —
3292        // with NO trailing comma (length-driven). Byte-identical to gdformat.
3293        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";
3294        assert_eq!(
3295            fmt(src),
3296            "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"
3297        );
3298        assert_eq!(fmt(&fmt(src)), fmt(src), "idempotent");
3299    }
3300
3301    #[test]
3302    fn reflow_nested_outer_explodes_inner_stays_inline() {
3303        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";
3304        assert_eq!(
3305            fmt(src),
3306            "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"
3307        );
3308    }
3309
3310    #[test]
3311    fn reflow_short_lines_stay_flat() {
3312        let src = "func f():\n\tvar short = call(a, b, c)\n";
3313        assert_eq!(fmt(src), src);
3314    }
3315
3316    #[test]
3317    fn reflow_off_leaves_long_lines() {
3318        let cfg = FmtConfig {
3319            reflow: false,
3320            ..FmtConfig::default()
3321        };
3322        let src = "func f():\n\tvar long_call = some_function(argument_one, argument_two, argument_three, argument_four, arg_five)\n";
3323        // spacing still normalized, but no wrapping
3324        assert_eq!(format(src, &cfg), src);
3325    }
3326
3327    #[test]
3328    fn reflow_keeps_an_already_canonical_wrapped_statement() {
3329        // A wrapped statement that is too long to collapse stays in its canonical exploded form.
3330        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";
3331        assert_eq!(fmt(src), src);
3332    }
3333
3334    // ---- Phase-4: layout ownership (re-flow already-multi-line statements) ----
3335
3336    #[test]
3337    fn reflow_collapses_a_short_hand_wrapped_statement() {
3338        // A statement the author wrapped that now fits is collapsed back onto one line.
3339        let src = "func f():\n\tvar x = call(\n\t\ta,\n\t\tb,\n\t\tc\n\t)\n";
3340        assert_eq!(fmt(src), "func f():\n\tvar x = call(a, b, c)\n");
3341    }
3342
3343    #[test]
3344    fn reflow_rewraps_a_still_too_long_wrapped_statement_idempotently() {
3345        // A wrapped statement still over the limit is re-laid-out to canonical form, idempotently.
3346        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";
3347        let out = fmt(src);
3348        assert!(
3349            out.contains("some_long_function_name(\n"),
3350            "should wrap: {out:?}"
3351        );
3352        assert_eq!(fmt(&out), out, "idempotent");
3353    }
3354
3355    #[test]
3356    fn reflow_keeps_a_statement_with_an_inner_comment_verbatim() {
3357        // A comment inside the brackets blocks a safe collapse — the statement is preserved.
3358        let src = "func f():\n\tvar x = call(\n\t\ta,  # first\n\t\tb,\n\t)\n";
3359        let out = fmt(src);
3360        assert!(out.contains("# first"), "{out:?}");
3361        assert!(super::same_significant_tokens(src, &out));
3362        assert_eq!(fmt(&out), out, "idempotent");
3363    }
3364
3365    // ---- Phase-4: CST-driven wrapping (gdformat parity — see `wrap`) ----
3366
3367    #[test]
3368    fn wrap_func_param_list_explodes_with_return_type_on_close_line() {
3369        // A func header over the limit wraps its *parameter list*; the `-> void:` stays a suffix on the
3370        // closing-paren line (it is never itself wrapped), matching gdformat.
3371        let src = "func process(first_argument: int, second_argument: String, third_argument: float, fourth: bool) -> void:\n\tpass\n";
3372        let out = fmt(src);
3373        assert_eq!(
3374            out,
3375            "func process(\n\tfirst_argument: int, second_argument: String, third_argument: float, fourth: bool\n) -> void:\n\tpass\n"
3376        );
3377        assert_eq!(fmt(&out), out, "idempotent");
3378    }
3379
3380    #[test]
3381    fn wrap_method_chain_bottom_up_wraps_final_call_args() {
3382        // When the chain prefix fits, gdformat wraps only the final call's arguments (bottom-up).
3383        let src = "func f():\n\tobject.method_one(argument).method_two(argument).method_three(argument).method_four(argument_xxxx)\n";
3384        let out = fmt(src);
3385        assert_eq!(
3386            out,
3387            "func f():\n\tobject.method_one(argument).method_two(argument).method_three(argument).method_four(\n\t\targument_xxxx\n\t)\n"
3388        );
3389        assert_eq!(fmt(&out), out, "idempotent");
3390    }
3391
3392    #[test]
3393    fn wrap_method_chain_explodes_leading_dot_when_compact_overflows() {
3394        // When even the compact chain overflows, gdformat wraps it in parens and breaks at each `.`,
3395        // leading-dot style (`. method`).
3396        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";
3397        let out = fmt(src);
3398        assert!(
3399            out.contains("\t(\n\t\ttween\n\t\t. tween_property("),
3400            "{out}"
3401        );
3402        assert!(
3403            out.contains("\n\t\t. set_ease(Tween.EASE_OUT)\n\t)\n"),
3404            "{out}"
3405        );
3406        assert_eq!(fmt(&out), out, "idempotent");
3407    }
3408
3409    #[test]
3410    fn wrap_assignment_operator_chain_wraps_in_parens_compact_first() {
3411        // A too-long assignment RHS that is an operator chain is wrapped in injected parens; the chain
3412        // stays on one continuation line while it fits there (gdformat's compact-first), exploding at
3413        // the operator only when even that overflows.
3414        let src = "func f():\n\tgravity_value = first_long_operand_value_xx * second_long_operand_value_yy * third_long_operand_value_zz\n";
3415        let out = fmt(src);
3416        assert_eq!(
3417            out,
3418            "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"
3419        );
3420        assert_eq!(fmt(&out), out, "idempotent");
3421    }
3422
3423    #[test]
3424    fn wrap_dict_entry_drops_multiline_value_below_the_key() {
3425        // gdformat's kv-pair rule: a multi-line dict-entry value drops to its own line(s) below the
3426        // `key =`, and a magic trailing comma forces the whole nest exploded.
3427        let src = "func f():\n\tvar d := {player = {position = a, health = b,}, enemies = [],}\n";
3428        let out = fmt(src);
3429        assert_eq!(
3430            out,
3431            "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"
3432        );
3433        assert_eq!(fmt(&out), out, "idempotent");
3434    }
3435
3436    #[test]
3437    fn wrap_magic_comma_chain_explodes_leading_dot() {
3438        // A method chain forced multi-line by a magic comma inside it goes straight to leading-dot.
3439        let src = "func f():\n\treturn obj.method({\"a\": 1, \"b\": 2,})\n";
3440        let out = fmt(src);
3441        assert_eq!(
3442            out,
3443            "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"
3444        );
3445        assert_eq!(fmt(&out), out, "idempotent");
3446    }
3447
3448    #[test]
3449    fn column_zero_trailing_region_comment_stays_put_without_forced_blanks() {
3450        // gdformat keeps a column-0 `#endregion` at column 0 and forces no blank lines before a
3451        // *trailing* comment (one that ends its block).
3452        let src = "func f():\n\tvar x = 1\n#endregion\n";
3453        assert_eq!(fmt(src), src);
3454        let src2 = "#region Section\nvar a = 1\nvar b = 2\n#endregion\n";
3455        assert_eq!(fmt(src2), src2);
3456    }
3457
3458    #[test]
3459    fn inline_suite_bodies_split_but_lambdas_stay_inline() {
3460        // gdformat moves an inline suite body to its own indented line; an inline *lambda* body stays.
3461        assert_eq!(
3462            fmt("func f():\n\tif cond: do_thing()\n"),
3463            "func f():\n\tif cond:\n\t\tdo_thing()\n"
3464        );
3465        assert_eq!(fmt("func g(): return 1\n"), "func g():\n\treturn 1\n");
3466        assert_eq!(
3467            fmt("func i():\n\tif a: b()\n\telse: c()\n"),
3468            "func i():\n\tif a:\n\t\tb()\n\telse:\n\t\tc()\n"
3469        );
3470        // an inline lambda body is preserved
3471        assert_eq!(
3472            fmt("func h():\n\tvar a := func(): return 1\n"),
3473            "func h():\n\tvar a := func(): return 1\n"
3474        );
3475    }
3476
3477    #[test]
3478    fn backslash_line_continuations_collapse_and_rewrap() {
3479        // gdformat collapses a `\`-continued statement and re-lays it out — onto one line when it now
3480        // fits, or re-wrapped (an operator chain becomes paren-wrapped) when it does not.
3481        assert_eq!(
3482            fmt("func f():\n\tvar x = a + \\\n\t\tb\n"),
3483            "func f():\n\tvar x = a + b\n"
3484        );
3485        let out = fmt(
3486            "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",
3487        );
3488        assert!(out.contains("\tif (\n"), "{out}");
3489        assert!(!out.contains('\\'), "backslash removed: {out}");
3490        assert_eq!(fmt(&out), out, "idempotent");
3491    }
3492
3493    #[test]
3494    fn redundant_grouping_parens_are_stripped() {
3495        // gdformat strips parens that merely group a standalone expression (value / condition /
3496        // iterable / argument / element / nested), but keeps precedence-significant ones.
3497        assert_eq!(fmt("var x = (y)\n"), "var x = y\n");
3498        assert_eq!(
3499            fmt("func f():\n\treturn (g(a))\n"),
3500            "func f():\n\treturn g(a)\n"
3501        );
3502        assert_eq!(
3503            fmt("func f():\n\tfor i in (a * b):\n\t\tpass\n"),
3504            "func f():\n\tfor i in a * b:\n\t\tpass\n"
3505        );
3506        assert_eq!(fmt("var a = g((x))\n"), "var a = g(x)\n"); // call arg + nested
3507        assert_eq!(fmt("var a = {(k): (v)}\n"), "var a = {k: v}\n");
3508        // precedence parens kept; an expr-statement assignment RHS keeps its parens (gdformat does too)
3509        assert_eq!(fmt("var a = (b + c) * d\n"), "var a = (b + c) * d\n");
3510        assert_eq!(fmt("func f():\n\tx = (y)\n"), "func f():\n\tx = (y)\n");
3511    }
3512
3513    #[test]
3514    fn nested_multiline_bracket_inside_lambda_body_parses_and_wraps() {
3515        // A lambda body that contains a call which itself wraps (`return new(\n …\n)`) must keep the
3516        // nested bracket's lines indentation-suppressed (they sit a level *deeper* than the lambda
3517        // body), then re-lay-out to gdformat's exact shape.
3518        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";
3519        let out = fmt(src);
3520        assert!(parses_clean(&out), "{out:?}");
3521        assert!(
3522            out.contains("\tvar v = use_memo(\n\t\tfunc():"),
3523            "lambda explodes: {out:?}"
3524        );
3525        assert!(
3526            out.contains("\t\t\treturn make.build(\n"),
3527            "nested call wraps: {out:?}"
3528        );
3529        assert_eq!(fmt(&out), out, "idempotent");
3530    }
3531
3532    #[test]
3533    fn subscript_on_a_long_call_chain_wraps_leading_dot() {
3534        // `Fmt.format(…)["text"]` is a dot-chain ending in a subscript; when the flat subscriptee
3535        // overflows, gdformat wraps leading-dot, keeping `. format(…)["text"]` (call then index) on
3536        // one segment line. (Routes `IndexExpr`-on-a-chain through the dot-chain formatter.)
3537        let src = "func f():\n\tvar no_space: String = Fmt.format(src, {\"singleAttributePerLine\": true, \"insertSpaceBeforeSelfClose\": false})[\"text\"]\n";
3538        let out = fmt(src);
3539        assert!(
3540            out.contains("\tvar no_space: String = (\n\t\tFmt\n"),
3541            "leading-dot: {out:?}"
3542        );
3543        assert!(
3544            out.contains("\n\t\t. format(src, {") && out.contains("})[\"text\"]\n"),
3545            "call+index segment: {out:?}"
3546        );
3547        assert!(parses_clean(&out), "{out:?}");
3548        assert_eq!(fmt(&out), out, "idempotent");
3549    }
3550
3551    #[test]
3552    fn dot_chain_with_lambda_in_earlier_segment_uses_leading_dot() {
3553        // gdformat bottom-ups a chain whose lambda is in the *final* call's args, but falls back to
3554        // leading-dot when the lambda sits in an earlier segment (`a.m(func…).n(…)`).
3555        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";
3556        let out = fmt(src);
3557        assert!(
3558            out.contains("\tvar t := (\n"),
3559            "leading-dot paren wrap: {out:?}"
3560        );
3561        assert!(
3562            out.contains("\n\t\t. set_delay("),
3563            "explodes at each dot: {out:?}"
3564        );
3565        assert!(parses_clean(&out), "{out:?}");
3566        assert_eq!(fmt(&out), out, "idempotent");
3567    }
3568
3569    #[test]
3570    fn lambda_body_ending_in_arg_separator_comma_parses_and_is_preserved() {
3571        // gdformat emits `func(): … return X,` where the `,` closes the lambda body mid-line and
3572        // separates the enclosing call's arguments. The prepass must suppress the lambda body's now
3573        // -stale trailing newline so the `,` and the next argument parse — and the statement (with an
3574        // inner comment, so it stays verbatim) round-trips unchanged.
3575        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";
3576        let out = fmt(src);
3577        assert!(out.contains("\t\t\treturn null,\n"), "{out:?}");
3578        assert!(out.contains("# c1"), "comment preserved: {out:?}");
3579        assert!(parses_clean(&out), "{out:?}");
3580        assert_eq!(fmt(&out), out, "idempotent");
3581    }
3582
3583    #[test]
3584    fn comments_in_a_reshaped_lambda_body_are_threaded_through() {
3585        // A statement reshaped by the CST wrapper carries its block comments: a trailing inline
3586        // comment stays on its statement's line (two-space offset), a standalone comment keeps its own
3587        // line — both at the reshaped indent. (The comment-multiset net falls back to verbatim if any
3588        // comment can't be placed, so this can only improve correctness.)
3589        assert_eq!(
3590            fmt(
3591                "func r():\n\tx.connect(func():\n\t\ta()  # trailing\n\t\t# standalone\n\t\tb()\n\t)\n"
3592            ),
3593            "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"
3594        );
3595    }
3596
3597    #[test]
3598    fn trailing_comment_on_a_reshaped_statement_survives() {
3599        // A comment trailing the whole statement (`const X := {…}  # note`) lands on the rendered last
3600        // line of the reshaped statement.
3601        let src = "const RESERVED := {\"a\": one_long_key_value_that_forces_a_wrap, \"b\": another_long_one_here}  # note\n";
3602        let out = fmt(src);
3603        assert!(out.contains("}  # note\n"), "{out:?}");
3604        assert!(parses_clean(&out), "{out:?}");
3605        assert_eq!(fmt(&out), out, "idempotent");
3606    }
3607
3608    #[test]
3609    fn lambda_arg_trailing_comment_keeps_separator_before_it() {
3610        // A lambda argument whose last body statement carries a trailing comment: the enclosing call's
3611        // argument separator `,` lands before the comment (`return when,  # block`), not after it
3612        // (which would swallow the `,` into the comment text).
3613        let src = "func p():\n\tg(func():\n\t\tif c:\n\t\t\twork()\n\t\treturn when  # block\n\t, other)\n";
3614        assert_eq!(
3615            fmt(src),
3616            "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"
3617        );
3618        assert_eq!(fmt(&fmt(src)), fmt(src), "idempotent");
3619    }
3620
3621    #[test]
3622    fn multiline_string_operator_chain_paren_wraps_verbatim() {
3623        // gdformat paren-wraps `x = """…""" % [args]`: the string's interior lines stay verbatim
3624        // (literal content, not re-indented), the `% [` goes on its own line, the array explodes.
3625        let src =
3626            "func p():\n\t$X.text = \"\"\"%d FPS\n\nObjects:\n%d\n\"\"\" % [\nfps,\nobjs,\n]\n";
3627        assert_eq!(
3628            fmt(src),
3629            "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"
3630        );
3631        assert_eq!(fmt(&fmt(src)), fmt(src), "idempotent");
3632    }
3633
3634    #[test]
3635    fn comments_in_a_collection_are_threaded_through() {
3636        // A bracket list carries its comments when it explodes: one trailing the open bracket, one
3637        // trailing an element, and a standalone one keeping its own line — all at the element indent.
3638        assert_eq!(
3639            fmt("var x = [  # head\n\t1,  # one\n\t# mid\n\t2,\n]\n"),
3640            "var x = [  # head\n\t1,  # one\n\t# mid\n\t2,\n]\n"
3641        );
3642        // a standalone comment forces the collection multi-line even when it would otherwise fit
3643        let out = fmt("var d = {\n\t# note\n\t\"a\": 1,\n}\n");
3644        assert!(out.contains("\t# note\n\t\"a\": 1,"), "{out:?}");
3645        assert!(parses_clean(&out), "{out:?}");
3646        assert_eq!(fmt(&out), out, "idempotent");
3647    }
3648
3649    #[test]
3650    fn semicolons_inside_lambda_body_expand_at_correct_depth() {
3651        // A multi-line lambda body's `;`-separated statements expand one-per-line at the body depth
3652        // (the lambda's block counts as a depth increment even though its own body is not split inline).
3653        assert_eq!(
3654            fmt("func r():\n\tvar cb = func():\n\t\ta(); b(); c()\n"),
3655            "func r():\n\tvar cb = func():\n\t\ta()\n\t\tb()\n\t\tc()\n"
3656        );
3657    }
3658
3659    #[test]
3660    fn inline_blocks_inside_inner_class_methods_expand_at_correct_depth() {
3661        // An inner class's `ClassBody` is one indent level deep, so `;`-separated statements and an
3662        // inline suite body inside its methods must expand at the method-body depth, not one short.
3663        assert_eq!(
3664            fmt("class P:\n\tfunc release():\n\t\ta = 1; b = 2\n"),
3665            "class P:\n\tfunc release():\n\t\ta = 1\n\t\tb = 2\n"
3666        );
3667        assert_eq!(
3668            fmt("class P:\n\tfunc release():\n\t\tif not x: return\n"),
3669            "class P:\n\tfunc release():\n\t\tif not x:\n\t\t\treturn\n"
3670        );
3671    }
3672
3673    #[test]
3674    fn inner_class_extends_moves_to_its_own_body_line() {
3675        // gdformat splits `class C extends B:` into `class C:` + a leading `extends B` body line.
3676        assert_eq!(
3677            fmt("class CRProps extends RefCounted:\n\tvar x = 1\n"),
3678            "class CRProps:\n\textends RefCounted\n\tvar x = 1\n"
3679        );
3680        // a file-level `extends` (not an inner class) is left alone
3681        assert_eq!(
3682            fmt("extends Node\n\nvar x = 1\n"),
3683            "extends Node\n\nvar x = 1\n"
3684        );
3685        // idempotent
3686        let once = fmt("class C extends B:\n\tpass\n");
3687        assert_eq!(fmt(&once), once);
3688    }
3689
3690    #[test]
3691    fn long_annotated_var_splits_annotation_to_its_own_line() {
3692        // A short annotated var stays prepended; a long one whose value cannot wrap moves the
3693        // annotation to its own line (gdformat's `indent + len(ann) + len(line) <= max`, no space).
3694        assert_eq!(fmt("@onready var x = $Path\n"), "@onready var x = $Path\n");
3695        let long = "@onready var pelvis: PhysicalBone3D = $\"root/root_001/Skeleton3D/PhysicalBoneSimulator3D/Physical Bone pelvis\"\n";
3696        let out = fmt(long);
3697        assert!(out.starts_with("@onready\nvar pelvis:"), "{out:?}");
3698        assert_eq!(fmt(&out), out, "idempotent");
3699    }
3700
3701    #[test]
3702    fn empty_signal_parens_are_removed() {
3703        // gdformat writes `signal s` not `signal s()`; a signal with parameters keeps its list.
3704        assert_eq!(fmt("signal done()\n"), "signal done\n");
3705        assert_eq!(fmt("signal hit(x, y)\n"), "signal hit(x, y)\n");
3706        assert_eq!(fmt("signal a\n"), "signal a\n");
3707    }
3708
3709    #[test]
3710    fn semicolon_separated_statements_split() {
3711        // gdformat splits `;`-separated statements onto their own lines and drops a trailing `;`.
3712        assert_eq!(
3713            fmt("func f():\n\ta = 1; b = 2\n"),
3714            "func f():\n\ta = 1\n\tb = 2\n"
3715        );
3716        assert_eq!(fmt("func g():\n\tpass;\n"), "func g():\n\tpass\n");
3717        assert_eq!(
3718            fmt("func h():\n\tif c: a(); b()\n"),
3719            "func h():\n\tif c:\n\t\ta()\n\t\tb()\n"
3720        );
3721    }
3722
3723    #[test]
3724    fn inline_match_arm_and_property_bodies_split() {
3725        // A match arm sits one level below `match`, so its inline body splits to two deeper levels.
3726        assert_eq!(
3727            fmt("func f():\n\tmatch x:\n\t\t\"inc\": return state + 1\n"),
3728            "func f():\n\tmatch x:\n\t\t\"inc\":\n\t\t\treturn state + 1\n"
3729        );
3730        // A property setter shorthand splits below the `var`.
3731        assert_eq!(
3732            fmt("var active: bool = false: set = set_active\n"),
3733            "var active: bool = false:\n\tset = set_active\n"
3734        );
3735        // Inline getter/setter bodies split too.
3736        assert_eq!(
3737            fmt("var q: int = 0:\n\tget: return _q\n\tset(v): _q = v\n"),
3738            "var q: int = 0:\n\tget:\n\t\treturn _q\n\tset(v):\n\t\t_q = v\n"
3739        );
3740    }
3741
3742    #[test]
3743    fn blank_runs_collapse_to_one_then_defs_restore_two() {
3744        // gdformat squeezes every blank run to one, then re-adds the 2nd blank only around defs.
3745        // Between two non-defs, that means a single blank regardless of how many were authored.
3746        assert_eq!(
3747            fmt("var a = 1\n\n\nvar b = 2\n"),
3748            "var a = 1\n\nvar b = 2\n"
3749        );
3750        assert_eq!(
3751            fmt("extends Node\n\n\nvar b = 2\n"),
3752            "extends Node\n\nvar b = 2\n"
3753        );
3754        // Two top-level funcs still get two blanks (def-forced), no matter the authored count.
3755        assert_eq!(
3756            fmt("func a():\n\tpass\n\n\n\nfunc b():\n\tpass\n"),
3757            "func a():\n\tpass\n\n\nfunc b():\n\tpass\n"
3758        );
3759    }
3760
3761    #[test]
3762    fn annotation_prefixed_def_forces_blanks_only_after_a_def() {
3763        // An `@rpc func` after a non-def (`extends`/`var`) keeps its source blanks; after a def it gets
3764        // the usual 2; a plain func always forces 2.
3765        assert_eq!(
3766            fmt("extends Node\n\n@rpc(\"x\")\nfunc f():\n\tpass\n"),
3767            "extends Node\n\n@rpc(\"x\")\nfunc f():\n\tpass\n"
3768        );
3769        assert_eq!(
3770            fmt("func a():\n\tpass\n@rpc(\"x\")\nfunc b():\n\tpass\n"),
3771            "func a():\n\tpass\n\n\n@rpc(\"x\")\nfunc b():\n\tpass\n"
3772        );
3773        assert_eq!(
3774            fmt("extends Node\nfunc f():\n\tpass\n"),
3775            "extends Node\n\n\nfunc f():\n\tpass\n"
3776        );
3777    }
3778
3779    #[test]
3780    fn leading_bom_is_preserved() {
3781        // gdformat keeps a leading byte-order mark; the reflow must not drop it when it re-renders the
3782        // first statement from its significant tokens.
3783        let src = "\u{feff}class_name Foo\nvar x = 1\n";
3784        let out = fmt(src);
3785        assert!(out.starts_with('\u{feff}'), "{out:?}");
3786        assert_eq!(&out[3..], "class_name Foo\nvar x = 1\n");
3787        assert_eq!(fmt(&out), out, "idempotent");
3788    }
3789
3790    #[test]
3791    fn soft_keyword_member_call_hugs_paren_but_statement_keeps_space() {
3792        // `obj.match(x)` is a member call (tight `(`), while a `match (x):` statement keeps its space.
3793        assert_eq!(
3794            fmt("func f():\n\tvar m = obj.match(\"a\", \"b\")\n"),
3795            "func f():\n\tvar m = obj.match(\"a\", \"b\")\n"
3796        );
3797        assert_eq!(
3798            fmt("func f():\n\tmatch (x):\n\t\tpass\n"),
3799            "func f():\n\tmatch (x):\n\t\tpass\n"
3800        );
3801    }
3802
3803    // ---- Phase-4: string-quote normalization (gdformat / Black rule) ----
3804
3805    #[test]
3806    fn canonical_string_rules() {
3807        use super::canonical_string as c;
3808        assert_eq!(c("'simple'"), "\"simple\""); // prefer double
3809        assert_eq!(c("\"already\""), "\"already\""); // unchanged
3810        assert_eq!(c("'has \"x\" in'"), "'has \"x\" in'"); // more " than ' -> keep single
3811        assert_eq!(c("'a\\'b'"), "\"a'b\""); // escaped ' -> double, unescaped
3812        assert_eq!(c("'both \" and \\' x'"), "\"both \\\" and ' x\""); // tie -> double, re-escape
3813        assert_eq!(c("&'name'"), "&\"name\""); // StringName prefix kept
3814        assert_eq!(c("^'a/b'"), "^\"a/b\""); // NodePath prefix kept
3815        assert_eq!(c("r'raw\\n'"), "r\"raw\\n\""); // raw: body verbatim
3816        assert_eq!(c("r'has \"x\"'"), "r'has \"x\"'"); // raw with " -> keep single (cannot escape)
3817        // gdformat collapses a single-line triple-SINGLE to a regular string; triple-double + any
3818        // multi-line triple are left verbatim.
3819        assert_eq!(c("'''triple'''"), "\"triple\"");
3820        assert_eq!(c("'''say \"hi\"'''"), "'say \"hi\"'"); // body has " -> regular single
3821        assert_eq!(c("\"\"\"triple\"\"\""), "\"\"\"triple\"\"\""); // triple-double verbatim
3822        assert_eq!(c("'''line1\nline2'''"), "'''line1\nline2'''"); // multi-line verbatim
3823        assert_eq!(c("'\\t\\n'"), "\"\\t\\n\""); // non-quote escapes preserved
3824    }
3825
3826    #[test]
3827    fn quote_normalization_in_format() {
3828        assert_eq!(fmt("var a = 'simple'\n"), "var a = \"simple\"\n");
3829        assert_eq!(fmt("var b = 'has \"x\" in'\n"), "var b = 'has \"x\" in'\n");
3830        assert_eq!(fmt("var f = &'n'\n"), "var f = &\"n\"\n");
3831        // idempotent
3832        assert_eq!(fmt("var a = \"simple\"\n"), "var a = \"simple\"\n");
3833    }
3834
3835    // ---- Phase-4: magic trailing comma ----
3836
3837    #[test]
3838    fn magic_trailing_comma_explodes_with_comma() {
3839        // A magic trailing comma forces exploded-one-per-line WITH the comma, even though it fits.
3840        assert_eq!(
3841            fmt("var a = call(x, y,)\n"),
3842            "var a = call(\n\tx,\n\ty,\n)\n"
3843        );
3844        assert_eq!(fmt("var b = [1, 2,]\n"), "var b = [\n\t1,\n\t2,\n]\n");
3845        assert_eq!(fmt("var g = call(only,)\n"), "var g = call(\n\tonly,\n)\n");
3846    }
3847
3848    #[test]
3849    fn magic_trailing_comma_nested_forces_outer_without_own_comma() {
3850        // The inner magic group explodes WITH its comma; the outer is forced multi-line by the
3851        // descendant but, not being magic itself, has no trailing comma after its last element.
3852        assert_eq!(
3853            fmt("var d = outer(inner(a, b,), c)\n"),
3854            "var d = outer(\n\tinner(\n\t\ta,\n\t\tb,\n\t),\n\tc\n)\n"
3855        );
3856    }
3857
3858    #[test]
3859    fn magic_trailing_comma_is_idempotent() {
3860        let once = fmt("var a = call(x, y,)\n");
3861        assert_eq!(fmt(&once), once);
3862    }
3863
3864    // ---- Phase-4: inline-comment offset ----
3865
3866    #[test]
3867    fn inline_comments_get_two_spaces() {
3868        assert_eq!(fmt("var x = 1 # one\n"), "var x = 1  # one\n");
3869        assert_eq!(fmt("var y = 2     # many\n"), "var y = 2  # many\n");
3870        assert_eq!(
3871            fmt("func f(): # c\n\tpass ## doc\n"),
3872            "func f():  # c\n\tpass  ## doc\n"
3873        );
3874        // a standalone comment (its own line) is unaffected — it is indentation, not an offset
3875        assert_eq!(
3876            fmt("func f():\n\t# standalone\n\tpass\n"),
3877            "func f():\n\t# standalone\n\tpass\n"
3878        );
3879    }
3880
3881    // ---- Phase-4: format_range ----
3882
3883    #[test]
3884    fn format_range_edits_only_the_changed_lines_overlapping_the_selection() {
3885        let src = "func f():\n\tvar x = 1\n\tvar y=2\n";
3886        // line 2 (bytes 21..30) needs spacing; selecting it returns just that line's edit
3887        let e = super::format_range(src, &FmtConfig::default(), 21..30).unwrap();
3888        assert_eq!(e.range, 21..30);
3889        assert_eq!(e.new_text, "\tvar y = 2\n");
3890        // selecting the already-formatted line 1 → no edit
3891        assert!(super::format_range(src, &FmtConfig::default(), 10..21).is_none());
3892        // a fully-formatted document → no edit
3893        assert!(
3894            super::format_range("func f():\n\tvar y = 2\n", &FmtConfig::default(), 0..20).is_none()
3895        );
3896    }
3897
3898    // ---- Phase-4: enum-brace spacing ----
3899
3900    #[test]
3901    fn enum_braces_are_spaced_dicts_are_not() {
3902        assert_eq!(fmt("enum E {A, B, C}\n"), "enum E { A, B, C }\n");
3903        assert_eq!(fmt("enum {A, B}\n"), "enum { A, B }\n"); // anonymous
3904        assert_eq!(
3905            fmt("enum Named {RED = 1, GREEN = 2}\n"),
3906            "enum Named { RED = 1, GREEN = 2 }\n"
3907        );
3908        assert_eq!(fmt("enum Empty {}\n"), "enum Empty {}\n"); // empty stays tight
3909        // a dict literal stays tight even right after an enum on the previous line
3910        assert_eq!(
3911            fmt("enum E {A}\nvar d = {\"k\": 1}\n"),
3912            "enum E { A }\nvar d = {\"k\": 1}\n"
3913        );
3914    }
3915
3916    // ---- Phase-4: operator-chain wrapping ----
3917
3918    #[test]
3919    fn operator_chain_if_condition_breaks_operator_leading() {
3920        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";
3921        assert_eq!(
3922            fmt(src),
3923            "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"
3924        );
3925        assert_eq!(fmt(&fmt(src)), fmt(src), "idempotent");
3926    }
3927
3928    #[test]
3929    fn operator_chain_breaks_at_lowest_precedence_only() {
3930        // `a and b or c and d`: break at the lower-precedence `or`, keeping the `and` groups inline.
3931        let src = "func f():\n\tif aaaaaaaaaaaaaaaaaaaaaaaa and bbbbbbbbbbbbbbbbbbbbbbbb or cccccccccccccccccccccccc and dddddddd:\n\t\tpass\n";
3932        assert_eq!(
3933            fmt(src),
3934            "func f():\n\tif (\n\t\taaaaaaaaaaaaaaaaaaaaaaaa and bbbbbbbbbbbbbbbbbbbbbbbb\n\t\tor cccccccccccccccccccccccc and dddddddd\n\t):\n\t\tpass\n"
3935        );
3936    }
3937
3938    #[test]
3939    fn operator_chain_dot_chain_wraps_compact() {
3940        let src = "func f():\n\tvar chain = some_object.first_method().second_method().third_method().fourth_method().fifth_method_x()\n";
3941        assert_eq!(
3942            fmt(src),
3943            "func f():\n\tvar chain = (\n\t\tsome_object.first_method().second_method().third_method().fourth_method().fifth_method_x()\n\t)\n"
3944        );
3945    }
3946
3947    #[test]
3948    fn operator_chain_never_splits_a_node_path() {
3949        // A node-path's `/` are path separators, not division — an over-long node-path must stay on
3950        // one line (the parser reads `$A / B` as the same path as `$A/B`, so splitting it would be a
3951        // silent meaning change in disguise — and gdformat leaves it alone anyway).
3952        let src = "func f():\n\tvar n = $LongContainerNameHere/AnotherLongChildNode/YetAnotherChildNode/AndOneMoreChildNodeHere/FinalNode\n";
3953        assert_eq!(fmt(src), src);
3954    }
3955
3956    #[test]
3957    fn quote_normalization_off() {
3958        let cfg = FmtConfig {
3959            normalize_strings: false,
3960            ..FmtConfig::default()
3961        };
3962        assert_eq!(format("var a = 'simple'\n", &cfg), "var a = 'simple'\n");
3963    }
3964
3965    #[test]
3966    fn meaning_preserved_accepts_quote_trailing_comma_and_redundant_parens() {
3967        use super::meaning_preserved as mp;
3968        // string quotes, trailing commas, and *redundant* grouping parens are all accepted
3969        assert!(mp("var a = 'x'\n", "var a = \"x\"\n"));
3970        assert!(mp("func f():\n\tg(a, b,)\n", "func f():\n\tg(a, b)\n"));
3971        assert!(mp(
3972            "func f():\n\tvar x = [1, 2,]\n",
3973            "func f():\n\tvar x = [1, 2]\n"
3974        ));
3975        assert!(mp("var x = (a + b)\n", "var x = a + b\n"));
3976        assert!(mp(
3977            "func f():\n\tif (a and b):\n\t\tpass\n",
3978            "func f():\n\tif a and b:\n\t\tpass\n"
3979        ));
3980        // but real changes — value, dropped token, and PRECEDENCE — are still caught
3981        assert!(!mp("var a = 'x'\n", "var a = \"y\"\n"));
3982        assert!(!mp("func f():\n\tg(a, b)\n", "func f():\n\tg(a)\n"));
3983        assert!(!mp("var x = (a + b) * c\n", "var x = a + b * c\n"));
3984    }
3985
3986    #[test]
3987    fn blank_at_the_start_of_a_block_is_stripped() {
3988        // gdformat removes a blank line between a compound header and its body's first statement.
3989        let src = "func f():\n\tfor i in range(3):\n\n\t\tprint(i)\n";
3990        assert_eq!(fmt(src), "func f():\n\tfor i in range(3):\n\t\tprint(i)\n");
3991    }
3992
3993    #[test]
3994    fn blank_after_a_leading_block_comment_is_kept() {
3995        // A comment is the block's first content, so a blank between it and the first statement is
3996        // preserved (the comment is not a header — the statement is not a new block's first line).
3997        let src = "func f():\n\t# note\n\n\tvar x = 1\n";
3998        assert_eq!(fmt(src), src);
3999    }
4000
4001    #[test]
4002    fn a_column0_comment_amid_a_function_body_forces_no_def_blanks() {
4003        // Commented-out code dedented to column 0 in the middle of a function body is structurally
4004        // inside the function (its next code is deeper), so it must not be treated as a class-level
4005        // sibling of the function and get 2 blank lines forced before it.
4006        let src = "func f():\n\tvar a = 1\n# commented out\n#\tvar b = 2\n\tvar c = 3\n";
4007        assert_eq!(fmt(src), src);
4008    }
4009
4010    #[test]
4011    fn a_trailing_comment_does_not_force_a_fitting_statement_to_wrap() {
4012        // gdformat measures line width without the trailing comment (appended in a post-pass), so an
4013        // author-wrapped statement whose code fits on one line is collapsed even though the comment
4014        // pushes the rendered line past the width.
4015        let src = "const C := [aaaaaaaaaa, bbbbbbbbbb, cccccccccc, dddddddddd, eeeeeeeeee, ffffffffff]  # note\n";
4016        let multi = "const C := [\n\taaaaaaaaaa,\n\tbbbbbbbbbb,\n\tcccccccccc,\n\tdddddddddd,\n\teeeeeeeeee,\n\tffffffffff\n]  # note\n";
4017        // code-without-comment is < 100, so both the already-flat and the author-wrapped forms collapse.
4018        assert_eq!(fmt(src), src);
4019        assert_eq!(fmt(multi), src);
4020    }
4021
4022    #[test]
4023    fn standalone_comments_after_a_lambda_arg_hang_off_the_body() {
4024        // A call with a multi-line lambda argument followed by another argument explodes its arg list;
4025        // a standalone comment dedented between the lambda and the next arg is re-emitted at the lambda
4026        // *body's* indent (gdformat's `_get_greater_indent`), and the arg separator `,` lands on the
4027        // lambda's last body line before its trailing comment.
4028        let src =
4029            "func f():\n\tcall(func():\n\t\ta()\n\t\treturn b  # t\n\t# between\n\t, [x, y])\n";
4030        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";
4031        assert_eq!(fmt(src), want);
4032    }
4033
4034    #[test]
4035    fn comments_thread_through_an_operator_chain_wrap() {
4036        // A standalone comment between operands of a paren-wrapped operator chain is re-emitted on its
4037        // own line at the operand indent (one level past the assignment), matching gdformat — and the
4038        // chain is forced multi-line by the comment even though it would otherwise fit on one line.
4039        let src = "func f():\n\tx = (\n\t\t\ta\n\t\t\t# note\n\t\t\t- b\n\t)\n";
4040        let want = "func f():\n\tx = (\n\t\ta\n\t\t# note\n\t\t- b\n\t)\n";
4041        assert_eq!(fmt(src), want);
4042    }
4043
4044    #[test]
4045    fn a_block_trailing_comment_keeps_its_shallower_indent() {
4046        // A comment after a nested block, indented to the enclosing function body, stays at the body's
4047        // depth (gdformat places it in the block whose range contains it), not snapped to column 0.
4048        let src = "func f():\n\tif c:\n\t\tx()\n\t# trailing\n\n\nfunc g():\n\tpass\n";
4049        let out = fmt(src);
4050        assert!(
4051            out.contains("\n\t# trailing\n"),
4052            "comment kept at one tab:\n{out}"
4053        );
4054    }
4055}