#![cfg_attr(docsrs, feature(doc_cfg))]
#![deny(missing_docs)]
use gdscript_syntax::SyntaxKind;
mod wrap;
#[derive(Debug, Clone, PartialEq, Eq)]
#[allow(
clippy::struct_excessive_bools,
reason = "a plain user-facing options bag; each bool is an independent formatter toggle, not a state machine"
)]
pub struct FmtConfig {
pub use_tabs: bool,
pub indent_size: usize,
pub line_width: usize,
pub normalize_spacing: bool,
pub collapse_blank_lines: bool,
pub insert_blank_lines: bool,
pub reflow: bool,
pub normalize_strings: bool,
pub expand_inline_blocks: bool,
pub strip_parens: bool,
pub collapse_lambdas: bool,
pub safe_mode: bool,
}
impl Default for FmtConfig {
fn default() -> Self {
Self {
use_tabs: true,
indent_size: 4,
line_width: 100,
normalize_spacing: true,
collapse_blank_lines: true,
insert_blank_lines: true,
reflow: true,
normalize_strings: true,
expand_inline_blocks: true,
strip_parens: true,
collapse_lambdas: true,
safe_mode: true,
}
}
}
impl FmtConfig {
#[must_use]
fn indent_unit(&self) -> String {
if self.use_tabs {
"\t".to_owned()
} else {
" ".repeat(self.indent_size)
}
}
}
#[must_use]
pub fn format(source: &str, config: &FmtConfig) -> String {
if let Some(rest) = source.strip_prefix('\u{feff}') {
return format!("\u{feff}{}", format(rest, config));
}
if source.contains("\r\n") {
let lf = source.replace("\r\n", "\n");
return format_lf(&lf, config).replace('\n', "\r\n");
}
format_lf(source, config)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RangeEdit {
pub range: core::ops::Range<usize>,
pub new_text: String,
}
#[must_use]
pub fn format_range(
source: &str,
config: &FmtConfig,
sel: core::ops::Range<usize>,
) -> Option<RangeEdit> {
let formatted = format(source, config);
if formatted == source {
return None;
}
let src: Vec<&str> = source.split_inclusive('\n').collect();
let out: Vec<&str> = formatted.split_inclusive('\n').collect();
let mut p = 0;
while p < src.len() && p < out.len() && src[p] == out[p] {
p += 1;
}
let mut s = 0;
while s < src.len() - p && s < out.len() - p && src[src.len() - 1 - s] == out[out.len() - 1 - s]
{
s += 1;
}
let (changed_start, changed_end) = (p, src.len() - s);
let mut starts = Vec::with_capacity(src.len() + 1);
let mut acc = 0;
for l in &src {
starts.push(acc);
acc += l.len();
}
starts.push(acc);
let line_of = |off: usize| starts.partition_point(|&b| b <= off).saturating_sub(1);
let last_byte = source.len().saturating_sub(1);
let sel_first = line_of(sel.start.min(last_byte));
let sel_last = line_of(sel.end.saturating_sub(1).max(sel.start).min(last_byte));
if changed_end <= sel_first || changed_start > sel_last {
return None; }
Some(RangeEdit {
range: starts[changed_start]..starts[changed_end],
new_text: out[p..out.len() - s].concat(),
})
}
fn format_lf(source: &str, config: &FmtConfig) -> String {
let input_parses = gdscript_syntax::parse(source).errors().is_empty();
if config.safe_mode && !input_parses {
return source.to_owned();
}
let unextended = split_inner_class_extends(source);
let source = unextended.as_deref().unwrap_or(source);
let collapsed = if config.collapse_lambdas {
collapse_inline_lambdas(source, config)
} else {
None
};
let source = collapsed.as_deref().unwrap_or(source);
let stripped = if config.strip_parens {
strip_outer_parens(source)
} else {
None
};
let source = stripped.as_deref().unwrap_or(source);
let expanded = if config.expand_inline_blocks {
expand_inline_blocks(source, config)
} else {
None
};
let source = expanded.as_deref().unwrap_or(source);
let mut out = reindent(source, config);
if config.insert_blank_lines {
out = insert_def_blanks(&out, config);
}
if config.reflow {
out = reflow(&out, config);
}
if config.safe_mode {
if !meaning_preserved(source, &out) {
return source.to_owned();
}
if input_parses && !gdscript_syntax::parse(&out).errors().is_empty() {
return source.to_owned();
}
}
out
}
fn split_inner_class_extends(source: &str) -> Option<String> {
let parse = gdscript_syntax::parse(source);
if !parse.errors().is_empty() {
return None;
}
let mut edits: Vec<(usize, usize, usize, String)> = Vec::new();
collect_inner_class_extends(&parse.syntax_node(), source, &mut edits);
if edits.is_empty() {
return None;
}
edits.sort_unstable_by_key(|e| std::cmp::Reverse(e.2));
let mut out = source.to_owned();
for (rs, re, ins_pos, text) in &edits {
out.insert_str(*ins_pos, text);
out.replace_range(*rs..*re, "");
}
if !same_token_multiset(source, &out) || !gdscript_syntax::parse(&out).errors().is_empty() {
return None;
}
Some(out)
}
fn same_token_multiset(a: &str, b: &str) -> bool {
fn bag(s: &str) -> Vec<(String, String)> {
let mut v: Vec<(String, String)> = gdscript_syntax::tokenize(s)
.into_iter()
.filter(|t| !t.kind.is_trivia() && t.kind != SyntaxKind::Semicolon)
.map(|t| (format!("{:?}", t.kind), s[t.range].to_owned()))
.collect();
v.sort();
v
}
bag(a) == bag(b)
}
fn collect_inner_class_extends(
node: &gdscript_syntax::GdNode,
src: &str,
edits: &mut Vec<(usize, usize, usize, String)>,
) {
use SyntaxKind as S;
for child in node.children() {
let edit = (child.kind() == S::InnerClassDecl)
.then(|| inner_class_extends_edit(child, src))
.flatten();
if let Some(edit) = edit {
edits.push(edit);
}
collect_inner_class_extends(child, src, edits);
}
}
fn inner_class_extends_edit(
class: &gdscript_syntax::GdNode,
src: &str,
) -> Option<(usize, usize, usize, String)> {
use SyntaxKind as S;
use cstree::util::NodeOrToken;
let name = class.children().find(|c| c.kind() == S::Name)?;
let body = class.children().find(|c| c.kind() == S::ClassBody)?;
let mut extends_start = None;
let mut colon_start = None;
for c in class.children_with_tokens() {
if let NodeOrToken::Token(t) = c {
match t.kind() {
S::ExtendsKw if extends_start.is_none() => {
extends_start = Some(usize::from(t.text_range().start()));
}
S::Colon if extends_start.is_some() && colon_start.is_none() => {
colon_start = Some(usize::from(t.text_range().start()));
}
_ => {}
}
}
}
let extends_start = extends_start?;
let colon_start = colon_start?;
let ty = src.get(extends_start..colon_start)?.trim();
let ty = ty.strip_prefix("extends")?.trim();
if ty.is_empty() {
return None;
}
let remove_start = usize::from(name.text_range().end());
let remove_end = colon_start;
let first_stmt = body.children().next()?;
let stmt_start = usize::from(first_stmt.text_range().start());
let indent: String = src[stmt_start..]
.chars()
.take_while(|&c| c == '\t' || c == ' ')
.collect();
let insert = format!("{indent}extends {ty}\n");
Some((remove_start, remove_end, stmt_start, insert))
}
fn expand_inline_blocks(source: &str, config: &FmtConfig) -> Option<String> {
let parse = gdscript_syntax::parse(source);
if !parse.errors().is_empty() {
return None;
}
let unit = config.indent_unit();
let mut splits: Vec<(usize, usize, String)> = Vec::new();
collect_inline_splits(&parse.syntax_node(), source, 0, &unit, &mut splits);
if splits.is_empty() {
return None;
}
splits.sort_by_key(|(off, ..)| std::cmp::Reverse(*off)); let mut out = source.to_owned();
for (off, len, text) in &splits {
out.replace_range(*off..*off + *len, text);
}
if !same_significant_tokens(source, &out) || !gdscript_syntax::parse(&out).errors().is_empty() {
return None;
}
Some(out)
}
fn collect_inline_splits(
node: &gdscript_syntax::GdNode,
src: &str,
depth: usize,
unit: &str,
splits: &mut Vec<(usize, usize, String)>,
) {
use SyntaxKind as S;
use cstree::util::NodeOrToken;
for elem in node.children_with_tokens() {
let child = match elem {
NodeOrToken::Node(n) => n,
NodeOrToken::Token(t) => {
if t.kind() == S::Semicolon {
let r = t.text_range();
let (s, e) = (usize::from(r.start()), usize::from(r.end()));
let trail = src[e..].len() - src[e..].trim_start_matches([' ', '\t']).len();
let rest = &src[e + trail..];
let text = if rest.starts_with('\n') || rest.is_empty() {
String::new()
} else {
format!("\n{}", unit.repeat(depth))
};
splits.push((s, e + trail - s, text));
}
continue;
}
};
let is_block = matches!(child.kind(), S::Block | S::PropertyBody);
let suite = is_block && node.kind() != S::LambdaExpr;
if suite {
let body_offset = if child.kind() == S::PropertyBody {
child.children().next().and_then(first_sig_offset)
} else {
first_sig_offset(child)
};
let inline_body =
body_offset.filter(|&bs| src[..bs].trim_end_matches([' ', '\t']).ends_with(':'));
if let Some(bs) = inline_body {
splits.push((bs, 0, format!("\n{}", unit.repeat(depth + 1))));
}
}
let deeper = is_block
|| child.kind() == S::ClassBody
|| (node.kind() == S::MatchStmt && child.kind() == S::MatchArm);
collect_inline_splits(child, src, depth + usize::from(deeper), unit, splits);
}
}
fn collapse_inline_lambdas(source: &str, config: &FmtConfig) -> Option<String> {
let parse = gdscript_syntax::parse(source);
if !parse.errors().is_empty() {
return None;
}
let mut edits: Vec<(usize, usize)> = Vec::new(); collect_lambda_collapses(&parse.syntax_node(), source, config, &mut edits);
if edits.is_empty() {
return None;
}
edits.sort_unstable_by_key(|(s, _)| std::cmp::Reverse(*s));
let mut out = source.to_owned();
for (s, e) in &edits {
out.replace_range(*s..*e, " ");
}
if !meaning_preserved(source, &out) || !gdscript_syntax::parse(&out).errors().is_empty() {
return None;
}
Some(out)
}
fn collect_lambda_collapses(
node: &gdscript_syntax::GdNode,
src: &str,
config: &FmtConfig,
edits: &mut Vec<(usize, usize)>,
) {
for child in node.children() {
let collapse = (child.kind() == SyntaxKind::LambdaExpr)
.then(|| lambda_collapse_range(child, src, config))
.flatten();
if let Some((s, e)) = collapse {
edits.push((s, e));
}
collect_lambda_collapses(child, src, config, edits);
}
}
fn lambda_collapse_range(
lambda: &gdscript_syntax::GdNode,
src: &str,
config: &FmtConfig,
) -> Option<(usize, usize)> {
use SyntaxKind as S;
let block = lambda.children().find(|c| c.kind() == S::Block)?;
let stmts: Vec<_> = block.children().collect(); let [stmt] = stmts.as_slice() else {
return None;
};
if matches!(
stmt.kind(),
S::IfStmt | S::ElifClause | S::ElseClause | S::ForStmt | S::WhileStmt | S::MatchStmt
) {
return None;
}
let bs = first_sig_offset(stmt)?; let be = usize::from(stmt.text_range().end());
if src[bs..be].contains('\n') {
return None; }
let pre = src[..bs].trim_end_matches([' ', '\t', '\n', '\r']);
if !pre.ends_with(':') {
return None;
}
let gap = &src[pre.len()..bs];
if !gap.contains('\n') || gap.contains('#') {
return None; }
let mut top = lambda.clone();
while let Some(p) = top.parent() {
if p.kind() == S::Block {
break;
}
top = p.clone();
}
let r = top.text_range();
let (ts, te) = (usize::from(r.start()), usize::from(r.end()));
let indent_cols = src[..ts]
.bytes()
.rev()
.take_while(|&b| b == b'\t' || b == b' ')
.map(|b| if b == b'\t' { 4 } else { 1 })
.sum::<usize>();
let content: usize = src[ts..te].split_whitespace().map(str::len).sum::<usize>()
+ src[ts..te].split_whitespace().count().saturating_sub(1); if indent_cols + content > config.line_width {
return None;
}
Some((pre.len(), bs))
}
fn strip_outer_parens(source: &str) -> Option<String> {
let parse = gdscript_syntax::parse(source);
if !parse.errors().is_empty() {
return None;
}
let mut dels: Vec<usize> = Vec::new(); collect_redundant_parens(&parse.syntax_node(), &mut dels);
collect_empty_signal_parens(&parse.syntax_node(), &mut dels);
if dels.is_empty() {
return None;
}
dels.sort_unstable_by(|a, b| b.cmp(a)); let mut out = source.to_owned();
for off in &dels {
out.replace_range(*off..=*off, "");
}
if !meaning_preserved(source, &out) || !gdscript_syntax::parse(&out).errors().is_empty() {
return None;
}
Some(out)
}
fn paren_parent_strips(parent: SyntaxKind) -> bool {
use SyntaxKind as S;
matches!(
parent,
S::VarDecl
| S::ConstDecl
| S::ReturnStmt
| S::ForStmt
| S::IfStmt
| S::ElifClause
| S::WhileStmt
| S::MatchStmt
| S::ExprStmt
| S::ArgList
| S::ArrayLit
| S::DictEntry
| S::ParenExpr
)
}
fn collect_redundant_parens(node: &gdscript_syntax::GdNode, dels: &mut Vec<usize>) {
use cstree::util::NodeOrToken;
for child in node.children() {
if child.kind() == SyntaxKind::ParenExpr && paren_parent_strips(node.kind()) {
for c in child.children_with_tokens() {
let NodeOrToken::Token(t) = c else { continue };
if matches!(t.kind(), SyntaxKind::LParen | SyntaxKind::RParen) {
dels.push(usize::from(t.text_range().start()));
}
}
}
collect_redundant_parens(child, dels);
}
}
fn collect_empty_signal_parens(node: &gdscript_syntax::GdNode, dels: &mut Vec<usize>) {
use cstree::util::NodeOrToken;
for child in node.children() {
let empty_params = (child.kind() == SyntaxKind::SignalDecl)
.then(|| child.children().find(|c| c.kind() == SyntaxKind::ParamList))
.flatten()
.filter(|p| p.children().next().is_none());
if let Some(params) = empty_params {
for c in params.children_with_tokens() {
let NodeOrToken::Token(t) = c else { continue };
if matches!(t.kind(), SyntaxKind::LParen | SyntaxKind::RParen) {
dels.push(usize::from(t.text_range().start()));
}
}
}
collect_empty_signal_parens(child, dels);
}
}
fn first_sig_offset(node: &gdscript_syntax::GdNode) -> Option<usize> {
use cstree::util::NodeOrToken;
for c in node.children_with_tokens() {
match c {
NodeOrToken::Token(t) => {
let k = t.kind();
if !k.is_trivia() && !k.is_synthetic_layout() {
return Some(usize::from(t.text_range().start()));
}
}
NodeOrToken::Node(n) => {
if let Some(o) = first_sig_offset(n) {
return Some(o);
}
}
}
}
None
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum Spacing {
None,
Single,
Verbatim,
}
fn is_operand_end(k: SyntaxKind) -> bool {
use SyntaxKind as S;
matches!(
k,
S::Int
| S::Float
| S::String
| S::StringName
| S::NodePath
| S::Ident
| S::True
| S::False
| S::Null
| S::ConstPi
| S::ConstTau
| S::ConstInf
| S::ConstNan
| S::SelfKw
| S::SuperKw
| S::RParen
| S::RBrack
| S::RBrace
)
}
fn is_open_bracket(k: SyntaxKind) -> bool {
matches!(
k,
SyntaxKind::LParen | SyntaxKind::LBrack | SyntaxKind::LBrace
)
}
fn is_close_bracket(k: SyntaxKind) -> bool {
matches!(
k,
SyntaxKind::RParen | SyntaxKind::RBrack | SyntaxKind::RBrace
)
}
fn space_before(
prev: SyntaxKind,
cur: SyntaxKind,
top: Option<SyntaxKind>,
prev_unary: bool,
top_enum: bool,
) -> Spacing {
use SyntaxKind as S;
if top_enum {
if prev == S::LBrace && cur != S::RBrace {
return Spacing::Single;
}
if cur == S::RBrace && prev != S::LBrace {
return Spacing::Single;
}
}
if is_open_bracket(prev) || is_close_bracket(cur) {
return Spacing::None;
}
if cur == S::Dot || prev == S::Dot {
return Spacing::None;
}
if cur == S::Comma || cur == S::Semicolon || prev == S::At {
return Spacing::None;
}
if prev == S::Tilde || prev == S::Bang || ((prev == S::Minus || prev == S::Plus) && prev_unary)
{
return Spacing::None;
}
if cur == S::Colon {
return if top == Some(S::LBrack) {
Spacing::Verbatim
} else {
Spacing::None
};
}
if prev == S::Colon {
return if top == Some(S::LBrack) {
Spacing::Verbatim
} else {
Spacing::Single
};
}
if prev == S::Comma || prev == S::Semicolon {
return Spacing::Single;
}
if cur == S::LParen {
return if is_operand_end(prev)
|| prev == S::PreloadKw
|| prev == S::AssertKw
|| prev == S::FuncKw
{
Spacing::None
} else {
Spacing::Single
};
}
if cur == S::LBrack {
return if is_operand_end(prev) {
Spacing::None
} else {
Spacing::Single
};
}
Spacing::Single
}
fn emit_break(
out: &mut String,
collapse_on: bool,
line_had_content: &mut bool,
pending_blanks: &mut usize,
) {
if collapse_on && !*line_had_content {
*pending_blanks += 1; } else {
out.push('\n');
}
*line_had_content = false;
}
fn next_code_info(toks: &[gdscript_syntax::RawToken], idx: usize, cur: usize) -> (usize, usize) {
use SyntaxKind as S;
let mut delta: i32 = 0;
let mut indent_len = 0usize;
for t in &toks[idx + 1..] {
match t.kind {
S::Indent => delta += 1,
S::Dedent => delta -= 1,
S::NewlinePhys | S::Newline => indent_len = 0,
S::Whitespace => indent_len = usize::from(t.range.len()),
k if k.is_trivia() => {} _ => {
let d = usize::try_from(i32::try_from(cur).unwrap_or(0) + delta).unwrap_or(0);
return (d, indent_len);
}
}
}
(cur, 0)
}
fn comment_depth(comment_levels: usize, prev_depth: usize, next_depth: usize) -> usize {
comment_levels.min(prev_depth.max(next_depth))
}
#[allow(
clippy::too_many_lines,
reason = "one cohesive token-stream state machine; the indentation, spacing, bracket-stack and node-path transitions are interdependent and clearer kept together than split across helpers"
)]
fn reindent(source: &str, config: &FmtConfig) -> String {
let raw = gdscript_syntax::tokenize(source);
let (toks, _diags) = gdscript_syntax::run_prepass(&raw, source);
let unit = config.indent_unit();
let spacing_on = config.normalize_spacing;
let collapse_on = config.collapse_blank_lines;
let mut out = String::with_capacity(source.len() + 16);
let mut depth: usize = 0;
let mut pending_blanks: usize = 0;
let mut line_had_content = false;
let mut seen_content = false;
let mut line_start = true;
let mut just_broke = false;
let mut stack: Vec<SyntaxKind> = Vec::new();
let mut brace_is_enum: Vec<bool> = Vec::new();
let mut pending_enum = false;
let mut prev_sig: Option<SyntaxKind> = None;
let mut prev_unary = false;
let mut node_path = false;
let mut pending_ws: Option<&str> = None;
let mut cont_line_start = false;
let mut line_indent_ws: Option<&str> = None;
let mut prev_content_depth: usize = 0;
let mut prev_was_comment = false;
for (idx, t) in toks.iter().enumerate() {
let text = &source[t.range];
match t.kind {
SyntaxKind::Indent => depth += 1,
SyntaxKind::Dedent => depth = depth.saturating_sub(1),
SyntaxKind::Newline => {
trim_trailing_inline_ws(&mut out);
if stack.is_empty() {
emit_break(
&mut out,
collapse_on,
&mut line_had_content,
&mut pending_blanks,
);
line_start = true;
prev_sig = None;
} else {
out.push('\n');
cont_line_start = true;
}
just_broke = true;
node_path = false;
pending_ws = None;
line_indent_ws = None;
pending_enum = false; }
SyntaxKind::NewlinePhys => {
if just_broke {
just_broke = false; } else {
trim_trailing_inline_ws(&mut out);
node_path = false;
pending_ws = None;
if stack.is_empty() {
emit_break(
&mut out,
collapse_on,
&mut line_had_content,
&mut pending_blanks,
);
line_start = true;
prev_sig = None;
line_indent_ws = None;
} else {
out.push('\n');
cont_line_start = true;
}
}
}
SyntaxKind::Whitespace => {
if line_start {
line_indent_ws = Some(text);
} else if spacing_on {
pending_ws = Some(text); } else {
out.push_str(text); }
}
_ => {
if line_start {
let is_comment = matches!(
t.kind,
SyntaxKind::LineComment
| SyntaxKind::DocComment
| SyntaxKind::RegionComment
| SyntaxKind::EndRegionComment
);
let comment_len = line_indent_ws.map_or(0, str::len);
let emit_depth = if is_comment {
if comment_len == 0 {
0
} else {
let (next_depth, _) = next_code_info(&toks, idx, depth);
let unit_len = unit.len().max(1);
comment_depth(comment_len / unit_len, depth, next_depth)
}
} else {
depth
};
if collapse_on {
let at_block_start =
!prev_was_comment && !is_comment && depth > prev_content_depth;
let cap = if at_block_start {
0
} else {
usize::from(seen_content)
};
for _ in 0..pending_blanks.min(cap) {
out.push('\n');
}
pending_blanks = 0;
}
prev_was_comment = is_comment;
if !is_comment {
prev_content_depth = depth;
}
for _ in 0..emit_depth {
out.push_str(&unit);
}
line_start = false;
cont_line_start = false;
} else if spacing_on {
if cont_line_start {
if let Some(ws) = pending_ws {
out.push_str(ws);
}
cont_line_start = false;
} else if matches!(
t.kind,
SyntaxKind::LineComment
| SyntaxKind::DocComment
| SyntaxKind::RegionComment
| SyntaxKind::EndRegionComment
) {
out.push_str(" ");
node_path = false;
} else if t.kind.is_trivia() {
if let Some(ws) = pending_ws {
out.push_str(ws);
}
node_path = false;
} else {
let path_continue = node_path
&& (matches!(
t.kind,
SyntaxKind::Ident | SyntaxKind::Slash | SyntaxKind::String
) || (t.kind == SyntaxKind::Percent
&& matches!(
prev_sig,
Some(SyntaxKind::Dollar | SyntaxKind::Slash)
)));
let spacing = if path_continue {
Spacing::Verbatim
} else {
node_path = false; match prev_sig {
Some(p) => {
let top_enum = stack.last() == Some(&SyntaxKind::LBrace)
&& brace_is_enum.last() == Some(&true);
space_before(
p,
t.kind,
stack.last().copied(),
prev_unary,
top_enum,
)
}
None => Spacing::None,
}
};
match spacing {
Spacing::None => {}
Spacing::Single => out.push(' '),
Spacing::Verbatim => {
if let Some(ws) = pending_ws {
out.push_str(ws);
}
}
}
}
}
pending_ws = None;
just_broke = false;
if config.normalize_strings
&& matches!(
t.kind,
SyntaxKind::String | SyntaxKind::StringName | SyntaxKind::NodePath
)
{
out.push_str(&canonical_string(text));
} else {
out.push_str(text);
}
line_had_content = true;
seen_content = true;
match t.kind {
SyntaxKind::LBrace => {
stack.push(t.kind);
brace_is_enum.push(pending_enum);
pending_enum = false;
}
SyntaxKind::LParen | SyntaxKind::LBrack => {
stack.push(t.kind);
}
SyntaxKind::RBrace => {
stack.pop();
brace_is_enum.pop();
}
SyntaxKind::RParen | SyntaxKind::RBrack => {
stack.pop();
}
_ => {}
}
if !t.kind.is_trivia() {
if t.kind == SyntaxKind::EnumKw {
pending_enum = true; }
let unary_ctx = prev_sig.is_none_or(|p| !is_operand_end(p));
if t.kind == SyntaxKind::Dollar || (t.kind == SyntaxKind::Percent && unary_ctx)
{
node_path = true;
}
prev_unary = match t.kind {
SyntaxKind::Minus | SyntaxKind::Plus => unary_ctx,
SyntaxKind::Tilde | SyntaxKind::Bang => true,
_ => false,
};
let member_soft_kw = matches!(t.kind, SyntaxKind::MatchKw | SyntaxKind::WhenKw)
&& prev_sig == Some(SyntaxKind::Dot);
prev_sig = Some(if member_soft_kw {
SyntaxKind::Ident
} else {
t.kind
});
}
}
}
}
let trimmed = out.trim_end();
let mut result = String::with_capacity(trimmed.len() + 1);
result.push_str(trimmed);
if !result.is_empty() {
result.push('\n');
}
result
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
enum LineRole {
Def,
Comment,
Annotation,
Other,
}
#[derive(Clone, Copy)]
struct HeadLine {
line: usize,
depth: usize,
role: LineRole,
}
#[derive(Clone, Copy)]
struct Unit {
head_line: usize,
depth: usize,
is_def: bool,
is_comment: bool,
ann_prefixed: bool,
}
fn classify_line(toks: &[gdscript_syntax::RawToken], start: usize) -> LineRole {
use SyntaxKind as S;
if matches!(
toks[start].kind,
S::LineComment | S::DocComment | S::RegionComment | S::EndRegionComment
) {
return LineRole::Comment;
}
let mut kinds: Vec<S> = Vec::new();
let mut local_stack = 0usize;
for t in &toks[start..] {
match t.kind {
S::Newline => break,
S::NewlinePhys if local_stack == 0 => break,
S::LParen | S::LBrack | S::LBrace => {
local_stack += 1;
kinds.push(t.kind);
}
S::RParen | S::RBrack | S::RBrace => {
local_stack = local_stack.saturating_sub(1);
kinds.push(t.kind);
}
k if k.is_trivia() || k.is_synthetic_layout() => {}
k => kinds.push(k),
}
}
let mut i = 0;
while kinds.get(i) == Some(&S::At) {
i += 1; if kinds.get(i) == Some(&S::Ident) {
i += 1; }
if kinds.get(i) == Some(&S::LParen) {
let mut d = 0usize;
while let Some(&k) = kinds.get(i) {
i += 1;
match k {
S::LParen => d += 1,
S::RParen => {
d -= 1;
if d == 0 {
break;
}
}
_ => {}
}
}
}
}
match kinds.get(i) {
None => LineRole::Annotation, Some(S::FuncKw | S::ClassKw) => LineRole::Def,
Some(S::StaticKw) if kinds.get(i + 1) == Some(&S::FuncKw) => LineRole::Def,
_ => LineRole::Other,
}
}
#[allow(
clippy::too_many_lines,
reason = "three cohesive sequential passes (find heads, group units, apply the edge rule) over the same token stream; clearer kept together than split"
)]
fn insert_def_blanks(formatted: &str, config: &FmtConfig) -> String {
use SyntaxKind as S;
let raw = gdscript_syntax::tokenize(formatted);
let (toks, _diags) = gdscript_syntax::run_prepass(&raw, formatted);
let lines: Vec<&str> = formatted.lines().collect();
let is_blank: Vec<bool> = lines.iter().map(|l| l.trim().is_empty()).collect();
let blank_above = |l: usize| l > 0 && is_blank.get(l - 1).copied().unwrap_or(false);
let line_starts: Vec<usize> = std::iter::once(0)
.chain(
formatted
.bytes()
.enumerate()
.filter_map(|(i, b)| (b == b'\n').then_some(i + 1)),
)
.collect();
let line_of = |offset: usize| line_starts.partition_point(|&s| s <= offset) - 1;
let line_depth = |l: usize| -> usize {
let s = lines.get(l).copied().unwrap_or("");
if config.use_tabs {
s.bytes().take_while(|&b| b == b'\t').count()
} else {
s.bytes()
.take_while(|&b| b == b' ')
.count()
.checked_div(config.indent_size)
.unwrap_or(0)
}
};
let mut heads: Vec<HeadLine> = Vec::new();
let mut depth = 0usize;
let mut stack = 0usize;
let mut this_line_continues = false; let mut next_line_is_cont = false; let mut seen_first_on_line = false;
for (idx, t) in toks.iter().enumerate() {
match t.kind {
S::Indent => depth += 1,
S::Dedent => depth = depth.saturating_sub(1),
S::NewlinePhys => {
next_line_is_cont = stack > 0 || this_line_continues;
this_line_continues = false;
seen_first_on_line = false;
}
S::LineContinuation => this_line_continues = true,
S::Newline | S::Whitespace | S::Bom => {}
_ => {
if !seen_first_on_line {
seen_first_on_line = true;
if stack == 0 && !next_line_is_cont {
let line = line_of(usize::from(t.range.start()));
let role = classify_line(&toks, idx);
let head_depth = if role == LineRole::Comment {
let visual = line_depth(line);
let next_depth = next_code_info(&toks, idx, depth).0;
if next_depth > visual {
next_depth
} else {
visual
}
} else {
depth
};
heads.push(HeadLine {
line,
depth: head_depth,
role,
});
}
}
match t.kind {
S::LParen | S::LBrack | S::LBrace => stack += 1,
S::RParen | S::RBrack | S::RBrace => stack = stack.saturating_sub(1),
_ => {}
}
}
}
}
let mut units: Vec<Unit> = Vec::new();
let mut i = 0;
while i < heads.len() {
let h = heads[i];
if matches!(h.role, LineRole::Comment | LineRole::Annotation) {
let mut j = i + 1;
while j < heads.len()
&& matches!(heads[j].role, LineRole::Comment | LineRole::Annotation)
&& heads[j].depth == h.depth
&& !blank_above(heads[j].line)
{
j += 1;
}
if j < heads.len()
&& heads[j].depth == h.depth
&& !blank_above(heads[j].line)
&& matches!(heads[j].role, LineRole::Def | LineRole::Other)
{
units.push(Unit {
head_line: h.line,
depth: h.depth,
is_def: heads[j].role == LineRole::Def,
is_comment: false,
ann_prefixed: h.role == LineRole::Annotation,
});
i = j + 1;
} else {
for h in &heads[i..j] {
units.push(Unit {
head_line: h.line,
depth: h.depth,
is_def: false,
is_comment: h.role == LineRole::Comment,
ann_prefixed: false,
});
}
i = j;
}
} else {
units.push(Unit {
head_line: h.line,
depth: h.depth,
is_def: h.role == LineRole::Def,
is_comment: false,
ann_prefixed: false,
});
i += 1;
}
}
let mut required: std::collections::HashMap<usize, usize> = std::collections::HashMap::new();
let mut last_is_def: Vec<Option<bool>> = Vec::new();
for (k, u) in units.iter().enumerate() {
while last_is_def.len() <= u.depth {
last_is_def.push(None);
}
let prev = last_is_def[u.depth];
let first_in_block = prev.is_none();
let n = if u.depth == 0 { 2 } else { 1 };
let trailing_comment = u.is_comment && units.get(k + 1).is_none_or(|nx| nx.depth < u.depth);
let needs = if u.ann_prefixed {
prev == Some(true)
} else {
u.is_def || prev == Some(true)
};
if !trailing_comment && !first_in_block && needs {
required.insert(u.head_line, n);
}
last_is_def[u.depth] = Some(u.is_def);
last_is_def.truncate(u.depth + 1); }
if required.is_empty() {
return formatted.to_owned();
}
let mut out = String::with_capacity(formatted.len() + required.len() * 2);
let mut trailing_blanks = 0usize;
for (lno, content) in lines.iter().enumerate() {
if let Some(&k) = required.get(&lno) {
for _ in trailing_blanks..k {
out.push('\n');
}
}
out.push_str(content);
out.push('\n');
trailing_blanks = if is_blank[lno] {
trailing_blanks + 1
} else {
0
};
}
out
}
struct Atom {
kind: SyntaxKind,
text: String,
space: bool,
}
enum ReflowDoc {
Text {
text: String,
space: bool,
},
Group {
space: bool,
open: String,
elems: Vec<Vec<ReflowDoc>>,
close: &'static str,
magic: bool,
},
}
fn group_forced(elems: &[Vec<ReflowDoc>], magic: bool) -> bool {
magic
|| elems.iter().any(|e| {
e.iter().any(|d| match d {
ReflowDoc::Group { elems, magic, .. } => group_forced(elems, *magic),
ReflowDoc::Text { .. } => false,
})
})
}
fn open_close(text: &str) -> Option<&'static str> {
match text {
"(" => Some(")"),
"[" => Some("]"),
"{" => Some("}"),
_ => None,
}
}
fn cols(s: &str) -> usize {
s.chars().count()
}
fn display_cols(line: &str, tw: usize) -> usize {
line.chars().map(|c| if c == '\t' { tw } else { 1 }).sum()
}
fn build_elems(atoms: &[Atom], i: &mut usize, close: Option<&str>) -> (Vec<Vec<ReflowDoc>>, bool) {
let mut elems: Vec<Vec<ReflowDoc>> = Vec::new();
let mut cur: Vec<ReflowDoc> = Vec::new();
let mut elem_start = true;
while *i < atoms.len() {
let a = &atoms[*i];
if close == Some(a.text.as_str()) {
*i += 1;
break;
}
if a.text == "," {
elems.push(std::mem::take(&mut cur));
elem_start = true;
*i += 1;
continue;
}
let space = !elem_start && a.space;
if let Some(cl) = open_close(&a.text) {
let open = a.text.clone();
*i += 1;
let (inner, magic) = build_elems(atoms, i, Some(cl));
cur.push(ReflowDoc::Group {
space,
open,
elems: inner,
close: cl,
magic,
});
} else {
cur.push(ReflowDoc::Text {
text: a.text.clone(),
space,
});
*i += 1;
}
elem_start = false;
}
let magic = elem_start && !elems.is_empty();
if !cur.is_empty() {
elems.push(cur);
}
(elems, magic)
}
fn flat_seq(docs: &[ReflowDoc]) -> String {
let mut out = String::new();
for d in docs {
match d {
ReflowDoc::Text { text, space } => {
if *space {
out.push(' ');
}
out.push_str(text);
}
ReflowDoc::Group {
space,
open,
elems,
close,
magic,
} => {
if *space {
out.push(' ');
}
out.push_str(open);
out.push_str(&flat_group_contents(elems, *magic));
out.push_str(close);
}
}
}
out
}
fn flat_group_contents(elems: &[Vec<ReflowDoc>], magic: bool) -> String {
let mut s = elems
.iter()
.map(|e| flat_seq(e))
.collect::<Vec<_>>()
.join(", ");
if magic && !elems.is_empty() {
s.push(',');
}
s
}
fn render_seq(
docs: &[ReflowDoc],
indent: usize,
mut col: usize,
cfg: &FmtConfig,
tw: usize,
unit: &str,
) -> (String, usize) {
let mut out = String::new();
let widths: Vec<usize> = docs
.iter()
.map(|d| cols(&flat_seq(std::slice::from_ref(d))))
.collect();
let total: usize = widths.iter().sum();
let first_text = match docs.first() {
Some(ReflowDoc::Text { text, .. }) => Some(text.as_str()),
_ => None,
};
let is_def = first_text == Some("func")
|| (first_text == Some("static")
&& matches!(docs.get(1), Some(ReflowDoc::Text { text, .. }) if text == "func"));
let wrap_target = if is_def {
docs.iter()
.position(|d| matches!(d, ReflowDoc::Group { .. }))
} else {
docs.iter()
.rposition(|d| matches!(d, ReflowDoc::Group { .. }))
};
let mut before = 0usize;
for (i, d) in docs.iter().enumerate() {
let tail = total - before - widths[i];
match d {
ReflowDoc::Text { text, space } => {
if *space {
out.push(' ');
col += 1;
}
out.push_str(text);
col += cols(text);
}
ReflowDoc::Group {
space,
open,
elems,
close,
magic,
} => {
if *space {
out.push(' ');
col += 1;
}
let (g, end) = if Some(i) == wrap_target || group_forced(elems, *magic) {
render_group(open, elems, close, *magic, indent, col, tail, cfg, tw, unit)
} else {
let flat = format!("{open}{}{close}", flat_group_contents(elems, *magic));
let end = col + cols(&flat);
(flat, end)
};
out.push_str(&g);
col = end;
}
}
before += widths[i];
}
(out, col)
}
#[allow(
clippy::too_many_arguments,
reason = "a focused internal renderer threading layout state"
)]
fn render_group(
open: &str,
elems: &[Vec<ReflowDoc>],
close: &str,
magic: bool,
indent: usize,
col: usize,
tail: usize,
cfg: &FmtConfig,
tw: usize,
unit: &str,
) -> (String, usize) {
let forced = group_forced(elems, magic);
let flat = format!("{open}{}{close}", flat_group_contents(elems, magic));
if !forced && (col + cols(&flat) + tail <= cfg.line_width || elems.is_empty()) {
let end = col + cols(&flat);
return (flat, end);
}
let inner = indent + 1;
let inner_col = inner * tw;
let close_end = indent * tw + cols(close);
if !forced {
let contents = flat_group_contents(elems, magic);
if inner_col + cols(&contents) <= cfg.line_width {
let s = format!(
"{open}\n{ind}{contents}\n{base}{close}",
ind = unit.repeat(inner),
base = unit.repeat(indent),
);
return (s, close_end);
}
}
let mut s = String::from(open);
s.push('\n');
for (k, elem) in elems.iter().enumerate() {
s.push_str(&unit.repeat(inner));
let (es, _) = render_seq(elem, inner, inner_col, cfg, tw, unit);
s.push_str(&es);
if magic || k + 1 < elems.len() {
s.push(',');
}
s.push('\n');
}
s.push_str(&unit.repeat(indent));
s.push_str(close);
(s, close_end)
}
fn line_atoms(body: &str) -> Option<Vec<Atom>> {
use SyntaxKind as S;
let toks = gdscript_syntax::tokenize(body);
let mut atoms: Vec<Atom> = Vec::new();
let mut space = false;
let mut depth: i32 = 0;
for t in &toks {
match t.kind {
S::Whitespace => space = true,
S::Bom => {}
S::LineComment
| S::DocComment
| S::RegionComment
| S::EndRegionComment
| S::LineContinuation => return None,
k => {
if matches!(k, S::LParen | S::LBrack | S::LBrace) {
depth += 1;
} else if matches!(k, S::RParen | S::RBrack | S::RBrace) {
depth -= 1;
if depth < 0 {
return None;
}
}
atoms.push(Atom {
kind: k,
text: body[t.range].to_string(),
space,
});
space = false;
}
}
}
if depth != 0 {
return None;
}
Some(atoms)
}
pub(crate) fn infix_prec(kind: SyntaxKind) -> Option<u8> {
use SyntaxKind as S;
Some(match kind {
S::OrKw | S::PipePipe => 4,
S::AndKw | S::AmpAmp => 5,
S::InKw => 7,
S::EqEq | S::Neq | S::Lt | S::Gt | S::Le | S::Ge => 8,
S::Pipe => 9,
S::Caret => 10,
S::Amp => 11,
S::Shl | S::Shr => 12,
S::Plus | S::Minus => 13,
S::Star | S::Slash | S::Percent => 14,
S::StarStar => 17,
_ => return None,
})
}
fn is_assign_op(kind: SyntaxKind) -> bool {
use SyntaxKind as S;
matches!(
kind,
S::Eq
| S::PlusEq
| S::MinusEq
| S::StarEq
| S::SlashEq
| S::StarStarEq
| S::PercentEq
| S::AmpEq
| S::PipeEq
| S::CaretEq
| S::ShlEq
| S::ShrEq
| S::ColonEq
)
}
fn flat_atoms(atoms: &[Atom]) -> String {
let mut s = String::new();
for (i, a) in atoms.iter().enumerate() {
if i > 0 && a.space {
s.push(' ');
}
s.push_str(&a.text);
}
s
}
fn expression_span(atoms: &[Atom]) -> Option<(usize, usize)> {
use SyntaxKind as S;
let first = atoms.first()?;
match first.kind {
S::IfKw | S::ElifKw | S::WhileKw => {
let suffix = if atoms.last()?.kind == S::Colon {
atoms.len() - 1
} else {
return None;
};
Some((1, suffix))
}
S::ReturnKw => Some((1, atoms.len())),
_ => {
let mut depth = 0i32;
for (i, a) in atoms.iter().enumerate() {
match a.kind {
S::LParen | S::LBrack | S::LBrace => depth += 1,
S::RParen | S::RBrack | S::RBrace => depth -= 1,
k if depth == 0 && is_assign_op(k) => return Some((i + 1, atoms.len())),
_ => {}
}
}
None
}
}
}
fn strip_redundant_parens(mut expr: &[Atom]) -> &[Atom] {
while expr.len() >= 2 && expr[0].text == "(" {
let mut depth = 0i32;
let mut close = None;
for (i, a) in expr.iter().enumerate() {
if open_close(&a.text).is_some() {
depth += 1;
} else if matches!(a.text.as_str(), ")" | "]" | "}") {
depth -= 1;
if depth == 0 {
close = Some(i);
break;
}
}
}
if close == Some(expr.len() - 1) {
expr = &expr[1..expr.len() - 1];
} else {
break;
}
}
expr
}
fn top_level_binary_ops(expr: &[Atom]) -> Vec<(usize, u8)> {
use SyntaxKind as S;
let mut out = Vec::new();
let mut depth = 0i32;
let mut node_path = false;
let mut prev_operand_end = false;
for (i, a) in expr.iter().enumerate() {
match a.kind {
S::LParen | S::LBrack | S::LBrace => depth += 1,
S::RParen | S::RBrack | S::RBrace => depth -= 1,
_ => {}
}
let in_path = node_path && matches!(a.kind, S::Ident | S::Slash | S::String);
let binary_here = depth == 0 && !in_path && i > 0 && prev_operand_end;
if let Some(p) = infix_prec(a.kind).filter(|_| binary_here) {
out.push((i, p));
}
if node_path && !matches!(a.kind, S::Ident | S::Slash | S::String) {
node_path = false;
}
if a.kind == S::Dollar || (a.kind == S::Percent && !prev_operand_end) {
node_path = true;
}
prev_operand_end = is_operand_end(a.kind);
}
out
}
fn operator_chain_wrap(
atoms: &[Atom],
indent: usize,
cfg: &FmtConfig,
tw: usize,
unit: &str,
) -> Option<String> {
let (pre_end, suf_start) = expression_span(atoms)?;
if pre_end >= suf_start {
return None;
}
let expr = strip_redundant_parens(&atoms[pre_end..suf_start]);
let ops = top_level_binary_ops(expr);
let min_prec = ops.iter().map(|&(_, p)| p).min()?;
let prefix = flat_atoms(&atoms[..pre_end]);
let suffix = flat_atoms(&atoms[suf_start..]);
let lead = |out: &mut String| {
out.push_str(&unit.repeat(indent));
out.push_str(&prefix);
if !prefix.is_empty() {
out.push(' ');
}
out.push_str("(\n");
};
let expr_flat = flat_atoms(expr);
if (indent + 1) * tw + cols(&expr_flat) <= cfg.line_width {
let mut out = String::new();
lead(&mut out);
out.push_str(&unit.repeat(indent + 1));
out.push_str(&expr_flat);
out.push('\n');
out.push_str(&unit.repeat(indent));
out.push(')');
out.push_str(&suffix);
return Some(out);
}
let split: Vec<usize> = ops
.iter()
.filter(|&&(_, p)| p == min_prec)
.map(|&(i, _)| i)
.collect();
let mut segs: Vec<(Option<&str>, &[Atom])> = Vec::new();
let mut start = 0;
let mut prev_op: Option<&str> = None;
for &oi in &split {
segs.push((prev_op, &expr[start..oi]));
prev_op = Some(expr[oi].text.as_str());
start = oi + 1;
}
segs.push((prev_op, &expr[start..]));
let mut out = String::new();
lead(&mut out);
for (op, operand) in &segs {
out.push_str(&unit.repeat(indent + 1));
let mut col = (indent + 1) * tw;
if let Some(op) = op {
out.push_str(op);
out.push(' ');
col += cols(op) + 1;
}
let (od, _) = build_elems(operand, &mut 0, None);
if let Some(seq) = od.first() {
let (r, _) = render_seq(seq, indent + 1, col, cfg, tw, unit);
out.push_str(&r);
}
out.push('\n');
}
out.push_str(&unit.repeat(indent));
out.push(')');
out.push_str(&suffix);
Some(out)
}
fn compact_paren_wrap(
atoms: &[Atom],
indent: usize,
cfg: &FmtConfig,
tw: usize,
unit: &str,
) -> Option<String> {
let (pre_end, suf_start) = expression_span(atoms)?;
let expr = strip_redundant_parens(atoms.get(pre_end..suf_start)?);
if expr.is_empty()
|| !top_level_binary_ops(expr).is_empty()
|| !expr.iter().any(|a| open_close(&a.text).is_some())
{
return None;
}
let expr_flat = flat_atoms(expr);
if (indent + 1) * tw + cols(&expr_flat) > cfg.line_width {
return None;
}
let prefix = flat_atoms(&atoms[..pre_end]);
let suffix = flat_atoms(&atoms[suf_start..]);
let mut out = String::new();
out.push_str(&unit.repeat(indent));
out.push_str(&prefix);
if !prefix.is_empty() {
out.push(' ');
}
out.push_str("(\n");
out.push_str(&unit.repeat(indent + 1));
out.push_str(&expr_flat);
out.push('\n');
out.push_str(&unit.repeat(indent));
out.push(')');
out.push_str(&suffix);
Some(out)
}
fn stmt_is_rewrappable_multiline(stmt: &str) -> bool {
let toks = gdscript_syntax::tokenize(stmt);
let lead = stmt.trim_start();
let lead = lead.strip_prefix("static ").unwrap_or(lead);
if lead.starts_with("func ") || lead.starts_with("func(") {
return false; }
let has_lambda = toks.iter().any(|t| t.kind == SyntaxKind::FuncKw);
let has_comment = toks.iter().any(|t| {
matches!(
t.kind,
SyntaxKind::LineComment
| SyntaxKind::DocComment
| SyntaxKind::RegionComment
| SyntaxKind::EndRegionComment
)
});
let has_multiline_string = toks
.iter()
.any(|t| t.kind == SyntaxKind::String && stmt[t.range].contains('\n'));
has_lambda || has_comment || has_multiline_string
}
fn flatten_statement(stmt: &str) -> Option<String> {
use SyntaxKind as S;
let raw = gdscript_syntax::tokenize(stmt);
let (toks, _diags) = gdscript_syntax::run_prepass(&raw, stmt);
let mut out = String::with_capacity(stmt.len());
let mut stack: Vec<S> = Vec::new();
let mut brace_enum: Vec<bool> = Vec::new();
let mut pending_enum = false;
let mut prev_sig: Option<S> = None;
let mut prev_unary = false;
let mut node_path = false;
let mut pending_break = false; for t in &toks {
match t.kind {
S::NewlinePhys => pending_break = true,
S::Newline if !stack.is_empty() => return None,
S::Indent | S::Dedent | S::Bom | S::Newline => {}
S::LineContinuation => trim_trailing_inline_ws(&mut out),
S::Whitespace => {
if !pending_break && prev_sig.is_some() {
out.push_str(&stmt[t.range]);
}
}
S::LineComment | S::DocComment | S::RegionComment | S::EndRegionComment => return None,
k => {
let text = &stmt[t.range];
if matches!(k, S::String | S::StringName | S::NodePath) && text.contains('\n') {
return None; }
if pending_break {
let sp = match prev_sig {
Some(_) if node_path && matches!(k, S::Ident | S::Slash | S::String) => {
Spacing::Verbatim
}
Some(p) => {
let top_enum = stack.last() == Some(&S::LBrace)
&& brace_enum.last() == Some(&true);
space_before(p, k, stack.last().copied(), prev_unary, top_enum)
}
None => Spacing::None,
};
if matches!(sp, Spacing::Single) {
out.push(' ');
}
pending_break = false;
}
out.push_str(text);
match k {
S::LBrace => {
stack.push(k);
brace_enum.push(pending_enum);
pending_enum = false;
}
S::LParen | S::LBrack => stack.push(k),
S::RBrace => {
stack.pop();
brace_enum.pop();
}
S::RParen | S::RBrack => {
stack.pop();
}
_ => {}
}
if k == S::EnumKw {
pending_enum = true;
}
let unary_ctx = prev_sig.is_none_or(|p| !is_operand_end(p));
if k == S::Dollar || (k == S::Percent && unary_ctx) {
node_path = true;
} else if node_path && !matches!(k, S::Ident | S::Slash | S::String) {
node_path = false;
}
prev_unary = match k {
S::Minus | S::Plus => unary_ctx,
S::Tilde | S::Bang => true,
_ => false,
};
prev_sig = Some(k);
}
}
}
Some(out)
}
fn render_statement(body: &str, indent: usize, cfg: &FmtConfig, tw: usize, unit: &str) -> String {
if let Some(out) = wrap::render(body, indent, cfg) {
return out;
}
let flat = || format!("{}{body}", unit.repeat(indent));
let Some(atoms) = line_atoms(body) else {
return flat();
};
let width = indent * tw + cols(body);
if width > cfg.line_width {
let oc = operator_chain_wrap(&atoms, indent, cfg, tw, unit);
if let Some(oc) = oc {
return oc;
}
}
if !atoms.iter().any(|a| open_close(&a.text).is_some()) {
return flat();
}
let (docs, _) = build_elems(&atoms, &mut 0, None);
if docs.len() != 1 {
return flat();
}
let top = &docs[0];
if !same_significant_tokens(&flat_seq(top), body) {
return flat();
}
let forced = top
.iter()
.any(|d| matches!(d, ReflowDoc::Group { elems, magic, .. } if group_forced(elems, *magic)));
if width <= cfg.line_width && !forced {
return flat();
}
let (rendered, _) = render_seq(top, indent, indent * tw, cfg, tw, unit);
let result = format!("{}{rendered}", unit.repeat(indent));
let overflows = result.lines().any(|l| display_cols(l, tw) > cfg.line_width);
if overflows {
let cp = compact_paren_wrap(&atoms, indent, cfg, tw, unit);
if let Some(cp) = cp {
return cp;
}
}
result
}
#[allow(
clippy::too_many_lines,
reason = "one cohesive pass: per-line bracket/straddle bookkeeping then logical-statement grouping"
)]
fn reflow(formatted: &str, config: &FmtConfig) -> String {
use SyntaxKind as S;
if config.line_width == 0 {
return formatted.to_owned();
}
let tw = if config.use_tabs {
4
} else {
config.indent_size.max(1)
};
let unit = config.indent_unit();
let lines: Vec<&str> = formatted.split('\n').collect();
let line_starts: Vec<usize> = std::iter::once(0)
.chain(
formatted
.bytes()
.enumerate()
.filter_map(|(i, b)| (b == b'\n').then_some(i + 1)),
)
.collect();
let line_of = |off: usize| line_starts.partition_point(|&s| s <= off).saturating_sub(1);
let raw = gdscript_syntax::tokenize(formatted);
let mut start_depth = vec![i32::MIN; lines.len()];
let mut straddled = vec![false; lines.len()];
let mut ends_cont = vec![false; lines.len()];
let mut depth: i32 = 0;
for t in &raw {
let s = usize::from(t.range.start());
let e = usize::from(t.range.end()).saturating_sub(1).max(s);
let (sl, el) = (line_of(s), line_of(e));
if sl < start_depth.len() && start_depth[sl] == i32::MIN {
start_depth[sl] = depth;
}
if el > sl {
let end = el.min(lines.len().saturating_sub(1));
straddled[sl..=end].fill(true);
}
match t.kind {
S::LParen | S::LBrack | S::LBrace => depth += 1,
S::RParen | S::RBrack | S::RBrace => depth -= 1,
S::LineContinuation if sl < ends_cont.len() => ends_cont[sl] = true,
S::NewlinePhys if sl + 1 < start_depth.len() && start_depth[sl + 1] == i32::MIN => {
start_depth[sl + 1] = depth;
}
_ => {}
}
}
let sd = |i: usize| {
let d = start_depth.get(i).copied().unwrap_or(0);
if d == i32::MIN { 0 } else { d }
};
let strad = |i: usize| straddled.get(i).copied().unwrap_or(false);
let cont = |i: usize| ends_cont.get(i).copied().unwrap_or(false);
let mut out = String::with_capacity(formatted.len());
let mut li = 0;
while li < lines.len() {
let line = lines[li];
let head = !line.trim().is_empty()
&& sd(li) == 0
&& (li == 0 || !cont(li - 1))
&& !(li > 0 && strad(li) && strad(li - 1));
if !head {
out.push_str(line);
if li + 1 < lines.len() {
out.push('\n');
}
li += 1;
continue;
}
let mut j = li;
while j + 1 < lines.len() && (sd(j + 1) != 0 || (strad(j) && strad(j + 1)) || cont(j)) {
j += 1;
}
let stmt = lines[li..=j].join("\n");
let indent = if config.use_tabs {
line.bytes().take_while(|&b| b == b'\t').count()
} else {
line.bytes()
.take_while(|&b| b == b' ')
.count()
.checked_div(config.indent_size)
.unwrap_or(0)
};
let rendered = match flatten_statement(&stmt) {
Some(body) => Some(render_statement(&body, indent, config, tw, &unit)),
None if stmt_is_rewrappable_multiline(&stmt) => {
wrap::render(&wrap::dedent(&stmt), indent, config)
}
None => None,
};
out.push_str(rendered.as_deref().unwrap_or(&stmt));
if j + 1 < lines.len() {
out.push('\n');
}
li = j + 1;
}
out
}
fn trim_trailing_inline_ws(out: &mut String) {
while out.ends_with(' ') || out.ends_with('\t') {
out.pop();
}
}
fn same_significant_tokens(a: &str, b: &str) -> bool {
fn sig(s: &str) -> Vec<(SyntaxKind, &str)> {
gdscript_syntax::tokenize(s)
.into_iter()
.filter(|t| !t.kind.is_trivia() && t.kind != SyntaxKind::Semicolon)
.map(|t| (t.kind, &s[t.range]))
.collect()
}
sig(a) == sig(b)
}
pub(crate) fn canonical_string(text: &str) -> String {
let Some(qpos) = text.find(['"', '\'']) else {
return text.to_owned(); };
let prefix = &text[..qpos];
let rest = &text[qpos..];
let rb = rest.as_bytes();
let quote = rb[0];
if rb.len() >= 6 && rb[1] == quote && rb[2] == quote {
if quote == b'\'' && rest.ends_with("'''") {
let body = &rest[3..rest.len() - 3];
if !body.contains('\n') {
return canonical_string(&format!("{prefix}'{body}'"));
}
}
return text.to_owned();
}
if rest.len() < 2 || rb[rest.len() - 1] != quote {
return text.to_owned(); }
let body = &rest[1..rest.len() - 1];
if prefix.contains('r') {
let target = if !body.contains('"') {
'"'
} else if !body.contains('\'') {
'\''
} else {
quote as char
};
return format!("{prefix}{target}{body}{target}");
}
let mut units: Vec<(bool, char)> = Vec::new();
let (mut dq, mut sq) = (0usize, 0usize);
let mut chars = body.chars();
while let Some(c) = chars.next() {
if c == '\\' {
if let Some(n) = chars.next() {
units.push((true, n));
match n {
'"' => dq += 1,
'\'' => sq += 1,
_ => {}
}
} else {
units.push((false, '\\'));
}
} else {
units.push((false, c));
match c {
'"' => dq += 1,
'\'' => sq += 1,
_ => {}
}
}
}
let target = if dq > sq { '\'' } else { '"' };
let mut out = String::with_capacity(text.len());
out.push_str(prefix);
out.push(target);
for (esc, c) in units {
if c == '"' || c == '\'' {
if c == target {
out.push('\\');
}
out.push(c);
} else {
if esc {
out.push('\\');
}
out.push(c);
}
}
out.push(target);
out
}
#[derive(Clone, PartialEq, Eq)]
enum TreeEvent {
Open(SyntaxKind),
Close,
Token(SyntaxKind, String),
}
fn emit_tree_events(node: &gdscript_syntax::GdNode, out: &mut Vec<TreeEvent>) {
use cstree::util::NodeOrToken;
if node.kind() == SyntaxKind::ParenExpr {
for child in node.children() {
emit_tree_events(child, out);
}
return;
}
out.push(TreeEvent::Open(node.kind()));
for child in node.children_with_tokens() {
match child {
NodeOrToken::Node(n)
if n.kind() == SyntaxKind::ParamList && n.children().next().is_none() => {}
NodeOrToken::Node(n) => emit_tree_events(n, out),
NodeOrToken::Token(t) => {
let kind = t.kind();
if kind.is_trivia() || kind.is_synthetic_layout() || kind == SyntaxKind::Semicolon {
continue;
}
let text = if matches!(
kind,
SyntaxKind::String | SyntaxKind::StringName | SyntaxKind::NodePath
) {
canonical_string(t.text())
} else {
t.text().to_owned()
};
out.push(TreeEvent::Token(kind, text));
}
}
}
out.push(TreeEvent::Close);
}
pub(crate) fn meaning_preserved(a: &str, b: &str) -> bool {
fn events(s: &str) -> Vec<TreeEvent> {
let mut raw = Vec::new();
emit_tree_events(&gdscript_syntax::parse(s).syntax_node(), &mut raw);
let mut out = Vec::with_capacity(raw.len());
for i in 0..raw.len() {
let trailing_comma = matches!(&raw[i], TreeEvent::Token(SyntaxKind::Comma, _))
&& matches!(
raw.get(i + 1),
Some(TreeEvent::Token(
SyntaxKind::RParen | SyntaxKind::RBrack | SyntaxKind::RBrace,
_
))
);
if !trailing_comma {
out.push(raw[i].clone());
}
}
out
}
events(a) == events(b)
}
#[cfg(test)]
mod tests {
use super::*;
fn fmt(src: &str) -> String {
format(src, &FmtConfig::default())
}
#[test]
fn normalizes_indentation_to_tabs() {
let src = "func f():\n if true:\n return 1\n";
assert_eq!(fmt(src), "func f():\n\tif true:\n\t\treturn 1\n");
}
#[test]
fn trims_trailing_whitespace_and_adds_final_newline() {
let src = "var x = 1 \nvar y = 2"; assert_eq!(fmt(src), "var x = 1\nvar y = 2\n");
}
#[test]
fn is_idempotent() {
let src = "func f():\n var a = 1\n if a:\n return a\n";
let once = fmt(src);
assert_eq!(fmt(&once), once, "formatting must be idempotent");
}
#[test]
fn already_formatted_is_unchanged() {
let src = "func f():\n\tvar a = 1\n\treturn a\n";
assert_eq!(fmt(src), src);
}
#[test]
fn indexed_array_literal_explodes_the_array_keeps_index_on_close_line() {
let src = "func f():\n\tvar x = [\"Landscape\", \"Portrait\", \"Landscape (reverse)\", \"Portrait (reverse)\", \"Defined by sensor\"][get_orientation()]\n";
let out = fmt(src);
assert!(
out.contains("var x = [\n"),
"the array must open/explode:\n{out}"
);
assert!(
out.contains("][get_orientation()]"),
"the index must stay on the close-bracket line:\n{out}"
);
assert!(
!out.contains("][\n"),
"the index must not be exploded onto its own line:\n{out}"
);
assert!(super::same_significant_tokens(src, &out));
assert_eq!(fmt(&out), out, "idempotent");
}
#[test]
fn operator_chain_paren_operand_with_leading_comment_compacts_and_hoists() {
let src = "func f():\n\tif (\n\t\t\t# on\n\t\t\taaa and bbb\n\t) or (\n\t\t\t# off\n\t\t\tccc and ddd\n\t):\n\t\tpass\n";
let out = fmt(src);
assert!(out.contains("(aaa and bbb)"), "operand 0 compacts:\n{out}");
assert!(
out.contains("or (ccc and ddd)"),
"operator leads the compacted operand 1:\n{out}"
);
assert!(
out.contains("# on") && out.contains("# off"),
"comments preserved:\n{out}"
);
assert_eq!(fmt(&out), out, "idempotent");
}
#[test]
fn preserves_significant_tokens_including_strings() {
let src = "func f():\n\tvar s = \"a + b\"\n\treturn s\n";
let out = fmt(src);
assert!(super::same_significant_tokens(src, &out));
assert!(out.contains("\"a + b\""));
}
#[test]
fn multiline_string_content_is_untouched() {
let src = "func f():\n\tvar s = \"\"\"line1\n keep \nline2\"\"\"\n\treturn s\n";
let out = fmt(src);
assert!(
out.contains("line1\n keep \nline2"),
"got: {out:?}"
);
}
#[test]
fn safe_mode_returns_input_on_syntax_error() {
let src = "func f(:\n\treturn"; assert_eq!(fmt(src), src);
}
#[test]
fn empty_input_stays_empty() {
assert_eq!(fmt(""), "");
assert_eq!(fmt("\n\n\n"), "");
}
#[test]
fn spaces_option_indents_with_spaces() {
let cfg = FmtConfig {
use_tabs: false,
indent_size: 2,
..FmtConfig::default()
};
let src = "func f():\n\treturn 1\n";
assert_eq!(format(src, &cfg), "func f():\n return 1\n");
}
fn parses_clean(src: &str) -> bool {
gdscript_syntax::parse(src).errors().is_empty()
}
#[test]
fn comment_between_statements_does_not_corrupt_the_next_line() {
let src = "func g():\n var a = 1\n # c\n var x = 1\n var y = 2\n";
let out = fmt(src);
assert_eq!(
out,
"func g():\n\tvar a = 1\n\t# c\n\tvar x = 1\n\tvar y = 2\n"
);
assert!(
parses_clean(&out),
"formatter must not emit mixed indent: {out:?}"
);
assert_eq!(fmt(&out), out, "must be idempotent");
}
#[test]
fn leading_body_comment_is_indented_to_the_block() {
let src = "func g():\n # c\n var x = 1\n var y = 2\n";
let out = fmt(src);
assert_eq!(out, "func g():\n\t# c\n\tvar x = 1\n\tvar y = 2\n");
assert!(parses_clean(&out), "{out:?}");
assert_eq!(fmt(&out), out, "must be idempotent");
}
#[test]
fn doc_comment_between_statements_is_reindented_and_does_not_corrupt() {
let src = "func g():\n var a = 1\n ## doc\n var x = 1\n";
let out = fmt(src);
assert_eq!(out, "func g():\n\tvar a = 1\n\t## doc\n\tvar x = 1\n");
assert!(parses_clean(&out), "{out:?}");
}
#[test]
fn bracketed_continuation_interior_is_preserved() {
let src = "func f():\n\tvar a = [\n\t\t1,\n\t\t2,\n\t]\n\treturn a\n";
let out = fmt(src);
assert!(parses_clean(&out), "{out:?}");
assert!(super::same_significant_tokens(src, &out));
assert_eq!(fmt(&out), out, "must be idempotent");
}
fn fmt_stmt(stmt: &str) -> String {
let src = format!("func _f():\n\t{stmt}\n");
let out = fmt(&src);
out.strip_prefix("func _f():\n\t")
.and_then(|s| s.strip_suffix('\n'))
.unwrap_or(&out)
.to_owned()
}
#[test]
fn spacing_operators_and_assignment() {
assert_eq!(fmt_stmt("var x=a+b"), "var x = a + b");
assert_eq!(fmt_stmt("var x = a-b"), "var x = a - b"); assert_eq!(fmt_stmt("var t = a * b"), "var t = a * b"); assert_eq!(
fmt_stmt("var z = a==b and c!=d"),
"var z = a == b and c != d"
);
assert_eq!(fmt_stmt("x+=1"), "x += 1");
}
#[test]
fn spacing_brackets_and_commas() {
assert_eq!(fmt_stmt("foo( x ,y )"), "foo(x, y)");
assert_eq!(fmt_stmt("var a = [1,2]"), "var a = [1, 2]");
assert_eq!(
fmt_stmt("var d = {\"x\":1,\"y\":2}"),
"var d = {\"x\": 1, \"y\": 2}"
);
assert_eq!(
fmt_stmt("var n = obj . field . method ( )"),
"var n = obj.field.method()"
);
}
#[test]
fn spacing_type_annotation_and_default_args() {
assert_eq!(fmt_stmt("var x:int=1"), "var x: int = 1");
assert_eq!(
fmt("func f(a,b:int=1)->int:\n\treturn 0\n"),
"func f(a, b: int = 1) -> int:\n\treturn 0\n"
);
assert_eq!(fmt_stmt("var a:Array[int]=[]"), "var a: Array[int] = []");
}
#[test]
fn spacing_unary_minus() {
assert_eq!(fmt_stmt("var x = -1"), "var x = -1"); 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"); }
#[test]
fn spacing_percent_is_modulo_or_format_when_after_an_operand() {
assert_eq!(fmt_stmt("var r = a%b"), "var r = a % b"); assert_eq!(fmt_stmt("var s = \"%d\"%n"), "var s = \"%d\" % n"); }
#[test]
fn spacing_node_paths_stay_tight() {
assert_eq!(
fmt_stmt("var n = get_node($Player/Bone)"),
"var n = get_node($Player/Bone)"
);
assert_eq!(fmt_stmt("var u = %Unique/Child"), "var u = %Unique/Child");
assert_eq!(
fmt_stmt("var p = $\"Player\".position"),
"var p = $\"Player\".position"
);
assert_eq!(fmt_stmt("var v = &\"Name\""), "var v = &\"Name\"");
assert_eq!(fmt_stmt("var q = ^\"a/b\""), "var q = ^\"a/b\"");
}
#[test]
fn spacing_keywords_paren_callee_and_grouping() {
assert_eq!(
fmt_stmt("var p = preload ( \"res://x.gd\" )"),
"var p = preload(\"res://x.gd\")"
);
assert_eq!(fmt_stmt("var x = a if c else b"), "var x = a if c else b"); assert_eq!(fmt_stmt("var y = not flag"), "var y = not flag");
assert_eq!(fmt_stmt("var z = n is int"), "var z = n is int");
assert_eq!(fmt_stmt("return ( x )"), "return x");
assert_eq!(fmt_stmt("return ( a + b ) * c"), "return (a + b) * c");
}
#[test]
fn spacing_lambda_func_paren_is_tight() {
assert_eq!(
fmt_stmt("var cb = func( ) -> void:\n\t\tpass"),
"var cb = func() -> void: pass"
);
assert_eq!(
fmt_stmt("var g = func(_text:String)->void:\n\t\tpass"),
"var g = func(_text: String) -> void: pass"
);
assert_eq!(
fmt("func named(a,b):\n\tpass\n"),
"func named(a, b):\n\tpass\n"
);
}
#[test]
fn multiline_lambda_collapses_when_single_statement_else_kept() {
assert_eq!(
fmt("func _r():\n\tx.connect(func() -> void:\n\t\tdo_thing()\n\t)\n"),
"func _r():\n\tx.connect(func() -> void: do_thing())\n"
);
let multi = "func _r():\n\tx.connect(func() -> void:\n\t\ta()\n\t\tb()\n\t)\n";
let out = fmt(multi);
assert_eq!(
out,
"func _r():\n\tx.connect(\n\t\tfunc() -> void:\n\t\t\ta()\n\t\t\tb()\n\t)\n"
);
assert!(parses_clean(&out), "{out:?}");
assert_eq!(fmt(&out), out, "idempotent");
}
#[test]
fn inline_lambda_value_in_exploded_dict_is_rendered() {
let src = "func r():\n\tvar n = V.line_edit({\"text\": t, \"on_changed\": func(x): cb.call(x), \"on_submit\": func(): add.call(), \"placeholder\": \"type a fairly long placeholder here\"})\n";
let out = fmt(src);
assert!(
out.contains("\"on_changed\": func(x): cb.call(x),"),
"{out:?}"
);
assert!(
out.contains("\"on_submit\": func(): add.call(),"),
"{out:?}"
);
assert!(parses_clean(&out), "{out:?}");
assert_eq!(fmt(&out), out, "idempotent");
}
#[test]
fn colon_dict_value_drops_below_key_when_entry_overflows() {
let src = "func r():\n\tvar d = V.label({\"text\": \"a moderately long string value that does not fit on the line at all here for sure\" % n, \"style\": {\"c\": 1}})\n";
let out = fmt(src);
assert!(
out.contains("\t\t\t\"text\":\n"),
"value should drop below key: {out:?}"
);
assert!(
out.contains("\"style\": {\"c\": 1}"),
"fitting entry stays inline: {out:?}"
);
assert!(parses_clean(&out), "{out:?}");
assert_eq!(fmt(&out), out, "idempotent");
}
#[test]
fn dot_chain_with_lambda_wraps_bottom_up_not_leading_dot() {
let src = "func r():\n\treturn V.fc(Demo.render, {\"title\": \"a fairly long title here for the box widget yes\"}, [V.button({\"on_pressed\": func(): go()})])\n";
let out = fmt(src);
assert!(out.contains("\treturn V.fc("), "should bottom-up: {out:?}");
assert!(!out.contains(". fc"), "should not leading-dot: {out:?}");
assert!(parses_clean(&out), "{out:?}");
assert_eq!(fmt(&out), out, "idempotent");
}
#[test]
fn over_indented_lambda_in_brackets_does_not_corrupt() {
let note_manager = "func _ready() -> void:\n\t_play_stats.changed.connect(\n\t\t\tfunc() -> void:\n\t\t\t\tplay_stats_updated.emit(_play_stats)\n\t\t\t\t)\n";
let main_gd = "func _r() -> void:\n\tlatency_line_edit.text_submitted.connect(\n\t\tfunc(_text: String) -> void:\n\t\t\tlatency_line_edit.release_focus())\n";
for src in [note_manager, main_gd] {
let cfg = FmtConfig {
safe_mode: false,
..FmtConfig::default()
};
let out = format(src, &cfg);
assert!(parses_clean(&out), "lambda-in-brackets must parse: {out:?}");
assert!(
super::same_significant_tokens(src, &out),
"tokens changed: {out:?}"
);
assert_eq!(format(&out, &cfg), out, "idempotent: {out:?}");
}
}
#[test]
fn spacing_annotation_is_tight() {
assert_eq!(
fmt("@export_range(0,100)\nvar speed = 1\n"),
"@export_range(0, 100)\nvar speed = 1\n"
);
assert_eq!(fmt("@export var hp=100\n"), "@export var hp = 100\n");
}
#[test]
fn spacing_colon_in_brackets_left_verbatim() {
let cfg = FmtConfig {
safe_mode: false,
..FmtConfig::default()
};
assert_eq!(
format("func _f():\n\tvar d = data[a:b]\n", &cfg),
"func _f():\n\tvar d = data[a:b]\n"
);
assert_eq!(
format("func _f():\n\tvar e=data[a : b]\n", &cfg),
"func _f():\n\tvar e = data[a : b]\n"
);
}
#[test]
fn spacing_is_idempotent_across_cases() {
let cases = [
"var x = a+b*c-d",
"func f(a,b:int=1)->int:\n\treturn a",
"var n = get_node($Player/Bone).position",
"var d = {\"k\": foo(-1, 2), \"m\": items.slice(i, j)}",
"var z = a if b<c else -d",
];
for c in cases {
let src = format!("func _w():\n\t{c}\n");
let once = fmt(&src);
assert_eq!(fmt(&once), once, "not idempotent for {c:?}: {once:?}");
assert!(parses_clean(&once), "did not parse: {once:?}");
assert!(
super::same_significant_tokens(&src, &once),
"tokens changed for {c:?}"
);
}
}
#[test]
fn blank_lines_collapsed_top_level_to_two() {
let src = "func a():\n\tpass\n\n\n\n\nfunc b():\n\tpass\n";
assert_eq!(fmt(src), "func a():\n\tpass\n\n\nfunc b():\n\tpass\n");
}
#[test]
fn blank_lines_collapsed_inside_block_to_one() {
let src = "func a():\n\tvar x = 1\n\n\n\n\tvar y = 2\n";
assert_eq!(fmt(src), "func a():\n\tvar x = 1\n\n\tvar y = 2\n");
}
#[test]
fn leading_blank_lines_stripped() {
assert_eq!(fmt("\n\n\nfunc a():\n\tpass\n"), "func a():\n\tpass\n");
}
#[test]
fn single_blank_between_top_defs_is_grown_to_two() {
let src = "func a():\n\tpass\n\nfunc b():\n\tpass\n";
assert_eq!(fmt(src), "func a():\n\tpass\n\n\nfunc b():\n\tpass\n");
}
#[test]
fn blank_lines_inside_a_multiline_string_are_untouched() {
let src = "func a():\n\tvar s = \"\"\"x\n\n\n\ny\"\"\"\n\treturn s\n";
let out = fmt(src);
assert!(
out.contains("x\n\n\n\ny"),
"string interior collapsed: {out:?}"
);
assert!(super::same_significant_tokens(src, &out));
}
#[test]
fn blank_lines_off_preserved() {
let cfg = FmtConfig {
collapse_blank_lines: false,
..FmtConfig::default()
};
let src = "func a():\n\tpass\n\n\n\n\nfunc b():\n\tpass\n";
assert_eq!(format(src, &cfg), src);
}
#[test]
fn spacing_off_is_indentation_only() {
let cfg = FmtConfig {
normalize_spacing: false,
..FmtConfig::default()
};
let src = "func f():\n var x=a+b\n";
assert_eq!(format(src, &cfg), "func f():\n\tvar x=a+b\n");
}
#[test]
fn trailing_body_comment_stays_at_block_depth() {
let src = "func foo():\n\tpass\n\t# trailing\n\n\nfunc bar():\n\tpass\n";
let out = fmt(src);
assert_eq!(
out,
"func foo():\n\tpass\n\t# trailing\n\n\nfunc bar():\n\tpass\n"
);
assert_eq!(fmt(&out), out, "idempotent");
}
#[test]
fn comment_after_nested_block_stays_at_outer_body_depth() {
let src = "func f():\n\tif x:\n\t\tpass\n\t# back at body level\n\treturn\n";
let out = fmt(src);
assert_eq!(
out,
"func f():\n\tif x:\n\t\tpass\n\t# back at body level\n\treturn\n"
);
assert_eq!(fmt(&out), out, "idempotent");
}
#[test]
fn over_indented_comment_snaps_to_block_and_col0_stays() {
assert_eq!(
fmt("func f():\n\t\t\t# over\n\tpass\n"),
"func f():\n\t# over\n\tpass\n"
);
assert_eq!(
fmt("func f():\n# at col 0\n\tpass\n"),
"func f():\n# at col 0\n\tpass\n"
);
}
#[test]
fn two_blanks_inserted_around_top_level_defs() {
let src = "extends Node\nfunc a():\n\tpass\nfunc b():\n\tpass\n";
assert_eq!(
fmt(src),
"extends Node\n\n\nfunc a():\n\tpass\n\n\nfunc b():\n\tpass\n"
);
}
#[test]
fn one_blank_inserted_between_methods_in_a_class() {
let src = "class C:\n\tfunc a():\n\t\tpass\n\tfunc b():\n\t\tpass\n";
assert_eq!(
fmt(src),
"class C:\n\tfunc a():\n\t\tpass\n\n\tfunc b():\n\t\tpass\n"
);
}
#[test]
fn blanks_go_before_an_attached_comment_or_annotation_prefix() {
let src = "func a():\n\tpass\n## docs for b\n@warning_ignore(\"x\")\nfunc b():\n\tpass\n";
assert_eq!(
fmt(src),
"func a():\n\tpass\n\n\n## docs for b\n@warning_ignore(\"x\")\nfunc b():\n\tpass\n"
);
}
#[test]
fn blanks_inserted_after_a_def_before_a_following_non_def() {
let src = "func a():\n\tpass\nvar x = 1\n";
assert_eq!(fmt(src), "func a():\n\tpass\n\n\nvar x = 1\n");
}
#[test]
fn static_var_is_not_a_def_but_static_func_is() {
let src = "var a = 1\nstatic var b = 2\nstatic func c():\n\tpass\n";
assert_eq!(
fmt(src),
"var a = 1\nstatic var b = 2\n\n\nstatic func c():\n\tpass\n"
);
}
#[test]
fn no_blank_before_the_first_def_in_the_file() {
let src = "func a():\n\tpass\n";
assert_eq!(fmt(src), src);
}
#[test]
fn blank_insertion_off_leaves_blanks_alone() {
let cfg = FmtConfig {
insert_blank_lines: false,
..FmtConfig::default()
};
let src = "func a():\n\tpass\nfunc b():\n\tpass\n";
assert_eq!(format(src, &cfg), src);
}
#[test]
fn crlf_line_endings_are_preserved() {
let src = "func a():\r\n\tvar x=1\r\nfunc b():\r\n\tpass\r\n";
assert_eq!(
fmt(src),
"func a():\r\n\tvar x = 1\r\n\r\n\r\nfunc b():\r\n\tpass\r\n"
);
}
#[test]
fn crlf_preserved_including_multiline_string_interior() {
let src = "var s = \"\"\"a\r\nb\"\"\"\r\n";
let out = fmt(src);
assert_eq!(out, "var s = \"\"\"a\r\nb\"\"\"\r\n");
assert!(super::same_significant_tokens(src, &out));
}
#[test]
fn lf_files_stay_lf() {
let src = "func a():\n\tpass\n";
assert!(!fmt(src).contains('\r'));
}
#[test]
fn reflow_compact_call() {
let src = "func f():\n\tvar long_call = some_function(argument_one, argument_two, argument_three, argument_four, arg_five)\n";
assert_eq!(
fmt(src),
"func f():\n\tvar long_call = some_function(\n\t\targument_one, argument_two, argument_three, argument_four, arg_five\n\t)\n"
);
assert_eq!(fmt(&fmt(src)), fmt(src), "idempotent");
}
#[test]
fn reflow_compact_array_and_dict() {
let arr = "func f():\n\tvar arr = [element_one, element_two, element_three, element_four, element_five, element_six, seven]\n";
assert_eq!(
fmt(arr),
"func f():\n\tvar arr = [\n\t\telement_one, element_two, element_three, element_four, element_five, element_six, seven\n\t]\n"
);
let dct = "func f():\n\tvar d = {\"key_one\": value_one, \"key_two\": value_two, \"key_three\": value_three, \"key4\": value_four}\n";
assert_eq!(
fmt(dct),
"func f():\n\tvar d = {\n\t\t\"key_one\": value_one, \"key_two\": value_two, \"key_three\": value_three, \"key4\": value_four\n\t}\n"
);
}
#[test]
fn reflow_exploded_when_compact_too_long() {
let src = "func f():\n\tvar x = process_data(first_long_argument_name_here, second_long_argument_name_here, third_long_argument_name_here, fourth_argument)\n";
assert_eq!(
fmt(src),
"func f():\n\tvar x = process_data(\n\t\tfirst_long_argument_name_here,\n\t\tsecond_long_argument_name_here,\n\t\tthird_long_argument_name_here,\n\t\tfourth_argument\n\t)\n"
);
assert_eq!(fmt(&fmt(src)), fmt(src), "idempotent");
}
#[test]
fn reflow_nested_outer_explodes_inner_stays_inline() {
let src = "func f():\n\tvar n = outermost_call(inner_first(aaaa, bbbb, cccc, dddd), inner_second(eeee, ffff, gggg, hhhh), inner_third(iiii, jjjj, kkkk))\n";
assert_eq!(
fmt(src),
"func f():\n\tvar n = outermost_call(\n\t\tinner_first(aaaa, bbbb, cccc, dddd),\n\t\tinner_second(eeee, ffff, gggg, hhhh),\n\t\tinner_third(iiii, jjjj, kkkk)\n\t)\n"
);
}
#[test]
fn reflow_short_lines_stay_flat() {
let src = "func f():\n\tvar short = call(a, b, c)\n";
assert_eq!(fmt(src), src);
}
#[test]
fn reflow_off_leaves_long_lines() {
let cfg = FmtConfig {
reflow: false,
..FmtConfig::default()
};
let src = "func f():\n\tvar long_call = some_function(argument_one, argument_two, argument_three, argument_four, arg_five)\n";
assert_eq!(format(src, &cfg), src);
}
#[test]
fn reflow_keeps_an_already_canonical_wrapped_statement() {
let src = "func f():\n\tvar n = outermost_call(\n\t\tinner_first(aaaa, bbbb, cccc, dddd),\n\t\tinner_second(eeee, ffff, gggg, hhhh),\n\t\tinner_third(iiii, jjjj, kkkk)\n\t)\n";
assert_eq!(fmt(src), src);
}
#[test]
fn reflow_collapses_a_short_hand_wrapped_statement() {
let src = "func f():\n\tvar x = call(\n\t\ta,\n\t\tb,\n\t\tc\n\t)\n";
assert_eq!(fmt(src), "func f():\n\tvar x = call(a, b, c)\n");
}
#[test]
fn reflow_rewraps_a_still_too_long_wrapped_statement_idempotently() {
let src = "func f():\n\tvar x = some_long_function_name(argument_number_one, argument_number_two,\n\t\targument_number_three, argument_number_four, argument_number_five)\n";
let out = fmt(src);
assert!(
out.contains("some_long_function_name(\n"),
"should wrap: {out:?}"
);
assert_eq!(fmt(&out), out, "idempotent");
}
#[test]
fn reflow_keeps_a_statement_with_an_inner_comment_verbatim() {
let src = "func f():\n\tvar x = call(\n\t\ta, # first\n\t\tb,\n\t)\n";
let out = fmt(src);
assert!(out.contains("# first"), "{out:?}");
assert!(super::same_significant_tokens(src, &out));
assert_eq!(fmt(&out), out, "idempotent");
}
#[test]
fn wrap_func_param_list_explodes_with_return_type_on_close_line() {
let src = "func process(first_argument: int, second_argument: String, third_argument: float, fourth: bool) -> void:\n\tpass\n";
let out = fmt(src);
assert_eq!(
out,
"func process(\n\tfirst_argument: int, second_argument: String, third_argument: float, fourth: bool\n) -> void:\n\tpass\n"
);
assert_eq!(fmt(&out), out, "idempotent");
}
#[test]
fn wrap_method_chain_bottom_up_wraps_final_call_args() {
let src = "func f():\n\tobject.method_one(argument).method_two(argument).method_three(argument).method_four(argument_xxxx)\n";
let out = fmt(src);
assert_eq!(
out,
"func f():\n\tobject.method_one(argument).method_two(argument).method_three(argument).method_four(\n\t\targument_xxxx\n\t)\n"
);
assert_eq!(fmt(&out), out, "idempotent");
}
#[test]
fn wrap_method_chain_explodes_leading_dot_when_compact_overflows() {
let src = "func _ready():\n\ttween.tween_property(self, ^\"modulate:a\", 0.0, fade_out_duration).set_trans(Tween.TRANS_LINEAR).set_ease(Tween.EASE_OUT)\n";
let out = fmt(src);
assert!(
out.contains("\t(\n\t\ttween\n\t\t. tween_property("),
"{out}"
);
assert!(
out.contains("\n\t\t. set_ease(Tween.EASE_OUT)\n\t)\n"),
"{out}"
);
assert_eq!(fmt(&out), out, "idempotent");
}
#[test]
fn wrap_assignment_operator_chain_wraps_in_parens_compact_first() {
let src = "func f():\n\tgravity_value = first_long_operand_value_xx * second_long_operand_value_yy * third_long_operand_value_zz\n";
let out = fmt(src);
assert_eq!(
out,
"func f():\n\tgravity_value = (\n\t\tfirst_long_operand_value_xx * second_long_operand_value_yy * third_long_operand_value_zz\n\t)\n"
);
assert_eq!(fmt(&out), out, "idempotent");
}
#[test]
fn wrap_dict_entry_drops_multiline_value_below_the_key() {
let src = "func f():\n\tvar d := {player = {position = a, health = b,}, enemies = [],}\n";
let out = fmt(src);
assert_eq!(
out,
"func f():\n\tvar d := {\n\t\tplayer =\n\t\t{\n\t\t\tposition = a,\n\t\t\thealth = b,\n\t\t},\n\t\tenemies = [],\n\t}\n"
);
assert_eq!(fmt(&out), out, "idempotent");
}
#[test]
fn wrap_magic_comma_chain_explodes_leading_dot() {
let src = "func f():\n\treturn obj.method({\"a\": 1, \"b\": 2,})\n";
let out = fmt(src);
assert_eq!(
out,
"func f():\n\treturn (\n\t\tobj\n\t\t. method(\n\t\t\t{\n\t\t\t\t\"a\": 1,\n\t\t\t\t\"b\": 2,\n\t\t\t}\n\t\t)\n\t)\n"
);
assert_eq!(fmt(&out), out, "idempotent");
}
#[test]
fn column_zero_trailing_region_comment_stays_put_without_forced_blanks() {
let src = "func f():\n\tvar x = 1\n#endregion\n";
assert_eq!(fmt(src), src);
let src2 = "#region Section\nvar a = 1\nvar b = 2\n#endregion\n";
assert_eq!(fmt(src2), src2);
}
#[test]
fn inline_suite_bodies_split_but_lambdas_stay_inline() {
assert_eq!(
fmt("func f():\n\tif cond: do_thing()\n"),
"func f():\n\tif cond:\n\t\tdo_thing()\n"
);
assert_eq!(fmt("func g(): return 1\n"), "func g():\n\treturn 1\n");
assert_eq!(
fmt("func i():\n\tif a: b()\n\telse: c()\n"),
"func i():\n\tif a:\n\t\tb()\n\telse:\n\t\tc()\n"
);
assert_eq!(
fmt("func h():\n\tvar a := func(): return 1\n"),
"func h():\n\tvar a := func(): return 1\n"
);
}
#[test]
fn backslash_line_continuations_collapse_and_rewrap() {
assert_eq!(
fmt("func f():\n\tvar x = a + \\\n\t\tb\n"),
"func f():\n\tvar x = a + b\n"
);
let out = fmt(
"func f():\n\tif long_condition_name_one == 1 or \\\n\t\t\tlong_condition_name_two == 2 or long_condition_name_three == 3:\n\t\tpass\n",
);
assert!(out.contains("\tif (\n"), "{out}");
assert!(!out.contains('\\'), "backslash removed: {out}");
assert_eq!(fmt(&out), out, "idempotent");
}
#[test]
fn redundant_grouping_parens_are_stripped() {
assert_eq!(fmt("var x = (y)\n"), "var x = y\n");
assert_eq!(
fmt("func f():\n\treturn (g(a))\n"),
"func f():\n\treturn g(a)\n"
);
assert_eq!(
fmt("func f():\n\tfor i in (a * b):\n\t\tpass\n"),
"func f():\n\tfor i in a * b:\n\t\tpass\n"
);
assert_eq!(fmt("var a = g((x))\n"), "var a = g(x)\n"); assert_eq!(fmt("var a = {(k): (v)}\n"), "var a = {k: v}\n");
assert_eq!(fmt("var a = (b + c) * d\n"), "var a = (b + c) * d\n");
assert_eq!(fmt("func f():\n\tx = (y)\n"), "func f():\n\tx = (y)\n");
}
#[test]
fn nested_multiline_bracket_inside_lambda_body_parses_and_wraps() {
let src = "func f():\n\tvar v = use_memo(func():\n\t\tif a == null:\n\t\t\treturn make.parse(\"/\")\n\t\treturn make.build(strip_basename(location.path, basename), location.query, location.state)\n\t, [a, base])\n";
let out = fmt(src);
assert!(parses_clean(&out), "{out:?}");
assert!(
out.contains("\tvar v = use_memo(\n\t\tfunc():"),
"lambda explodes: {out:?}"
);
assert!(
out.contains("\t\t\treturn make.build(\n"),
"nested call wraps: {out:?}"
);
assert_eq!(fmt(&out), out, "idempotent");
}
#[test]
fn subscript_on_a_long_call_chain_wraps_leading_dot() {
let src = "func f():\n\tvar no_space: String = Fmt.format(src, {\"singleAttributePerLine\": true, \"insertSpaceBeforeSelfClose\": false})[\"text\"]\n";
let out = fmt(src);
assert!(
out.contains("\tvar no_space: String = (\n\t\tFmt\n"),
"leading-dot: {out:?}"
);
assert!(
out.contains("\n\t\t. format(src, {") && out.contains("})[\"text\"]\n"),
"call+index segment: {out:?}"
);
assert!(parses_clean(&out), "{out:?}");
assert_eq!(fmt(&out), out, "idempotent");
}
#[test]
fn dot_chain_with_lambda_in_earlier_segment_uses_leading_dot() {
let src = "func f():\n\tvar t := obj.tween_method(func(x: float) -> void: node.position = origin + curve.sample(x), 0.0, 100.0).set_delay(5.0)\n";
let out = fmt(src);
assert!(
out.contains("\tvar t := (\n"),
"leading-dot paren wrap: {out:?}"
);
assert!(
out.contains("\n\t\t. set_delay("),
"explodes at each dot: {out:?}"
);
assert!(parses_clean(&out), "{out:?}");
assert_eq!(fmt(&out), out, "idempotent");
}
#[test]
fn lambda_body_ending_in_arg_separator_comma_parses_and_is_preserved() {
let src = "func f():\n\tHooks.use_effect(\n\t\tfunc():\n\t\t\tsfx.call(null) # c1\n\t\t\treturn null,\n\t\t[]\n\t)\n";
let out = fmt(src);
assert!(out.contains("\t\t\treturn null,\n"), "{out:?}");
assert!(out.contains("# c1"), "comment preserved: {out:?}");
assert!(parses_clean(&out), "{out:?}");
assert_eq!(fmt(&out), out, "idempotent");
}
#[test]
fn comments_in_a_reshaped_lambda_body_are_threaded_through() {
assert_eq!(
fmt(
"func r():\n\tx.connect(func():\n\t\ta() # trailing\n\t\t# standalone\n\t\tb()\n\t)\n"
),
"func r():\n\tx.connect(\n\t\tfunc():\n\t\t\ta() # trailing\n\t\t\t# standalone\n\t\t\tb()\n\t)\n"
);
}
#[test]
fn trailing_comment_on_a_reshaped_statement_survives() {
let src = "const RESERVED := {\"a\": one_long_key_value_that_forces_a_wrap, \"b\": another_long_one_here} # note\n";
let out = fmt(src);
assert!(out.contains("} # note\n"), "{out:?}");
assert!(parses_clean(&out), "{out:?}");
assert_eq!(fmt(&out), out, "idempotent");
}
#[test]
fn lambda_arg_trailing_comment_keeps_separator_before_it() {
let src = "func p():\n\tg(func():\n\t\tif c:\n\t\t\twork()\n\t\treturn when # block\n\t, other)\n";
assert_eq!(
fmt(src),
"func p():\n\tg(\n\t\tfunc():\n\t\t\tif c:\n\t\t\t\twork()\n\t\t\treturn when, # block\n\t\tother\n\t)\n"
);
assert_eq!(fmt(&fmt(src)), fmt(src), "idempotent");
}
#[test]
fn multiline_string_operator_chain_paren_wraps_verbatim() {
let src =
"func p():\n\t$X.text = \"\"\"%d FPS\n\nObjects:\n%d\n\"\"\" % [\nfps,\nobjs,\n]\n";
assert_eq!(
fmt(src),
"func p():\n\t$X.text = (\n\t\t\"\"\"%d FPS\n\nObjects:\n%d\n\"\"\"\n\t\t% [\n\t\t\tfps,\n\t\t\tobjs,\n\t\t]\n\t)\n"
);
assert_eq!(fmt(&fmt(src)), fmt(src), "idempotent");
}
#[test]
fn comments_in_a_collection_are_threaded_through() {
assert_eq!(
fmt("var x = [ # head\n\t1, # one\n\t# mid\n\t2,\n]\n"),
"var x = [ # head\n\t1, # one\n\t# mid\n\t2,\n]\n"
);
let out = fmt("var d = {\n\t# note\n\t\"a\": 1,\n}\n");
assert!(out.contains("\t# note\n\t\"a\": 1,"), "{out:?}");
assert!(parses_clean(&out), "{out:?}");
assert_eq!(fmt(&out), out, "idempotent");
}
#[test]
fn semicolons_inside_lambda_body_expand_at_correct_depth() {
assert_eq!(
fmt("func r():\n\tvar cb = func():\n\t\ta(); b(); c()\n"),
"func r():\n\tvar cb = func():\n\t\ta()\n\t\tb()\n\t\tc()\n"
);
}
#[test]
fn inline_blocks_inside_inner_class_methods_expand_at_correct_depth() {
assert_eq!(
fmt("class P:\n\tfunc release():\n\t\ta = 1; b = 2\n"),
"class P:\n\tfunc release():\n\t\ta = 1\n\t\tb = 2\n"
);
assert_eq!(
fmt("class P:\n\tfunc release():\n\t\tif not x: return\n"),
"class P:\n\tfunc release():\n\t\tif not x:\n\t\t\treturn\n"
);
}
#[test]
fn inner_class_extends_moves_to_its_own_body_line() {
assert_eq!(
fmt("class CRProps extends RefCounted:\n\tvar x = 1\n"),
"class CRProps:\n\textends RefCounted\n\tvar x = 1\n"
);
assert_eq!(
fmt("extends Node\n\nvar x = 1\n"),
"extends Node\n\nvar x = 1\n"
);
let once = fmt("class C extends B:\n\tpass\n");
assert_eq!(fmt(&once), once);
}
#[test]
fn long_annotated_var_splits_annotation_to_its_own_line() {
assert_eq!(fmt("@onready var x = $Path\n"), "@onready var x = $Path\n");
let long = "@onready var pelvis: PhysicalBone3D = $\"root/root_001/Skeleton3D/PhysicalBoneSimulator3D/Physical Bone pelvis\"\n";
let out = fmt(long);
assert!(out.starts_with("@onready\nvar pelvis:"), "{out:?}");
assert_eq!(fmt(&out), out, "idempotent");
}
#[test]
fn empty_signal_parens_are_removed() {
assert_eq!(fmt("signal done()\n"), "signal done\n");
assert_eq!(fmt("signal hit(x, y)\n"), "signal hit(x, y)\n");
assert_eq!(fmt("signal a\n"), "signal a\n");
}
#[test]
fn semicolon_separated_statements_split() {
assert_eq!(
fmt("func f():\n\ta = 1; b = 2\n"),
"func f():\n\ta = 1\n\tb = 2\n"
);
assert_eq!(fmt("func g():\n\tpass;\n"), "func g():\n\tpass\n");
assert_eq!(
fmt("func h():\n\tif c: a(); b()\n"),
"func h():\n\tif c:\n\t\ta()\n\t\tb()\n"
);
}
#[test]
fn inline_match_arm_and_property_bodies_split() {
assert_eq!(
fmt("func f():\n\tmatch x:\n\t\t\"inc\": return state + 1\n"),
"func f():\n\tmatch x:\n\t\t\"inc\":\n\t\t\treturn state + 1\n"
);
assert_eq!(
fmt("var active: bool = false: set = set_active\n"),
"var active: bool = false:\n\tset = set_active\n"
);
assert_eq!(
fmt("var q: int = 0:\n\tget: return _q\n\tset(v): _q = v\n"),
"var q: int = 0:\n\tget:\n\t\treturn _q\n\tset(v):\n\t\t_q = v\n"
);
}
#[test]
fn blank_runs_collapse_to_one_then_defs_restore_two() {
assert_eq!(
fmt("var a = 1\n\n\nvar b = 2\n"),
"var a = 1\n\nvar b = 2\n"
);
assert_eq!(
fmt("extends Node\n\n\nvar b = 2\n"),
"extends Node\n\nvar b = 2\n"
);
assert_eq!(
fmt("func a():\n\tpass\n\n\n\nfunc b():\n\tpass\n"),
"func a():\n\tpass\n\n\nfunc b():\n\tpass\n"
);
}
#[test]
fn annotation_prefixed_def_forces_blanks_only_after_a_def() {
assert_eq!(
fmt("extends Node\n\n@rpc(\"x\")\nfunc f():\n\tpass\n"),
"extends Node\n\n@rpc(\"x\")\nfunc f():\n\tpass\n"
);
assert_eq!(
fmt("func a():\n\tpass\n@rpc(\"x\")\nfunc b():\n\tpass\n"),
"func a():\n\tpass\n\n\n@rpc(\"x\")\nfunc b():\n\tpass\n"
);
assert_eq!(
fmt("extends Node\nfunc f():\n\tpass\n"),
"extends Node\n\n\nfunc f():\n\tpass\n"
);
}
#[test]
fn leading_bom_is_preserved() {
let src = "\u{feff}class_name Foo\nvar x = 1\n";
let out = fmt(src);
assert!(out.starts_with('\u{feff}'), "{out:?}");
assert_eq!(&out[3..], "class_name Foo\nvar x = 1\n");
assert_eq!(fmt(&out), out, "idempotent");
}
#[test]
fn soft_keyword_member_call_hugs_paren_but_statement_keeps_space() {
assert_eq!(
fmt("func f():\n\tvar m = obj.match(\"a\", \"b\")\n"),
"func f():\n\tvar m = obj.match(\"a\", \"b\")\n"
);
assert_eq!(
fmt("func f():\n\tmatch (x):\n\t\tpass\n"),
"func f():\n\tmatch (x):\n\t\tpass\n"
);
}
#[test]
fn canonical_string_rules() {
use super::canonical_string as c;
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\"");
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\""); }
#[test]
fn quote_normalization_in_format() {
assert_eq!(fmt("var a = 'simple'\n"), "var a = \"simple\"\n");
assert_eq!(fmt("var b = 'has \"x\" in'\n"), "var b = 'has \"x\" in'\n");
assert_eq!(fmt("var f = &'n'\n"), "var f = &\"n\"\n");
assert_eq!(fmt("var a = \"simple\"\n"), "var a = \"simple\"\n");
}
#[test]
fn magic_trailing_comma_explodes_with_comma() {
assert_eq!(
fmt("var a = call(x, y,)\n"),
"var a = call(\n\tx,\n\ty,\n)\n"
);
assert_eq!(fmt("var b = [1, 2,]\n"), "var b = [\n\t1,\n\t2,\n]\n");
assert_eq!(fmt("var g = call(only,)\n"), "var g = call(\n\tonly,\n)\n");
}
#[test]
fn magic_trailing_comma_nested_forces_outer_without_own_comma() {
assert_eq!(
fmt("var d = outer(inner(a, b,), c)\n"),
"var d = outer(\n\tinner(\n\t\ta,\n\t\tb,\n\t),\n\tc\n)\n"
);
}
#[test]
fn magic_trailing_comma_is_idempotent() {
let once = fmt("var a = call(x, y,)\n");
assert_eq!(fmt(&once), once);
}
#[test]
fn inline_comments_get_two_spaces() {
assert_eq!(fmt("var x = 1 # one\n"), "var x = 1 # one\n");
assert_eq!(fmt("var y = 2 # many\n"), "var y = 2 # many\n");
assert_eq!(
fmt("func f(): # c\n\tpass ## doc\n"),
"func f(): # c\n\tpass ## doc\n"
);
assert_eq!(
fmt("func f():\n\t# standalone\n\tpass\n"),
"func f():\n\t# standalone\n\tpass\n"
);
}
#[test]
fn format_range_edits_only_the_changed_lines_overlapping_the_selection() {
let src = "func f():\n\tvar x = 1\n\tvar y=2\n";
let e = super::format_range(src, &FmtConfig::default(), 21..30).unwrap();
assert_eq!(e.range, 21..30);
assert_eq!(e.new_text, "\tvar y = 2\n");
assert!(super::format_range(src, &FmtConfig::default(), 10..21).is_none());
assert!(
super::format_range("func f():\n\tvar y = 2\n", &FmtConfig::default(), 0..20).is_none()
);
}
#[test]
fn enum_braces_are_spaced_dicts_are_not() {
assert_eq!(fmt("enum E {A, B, C}\n"), "enum E { A, B, C }\n");
assert_eq!(fmt("enum {A, B}\n"), "enum { A, B }\n"); assert_eq!(
fmt("enum Named {RED = 1, GREEN = 2}\n"),
"enum Named { RED = 1, GREEN = 2 }\n"
);
assert_eq!(fmt("enum Empty {}\n"), "enum Empty {}\n"); assert_eq!(
fmt("enum E {A}\nvar d = {\"k\": 1}\n"),
"enum E { A }\nvar d = {\"k\": 1}\n"
);
}
#[test]
fn operator_chain_if_condition_breaks_operator_leading() {
let src = "func f():\n\tif condition_number_one and condition_number_two and condition_number_three and condition_number_four:\n\t\tpass\n";
assert_eq!(
fmt(src),
"func f():\n\tif (\n\t\tcondition_number_one\n\t\tand condition_number_two\n\t\tand condition_number_three\n\t\tand condition_number_four\n\t):\n\t\tpass\n"
);
assert_eq!(fmt(&fmt(src)), fmt(src), "idempotent");
}
#[test]
fn operator_chain_breaks_at_lowest_precedence_only() {
let src = "func f():\n\tif aaaaaaaaaaaaaaaaaaaaaaaa and bbbbbbbbbbbbbbbbbbbbbbbb or cccccccccccccccccccccccc and dddddddd:\n\t\tpass\n";
assert_eq!(
fmt(src),
"func f():\n\tif (\n\t\taaaaaaaaaaaaaaaaaaaaaaaa and bbbbbbbbbbbbbbbbbbbbbbbb\n\t\tor cccccccccccccccccccccccc and dddddddd\n\t):\n\t\tpass\n"
);
}
#[test]
fn operator_chain_dot_chain_wraps_compact() {
let src = "func f():\n\tvar chain = some_object.first_method().second_method().third_method().fourth_method().fifth_method_x()\n";
assert_eq!(
fmt(src),
"func f():\n\tvar chain = (\n\t\tsome_object.first_method().second_method().third_method().fourth_method().fifth_method_x()\n\t)\n"
);
}
#[test]
fn operator_chain_never_splits_a_node_path() {
let src = "func f():\n\tvar n = $LongContainerNameHere/AnotherLongChildNode/YetAnotherChildNode/AndOneMoreChildNodeHere/FinalNode\n";
assert_eq!(fmt(src), src);
}
#[test]
fn quote_normalization_off() {
let cfg = FmtConfig {
normalize_strings: false,
..FmtConfig::default()
};
assert_eq!(format("var a = 'simple'\n", &cfg), "var a = 'simple'\n");
}
#[test]
fn meaning_preserved_accepts_quote_trailing_comma_and_redundant_parens() {
use super::meaning_preserved as mp;
assert!(mp("var a = 'x'\n", "var a = \"x\"\n"));
assert!(mp("func f():\n\tg(a, b,)\n", "func f():\n\tg(a, b)\n"));
assert!(mp(
"func f():\n\tvar x = [1, 2,]\n",
"func f():\n\tvar x = [1, 2]\n"
));
assert!(mp("var x = (a + b)\n", "var x = a + b\n"));
assert!(mp(
"func f():\n\tif (a and b):\n\t\tpass\n",
"func f():\n\tif a and b:\n\t\tpass\n"
));
assert!(!mp("var a = 'x'\n", "var a = \"y\"\n"));
assert!(!mp("func f():\n\tg(a, b)\n", "func f():\n\tg(a)\n"));
assert!(!mp("var x = (a + b) * c\n", "var x = a + b * c\n"));
}
#[test]
fn blank_at_the_start_of_a_block_is_stripped() {
let src = "func f():\n\tfor i in range(3):\n\n\t\tprint(i)\n";
assert_eq!(fmt(src), "func f():\n\tfor i in range(3):\n\t\tprint(i)\n");
}
#[test]
fn blank_after_a_leading_block_comment_is_kept() {
let src = "func f():\n\t# note\n\n\tvar x = 1\n";
assert_eq!(fmt(src), src);
}
#[test]
fn a_column0_comment_amid_a_function_body_forces_no_def_blanks() {
let src = "func f():\n\tvar a = 1\n# commented out\n#\tvar b = 2\n\tvar c = 3\n";
assert_eq!(fmt(src), src);
}
#[test]
fn a_trailing_comment_does_not_force_a_fitting_statement_to_wrap() {
let src = "const C := [aaaaaaaaaa, bbbbbbbbbb, cccccccccc, dddddddddd, eeeeeeeeee, ffffffffff] # note\n";
let multi = "const C := [\n\taaaaaaaaaa,\n\tbbbbbbbbbb,\n\tcccccccccc,\n\tdddddddddd,\n\teeeeeeeeee,\n\tffffffffff\n] # note\n";
assert_eq!(fmt(src), src);
assert_eq!(fmt(multi), src);
}
#[test]
fn standalone_comments_after_a_lambda_arg_hang_off_the_body() {
let src =
"func f():\n\tcall(func():\n\t\ta()\n\t\treturn b # t\n\t# between\n\t, [x, y])\n";
let want = "func f():\n\tcall(\n\t\tfunc():\n\t\t\ta()\n\t\t\treturn b, # t\n\t\t\t# between\n\t\t[x, y]\n\t)\n";
assert_eq!(fmt(src), want);
}
#[test]
fn comments_thread_through_an_operator_chain_wrap() {
let src = "func f():\n\tx = (\n\t\t\ta\n\t\t\t# note\n\t\t\t- b\n\t)\n";
let want = "func f():\n\tx = (\n\t\ta\n\t\t# note\n\t\t- b\n\t)\n";
assert_eq!(fmt(src), want);
}
#[test]
fn a_block_trailing_comment_keeps_its_shallower_indent() {
let src = "func f():\n\tif c:\n\t\tx()\n\t# trailing\n\n\nfunc g():\n\tpass\n";
let out = fmt(src);
assert!(
out.contains("\n\t# trailing\n"),
"comment kept at one tab:\n{out}"
);
}
}