diff --git a/crates/air_r_formatter/src/comments.rs b/crates/air_r_formatter/src/comments.rs index fa77b1ed..60fbd62f 100644 --- a/crates/air_r_formatter/src/comments.rs +++ b/crates/air_r_formatter/src/comments.rs @@ -152,16 +152,55 @@ fn handle_empty_statement_comment( match comment.text_position() { CommentTextPosition::EndOfLine => match (preceding, following) { + (Some(preceding), Some(following)) => { + // Is the comment on the same line as the closest non-empty `preceding` node? + let is_on_preceding_line = comment.preceding_node().is_some_and(|preceding| { + preceding + .siblings(Direction::Prev) + .take_while(|sibling| REmptyStatement::can_cast(sibling.kind())) + .all(|sibling| !sibling.has_leading_newline()) + }); + + if is_on_preceding_line { + // ```r + // { + // 1; ; # Trails preceding + // 2 + // } + // ``` + CommentPlacement::trailing(preceding, comment) + } else { + // Leading keeps the relative ordering here: + // + // ```r + // { + // 1 + // # Leads following 1 + // ; # Leads following 2 + // 2 + // } + // ``` + // + // And generally also makes sense here where the `;` is + // simply removed and the comment doesn't move + // + // ```r + // { + // 1 + // ; # Leads following + // 2 + // } + // + // ``` + CommentPlacement::leading(following, comment) + } + } + // ```r // { - // 1 - // ; # Trails preceding - // 2 + // 1; ; # Trails preceding // } - // ``` - (Some(preceding), Some(_)) => CommentPlacement::trailing(preceding, comment), - - // ```r + // // { // 1 // ; # Trails preceding diff --git a/crates/air_r_formatter/src/r/auxiliary/empty_statement.rs b/crates/air_r_formatter/src/r/auxiliary/empty_statement.rs index 8bfdfc1f..0099e93f 100644 --- a/crates/air_r_formatter/src/r/auxiliary/empty_statement.rs +++ b/crates/air_r_formatter/src/r/auxiliary/empty_statement.rs @@ -527,6 +527,51 @@ mod test { Ok(()) } + #[test] + fn test_comments_around_lone_semicolon_keep_order() -> anyhow::Result<()> { + let source = "\ +{ + 1 + # a + ; # b + 2 +}"; + let expected = "\ +{ + 1 + # a + # b + 2 +} +"; + let tree = root([statement(braces( + token(L_CURLY, &[], "{", &[]), + [ + statement(double(token( + R_DOUBLE_LITERAL, + &[Newline, INDENT], + "1", + &[], + ))), + empty(token( + SEMICOLON, + &[Newline, INDENT, Comment("# a"), Newline, INDENT], + ";", + &[SPACE, Comment("# b")], + )), + statement(double(token( + R_DOUBLE_LITERAL, + &[Newline, INDENT], + "2", + &[], + ))), + ], + token(R_CURLY, &[Newline], "}", &[]), + ))]); + assert_eq!(format(tree, source)?, expected); + Ok(()) + } + /// Handles every variant of [crate::comments::handle_empty_statement_comment] #[test] fn test_handle_empty_statement_comment() -> anyhow::Result<()> { @@ -538,8 +583,37 @@ mod test { let source = "\ { - 1 - ; # Trails preceding + 1; ; # Trails preceding + 2 +}"; + let expected = "\ +{ + 1 # Trails preceding + 2 +} +"; + let tree = root([statement(braces( + token(L_CURLY, &[], "{", &[]), + [ + statement_with_semicolon( + double(token(R_DOUBLE_LITERAL, &[Newline, INDENT], "1", &[])), + token(SEMICOLON, &[], ";", &[]), + ), + empty(token(SEMICOLON, &[SPACE], ";", &[SPACE, TRAILS])), + statement(double(token( + R_DOUBLE_LITERAL, + &[Newline, INDENT], + "2", + &[], + ))), + ], + token(R_CURLY, &[Newline], "}", &[]), + ))]); + assert_eq!(format(tree, source)?, expected); + + let source = "\ +{ + 1; ; ; # Trails preceding 2 }"; let expected = "\ @@ -547,6 +621,39 @@ mod test { 1 # Trails preceding 2 } +"; + let tree = root([statement(braces( + token(L_CURLY, &[], "{", &[]), + [ + statement_with_semicolon( + double(token(R_DOUBLE_LITERAL, &[Newline, INDENT], "1", &[])), + token(SEMICOLON, &[], ";", &[]), + ), + empty(token(SEMICOLON, &[SPACE], ";", &[])), + empty(token(SEMICOLON, &[SPACE], ";", &[SPACE, TRAILS])), + statement(double(token( + R_DOUBLE_LITERAL, + &[Newline, INDENT], + "2", + &[], + ))), + ], + token(R_CURLY, &[Newline], "}", &[]), + ))]); + assert_eq!(format(tree, source)?, expected); + + let source = "\ +{ + 1 + ; ; # Leads following + 2 +}"; + let expected = "\ +{ + 1 + # Leads following + 2 +} "; let tree = root([statement(braces( token(L_CURLY, &[], "{", &[]), @@ -557,7 +664,42 @@ mod test { "1", &[], ))), - empty(token(SEMICOLON, &[Newline, INDENT], ";", &[SPACE, TRAILS])), + empty(token(SEMICOLON, &[Newline, INDENT], ";", &[])), + empty(token(SEMICOLON, &[SPACE], ";", &[SPACE, LEADS])), + statement(double(token( + R_DOUBLE_LITERAL, + &[Newline, INDENT], + "2", + &[], + ))), + ], + token(R_CURLY, &[Newline], "}", &[]), + ))]); + assert_eq!(format(tree, source)?, expected); + + let source = "\ +{ + 1 + ; # Leads following + 2 +}"; + let expected = "\ +{ + 1 + # Leads following + 2 +} +"; + let tree = root([statement(braces( + token(L_CURLY, &[], "{", &[]), + [ + statement(double(token( + R_DOUBLE_LITERAL, + &[Newline, INDENT], + "1", + &[], + ))), + empty(token(SEMICOLON, &[Newline, INDENT], ";", &[SPACE, LEADS])), statement(double(token( R_DOUBLE_LITERAL, &[Newline, INDENT],