use crate::{
formatter::{FormattedCode, FormatterError},
utils::map::byte_span::{ByteSpan, LeafSpans},
};
use ropey::Rope;
use std::{
collections::BTreeMap,
fmt::Write,
ops::Bound::{Excluded, Included},
path::PathBuf,
sync::Arc,
};
use sway_ast::{
token::{Comment, CommentedTokenTree, CommentedTree},
Module,
};
use sway_parse::lex_commented;
use sway_types::Spanned;
use super::byte_span;
pub type CommentMap = BTreeMap<ByteSpan, Comment>;
trait CommentRange {
fn comments_in_range(&self, from: &ByteSpan, to: &ByteSpan) -> Vec<(ByteSpan, Comment)>;
}
impl CommentRange for CommentMap {
fn comments_in_range(&self, from: &ByteSpan, to: &ByteSpan) -> Vec<(ByteSpan, Comment)> {
if from == &byte_span::STARTING_BYTE_SPAN {
self.range(..to)
.map(|items| (items.0.clone(), items.1.clone()))
.collect()
} else {
self.range((Included(from), Excluded(to)))
.map(|items| (items.0.clone(), items.1.clone()))
.collect()
}
}
}
pub fn comment_map_from_src(input: Arc<str>) -> Result<CommentMap, FormatterError> {
let mut comment_map = BTreeMap::new();
let commented_token_stream = lex_commented(&input, 0, input.len(), None)?;
let tts = commented_token_stream.token_trees().iter();
for comment in tts {
collect_comments_from_token_stream(comment, &mut comment_map);
}
Ok(comment_map)
}
fn collect_comments_from_token_stream(
commented_token_tree: &CommentedTokenTree,
comment_map: &mut CommentMap,
) {
match commented_token_tree {
CommentedTokenTree::Comment(comment) => {
let comment_span = ByteSpan {
start: comment.span.start(),
end: comment.span.end(),
};
comment_map.insert(comment_span, comment.clone());
}
CommentedTokenTree::Tree(CommentedTree::Group(group)) => {
for item in group.token_stream.token_trees().iter() {
collect_comments_from_token_stream(item, comment_map);
}
}
_ => {}
}
}
pub fn handle_comments(
unformatted_input: Arc<str>,
unformatted_module: &Module,
formatted_input: Arc<str>,
path: Option<Arc<PathBuf>>,
formatted_code: &mut FormattedCode,
) -> Result<(), FormatterError> {
let comment_map = comment_map_from_src(unformatted_input.clone())?;
let formatted_module = sway_parse::parse_file_standalone(formatted_input, path)?;
add_comments(
comment_map,
unformatted_module,
&formatted_module,
formatted_code,
unformatted_input,
)
}
fn add_comments(
comment_map: CommentMap,
unformatted_module: &Module,
formatted_module: &Module,
formatted_code: &mut FormattedCode,
unformatted_code: Arc<str>,
) -> Result<(), FormatterError> {
let mut unformatted_comment_spans = unformatted_module.leaf_spans();
let mut formatted_comment_spans = formatted_module.leaf_spans();
unformatted_comment_spans.push(ByteSpan {
start: unformatted_code.len(),
end: unformatted_code.len(),
});
formatted_comment_spans.push(ByteSpan {
start: formatted_code.len(),
end: formatted_code.len(),
});
let mut offset = 0;
let mut previous_unformatted_comment_span = unformatted_comment_spans
.first()
.ok_or(FormatterError::CommentError)?;
let mut previous_formatted_comment_span = formatted_comment_spans
.first()
.ok_or(FormatterError::CommentError)?;
for (unformatted_comment_span, formatted_comment_span) in unformatted_comment_spans
.iter()
.skip(1)
.zip(formatted_comment_spans.iter().skip(1))
{
let comments_found = get_comments_between_spans(
previous_unformatted_comment_span,
unformatted_comment_span,
&comment_map,
&unformatted_code,
);
if !comments_found.is_empty() {
offset += insert_after_span(
previous_formatted_comment_span,
comments_found,
offset,
formatted_code,
)?;
}
previous_unformatted_comment_span = unformatted_comment_span;
previous_formatted_comment_span = formatted_comment_span;
}
Ok(())
}
type CommentWithContext = (Comment, String);
fn get_comments_between_spans(
from: &ByteSpan,
to: &ByteSpan,
comment_map: &CommentMap,
unformatted_code: &Arc<str>,
) -> Vec<CommentWithContext> {
let mut comments_with_context: Vec<CommentWithContext> = Vec::new();
if from < to {
for (index, (comment_span, comment)) in
comment_map.comments_in_range(from, to).iter().enumerate()
{
let starting_position_for_context = if index == 0 {
from.end
} else {
comments_with_context[index - 1].0.span.end()
};
comments_with_context.push((
comment.clone(),
unformatted_code[starting_position_for_context..comment_span.start].to_string(),
));
}
}
comments_with_context
}
fn format_context(context: &str, threshold: usize) -> String {
let mut remaining_newlines = threshold;
let mut formatted_context = String::new();
for char in context.chars() {
if char == '\n' {
if remaining_newlines > 0 {
formatted_context.push('\n');
remaining_newlines -= 1;
}
} else {
formatted_context.push(char);
}
}
if formatted_context.starts_with("\n\n") {
formatted_context.remove(0);
}
formatted_context
}
fn insert_after_span(
from: &ByteSpan,
comments_to_insert: Vec<CommentWithContext>,
offset: usize,
formatted_code: &mut FormattedCode,
) -> Result<usize, FormatterError> {
let iter = comments_to_insert.iter();
let mut offset = offset;
let mut comment_str = String::new();
for comment_with_context in iter {
let (comment_value, comment_context) = comment_with_context;
write!(
comment_str,
"{}{}",
format_context(comment_context, 2),
&format_comment(comment_value)
)?;
}
let mut src_rope = Rope::from_str(formatted_code);
if formatted_code.chars().nth(from.end + offset + 1) == Some('\n') {
offset += 1;
}
src_rope.insert(from.end + offset, &comment_str);
formatted_code.clear();
formatted_code.push_str(&src_rope.to_string());
Ok(comment_str.len())
}
fn format_comment(comment: &Comment) -> String {
let mut comment_str = comment.span().str();
if comment.span.start() == 0 {
comment_str.push('\n');
}
comment_str
}
#[cfg(test)]
mod tests {
use crate::utils::map::comments::CommentRange;
use super::{comment_map_from_src, ByteSpan};
use std::{ops::Bound::Included, sync::Arc};
#[test]
fn test_comment_span_map_standalone_comment() {
let input = r#"
// Single-line comment.
let var = 256; // This is a comment.
struct Foo {
/* multi-
* line-
* comment */
bar: i32,
}
"#;
let map = comment_map_from_src(Arc::from(input)).unwrap();
assert!(!map.is_empty());
let range_start_span = ByteSpan { start: 0, end: 32 };
let range_end_span = ByteSpan { start: 33, end: 34 };
let found_comment = map
.range((Included(range_start_span), Included(range_end_span)))
.last()
.unwrap();
assert_eq!(found_comment.1.span.as_str(), "// Single-line comment.");
}
#[test]
fn test_comment_span_map_standalone_next_to_item() {
let input = r#"
// Single-line comment.
let var = 256; // This is a comment.
struct Foo {
/* multi-
* line-
* comment */
bar: i32,
}
"#;
let map = comment_map_from_src(Arc::from(input)).unwrap();
assert!(!map.is_empty());
let range_start_span = ByteSpan { start: 40, end: 54 };
let range_end_span = ByteSpan {
start: 100,
end: 115,
};
let found_comment = map
.range((Included(range_start_span), Included(range_end_span)))
.last()
.unwrap();
assert_eq!(found_comment.1.span.as_str(), "// This is a comment.");
}
#[test]
fn test_comment_span_map_standalone_inside_block() {
let input = r#"
// Single-line comment.
let var = 256; // This is a comment.
struct Foo {
/* multi-
* line-
* comment */
bar: i32,
}
"#;
let map = comment_map_from_src(Arc::from(input)).unwrap();
assert!(!map.is_empty());
let range_start_span = ByteSpan {
start: 110,
end: 116,
};
let range_end_span = ByteSpan {
start: 200,
end: 201,
};
let found_comment = map
.range((Included(range_start_span), Included(range_end_span)))
.last()
.unwrap();
assert_eq!(
found_comment.1.span.as_str(),
"/* multi-\n * line-\n * comment */"
);
}
#[test]
fn test_comment_map_range_from_start() {
let range_start_span = ByteSpan { start: 0, end: 0 };
let range_end_span = ByteSpan { start: 8, end: 16 };
let input = r#"// test
contract;"#;
let map = comment_map_from_src(Arc::from(input)).unwrap();
assert!(!map.is_empty());
let found_comments = map.comments_in_range(&range_start_span, &range_end_span);
assert_eq!(found_comments[0].1.span.as_str(), "// test");
}
}