// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0

// cSpell:ignore Snaf Tatta

use super::writer::TokenWriter;
use i_slint_compiler::parser::{NodeOrToken, SyntaxKind, SyntaxNode, syntax_nodes};

pub fn format_document(
    doc: syntax_nodes::Document,
    writer: &mut impl TokenWriter,
) -> Result<(), std::io::Error> {
    let mut state = FormatState::default();
    format_node(&doc, writer, &mut state)
}

#[derive(Default)]
struct FormatState {
    /// The whitespace have been written, all further whitespace can be skipped
    skip_all_whitespace: bool,
    /// The whitespace to add before the next token
    whitespace_to_add: Option<String>,

    /// If the last token was SyntaxKind::WhiteSpace and it was not emit because of skip_all_whitespace,
    /// this contains the whitespace that was removed, so that it can be added again in case the next
    /// token is a comment
    last_removed_whitespace: Option<String>,

    /// The level of indentation
    indentation_level: u32,

    /// A counter that is incremented when something is inserted
    insertion_count: usize,

    /// a comment has been written followed maybe by some spacing
    after_comment: bool,

    /// When `true`, the formatter is inside a code block being kept on a single
    /// line. `new_line()` then becomes a space, so statements that would
    /// normally insert a trailing newline (e.g. `return` / `let`) stay inline.
    inline_codeblock: bool,
}

impl FormatState {
    fn new_line(&mut self) {
        if self.inline_codeblock {
            self.insert_whitespace(" ");
            return;
        }
        if self.after_comment {
            return;
        }
        self.skip_all_whitespace = true;
        if let Some(x) = &mut self.whitespace_to_add {
            x.insert(0, '\n');
            return;
        }
        let mut new_line = String::from("\n");
        for _ in 0..self.indentation_level {
            new_line += "    ";
        }
        self.whitespace_to_add = Some(new_line);
    }

    fn insert_whitespace(&mut self, arg: &str) {
        if self.after_comment {
            return;
        }
        self.skip_all_whitespace = true;
        if !arg.is_empty() {
            if let Some(ws) = &mut self.whitespace_to_add {
                *ws += arg;
            } else {
                self.whitespace_to_add = Some(arg.into());
            }
        }
    }
}

fn format_node(
    node: &SyntaxNode,
    writer: &mut impl TokenWriter,
    state: &mut FormatState,
) -> Result<(), std::io::Error> {
    match node.kind() {
        SyntaxKind::Component => {
            return format_component(node, writer, state);
        }
        SyntaxKind::Document => {
            return format_document_node(node, writer, state);
        }
        SyntaxKind::Element => {
            return format_element(node, writer, state);
        }
        SyntaxKind::SubElement => {
            return format_sub_element(node, writer, state);
        }
        SyntaxKind::PropertyDeclaration => {
            return format_property_declaration(node, writer, state);
        }
        SyntaxKind::PropertyDeprecation => {
            return format_property_deprecation(node, writer, state);
        }
        SyntaxKind::Binding => {
            return format_binding(node, writer, state);
        }
        SyntaxKind::TwoWayBinding => {
            return format_two_way_binding(node, writer, state);
        }
        SyntaxKind::ImplementStatement => {
            return format_implement_statement(node, writer, state);
        }
        SyntaxKind::CallbackConnection => {
            return format_callback_connection(node, writer, state);
        }
        SyntaxKind::CallbackDeclaration => {
            return format_callback_declaration(node, writer, state);
        }
        SyntaxKind::Function => {
            return format_function(node, writer, state);
        }
        SyntaxKind::ArgumentDeclaration => {
            return format_argument_declaration(node, writer, state);
        }
        SyntaxKind::QualifiedName => {
            return format_qualified_name(node, writer, state);
        }
        SyntaxKind::SelfAssignment | SyntaxKind::BinaryExpression => {
            return format_binary_expression(node, writer, state);
        }
        SyntaxKind::ConditionalExpression => {
            return format_conditional_expression(node, writer, state);
        }
        SyntaxKind::Expression => {
            return format_expression(node, writer, state);
        }
        SyntaxKind::CodeBlock => {
            return format_codeblock(node, writer, state);
        }
        SyntaxKind::ReturnStatement => {
            return format_return_statement(node, writer, state);
        }
        SyntaxKind::LetStatement => {
            return format_let_statement(node, writer, state);
        }
        SyntaxKind::AtGradient => {
            return format_at_gradient(node, writer, state);
        }
        SyntaxKind::ChildrenPlaceholder => {
            return format_children_placeholder(node, writer, state);
        }
        SyntaxKind::SlotDeclaration => {
            return format_slot_declaration(node, writer, state);
        }
        SyntaxKind::RepeatedElement => {
            return format_repeated_element(node, writer, state);
        }
        SyntaxKind::RepeatedIndex => {
            return format_repeated_index(node, writer, state);
        }
        SyntaxKind::MatchElement => {
            return format_match_element(node, writer, state);
        }
        SyntaxKind::MatchCase | SyntaxKind::WildcardMatchCase => {
            return format_match_case(node, writer, state);
        }
        SyntaxKind::Array => {
            return format_array(node, writer, state);
        }
        SyntaxKind::State => {
            return format_state(node, writer, state);
        }
        SyntaxKind::States => {
            return format_states(node, writer, state);
        }
        SyntaxKind::StatePropertyChange => {
            return format_state_prop_change(node, writer, state);
        }
        SyntaxKind::Transition => {
            return format_transition(node, writer, state);
        }
        SyntaxKind::PropertyAnimation => {
            return format_property_animation(node, writer, state);
        }
        SyntaxKind::ObjectLiteral => {
            return format_object_literal(node, writer, state);
        }
        SyntaxKind::StructDeclaration => {
            return format_struct_declaration(node, writer, state);
        }
        SyntaxKind::EnumDeclaration => {
            return format_enum_declaration(node, writer, state);
        }
        SyntaxKind::ExportsList => {
            return format_exports_list(node, writer, state);
        }
        SyntaxKind::ExportSpecifier => {
            return format_export_specifier(node, writer, state);
        }
        SyntaxKind::ObjectType => {
            return format_object_type(node, writer, state);
        }
        SyntaxKind::ObjectTypeMember => {
            return format_object_type_member(node, writer, state);
        }
        SyntaxKind::PropertyChangedCallback => {
            return format_property_changed_callback(node, writer, state);
        }
        SyntaxKind::MemberAccess => {
            return format_member_access(node, writer, state);
        }
        SyntaxKind::ImportSpecifier => {
            return format_import_specifier(node, writer, state);
        }
        SyntaxKind::Closure => {
            return format_closure(node, writer, state);
        }
        _ => (),
    }

    for n in node.children_with_tokens() {
        fold(n, writer, state)?;
    }
    Ok(())
}

fn format_document_node(
    node: &SyntaxNode,
    writer: &mut impl TokenWriter,
    state: &mut FormatState,
) -> Result<(), std::io::Error> {
    for n in node.children_with_tokens() {
        if let Some(t) = n.as_token()
            && t.kind() == SyntaxKind::Whitespace
            && whitespace_has_empty_line(t.text())
            && next_non_trivia_token_kind(&n).is_some_and(|kind| kind != SyntaxKind::Eof)
        {
            // Preserve document-level blank lines as written and override any
            // pending newline inserted by the preceding formatter branch.
            state.skip_all_whitespace = false;
            state.whitespace_to_add = None;
            state.last_removed_whitespace = None;
            state.after_comment = false;
            state.insertion_count += 1;
            writer.no_change(t.clone())?;
            continue;
        }

        // Comments are tokens, but top-level items are nodes. Make sure a preceding
        // comment does not suppress indentation inside the next formatted node.
        if n.as_node().is_some() {
            state.after_comment = false;
        }
        fold(n, writer, state)?;
    }
    Ok(())
}

fn fold(
    n: NodeOrToken,
    writer: &mut impl TokenWriter,
    state: &mut FormatState,
) -> std::io::Result<()> {
    match n {
        NodeOrToken::Node(n) => format_node(&n, writer, state),
        NodeOrToken::Token(t) => {
            if t.kind() == SyntaxKind::Eof {
                if state.skip_all_whitespace {
                    writer.with_new_content(t, "\n")?;
                }
                return Ok(());
            } else if t.kind() == SyntaxKind::Whitespace {
                if state.skip_all_whitespace && !state.after_comment {
                    state.last_removed_whitespace = Some(t.text().to_string());
                    writer.with_new_content(t, "")?;
                    return Ok(());
                }
            } else {
                state.after_comment = t.kind() == SyntaxKind::Comment;
                state.skip_all_whitespace = false;
                if let Some(ws) = state.last_removed_whitespace.take()
                    && state.after_comment
                {
                    // restore the previously skipped spaces before comment.
                    state.insertion_count += 1;
                    writer.insert_before(t, &ws)?;
                    state.whitespace_to_add = None;
                    return Ok(());
                }
                if let Some(x) = state.whitespace_to_add.take() {
                    state.insertion_count += 1;
                    writer.insert_before(t, x.as_ref())?;
                    return Ok(());
                }
            }
            state.insertion_count += 1;
            writer.no_change(t)
        }
    }
}

#[derive(Clone, Copy, PartialEq, Eq, Debug)]
enum SyntaxMatch {
    NotFound,
    Found(SyntaxKind),
}

impl SyntaxMatch {
    fn is_found(self) -> bool {
        matches!(self, SyntaxMatch::Found(..))
    }
}

fn whitespace_has_empty_line(text: &str) -> bool {
    let mut newlines = 0;
    for b in text.bytes() {
        if b == b'\n' {
            newlines += 1;
            if newlines >= 2 {
                return true;
            }
        }
    }
    false
}

fn next_non_trivia_token_kind(item: &NodeOrToken) -> Option<SyntaxKind> {
    match item {
        NodeOrToken::Node(node) => Some(node.kind()),
        NodeOrToken::Token(token) => {
            let mut current = token.clone();
            loop {
                match current.kind() {
                    SyntaxKind::Whitespace | SyntaxKind::Comment => {
                        current = current.next_token()?;
                    }
                    kind => return Some(kind),
                }
            }
        }
    }
}

fn prev_non_trivia_token_kind(token: i_slint_compiler::parser::SyntaxToken) -> Option<SyntaxKind> {
    let mut current = token;
    loop {
        match current.kind() {
            SyntaxKind::Whitespace | SyntaxKind::Comment => {
                current = current.prev_token()?;
            }
            kind => return Some(kind),
        }
    }
}

fn whitespace_to(
    sub: &mut impl Iterator<Item = NodeOrToken>,
    element: SyntaxKind,
    writer: &mut impl TokenWriter,
    state: &mut FormatState,
    prefix_whitespace: &str,
) -> Result<bool, std::io::Error> {
    whitespace_to_one_of(sub, &[element], writer, state, prefix_whitespace)
        .map(SyntaxMatch::is_found)
}

fn whitespace_to_one_of(
    sub: &mut impl Iterator<Item = NodeOrToken>,
    elements: &[SyntaxKind],
    writer: &mut impl TokenWriter,
    state: &mut FormatState,
    prefix_whitespace: &str,
) -> Result<SyntaxMatch, std::io::Error> {
    state.insert_whitespace(prefix_whitespace);
    for n in sub {
        match n.kind() {
            SyntaxKind::Whitespace | SyntaxKind::Comment => (),
            expected_kind if elements.contains(&expected_kind) => {
                fold(n, writer, state)?;
                return Ok(SyntaxMatch::Found(expected_kind));
            }
            _ => {
                tracing::warn!("Inconsistency: expected {elements:?},  found {n:?}");
                fold(n, writer, state)?;
                return Ok(SyntaxMatch::NotFound);
            }
        }
        fold(n, writer, state)?;
    }
    tracing::warn!("Inconsistency: expected {elements:?},  not found");
    Ok(SyntaxMatch::NotFound)
}

fn finish_node(
    sub: impl Iterator<Item = NodeOrToken>,
    writer: &mut impl TokenWriter,
    state: &mut FormatState,
) -> Result<bool, std::io::Error> {
    // FIXME:  We should check that there are only comments or whitespace in sub
    for n in sub {
        fold(n, writer, state)?;
    }
    Ok(true)
}

fn format_component(
    node: &SyntaxNode,
    writer: &mut impl TokenWriter,
    state: &mut FormatState,
) -> Result<(), std::io::Error> {
    if node.child_token(SyntaxKind::ColonEqual).is_some() {
        // Legacy syntax
        let mut sub = node.children_with_tokens();
        let _ok = whitespace_to(&mut sub, SyntaxKind::DeclaredIdentifier, writer, state, "")?
            && whitespace_to(&mut sub, SyntaxKind::ColonEqual, writer, state, " ")?
            && whitespace_to(&mut sub, SyntaxKind::Element, writer, state, " ")?;

        finish_node(sub, writer, state)?;
        state.new_line();
    } else {
        let mut sub = node.children_with_tokens();
        let _ok = whitespace_to(&mut sub, SyntaxKind::Identifier, writer, state, "")?
            && whitespace_to(&mut sub, SyntaxKind::DeclaredIdentifier, writer, state, " ")?;
        let r = whitespace_to_one_of(
            &mut sub,
            &[SyntaxKind::Identifier, SyntaxKind::Element],
            writer,
            state,
            " ",
        )?;
        if r == SyntaxMatch::Found(SyntaxKind::Identifier) {
            whitespace_to(&mut sub, SyntaxKind::Element, writer, state, " ")?;
        }

        finish_node(sub, writer, state)?;
        state.new_line();
    }

    Ok(())
}

fn format_element(
    node: &SyntaxNode,
    writer: &mut impl TokenWriter,
    state: &mut FormatState,
) -> Result<(), std::io::Error> {
    let mut sub = node.children_with_tokens();

    let ok = if node.child_node(SyntaxKind::QualifiedName).is_some() {
        whitespace_to(&mut sub, SyntaxKind::QualifiedName, writer, state, "")?
            && whitespace_to(&mut sub, SyntaxKind::LBrace, writer, state, " ")?
    } else {
        whitespace_to(&mut sub, SyntaxKind::LBrace, writer, state, "")?
    };

    if !ok {
        finish_node(sub, writer, state)?;
        return Ok(());
    }

    state.indentation_level += 1;
    state.new_line();
    let ins_ctn = state.insertion_count;
    let mut inserted_newline = false;

    let mut sub = sub.peekable();
    while let Some(n) = sub.next() {
        if n.kind() != SyntaxKind::Comment
            && let Some(last_removed_whitespace) = state.last_removed_whitespace.take()
        {
            let is_empty_line = whitespace_has_empty_line(&last_removed_whitespace);
            if is_empty_line && !inserted_newline {
                state.new_line();
            }
        }
        if n.kind() == SyntaxKind::Whitespace {
            if state.after_comment {
                // After a comment: check if the next non-trivia child is
                // the closing brace. If so, skip this whitespace so the
                // brace can get a properly indented newline.
                let next_is_rbrace = sub
                    .peek()
                    .and_then(next_non_trivia_token_kind)
                    .is_some_and(|k| k == SyntaxKind::RBrace);
                if next_is_rbrace {
                    state.after_comment = false;
                    state.skip_all_whitespace = true;
                    fold(n, writer, state)?;
                    continue;
                }
            } else {
                let is_empty_line =
                    n.as_token().map(|n| whitespace_has_empty_line(n.text())).unwrap_or(false);
                if is_empty_line {
                    if !inserted_newline {
                        state.new_line();
                    }
                    continue;
                }
            }
        }
        inserted_newline = false;

        if n.kind() == SyntaxKind::RBrace {
            state.indentation_level -= 1;
            state.whitespace_to_add = None;
            if ins_ctn == state.insertion_count {
                state.insert_whitespace(" ");
            } else {
                state.new_line();
            }
            fold(n, writer, state)?;
            state.new_line();
        } else {
            let put_newline_after = n.kind() == SyntaxKind::SubElement;

            fold(n, writer, state)?;

            if put_newline_after {
                state.new_line();
                inserted_newline = true;
            }
        }
    }
    Ok(())
}

fn format_sub_element(
    node: &SyntaxNode,
    writer: &mut impl TokenWriter,
    state: &mut FormatState,
) -> Result<(), std::io::Error> {
    let mut sub = node.children_with_tokens().peekable();

    // Let's decide based on the first child
    match sub.peek() {
        Some(first_node_or_token) => {
            if first_node_or_token.kind() == SyntaxKind::Identifier {
                // In this branch the sub element starts with an identifier, eg.
                // Something := Text {...}
                if !(whitespace_to(&mut sub, SyntaxKind::Identifier, writer, state, "")?
                    && whitespace_to(&mut sub, SyntaxKind::ColonEqual, writer, state, " ")?)
                {
                    // There is an error finding the QualifiedName and LBrace when we do this branch.
                    finish_node(sub, writer, state)?;
                    return Ok(());
                }
                state.insert_whitespace(" ");
            }
            // If the first child was not an identifier, we just fold it
            // (it might be an element, eg. Text {...})
            for s in sub {
                fold(s, writer, state)?;
            }
            Ok(())
        }
        // No children found -> we ignore this node
        None => Ok(()),
    }
}

fn format_property_declaration(
    node: &SyntaxNode,
    writer: &mut impl TokenWriter,
    state: &mut FormatState,
) -> Result<(), std::io::Error> {
    let mut sub = node.children_with_tokens();
    whitespace_to(&mut sub, SyntaxKind::Identifier, writer, state, "")?;
    while let SyntaxMatch::Found(x) = whitespace_to_one_of(
        &mut sub,
        &[SyntaxKind::Identifier, SyntaxKind::LAngle, SyntaxKind::DeclaredIdentifier],
        writer,
        state,
        " ",
    )? {
        match x {
            SyntaxKind::DeclaredIdentifier => break,
            SyntaxKind::LAngle => {
                let _ok = whitespace_to(&mut sub, SyntaxKind::Type, writer, state, "")?
                    && whitespace_to(&mut sub, SyntaxKind::RAngle, writer, state, "")?
                    && whitespace_to(&mut sub, SyntaxKind::DeclaredIdentifier, writer, state, " ")?;
                break;
            }
            _ => continue,
        }
    }
    let need_newline = node.child_node(SyntaxKind::TwoWayBinding).is_none();

    state.skip_all_whitespace = true;
    for s in sub {
        fold(s, writer, state)?;
    }
    if need_newline {
        state.new_line();
    }
    Ok(())
}

fn format_property_deprecation(
    node: &SyntaxNode,
    writer: &mut impl TokenWriter,
    state: &mut FormatState,
) -> Result<(), std::io::Error> {
    let mut sub = node.children_with_tokens();
    whitespace_to(&mut sub, SyntaxKind::At, writer, state, "")?;
    // The "deprecated" keyword and an optional `("message")`.
    whitespace_to(&mut sub, SyntaxKind::Identifier, writer, state, "")?;
    if node.child_token(SyntaxKind::LParent).is_some() {
        let _ok = whitespace_to(&mut sub, SyntaxKind::LParent, writer, state, "")?
            && whitespace_to(&mut sub, SyntaxKind::StringLiteral, writer, state, "")?
            && whitespace_to(&mut sub, SyntaxKind::RParent, writer, state, "")?;
    }
    // Drop the whitespace between the deprecation and the property; the caller
    // inserts a single space before the following keyword.
    state.skip_all_whitespace = true;
    Ok(())
}

fn format_binding(
    node: &SyntaxNode,
    writer: &mut impl TokenWriter,
    state: &mut FormatState,
) -> Result<(), std::io::Error> {
    let mut sub = node.children_with_tokens();
    let _ok = whitespace_to(&mut sub, SyntaxKind::Identifier, writer, state, "")?
        && whitespace_to(&mut sub, SyntaxKind::Colon, writer, state, "")?
        && whitespace_to(&mut sub, SyntaxKind::BindingExpression, writer, state, " ")?;
    // FIXME: more formatting
    for s in sub {
        fold(s, writer, state)?;
    }
    state.new_line();
    Ok(())
}

fn format_two_way_binding(
    node: &SyntaxNode,
    writer: &mut impl TokenWriter,
    state: &mut FormatState,
) -> Result<(), std::io::Error> {
    let mut sub = node.children_with_tokens();
    if node.child_token(SyntaxKind::Identifier).is_some() {
        whitespace_to(&mut sub, SyntaxKind::Identifier, writer, state, "")?;
    }
    let _ok = whitespace_to(&mut sub, SyntaxKind::DoubleArrow, writer, state, " ")?
        && whitespace_to(&mut sub, SyntaxKind::Expression, writer, state, " ")?;
    if node.child_token(SyntaxKind::Semicolon).is_some() {
        whitespace_to(&mut sub, SyntaxKind::Semicolon, writer, state, "")?;
        state.new_line();
    }
    for s in sub {
        fold(s, writer, state)?;
    }
    Ok(())
}

fn format_implement_statement(
    node: &SyntaxNode,
    writer: &mut impl TokenWriter,
    state: &mut FormatState,
) -> Result<(), std::io::Error> {
    let mut sub = node.children_with_tokens();
    whitespace_to(&mut sub, SyntaxKind::Identifier, writer, state, "")?; // "implement"
    let _ok = whitespace_to(&mut sub, SyntaxKind::QualifiedName, writer, state, " ")?
        && whitespace_to(&mut sub, SyntaxKind::DoubleArrow, writer, state, " ")?
        && whitespace_to(&mut sub, SyntaxKind::DeclaredIdentifier, writer, state, " ")?;
    if node.child_token(SyntaxKind::Semicolon).is_some() {
        whitespace_to(&mut sub, SyntaxKind::Semicolon, writer, state, "")?;
        state.new_line();
    }
    for s in sub {
        fold(s, writer, state)?;
    }
    Ok(())
}

fn format_callback_declaration(
    node: &SyntaxNode,
    writer: &mut impl TokenWriter,
    state: &mut FormatState,
) -> Result<(), std::io::Error> {
    let mut sub = node.children_with_tokens();
    whitespace_to(&mut sub, SyntaxKind::Identifier, writer, state, "")?;
    while whitespace_to_one_of(
        &mut sub,
        &[SyntaxKind::Identifier, SyntaxKind::DeclaredIdentifier],
        writer,
        state,
        " ",
    )? == SyntaxMatch::Found(SyntaxKind::Identifier)
    {}

    while let Some(n) = sub.next() {
        state.skip_all_whitespace = true;
        match n.kind() {
            SyntaxKind::Comma => {
                fold(n, writer, state)?;
                state.insert_whitespace(" ");
            }
            SyntaxKind::Arrow => {
                state.insert_whitespace(" ");
                fold(n, writer, state)?;
                whitespace_to(&mut sub, SyntaxKind::ReturnType, writer, state, " ")?;
            }
            SyntaxKind::TwoWayBinding => {
                fold(n, writer, state)?;
            }
            SyntaxKind::CallbackDeclarationParameter => {
                for n in n.as_node().unwrap().children_with_tokens() {
                    state.skip_all_whitespace = true;
                    match n.kind() {
                        SyntaxKind::Colon => {
                            fold(n, writer, state)?;
                            state.insert_whitespace(" ");
                        }
                        _ => {
                            fold(n, writer, state)?;
                        }
                    }
                }
            }
            _ => {
                fold(n, writer, state)?;
            }
        }
    }
    state.new_line();
    Ok(())
}

fn format_function(
    node: &SyntaxNode,
    writer: &mut impl TokenWriter,
    state: &mut FormatState,
) -> Result<(), std::io::Error> {
    let mut sub = node.children_with_tokens();
    whitespace_to(&mut sub, SyntaxKind::Identifier, writer, state, "")?;
    while whitespace_to_one_of(
        &mut sub,
        &[SyntaxKind::Identifier, SyntaxKind::DeclaredIdentifier],
        writer,
        state,
        " ",
    )? == SyntaxMatch::Found(SyntaxKind::Identifier)
    {}

    while let Some(n) = sub.next() {
        state.skip_all_whitespace = true;
        match n.kind() {
            SyntaxKind::Comma => {
                fold(n, writer, state)?;
                state.insert_whitespace(" ");
            }
            SyntaxKind::Arrow => {
                state.insert_whitespace(" ");
                fold(n, writer, state)?;
                whitespace_to(&mut sub, SyntaxKind::ReturnType, writer, state, " ")?;
            }
            SyntaxKind::CodeBlock => {
                state.insert_whitespace(" ");
                fold(n, writer, state)?;
            }
            _ => {
                fold(n, writer, state)?;
            }
        }
    }
    state.new_line();
    Ok(())
}

fn format_argument_declaration(
    node: &SyntaxNode,
    writer: &mut impl TokenWriter,
    state: &mut FormatState,
) -> Result<(), std::io::Error> {
    for n in node.children_with_tokens() {
        state.skip_all_whitespace = true;
        match n.kind() {
            SyntaxKind::Colon => {
                fold(n, writer, state)?;
                state.insert_whitespace(" ");
            }
            _ => {
                fold(n, writer, state)?;
            }
        }
    }
    Ok(())
}

fn format_callback_connection(
    node: &SyntaxNode,
    writer: &mut impl TokenWriter,
    state: &mut FormatState,
) -> Result<(), std::io::Error> {
    let mut sub = node.children_with_tokens();
    whitespace_to(&mut sub, SyntaxKind::Identifier, writer, state, "")?;

    for s in sub {
        state.skip_all_whitespace = true;
        match s.kind() {
            SyntaxKind::FatArrow => {
                state.insert_whitespace(" ");
                fold(s, writer, state)?;
                state.insert_whitespace(" ");
            }
            SyntaxKind::Comma => {
                fold(s, writer, state)?;
                state.insert_whitespace(" ");
            }
            _ => fold(s, writer, state)?,
        }
    }
    state.new_line();
    Ok(())
}

fn format_qualified_name(
    node: &SyntaxNode,
    writer: &mut impl TokenWriter,
    state: &mut FormatState,
) -> Result<(), std::io::Error> {
    for n in node.children_with_tokens() {
        state.skip_all_whitespace = true;
        fold(n, writer, state)?;
    }
    Ok(())
}

/// Check whether the whitespace immediately preceding `token` in the token
/// stream contains a newline.
fn has_newline_before(token: &i_slint_compiler::parser::SyntaxToken) -> bool {
    token
        .prev_token()
        .is_some_and(|prev| prev.kind() == SyntaxKind::Whitespace && prev.text().contains('\n'))
}

// Called both for BinaryExpression and SelfAssignment
fn format_binary_expression(
    node: &SyntaxNode,
    writer: &mut impl TokenWriter,
    state: &mut FormatState,
) -> Result<(), std::io::Error> {
    let op_token = node.children_with_tokens().find(|n| {
        !matches!(n.kind(), SyntaxKind::Whitespace | SyntaxKind::Comment | SyntaxKind::Expression)
    });
    let has_newline = op_token.as_ref().and_then(|op| op.as_token()).is_some_and(|op| {
        has_newline_before(op)
            || op
                .next_token()
                .is_some_and(|t| t.kind() == SyntaxKind::Whitespace && t.text().contains('\n'))
    });

    if has_newline {
        let mut seen_first_expr = false;
        for n in node.children_with_tokens() {
            match n.kind() {
                SyntaxKind::Whitespace => {
                    state.skip_all_whitespace = true;
                    fold(n, writer, state)?;
                }
                SyntaxKind::Comment => {
                    fold(n, writer, state)?;
                }
                SyntaxKind::Expression => {
                    if seen_first_expr {
                        let nl = n
                            .as_node()
                            .and_then(|n| n.first_token())
                            .is_some_and(|t| has_newline_before(&t));
                        if nl {
                            state.whitespace_to_add = None;
                            state.indentation_level += 1;
                            state.new_line();
                            state.indentation_level -= 1;
                        }
                    }
                    seen_first_expr = true;
                    fold(n, writer, state)?;
                }
                _ => {
                    if let Some(t) = n.as_token()
                        && has_newline_before(t)
                    {
                        state.whitespace_to_add = None;
                        state.indentation_level += 1;
                        state.new_line();
                        state.indentation_level -= 1;
                    } else {
                        state.insert_whitespace(" ");
                    }
                    fold(n, writer, state)?;
                    state.insert_whitespace(" ");
                }
            }
        }
    } else {
        let mut sub = node.children_with_tokens();
        let _ok = whitespace_to(&mut sub, SyntaxKind::Expression, writer, state, "")?
            && whitespace_to_one_of(
                &mut sub,
                &[
                    SyntaxKind::Plus,
                    SyntaxKind::Minus,
                    SyntaxKind::Star,
                    SyntaxKind::Div,
                    SyntaxKind::AndAnd,
                    SyntaxKind::OrOr,
                    SyntaxKind::EqualEqual,
                    SyntaxKind::NotEqual,
                    SyntaxKind::LAngle,
                    SyntaxKind::LessEqual,
                    SyntaxKind::RAngle,
                    SyntaxKind::GreaterEqual,
                    SyntaxKind::Equal,
                    SyntaxKind::PlusEqual,
                    SyntaxKind::MinusEqual,
                    SyntaxKind::StarEqual,
                    SyntaxKind::DivEqual,
                ],
                writer,
                state,
                " ",
            )?
            .is_found()
            && whitespace_to(&mut sub, SyntaxKind::Expression, writer, state, " ")?;
    }
    Ok(())
}

fn format_conditional_expression(
    node: &SyntaxNode,
    writer: &mut impl TokenWriter,
    state: &mut FormatState,
) -> Result<(), std::io::Error> {
    let has_if = node.child_text(SyntaxKind::Identifier).is_some_and(|x| x == "if");

    let mut sub = node.children_with_tokens();
    if has_if {
        let ok = whitespace_to(&mut sub, SyntaxKind::Identifier, writer, state, "")?
            && whitespace_to(&mut sub, SyntaxKind::Expression, writer, state, " ")?
            && whitespace_to(&mut sub, SyntaxKind::Expression, writer, state, " ")?;
        if !ok {
            finish_node(sub, writer, state)?;
            return Ok(());
        }
        let mut pending_newline = false;
        while let Some(n) = sub.next() {
            state.skip_all_whitespace = true;
            match n.kind() {
                SyntaxKind::Whitespace => {
                    if n.as_token().is_some_and(|t| t.text().contains('\n')) {
                        pending_newline = true;
                    }
                    fold(n, writer, state)?;
                }
                SyntaxKind::Identifier => {
                    // `else` keyword
                    if pending_newline {
                        state.new_line();
                        pending_newline = false;
                    } else {
                        state.insert_whitespace(" ");
                    }
                    fold(n, writer, state)?;
                    whitespace_to(&mut sub, SyntaxKind::Expression, writer, state, " ")?;
                }
                _ => {
                    fold(n, writer, state)?;
                }
            }
        }
        state.whitespace_to_add = None;
        state.new_line();
    } else {
        let has_newline = node.children_with_tokens().any(|n| {
            matches!(n.kind(), SyntaxKind::Question | SyntaxKind::Colon)
                && n.as_token().is_some_and(|t| {
                    has_newline_before(t)
                        || t.next_token().is_some_and(|nt| {
                            nt.kind() == SyntaxKind::Whitespace && nt.text().contains('\n')
                        })
                })
        });

        if has_newline {
            let mut seen_first_expr = false;
            for n in node.children_with_tokens() {
                match n.kind() {
                    SyntaxKind::Whitespace => {
                        state.skip_all_whitespace = true;
                        fold(n, writer, state)?;
                    }
                    SyntaxKind::Comment => {
                        fold(n, writer, state)?;
                    }
                    SyntaxKind::Expression => {
                        if seen_first_expr {
                            let nl = n
                                .as_node()
                                .and_then(|n| n.first_token())
                                .is_some_and(|t| has_newline_before(&t));
                            if nl {
                                state.whitespace_to_add = None;
                                state.indentation_level += 1;
                                state.new_line();
                                state.indentation_level -= 1;
                            }
                        }
                        seen_first_expr = true;
                        fold(n, writer, state)?;
                    }
                    SyntaxKind::Question | SyntaxKind::Colon => {
                        if let Some(t) = n.as_token()
                            && has_newline_before(t)
                        {
                            state.whitespace_to_add = None;
                            state.indentation_level += 1;
                            state.new_line();
                            state.indentation_level -= 1;
                        } else {
                            state.insert_whitespace(" ");
                        }
                        fold(n, writer, state)?;
                        state.insert_whitespace(" ");
                    }
                    _ => {
                        fold(n, writer, state)?;
                    }
                }
            }
        } else {
            let _ok = whitespace_to(&mut sub, SyntaxKind::Expression, writer, state, "")?
                && whitespace_to(&mut sub, SyntaxKind::Question, writer, state, " ")?
                && whitespace_to(&mut sub, SyntaxKind::Expression, writer, state, " ")?
                && whitespace_to(&mut sub, SyntaxKind::Colon, writer, state, " ")?
                && whitespace_to(&mut sub, SyntaxKind::Expression, writer, state, " ")?;
            finish_node(sub, writer, state)?;
        }
    }
    Ok(())
}

fn format_expression(
    node: &SyntaxNode,
    writer: &mut impl TokenWriter,
    state: &mut FormatState,
) -> Result<(), std::io::Error> {
    let children: Vec<_> = node.children_with_tokens().collect();
    let mut saw_non_ws = false;

    for (i, n) in children.iter().enumerate() {
        if n.kind() == SyntaxKind::Whitespace {
            if saw_non_ws
                && children[i + 1..]
                    .iter()
                    .any(|c| !matches!(c.kind(), SyntaxKind::Whitespace | SyntaxKind::Comment))
                && n.as_token().is_some_and(|t| t.text().contains('\n'))
            {
                state.whitespace_to_add = None;
                state.indentation_level += 1;
                state.new_line();
                state.indentation_level -= 1;
            }
            state.skip_all_whitespace = true;
            fold(n.clone(), writer, state)?;
        } else {
            saw_non_ws = true;
            state.skip_all_whitespace = true;
            fold(n.clone(), writer, state)?;
        }
    }
    Ok(())
}

fn format_codeblock(
    node: &SyntaxNode,
    writer: &mut impl TokenWriter,
    state: &mut FormatState,
) -> Result<(), std::io::Error> {
    if node.first_child_or_token().is_none() {
        // empty CodeBlock happens when there is no `else` for example
        return Ok(());
    }

    // Keep a code block on a single line if the source had it on a single line.
    if !node.text().contains_char('\n') {
        return format_codeblock_single_line(node, writer, state);
    }

    let mut sub = node.children_with_tokens();
    if !whitespace_to(&mut sub, SyntaxKind::LBrace, writer, state, "")? {
        finish_node(sub, writer, state)?;
        return Ok(());
    }
    state.indentation_level += 1;
    state.new_line();
    for n in sub {
        state.skip_all_whitespace = true;
        if n.kind() == SyntaxKind::Whitespace {
            let is_empty_line =
                n.as_token().map(|n| whitespace_has_empty_line(n.text())).unwrap_or(false);
            if is_empty_line {
                state.new_line();
            }
        } else if n.kind() == SyntaxKind::RBrace {
            state.indentation_level -= 1;
            state.whitespace_to_add = None;
            state.new_line();
        }

        let is_semicolon = n.kind() == SyntaxKind::Semicolon;

        fold(n, writer, state)?;

        if is_semicolon {
            state.whitespace_to_add = None;
            state.new_line();
        }
    }
    state.skip_all_whitespace = true;
    Ok(())
}

fn format_codeblock_single_line(
    node: &SyntaxNode,
    writer: &mut impl TokenWriter,
    state: &mut FormatState,
) -> Result<(), std::io::Error> {
    // Is there anything between `{` and `}` other than trivia?
    let has_content = node.children_with_tokens().any(|n| {
        !matches!(n.kind(), SyntaxKind::Whitespace | SyntaxKind::LBrace | SyntaxKind::RBrace)
    });

    let mut sub = node.children_with_tokens();
    if !whitespace_to(&mut sub, SyntaxKind::LBrace, writer, state, "")? {
        finish_node(sub, writer, state)?;
        return Ok(());
    }
    let was_inline = std::mem::replace(&mut state.inline_codeblock, true);
    if has_content {
        state.insert_whitespace(" ");
    }
    for n in sub {
        state.skip_all_whitespace = true;
        if n.kind() == SyntaxKind::RBrace {
            state.whitespace_to_add = None;
            if has_content {
                // ensure a space even if the previous token was a comment
                state.after_comment = false;
                state.insert_whitespace(" ");
            }
        }

        let is_semicolon = n.kind() == SyntaxKind::Semicolon;

        fold(n, writer, state)?;

        if is_semicolon {
            state.whitespace_to_add = None;
            state.insert_whitespace(" ");
        }
        // Don't preserve whitespace following a comment verbatim — the next
        // iteration should normalize it to a single space too.
        state.after_comment = false;
    }
    state.skip_all_whitespace = true;
    state.inline_codeblock = was_inline;
    Ok(())
}

fn format_return_statement(
    node: &SyntaxNode,
    writer: &mut impl TokenWriter,
    state: &mut FormatState,
) -> Result<(), std::io::Error> {
    let mut sub = node.children_with_tokens();
    whitespace_to(&mut sub, SyntaxKind::Identifier, writer, state, "")?;
    if node.child_node(SyntaxKind::Expression).is_some() {
        whitespace_to(&mut sub, SyntaxKind::Expression, writer, state, " ")?;
    }
    whitespace_to(&mut sub, SyntaxKind::Semicolon, writer, state, "")?;
    state.new_line();
    finish_node(sub, writer, state)?;
    Ok(())
}

fn format_let_statement(
    node: &SyntaxNode,
    writer: &mut impl TokenWriter,
    state: &mut FormatState,
) -> Result<(), std::io::Error> {
    let mut sub = node.children_with_tokens();
    whitespace_to(&mut sub, SyntaxKind::Identifier, writer, state, "")?; // "let"
    whitespace_to(&mut sub, SyntaxKind::DeclaredIdentifier, writer, state, " ")?;
    // if type annotated
    if node.child_token(SyntaxKind::Colon).is_some() {
        whitespace_to(&mut sub, SyntaxKind::Colon, writer, state, "")?;
        if node.child_node(SyntaxKind::Type).is_some() {
            whitespace_to(&mut sub, SyntaxKind::Type, writer, state, " ")?;
        }
    }
    whitespace_to(&mut sub, SyntaxKind::Equal, writer, state, " ")?;
    whitespace_to(&mut sub, SyntaxKind::Expression, writer, state, " ")?;
    whitespace_to(&mut sub, SyntaxKind::Semicolon, writer, state, "")?;
    state.new_line();
    finish_node(sub, writer, state)?;
    Ok(())
}

fn format_at_gradient(
    node: &SyntaxNode,
    writer: &mut impl TokenWriter,
    state: &mut FormatState,
) -> Result<(), std::io::Error> {
    // ensure that two consecutive expression are separated with space
    let mut seen_expression = false;
    for n in node.children_with_tokens() {
        state.skip_all_whitespace = true;
        if n.kind() == SyntaxKind::Expression {
            if seen_expression {
                state.insert_whitespace(" ");
            }
            seen_expression = true;
        }
        fold(n, writer, state)?;
    }
    Ok(())
}

fn format_children_placeholder(
    node: &SyntaxNode,
    writer: &mut impl TokenWriter,
    state: &mut FormatState,
) -> Result<(), std::io::Error> {
    for n in node.children_with_tokens() {
        fold(n, writer, state)?;
    }
    state.new_line();
    Ok(())
}

fn format_slot_declaration(
    node: &SyntaxNode,
    writer: &mut impl TokenWriter,
    state: &mut FormatState,
) -> Result<(), std::io::Error> {
    let mut sub = node.children_with_tokens();
    let _ok = whitespace_to(&mut sub, SyntaxKind::Identifier, writer, state, "")?
        && whitespace_to(&mut sub, SyntaxKind::DeclaredIdentifier, writer, state, " ")?
        && whitespace_to(&mut sub, SyntaxKind::Semicolon, writer, state, "")?;
    finish_node(sub, writer, state)?;
    state.new_line();
    Ok(())
}

fn format_repeated_element(
    node: &SyntaxNode,
    writer: &mut impl TokenWriter,
    state: &mut FormatState,
) -> Result<(), std::io::Error> {
    let mut sub = node.children_with_tokens();
    whitespace_to(&mut sub, SyntaxKind::Identifier, writer, state, "")?;
    whitespace_to(&mut sub, SyntaxKind::DeclaredIdentifier, writer, state, " ")?;

    let (kind, prefix_whitespace) = if node.child_node(SyntaxKind::RepeatedIndex).is_some() {
        (SyntaxKind::RepeatedIndex, "")
    } else {
        (SyntaxKind::Identifier, " ")
    };
    whitespace_to(&mut sub, kind, writer, state, prefix_whitespace)?;

    if kind == SyntaxKind::RepeatedIndex {
        whitespace_to(&mut sub, SyntaxKind::Identifier, writer, state, " ")?;
    }

    whitespace_to(&mut sub, SyntaxKind::Expression, writer, state, " ")?;
    whitespace_to(&mut sub, SyntaxKind::Colon, writer, state, "")?;
    state.insert_whitespace(" ");
    state.skip_all_whitespace = true;

    for s in sub {
        fold(s, writer, state)?;
    }
    Ok(())
}

fn format_match_element(
    node: &SyntaxNode,
    writer: &mut impl TokenWriter,
    state: &mut FormatState,
) -> Result<(), std::io::Error> {
    let mut sub = node.children_with_tokens();
    whitespace_to(&mut sub, SyntaxKind::Identifier, writer, state, "")?;
    whitespace_to(&mut sub, SyntaxKind::Expression, writer, state, " ")?;
    whitespace_to(&mut sub, SyntaxKind::LBrace, writer, state, " ")?;

    state.indentation_level += 1;
    state.new_line();

    for s in sub {
        if s.kind() == SyntaxKind::RBrace {
            state.indentation_level -= 1;
            state.whitespace_to_add = None;
            state.new_line();
            fold(s, writer, state)?;
            state.new_line();
        } else {
            fold(s, writer, state)?;
        }
    }
    Ok(())
}

fn format_match_case(
    node: &SyntaxNode,
    writer: &mut impl TokenWriter,
    state: &mut FormatState,
) -> Result<(), std::io::Error> {
    let mut sub = node.children_with_tokens().peekable();
    whitespace_to(&mut sub, SyntaxKind::Expression, writer, state, "")?;
    whitespace_to(&mut sub, SyntaxKind::Colon, writer, state, "")?;
    if node.child_node(SyntaxKind::SubElement).is_none() {
        whitespace_to(&mut sub, SyntaxKind::LBrace, writer, state, " ")?;
        whitespace_to(&mut sub, SyntaxKind::RBrace, writer, state, " ")?;
        state.skip_all_whitespace = true;
        state.new_line();
    } else {
        state.insert_whitespace(" ");
        state.skip_all_whitespace = true;
        for s in sub {
            fold(s, writer, state)?;
        }
    }
    Ok(())
}

fn format_repeated_index(
    node: &SyntaxNode,
    writer: &mut impl TokenWriter,
    state: &mut FormatState,
) -> Result<(), std::io::Error> {
    let mut sub = node.children_with_tokens();
    whitespace_to(&mut sub, SyntaxKind::LBracket, writer, state, "")?;
    whitespace_to(&mut sub, SyntaxKind::Identifier, writer, state, "")?;
    whitespace_to(&mut sub, SyntaxKind::RBracket, writer, state, "")?;
    Ok(())
}

fn format_array(
    node: &SyntaxNode,
    writer: &mut impl TokenWriter,
    state: &mut FormatState,
) -> Result<(), std::io::Error> {
    let has_trailing_comma = node
        .last_token()
        .and_then(|last| last.prev_token())
        .and_then(prev_non_trivia_token_kind)
        .map(|kind| kind == SyntaxKind::Comma)
        .unwrap_or(false);
    // len of all children
    let len = node.children().fold(0, |acc, e| {
        let mut len = 0;
        e.text().for_each_chunk(|s| len += s.trim().len());
        acc + len
    });
    // add , and 1-space len for each
    // not really accurate, e.g., [1] should have len 1, but due to this
    // it will be 3, but it doesn't matter
    let len = len + (2 * node.children().count());
    let is_large_array = len >= 80;
    let mut sub = node.children_with_tokens().peekable();
    whitespace_to(&mut sub, SyntaxKind::LBracket, writer, state, "")?;

    let is_empty_array = node.children().count() == 0;
    if is_empty_array {
        whitespace_to(&mut sub, SyntaxKind::RBracket, writer, state, "")?;
        return Ok(());
    }

    if is_large_array || has_trailing_comma {
        state.indentation_level += 1;
        state.new_line();
    }

    loop {
        whitespace_to(&mut sub, SyntaxKind::Expression, writer, state, "")?;
        let at_end = sub.peek().map(|next| next.kind() == SyntaxKind::RBracket).unwrap_or(false);
        if at_end && is_large_array {
            state.indentation_level -= 1;
            state.new_line();
        }

        let el = whitespace_to_one_of(
            &mut sub,
            &[SyntaxKind::RBracket, SyntaxKind::Comma],
            writer,
            state,
            "",
        )?;

        match el {
            SyntaxMatch::Found(SyntaxKind::RBracket) => break,
            SyntaxMatch::Found(SyntaxKind::Comma) => {
                let is_trailing_comma = sub
                    .peek()
                    .map(|next| {
                        if next.kind() == SyntaxKind::Whitespace {
                            next.as_token()
                                .and_then(|ws| ws.next_token())
                                .map(|n| n.kind() == SyntaxKind::RBracket)
                                .unwrap_or(false)
                        } else {
                            next.kind() == SyntaxKind::RBracket
                        }
                    })
                    .unwrap_or(false);

                if is_trailing_comma {
                    state.indentation_level -= 1;
                    state.new_line();
                    whitespace_to(&mut sub, SyntaxKind::RBracket, writer, state, "")?;
                    break;
                }
                if is_large_array || has_trailing_comma {
                    state.new_line();
                } else {
                    state.insert_whitespace(" ");
                }
            }
            SyntaxMatch::NotFound | SyntaxMatch::Found(_) => {
                tracing::warn!("Inconsistency: unexpected syntax in array.");
                break;
            }
        }
    }

    Ok(())
}

fn format_state(
    node: &SyntaxNode,
    writer: &mut impl TokenWriter,
    state: &mut FormatState,
) -> Result<(), std::io::Error> {
    let has_when = node.child_text(SyntaxKind::Identifier).is_some_and(|x| x == "when");
    let mut sub = node.children_with_tokens();
    let ok = if has_when {
        whitespace_to(&mut sub, SyntaxKind::DeclaredIdentifier, writer, state, "")?
            && whitespace_to(&mut sub, SyntaxKind::Identifier, writer, state, " ")?
            && whitespace_to(&mut sub, SyntaxKind::Expression, writer, state, " ")?
            && whitespace_to(&mut sub, SyntaxKind::Colon, writer, state, "")?
            && whitespace_to(&mut sub, SyntaxKind::LBrace, writer, state, " ")?
    } else {
        whitespace_to(&mut sub, SyntaxKind::DeclaredIdentifier, writer, state, "")?
            && whitespace_to(&mut sub, SyntaxKind::Colon, writer, state, "")?
            && whitespace_to(&mut sub, SyntaxKind::LBrace, writer, state, " ")?
    };
    if !ok {
        finish_node(sub, writer, state)?;
        return Ok(());
    }

    state.new_line();
    let ins_ctn = state.insertion_count;
    let mut first = true;

    for n in sub {
        if matches!(n.kind(), SyntaxKind::StatePropertyChange | SyntaxKind::Transition) {
            if first {
                // add new line after brace + increase indent
                state.indentation_level += 1;
                state.whitespace_to_add = None;
                state.new_line();
            }
            first = false;
            fold(n, writer, state)?;
        } else if n.kind() == SyntaxKind::RBrace {
            if !first {
                state.indentation_level -= 1;
            }
            state.whitespace_to_add = None;
            if ins_ctn == state.insertion_count {
                state.insert_whitespace("");
            } else {
                state.new_line();
            }
            fold(n, writer, state)?;
            state.new_line();
        } else {
            fold(n, writer, state)?;
        }
    }

    Ok(())
}

fn format_states(
    node: &SyntaxNode,
    writer: &mut impl TokenWriter,
    state: &mut FormatState,
) -> Result<(), std::io::Error> {
    let mut sub = node.children_with_tokens();
    let ok = whitespace_to(&mut sub, SyntaxKind::Identifier, writer, state, "")?
        && whitespace_to(&mut sub, SyntaxKind::LBracket, writer, state, " ")?;

    if !ok {
        tracing::warn!("Inconsistency: Expect states and ']'");
        return Ok(());
    }

    state.indentation_level += 1;
    state.new_line();

    for n in sub {
        if n.kind() == SyntaxKind::RBracket {
            state.whitespace_to_add = None;
            state.indentation_level -= 1;
            state.new_line();
        }
        fold(n, writer, state)?;
    }
    state.new_line();
    Ok(())
}

fn format_state_prop_change(
    node: &SyntaxNode,
    writer: &mut impl TokenWriter,
    state: &mut FormatState,
) -> Result<(), std::io::Error> {
    let mut sub = node.children_with_tokens();
    let _ok = whitespace_to(&mut sub, SyntaxKind::QualifiedName, writer, state, "")?
        && whitespace_to(&mut sub, SyntaxKind::Colon, writer, state, "")?
        && whitespace_to(&mut sub, SyntaxKind::BindingExpression, writer, state, " ")?;

    for n in sub {
        state.skip_all_whitespace = true;
        fold(n, writer, state)?;
    }
    state.new_line();
    Ok(())
}

fn format_transition(
    node: &SyntaxNode,
    writer: &mut impl TokenWriter,
    state: &mut FormatState,
) -> Result<(), std::io::Error> {
    let mut sub = node.children_with_tokens();
    let ok = whitespace_to(&mut sub, SyntaxKind::Identifier, writer, state, "")?
        && whitespace_to(&mut sub, SyntaxKind::LBrace, writer, state, " ")?;

    if !ok {
        finish_node(sub, writer, state)?;
        return Ok(());
    }
    state.indentation_level += 1;
    state.new_line();
    for n in sub {
        if n.kind() == SyntaxKind::RBrace {
            state.indentation_level -= 1;
            state.whitespace_to_add = None;
            state.new_line();
            fold(n, writer, state)?;
            state.new_line();
        } else {
            fold(n, writer, state)?;
        }
    }
    Ok(())
}

fn format_property_animation(
    node: &SyntaxNode,
    writer: &mut impl TokenWriter,
    state: &mut FormatState,
) -> Result<(), std::io::Error> {
    let mut sub = node.children_with_tokens().peekable();
    let _ok = whitespace_to(&mut sub, SyntaxKind::Identifier, writer, state, "")?
        && whitespace_to(&mut sub, SyntaxKind::QualifiedName, writer, state, " ")?;

    loop {
        let next_kind = sub.peek().map(|n| n.kind()).unwrap_or(SyntaxKind::Error);
        match next_kind {
            SyntaxKind::Whitespace | SyntaxKind::Comment => {
                let n = sub.next().unwrap();
                state.skip_all_whitespace = true;
                fold(n, writer, state)?;
                continue;
            }
            SyntaxKind::Comma => {
                whitespace_to(&mut sub, SyntaxKind::Comma, writer, state, "")?;
                continue;
            }
            SyntaxKind::QualifiedName => {
                whitespace_to(&mut sub, SyntaxKind::QualifiedName, writer, state, " ")?;
                continue;
            }
            SyntaxKind::LBrace => {
                whitespace_to(&mut sub, SyntaxKind::LBrace, writer, state, " ")?;
                break;
            }
            _ => break,
        }
    }

    let bindings = node.children().fold(0, |acc, e| {
        if e.kind() == SyntaxKind::Binding {
            return acc + 1;
        }
        acc
    });

    if bindings > 1 {
        state.indentation_level += 1;
        state.new_line();
    } else {
        state.insert_whitespace(" ");
    }

    for n in sub {
        if n.kind() == SyntaxKind::RBrace {
            state.whitespace_to_add = None;
            if bindings > 1 {
                state.indentation_level -= 1;
                state.new_line();
            } else {
                state.insert_whitespace(" ");
            }
            fold(n, writer, state)?;
            state.new_line();
        } else {
            fold(n, writer, state)?;
        }
    }
    Ok(())
}

fn format_object_literal(
    node: &SyntaxNode,
    writer: &mut impl TokenWriter,
    state: &mut FormatState,
) -> Result<(), std::io::Error> {
    let has_trailing_comma = node
        .last_token()
        .and_then(|last| last.prev_token())
        .and_then(prev_non_trivia_token_kind)
        .map(|kind| kind == SyntaxKind::Comma)
        .unwrap_or(false);
    // len of all children
    let len = node.children().fold(0, |acc, e| {
        let mut len = 0;
        e.text().for_each_chunk(|s| len += s.trim().len());
        acc + len
    });
    let is_large_literal = len >= 80;

    let mut sub = node.children_with_tokens().peekable();
    whitespace_to(&mut sub, SyntaxKind::LBrace, writer, state, "")?;
    let indent_with_new_line = is_large_literal || has_trailing_comma;

    if indent_with_new_line {
        state.indentation_level += 1;
        state.new_line();
    } else {
        state.insert_whitespace(" ");
    }

    loop {
        let el = whitespace_to_one_of(
            &mut sub,
            &[SyntaxKind::ObjectMember, SyntaxKind::RBrace],
            writer,
            state,
            "",
        )?;

        if let SyntaxMatch::Found(SyntaxKind::ObjectMember) = el {
            if indent_with_new_line {
                state.after_comment = false;
                state.new_line();
            } else {
                state.insert_whitespace(" ");
            }

            // are we at the end of literal?
            let at_end = sub
                .peek()
                .and_then(next_non_trivia_token_kind)
                .map(|kind| kind == SyntaxKind::RBrace)
                .unwrap_or(false);

            if at_end && indent_with_new_line {
                state.indentation_level -= 1;
                state.whitespace_to_add = None;
                state.new_line();
            }

            continue;
        } else if let SyntaxMatch::Found(SyntaxKind::RBrace) = el {
            break;
        } else {
            tracing::warn!("Inconsistency: unexpected syntax in object literal.");
            break;
        }
    }
    Ok(())
}

fn format_struct_declaration(
    node: &SyntaxNode,
    writer: &mut impl TokenWriter,
    state: &mut FormatState,
) -> Result<(), std::io::Error> {
    let mut sub = node.children_with_tokens();
    let _ok = whitespace_to(&mut sub, SyntaxKind::Identifier, writer, state, "")?
        && whitespace_to(&mut sub, SyntaxKind::DeclaredIdentifier, writer, state, " ")?
        && whitespace_to(&mut sub, SyntaxKind::ObjectType, writer, state, " ")?;
    finish_node(sub, writer, state)?;
    state.new_line();
    Ok(())
}

fn format_enum_declaration(
    node: &SyntaxNode,
    writer: &mut impl TokenWriter,
    state: &mut FormatState,
) -> Result<(), std::io::Error> {
    let has_trailing_comma = node
        .last_token()
        .and_then(|last| last.prev_token())
        .and_then(prev_non_trivia_token_kind)
        .map(|kind| kind == SyntaxKind::Comma)
        .unwrap_or(false);
    let len = node.children().fold(0, |acc, e| {
        let mut len = 0;
        e.text().for_each_chunk(|s| len += s.trim().len());
        acc + len
    });
    let is_large = len >= 80;
    let indent_with_new_line = is_large || has_trailing_comma;

    let mut sub = node.children_with_tokens().peekable();
    // `enum`
    whitespace_to(&mut sub, SyntaxKind::Identifier, writer, state, "")?;
    // name
    whitespace_to(&mut sub, SyntaxKind::DeclaredIdentifier, writer, state, " ")?;
    // `{`
    whitespace_to(&mut sub, SyntaxKind::LBrace, writer, state, " ")?;

    if indent_with_new_line {
        state.indentation_level += 1;
        state.new_line();
    } else {
        state.insert_whitespace(" ");
    }

    loop {
        let el = whitespace_to_one_of(
            &mut sub,
            &[SyntaxKind::EnumValue, SyntaxKind::RBrace],
            writer,
            state,
            "",
        )?;

        match el {
            SyntaxMatch::Found(SyntaxKind::EnumValue) => {
                // Check if the next non-whitespace token is RBrace (no comma after last value)
                let next_is_rbrace = sub
                    .peek()
                    .map(|next| {
                        if next.kind() == SyntaxKind::Whitespace {
                            next.as_token()
                                .and_then(|ws| ws.next_token())
                                .map(|n| n.kind() == SyntaxKind::RBrace)
                                .unwrap_or(false)
                        } else {
                            next.kind() == SyntaxKind::RBrace
                        }
                    })
                    .unwrap_or(false);

                if next_is_rbrace {
                    whitespace_to(&mut sub, SyntaxKind::RBrace, writer, state, " ")?;
                    break;
                }

                let comma = whitespace_to_one_of(
                    &mut sub,
                    &[SyntaxKind::Comma, SyntaxKind::RBrace],
                    writer,
                    state,
                    "",
                )?;
                match comma {
                    SyntaxMatch::Found(SyntaxKind::RBrace) => break,
                    SyntaxMatch::Found(SyntaxKind::Comma) => {
                        let is_trailing = sub
                            .peek()
                            .map(|next| {
                                if next.kind() == SyntaxKind::Whitespace {
                                    next.as_token()
                                        .and_then(|ws| ws.next_token())
                                        .map(|n| n.kind() == SyntaxKind::RBrace)
                                        .unwrap_or(false)
                                } else {
                                    next.kind() == SyntaxKind::RBrace
                                }
                            })
                            .unwrap_or(false);

                        if is_trailing {
                            if indent_with_new_line {
                                state.indentation_level -= 1;
                            }
                            state.new_line();
                            whitespace_to(&mut sub, SyntaxKind::RBrace, writer, state, "")?;
                            break;
                        }
                        if indent_with_new_line {
                            state.new_line();
                        } else {
                            state.insert_whitespace(" ");
                        }
                    }
                    _ => break,
                }
            }
            SyntaxMatch::Found(SyntaxKind::RBrace) => {
                if indent_with_new_line {
                    state.indentation_level -= 1;
                }
                break;
            }
            _ => break,
        }
    }
    state.skip_all_whitespace = true;
    finish_node(sub, writer, state)?;
    state.new_line();
    Ok(())
}

fn format_exports_list(
    node: &SyntaxNode,
    writer: &mut impl TokenWriter,
    state: &mut FormatState,
) -> Result<(), std::io::Error> {
    // ExportsList may contain `export { Foo, Bar }` or `export component ...` etc.
    // Only handle the brace-list case specially; otherwise fall through.
    let has_lbrace = node.children_with_tokens().any(|n| n.kind() == SyntaxKind::LBrace);
    if !has_lbrace {
        // `export component ...` or `export struct ...` — delegate to default
        for n in node.children_with_tokens() {
            fold(n, writer, state)?;
        }
        return Ok(());
    }

    let has_trailing_comma = node
        .children_with_tokens()
        .filter_map(|n| n.into_token())
        .find(|t| t.kind() == SyntaxKind::RBrace)
        .and_then(|rbrace| rbrace.prev_token())
        .and_then(prev_non_trivia_token_kind)
        .map(|kind| kind == SyntaxKind::Comma)
        .unwrap_or(false);
    let is_large = node.text().len() > 80.into();
    let indent_with_new_line = is_large || has_trailing_comma;

    let mut sub = node.children_with_tokens().peekable();
    // `export`
    whitespace_to(&mut sub, SyntaxKind::Identifier, writer, state, "")?;
    // `{`
    whitespace_to(&mut sub, SyntaxKind::LBrace, writer, state, " ")?;

    if indent_with_new_line {
        state.indentation_level += 1;
        state.new_line();
    } else {
        state.insert_whitespace(" ");
    }

    loop {
        let el = whitespace_to_one_of(
            &mut sub,
            &[SyntaxKind::ExportSpecifier, SyntaxKind::RBrace],
            writer,
            state,
            "",
        )?;

        match el {
            SyntaxMatch::Found(SyntaxKind::ExportSpecifier) => {
                // Check if the next non-whitespace token is RBrace (no comma after last specifier)
                let next_is_rbrace = sub
                    .peek()
                    .map(|next| {
                        if next.kind() == SyntaxKind::Whitespace {
                            next.as_token()
                                .and_then(|ws| ws.next_token())
                                .map(|n| n.kind() == SyntaxKind::RBrace)
                                .unwrap_or(false)
                        } else {
                            next.kind() == SyntaxKind::RBrace
                        }
                    })
                    .unwrap_or(false);

                if next_is_rbrace {
                    if indent_with_new_line {
                        writer.insert_content(",")?;
                        state.indentation_level -= 1;
                        state.new_line();
                        whitespace_to(&mut sub, SyntaxKind::RBrace, writer, state, "")?;
                    } else {
                        whitespace_to(&mut sub, SyntaxKind::RBrace, writer, state, " ")?;
                    }
                    break;
                }

                let comma = whitespace_to_one_of(
                    &mut sub,
                    &[SyntaxKind::Comma, SyntaxKind::RBrace],
                    writer,
                    state,
                    "",
                )?;
                match comma {
                    SyntaxMatch::Found(SyntaxKind::RBrace) => {
                        if indent_with_new_line {
                            state.indentation_level -= 1;
                        }
                        break;
                    }
                    SyntaxMatch::Found(SyntaxKind::Comma) => {
                        let is_trailing = sub
                            .peek()
                            .map(|next| {
                                if next.kind() == SyntaxKind::Whitespace {
                                    next.as_token()
                                        .and_then(|ws| ws.next_token())
                                        .map(|n| n.kind() == SyntaxKind::RBrace)
                                        .unwrap_or(false)
                                } else {
                                    next.kind() == SyntaxKind::RBrace
                                }
                            })
                            .unwrap_or(false);

                        if is_trailing {
                            if indent_with_new_line {
                                state.indentation_level -= 1;
                            }
                            state.new_line();
                            whitespace_to(&mut sub, SyntaxKind::RBrace, writer, state, "")?;
                            break;
                        }
                        if indent_with_new_line {
                            state.new_line();
                        } else {
                            state.insert_whitespace(" ");
                        }
                    }
                    _ => break,
                }
            }
            SyntaxMatch::Found(SyntaxKind::RBrace) => {
                if indent_with_new_line {
                    state.indentation_level -= 1;
                }
                break;
            }
            _ => break,
        }
    }
    state.skip_all_whitespace = true;
    finish_node(sub, writer, state)?;
    state.new_line();
    Ok(())
}

fn format_export_specifier(
    node: &SyntaxNode,
    writer: &mut impl TokenWriter,
    state: &mut FormatState,
) -> Result<(), std::io::Error> {
    let has_as = node.children_with_tokens().any(|n| {
        n.kind() == SyntaxKind::Identifier && n.as_token().is_some_and(|t| t.text() == "as")
    });

    let mut sub = node.children_with_tokens();
    whitespace_to(&mut sub, SyntaxKind::ExportIdentifier, writer, state, "")?;
    if has_as {
        whitespace_to(&mut sub, SyntaxKind::Identifier, writer, state, " ")?;
        whitespace_to(&mut sub, SyntaxKind::ExportName, writer, state, " ")?;
    }
    state.skip_all_whitespace = true;
    finish_node(sub, writer, state)?;
    Ok(())
}

fn format_object_type(
    node: &SyntaxNode,
    writer: &mut impl TokenWriter,
    state: &mut FormatState,
) -> Result<(), std::io::Error> {
    // Mirror object literal formatting, but for type definitions
    let has_trailing_comma = node
        .last_token()
        .and_then(|last| last.prev_token())
        .and_then(prev_non_trivia_token_kind)
        .map(|kind| kind == SyntaxKind::Comma)
        .unwrap_or(false);
    let len = node.children().fold(0, |acc, e| {
        let mut len = 0;
        e.text().for_each_chunk(|s| len += s.trim().len());
        acc + len
    });
    let is_large_object = len >= 80;
    let has_comment = node.descendants_with_tokens().any(|n| n.kind() == SyntaxKind::Comment);
    let member_count = node.children().filter(|n| n.kind() == SyntaxKind::ObjectTypeMember).count();

    let mut sub = node.children_with_tokens().peekable();
    whitespace_to(&mut sub, SyntaxKind::LBrace, writer, state, "")?;
    // Trailing commas, long content, or comments keep braces on separate lines.
    let indent_with_new_line = is_large_object || has_trailing_comma || has_comment;

    if indent_with_new_line {
        state.indentation_level += 1;
        state.new_line();
    } else if member_count > 1 {
        // Multi-field inline structs keep a space after `{`.
        state.insert_whitespace(" ");
    }

    loop {
        let el = whitespace_to_one_of(
            &mut sub,
            &[SyntaxKind::ObjectTypeMember, SyntaxKind::RBrace],
            writer,
            state,
            "",
        )?;

        if let SyntaxMatch::Found(SyntaxKind::ObjectTypeMember) = el {
            let at_end = sub
                .peek()
                .and_then(next_non_trivia_token_kind)
                .map(|kind| kind == SyntaxKind::RBrace)
                .unwrap_or(false);

            if indent_with_new_line {
                state.new_line();
            } else if !at_end || member_count > 1 {
                state.insert_whitespace(" ");
            }

            if at_end && indent_with_new_line {
                state.indentation_level -= 1;
                state.whitespace_to_add = None;
                state.new_line();
            }

            continue;
        } else if let SyntaxMatch::Found(SyntaxKind::RBrace) = el {
            break;
        } else {
            tracing::warn!("Inconsistency: unexpected syntax in object type.");
            break;
        }
    }
    Ok(())
}

fn format_object_type_member(
    node: &SyntaxNode,
    writer: &mut impl TokenWriter,
    state: &mut FormatState,
) -> Result<(), std::io::Error> {
    // Format a single struct field: `name: Type` or `name: Type = default-value`
    // (comma handled if present).
    let mut sub = node.children_with_tokens();
    let _ok = whitespace_to(&mut sub, SyntaxKind::Identifier, writer, state, "")?
        && whitespace_to(&mut sub, SyntaxKind::Colon, writer, state, "")?
        && whitespace_to(&mut sub, SyntaxKind::Type, writer, state, " ")?;
    if node.child_token(SyntaxKind::Equal).is_some() {
        let _ok = whitespace_to(&mut sub, SyntaxKind::Equal, writer, state, " ")?
            && whitespace_to(&mut sub, SyntaxKind::Expression, writer, state, " ")?;
    }
    if node.child_token(SyntaxKind::Comma).is_some() {
        whitespace_to(&mut sub, SyntaxKind::Comma, writer, state, "")?;
    }
    // Strip trailing whitespace unless it belongs to a trailing comment.
    if !state.after_comment {
        state.skip_all_whitespace = true;
    }
    finish_node(sub, writer, state)?;
    Ok(())
}

fn format_property_changed_callback(
    node: &SyntaxNode,
    writer: &mut impl TokenWriter,
    state: &mut FormatState,
) -> Result<(), std::io::Error> {
    let mut sub = node.children_with_tokens().peekable();
    let _ok = whitespace_to(&mut sub, SyntaxKind::Identifier, writer, state, "")?
        && whitespace_to(&mut sub, SyntaxKind::DeclaredIdentifier, writer, state, " ")?
        && whitespace_to(&mut sub, SyntaxKind::FatArrow, writer, state, " ")?
        && whitespace_to(&mut sub, SyntaxKind::CodeBlock, writer, state, " ")?;
    for n in sub {
        fold(n, writer, state)?;
    }
    state.new_line();
    Ok(())
}

fn format_member_access(
    node: &SyntaxNode,
    writer: &mut impl TokenWriter,
    state: &mut FormatState,
) -> Result<(), std::io::Error> {
    let n = syntax_nodes::MemberAccess::from(node.clone());
    // Special case of things like `42 .mod(x)` where a space is needed otherwise it lexes differently
    let need_space = n.Expression().child_token(SyntaxKind::NumberLiteral).is_some_and(|nl| {
        !nl.text().contains('.') && nl.text().chars().last().is_some_and(|c| c.is_numeric())
    });
    let space_before_dot = if need_space { " " } else { "" };
    let mut sub = n.children_with_tokens();
    let _ok = whitespace_to(&mut sub, SyntaxKind::Expression, writer, state, "")?
        && whitespace_to(&mut sub, SyntaxKind::Dot, writer, state, space_before_dot)?
        && whitespace_to(&mut sub, SyntaxKind::Identifier, writer, state, "")?;
    for n in sub {
        fold(n, writer, state)?;
    }
    Ok(())
}

/// Formats an import specifier.
///
/// This catches the whole import line (or multiline), and it checks its size only,
/// to later delegate to import_identifier
///
/// Given this line:
/// ```slint
/// import { foo, bar, } from 'module';
/// ```
///
/// It calculates the size of the line and passes the identifier list to the next formatter:
/// ```slint
/// { foo, bar, }
/// ```
///
/// The formatter will then execute the code, and in this case, output:
/// ```slint
/// {
///     foo,
///     bar,
/// }
/// ```
fn format_import_specifier(
    node: &SyntaxNode,
    writer: &mut impl TokenWriter,
    state: &mut FormatState,
) -> Result<(), std::io::Error> {
    let is_too_long = node.text().len() > 80.into();

    for n in node.children_with_tokens() {
        match n.kind() {
            SyntaxKind::ImportIdentifierList => {
                if let NodeOrToken::Node(n) = n {
                    format_import_identifier(&n, writer, state, is_too_long)?
                };
            }
            _ => {
                fold(n, writer, state)?;
            }
        }
    }

    Ok(())
}

fn format_closure(
    node: &SyntaxNode,
    writer: &mut impl TokenWriter,
    state: &mut FormatState,
) -> Result<(), std::io::Error> {
    for s in node.children_with_tokens() {
        state.skip_all_whitespace = true;
        match s.kind() {
            SyntaxKind::FatArrow => {
                state.insert_whitespace(" ");
                fold(s, writer, state)?;
                state.insert_whitespace(" ");
            }
            _ => fold(s, writer, state)?,
        }
    }

    Ok(())
}

/// Format import list
///
/// If the line is too long, it formats the import identifier in a new line, unless
/// there's a comment or the import has a trailing comma.
///
/// If the line is short, it just adds the spaces around the import identifier, unless
/// the import has a trailing comma.
fn format_import_identifier(
    node: &SyntaxNode,
    writer: &mut impl TokenWriter,
    state: &mut FormatState,
    is_too_long: bool,
) -> Result<(), std::io::Error> {
    let count_commas = node
        .children_with_tokens()
        .filter(|child_token| child_token.kind() == SyntaxKind::Comma)
        .count();
    let mut count_import_identifiers = node
        .children_with_tokens()
        .filter(|child_token| child_token.kind() == SyntaxKind::ImportIdentifier)
        .count();
    let has_comment =
        node.children_with_tokens().any(|child_token| child_token.kind() == SyntaxKind::Comment);
    let has_trailing_comma = count_commas == count_import_identifiers;
    let sub = node.children_with_tokens();

    // With stay_single_line we control whether to split the import statement or not
    let stay_single_line = !is_too_long && !has_trailing_comma && !has_comment;

    let indentation_level = if stay_single_line { 0 } else { 1 };
    for n in sub {
        state.skip_all_whitespace = true;
        match n.kind() {
            SyntaxKind::Whitespace => {
                fold(n, writer, state)?;
            }
            SyntaxKind::LBrace => {
                state.indentation_level += indentation_level;
                fold(n, writer, state)?;
            }
            SyntaxKind::ImportIdentifier => {
                count_import_identifiers -= 1;
                let is_last_import = count_import_identifiers == 0;
                if let Some(children) = n.as_node() {
                    let has_internal_name = children
                        .children_with_tokens()
                        .any(|child| child.kind() == SyntaxKind::InternalName);
                    for nn in children.children_with_tokens() {
                        state.skip_all_whitespace = true;
                        match nn.kind() {
                            SyntaxKind::Whitespace => {
                                fold(nn, writer, state)?;
                            }
                            SyntaxKind::ExternalName => {
                                if stay_single_line {
                                    state.insert_whitespace(" ");
                                } else {
                                    state.new_line();
                                }
                                fold(nn, writer, state)?;

                                // We don't want to add a comma after the
                                // external name if there is an internal name
                                if has_internal_name {
                                    continue;
                                }
                                if !stay_single_line && !has_trailing_comma && is_last_import {
                                    writer.insert_content(",")?;
                                }
                            }
                            SyntaxKind::InternalName => {
                                state.whitespace_to_add = Some(" ".into());
                                fold(nn, writer, state)?;

                                // Internal name is optional, and, if present,
                                // we know that the comma in external has been skipped,
                                // so we can add if needed
                                if !stay_single_line && !has_trailing_comma && is_last_import {
                                    writer.insert_content(",")?;
                                }
                            }
                            _ => {
                                state.whitespace_to_add = Some(" ".into());
                                fold(nn, writer, state)?;
                            }
                        }
                    }
                }
            }
            SyntaxKind::RBrace => {
                state.indentation_level -= indentation_level;
                if !stay_single_line {
                    state.new_line();
                } else {
                    state.insert_whitespace(" ");
                }
                fold(n, writer, state)?;
            }
            _ => {
                state.skip_all_whitespace = true;
                fold(n, writer, state)?;
            }
        }
    }
    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::fmt::writer::FileWriter;
    use i_slint_compiler::diagnostics::BuildDiagnostics;

    // FIXME more descriptive errors when an assertion fails
    #[track_caller]
    fn assert_formatting(unformatted: &str, formatted: &str) {
        // Parse the unformatted string
        let syntax_node = i_slint_compiler::parser::parse(
            String::from(unformatted),
            None,
            &mut BuildDiagnostics::default(),
        );
        // Turn the syntax node into a document
        let doc = syntax_nodes::Document::new(syntax_node).unwrap();
        let mut file = Vec::new();
        format_document(doc, &mut FileWriter { file: &mut file }).unwrap();
        let first_pass = String::from_utf8(file).unwrap();
        assert_eq!(first_pass, formatted);

        let syntax_node = i_slint_compiler::parser::parse(
            first_pass.clone(),
            None,
            &mut BuildDiagnostics::default(),
        );
        let doc = syntax_nodes::Document::new(syntax_node).unwrap();
        let mut file = Vec::new();
        format_document(doc, &mut FileWriter { file: &mut file }).unwrap();
        assert_eq!(String::from_utf8(file).unwrap(), first_pass);
    }

    #[test]
    fn basic_formatting() {
        assert_formatting("A:=Text{}", "A := Text { }\n");
    }

    #[test]
    fn components() {
        assert_formatting(
            "component   A   {}  export component  B  inherits  Text {  }",
            "component A { }\n\nexport component B inherits Text { }\n",
        );
    }

    #[test]
    fn with_comments() {
        assert_formatting(
            r#"component /* */ Foo // aaa
            inherits  /* x */  // bbb
            Window // ccc
            /*y*/ {   // c
              Foo /*aa*/ /*bb*/ {}
            }
        "#,
            r#"component /* */ Foo // aaa
            inherits  /* x */  // bbb
            Window // ccc
            /*y*/ {   // c
              Foo /*aa*/ /*bb*/ { }
}
"#,
        );

        assert_formatting(
            r#"//xxx
component  C1 {
    // before a property
    property< int> p1;
    property<int > p2; // on the same line of a property
    property <int > p3;
    // After a property
    // ...
}
"#,
            r#"//xxx
component C1 {
    // before a property
    property <int> p1;
    property <int> p2; // on the same line of a property
    property <int> p3;
    // After a property
    // ...
}
"#,
        );
    }

    #[test]
    fn complex_formatting() {
        assert_formatting(
            r#"
Main :=Window{callback some-fn(string,string)->bool;some-fn(a, b)=>{a<=b} property<bool>prop-x;
    VerticalBox {      combo:=ComboBox{}    }
    pure   callback   some-fn  ({x: int},string);  in property <  int >  foo: 42; }
            "#,
            r#"
Main := Window {
    callback some-fn(string, string) -> bool;
    some-fn(a, b) => { a <= b }
    property <bool> prop-x;
    VerticalBox {
        combo := ComboBox { }
    }

    pure callback some-fn({x: int}, string);
    in property <int> foo: 42;
}
"#,
        );
    }

    #[test]
    fn deprecated_property() {
        assert_formatting(
            r#"
component W {
    in-out property <int> new-prop;
    @deprecated  in-out    property   <int>   old-prop   <=>   new-prop;
    @deprecated

        in-out property <int> spaced-prop <=> new-prop;
    @deprecated (   "Use 'new-prop' instead"   )   in-out property <int> older-prop <=> new-prop;
    @deprecated("msg")in-out property<int>compact<=>new-prop;
}
"#,
            r#"
component W {
    in-out property <int> new-prop;
    @deprecated in-out property <int> old-prop <=> new-prop;
    @deprecated in-out property <int> spaced-prop <=> new-prop;
    @deprecated("Use 'new-prop' instead") in-out property <int> older-prop <=> new-prop;
    @deprecated("msg") in-out property <int> compact <=> new-prop;
}
"#,
        );
    }

    #[test]
    fn callback_declaration() {
        assert_formatting(
            r#"
component W inherits Window{
    callback hello( with-name :int , { x: int, y: float} , foo: string );
    callback world   (  )  -> string;
    callback another_callback ;
}
"#,
            r#"
component W inherits Window {
    callback hello(with-name: int, { x: int, y: float }, foo: string);
    callback world() -> string;
    callback another_callback;
}
"#,
        );
    }

    #[test]
    fn parent_access() {
        assert_formatting(
            r#"
Main := Parent{            Child{
    some-prop: parent.foo - 60px;
}}"#,
            r#"
Main := Parent {
    Child {
        some-prop: parent.foo - 60px;
    }
}
"#,
        );
    }

    #[test]
    fn space_with_braces() {
        assert_formatting("Main := Window{}", "Main := Window { }\n");
        // Also in a child
        assert_formatting(
            r#"
Main := Window{Child{}}"#,
            r#"
Main := Window {
    Child { }
}
"#,
        );
        assert_formatting(
            r#"
Main := VerticalLayout{HorizontalLayout{prop:3;}}"#,
            r#"
Main := VerticalLayout {
    HorizontalLayout {
        prop: 3;
    }
}
"#,
        );
    }

    #[test]
    fn binary_expressions() {
        assert_formatting(
            r#"
Main := Some{
    a:3+2;  b:4-7;  c:3*7;  d:3/9;
    e:3==4; f:3!=4; g:3<4;  h:3<=4;
    i:3>4;  j:3>=4; k:3&&4; l:3||4;
}"#,
            r#"
Main := Some {
    a: 3 + 2;
    b: 4 - 7;
    c: 3 * 7;
    d: 3 / 9;
    e: 3 == 4;
    f: 3 != 4;
    g: 3 < 4;
    h: 3 <= 4;
    i: 3 > 4;
    j: 3 >= 4;
    k: 3 && 4;
    l: 3 || 4;
}
"#,
        );

        assert_formatting(
            r#"
Main := Some{
    m: 3 + 8;
    m:3 + 8;
    m: 3+ 8;
    m:3+ 8;
    m: 3 +8;
    m:3 +8;
    m: 3+8;
    m:3+8;
}"#,
            r#"
Main := Some {
    m: 3 + 8;
    m: 3 + 8;
    m: 3 + 8;
    m: 3 + 8;
    m: 3 + 8;
    m: 3 + 8;
    m: 3 + 8;
    m: 3 + 8;
}
"#,
        );
    }

    #[test]
    fn file_with_an_import() {
        assert_formatting(
            r#"
import { Some } from "./here.slint";

A := Some{    padding-left: 10px;    Text{        x: 3px;    }}"#,
            r#"
import { Some } from "./here.slint";

A := Some {
    padding-left: 10px;
    Text {
        x: 3px;
    }
}
"#,
        );
    }

    #[test]
    fn children() {
        // Regression test - children was causing additional newlines
        assert_formatting(
            r#"
A := B {
    C {
        @children
    }
}"#,
            r#"
A := B {
    C {
        @children
    }
}
"#,
        );
    }

    #[test]
    fn for_in() {
        assert_formatting(
            r#"
A := B {  for c   in root.d: T  { e: c.attr; } }
        "#,
            r#"
A := B {
    for c in root.d: T {
        e: c.attr;
    }
}
"#,
        );
    }

    #[test]
    fn for_in_index() {
        // FIXME: there should not be a whitespace before [index]
        assert_formatting(
            r#"
A := B {
    for number   [  index
         ]  in [1,2,3]: C { d:number*index; }
}
        "#,
            r#"
A := B {
    for number[index] in [1, 2, 3]: C {
        d: number * index;
    }
}
"#,
        );
    }

    #[test]
    fn if_element() {
        assert_formatting(
            r#"
component A {  if condition : Text {  }  }
        "#,
            r#"
component A {
    if condition: Text { }
}
"#,
        );
    }

    #[test]
    fn array() {
        assert_formatting(
            r#"
A := B { c: [1,2,3]; }
"#,
            r#"
A := B {
    c: [1, 2, 3];
}
"#,
        );
        assert_formatting(
            r#"
A := B { c: [    1    ]; }
"#,
            r#"
A := B {
    c: [1];
}
"#,
        );
        assert_formatting(
            r#"
A := B { c:   [    ]  ; }
"#,
            r#"
A := B {
    c: [];
}
"#,
        );
        assert_formatting(
            r#"
A := B { c:   [

1,

2

  ]  ; }
"#,
            r#"
A := B {
    c: [1, 2];
}
"#,
        );
    }

    #[test]
    fn states() {
        assert_formatting(
            r#"
component FooBar {
    states [
        dummy1    when    a == true   :{

        }
    ]
}
"#,
            r#"
component FooBar {
    states [
        dummy1 when a == true: {}
    ]
}
"#,
        );

        assert_formatting(
            r#"
component ABC {
    in-out property <bool> b: false;
    in-out property <int> a: 1;
    states[
        is-selected when root.b == root.b: {
            b:false;
        root.a:1;
        }
        is-not-selected when root.b!=root.b: {
            root.a: 1;
        }
    ]    foo := Rectangle { }
}
"#,
            r#"
component ABC {
    in-out property <bool> b: false;
    in-out property <int> a: 1;
    states [
        is-selected when root.b == root.b: {
            b: false;
            root.a: 1;
        }
        is-not-selected when root.b != root.b: {
            root.a: 1;
        }
    ]
    foo := Rectangle { }
}
"#,
        );
    }

    #[test]
    fn state_issue_4850() {
        // #4850
        assert_formatting(
            "export component LspCrashMvp { states [ active: { } inactive: { } ] }",
            r#"export component LspCrashMvp {
    states [
        active: {}
        inactive: {}
    ]
}
"#,
        );
    }

    #[test]
    fn states_transitions() {
        assert_formatting(
            r#"
component FooBar {
    states [
        //comment
        s1 when true: {
    vv: 0;  in { animate vv {  duration: 400ms; }}  out { animate /*...*/  vv { duration:    400ms;   } animate dd { duration: 100ms+400ms ;  easing: ease-out; }   }  }
    ]
}
"#,
            r#"
component FooBar {
    states [
        //comment
        s1 when true: {
            vv: 0;
            in {
                animate vv { duration: 400ms; }
            }
            out {
                animate /*...*/  vv { duration: 400ms; }
                animate dd {
                    duration: 100ms + 400ms;
                    easing: ease-out;
                }
            }
        }
    ]
}
"#,
        );

        assert_formatting(
            "component FooBar {states[foo:{in{animate x{duration:1ms;}}x:0;}]}",
            r"component FooBar {
    states [
        foo: {
            in {
                animate x { duration: 1ms; }
            }
            x: 0;
        }
    ]
}
",
        );
    }

    #[test]
    fn if_else() {
        assert_formatting(
            r#"
A := B { c: true  ||false?  45 + 6:34+ 1; }
"#,
            r#"
A := B {
    c: true || false ? 45 + 6 : 34 + 1;
}
"#,
        );
        assert_formatting(
            r#"A := B { c => { if(!abc){nothing}else   if  (true){if (0== 8) {}    } else{  } } } "#,
            r#"A := B {
    c => { if (!abc) { nothing } else if (true) { if (0 == 8) {} } else {} }
}
"#,
        );
        assert_formatting(
            r#"A := B { c => { if !abc{nothing}else   if  true{if 0== 8 {}    } else{  } } } "#,
            r#"A := B {
    c => { if !abc { nothing } else if true { if 0 == 8 {} } else {} }
}
"#,
        );

        assert_formatting(
            "component A { c => { if( a == 1 ){b+=1;} else if (a==2)\n{b+=2;} else if a==3{\nb+=3;\n} else\n if(a==4){ a+=4} return 0;  } }",
            r"component A {
    c => {
        if (a == 1) { b += 1; } else if (a == 2) { b += 2; } else if a == 3 {
            b += 3;
        } else if (a == 4) { a += 4 }
        return 0;
    }
}
",
        );
    }

    #[test]
    fn code_block() {
        assert_formatting(
            r#"
component ABC {
    in-out property <bool> logged_in: false;
    function clicked() -> bool {
        if (logged_in) { foo();
            logged_in = false;
        return
        true;
        } else {
            logged_in = false; return false;
        }
    }
}
"#,
            r#"
component ABC {
    in-out property <bool> logged_in: false;
    function clicked() -> bool {
        if (logged_in) {
            foo();
            logged_in = false;
            return true;
        } else {
            logged_in = false;
            return false;
        }
    }
}
"#,
        );
    }

    #[test]
    fn trailing_comma_array() {
        assert_formatting(
            r#"
component ABC {
    in-out property <[int]> ar: [1, ];
    in-out property <[int]> ar: [1, 2, 3, 4, 5,];
    in-out property <[int]> ar2: [1, 2, 3, 4, 5];
}
"#,
            r#"
component ABC {
    in-out property <[int]> ar: [
        1,
    ];
    in-out property <[int]> ar: [
        1,
        2,
        3,
        4,
        5,
    ];
    in-out property <[int]> ar2: [1, 2, 3, 4, 5];
}
"#,
        );
    }

    #[test]
    fn large_array() {
        assert_formatting(
            r#"
component ABC {
    in-out property <[string]> large: ["first string", "second string", "third string", "fourth string", "fifth string"];
    in property <[int]> model: [
                                    1,
                                    2
    ];
}
"#,
            r#"
component ABC {
    in-out property <[string]> large: [
        "first string",
        "second string",
        "third string",
        "fourth string",
        "fifth string"
    ];
    in property <[int]> model: [1, 2];
}
"#,
        );
    }

    #[test]
    fn property_animation() {
        assert_formatting(
            r#"
export component MainWindow inherits Window {
    animate background { duration: 800ms;}
    animate x { duration: 100ms; easing: ease-out-bounce; }
    Rectangle {}
}
"#,
            r#"
export component MainWindow inherits Window {
    animate background { duration: 800ms; }
    animate x {
        duration: 100ms;
        easing: ease-out-bounce;
    }
    Rectangle { }
}
"#,
        );
    }

    #[test]
    fn object_literal() {
        assert_formatting(
            r#"
export component MainWindow inherits Window {
    in property <[TileData]> memory-tiles : [
        { image: @image-url("icons/at.png"), image-visible: false, solved: false, },
        { image: @image-url("icons/at.png"), image-visible: false, solved: false,},
        { image: @image-url("icons/at.png"), image-visible: false, solved: false, some_other_property: 12345 },
        { image: @image-url("icons/at.png"), image-visible: false, solved: false, some_other_property: 12345},
        { image: @image-url("icons/balance-scale.png") },
    ];
}
"#,
            r#"
export component MainWindow inherits Window {
    in property <[TileData]> memory-tiles: [
        {
            image: @image-url("icons/at.png"),
            image-visible: false,
            solved: false,
        },
        {
            image: @image-url("icons/at.png"),
            image-visible: false,
            solved: false,
        },
        {
            image: @image-url("icons/at.png"),
            image-visible: false,
            solved: false,
            some_other_property: 12345
        },
        {
            image: @image-url("icons/at.png"),
            image-visible: false,
            solved: false,
            some_other_property: 12345
        },
        { image: @image-url("icons/balance-scale.png") },
    ];
}
"#,
        );
    }

    #[test]
    fn struct_declaration() {
        assert_formatting(
            r#"
export struct ParsedMarkdown {  string :string , span:{start:int,end:int,}, }
"#,
            r#"
export struct ParsedMarkdown {
    string: string,
    span: {
        start: int,
        end: int,
    },
}
"#,
        );

        assert_formatting(
            r#"
struct ParsedMarkdown {  string :string , span:{start: int, end:int} , x: int }
"#,
            r#"
struct ParsedMarkdown { string: string, span: { start: int, end: int }, x: int }
"#,
        );

        // issue 10647
        assert_formatting(
            r#"
struct PrinterQueueItem {
    status: string,
    progress: int,
    title: string,
    owner: string,
    pages: int,
    size: string, // number instead and format in .slint?
    submission-date: string}

            "#,
            r#"
struct PrinterQueueItem {
    status: string,
    progress: int,
    title: string,
    owner: string,
    pages: int,
    size: string, // number instead and format in .slint?
    submission-date: string
}
"#,
        );
    }

    #[test]
    fn struct_declaration_preserves_comments_and_top_level_spacing() {
        assert_formatting(
            r#"
export struct AttributeData {
    key: string,
    key-error: string,
    type: int,
    value: string,
    value-valid: bool,
    unit: int,
    action: int,  // Some comment
}

export struct LineEditData {
    enabled: bool,
    text: string,
    placeholder: string,
    suggestions: [string],
}
"#,
            r#"
export struct AttributeData {
    key: string,
    key-error: string,
    type: int,
    value: string,
    value-valid: bool,
    unit: int,
    action: int,  // Some comment
}

export struct LineEditData {
    enabled: bool,
    text: string,
    placeholder: string,
    suggestions: [string],
}
"#,
        );

        assert_formatting(
            r#"
export struct AttributeData {
    action: int,  // Some comment
}

export struct LineEditData {
    enabled: bool,
    text: string,
    placeholder: string,
    suggestions: [string],
}
"#,
            r#"
export struct AttributeData {
    action: int,  // Some comment
}

export struct LineEditData {
    enabled: bool,
    text: string,
    placeholder: string,
    suggestions: [string],
}
"#,
        );

        assert_formatting(
            r#"
export struct AttributeData {
    key: string, // Set by UI, reset by backend
    key-error: string,
    type: int,
}

export struct LineEditData {
    enabled: bool,
    text: string,
    placeholder: string,
    suggestions: [string],
}
"#,
            r#"
export struct AttributeData {
    key: string, // Set by UI, reset by backend
    key-error: string,
    type: int,
}

export struct LineEditData {
    enabled: bool,
    text: string,
    placeholder: string,
    suggestions: [string],
}
"#,
        );
    }

    #[test]
    fn multiline_binary_expression() {
        assert_formatting(
            "component A {\n    x: a && b && c;\n}\n",
            "component A {\n    x: a && b && c;\n}\n",
        );
        assert_formatting(
            "component A {\n    x: a &&\nb &&\nc;\n}\n",
            "component A {\n    x: a &&\n        b &&\n        c;\n}\n",
        );
        assert_formatting(
            "component A {\n    x: a\n&& b\n&& c;\n}\n",
            "component A {\n    x: a\n        && b\n        && c;\n}\n",
        );
    }

    #[test]
    fn multiline_ternary_expression() {
        assert_formatting(
            "component A {\n    x: a ? b : c;\n}\n",
            "component A {\n    x: a ? b : c;\n}\n",
        );
        assert_formatting(
            "component A {\n    x: a\n? b\n: c;\n}\n",
            "component A {\n    x: a\n        ? b\n        : c;\n}\n",
        );
        assert_formatting(
            "component A {\n    x: cond1 ? 45\n        : cond2 ? 89\n        : 32;\n}\n",
            "component A {\n    x: cond1 ? 45\n        : cond2 ? 89\n        : 32;\n}\n",
        );
    }

    #[test]
    fn enum_declaration() {
        assert_formatting("enum Foo {  a,   b,   c }\n", "enum Foo { a, b, c }\n");
        assert_formatting("export enum Foo {  a,   b,   c }\n", "export enum Foo { a, b, c }\n");
        assert_formatting("enum Foo { a, b, c, }\n", "enum Foo {\n    a,\n    b,\n    c,\n}\n");
    }

    #[test]
    fn export_list() {
        assert_formatting("export {Foo,Bar}\n", "export { Foo, Bar }\n");
        assert_formatting("export {Foo  as   Bar}\n", "export { Foo as Bar }\n");
        assert_formatting(
            "export { SuperLongTypeName, AnotherVeryLongTypeName, YetAnotherExtremelyLongTypeName }\n",
            "export {\n    SuperLongTypeName,\n    AnotherVeryLongTypeName,\n    YetAnotherExtremelyLongTypeName,\n}\n",
        );
        assert_formatting("export { Foo, }\n", "export {\n    Foo,\n}\n");
    }

    #[test]
    fn preserve_empty_lines() {
        assert_formatting(
            r#"
export component MainWindow inherits Rectangle {
    in property <bool> open-curtain;
    callback clicked;

    border-radius: 8px;


    animate background { duration: 800ms; }

    Image {
        y: 8px;
    }


    Image {
        y: 8px;
    }
}
"#,
            r#"
export component MainWindow inherits Rectangle {
    in property <bool> open-curtain;
    callback clicked;

    border-radius: 8px;

    animate background { duration: 800ms; }

    Image {
        y: 8px;
    }

    Image {
        y: 8px;
    }
}
"#,
        );
    }

    #[test]
    fn preserve_empty_lines_between_top_level_declarations() {
        assert_formatting(
            r#"
struct Foo {}

struct Bar {
    dummy: int,
}

component HelloWorld {
    // ...
}
"#,
            r#"
struct Foo {}

struct Bar {
    dummy: int,
}

component HelloWorld {
    // ...
}
"#,
        );
    }

    #[test]
    fn struct_field_default_values() {
        assert_formatting(
            r#"
struct Foo {
    a: int=42,
    b: string    =   "hello",
    c: color= #ff0000,
    d: int,
}
"#,
            r#"
struct Foo {
    a: int = 42,
    b: string = "hello",
    c: color = #ff0000,
    d: int,
}
"#,
        );
    }

    #[test]
    fn preserve_top_level_comment_spacing() {
        assert_formatting(
            r#"
component RoundButton inherits Image {
    property <int> x;
}
// From UpAndDownButton.cpp
component UpAndDownButton inherits Rectangle {
    callback changed(int);
}
"#,
            r#"
component RoundButton inherits Image {
    property <int> x;
}
// From UpAndDownButton.cpp
component UpAndDownButton inherits Rectangle {
    callback changed(int);
}
"#,
        );
    }

    #[test]
    fn preserve_blank_line_before_top_level_comment() {
        assert_formatting(
            r#"
component RoundButton inherits Image {
    callback clicked;
}

// From UpAndDownButton.cpp
component UpAndDownButton inherits Rectangle {
    callback changed(int);
}
"#,
            r#"
component RoundButton inherits Image {
    callback clicked;
}

// From UpAndDownButton.cpp
component UpAndDownButton inherits Rectangle {
    callback changed(int);
}
"#,
        );
    }

    #[test]
    fn preserve_blank_lines_between_top_level_items() {
        assert_formatting(
            r#"
struct Palette {}

global Skin {
    // ...
}

export component Clock {
}
"#,
            r#"
struct Palette {}

global Skin {
    // ...
}

export component Clock { }
"#,
        );
    }

    #[test]
    fn multiple_property_animation() {
        assert_formatting(
            r#"
export component MainWindow inherits Rectangle {
    animate x , y { duration: 170ms; easing: cubic-bezier(0.17,0.76,0.4,1.75); }
    animate x , y { duration: 170ms;}
}
"#,
            r#"
export component MainWindow inherits Rectangle {
    animate x, y {
        duration: 170ms;
        easing: cubic-bezier(0.17,0.76,0.4,1.75);
    }
    animate x, y { duration: 170ms; }
}
"#,
        );
    }

    #[test]
    fn empty_array() {
        assert_formatting(
            r#"
export component MainWindow2 inherits Rectangle {
    in property <[string]> model: [ ];
}
"#,
            r#"
export component MainWindow2 inherits Rectangle {
    in property <[string]> model: [];
}
"#,
        );
    }

    #[test]
    fn two_way_binding() {
        assert_formatting(
            "export component Foobar{foo<=>xx.bar ; property<int\n>xx   <=>   ff . mm  ; callback doo <=> moo\n;\nproperty  e-e<=>f-f; }",
            r#"export component Foobar {
    foo <=> xx.bar;
    property <int> xx <=> ff.mm;
    callback doo <=> moo;
    property e-e <=> f-f;
}
"#,
        );
    }

    #[test]
    fn implement_statement() {
        assert_formatting(
            "export component Foobar{implement   Foo<=>self ;}",
            r#"export component Foobar {
    implement Foo <=> self;
}
"#,
        );
    }

    #[test]
    fn callback_connection() {
        assert_formatting(
            "export component Foobar{ init=>{  debug (1 );} \n\nfoo=>{}  clicked =>   debug(2) ; TouchArea { clicked => root.clicked(); moved=>{debug(3)};\n\n//some comment\n        bar=>{} }  }",
            r#"export component Foobar {
    init => { debug(1); }

    foo => {}
    clicked => debug(2);
    TouchArea {
        clicked => root.clicked();
        moved => { debug(3) };

        //some comment
        bar => {}
    }
}
"#,
        );
    }

    #[test]
    fn code_block_single_line() {
        // A code block that fits on one line in the source stays on one line
        assert_formatting(
            "component A { c => { root.foo = true; } click2 => {} f() => { a; b; c; } }\n",
            r#"component A {
    c => { root.foo = true; }
    click2 => {}
    f() => { a; b; c; }
}
"#,
        );
        // A code block with a newline in the source is expanded
        assert_formatting(
            "component A {\n  c => {\n    root.foo = true;\n  }\n}\n",
            r#"component A {
    c => {
        root.foo = true;
    }
}
"#,
        );
    }

    #[test]
    fn function() {
        assert_formatting(
            "export component Foo-bar{ pure\nfunction\n(x  :  int,y:string)->int{ self.y=0;\n\nif(true){return(45); a=0;} return x;  } function a(){/* ddd */}}",
            r#"export component Foo-bar {
    pure function (x: int, y: string) -> int {
        self.y = 0;

        if (true) { return (45); a = 0; }
        return x;
    }
    function a() { /* ddd */ }
}
"#,
        );
    }

    #[test]
    fn changed() {
        assert_formatting(
            "component X { changed   width=>{ x+=1;  }    changed/*-*/height     =>     {y+=1;} }",
            r#"component X {
    changed width => { x += 1; }
    changed /*-*/height => { y += 1; }
}
"#,
        );
    }

    #[test]
    fn access_member() {
        assert_formatting(
            "component X { expr: 42   .log(x) + 41 . log(y) + foo . bar +  21.0.log(0) + 54.   .log(8) ; x: 42px.max(42px . min (0.px)); }",
            r#"component X {
    expr: 42 .log(x) + 41 .log(y) + foo.bar + 21.0.log(0) + 54..log(8);
    x: 42px.max(42px.min(0.px));
}
"#,
        );
    }

    #[test]
    fn let_statement() {
        assert_formatting(
            "component X { function foo() { let bar=42; } }",
            r#"component X {
    function foo() { let bar = 42; }
}
"#,
        );
    }

    #[test]
    fn let_statement_type_annotation() {
        assert_formatting(
            "component X { function foo() { let bar : int=42; } }",
            r#"component X {
    function foo() { let bar: int = 42; }
}
"#,
        );
    }

    #[test]
    fn comment_in_nest() {
        assert_formatting(
            r#"component X {
    // function foo() {
    // }
    }
"#,
            r#"component X {
    // function foo() {
    // }
}
"#,
        );
    }

    #[test]
    fn closure() {
        assert_formatting(
            "component X { property <[int]> arr: [1, 2, 3, 4, 5]; function foo() { arr.any((x)\n     =>   x         ==  1     ); } }",
            r#"component X {
    property <[int]> arr: [1, 2, 3, 4, 5];
    function foo() {
        arr.any((x) => x == 1);
    }
}
"#,
        );
    }

    // cspell:disable
    #[test]
    fn import_line_too_long() {
        assert_formatting(
            r#"import { SuperFooooooooooooooooooooooooooooooooooooooooooooooooo } from "./here.slint";"#,
            r#"import {
    SuperFooooooooooooooooooooooooooooooooooooooooooooooooo,
} from "./here.slint";"#,
        );
    }

    #[test]
    fn import_line_is_too_long_because_of_trailing_spaces_but_no_format_happens() {
        // In this case the format should not happen
        assert_formatting(
            r#"import { SuperFoooooooooooooooooooooooooo } from "./here.slint;                     "#,
            r#"import { SuperFoooooooooooooooooooooooooo } from "./here.slint;                     "#,
        );
    }

    #[test]
    fn single_import_space() {
        assert_formatting(
            r#"import {Foo} from "./here.slint";"#,
            r#"import { Foo } from "./here.slint";"#,
        );

        assert_formatting(
            r#"import { Foo} from "./here.slint";"#,
            r#"import { Foo } from "./here.slint";"#,
        );

        assert_formatting(
            r#"import {Foo } from "./here.slint";"#,
            r#"import { Foo } from "./here.slint";"#,
        );

        assert_formatting(
            r#"import {     Foo     } from "./here.slint";"#,
            r#"import { Foo } from "./here.slint";"#,
        );

        assert_formatting(
            r#"import {Foo as FooBar } from "./here.slint";"#,
            r#"import { Foo as FooBar } from "./here.slint";"#,
        );

        assert_formatting(
            r#"import {Foo as FooBar} from "./here.slint";"#,
            r#"import { Foo as FooBar } from "./here.slint";"#,
        );

        assert_formatting(
            r#"import {Foooooooooooooooooooooooooooooooo as FooooooooooooooooooooooooooooooooBar} from "./here.slint";"#,
            r#"import {
    Foooooooooooooooooooooooooooooooo as FooooooooooooooooooooooooooooooooBar,
} from "./here.slint";"#,
        );
    }

    // cspell:enable

    #[test]
    /// format_import_identifier
    fn import_comma_new_line() {
        // when user adds a trail comma, it goes to a new line automatically, good for git from the get go
        assert_formatting(
            r#"import {Foo,} from "./here.slint";"#,
            r#"import {
    Foo,
} from "./here.slint";"#,
        );

        assert_formatting(
            r#"import {  Foo,} from "./here.slint";"#,
            r#"import {
    Foo,
} from "./here.slint";"#,
        );

        assert_formatting(
            r#"import {Foo,  } from "./here.slint";"#,
            r#"import {
    Foo,
} from "./here.slint";"#,
        );

        assert_formatting(
            r#"import {Foo as Fur,  } from "./here.slint";"#,
            r#"import {
    Foo as Fur,
} from "./here.slint";"#,
        );
    }

    #[test]
    /// format_import_identifier
    fn multiple_imports_behavior() {
        assert_formatting(
            r#"import {Foo, Bar} from "./here.slint";"#,
            r#"import { Foo, Bar } from "./here.slint";"#,
        );

        assert_formatting(
            r#"import {Foo,Bar,} from "./here.slint";"#,
            r#"import {
    Foo,
    Bar,
} from "./here.slint";"#,
        );

        assert_formatting(
            r#"import {Foo,Bar  } from "./here.slint";"#,
            r#"import { Foo, Bar } from "./here.slint";"#,
        );

        assert_formatting(
            r#"import {Foo,Bar as BarBer } from "./here.slint";"#,
            r#"import { Foo, Bar as BarBer } from "./here.slint";"#,
        );

        assert_formatting(
            r#"import { Foo, Bar} from "./here.slint";"#,
            r#"import { Foo, Bar } from "./here.slint";"#,
        );

        assert_formatting(
            r#"import {
    Foo,
    Bar
} from "./here.slint";"#,
            r#"import { Foo, Bar } from "./here.slint";"#,
        );

        assert_formatting(
            r#"import {
    Foo as Fur,
    Bar
} from "./here.slint";"#,
            r#"import { Foo as Fur, Bar } from "./here.slint";"#,
        );
    }

    #[test]
    fn import_new_line_with_comments() {
        assert_formatting(
            r#"import {
    Foo, // comment foo
    Bar   // comment bar
} from "./here.slint";"#,
            r#"import {
    Foo, // comment foo
    Bar,   // comment bar
} from "./here.slint";"#,
        );

        assert_formatting(
            r#"import {
    Foo, // comment foo
    Bar,   // comment bar
} from "./here.slint";"#,
            r#"import {
    Foo, // comment foo
    Bar,   // comment bar
} from "./here.slint";"#,
        );

        assert_formatting(
            r#"import {
    Foo, // comment foo
    Bar as BarBer   // comment bar
} from "./here.slint";"#,
            r#"import {
    Foo, // comment foo
    Bar as BarBer,   // comment bar
} from "./here.slint";"#,
        );
    }

    #[test]
    fn import_many() {
        assert_formatting(
            r#"import {
            Baz, Quz,
    Foo, // comment foo
    Bar as BarBer,   // comment bar
    Snaf,Tar, // comment
    Jar
} from "./here.slint";"#,
            r#"import {
    Baz,
    Quz,
    Foo, // comment foo
    Bar as BarBer,   // comment bar
    Snaf,
    Tar, // comment
    Jar,
} from "./here.slint";"#,
        );

        assert_formatting(
            r#"import { Foo, Bar as BarBer, Quz, Baz, Snaf, Tatta, Tar, Jar } from "./here.slint";"#,
            r#"import {
    Foo,
    Bar as BarBer,
    Quz,
    Baz,
    Snaf,
    Tatta,
    Tar,
    Jar,
} from "./here.slint";"#,
        );
    }

    #[test]
    fn nested_closure() {
        assert_formatting(
            "component X { property <[[int]]> arr: [[1, 2, 3, 4, 5]]; function foo() { arr.any((x)     =>   x.all((y)   => y   ==  7     )      ); } }",
            r#"component X {
    property <[[int]]> arr: [[1, 2, 3, 4, 5]];
    function foo() { arr.any((x) => x.all((y) => y == 7)); }
}
"#,
        );

        assert_formatting(
            "component X { property <[[{age: int}]]> groups: []; function foo() { groups.any((group)     =>   group.any((person)   => person.age   >  20     ) && group.length == 2      ); } }",
            r#"component X {
    property <[[{age: int}]]> groups: [];
    function foo() { groups.any((group) => group.any((person) => person.age > 20) && group.length == 2); }
}
"#,
        );
    }
}
