use crate::{
formatter::FormattedCode,
parse::parse_snippet,
utils::map::{
byte_span::{ByteSpan, LeafSpans},
comments::CommentMap,
},
Format, Formatter, FormatterError,
};
use ropey::Rope;
use std::{fmt::Write, ops::Range};
use sway_ast::token::{Comment, CommentKind};
use sway_types::{Span, Spanned};
pub type UnformattedCode = String;
#[derive(Debug, Default, Clone)]
pub struct CommentsContext {
pub map: CommentMap,
unformatted_code: UnformattedCode,
}
impl CommentsContext {
pub fn new(map: CommentMap, unformatted_code: UnformattedCode) -> Self {
Self {
map,
unformatted_code,
}
}
pub fn unformatted_code(&self) -> &str {
&self.unformatted_code
}
}
#[inline]
pub fn has_comments_in_formatter(formatter: &Formatter, range: &Range<usize>) -> bool {
formatter
.comments_context
.map
.comments_between(range)
.peekable()
.peek()
.is_some()
}
#[inline]
pub fn has_comments<I: Iterator>(comments: I) -> bool {
comments.peekable().peek().is_some()
}
pub fn collect_newlines_after_comment(
comments_context: &CommentsContext,
comment: &Comment,
) -> String {
comments_context.unformatted_code()[comment.span().end()..]
.chars()
.take_while(|&c| c.is_whitespace())
.filter(|&c| c == '\n')
.collect()
}
fn write_trailing_comment(
formatted_code: &mut FormattedCode,
comment: &Comment,
) -> Result<(), FormatterError> {
formatted_code.truncate(formatted_code.trim_end().len());
writeln!(formatted_code, " {}", comment.span().as_str().trim_end())?;
Ok(())
}
pub fn write_comments(
formatted_code: &mut FormattedCode,
range: Range<usize>,
formatter: &mut Formatter,
) -> Result<bool, FormatterError> {
{
let mut comments_iter = formatter
.comments_context
.map
.comments_between(&range)
.peekable();
if comments_iter.peek().is_none() {
return Ok(false);
};
if formatted_code.ends_with(['{', '}']) && !formatted_code.ends_with('\n') {
writeln!(formatted_code)?;
}
for comment in comments_iter {
let newlines = collect_newlines_after_comment(&formatter.comments_context, comment);
match comment.comment_kind {
CommentKind::Newlined => {
write!(
formatted_code,
"{}{}{}",
formatter.indent_to_str()?,
comment.span().as_str(),
newlines
)?;
}
CommentKind::Trailing => {
write_trailing_comment(formatted_code, comment)?;
}
CommentKind::Inlined => {
formatted_code.truncate(formatted_code.trim_end().len());
write!(formatted_code, " {} ", comment.span().as_str())?;
}
CommentKind::Multilined => {
write!(
formatted_code,
"{}{}",
formatter.indent_to_str()?,
comment.span().as_str(),
)?;
}
}
}
}
formatter
.comments_context
.map
.retain(|bs, _| !bs.contained_within(&range));
Ok(true)
}
pub fn rewrite_with_comments<T: sway_parse::Parse + Format + LeafSpans>(
formatter: &mut Formatter,
unformatted_span: Span,
unformatted_leaf_spans: Vec<ByteSpan>,
formatted_code: &mut FormattedCode,
last_formatted: usize,
) -> Result<(), FormatterError> {
let mut offset = 0;
let mut to_rewrite = formatted_code[last_formatted..].to_string();
let formatted_leaf_spans =
parse_snippet::<T>(&formatted_code[last_formatted..], formatter.experimental)?.leaf_spans();
let mut previous_unformatted_leaf_span = unformatted_leaf_spans
.first()
.ok_or(FormatterError::CommentError)?;
let mut previous_formatted_leaf_span = formatted_leaf_spans
.first()
.ok_or(FormatterError::CommentError)?;
for (unformatted_leaf_span, formatted_leaf_span) in unformatted_leaf_spans
.iter()
.zip(formatted_leaf_spans.iter())
{
let range = std::ops::Range {
start: previous_unformatted_leaf_span.end,
end: unformatted_leaf_span.start,
};
let iter = formatter.comments_context.map.comments_between(&range);
let mut comments_found = vec![];
for i in iter {
comments_found.push(i.clone());
}
if !comments_found.is_empty() {
let extra_newlines = collect_extra_newlines(unformatted_span.clone(), &comments_found);
offset += insert_after_span(
previous_formatted_leaf_span,
comments_found.clone(),
offset,
&mut to_rewrite,
extra_newlines,
)?;
formatter
.comments_context
.map
.retain(|bs, _| !bs.contained_within(&range))
}
previous_unformatted_leaf_span = unformatted_leaf_span;
previous_formatted_leaf_span = formatted_leaf_span;
}
formatted_code.truncate(last_formatted);
write!(formatted_code, "{to_rewrite}")?;
Ok(())
}
fn collect_extra_newlines(unformatted_span: Span, comments_found: &Vec<Comment>) -> Vec<usize> {
let mut extra_newlines = vec![0];
if comments_found.len() == 1 {
return extra_newlines;
}
let mut prev_comment: Option<&Comment> = None;
for comment in comments_found {
if let Some(prev_comment) = prev_comment {
let whitespace_between = unformatted_span.as_str()[prev_comment.span().end()
- unformatted_span.start()
..comment.span().start() - unformatted_span.start()]
.to_string();
let mut extra_newlines_count = 0;
if whitespace_between.chars().filter(|&c| c == '\n').count() > 1 {
extra_newlines_count = 1;
};
extra_newlines.push(extra_newlines_count);
}
prev_comment = Some(comment);
}
extra_newlines
}
fn is_empty_block(formatted_code: &FormattedCode, end: usize) -> bool {
formatted_code.chars().nth(end - 1) == Some('{') && formatted_code.chars().nth(end) == Some('}')
}
fn insert_after_span(
from: &ByteSpan,
comments_to_insert: Vec<Comment>,
offset: usize,
formatted_code: &mut FormattedCode,
extra_newlines: Vec<usize>,
) -> Result<usize, FormatterError> {
let mut comment_str = String::new();
let mut indent = formatted_code[from.end + offset..]
.chars()
.take_while(|c| c.is_whitespace())
.collect::<String>();
if !is_empty_block(formatted_code, from.end) {
if formatted_code.chars().nth(from.end + offset + indent.len()) == Some('}') {
if comments_to_insert
.iter()
.any(|c| c.comment_kind == CommentKind::Newlined)
{
let prev_line = formatted_code[..from.end + offset]
.trim_end()
.chars()
.rev()
.take_while(|&c| c != '\n')
.collect::<String>();
indent = prev_line
.chars()
.rev()
.take_while(|c| c.is_whitespace())
.collect();
if let Some(comment) = comments_to_insert.first() {
if comment.comment_kind != CommentKind::Trailing {
comment_str.push('\n');
}
}
}
}
for (comment, extra_newlines) in comments_to_insert.iter().zip(extra_newlines) {
for _ in 0..extra_newlines {
comment_str.push('\n');
}
match comment.comment_kind {
CommentKind::Trailing => {
if comments_to_insert.len() > 1 && indent.starts_with('\n') {
write!(comment_str, " {}", comment.span().as_str())?;
} else {
writeln!(comment_str, " {}", comment.span().as_str())?;
}
}
CommentKind::Newlined => {
if comments_to_insert.len() > 1 && indent.starts_with('\n') {
write!(comment_str, "{}{}", indent, comment.span().as_str())?;
} else {
writeln!(comment_str, "{}{}", indent, comment.span().as_str())?;
}
}
CommentKind::Inlined => {
if !formatted_code[..from.end].ends_with(' ') {
write!(comment_str, " ")?;
}
write!(comment_str, "{}", comment.span().as_str())?;
if !formatted_code[from.end + offset..].starts_with([' ', '\n']) {
write!(comment_str, " ")?;
}
}
CommentKind::Multilined => {
write!(comment_str, "{}{}", indent, comment.span().as_str())?;
}
};
}
let mut src_rope = Rope::from_str(formatted_code);
if let Some(char) = src_rope.get_char(from.end + offset) {
if char == '\n' && comment_str.ends_with('\n') {
comment_str.pop();
}
};
src_rope
.try_insert(from.end + offset, &comment_str)
.map_err(|_| FormatterError::CommentError)?;
formatted_code.clear();
formatted_code.push_str(&src_rope.to_string());
}
Ok(comment_str.chars().count())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::utils::map::byte_span::ByteSpan;
#[test]
fn test_collect_newlines_after_comment() {
let commented_code = r#"contract;
// 10-18
pub fn main() -> bool {
true
}
"#;
let mut comments_ctx = CommentsContext::new(
CommentMap::from_src(commented_code.into()).unwrap(),
commented_code.to_string(),
);
assert_eq!(
collect_newlines_after_comment(
&comments_ctx,
comments_ctx
.map
.get(&ByteSpan { start: 10, end: 18 })
.unwrap(),
),
"\n"
);
let multiline_comment = r#"contract;
pub fn main() -> bool {
// 38-46
// 51-59
true
}
"#;
comments_ctx = CommentsContext::new(
CommentMap::from_src(multiline_comment.into()).unwrap(),
multiline_comment.to_string(),
);
assert_eq!(
collect_newlines_after_comment(
&comments_ctx,
comments_ctx
.map
.get(&ByteSpan { start: 38, end: 46 })
.unwrap(),
),
"\n"
);
let multi_newline_comments = r#"contract;
pub fn main() -> bool {
// 38-46
// 52-60
true
}
"#;
comments_ctx = CommentsContext::new(
CommentMap::from_src(multi_newline_comments.into()).unwrap(),
multi_newline_comments.to_string(),
);
assert_eq!(
collect_newlines_after_comment(
&comments_ctx,
comments_ctx
.map
.get(&ByteSpan { start: 38, end: 46 })
.unwrap(),
),
"\n\n"
);
}
}