use cstree::util::NodeOrToken;
use gdscript_syntax::{GdNode, SyntaxKind as S};
use crate::{FmtConfig, canonical_string, infix_prec};
const TAB_W: usize = 4;
enum El {
Node(GdNode),
Tok(S, String),
}
fn sig(node: &GdNode) -> Vec<El> {
let mut v = Vec::new();
for c in node.children_with_tokens() {
match c {
NodeOrToken::Node(n) => v.push(El::Node(n.clone())),
NodeOrToken::Token(t) => {
let k = t.kind();
if !k.is_trivia() && !k.is_synthetic_layout() {
v.push(El::Tok(k, t.text().to_owned()));
}
}
}
}
v
}
fn child_nodes(node: &GdNode) -> Vec<GdNode> {
node.children().map(Clone::clone).collect()
}
fn width(s: &str) -> usize {
s.chars().map(|c| if c == '\t' { TAB_W } else { 1 }).sum()
}
fn node_text(node: &GdNode) -> String {
let mut out = String::new();
for c in node.children_with_tokens() {
match c {
NodeOrToken::Node(n) => out.push_str(&node_text(n)),
NodeOrToken::Token(t) => {
let k = t.kind();
if k.is_trivia() || k.is_synthetic_layout() {
continue;
}
out.push_str(t.text());
if k == S::Comma {
out.push(' ');
}
}
}
}
out
}
struct W<'a> {
cfg: &'a FmtConfig,
unit: String,
max: usize,
src: String,
}
impl W<'_> {
fn indent(&self, level: usize) -> String {
self.unit.repeat(level)
}
}
fn find_child(node: &GdNode, kind: S) -> Option<GdNode> {
node.children().find(|c| c.kind() == kind).cloned()
}
fn find_child_ty(node: &GdNode) -> Option<GdNode> {
node.children()
.find(|c| matches!(c.kind(), S::TypeRef | S::TypedArray | S::TypedDict))
.cloned()
}
fn name_of(node: &GdNode) -> Option<String> {
let name = node.children().find(|c| c.kind() == S::Name)?;
name.children_with_tokens().find_map(|c| match c {
NodeOrToken::Token(t) if matches!(t.kind(), S::Ident | S::MatchKw | S::WhenKw) => {
Some(t.text().to_owned())
}
_ => None,
})
}
pub(crate) fn render(body: &str, indent: usize, cfg: &FmtConfig) -> Option<String> {
if !cfg.reflow {
return None;
}
let class_level = starts_with_class_keyword(body);
let block_header = body.trim_end().ends_with(':')
&& matches!(
leading_keyword(body),
"if" | "elif" | "else" | "for" | "while" | "match" | "func" | "class"
);
let parsed_src = if class_level {
if block_header {
format!("{body}\n\tpass\n")
} else {
format!("{body}\n")
}
} else {
let indented = reindent_to_skipping_strings(body, 1);
if block_header {
format!("func __():\n{indented}\n\t\tpass\n")
} else {
format!("func __():\n{indented}\n")
}
};
let parse = gdscript_syntax::parse(&parsed_src);
let w = W {
cfg,
unit: if cfg.use_tabs {
"\t".to_owned()
} else {
" ".repeat(cfg.indent_size.max(1))
},
max: cfg.line_width,
src: parsed_src,
};
let root = parse.syntax_node();
let container = if class_level {
root.clone()
} else {
let func = root.children().find(|c| c.kind() == S::FuncDecl)?;
func.children().find(|c| c.kind() == S::Block)?.clone()
};
let (anns, stmt) = find_statement_with_anns(&container)?;
let mut lines = format_statement(&w, &stmt, indent)?;
if !anns.is_empty() {
let prefix = anns.iter().map(node_text).collect::<Vec<_>>().join(" ");
let first = &lines[0];
let lead = &first[..first.len() - first.trim_start().len()];
let prepended = format!("{lead}{prefix} {}", &first[lead.len()..]);
if width(&prepended) > w.max + 1 {
lines.insert(0, format!("{lead}{prefix}"));
} else {
lines[0] = prepended;
}
}
let trailing = statement_trailing_comment(&container, &stmt, &w.src);
if lines.len() == 1 && trailing.is_none() {
return None;
}
if let Some(c) = trailing {
let last = lines.last_mut()?;
last.push_str(" ");
last.push_str(&c);
}
let out = lines.join("\n");
let out_norm = reindent_to(&dedent(&out), 1);
let body_norm = reindent_to(&dedent(body), 1);
let (mine, orig) = if class_level {
(dedent(&out), dedent(body))
} else {
(
format!("func __():\n{out_norm}\n"),
format!("func __():\n{body_norm}\n"),
)
};
if !crate::meaning_preserved(&mine, &orig) {
return None;
}
if comment_multiset(&out) != comment_multiset(body) {
return None;
}
Some(out)
}
fn comment_multiset(s: &str) -> Vec<String> {
let mut v: Vec<String> = gdscript_syntax::tokenize(s)
.into_iter()
.filter(|t| is_comment_kind(t.kind))
.map(|t| s[t.range].trim_end().to_owned())
.collect();
v.sort();
v
}
fn statement_trailing_comment(container: &GdNode, stmt: &GdNode, src: &str) -> Option<String> {
let (_, ce) = code_span(stmt)?;
let end_line = line_of(src, ce.saturating_sub(1));
let mut comments = Vec::new();
comment_tokens(container, src, &mut comments);
comments
.into_iter()
.find(|&(off, line, _)| off >= ce && line == end_line)
.map(|(_, _, text)| text)
}
pub(crate) fn dedent(s: &str) -> String {
let min = s
.lines()
.filter(|l| !l.trim().is_empty())
.map(|l| l.bytes().take_while(|&b| b == b'\t' || b == b' ').count())
.min()
.unwrap_or(0);
s.lines()
.map(|l| if l.len() >= min { &l[min..] } else { l })
.collect::<Vec<_>>()
.join("\n")
}
fn reindent_to_skipping_strings(s: &str, levels: usize) -> String {
let raw = gdscript_syntax::tokenize(s);
let mut inside: std::collections::HashSet<usize> = std::collections::HashSet::new();
for t in &raw {
if t.kind == S::String {
let (sl, el) = (
line_of(s, usize::from(t.range.start())),
line_of(s, usize::from(t.range.end()).saturating_sub(1)),
);
inside.extend((sl + 1)..=el); }
}
let pad = "\t".repeat(levels);
s.lines()
.enumerate()
.map(|(i, l)| {
if l.trim().is_empty() || inside.contains(&i) {
l.to_owned()
} else {
format!("{pad}{l}")
}
})
.collect::<Vec<_>>()
.join("\n")
}
fn reindent_to(s: &str, levels: usize) -> String {
let pad = "\t".repeat(levels);
s.lines()
.map(|l| {
if l.trim().is_empty() {
String::new()
} else {
format!("{pad}{l}")
}
})
.collect::<Vec<_>>()
.join("\n")
}
fn leading_keyword(body: &str) -> &str {
let t = body.trim_start();
let t = t.strip_prefix("static ").unwrap_or(t);
t.split([' ', '(', ':']).next().unwrap_or("")
}
fn starts_with_class_keyword(body: &str) -> bool {
let t = body.trim_start();
t.starts_with('@')
|| matches!(
leading_keyword(body),
"func" | "var" | "const" | "signal" | "enum" | "class" | "class_name" | "extends"
)
}
fn find_statement_with_anns(container: &GdNode) -> Option<(Vec<GdNode>, GdNode)> {
let mut anns = Vec::new();
for c in container.children() {
if c.kind() == S::Annotation {
anns.push(c.clone());
} else if is_statement_kind(c.kind()) {
return Some((anns, c.clone()));
}
}
anns.pop().map(|a| (Vec::new(), a))
}
fn is_statement_kind(k: S) -> bool {
matches!(
k,
S::VarDecl
| S::ConstDecl
| S::FuncDecl
| S::SignalDecl
| S::EnumDecl
| S::Annotation
| S::IfStmt
| S::ElifClause
| S::WhileStmt
| S::ForStmt
| S::MatchStmt
| S::ReturnStmt
| S::ExprStmt
)
}
fn has_error(node: &GdNode) -> bool {
if node.kind() == S::ErrorNode {
return true;
}
node.children().any(has_error)
}
fn format_statement(w: &W, stmt: &GdNode, indent: usize) -> Option<Vec<String>> {
match stmt.kind() {
S::VarDecl | S::ConstDecl => format_var_like(w, stmt, indent),
S::FuncDecl => format_func_header(w, stmt, indent),
S::SignalDecl => format_signal(w, stmt, indent),
S::EnumDecl => format_enum(w, stmt, indent),
S::Annotation => format_annotation(w, stmt, indent),
S::IfStmt | S::ElifClause => format_branch(w, stmt, indent),
S::WhileStmt => format_keyword_expr(w, stmt, indent, "while ", ":", S::WhileKw),
S::MatchStmt => format_keyword_expr(w, stmt, indent, "match ", ":", S::MatchKw),
S::ForStmt => format_for(w, stmt, indent),
S::ReturnStmt => format_return(w, stmt, indent),
S::ExprStmt => {
let expr = first_expr_child(stmt)?;
if has_error(&expr) {
return None;
}
format_expression(w, &expr, indent, "", "")
}
_ => None,
}
}
fn format_full_statement(w: &W, stmt: &GdNode, indent: usize) -> Option<Vec<String>> {
match stmt.kind() {
S::IfStmt => {
let mut out = format_branch(w, stmt, indent)?;
out.extend(format_block_body(
w,
&find_child(stmt, S::Block)?,
indent + 1,
"",
)?);
for clause in stmt.children() {
match clause.kind() {
S::ElifClause => {
out.extend(format_branch(w, clause, indent)?);
out.extend(format_block_body(
w,
&find_child(clause, S::Block)?,
indent + 1,
"",
)?);
}
S::ElseClause => {
out.push(format!("{}else:", w.indent(indent)));
out.extend(format_block_body(
w,
&find_child(clause, S::Block)?,
indent + 1,
"",
)?);
}
_ => {}
}
}
Some(out)
}
S::ForStmt | S::WhileStmt => {
let mut out = format_statement(w, stmt, indent)?;
out.extend(format_block_body(
w,
&find_child(stmt, S::Block)?,
indent + 1,
"",
)?);
Some(out)
}
S::MatchStmt => None,
_ => format_statement(w, stmt, indent),
}
}
fn is_comment_kind(k: S) -> bool {
matches!(
k,
S::LineComment | S::DocComment | S::RegionComment | S::EndRegionComment
)
}
fn line_of(src: &str, off: usize) -> usize {
src[..off.min(src.len())].matches('\n').count()
}
fn line_level(line: &str, unit: &str) -> usize {
if unit.is_empty() {
return 0;
}
let mut rest = line;
let mut n = 0;
while let Some(r) = rest.strip_prefix(unit) {
rest = r;
n += 1;
}
n
}
fn comment_tokens(node: &GdNode, src: &str, out: &mut Vec<(usize, usize, String)>) {
for c in node.children_with_tokens() {
match c {
NodeOrToken::Node(n) => comment_tokens(n, src, out),
NodeOrToken::Token(t) if is_comment_kind(t.kind()) => {
let off = usize::from(t.text_range().start());
out.push((off, line_of(src, off), t.text().trim_end().to_owned()));
}
NodeOrToken::Token(_) => {}
}
}
}
fn code_span(node: &GdNode) -> Option<(usize, usize)> {
fn rec(node: &GdNode, first: &mut Option<usize>, last: &mut usize) {
for c in node.children_with_tokens() {
match c {
NodeOrToken::Node(n) => rec(n, first, last),
NodeOrToken::Token(t) => {
if t.kind().is_trivia() || t.kind().is_synthetic_layout() {
continue;
}
let r = t.text_range();
if first.is_none() {
*first = Some(usize::from(r.start()));
}
*last = usize::from(r.end());
}
}
}
}
let mut first = None;
let mut last = 0usize;
rec(node, &mut first, &mut last);
first.map(|f| (f, last))
}
struct BlockComments {
before: Vec<Vec<String>>,
trailing: Vec<Option<String>>,
tail: Vec<String>,
}
fn collect_block_comments(block: &GdNode, src: &str, stmts: &[GdNode]) -> BlockComments {
let n = stmts.len();
let spans: Vec<Option<(usize, usize)>> = stmts.iter().map(code_span).collect();
let mut bc = BlockComments {
before: vec![Vec::new(); n],
trailing: vec![None; n],
tail: Vec::new(),
};
let body_len = spans.iter().flatten().next().map(|&(cs, _)| {
let ls = src[..cs].rfind('\n').map_or(0, |i| i + 1);
src[ls..cs].len()
});
let mut comments = Vec::new();
comment_tokens(block, src, &mut comments);
for (off, line, text) in comments {
if spans
.iter()
.flatten()
.any(|&(cs, ce)| cs <= off && off < ce)
{
continue; }
let prev = spans
.iter()
.enumerate()
.rev()
.find_map(|(i, s)| s.filter(|&(_, ce)| ce <= off).map(|(_, ce)| (i, ce)));
if let Some((i, _)) = prev.filter(|&(_, ce)| line_of(src, ce.saturating_sub(1)) == line) {
bc.trailing[i] = Some(text);
continue;
}
let ls = src[..off].rfind('\n').map_or(0, |i| i + 1);
if body_len.is_some_and(|bl| src[ls..off].len() < bl) {
continue; }
match spans.iter().position(|s| s.is_some_and(|(cs, _)| cs > off)) {
Some(nx) => bc.before[nx].push(text),
None => bc.tail.push(text),
}
}
bc
}
fn format_block_body(
w: &W,
block: &GdNode,
indent: usize,
tail_suffix: &str,
) -> Option<Vec<String>> {
let stmts: Vec<GdNode> = block.children().cloned().collect();
let bc = collect_block_comments(block, &w.src, &stmts);
let mut out = Vec::new();
let n = stmts.len();
for (i, stmt) in stmts.iter().enumerate() {
for c in &bc.before[i] {
out.push(format!("{}{c}", w.indent(indent)));
}
let mut lines = format_full_statement(w, stmt, indent)?;
if i + 1 == n && bc.tail.is_empty() {
lines.last_mut()?.push_str(tail_suffix);
}
if let Some(c) = &bc.trailing[i] {
let last = lines.last_mut()?;
last.push_str(" ");
last.push_str(c);
}
out.append(&mut lines);
}
for c in &bc.tail {
out.push(format!("{}{c}", w.indent(indent)));
}
Some(out)
}
fn first_expr_child(node: &GdNode) -> Option<GdNode> {
node.children().find(|c| is_expr_kind(c.kind())).cloned()
}
fn is_expr_kind(k: S) -> bool {
matches!(
k,
S::BinExpr
| S::UnaryExpr
| S::TernaryExpr
| S::CastExpr
| S::IsExpr
| S::InExpr
| S::CallExpr
| S::IndexExpr
| S::FieldExpr
| S::AwaitExpr
| S::LambdaExpr
| S::ParenExpr
| S::ArrayLit
| S::DictLit
| S::NameRef
| S::Literal
| S::GetNodeExpr
| S::UniqueNodeExpr
| S::PreloadExpr
)
}
fn format_var_like(w: &W, stmt: &GdNode, indent: usize) -> Option<Vec<String>> {
if stmt.children().any(|c| c.kind() == S::PropertyBody) {
return None;
}
let els = sig(stmt);
let mut assign: Option<&str> = None;
let mut ty: Option<String> = None;
let mut is_static = false;
let mut expr: Option<GdNode> = None;
let kw = if stmt.kind() == S::ConstDecl {
"const"
} else {
"var"
};
for el in &els {
match el {
El::Tok(S::StaticKw, _) => is_static = true,
El::Tok(S::Eq, _) => assign = Some("="),
El::Tok(S::ColonEq, _) => assign = Some(":="),
El::Node(n) if matches!(n.kind(), S::TypeRef | S::TypedArray | S::TypedDict) => {
ty = Some(node_text(n));
}
El::Node(n) if is_expr_kind(n.kind()) => expr = Some(n.clone()),
_ => {}
}
}
let name = name_of(stmt)?;
let (Some(expr), Some(assign)) = (expr, assign) else {
return None;
};
if has_error(&expr) {
return None;
}
let mut prefix = String::new();
if is_static {
prefix.push_str("static ");
}
prefix.push_str(kw);
prefix.push(' ');
prefix.push_str(&name);
if let Some(ty) = ty {
prefix.push_str(": ");
prefix.push_str(&ty);
}
prefix.push(' ');
prefix.push_str(assign);
prefix.push(' ');
format_expression(w, &expr, indent, &prefix, "")
}
fn format_func_header(w: &W, stmt: &GdNode, indent: usize) -> Option<Vec<String>> {
let mut is_static = false;
let mut params: Option<GdNode> = None;
let mut ret: Option<String> = None;
for el in &sig(stmt) {
match el {
El::Tok(S::StaticKw, _) => is_static = true,
El::Node(n) if n.kind() == S::ParamList => params = Some(n.clone()),
El::Node(n) if matches!(n.kind(), S::TypeRef | S::TypedArray | S::TypedDict) => {
ret = Some(node_text(n));
}
_ => {}
}
}
let name = name_of(stmt)?;
let params = params?;
let mut prefix = String::new();
if is_static {
prefix.push_str("static ");
}
prefix.push_str("func ");
prefix.push_str(&name);
let suffix = match ret {
Some(t) => format!(" -> {t}:"),
None => ":".to_owned(),
};
format_paren_list(w, ¶ms, indent, &prefix, &suffix, "(", ")")
}
fn format_signal(w: &W, stmt: &GdNode, indent: usize) -> Option<Vec<String>> {
let name = name_of(stmt)?;
let params = find_child(stmt, S::ParamList)?;
let prefix = format!("signal {name}");
format_paren_list(w, ¶ms, indent, &prefix, "", "(", ")")
}
fn format_annotation(w: &W, stmt: &GdNode, indent: usize) -> Option<Vec<String>> {
let name = sig(stmt).iter().find_map(|el| match el {
El::Tok(S::Ident, t) => Some(t.clone()),
_ => None,
})?;
let args = find_child(stmt, S::AnnotationArgList)?;
let prefix = format!("@{name}");
format_paren_list(w, &args, indent, &prefix, "", "(", ")")
}
fn format_branch(w: &W, stmt: &GdNode, indent: usize) -> Option<Vec<String>> {
let kw = if stmt.kind() == S::ElifClause {
"elif "
} else {
"if "
};
let expr = first_expr_child(stmt)?;
if has_error(&expr) {
return None;
}
format_expression(w, &expr, indent, kw, ":")
}
fn format_keyword_expr(
w: &W,
stmt: &GdNode,
indent: usize,
prefix: &str,
suffix: &str,
_kw: S,
) -> Option<Vec<String>> {
let expr = first_expr_child(stmt)?;
if has_error(&expr) {
return None;
}
format_expression(w, &expr, indent, prefix, suffix)
}
fn format_for(w: &W, stmt: &GdNode, indent: usize) -> Option<Vec<String>> {
let name = name_of(stmt)?;
let ty = find_child_ty(stmt).map(|n| node_text(&n));
let expr = first_expr_child(stmt)?;
if has_error(&expr) {
return None;
}
let prefix = match ty {
Some(t) => format!("for {name}: {t} in "),
None => format!("for {name} in "),
};
format_expression(w, &expr, indent, &prefix, ":")
}
fn format_return(w: &W, stmt: &GdNode, indent: usize) -> Option<Vec<String>> {
let Some(expr) = first_expr_child(stmt) else {
return None; };
if has_error(&expr) {
return None;
}
format_expression(w, &expr, indent, "return ", "")
}
fn format_enum(w: &W, stmt: &GdNode, indent: usize) -> Option<Vec<String>> {
let name = name_of(stmt);
let prefix = match name {
Some(n) => format!("enum {n} "),
None => "enum ".to_owned(),
};
let variants: Vec<GdNode> = stmt
.children()
.filter(|c| c.kind() == S::EnumVariant)
.cloned()
.collect();
let elements: Vec<String> = variants
.iter()
.map(|v| enum_variant_str(w, v))
.collect::<Option<Vec<_>>>()?;
let magic = has_trailing_comma(stmt);
let flat = format!(
"{}{}{{ {} }}",
w.indent(indent),
prefix,
elements.join(", ")
);
if !magic && width(&flat) <= w.max {
return Some(vec![flat]);
}
let mut out = vec![format!("{}{}{{", w.indent(indent), prefix)];
let inner = w.indent(indent + 1);
for e in &elements {
out.push(format!("{inner}{e},"));
}
out.push(format!("{}}}", w.indent(indent)));
Some(out)
}
fn enum_variant_str(w: &W, variant: &GdNode) -> Option<String> {
let name = variant.children_with_tokens().find_map(|c| match c {
NodeOrToken::Token(t) if matches!(t.kind(), S::Ident | S::MatchKw | S::WhenKw) => {
Some(t.text().to_owned())
}
_ => None,
})?;
match first_expr_child(variant) {
Some(val) => Some(format!("{name} = {}", expr_to_str(w, &val)?)),
None => Some(name),
}
}
fn format_expression(
w: &W,
node: &GdNode,
indent: usize,
prefix: &str,
suffix: &str,
) -> Option<Vec<String>> {
let node = remove_outer_parens(node);
format_concrete(w, &node, indent, prefix, suffix)
}
fn remove_outer_parens(node: &GdNode) -> GdNode {
let mut n = node.clone();
while n.kind() == S::ParenExpr {
match child_nodes(&n).into_iter().find(|c| is_expr_kind(c.kind())) {
Some(inner) => n = inner,
None => break,
}
}
n
}
fn format_concrete(
w: &W,
node: &GdNode,
indent: usize,
prefix: &str,
suffix: &str,
) -> Option<Vec<String>> {
if is_multiline_string(node) {
return multiline_string_lines(w, node, indent, prefix, suffix);
}
if is_foldable(node) {
format_foldable(w, node, indent, prefix, suffix)
} else {
Some(vec![single_line(w, node, indent, prefix, suffix)?])
}
}
fn multiline_string_text(node: &GdNode) -> Option<String> {
if node.kind() != S::Literal {
return None;
}
sig(node).into_iter().find_map(|el| match el {
El::Tok(S::String, t) if t.contains('\n') => Some(t),
_ => None,
})
}
fn is_multiline_string(node: &GdNode) -> bool {
multiline_string_text(node).is_some()
}
fn multiline_string_lines(
w: &W,
node: &GdNode,
indent: usize,
prefix: &str,
suffix: &str,
) -> Option<Vec<String>> {
let text = multiline_string_text(node)?;
let lines: Vec<&str> = text.split('\n').collect();
if lines.len() < 2 {
return None;
}
let mut out = vec![format!("{}{}{}", w.indent(indent), prefix, lines[0])];
out.extend(lines[1..lines.len() - 1].iter().map(|l| (*l).to_owned()));
out.push(format!("{}{}", lines[lines.len() - 1], suffix));
Some(out)
}
fn single_line(w: &W, node: &GdNode, indent: usize, prefix: &str, suffix: &str) -> Option<String> {
Some(format!(
"{}{}{}{}",
w.indent(indent),
prefix,
expr_to_str(w, node)?,
suffix
))
}
fn format_foldable(
w: &W,
node: &GdNode,
indent: usize,
prefix: &str,
suffix: &str,
) -> Option<Vec<String>> {
if forcing_multiline(node) || node_has_standalone_comment(node, &w.src) {
return explode(w, node, indent, prefix, suffix);
}
let line = single_line(w, node, indent, prefix, suffix)?;
if width(&line) <= w.max {
return Some(vec![line]);
}
explode(w, node, indent, prefix, suffix)
}
fn is_foldable(node: &GdNode) -> bool {
!matches!(
node.kind(),
S::Literal | S::NameRef | S::GetNodeExpr | S::UniqueNodeExpr
)
}
fn forcing_multiline(node: &GdNode) -> bool {
if is_multiline_string(node) {
return true; }
if node.kind() == S::LambdaExpr && lambda_forces_multiline(node) {
return true;
}
has_trailing_comma(node) || node.children().any(forcing_multiline)
}
fn node_contains_lambda(node: &GdNode) -> bool {
node.kind() == S::LambdaExpr || node.children().any(node_contains_lambda)
}
fn node_has_standalone_comment(node: &GdNode, src: &str) -> bool {
let Some((cs, ce)) = code_span(node) else {
return false;
};
let mut comments = Vec::new();
comment_tokens(node, src, &mut comments);
comments.iter().any(|&(off, _, _)| {
cs < off && off < ce && {
let line_start = src[..off].rfind('\n').map_or(0, |i| i + 1);
src[line_start..off].trim().is_empty()
}
})
}
fn lambda_forces_multiline(node: &GdNode) -> bool {
let Some(block) = find_child(node, S::Block) else {
return false;
};
let stmts: Vec<GdNode> = block.children().cloned().collect();
match stmts.as_slice() {
[one] => matches!(
one.kind(),
S::IfStmt | S::ElifClause | S::ElseClause | S::ForStmt | S::WhileStmt | S::MatchStmt
),
_ => true,
}
}
fn has_trailing_comma(node: &GdNode) -> bool {
if !matches!(
node.kind(),
S::ArgList | S::ArrayLit | S::DictLit | S::ParamList | S::AnnotationArgList | S::EnumDecl
) {
return false;
}
let mut last_significant: Option<S> = None;
for c in node.children_with_tokens() {
match c {
NodeOrToken::Node(_) => last_significant = Some(S::Tombstone), NodeOrToken::Token(t) => {
let k = t.kind();
if k.is_trivia() || k.is_synthetic_layout() {
continue;
}
if is_close_bracket(k) {
return last_significant == Some(S::Comma);
}
last_significant = Some(k);
}
}
}
false
}
fn is_close_bracket(k: S) -> bool {
matches!(k, S::RParen | S::RBrack | S::RBrace)
}
fn explode(w: &W, node: &GdNode, indent: usize, prefix: &str, suffix: &str) -> Option<Vec<String>> {
match node.kind() {
S::BinExpr => {
let op = bin_op(node)?;
if is_assign_op(op) {
format_assignment(w, node, indent, prefix, suffix)
} else {
format_operator_chain(w, node, indent, prefix, suffix)
}
}
S::TernaryExpr | S::IsExpr | S::CastExpr | S::InExpr => {
format_operator_chain(w, node, indent, prefix, suffix)
}
S::UnaryExpr => format_unary(w, node, indent, prefix, suffix),
S::AwaitExpr => format_await(w, node, indent, prefix, suffix),
S::CallExpr => format_call(w, node, indent, prefix, suffix),
S::PreloadExpr => format_preload(w, node, indent, prefix, suffix),
S::IndexExpr => format_index(w, node, indent, prefix, suffix),
S::FieldExpr => format_dot_chain(w, node, indent, prefix, suffix),
S::ArrayLit => format_paren_list(w, node, indent, prefix, suffix, "[", "]"),
S::DictLit => format_paren_list(w, node, indent, prefix, suffix, "{", "}"),
S::ParenExpr => {
let inner = child_nodes(node)
.into_iter()
.find(|c| is_expr_kind(c.kind()))?;
format_expression(
w,
&inner,
indent,
&format!("{prefix}("),
&format!("){suffix}"),
)
}
S::DictEntry => format_dict_entry(w, node, indent, prefix, suffix),
S::LambdaExpr => format_lambda(w, node, indent, prefix, suffix),
_ => None,
}
}
fn format_lambda(
w: &W,
node: &GdNode,
indent: usize,
prefix: &str,
suffix: &str,
) -> Option<Vec<String>> {
let header = lambda_header_str(w, node)?;
let mut out = vec![format!("{}{prefix}{header}", w.indent(indent))];
let block = find_child(node, S::Block)?;
out.extend(format_block_body(w, &block, indent + 1, suffix)?);
Some(out)
}
fn dict_entry_kv(node: &GdNode) -> Option<(GdNode, GdNode, bool)> {
let nodes = child_nodes(node);
if nodes.len() == 1 && nodes[0].kind() == S::BinExpr && bin_op(&nodes[0]) == Some(S::Eq) {
let bn = child_nodes(&nodes[0]);
return Some((bn.first()?.clone(), bn.get(1)?.clone(), false));
}
let colon = sig(node).iter().any(|e| matches!(e, El::Tok(S::Colon, _)));
Some((nodes.first()?.clone(), nodes.get(1)?.clone(), colon))
}
fn format_dict_entry(
w: &W,
node: &GdNode,
indent: usize,
prefix: &str,
suffix: &str,
) -> Option<Vec<String>> {
let (key, val, colon) = dict_entry_kv(node)?;
let key_suffix = if colon { ":" } else { " =" };
let mut out = format_expression(w, &key, indent, prefix, key_suffix)?;
out.extend(format_expression(w, &val, indent, "", suffix)?);
Some(out)
}
fn bin_op(node: &GdNode) -> Option<S> {
sig(node).into_iter().find_map(|el| match el {
El::Tok(k, _) if infix_prec(k).is_some() || is_assign_op(k) => Some(k),
_ => None,
})
}
fn is_assign_op(k: S) -> bool {
matches!(
k,
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 format_assignment(
w: &W,
node: &GdNode,
indent: usize,
prefix: &str,
suffix: &str,
) -> Option<Vec<String>> {
let nodes = child_nodes(node);
let lhs = nodes.first()?;
let rhs = nodes.get(1)?;
let op = bin_op(node)?;
let new_prefix = format!("{prefix}{} {} ", expr_to_str(w, lhs)?, op_str(op));
format_concrete(w, rhs, indent, &new_prefix, suffix)
}
fn op_str(k: S) -> &'static str {
match k {
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 => ":=",
_ => "",
}
}
enum Operand {
Node(GdNode),
Text(String),
}
fn chain_parts(node: &GdNode) -> Option<(Vec<Operand>, Vec<String>)> {
match node.kind() {
S::BinExpr => {
let op = bin_op(node)?;
let prec = infix_prec(op)?;
let nodes = child_nodes(node);
let lhs = nodes.first()?.clone();
let rhs = nodes.get(1)?.clone();
let (mut operands, mut ops) =
if lhs.kind() == S::BinExpr && bin_op(&lhs).and_then(infix_prec) == Some(prec) {
chain_parts(&lhs)?
} else {
(vec![Operand::Node(lhs)], Vec::new())
};
ops.push(op_str_bin(op).to_owned());
operands.push(Operand::Node(rhs));
Some((operands, ops))
}
S::TernaryExpr => {
let nodes = child_nodes(node);
let a = nodes.first()?.clone();
let c = nodes.get(1)?.clone();
let b = nodes.get(2)?.clone();
Some((
vec![Operand::Node(a), Operand::Node(c), Operand::Node(b)],
vec!["if".to_owned(), "else".to_owned()],
))
}
S::InExpr => {
let nodes = child_nodes(node);
let a = nodes.first()?.clone();
let b = nodes.get(1)?.clone();
let not = sig(node).iter().any(|e| matches!(e, El::Tok(S::NotKw, _)));
let op = if not { "not in" } else { "in" };
Some((
vec![Operand::Node(a), Operand::Node(b)],
vec![op.to_owned()],
))
}
S::IsExpr => {
let nodes = child_nodes(node);
let a = nodes.first()?.clone();
let ty = node
.children()
.find(|c| matches!(c.kind(), S::TypeRef | S::TypedArray | S::TypedDict))?;
let not = sig(node).iter().any(|e| matches!(e, El::Tok(S::NotKw, _)));
let op = if not { "is not" } else { "is" };
Some((
vec![Operand::Node(a), Operand::Text(node_text(ty))],
vec![op.to_owned()],
))
}
S::CastExpr => {
let nodes = child_nodes(node);
let a = nodes.first()?.clone();
let ty = node
.children()
.find(|c| matches!(c.kind(), S::TypeRef | S::TypedArray | S::TypedDict))?;
Some((
vec![Operand::Node(a), Operand::Text(node_text(ty))],
vec!["as".to_owned()],
))
}
_ => None,
}
}
fn op_str_bin(k: S) -> &'static str {
match k {
S::Plus => "+",
S::Minus => "-",
S::Star => "*",
S::Slash => "/",
S::Percent => "%",
S::StarStar => "**",
S::EqEq => "==",
S::Neq => "!=",
S::Lt => "<",
S::Gt => ">",
S::Le => "<=",
S::Ge => ">=",
S::AndKw => "and",
S::OrKw => "or",
S::AmpAmp => "&&",
S::PipePipe => "||",
S::Pipe => "|",
S::Caret => "^",
S::Amp => "&",
S::Shl => "<<",
S::Shr => ">>",
S::InKw => "in",
_ => "",
}
}
fn format_operator_chain(
w: &W,
node: &GdNode,
indent: usize,
prefix: &str,
suffix: &str,
) -> Option<Vec<String>> {
let (operands, ops) = chain_parts(node)?;
let inside_par = (prefix.ends_with('(') && suffix.starts_with(')'))
|| (prefix.ends_with('[') && suffix.starts_with(']'));
let (lpar, rpar) = if inside_par { ("", "") } else { ("(", ")") };
let mut out = vec![format!("{}{}{}", w.indent(indent), prefix, lpar)];
let child = indent + 1;
let (before, tail) = chain_standalone_comments(w, &operands, node);
let has_standalone = node_has_standalone_comment(node, &w.src);
if let Some(flat) = expr_to_str(w, node) {
let compact = format!("{}{}", w.indent(child), flat);
if !forcing_multiline(node) && !has_standalone && width(&compact) <= w.max {
out.push(compact);
out.push(format!("{}{}{}", w.indent(indent), rpar, suffix));
return Some(out);
}
}
for c in &before[0] {
out.push(format!("{}{c}", w.indent(child)));
}
out.extend(format_operand(w, &operands[0], child, "", "")?);
for (i, op) in ops.iter().enumerate() {
for c in &before[i + 1] {
out.push(format!("{}{c}", w.indent(child)));
}
let pfx = format!("{op} ");
out.extend(format_operand(w, &operands[i + 1], child, &pfx, "")?);
}
for c in &tail {
out.push(format!("{}{c}", w.indent(child)));
}
out.push(format!("{}{}{}", w.indent(indent), rpar, suffix));
Some(out)
}
fn chain_standalone_comments(
w: &W,
operands: &[Operand],
node: &GdNode,
) -> (Vec<Vec<String>>, Vec<String>) {
let spans: Vec<Option<(usize, usize)>> = operands
.iter()
.map(|o| match o {
Operand::Node(n) => code_span(n),
Operand::Text(_) => None,
})
.collect();
let mut before: Vec<Vec<String>> = vec![Vec::new(); operands.len()];
let mut tail: Vec<String> = Vec::new();
let mut all = Vec::new();
comment_tokens(node, &w.src, &mut all);
for (off, _, text) in all {
let line_start = w.src[..off].rfind('\n').map_or(0, |i| i + 1);
if !w.src[line_start..off].trim().is_empty() {
continue; }
if spans.iter().flatten().any(|&(s, e)| s <= off && off < e) {
continue; }
match spans.iter().position(|s| s.is_some_and(|(s, _)| s > off)) {
Some(i) => before[i].push(text),
None => tail.push(text),
}
}
(before, tail)
}
fn format_operand(
w: &W,
operand: &Operand,
indent: usize,
prefix: &str,
suffix: &str,
) -> Option<Vec<String>> {
match operand {
Operand::Node(n) => paren_operand_with_leading_comment(w, n, indent, prefix, suffix)
.or_else(|| format_concrete(w, n, indent, prefix, suffix)),
Operand::Text(t) => Some(vec![format!(
"{}{}{}{}",
w.indent(indent),
prefix,
t,
suffix
)]),
}
}
fn paren_operand_with_leading_comment(
w: &W,
n: &GdNode,
indent: usize,
prefix: &str,
suffix: &str,
) -> Option<Vec<String>> {
if n.kind() != S::ParenExpr {
return None;
}
let inner = n.children().find(|c| is_expr_kind(c.kind()))?;
let inner_start = code_span(inner)?.0;
let mut comments = Vec::new();
comment_tokens(n, &w.src, &mut comments);
let mut leading: Vec<String> = Vec::new();
for (off, _, text) in &comments {
let line_start = w.src[..*off].rfind('\n').map_or(0, |i| i + 1);
if !w.src[line_start..*off].trim().is_empty() || *off >= inner_start {
return None;
}
leading.push(text.clone());
}
if leading.is_empty() || forcing_multiline(inner) {
return None;
}
let flat = expr_to_str(w, n)?;
let compact = format!("{}{prefix}{flat}{suffix}", w.indent(indent));
if width(&compact) > w.max {
return None; }
let mut out = vec![compact];
out.extend(
leading
.into_iter()
.map(|c| format!("{}{c}", w.indent(indent))),
);
Some(out)
}
fn format_unary(
w: &W,
node: &GdNode,
indent: usize,
prefix: &str,
suffix: &str,
) -> Option<Vec<String>> {
let op = sig(node).into_iter().find_map(|el| match el {
El::Tok(k @ (S::NotKw | S::Bang | S::Minus | S::Plus | S::Tilde), _) => Some(k),
_ => None,
})?;
let operand = first_expr_child(node)?;
let spacing = if op == S::NotKw { " " } else { "" };
let op_txt = match op {
S::NotKw => "not",
S::Bang => "!",
S::Minus => "-",
S::Plus => "+",
S::Tilde => "~",
_ => return None,
};
format_concrete(
w,
&operand,
indent,
&format!("{prefix}{op_txt}{spacing}"),
suffix,
)
}
fn format_await(
w: &W,
node: &GdNode,
indent: usize,
prefix: &str,
suffix: &str,
) -> Option<Vec<String>> {
let operand = first_expr_child(node)?;
format_concrete(w, &operand, indent, &format!("{prefix}await "), suffix)
}
fn format_call(
w: &W,
node: &GdNode,
indent: usize,
prefix: &str,
suffix: &str,
) -> Option<Vec<String>> {
let nodes = child_nodes(node);
let callee = nodes.first()?;
if matches!(callee.kind(), S::FieldExpr | S::IndexExpr | S::CallExpr) {
return format_dot_chain(w, node, indent, prefix, suffix);
}
let arglist = find_child(node, S::ArgList)?;
let callee_str = expr_to_str(w, callee)?;
let elems = list_elements(&arglist);
if elems.is_empty() {
return Some(vec![format!(
"{}{}{}(){}",
w.indent(indent),
prefix,
callee_str,
suffix
)]);
}
let new_prefix = format!("{prefix}{callee_str}(");
let new_suffix = format!("){suffix}");
format_comma_list(w, &arglist, indent, &new_prefix, &new_suffix)
}
fn format_preload(
w: &W,
node: &GdNode,
indent: usize,
prefix: &str,
suffix: &str,
) -> Option<Vec<String>> {
let arglist = find_child(node, S::ArgList)?;
let new_prefix = format!("{prefix}preload(");
let new_suffix = format!("){suffix}");
format_comma_list(w, &arglist, indent, &new_prefix, &new_suffix)
}
fn format_index(
w: &W,
node: &GdNode,
indent: usize,
prefix: &str,
suffix: &str,
) -> Option<Vec<String>> {
let nodes = child_nodes(node);
let obj = nodes.first()?;
let idx = nodes.get(1)?;
if matches!(obj.kind(), S::FieldExpr | S::CallExpr | S::IndexExpr) {
return format_dot_chain(w, node, indent, prefix, suffix);
}
if matches!(obj.kind(), S::ArrayLit | S::DictLit)
&& let Some(idx_str) = expr_to_str(w, idx)
{
return format_expression(w, obj, indent, prefix, &format!("[{idx_str}]{suffix}"));
}
let obj_str = expr_to_str(w, obj)?;
format_expression(
w,
idx,
indent,
&format!("{prefix}{obj_str}["),
&format!("]{suffix}"),
)
}
fn format_paren_list(
w: &W,
list_node: &GdNode,
indent: usize,
prefix: &str,
suffix: &str,
open: &str,
close: &str,
) -> Option<Vec<String>> {
let elems = list_elements(list_node);
let magic = has_trailing_comma(list_node);
if !magic && !elems.iter().any(forcing_multiline) {
let flat_elems = elems
.iter()
.map(|e| expr_to_str(w, e))
.collect::<Option<Vec<_>>>()?;
let flat = format!(
"{}{}{}{}{}{}",
w.indent(indent),
prefix,
open,
flat_elems.join(", "),
close,
suffix
);
if width(&flat) <= w.max {
return Some(vec![flat]);
}
}
if elems.is_empty() {
return Some(vec![format!(
"{}{}{}{}{}",
w.indent(indent),
prefix,
open,
close,
suffix
)]);
}
format_comma_list(
w,
list_node,
indent,
&format!("{prefix}{open}"),
&format!("{close}{suffix}"),
)
}
fn format_comma_list(
w: &W,
list_node: &GdNode,
indent: usize,
prefix: &str,
suffix: &str,
) -> Option<Vec<String>> {
let elems = list_elements(list_node);
let magic = has_trailing_comma(list_node);
let lc = collect_list_comments(list_node, &w.src, &elems);
let mut out = vec![format!("{}{}", w.indent(indent), prefix)];
let child = indent + 1;
let compact = (!magic && !lc.any() && !elems.iter().any(forcing_multiline))
.then(|| {
elems
.iter()
.map(|e| expr_to_str(w, e))
.collect::<Option<Vec<_>>>()
.map(|parts| parts.join(", "))
})
.flatten()
.map(|flat| format!("{}{}", w.indent(child), flat))
.filter(|line| width(line) <= w.max);
if let Some(compact) = compact {
out.push(compact);
out.push(format!("{}{}", w.indent(indent), suffix));
return Some(out);
}
if let Some(c) = &lc.after_open {
let last = out.last_mut()?;
last.push_str(" ");
last.push_str(c);
}
let n = elems.len();
let mut prev_last_level = child;
for (i, e) in elems.iter().enumerate() {
for c in &lc.before[i] {
out.push(format!("{}{c}", w.indent(prev_last_level.max(child))));
}
let elem_suffix = if i + 1 < n || magic { "," } else { "" };
let mut lines = format_expression(w, e, child, "", elem_suffix)?;
if let Some(c) = &lc.trailing[i] {
let last = lines.last_mut()?;
last.push_str(" ");
last.push_str(c);
}
prev_last_level = lines.last().map_or(child, |l| line_level(l, &w.unit));
out.append(&mut lines);
}
for c in &lc.tail {
out.push(format!("{}{c}", w.indent(prev_last_level.max(child))));
}
out.push(format!("{}{}", w.indent(indent), suffix));
Some(out)
}
struct ListComments {
after_open: Option<String>,
before: Vec<Vec<String>>,
trailing: Vec<Option<String>>,
tail: Vec<String>,
}
impl ListComments {
fn any(&self) -> bool {
self.after_open.is_some()
|| self.before.iter().any(|v| !v.is_empty())
|| self.trailing.iter().any(Option::is_some)
|| !self.tail.is_empty()
}
}
fn owned_end(elem: &GdNode, src: &str) -> usize {
let full = usize::from(elem.text_range().end());
let (Some(body_len), Some((_, ce))) = (lambda_body_indent_len(elem, src), code_span(elem))
else {
return full;
};
let mut comments = Vec::new();
comment_tokens(elem, src, &mut comments);
for (off, _, _) in comments {
if off < ce {
continue;
}
let ls = src[..off].rfind('\n').map_or(0, |i| i + 1);
let lead = &src[ls..off];
if lead.trim().is_empty() && lead.len() < body_len {
return off; }
}
full
}
fn lambda_body_indent_len(elem: &GdNode, src: &str) -> Option<usize> {
let mut blocks = Vec::new();
collect_blocks(elem, &mut blocks);
let block = blocks.into_iter().next()?;
let first = block.children().next()?;
let (cs, _) = code_span(first)?;
let ls = src[..cs].rfind('\n').map_or(0, |i| i + 1);
Some(src[ls..cs].len())
}
fn collect_blocks(node: &GdNode, out: &mut Vec<GdNode>) {
for c in node.children() {
if c.kind() == S::Block {
out.push(c.clone());
}
collect_blocks(c, out);
}
}
fn collect_list_comments(list_node: &GdNode, src: &str, elems: &[GdNode]) -> ListComments {
let n = elems.len();
let mut lc = ListComments {
after_open: None,
before: vec![Vec::new(); n],
trailing: vec![None; n],
tail: Vec::new(),
};
let Some((open_off, _)) = code_span(list_node) else {
return lc;
};
let open_line = line_of(src, open_off);
let spans: Vec<Option<(usize, usize)>> = elems.iter().map(code_span).collect();
let owned: Vec<Option<(usize, usize)>> = elems
.iter()
.zip(&spans)
.map(|(e, s)| s.map(|(cs, _)| (cs, owned_end(e, src))))
.collect();
let first_start = spans.iter().flatten().map(|&(cs, _)| cs).min();
let mut comments = Vec::new();
comment_tokens(list_node, src, &mut comments);
for (off, line, text) in comments {
if owned
.iter()
.flatten()
.any(|&(cs, ne)| cs <= off && off < ne)
{
continue; }
if line == open_line && first_start.is_none_or(|fs| off < fs) {
lc.after_open = Some(text);
continue;
}
let prev = spans
.iter()
.enumerate()
.rev()
.find_map(|(i, s)| s.filter(|&(_, ce)| ce <= off).map(|(_, ce)| (i, ce)));
if let Some((i, _)) = prev.filter(|&(_, ce)| line_of(src, ce.saturating_sub(1)) == line) {
lc.trailing[i] = Some(text);
continue;
}
match spans.iter().position(|s| s.is_some_and(|(cs, _)| cs > off)) {
Some(nx) => lc.before[nx].push(text),
None => lc.tail.push(text),
}
}
lc
}
fn list_elements(list_node: &GdNode) -> Vec<GdNode> {
list_node
.children()
.filter(|c| is_expr_kind(c.kind()) || matches!(c.kind(), S::DictEntry | S::Param))
.cloned()
.collect()
}
enum Seg {
Field(String), Call(GdNode), Index(GdNode), }
fn collapse_chain(node: &GdNode) -> Option<(GdNode, Vec<Seg>)> {
let mut segs = Vec::new();
let mut cur = node.clone();
loop {
match cur.kind() {
S::CallExpr => {
let nodes = child_nodes(&cur);
let callee = nodes.first()?.clone();
let args = find_child(&cur, S::ArgList)?;
segs.push(Seg::Call(args));
cur = callee;
}
S::IndexExpr => {
let nodes = child_nodes(&cur);
let obj = nodes.first()?.clone();
let idx = nodes.get(1)?.clone();
segs.push(Seg::Index(idx));
cur = obj;
}
S::FieldExpr => {
let nodes = child_nodes(&cur);
if nodes.len() < 2 {
return None;
}
let obj = nodes.first()?.clone();
let name = node_text(nodes.last()?);
segs.push(Seg::Field(name));
cur = obj;
}
_ => break,
}
}
segs.reverse();
Some((cur, segs))
}
fn format_dot_chain(
w: &W,
node: &GdNode,
indent: usize,
prefix: &str,
suffix: &str,
) -> Option<Vec<String>> {
let endable = matches!(node.kind(), S::CallExpr | S::IndexExpr);
let bottom_up = if endable && node_contains_lambda(node) {
format_chain_bottom_up(w, node, indent, prefix, suffix)
} else if endable && !forcing_multiline(node) {
format_chain_bottom_up(w, node, indent, prefix, suffix)
.filter(|b| b.iter().all(|l| width(l) <= w.max))
} else {
None
};
if let Some(bottom_up) = bottom_up {
return Some(bottom_up);
}
format_chain_leading_dot(w, node, indent, prefix, suffix)
}
fn format_chain_bottom_up(
w: &W,
node: &GdNode,
indent: usize,
prefix: &str,
suffix: &str,
) -> Option<Vec<String>> {
match node.kind() {
S::CallExpr => {
let nodes = child_nodes(node);
let callee = nodes.first()?;
if node_contains_lambda(callee) {
return None;
}
let arglist = find_child(node, S::ArgList)?;
let callee_str = expr_to_str(w, callee)?;
let elems = list_elements(&arglist);
if elems.is_empty() {
return None; }
format_comma_list(
w,
&arglist,
indent,
&format!("{prefix}{callee_str}("),
&format!("){suffix}"),
)
}
S::IndexExpr => {
let nodes = child_nodes(node);
let obj = nodes.first()?;
let idx = nodes.get(1)?;
if node_contains_lambda(obj) {
return None;
}
let obj_str = expr_to_str(w, obj)?;
format_expression(
w,
idx,
indent,
&format!("{prefix}{obj_str}["),
&format!("]{suffix}"),
)
}
_ => None,
}
}
struct Unit {
lead_dot: bool,
head: String, base_node: Option<GdNode>, call: Option<GdNode>,
index: Option<GdNode>,
}
fn format_chain_leading_dot(
w: &W,
node: &GdNode,
indent: usize,
prefix: &str,
suffix: &str,
) -> Option<Vec<String>> {
let (base, segs) = collapse_chain(node)?;
let mut units: Vec<Unit> = vec![Unit {
lead_dot: false,
head: String::new(),
base_node: Some(base.clone()),
call: None,
index: None,
}];
for seg in segs {
match seg {
Seg::Field(name) => units.push(Unit {
lead_dot: true,
head: name,
base_node: None,
call: None,
index: None,
}),
Seg::Call(args) => {
let u = units.last_mut()?;
if u.call.is_some() || u.index.is_some() {
return None;
}
u.call = Some(args);
}
Seg::Index(idx) => {
let u = units.last_mut()?;
if u.index.is_some() {
return None;
}
u.index = Some(idx);
}
}
}
let inside_par = (prefix.ends_with('(') && suffix.starts_with(')'))
|| (prefix.ends_with('[') && suffix.starts_with(']'));
let (lpar, rpar) = if inside_par { ("", "") } else { ("(", ")") };
let mut out = vec![format!("{}{}{}", w.indent(indent), prefix, lpar)];
let child = indent + 1;
if let Some(flat) = expr_to_str(w, node) {
let compact = format!("{}{}", w.indent(child), flat);
if !forcing_multiline(node) && width(&compact) <= w.max {
out.push(compact);
out.push(format!("{}{}{}", w.indent(indent), rpar, suffix));
return Some(out);
}
}
for u in &units {
let lead = if u.lead_dot { ". " } else { "" };
let head = if let Some(bn) = &u.base_node {
expr_to_str(w, bn)?
} else {
u.head.clone()
};
let idx_suffix = match &u.index {
Some(idx) if u.call.is_some() => format!("[{}]", expr_to_str(w, idx)?),
_ => String::new(),
};
if let Some(args) = &u.call {
let flat = format!(
"{}{}{}({}){idx_suffix}",
w.indent(child),
lead,
head,
args_to_str(w, args)?
);
if !forcing_multiline(args) && width(&flat) <= w.max {
out.push(flat);
} else if list_elements(args).is_empty() {
out.push(format!("{}{}{}(){idx_suffix}", w.indent(child), lead, head));
} else {
out.extend(format_comma_list(
w,
args,
child,
&format!("{lead}{head}("),
&format!("){idx_suffix}"),
)?);
}
} else if let Some(idx) = &u.index {
out.push(format!(
"{}{}{}[{}]",
w.indent(child),
lead,
head,
expr_to_str(w, idx)?
));
} else {
out.push(format!("{}{}{}", w.indent(child), lead, head));
}
}
out.push(format!("{}{}{}", w.indent(indent), rpar, suffix));
Some(out)
}
#[allow(
clippy::too_many_lines,
reason = "one dispatch table mirroring gdformat's expression_to_str — splitting it would obscure the 1:1 mapping"
)]
fn expr_to_str(w: &W, node: &GdNode) -> Option<String> {
match node.kind() {
S::Literal => {
let els = sig(node);
let El::Tok(k, t) = els.first()? else {
return None;
};
if *k == S::String && w.cfg.normalize_strings {
Some(canonical_string(t))
} else {
Some(t.clone())
}
}
S::NameRef | S::GetNodeExpr | S::UniqueNodeExpr => Some(node_text(node)),
S::FieldExpr => {
let nodes = child_nodes(node);
if nodes.len() < 2 {
return None;
}
let obj = expr_to_str(w, nodes.first()?)?;
let name = node_text(nodes.last()?);
Some(format!("{obj}.{name}"))
}
S::IndexExpr => {
let nodes = child_nodes(node);
Some(format!(
"{}[{}]",
expr_to_str(w, nodes.first()?)?,
expr_to_str(w, nodes.get(1)?)?
))
}
S::CallExpr => {
let nodes = child_nodes(node);
let callee = expr_to_str(w, nodes.first()?)?;
let arglist = find_child(node, S::ArgList)?;
Some(format!("{callee}({})", args_to_str(w, &arglist)?))
}
S::PreloadExpr => {
let arglist = find_child(node, S::ArgList)?;
Some(format!("preload({})", args_to_str(w, &arglist)?))
}
S::BinExpr => {
let nodes = child_nodes(node);
let op = bin_op(node)?;
let lhs = expr_to_str(w, nodes.first()?)?;
let rhs = expr_to_str(w, nodes.get(1)?)?;
let op_txt = if is_assign_op(op) {
op_str(op)
} else {
op_str_bin(op)
};
Some(format!("{lhs} {op_txt} {rhs}"))
}
S::UnaryExpr => {
let op = sig(node).into_iter().find_map(|el| match el {
El::Tok(k @ (S::NotKw | S::Bang | S::Minus | S::Plus | S::Tilde), _) => Some(k),
_ => None,
})?;
let operand = expr_to_str(w, &first_expr_child(node)?)?;
let (op_txt, sp) = match op {
S::NotKw => ("not", " "),
S::Bang => ("!", ""),
S::Minus => ("-", ""),
S::Plus => ("+", ""),
S::Tilde => ("~", ""),
_ => return None,
};
Some(format!("{op_txt}{sp}{operand}"))
}
S::AwaitExpr => Some(format!(
"await {}",
expr_to_str(w, &first_expr_child(node)?)?
)),
S::TernaryExpr => {
let nodes = child_nodes(node);
Some(format!(
"{} if {} else {}",
expr_to_str(w, nodes.first()?)?,
expr_to_str(w, nodes.get(1)?)?,
expr_to_str(w, nodes.get(2)?)?
))
}
S::IsExpr => {
let a = expr_to_str(w, &first_expr_child(node)?)?;
let ty = node
.children()
.find(|c| matches!(c.kind(), S::TypeRef | S::TypedArray | S::TypedDict))?;
let not = sig(node).iter().any(|e| matches!(e, El::Tok(S::NotKw, _)));
Some(format!(
"{a} {} {}",
if not { "is not" } else { "is" },
node_text(ty)
))
}
S::CastExpr => {
let a = expr_to_str(w, &first_expr_child(node)?)?;
let ty = node
.children()
.find(|c| matches!(c.kind(), S::TypeRef | S::TypedArray | S::TypedDict))?;
Some(format!("{a} as {}", node_text(ty)))
}
S::InExpr => {
let nodes = child_nodes(node);
let a = expr_to_str(w, nodes.first()?)?;
let b = expr_to_str(w, nodes.get(1)?)?;
let not = sig(node).iter().any(|e| matches!(e, El::Tok(S::NotKw, _)));
Some(format!("{a} {} {b}", if not { "not in" } else { "in" }))
}
S::ParenExpr => {
let inner = child_nodes(node)
.into_iter()
.find(|c| is_expr_kind(c.kind()))?;
Some(format!(
"({})",
expr_to_str(w, &remove_outer_parens(&inner))?
))
}
S::ArrayLit => {
let elems = list_elements(node)
.iter()
.map(|e| expr_to_str(w, e))
.collect::<Option<Vec<_>>>()?;
Some(format!("[{}]", elems.join(", ")))
}
S::DictLit => {
let elems = list_elements(node)
.iter()
.map(|e| expr_to_str(w, e))
.collect::<Option<Vec<_>>>()?;
Some(format!("{{{}}}", elems.join(", ")))
}
S::DictEntry => {
let (key, val, colon) = dict_entry_kv(node)?;
let key = expr_to_str(w, &key)?;
let val = expr_to_str(w, &val)?;
Some(if colon {
format!("{key}: {val}")
} else {
format!("{key} = {val}")
})
}
S::Param => {
let name = name_of(node)?;
let ty = find_child_ty(node).map(|t| node_text(&t));
let default = first_expr_child(node);
let inferred = sig(node)
.iter()
.any(|e| matches!(e, El::Tok(S::ColonEq, _)));
let mut s = name;
if let Some(ty) = ty {
s.push_str(": ");
s.push_str(&ty);
}
if let Some(def) = default {
s.push_str(if inferred { " := " } else { " = " });
s.push_str(&expr_to_str(w, &def)?);
}
Some(s)
}
S::LambdaExpr => {
let header = lambda_header_str(w, node)?;
let block = find_child(node, S::Block)?;
let stmts: Vec<GdNode> = block.children().cloned().collect();
let [stmt] = stmts.as_slice() else {
return None;
};
Some(format!("{header} {}", stmt_to_str(w, stmt)?))
}
_ => None,
}
}
fn lambda_header_str(w: &W, node: &GdNode) -> Option<String> {
let name = name_of(node).map(|n| format!(" {n}")).unwrap_or_default();
let params = find_child(node, S::ParamList)?;
let ty = find_child_ty(node)
.map(|t| format!(" -> {}", node_text(&t)))
.unwrap_or_default();
Some(format!("func{name}({}){ty}:", args_to_str(w, ¶ms)?))
}
fn stmt_to_str(w: &W, stmt: &GdNode) -> Option<String> {
match stmt.kind() {
S::ExprStmt => expr_to_str(w, &first_expr_child(stmt)?),
S::ReturnStmt => match first_expr_child(stmt) {
Some(e) => Some(format!("return {}", expr_to_str(w, &e)?)),
None => Some("return".to_owned()),
},
S::PassStmt => Some("pass".to_owned()),
S::BreakStmt => Some("break".to_owned()),
S::ContinueStmt => Some("continue".to_owned()),
_ => None,
}
}
fn args_to_str(w: &W, arglist: &GdNode) -> Option<String> {
Some(
list_elements(arglist)
.iter()
.map(|e| expr_to_str(w, e))
.collect::<Option<Vec<_>>>()?
.join(", "),
)
}