use anyhow::Result;
use ropey::{str_utils::byte_to_char_idx, Rope};
use std::{collections::BTreeMap, fmt::Display, sync::Arc};
use sway_ast::Module;
use sway_types::span::Source;
use crate::{
constants::NEW_LINE,
formatter::{FormattedCode, Formatter},
parse::parse_file,
utils::map::byte_span::{ByteSpan, LeafSpans},
FormatterError,
};
#[derive(Debug, Clone, PartialEq)]
struct NewlineSequence {
sequence_length: usize,
}
impl Display for NewlineSequence {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}",
(0..self.sequence_length - 1)
.map(|_| NEW_LINE)
.collect::<String>()
)
}
}
type NewlineMap = BTreeMap<ByteSpan, NewlineSequence>;
#[inline]
fn is_new_line_in_rope(rope: &Rope, index: usize) -> bool {
for (p, new_line) in NEW_LINE.chars().enumerate() {
if rope.get_char(index + p) != Some(new_line) {
return false;
}
}
true
}
fn newline_map_from_src(unformatted_input: &str) -> Result<NewlineMap, FormatterError> {
let mut newline_map = BTreeMap::new();
let mut input_iter = unformatted_input.chars().peekable();
let mut current_sequence_length = 0;
let mut in_sequence = false;
let mut sequence_start = 0;
let mut bytes_offset = 0;
while let Some(char) = input_iter.next() {
let char_index = bytes_offset;
bytes_offset += char.len_utf8();
let next_char = input_iter.peek();
let is_new_line = unformatted_input
.get(char_index..char_index + NEW_LINE.len())
.map(|c| c == NEW_LINE)
.unwrap_or(false);
let is_new_line_next = unformatted_input
.get(char_index + 1..char_index + 1 + NEW_LINE.len())
.map(|c| c == NEW_LINE)
.unwrap_or(false);
if matches!(char, ';' | '}') && is_new_line_next {
if !in_sequence {
sequence_start = char_index + NEW_LINE.len();
in_sequence = true;
}
} else if is_new_line && in_sequence {
current_sequence_length += 1;
}
if (Some(&'}') == next_char || Some(&'(') == next_char) && in_sequence {
current_sequence_length = 0;
in_sequence = false;
}
if next_char == Some(&' ') || next_char == Some(&'\t') {
continue;
}
if !is_new_line_next && current_sequence_length > 0 && in_sequence {
let byte_span = ByteSpan {
start: sequence_start,
end: char_index,
};
let newline_sequence = NewlineSequence {
sequence_length: current_sequence_length,
};
newline_map.insert(byte_span, newline_sequence);
current_sequence_length = 0;
in_sequence = false;
}
}
Ok(newline_map)
}
pub fn handle_newlines(
unformatted_input: Arc<str>,
unformatted_module: &Module,
formatted_input: Source,
formatted_code: &mut FormattedCode,
formatter: &Formatter,
) -> Result<(), FormatterError> {
let newline_threshold = formatter.config.whitespace.newline_threshold;
let newline_map = newline_map_from_src(&unformatted_input)?;
let formatted_module = parse_file(formatted_input, formatter.experimental)?.value;
add_newlines(
newline_map,
unformatted_module,
&formatted_module,
formatted_code,
unformatted_input,
newline_threshold,
&formatter.removed_spans,
)?;
Ok(())
}
#[inline]
fn calculate_offset(base: usize, offset: i64) -> usize {
offset
.checked_add(base as i64)
.unwrap_or(base as i64)
.try_into()
.unwrap_or(base)
}
fn add_newlines(
newline_map: NewlineMap,
unformatted_module: &Module,
formatted_module: &Module,
formatted_code: &mut FormattedCode,
unformatted_code: Arc<str>,
newline_threshold: usize,
removed_spans: &[(usize, usize)],
) -> Result<(), FormatterError> {
let mut unformatted_newline_spans = unformatted_module.leaf_spans();
let mut formatted_newline_spans = formatted_module.leaf_spans();
unformatted_newline_spans.push(ByteSpan {
start: unformatted_code.len(),
end: unformatted_code.len(),
});
formatted_newline_spans.push(ByteSpan {
start: formatted_code.len(),
end: formatted_code.len(),
});
let mut offset = 0;
let mut previous_unformatted_newline_span = unformatted_newline_spans
.first()
.ok_or(FormatterError::NewlineSequenceError)?;
let mut previous_formatted_newline_span = formatted_newline_spans
.first()
.ok_or(FormatterError::NewlineSequenceError)?;
if !removed_spans.is_empty() {
newline_map.iter().try_fold(
0_i64,
|mut offset, (newline_span, newline_sequence)| -> Result<i64, FormatterError> {
let formatted_pos =
map_unformatted_to_formatted_position(newline_span.end, removed_spans);
offset += insert_after_span(
calculate_offset(formatted_pos, offset),
newline_sequence.clone(),
formatted_code,
newline_threshold,
)?;
Ok(offset)
},
)?;
return Ok(());
}
for (unformatted_newline_span, formatted_newline_span) in unformatted_newline_spans
.iter()
.skip(1)
.zip(formatted_newline_spans.iter().skip(1))
{
if previous_unformatted_newline_span.end < unformatted_newline_span.start {
let whitespaces_with_comments = &unformatted_code
[previous_unformatted_newline_span.end..unformatted_newline_span.start];
let mut whitespaces_with_comments_it =
whitespaces_with_comments.char_indices().peekable();
let start = previous_unformatted_newline_span.end;
let mut comment_found = false;
while let Some((idx, character)) = whitespaces_with_comments_it.next() {
if character == '/' {
if let Some((_, '/') | (_, '*')) = whitespaces_with_comments_it.peek() {
comment_found = true;
if let Some(newline_sequence) = first_newline_sequence_in_span(
&ByteSpan {
start,
end: start + idx,
},
&newline_map,
) {
offset += insert_after_span(
calculate_offset(previous_formatted_newline_span.end, offset),
newline_sequence,
formatted_code,
newline_threshold,
)?;
break;
}
}
}
if idx == whitespaces_with_comments.len() - 1 && !comment_found {
if let Some(newline_sequence) = first_newline_sequence_in_span(
&ByteSpan {
start,
end: unformatted_newline_span.start,
},
&newline_map,
) {
offset += insert_after_span(
calculate_offset(previous_formatted_newline_span.end, offset),
newline_sequence,
formatted_code,
newline_threshold,
)?;
}
}
}
if comment_found {
let mut whitespaces_with_comments_rev_it =
whitespaces_with_comments.char_indices().rev().peekable();
let mut end_of_last_comment = whitespaces_with_comments.len();
for (idx, character) in whitespaces_with_comments_rev_it.by_ref() {
if !character.is_whitespace() {
end_of_last_comment = idx + 1;
break;
}
}
let mut prev_character = None;
while let Some((_, character)) = whitespaces_with_comments_rev_it.next() {
if character == '/' {
let next_character =
whitespaces_with_comments_rev_it.peek().map(|(_, c)| *c);
if next_character == Some('/') || prev_character == Some('*') {
if let Some(newline_sequence) = first_newline_sequence_in_span(
&ByteSpan {
start: start + end_of_last_comment,
end: unformatted_newline_span.start,
},
&newline_map,
) {
offset += insert_after_span(
calculate_offset(
previous_formatted_newline_span.end + end_of_last_comment,
offset,
),
newline_sequence,
formatted_code,
newline_threshold,
)?;
}
break;
}
}
prev_character = Some(character);
}
}
}
previous_unformatted_newline_span = unformatted_newline_span;
previous_formatted_newline_span = formatted_newline_span;
}
Ok(())
}
fn format_newline_sequence(newline_sequence: &NewlineSequence, threshold: usize) -> String {
if newline_sequence.sequence_length > threshold {
(0..threshold).map(|_| NEW_LINE).collect::<String>()
} else {
newline_sequence.to_string()
}
}
#[inline]
fn is_alphanumeric(c: char) -> bool {
c.is_alphanumeric() || c == '_' || c == '.'
}
fn insert_after_span(
at: usize,
newline_sequence: NewlineSequence,
formatted_code: &mut FormattedCode,
threshold: usize,
) -> Result<i64, FormatterError> {
let sequence_string = format_newline_sequence(&newline_sequence, threshold);
let mut len = sequence_string.len() as i64;
let mut src_rope = Rope::from_str(formatted_code);
let at = byte_to_char_idx(formatted_code, at);
let mut remove_until = at;
for i in at..at + newline_sequence.sequence_length {
if !is_new_line_in_rope(&src_rope, i) {
break;
}
remove_until = i;
}
let removed = if remove_until > at {
src_rope
.try_remove(at..remove_until)
.map_err(|_| FormatterError::NewlineSequenceError)?;
let removed = (remove_until - at) as i64;
len -= removed;
removed
} else {
0
};
let is_token = src_rope
.get_char(at)
.map(is_alphanumeric)
.unwrap_or_default()
&& src_rope
.get_char(at + 1)
.map(is_alphanumeric)
.unwrap_or_default();
if !is_token {
src_rope
.try_insert(at, &sequence_string)
.map_err(|_| FormatterError::NewlineSequenceError)?;
} else {
len = -removed;
}
formatted_code.clear();
formatted_code.push_str(&src_rope.to_string());
Ok(len)
}
fn first_newline_sequence_in_span(
span: &ByteSpan,
newline_map: &NewlineMap,
) -> Option<NewlineSequence> {
for (range, sequence) in newline_map.iter() {
if span.start <= range.start && range.end < span.end {
return Some(sequence.clone());
}
}
None
}
fn map_unformatted_to_formatted_position(
unformatted_pos: usize,
removed_spans: &[(usize, usize)],
) -> usize {
let total_removed = removed_spans
.iter()
.filter(|(removed_pos, _)| *removed_pos < unformatted_pos)
.map(|(_, removed_count)| removed_count)
.sum::<usize>();
unformatted_pos.saturating_sub(total_removed)
}
#[cfg(test)]
mod tests {
use crate::utils::map::{byte_span::ByteSpan, newline::first_newline_sequence_in_span};
use super::{newline_map_from_src, NewlineMap, NewlineSequence};
#[test]
fn test_newline_map() {
let raw_src = r#"script;
fn main() {
let number: u64 = 10;
let number2: u64 = 20;
let number3: u64 = 30;
}"#;
let newline_map = newline_map_from_src(raw_src.trim_start()).unwrap();
let newline_sequence_lengths = Vec::from_iter(
newline_map
.iter()
.map(|map_item| map_item.1.sequence_length),
);
let correct_newline_sequence_lengths = vec![2, 2, 3];
assert_eq!(newline_sequence_lengths, correct_newline_sequence_lengths);
}
#[test]
fn test_newline_map_with_whitespaces() {
let raw_src = r#"script;
fuel_coin.mint {
gas: default_gas
}
(11);"#;
let newline_map = newline_map_from_src(raw_src.trim_start()).unwrap();
let newline_sequence_lengths = Vec::from_iter(
newline_map
.iter()
.map(|map_item| map_item.1.sequence_length),
);
let correct_newline_sequence_lengths = vec![1];
assert_eq!(newline_sequence_lengths, correct_newline_sequence_lengths);
}
#[test]
fn test_newline_range_simple() {
let mut newline_map = NewlineMap::new();
let newline_sequence = NewlineSequence { sequence_length: 2 };
newline_map.insert(ByteSpan { start: 9, end: 10 }, newline_sequence.clone());
assert_eq!(
newline_sequence,
first_newline_sequence_in_span(&ByteSpan { start: 8, end: 11 }, &newline_map).unwrap()
);
assert_eq!(
newline_sequence,
first_newline_sequence_in_span(&ByteSpan { start: 9, end: 11 }, &newline_map).unwrap()
);
assert!(
first_newline_sequence_in_span(&ByteSpan { start: 9, end: 10 }, &newline_map).is_none()
);
assert!(
first_newline_sequence_in_span(&ByteSpan { start: 9, end: 9 }, &newline_map).is_none()
);
assert!(
first_newline_sequence_in_span(&ByteSpan { start: 8, end: 8 }, &newline_map).is_none()
);
}
}