nixfmt_rs 0.2.0

Rust implementation of nixfmt with exact Haskell compatibility
Documentation
use crate::predoc::{
    Doc, DocE, GroupAnn, Pretty, hardline, hardspace, line, line_prime, push_group, push_group_ann,
    push_nested, unexpand_spacing_prime,
};
use crate::types::{Expression, FirstToken, Item, Parameter, Term, Token, Trivia};

use super::absorb::{is_absorbable_term, push_absorb_paren};
use super::term::{push_pretty_term, push_pretty_term_wide, push_render_list};
use super::util::{has_trivia, is_simple_expression, is_simple_term};

const fn is_list_arg(e: &Expression) -> bool {
    matches!(e, Expression::Term(Term::List(_, _, _)))
}

const fn is_selection_arg(e: &Expression) -> bool {
    matches!(e, Expression::Term(Term::Selection(_, _, _)))
}

/// `absorbInner` from Pretty.hs: short lists of simple terms get a soft `line`
/// separator so they may stay on one line; everything else falls back to `pretty`.
fn push_absorb_inner(doc: &mut Doc, arg: &Expression) {
    if let Expression::Term(Term::List(open, items, close)) = arg {
        let all_simple = items.0.iter().all(|item| match item {
            Item::Item(t) => is_simple_term(t),
            Item::Comments(_) => true,
        });
        if items.0.len() <= 6 && all_simple {
            push_render_list(doc, &line(), open, items, close);
            return;
        }
    }
    arg.pretty(doc);
}

/// Return the pre-trivia of the first token of `expr` after applying
/// `move_trailing_comment_up` to it, without cloning the expression.
/// This is the projection half of Haskell's
/// `mapFirstToken' ((\a -> (a{preTrivia=[]}, preTrivia)) . moveTrailingCommentUp)`.
fn first_token_comment(expr: &Expression) -> Trivia {
    let slot = expr.first_token();
    let mut t = slot.pre_trivia.clone();
    if let Some(tc) = slot.trail_comment {
        t.push(tc.into());
    }
    t
}

/// Rebuild `expr` with the first token's `pre_trivia` and `trail_comment`
/// cleared. Only invoked on the leftmost (non-`Application`) head of a call
/// chain, which is almost always a small `Term`, so the deep clone is cheap.
fn strip_first_comment(expr: &Expression) -> Expression {
    let mut e = expr.clone();
    let slot = e.first_token_mut();
    *slot.pre_trivia = Trivia::new();
    *slot.trail_comment = None;
    e
}

/// Walk the function-call chain. Mirrors Haskell `absorbApp` (Pretty.hs).
fn push_absorb_app(doc: &mut Doc, expr: &Expression, indent_function: bool, comment: &Trivia) {
    let recurse_head = |doc: &mut Doc, head: &Expression| {
        push_group_ann(doc, GroupAnn::Transparent, |g| {
            push_absorb_app(g, head, indent_function, comment);
        });
    };

    let Expression::Application(f, a) = expr else {
        // Base case: the function expression itself. The first token's
        // pre-trivia/trailing comment was already emitted by `push_pretty_app`,
        // so render the head with that trivia stripped.
        if !comment.is_empty() {
            strip_first_comment(expr).pretty(doc);
        } else if indent_function {
            push_nested(doc, |n| {
                push_group(n, |g| {
                    g.push(line_prime());
                    expr.pretty(g);
                });
            });
        } else {
            expr.pretty(doc);
        }
        return;
    };

    // Two consecutive list arguments stay together: if one wraps, both wrap.
    if is_list_arg(a)
        && let Expression::Application(f2, l1) = &**f
        && is_list_arg(l1)
    {
        push_group_ann(doc, GroupAnn::Transparent, |outer| {
            recurse_head(outer, f2);
            push_nested(outer, |n| {
                push_group(n, |g| {
                    g.push(line());
                    push_group(g, |inner| push_absorb_inner(inner, l1));
                    g.push(line());
                    push_group(g, |inner| push_absorb_inner(inner, a));
                });
            });
        });
        return;
    }

    recurse_head(doc, f);
    doc.push(line());
    // Selections must not priority-expand: only the `.`-suffix would move,
    // which looks odd.
    let arg_ann = if is_selection_arg(a) {
        GroupAnn::RegularG
    } else {
        GroupAnn::Priority
    };
    push_nested(doc, |n| {
        push_group_ann(n, arg_ann, |g| push_absorb_inner(g, a));
    });
}

/// `group' Priority $ nest …`
fn push_priority_nest(doc: &mut Doc, f: impl FnOnce(&mut Doc)) {
    push_group_ann(doc, GroupAnn::Priority, |g| push_nested(g, f));
}

/// Render the last argument of a function call. Mirrors Haskell `absorbLast`.
fn push_absorb_last(doc: &mut Doc, arg: &Expression) {
    if let Expression::Term(t) = arg
        && is_absorbable_term(t)
    {
        // Haskell: `group' Priority $ nest $ prettyTerm t`. `prettyTerm`
        // (unlike `instance Pretty Term`) does *not* wrap a `List` in an
        // extra group.
        return push_priority_nest(doc, |n| push_pretty_term(n, t));
    }

    if let Expression::Term(Term::Parenthesized(open, inner, close)) = arg {
        // Parenthesised single-ID-parameter abstraction with absorbable body.
        if let Expression::Abstraction(Parameter::ID(name), colon, body) = &**inner
            && let Expression::Term(body_term) = &**body
            && is_absorbable_term(body_term)
            && !has_trivia(open)
            && !has_trivia(name)
            && !has_trivia(colon)
        {
            return push_priority_nest(doc, |n| {
                open.pretty(n);
                name.pretty(n);
                colon.pretty(n);
                n.push(hardspace());
                push_pretty_term_wide(n, body_term);
                close.pretty(n);
            });
        }
        // Parenthesised `ident { ... }` application with absorbable body.
        if let Expression::Application(f, a) = &**inner
            && let Expression::Term(Term::Token(ident)) = &**f
            && matches!(ident.value, Token::Identifier(_))
            && let Expression::Term(body_term) = &**a
            && is_absorbable_term(body_term)
            && !has_trivia(open)
            && !has_trivia(ident)
            && !has_trivia(close)
        {
            return push_priority_nest(doc, |n| {
                open.pretty(n);
                ident.pretty(n);
                n.push(hardspace());
                push_pretty_term_wide(n, body_term);
                close.pretty(n);
            });
        }
        return push_absorb_paren(doc, open, inner, close);
    }

    push_group(doc, |g| push_nested(g, |n| arg.pretty(n)));
}

/// Render function applications (Haskell `prettyApp indentFunction pre hasPost f a`).
pub(super) fn push_pretty_app(
    doc: &mut Doc,
    indent_function: bool,
    pre: &[DocE],
    has_post: bool,
    expr: &Expression,
) {
    let Expression::Application(f, a) = expr else {
        unreachable!("push_pretty_app requires an Application");
    };

    let comment = first_token_comment(f);

    let post_hardline = |doc: &mut Doc| {
        if has_post && !comment.is_empty() {
            doc.push(hardline());
        }
    };

    comment.pretty(doc);

    // Two trailing list arguments are rendered as a pair of regular groups so
    // they wrap together; lists are never "simple", so renderSimple cannot apply.
    if is_list_arg(a)
        && let Expression::Application(f2, l1) = &**f
        && is_list_arg(l1)
    {
        push_group(doc, |g| {
            g.extend_from_slice(pre);
            push_group_ann(g, GroupAnn::Transparent, |inner| {
                push_absorb_app(inner, f2, indent_function, &comment);
            });
            g.push(line());
            push_nested(g, |n| push_group(n, |gr| push_absorb_inner(gr, l1)));
            g.push(line());
            push_nested(g, |n| push_group(n, |gr| push_absorb_inner(gr, a)));
            if has_post {
                g.push(line_prime());
            }
        });
        post_hardline(doc);
        return;
    }

    let mut rendered_f: Doc = pre.to_vec();
    push_group_ann(&mut rendered_f, GroupAnn::Transparent, |g| {
        push_absorb_app(g, f, indent_function, &comment);
    });

    // renderSimple
    if is_simple_expression(expr)
        && let Some(unexpanded) = unexpand_spacing_prime(None, &rendered_f)
    {
        push_group(doc, |g| {
            g.extend(unexpanded);
            g.push(hardspace());
            push_absorb_last(g, a);
        });
        post_hardline(doc);
        return;
    }

    push_group(doc, |g| {
        g.extend(rendered_f);
        g.push(line());
        push_absorb_last(g, a);
        if has_post {
            g.push(line_prime());
        }
    });
    post_hardline(doc);
}