use std::cmp::max;
use crate::{
helpers::{Indent, INDENT_STR},
renderer::token_len,
token::{Position, Token},
};
use super::is_newline;
pub(super) fn align_comments(buf: &mut Vec<(Token<'_>, Indent)>) {
let mut candidates = vec![];
let mut i = 0;
let mut newline_count = 0;
let mut last_newline_count = 0;
while i < buf.len() {
let (pos, _) = match &buf[i] {
(Token::Comment(_, v), indent) => (*v, indent),
(Token::Newline, _) => {
newline_count += 1;
i += 1;
continue;
}
_ => {
i += 1;
continue;
}
};
if candidates
.last()
.map(|(_idx, pos)| pos)
.is_some_and(|v: &crate::token::Position| {
(v.unwrap_row() + 1) != pos.unwrap_row()
|| v.unwrap_col() != pos.unwrap_col()
|| (newline_count - last_newline_count) > 1
})
{
process_candidates(buf, &mut candidates);
candidates.truncate(0);
}
last_newline_count = newline_count;
if buf.get(i + 1).is_some_and(|(v, _)| is_newline(v)) {
candidates.push((i, pos));
}
i += 1;
}
process_candidates(buf, &mut candidates);
}
fn process_candidates(buf: &mut [(Token<'_>, Indent)], candidates: &mut [(usize, Position)]) {
if candidates.len() < 2 {
return;
}
debug_assert!(buf.len() >= candidates.len());
let start = candidates[0].0;
debug_assert!(matches!(buf[candidates[0].0].0, Token::Comment(..)));
let start = (0..start).rev().find(|&i| is_newline(&buf[i].0));
let start = match start {
Some(v) => v,
None => {
return;
}
};
let end = candidates
.last()
.expect("must have candidate for comment alignment")
.0;
debug_assert!(matches!(buf[end].0, Token::Comment(..)));
debug_assert!(is_newline(&buf[end + 1].0));
let lines = Vec::with_capacity(candidates.len());
let mut max_line = 0;
let iter = buf[start..=end].iter();
let lines = line_len(iter).fold(lines, |mut acc, v| {
max_line = max(max_line, v);
acc.push(v);
acc
});
debug_assert_eq!(lines.len(), candidates.len());
let mut new_col = max(max_line + 1, candidates[0].1.unwrap_col());
if buf[start..=end]
.iter()
.all(|(v, _)| matches!(v, Token::Comment(_, _)) || is_newline(v))
{
new_col = lines[0];
}
for (candidate_idx, (buf_idx, ..)) in candidates.iter().enumerate() {
match &mut buf[*buf_idx] {
(Token::Comment(_, pos), _) => {
*pos = Position::Relative(new_col - lines[candidate_idx])
}
_ => unreachable!(),
}
}
}
fn line_len<'a, T>(iter: T) -> impl Iterator<Item = usize> + use<'a, T>
where
T: Iterator<Item = &'a (Token<'a>, Indent)>,
{
let mut iter = iter.peekable();
debug_assert!(iter.peek().is_some_and(|v| is_newline(&v.0)));
let mut len = 0;
let mut line_tokens = 0;
std::iter::from_fn(move || {
loop {
let (t, _) = iter.next()?;
if is_newline(t) {
len = iter.peek().unwrap().1.get() as usize * INDENT_STR.len();
line_tokens = 0;
continue;
}
if matches!(t, Token::Comment(..)) && iter.peek().is_some_and(|v| is_newline(&v.0))
|| iter.peek().is_none()
{
if line_tokens == 0 {
return Some(len);
}
return Some(len.saturating_sub(1));
}
let next = iter.peek();
if let Some((next, _)) = next {
if !t.can_precede(next) {
continue;
}
}
len += token_len(t);
line_tokens += 1;
if let Some(n) = next.map(|(v, _)| t.delimiting_space_len(v)) {
len += n;
}
}
})
}
#[cfg(test)]
mod tests {
use crate::assert_rewrite;
use super::*;
#[test]
fn test_line_len_1() {
let tokens = [
(Token::Newline, Indent::new(255)),
(Token::And, Indent::new(1)), (Token::Ident("bananas"), Indent::new(255)), (Token::Eq, Indent::new(255)), (Token::Lit("42"), Indent::new(255)), ];
let iter = tokens.iter().peekable();
let got = line_len(iter).next().unwrap();
assert_eq!(got, 16);
}
#[test]
fn test_line_len_2() {
let tokens = [
(Token::SourceNewline, Indent::new(255)),
(Token::Newline, Indent::new(255)),
(Token::And, Indent::new(1)), (Token::Ident("platanos"), Indent::new(255)), (Token::Prime, Indent::new(255)), (Token::Eq, Indent::new(255)), (Token::Lit("42"), Indent::new(255)), ];
let iter = tokens.iter().peekable();
let got = line_len(iter).next().unwrap();
assert_eq!(got, 18);
}
#[test]
fn test_line_len_only_comment() {
let tokens = [
(Token::SourceNewline, Indent::new(255)),
(
Token::Comment("(* bananas *)", Position::Source { row: 2, col: 40 }),
Indent::new(1),
),
(Token::SourceNewline, Indent::new(255)),
];
let iter = tokens.iter().peekable();
let got = line_len(iter).next().unwrap();
assert_eq!(got, 4);
}
#[test]
fn test_comment_manually_aligned() {
assert_rewrite!(
r"
---- MODULE bananas ----
Op == /\ bananas = 42 \* This is an important number.
/\ platanos' = 42 \* That should be assigned here.
====
"
);
}
#[test]
fn test_comment_partially_aligned() {
assert_rewrite!(
r"
---- MODULE bananas ----
Op == /\ bananas = 42 \* This is an important number.
/\ platanos' = 42 \* That should be assigned here.
/\ platanos' = 42 \* That should be assigned here.
====
"
);
}
#[test]
fn test_comment_non_adjacent_partially_aligned() {
assert_rewrite!(
r"
---- MODULE bananas ----
Op == /\ bananas = 42 \* This is an important number.
/\ platanos' = 42 \* That should be assigned here.
/\ platanos' = 42 \* That should be assigned here.
====
"
);
}
#[test]
fn test_comment_manually_unaligned() {
assert_rewrite!(
r"
---- MODULE bananas ----
Op == /\ bananas = 42 \* This is an important number.
/\ platanos' = 42 \* That should be assigned here.
====
"
);
}
#[test]
fn test_comment_align_push() {
assert_rewrite!(
r"
---- MODULE bananas ----
Op == /\ bananas = 42 \* This is an important number.
/\x=4+1+1+1+1+1+1 \* That should be assigned here.
/\ platanos' = 42 \* That should be assigned here.
====
"
);
}
#[test]
fn test_comment_align_shrink() {
assert_rewrite!(
r"
---- MODULE bananas ----
Op == /\ bananas=42 \* This is an important number.
/\ platanos' = 42 \* That should be assigned here.
====
"
);
}
mod comment_only_lines {
use crate::assert_rewrite;
#[test]
fn test_comment_only_lines_1() {
assert_rewrite!(
r"
---- MODULE bananas ----
A ==
/\ X = 42
\* Bananas
\* Platanos
\* Apples
/\ Y = 25
====
"
);
}
#[test]
fn test_comment_only_lines_2() {
assert_rewrite!(
r"
---- MODULE bananas ----
TypeOK ==
\* Bananas
/\ X = 42
\* Platanos
\* Apples
/\ Y = 24
====
"
);
}
#[test]
fn test_comment_only_lines_3() {
assert_rewrite!(
r"
---- MODULE bananas ----
TypeOK ==
\* Bananas
\* Bananas
/\ X = 42
\* Platanos
\* Apples
/\ Y = 24
====
"
);
}
#[test]
fn test_comment_only_lines_4() {
assert_rewrite!(
r"
---- MODULE bananas ----
Spec == /\ Init /\ [][Next]_vars
/\ WF_vars(DetectTermination)
\* reasonable but not necessary for detecting termination
\* /\ \A i \in Node : WF_vars(Wakeup(i))
====
"
);
}
#[test]
fn test_comment_only_lines_5() {
assert_rewrite!(
r"
---- MODULE bananas ----
Op == /\ bananas = 42 \* This is an important number.
/\ platanos' = 42 \* Something here.
\* Continues here.
/\ platanos' = 42 \* More here.
====
"
);
}
#[test]
fn test_comment_only_lines_6() {
assert_rewrite!(
r"
---- MODULE bananas ----
A ==
/\ X = 42
\* Bananas
\* Platanos
\* Apples
/\ Y = 25
====
"
);
}
#[test]
fn test_comment_only_lines_7() {
assert_rewrite!(
r"
---- MODULE bananas ----
AlwaysResponds ==
(*************************************************************************)
(* Some simple liveness properties, implied by the fact that every *)
(* request eventually generates a response. *)
(*************************************************************************)
/\ \A p \in Proc, r \in Reg :
X = 42
/\ \A oi \in [proc : Proc, idx : Nat] :
Y = 24
====
"
);
}
#[test]
fn test_comment_only_lines_8() {
assert_rewrite!(
r"
---- MODULE bananas ----
SetToSeqs == UNION {{x \in [1 -> set]:
\* A filter applied on each permutation
\* generated by [S -> T]
Op(x)}}
====
"
);
}
#[test]
fn test_comment_only_lines_9() {
assert_rewrite!(
r"
---- MODULE bananas ----
TypeOK == /\ X = 42
\* Platanos
\* Platanos
/\ Y = 24
====
"
)
}
#[test]
fn test_comment_only_lines_10() {
assert_rewrite!(
r#"
---- MODULE bananas ----
SvrHidenProperty ==
/\ (\A x \in sTCPLinkSet: /\ x.Type # "Attacker"
/\ x.State = "ESTABLISHED") \* C1
/\ (\A y \in aTCPLinkSet: /\ y.State # "ESTABLISHED") \* C2
====
"#
);
}
}
#[test]
fn test_fuzz_input_contains_null() {
let s = String::from_utf8(vec![
0x71, 0x00, 0x0a, 0x2a, 0x5c, 0x2a, 0x0a, 0x4a, 0x5c, 0x2a, 0x0a, 0x2b, 0x41, 0x7e,
0x41,
])
.unwrap();
assert_rewrite!(&s);
}
}