microcad-lang-format 0.5.0

µcad language formatter
Documentation
// Copyright © 2026 The µcad authors <info@microcad.xyz>
// SPDX-License-Identifier: AGPL-3.0-or-later

use crate::{BreakMode, Format, FormatConfig, Node, extras::leading_extras_without_newline, node};

use microcad_lang_parse::ast;

impl Format for ast::BinaryOperator {
    fn format(&self, _: &FormatConfig) -> Node {
        use ast::BinaryOperatorType::*;
        match &self.operation {
            GreaterEqual => ">=",
            LessEqual => "<=",
            And => "and",
            Or => "or",
            op => op.as_str(),
        }
        .into()
    }
}

impl Format for ast::UnaryOperator {
    fn format(&self, _: &FormatConfig) -> Node {
        self.operation.as_str().into()
    }
}

impl Format for ast::Body {
    fn format(&self, f: &FormatConfig) -> Node {
        let body = &self.statements;

        match (body.statements.is_empty(), &body.tail) {
            (true, Some(tail)) => {
                let tail_node = node!(f, body.extras => tail.format(f));
                if tail_node.contains_hardline() {
                    node!(
                        "{" Node::indent(f.indent_width, tail_node)
                        "}"
                    )
                } else {
                    node!("{ " tail_node " }")
                }
            }
            (true, None) => {
                node!("{" Node::indent(f.indent_width, node!(f, body.extras => Node::Nil)) "}")
            }
            _ => {
                let leading = body.extras.leading.format(f);
                let node = Node::indent(f.indent_width, body.format(f));
                node!(
                "{" if leading.starts_with_hardline() { Node::Nil } else { Node::Hardline }
                    node.clone()
                    if node.ends_with_hardline() { Node::Nil } else { Node::Hardline }
                "}")
            }
        }
    }
}

impl Format for ast::Expression {
    fn format(&self, f: &FormatConfig) -> Node {
        match &self {
            ast::Expression::Literal(literal) => literal.format(f),
            ast::Expression::Bracketed(bracket, _) => node!('(' bracket.format(f) ')'),
            ast::Expression::Tuple(tuple_expression) => tuple_expression.format(f),
            ast::Expression::ArrayRange(array_range_expression) => array_range_expression.format(f),
            ast::Expression::ArrayList(array_list_expression) => array_list_expression.format(f),
            ast::Expression::String(format_string) => format_string.format(f),
            ast::Expression::QualifiedName(qualified_name) => qualified_name.format(f),
            ast::Expression::Marker(identifier) => format!("@{}", identifier.name).into(),
            ast::Expression::BinaryOperation(binary_operation) => binary_operation.format(f),
            ast::Expression::UnaryOperation(unary_operation) => unary_operation.format(f),
            ast::Expression::Body(body) => body.format(f),
            ast::Expression::Call(call) => call.format(f),
            ast::Expression::ElementAccess(element_access) => element_access.format(f),
            ast::Expression::If(i) => i.format(f),
            ast::Expression::Error(_) => Node::Nil,
        }
    }
}

impl Format for ast::StringPart {
    fn format(&self, f: &FormatConfig) -> Node {
        match &self {
            ast::StringPart::Char(string_character) => string_character.format(f),
            ast::StringPart::Content(string_literal) =>
            // Simply clone the content of the string literal, because we do not want to have quotes '"'
            {
                string_literal.content.clone().into()
            }
            ast::StringPart::Expression(string_expression) => string_expression.format(f),
        }
    }
}

impl Format for ast::StringCharacter {
    fn format<'a>(&self, _: &FormatConfig) -> Node {
        self.character.into()
    }
}

impl Format for ast::StringExpression {
    fn format<'a>(&self, f: &FormatConfig) -> Node {
        node!(f, self.extras =>
            '{' self.specification self.expression '}'
        )
    }
}

impl Format for ast::StringFormatSpecification {
    fn format<'a>(&self, _f: &FormatConfig) -> Node {
        match (&self.precision, &self.width) {
            (Some(Ok(width)), Some(Ok(precision))) => format!("0{width}.{precision}").into(),
            (None, Some(Ok(precision))) => format!(".{precision}").into(),
            (Some(Ok(width)), None) => format!("0{width}").into(),
            _ => Node::Nil,
        }
    }
}

impl Format for ast::FormatString {
    fn format(&self, f: &FormatConfig) -> Node {
        node!(f, self.extras =>
            '"'
            self.parts
                .iter()
                .map(|part| part.format(f))
                .collect::<Vec<_>>()
            '"'
        )
    }
}

impl Format for ast::TupleItem {
    fn format(&self, f: &FormatConfig) -> Node {
        node!(f, leading_extras_without_newline(&self.extras) =>
            match &self.name {
                Some(name) => node!(f => name " = " self.value),
                None => node!(f => self.value)
            }
        )
    }
}

impl Format for ast::TupleExpression {
    fn format(&self, f: &FormatConfig) -> Node {
        let nodes: Vec<Node> = self.values.iter().map(|item| item.format(f)).collect();
        let break_mode = BreakMode::from_layout(&nodes, 4, f);
        node!(f, leading_extras_without_newline(&self.extras) =>
            '(' Node::list(nodes, ',', break_mode) ')'
        )
    }
}

impl Format for ast::ArrayItem {
    fn format(&self, f: &FormatConfig) -> Node {
        node!(f, self.extras => self.expression)
    }
}

impl Format for ast::ArrayRangeExpression {
    fn format(&self, f: &FormatConfig) -> Node {
        node!(f, self.extras =>
            '[' self.start ".." self.end ']' self.unit
        )
    }
}

impl Format for ast::ArrayListExpression {
    fn format(&self, f: &FormatConfig) -> Node {
        let nodes: Vec<Node> = self.items.iter().map(|item| item.format(f)).collect();
        let break_mode = BreakMode::from_layout(&nodes, 0, f);
        node!(f, self.extras =>
            '[' Node::list(nodes, ',', break_mode) ']' self.unit
        )
    }
}

impl Format for ast::QualifiedName {
    fn format(&self, f: &FormatConfig) -> Node {
        node!(f, self.extras =>
            Node::hlist(self.parts.iter().map(|identifier| identifier.format(f)), "::")
        )
    }
}

impl Format for ast::BinaryOperation {
    fn format(&self, f: &FormatConfig) -> Node {
        node!(f => self.lhs Node::Softline self.operation Node::Softline self.rhs)
    }
}

impl Format for ast::UnaryOperation {
    fn format(&self, f: &FormatConfig) -> Node {
        node!(f => self.operation self.rhs)
    }
}

impl Format for ast::Argument {
    fn format(&self, f: &FormatConfig) -> Node {
        match self {
            ast::Argument::Unnamed(arg) => arg.format(f),
            ast::Argument::Named(arg) => arg.format(f),
        }
    }
}

impl Format for ast::UnnamedArgument {
    fn format(&self, f: &FormatConfig) -> Node {
        node!(f, self.extras => self.value)
    }
}

impl Format for ast::NamedArgument {
    fn format(&self, f: &FormatConfig) -> Node {
        node!(f, self.extras => self.name " = " self.value)
    }
}

impl Format for ast::ArgumentList {
    fn format(&self, f: &FormatConfig) -> Node {
        let nodes: Vec<Node> = self.arguments.iter().map(|item| item.format(f)).collect();
        let break_mode = BreakMode::from_layout(&nodes, 4, f);

        node!('(' node!(f, leading_extras_without_newline(&self.extras) => Node::list(nodes, ',', break_mode)) ')')
    }
}

impl Format for ast::Call {
    fn format(&self, f: &FormatConfig) -> Node {
        node!(f, self.extras => self.name self.arguments)
    }
}

impl Format for ast::ElementInner {
    fn format(&self, f: &FormatConfig) -> Node {
        use ast::ElementInner::*;
        match &self {
            Attribute(identifier) => node!(f => '#' identifier),
            Tuple(identifier) => node!(f => '.' identifier),
            Method(call) => node!(f => '.' call),
            ArrayElement(expression) => node!(f => '[' expression ']'),
        }
    }
}

impl Format for ast::Element {
    fn format(&self, f: &FormatConfig) -> Node {
        node!(f, leading_extras_without_newline(&self.extras) => self.inner)
    }
}

impl Format for ast::ElementAccess {
    fn format(&self, f: &FormatConfig) -> Node {
        // If this is true, we place an indent on the next line
        let mut indent = match &self.value.as_ref() {
            ast::Expression::Literal(_) => false,
            ast::Expression::Bracketed(_, _) => true,
            ast::Expression::Tuple(_) => true,
            ast::Expression::ArrayRange(_) => false,
            ast::Expression::ArrayList(_) => true,
            ast::Expression::String(_) => true,
            ast::Expression::QualifiedName(_) => true,
            ast::Expression::Marker(_) => true,
            ast::Expression::BinaryOperation(_) => true,
            ast::Expression::UnaryOperation(_) => true,
            ast::Expression::Body(_) => false,
            ast::Expression::Call(_) => true,
            ast::Expression::ElementAccess(_) => false,
            ast::Expression::If(_) => false,
            ast::Expression::Error(_) => false,
        };

        let nodes: Vec<Node> = self
            .element_chain
            .iter()
            .map(|element| node!(f => element))
            .collect();

        // Indent if the first node already starts with a hardline
        indent |= nodes
            .iter()
            .take(1)
            .any(|node| !node.starts_with_hardline());

        let element_chain_node = match BreakMode::from_layout(&nodes, 3, f) {
            BreakMode::NoBreak if !indent => Node::hlist(nodes, Node::Nil),
            BreakMode::NoBreak => Node::indent(f.indent_width, nodes),
            BreakMode::WithIndent(indent_width) => {
                Node::indent(if indent { indent_width } else { 0 }, nodes)
            }
        };

        node!(f => self.value element_chain_node)
    }
}

impl Format for ast::If {
    fn format(&self, f: &FormatConfig) -> Node {
        node!(f, self.extras =>
            "if " self.condition ' ' self.body
            match &self.else_body {
                Some(else_body) => node!(f => Node::Softline "else " else_body),
                None => match &self.next_if {
                    Some(_) => Node::Nil,
                    None => Node::Hardline
                }
            }
            match &self.next_if {
                Some(next_if) => node!(f => Node::Softline "else " next_if),
                None => Node::Nil
            }
        )
    }
}