use crate::ast::*;
use crate::span::Span;
const INLINE_MAX: usize = 80;
pub fn format(file: &File, src: &str) -> String {
let f = Formatter { src };
let mut out = String::new();
let version = file
.header
.as_ref()
.map(|h| h.version.as_str())
.unwrap_or("0.1");
out.push_str(&format!("drawl {version}\n"));
let mut first = true;
for stmt in &file.stmts {
let want_blank = if first {
true
} else {
stmt.trivia.blank_before || !stmt.trivia.leading.is_empty() || is_block_stmt(stmt)
};
if want_blank {
out.push('\n');
}
first = false;
f.write_stmt(&mut out, stmt, 0);
}
while out.ends_with("\n\n") {
out.pop();
}
if !out.ends_with('\n') {
out.push('\n');
}
out
}
fn is_block_stmt(stmt: &Stmt) -> bool {
matches!(
stmt.kind,
StmtKind::Canvas(_)
| StmtKind::Def(_)
| StmtKind::Group(_)
| StmtKind::Class(_)
| StmtKind::Constrain(_)
| StmtKind::For(_)
)
}
struct Formatter<'a> {
src: &'a str,
}
impl<'a> Formatter<'a> {
fn snip(&self, span: Span) -> &str {
let s = span.start.min(self.src.len());
let e = span.end.min(self.src.len());
self.src[s..e].trim()
}
fn write_stmt(&self, out: &mut String, stmt: &Stmt, depth: usize) {
let indent = " ".repeat(depth);
for c in &stmt.trivia.leading {
out.push_str(&indent);
if c.is_empty() {
out.push_str("//\n");
} else {
out.push_str(&format!("// {c}\n"));
}
}
if let StmtKind::Prop(p) = &stmt.kind {
if p.key.is_empty() {
return;
}
}
let trailing = stmt
.trivia
.trailing
.as_ref()
.map(|c| format!(" // {c}"))
.unwrap_or_default();
match &stmt.kind {
StmtKind::Canvas(block) => {
out.push_str(&format!("{indent}canvas {{\n"));
self.write_block_stmts(out, block, depth + 1);
out.push_str(&format!("{indent}}}{trailing}\n"));
}
StmtKind::Def(d) => {
let params: Vec<&str> = d.params.iter().map(|p| p.name.as_str()).collect();
out.push_str(&format!(
"{indent}def {}({}) {{\n",
d.name.name,
params.join(", ")
));
self.write_block_stmts(out, &d.body, depth + 1);
out.push_str(&format!("{indent}}}{trailing}\n"));
}
StmtKind::Group(g) => {
let label = g
.label
.as_ref()
.map(|l| format!(" {}", self.snip(l.span)))
.unwrap_or_default();
out.push_str(&format!("{indent}group {}{label} {{\n", g.name.name));
self.write_block_stmts(out, &g.body, depth + 1);
out.push_str(&format!("{indent}}}{trailing}\n"));
}
StmtKind::Class(c) => {
if let Some(line) = self.inline_block(&c.body) {
out.push_str(&format!("{indent}class {} {line}{trailing}\n", c.name.name));
} else {
out.push_str(&format!("{indent}class {} {{\n", c.name.name));
self.write_block_stmts(out, &c.body, depth + 1);
out.push_str(&format!("{indent}}}{trailing}\n"));
}
}
StmtKind::Constrain(cs) => {
out.push_str(&format!("{indent}constrain {{\n"));
let inner = " ".repeat(depth + 1);
for c in cs {
for lc in &c.trivia.leading {
out.push_str(&format!("{inner}// {lc}\n"));
}
let t = c
.trivia
.trailing
.as_ref()
.map(|x| format!(" // {x}"))
.unwrap_or_default();
out.push_str(&format!("{inner}{}{t}\n", self.snip(c.span)));
}
out.push_str(&format!("{indent}}}{trailing}\n"));
}
StmtKind::Pin(p) => {
out.push_str(&format!(
"{indent}pin {} at ({}, {}){trailing}\n",
self.snip(p.target.span),
self.snip(p.x.span),
self.snip(p.y.span)
));
}
StmtKind::For(f) => {
if let Some(line) = self.inline_for(f) {
if indent.len() + line.len() <= INLINE_MAX {
out.push_str(&format!("{indent}{line}{trailing}\n"));
return;
}
}
out.push_str(&format!(
"{indent}for {} in {}..{} {{\n",
f.var.name,
self.snip(f.start.span),
self.snip(f.end.span)
));
self.write_block_stmts(out, &f.body, depth + 1);
out.push_str(&format!("{indent}}}{trailing}\n"));
}
StmtKind::Port(p) => {
if let Some(line) = self.inline_block(&p.body) {
out.push_str(&format!("{indent}port {} {line}{trailing}\n", p.name.name));
} else {
out.push_str(&format!("{indent}port {} {{\n", p.name.name));
self.write_block_stmts(out, &p.body, depth + 1);
out.push_str(&format!("{indent}}}{trailing}\n"));
}
}
StmtKind::Prop(p) => {
out.push_str(&format!("{indent}{}{trailing}\n", self.prop_line(p)));
}
StmtKind::Node(n) => {
if let Some(line) = self.inline_node(n) {
if indent.len() + line.len() <= INLINE_MAX {
out.push_str(&format!("{indent}{line}{trailing}\n"));
return;
}
}
self.write_node_multiline(out, n, depth, &trailing);
}
StmtKind::Edge(e) => {
let op = match e.op {
EdgeOp::Forward => "->",
EdgeOp::Bidirectional => "<->",
};
let label = e
.label
.as_ref()
.map(|l| format!(" : {}", self.snip(l.span)))
.unwrap_or_default();
let props = match &e.props {
Some(b) => match self.inline_block(b) {
Some(line) => format!(" {line}"),
None => format!(" {}", self.snip(b.span)),
},
None => String::new(),
};
out.push_str(&format!(
"{indent}{} {op} {}{label}{props}{trailing}\n",
self.snip(e.from.span),
self.snip(e.to.span)
));
}
}
}
fn write_block_stmts(&self, out: &mut String, block: &Block, depth: usize) {
let mut first = true;
for stmt in &block.stmts {
if !first && (stmt.trivia.blank_before || !stmt.trivia.leading.is_empty()) {
out.push('\n');
}
first = false;
self.write_stmt(out, stmt, depth);
}
}
fn write_node_multiline(&self, out: &mut String, n: &Node, depth: usize, trailing: &str) {
let indent = " ".repeat(depth);
match &n.kind {
NodeKind::Plain { body } => {
let name = n.name.as_ref().expect("plain nodes are named");
if body.stmts.is_empty() {
out.push_str(&format!("{indent}{}{trailing}\n", name.name));
return;
}
out.push_str(&format!("{indent}{} {{\n", name.name));
self.write_block_stmts(out, body, depth + 1);
out.push_str(&format!("{indent}}}{trailing}\n"));
}
NodeKind::Container { ctype, body, .. } => {
let name = n.name.as_ref().expect("containers are named");
let kw = match ctype {
ContainerType::Row => "row".to_string(),
ContainerType::Column => "column".to_string(),
ContainerType::Grid { cols, rows } => format!("grid {cols}x{rows}"),
};
out.push_str(&format!("{indent}{}: {kw} {{\n", name.name));
self.write_block_stmts(out, body, depth + 1);
out.push_str(&format!("{indent}}}{trailing}\n"));
}
NodeKind::Call { callee, args, body } => {
let args: Vec<&str> = args.iter().map(|a| self.snip(a.span)).collect();
let head = match &n.name {
Some(name) => format!("{}: {}({})", name.name, callee.name, args.join(", ")),
None => format!("{}({})", callee.name, args.join(", ")),
};
match body {
Some(b) if !b.stmts.is_empty() => {
out.push_str(&format!("{indent}{head} {{\n"));
self.write_block_stmts(out, b, depth + 1);
out.push_str(&format!("{indent}}}{trailing}\n"));
}
_ => out.push_str(&format!("{indent}{head}{trailing}\n")),
}
}
}
}
fn prop_line(&self, p: &Prop) -> String {
let key: Vec<&str> = p.key.iter().map(|k| k.name.as_str()).collect();
format!("{}: {}", key.join("."), self.snip(p.value.span()))
}
fn inline_block(&self, block: &Block) -> Option<String> {
if block.stmts.is_empty() {
return None;
}
if block.stmts.len() > 2 {
return None;
}
let mut parts = Vec::new();
for s in &block.stmts {
if !s.trivia.leading.is_empty() || s.trivia.trailing.is_some() {
return None;
}
match &s.kind {
StmtKind::Prop(p) if !p.key.is_empty() => parts.push(self.prop_line(p)),
_ => return None,
}
}
let line = format!("{{ {} }}", parts.join("; "));
(line.len() <= INLINE_MAX - 10).then_some(line)
}
fn inline_node(&self, n: &Node) -> Option<String> {
match &n.kind {
NodeKind::Plain { body } => {
let name = n.name.as_ref()?;
if body.stmts.is_empty() {
return Some(name.name.clone());
}
if body.stmts.len() > 2 {
return None;
}
let mut parts = Vec::new();
for s in &body.stmts {
if !s.trivia.leading.is_empty() || s.trivia.trailing.is_some() {
return None;
}
match &s.kind {
StmtKind::Prop(p) if !p.key.is_empty() => parts.push(self.prop_line(p)),
StmtKind::Port(p) => {
let inner = self.inline_block(&p.body)?;
parts.push(format!("port {} {inner}", p.name.name));
}
_ => return None,
}
}
Some(format!("{} {{ {} }}", name.name, parts.join("; ")))
}
NodeKind::Container { ctype, body, .. } => {
let name = n.name.as_ref()?;
if body.stmts.len() != 1 {
return None;
}
let inner_stmt = &body.stmts[0];
if !inner_stmt.trivia.leading.is_empty() || inner_stmt.trivia.trailing.is_some() {
return None;
}
let inner = match &inner_stmt.kind {
StmtKind::For(f) => self.inline_for(f)?,
StmtKind::Node(inner_n) => self.inline_node(inner_n)?,
_ => return None,
};
let kw = match ctype {
ContainerType::Row => "row".to_string(),
ContainerType::Column => "column".to_string(),
ContainerType::Grid { cols, rows } => format!("grid {cols}x{rows}"),
};
Some(format!("{}: {kw} {{ {inner} }}", name.name))
}
NodeKind::Call { callee, args, body } => {
let args: Vec<&str> = args.iter().map(|a| self.snip(a.span)).collect();
let head = match &n.name {
Some(name) => format!("{}: {}({})", name.name, callee.name, args.join(", ")),
None => format!("{}({})", callee.name, args.join(", ")),
};
match body {
None => Some(head),
Some(b) if b.stmts.is_empty() => Some(head),
Some(b) => {
let inner = self.inline_block(b)?;
Some(format!("{head} {inner}"))
}
}
}
}
}
fn inline_for(&self, f: &For) -> Option<String> {
if f.body.stmts.len() != 1 {
return None;
}
let s = &f.body.stmts[0];
if !s.trivia.leading.is_empty() || s.trivia.trailing.is_some() {
return None;
}
let inner = match &s.kind {
StmtKind::Node(n) => self.inline_node(n)?,
StmtKind::Edge(e) => {
let op = match e.op {
EdgeOp::Forward => "->",
EdgeOp::Bidirectional => "<->",
};
let label = e
.label
.as_ref()
.map(|l| format!(" : {}", self.snip(l.span)))
.unwrap_or_default();
let props = match &e.props {
Some(b) => format!(" {}", self.inline_block(b)?),
None => String::new(),
};
format!(
"{} {op} {}{label}{props}",
self.snip(e.from.span),
self.snip(e.to.span)
)
}
_ => return None,
};
Some(format!(
"for {} in {}..{} {{ {inner} }}",
f.var.name,
self.snip(f.start.span),
self.snip(f.end.span)
))
}
}