@@ -408,6 +408,26 @@ impl LineBuilder {
408408 pub fn push_space ( & mut self ) {
409409 self . push_child ( LineComponent :: Space ) ;
410410 }
411+ /// Whether a separating space already effectively precedes a comment that is about to be
412+ /// appended, so that no extra space should be inserted before it. This is the case when:
413+ /// * a not-yet-flushed break line point already renders as a space when unbroken (inserting a
414+ /// space would produce a double space); or
415+ /// * the previous component is an explicit space; or
416+ /// * the previous component is itself a comment - inserting a space would prevent consecutive
417+ /// comment lines from being aggregated and reflowed together.
418+ fn comment_already_separated ( & mut self ) -> bool {
419+ let active_builder = self . get_active_builder_mut ( ) ;
420+ if matches ! (
421+ active_builder. pending_break_line_points. last( ) ,
422+ Some ( LineComponent :: BreakLinePoint ( properties) ) if properties. space_if_not_broken
423+ ) {
424+ return true ;
425+ }
426+ matches ! (
427+ active_builder. children. last( ) ,
428+ Some ( LineComponent :: Space | LineComponent :: Comment { .. } )
429+ )
430+ }
411431 /// Appends a user-inserted empty line to the line.
412432 pub fn push_empty_line_break_line_point ( & mut self ) {
413433 self . push_child ( LineComponent :: BreakLinePoint ( BreakLinePointProperties :: new_empty_line ( ) ) ) ;
@@ -945,6 +965,10 @@ pub struct FormatterImpl<'a> {
945965 is_current_line_whitespaces : bool ,
946966 /// Indicates whether the last element handled was a comment.
947967 is_last_element_comment : bool ,
968+ /// Indicates whether any token has been emitted yet. Used to avoid emitting a separating space
969+ /// before a comment that opens the file (it has nothing to be separated from, and the leading
970+ /// space would not be trimmed when there is no surrounding break line point).
971+ has_emitted_token : bool ,
948972}
949973impl < ' a > FormatterImpl < ' a > {
950974 pub fn new ( db : & ' a dyn Database , config : FormatterConfig ) -> Self {
@@ -955,6 +979,7 @@ impl<'a> FormatterImpl<'a> {
955979 empty_lines_allowance : 0 ,
956980 is_current_line_whitespaces : true ,
957981 is_last_element_comment : false ,
982+ has_emitted_token : false ,
958983 }
959984 }
960985 /// Gets a root of a syntax tree and returns the formatted string of the code it represents.
@@ -1227,7 +1252,17 @@ impl<'a> FormatterImpl<'a> {
12271252 ast:: Trivium :: SingleLineComment ( _)
12281253 | ast:: Trivium :: SingleLineDocComment ( _)
12291254 | ast:: Trivium :: SingleLineInnerComment ( _) => {
1230- if !is_leading {
1255+ // Emit a separating space before the comment unless one is already present. The
1256+ // line-breaking pass drops spaces at the start of a line, so this is a no-op
1257+ // for a comment that stays on its own line, but produces a
1258+ // consistent ` //` spacing when a (leading) comment is
1259+ // collapsed onto the previous line. Without this,
1260+ // such a collapsed comment is emitted with no space on the first formatting
1261+ // pass and a space on the second, making `cairo-format`
1262+ // non-idempotent.
1263+ if self . has_emitted_token
1264+ && !self . line_state . line_buffer . comment_already_separated ( )
1265+ {
12311266 self . line_state . line_buffer . push_space ( ) ;
12321267 }
12331268 self . line_state
@@ -1264,6 +1299,7 @@ impl<'a> FormatterImpl<'a> {
12641299 self . line_state . prevent_next_space = syntax_node. force_no_space_after ( self . db ) ;
12651300 if syntax_node. kind ( self . db ) != SyntaxKind :: TokenWhitespace {
12661301 self . is_current_line_whitespaces = false ;
1302+ self . has_emitted_token = true ;
12671303 }
12681304 let node_break_points = syntax_node. get_wrapping_break_line_point_properties ( self . db ) ;
12691305 self . append_break_line_point ( node_break_points. leading ( ) ) ;
@@ -1284,13 +1320,8 @@ fn compare_use_paths<'a>(a: &UsePath<'a>, b: &UsePath<'a>, db: &dyn Database) ->
12841320 match ( a, b) {
12851321 // Case for multi vs multi.
12861322 ( UsePath :: Multi ( a_multi) , UsePath :: Multi ( b_multi) ) => {
1287- let empty_string = "" . into ( ) ;
12881323 let get_min_child = |multi : & ast:: UsePathMulti < ' a > | {
1289- multi. use_paths ( db) . elements ( db) . min_by_key ( |child| match child {
1290- UsePath :: Leaf ( leaf) => leaf. extract_ident ( db) ,
1291- UsePath :: Single ( single) => single. extract_ident ( db) ,
1292- _ => & empty_string,
1293- } )
1324+ multi. use_paths ( db) . elements ( db) . min_by ( |a, b| compare_use_paths ( a, b, db) )
12941325 } ;
12951326 match ( get_min_child ( a_multi) , get_min_child ( b_multi) ) {
12961327 ( Some ( a_min) , Some ( b_min) ) => compare_use_paths ( & a_min, & b_min, db) ,
0 commit comments