1#![cfg_attr(docsrs, feature(doc_cfg))]
31#![deny(missing_docs)]
32
33use gdscript_syntax::SyntaxKind;
34
35mod wrap;
36
37#[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 pub use_tabs: bool,
46 pub indent_size: usize,
48 pub line_width: usize,
50 pub normalize_spacing: bool,
54 pub collapse_blank_lines: bool,
57 pub insert_blank_lines: bool,
61 pub reflow: bool,
65 pub normalize_strings: bool,
69 pub expand_inline_blocks: bool,
73 pub strip_parens: bool,
78 pub collapse_lambdas: bool,
82 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 #[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#[must_use]
126pub fn format(source: &str, config: &FmtConfig) -> String {
127 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#[derive(Debug, Clone, PartialEq, Eq)]
142pub struct RangeEdit {
143 pub range: core::ops::Range<usize>,
145 pub new_text: String,
147}
148
149#[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 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); 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; }
194 Some(RangeEdit {
195 range: starts[changed_start]..starts[changed_end],
196 new_text: out[p..out.len() - s].concat(),
197 })
198}
199
200fn format_lf(source: &str, config: &FmtConfig) -> String {
202 let input_parses = gdscript_syntax::parse(source).errors().is_empty();
203 if config.safe_mode && !input_parses {
205 return source.to_owned();
206 }
207 let unextended = split_inner_class_extends(source);
210 let source = unextended.as_deref().unwrap_or(source);
211 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 out = insert_def_blanks(&out, config);
236 }
237 if config.reflow {
238 out = reflow(&out, config);
240 }
241 if config.safe_mode {
242 if !meaning_preserved(source, &out) {
247 return source.to_owned();
248 }
249 if input_parses && !gdscript_syntax::parse(&out).errors().is_empty() {
253 return source.to_owned();
254 }
255 }
256 out
257}
258
259fn 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 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 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 if !same_token_multiset(source, &out) || !gdscript_syntax::parse(&out).errors().is_empty() {
285 return None;
286 }
287 Some(out)
288}
289
290fn 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
305fn 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
323fn 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 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 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 let remove_start = usize::from(name.text_range().end());
360 let remove_end = colon_start;
361 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
373fn 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 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)); let mut out = source.to_owned();
393 for (off, len, text) in &splits {
394 out.replace_range(*off..*off + *len, text);
395 }
396 if !same_significant_tokens(source, &out) || !gdscript_syntax::parse(&out).errors().is_empty() {
399 return None;
400 }
401 Some(out)
402}
403
404fn 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 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 let is_block = matches!(child.kind(), S::Block | S::PropertyBody);
443 let suite = is_block && node.kind() != S::LambdaExpr;
444 if suite {
445 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 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
468fn 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(); 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
494fn 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
513fn 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(); let [stmt] = stmts.as_slice() else {
525 return None;
526 };
527 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)?; let be = usize::from(stmt.text_range().end());
536 if src[bs..be].contains('\n') {
537 return None; }
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; }
547 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); if indent_cols + content > config.line_width {
569 return None;
570 }
571 Some((pre.len(), bs))
572}
573
574fn 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(); 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)); 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
602fn 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
624fn 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
640fn 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
661fn 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#[derive(Clone, Copy, PartialEq, Eq)]
684enum Spacing {
685 None,
687 Single,
689 Verbatim,
692}
693
694fn 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
735fn 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 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 if is_open_bracket(prev) || is_close_bracket(cur) {
760 return Spacing::None;
761 }
762 if cur == S::Dot || prev == S::Dot {
765 return Spacing::None;
766 }
767 if cur == S::Comma || cur == S::Semicolon || prev == S::At {
769 return Spacing::None;
770 }
771 if prev == S::Tilde || prev == S::Bang || ((prev == S::Minus || prev == S::Plus) && prev_unary)
773 {
774 return Spacing::None;
775 }
776 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 if prev == S::Comma || prev == S::Semicolon {
795 return Spacing::Single;
796 }
797 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 if cur == S::LBrack {
814 return if is_operand_end(prev) {
815 Spacing::None
816 } else {
817 Spacing::Single
818 };
819 }
820 Spacing::Single
823}
824
825fn 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; } else {
836 out.push('\n');
837 }
838 *line_had_content = false;
839}
840
841fn 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() => {} _ => {
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
863fn comment_depth(comment_levels: usize, prev_depth: usize, next_depth: usize) -> usize {
873 comment_levels.min(prev_depth.max(next_depth))
874}
875
876#[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 let mut pending_blanks: usize = 0;
899 let mut line_had_content = false;
902 let mut seen_content = false;
904 let mut line_start = true;
907 let mut just_broke = false;
910 let mut stack: Vec<SyntaxKind> = Vec::new();
913 let mut brace_is_enum: Vec<bool> = Vec::new();
917 let mut pending_enum = false;
918 let mut prev_sig: Option<SyntaxKind> = None;
920 let mut prev_unary = false;
921 let mut node_path = false;
922 let mut pending_ws: Option<&str> = None;
925 let mut cont_line_start = false;
928 let mut line_indent_ws: Option<&str> = None;
931 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 SyntaxKind::Newline => {
947 trim_trailing_inline_ws(&mut out);
948 if stack.is_empty() {
949 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 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; }
976 SyntaxKind::NewlinePhys => {
977 if just_broke {
978 just_broke = false; } else {
980 trim_trailing_inline_ws(&mut out);
981 node_path = false;
982 pending_ws = None;
983 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 line_indent_ws = Some(text);
1007 } else if spacing_on {
1008 pending_ws = Some(text); } else {
1010 out.push_str(text); }
1012 }
1013 _ => {
1015 if line_start {
1016 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 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 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 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 out.push_str(" ");
1091 node_path = false;
1092 } else if t.kind.is_trivia() {
1093 if let Some(ws) = pending_ws {
1095 out.push_str(ws);
1096 }
1097 node_path = false;
1098 } else {
1099 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 Spacing::Verbatim
1117 } else {
1118 node_path = false; 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 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 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 if !t.kind.is_trivia() {
1182 if t.kind == SyntaxKind::EnumKw {
1183 pending_enum = true; }
1185 let unary_ctx = prev_sig.is_none_or(|p| !is_operand_end(p));
1186 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 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 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#[derive(Clone, Copy, PartialEq, Eq, Debug)]
1223enum LineRole {
1224 Def,
1226 Comment,
1228 Annotation,
1231 Other,
1233}
1234
1235#[derive(Clone, Copy)]
1239struct HeadLine {
1240 line: usize,
1241 depth: usize,
1242 role: LineRole,
1243}
1244
1245#[derive(Clone, Copy)]
1248struct Unit {
1249 head_line: usize,
1250 depth: usize,
1251 is_def: bool,
1252 is_comment: bool,
1256 ann_prefixed: bool,
1262}
1263
1264fn 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 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 let mut i = 0;
1295 while kinds.get(i) == Some(&S::At) {
1296 i += 1; if kinds.get(i) == Some(&S::Ident) {
1298 i += 1; }
1300 if kinds.get(i) == Some(&S::LParen) {
1301 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, 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#[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 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 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 let mut heads: Vec<HeadLine> = Vec::new();
1370 let mut depth = 0usize;
1371 let mut stack = 0usize;
1372 let mut this_line_continues = false; let mut next_line_is_cont = false; 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 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 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 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 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 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 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 let trailing_comment = u.is_comment && units.get(k + 1).is_none_or(|nx| nx.depth < u.depth);
1495 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); }
1508 if required.is_empty() {
1509 return formatted.to_owned();
1510 }
1511
1512 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
1532struct Atom {
1545 kind: SyntaxKind,
1546 text: String,
1547 space: bool,
1548}
1549
1550enum 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 magic: bool,
1564 },
1565}
1566
1567fn 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
1588fn cols(s: &str) -> usize {
1590 s.chars().count()
1591}
1592
1593fn display_cols(line: &str, tw: usize) -> usize {
1595 line.chars().map(|c| if c == '\t' { tw } else { 1 }).sum()
1596}
1597
1598fn 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 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
1687fn 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 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 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 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 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 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
1818fn 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
1860pub(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
1900fn 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
1912fn 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
1943fn 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
1970fn 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
2002fn 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 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 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
2085fn 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 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
2128fn 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; }
2144 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; for t in &toks {
2176 match t.kind {
2177 S::NewlinePhys => pending_break = true,
2178 S::Newline if !stack.is_empty() => return None,
2180 S::Indent | S::Dedent | S::Bom | S::Newline => {}
2181 S::LineContinuation => trim_trailing_inline_ws(&mut out),
2186 S::Whitespace => {
2187 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; }
2199 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
2255fn render_statement(body: &str, indent: usize, cfg: &FmtConfig, tw: usize, unit: &str) -> String {
2258 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 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 if !same_significant_tokens(&flat_seq(top), body) {
2288 return flat();
2289 }
2290 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 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#[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 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 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 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 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 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
2436fn trim_trailing_inline_ws(out: &mut String) {
2438 while out.ends_with(' ') || out.ends_with('\t') {
2439 out.pop();
2440 }
2441}
2442
2443fn 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 .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
2458pub(crate) fn canonical_string(text: &str) -> String {
2462 let Some(qpos) = text.find(['"', '\'']) else {
2463 return text.to_owned(); };
2465 let prefix = &text[..qpos];
2466 let rest = &text[qpos..];
2467 let rb = rest.as_bytes();
2468 let quote = rb[0];
2469 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(); }
2484 let body = &rest[1..rest.len() - 1];
2485 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 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#[derive(Clone, PartialEq, Eq)]
2544enum TreeEvent {
2545 Open(SyntaxKind),
2546 Close,
2547 Token(SyntaxKind, String),
2548}
2549
2550fn 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 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 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
2595pub(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 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 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"; 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 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 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 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 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 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 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"; 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 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 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 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 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 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 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"); assert_eq!(fmt_stmt("var t = a * b"), "var t = a * b"); 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 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"); assert_eq!(fmt_stmt("foo( -1 , -2 )"), "foo(-1, -2)"); assert_eq!(fmt_stmt("var a = [-1,-2]"), "var a = [-1, -2]"); assert_eq!(fmt_stmt("var n = -2**2"), "var n = -2 ** 2"); assert_eq!(fmt_stmt("var d = a - -b"), "var d = a - -b"); }
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"); assert_eq!(fmt_stmt("var s = \"%d\"%n"), "var s = \"%d\" % n"); }
2872
2873 #[test]
2874 fn spacing_node_paths_stay_tight() {
2875 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 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"); 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 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 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 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 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 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 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 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 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 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 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 #[test]
3070 fn blank_lines_collapsed_top_level_to_two() {
3071 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 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 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 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 #[test]
3133 fn trailing_body_comment_stays_at_block_depth() {
3134 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 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 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 #[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 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 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 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 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 #[test]
3238 fn crlf_line_endings_are_preserved() {
3239 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 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 #[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 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 assert_eq!(format(src, &cfg), src);
3325 }
3326
3327 #[test]
3328 fn reflow_keeps_an_already_canonical_wrapped_statement() {
3329 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 #[test]
3337 fn reflow_collapses_a_short_hand_wrapped_statement() {
3338 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 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 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 #[test]
3368 fn wrap_func_param_list_explodes_with_return_type_on_close_line() {
3369 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 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 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 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 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 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 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 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 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 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 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"); assert_eq!(fmt("var a = {(k): (v)}\n"), "var a = {k: v}\n");
3508 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 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 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 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 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 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 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 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 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 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 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 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 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 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 assert_eq!(
3682 fmt("extends Node\n\nvar x = 1\n"),
3683 "extends Node\n\nvar x = 1\n"
3684 );
3685 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 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 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 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 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 assert_eq!(
3732 fmt("var active: bool = false: set = set_active\n"),
3733 "var active: bool = false:\n\tset = set_active\n"
3734 );
3735 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 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 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 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 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 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 #[test]
3806 fn canonical_string_rules() {
3807 use super::canonical_string as c;
3808 assert_eq!(c("'simple'"), "\"simple\""); assert_eq!(c("\"already\""), "\"already\""); assert_eq!(c("'has \"x\" in'"), "'has \"x\" in'"); assert_eq!(c("'a\\'b'"), "\"a'b\""); assert_eq!(c("'both \" and \\' x'"), "\"both \\\" and ' x\""); assert_eq!(c("&'name'"), "&\"name\""); assert_eq!(c("^'a/b'"), "^\"a/b\""); assert_eq!(c("r'raw\\n'"), "r\"raw\\n\""); assert_eq!(c("r'has \"x\"'"), "r'has \"x\"'"); assert_eq!(c("'''triple'''"), "\"triple\"");
3820 assert_eq!(c("'''say \"hi\"'''"), "'say \"hi\"'"); assert_eq!(c("\"\"\"triple\"\"\""), "\"\"\"triple\"\"\""); assert_eq!(c("'''line1\nline2'''"), "'''line1\nline2'''"); assert_eq!(c("'\\t\\n'"), "\"\\t\\n\""); }
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 assert_eq!(fmt("var a = \"simple\"\n"), "var a = \"simple\"\n");
3833 }
3834
3835 #[test]
3838 fn magic_trailing_comma_explodes_with_comma() {
3839 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 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 #[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 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 #[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 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 assert!(super::format_range(src, &FmtConfig::default(), 10..21).is_none());
3892 assert!(
3894 super::format_range("func f():\n\tvar y = 2\n", &FmtConfig::default(), 0..20).is_none()
3895 );
3896 }
3897
3898 #[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"); 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"); 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 #[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 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 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 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 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 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 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 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 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 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 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 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 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}