diff --git a/crates/common/src/comments/mod.rs b/crates/common/src/comments/mod.rs index 3e344c5a56ddf..033146da63ad2 100644 --- a/crates/common/src/comments/mod.rs +++ b/crates/common/src/comments/mod.rs @@ -235,9 +235,9 @@ impl<'ast> CommentGatherer<'ast> { let line_begin_pos = (line_begin_in_file - self.start_bpos).to_usize(); let mut col = CharPos(self.text[line_begin_pos..self.pos].chars().count()); - // To preserve alignment in non-doc comments, normalize the block based on its - // least-indented line. - if !is_doc { + // To preserve alignment in multi-line non-doc comments, normalize the block based + // on its least-indented line. + if !is_doc && token_text.contains('\n') { col = token_text.lines().skip(1).fold(col, |min, line| { if line.is_empty() { return min; diff --git a/crates/fmt/src/state/mod.rs b/crates/fmt/src/state/mod.rs index 46e68b065d3fc..308a08dfee192 100644 --- a/crates/fmt/src/state/mod.rs +++ b/crates/fmt/src/state/mod.rs @@ -584,52 +584,83 @@ impl<'sess> State<'sess, '_> { return; } - let post_break_prefix = |prefix: &'static str, line_len: usize| -> &'static str { + fn post_break_prefix(prefix: &'static str, has_content: bool) -> &'static str { + if !has_content { + return prefix; + } match prefix { - "///" if line_len > 3 => "/// ", - "//" if line_len > 2 => "// ", - "/*" if line_len > 2 => "/* ", - " *" if line_len > 2 => " * ", + "///" => "/// ", + "//" => "// ", + "/*" => "/* ", + " *" => " * ", _ => prefix, } - }; + } self.ibox(0); - let (prefix, content) = if is_doc { - // Doc comments preserve leading whitespaces (right after the prefix). - self.word(prefix); - let content = &line[prefix.len()..]; - let (leading_ws, rest) = - content.split_at(content.chars().take_while(|&c| c.is_whitespace()).count()); + self.word(prefix); + + let content = &line[prefix.len()..]; + let content = if is_doc { + // Doc comments preserve leading whitespaces (right after the prefix) as nbps. + let ws_len = content + .char_indices() + .take_while(|(_, c)| c.is_whitespace()) + .last() + .map_or(0, |(idx, c)| idx + c.len_utf8()); + let (leading_ws, rest) = content.split_at(ws_len); if !leading_ws.is_empty() { self.word(leading_ws.to_owned()); } - let prefix = post_break_prefix(prefix, rest.len()); - (prefix, rest) + rest } else { - let content = line[prefix.len()..].trim(); - let prefix = post_break_prefix(prefix, content.len()); - self.word(prefix); - (prefix, content) - }; - - // Split the rest of the content into words. - let mut words = content.split_whitespace().peekable(); - while let Some(word) = words.next() { - self.word(word.to_owned()); - if let Some(next_word) = words.peek() { - if *next_word == "*/" { + // Non-doc comments: replace first whitespace with nbsp, rest of content continues + if let Some(first_char) = content.chars().next() { + if first_char.is_whitespace() { self.nbsp(); + &content[first_char.len_utf8()..] } else { - self.s.scan_break(BreakToken { - offset: break_offset, - blank_space: 1, - post_break: if matches!(prefix, "/* ") { None } else { Some(prefix) }, - ..Default::default() - }); + content + } + } else { + "" + } + }; + + let post_break = post_break_prefix(prefix, !content.is_empty()); + + // Process content character by character to preserve consecutive whitespaces + let (mut chars, mut current_word) = (content.chars().peekable(), String::new()); + while let Some(ch) = chars.next() { + if ch.is_whitespace() { + // Print current word + if !current_word.is_empty() { + self.word(std::mem::take(&mut current_word)); } + + // Preserve multiple spaces while adding a single break + let mut ws_count = 1; + while chars.peek().is_some_and(|c| c.is_whitespace()) { + ws_count += 1; + chars.next(); + } + self.s.scan_break(BreakToken { + offset: break_offset, + blank_space: ws_count, + post_break: if post_break.starts_with("/*") { None } else { Some(post_break) }, + ..Default::default() + }); + continue; } + + current_word.push(ch); + } + + // Print final word + if !current_word.is_empty() { + self.word(current_word); } + self.end(); } diff --git a/crates/fmt/testdata/SimpleComments/fmt.sol b/crates/fmt/testdata/SimpleComments/fmt.sol index edb82abfac46e..e9f399968e36d 100644 --- a/crates/fmt/testdata/SimpleComments/fmt.sol +++ b/crates/fmt/testdata/SimpleComments/fmt.sol @@ -1,6 +1,14 @@ contract SimpleComments { + //´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*: + // VARIABLES + //.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.• + mapping(address /* asset */ => address /* router */) public router; + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* FUNCTIONS */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + constructor() { // TODO: do this and that diff --git a/crates/fmt/testdata/SimpleComments/original.sol b/crates/fmt/testdata/SimpleComments/original.sol index 6ec154b1a04fc..2dff78dce6a3c 100644 --- a/crates/fmt/testdata/SimpleComments/original.sol +++ b/crates/fmt/testdata/SimpleComments/original.sol @@ -1,6 +1,13 @@ contract SimpleComments { + //´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*: + // VARIABLES + //.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.• + mapping(address /* asset */ => address /* router */) public router; + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* FUNCTIONS */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ constructor() { // TODO: do this and that diff --git a/crates/fmt/testdata/SimpleComments/wrap-comments.fmt.sol b/crates/fmt/testdata/SimpleComments/wrap-comments.fmt.sol index 470ddf3922eef..7e30f8e9d507b 100644 --- a/crates/fmt/testdata/SimpleComments/wrap-comments.fmt.sol +++ b/crates/fmt/testdata/SimpleComments/wrap-comments.fmt.sol @@ -1,9 +1,18 @@ // config: line_length = 60 // config: wrap_comments = true contract SimpleComments { + //´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*: + // VARIABLES + //.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.• + mapping(address /* asset */ => address /* router */) public router; + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* FUNCTIONS + */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + constructor() { // TODO: do this and that