use proc_macro2::Span;
use quote::ToTokens as _;
pub fn operator_with_whitespace(expr: &syn::ExprBinary) -> Option<OperatorWithSpacing> {
let (_left_start, left_end) = stream_start_end_spans(expr.left.to_token_stream())?;
let (op_start, op_end) = stream_start_end_spans(expr.op.to_token_stream())?;
let (right_start, _right_end) = stream_start_end_spans(expr.right.to_token_stream())?;
let left_spacing = whitespace_between(left_end, op_start)?;
let right_spacing = whitespace_between(op_end, right_start)?;
Some(OperatorWithSpacing {
before: left_spacing,
operator: expr.op,
after: right_spacing,
})
}
pub struct OperatorWithSpacing {
pub before: Whitespace,
pub operator: syn::BinOp,
pub after: Whitespace,
}
impl OperatorWithSpacing {
pub fn new(operator: syn::BinOp) -> Self {
Self {
before: Whitespace::new().with_spaces(1),
operator,
after: Whitespace::new().with_spaces(1),
}
}
pub fn new_logical_and() -> Self {
Self::new(syn::BinOp::And(syn::token::AndAnd(Span::call_site())))
}
pub fn total_newlines(&self) -> usize {
self.before.lines + self.after.lines
}
pub fn min_indent(&self) -> Option<usize> {
[self.before.indentation(), self.after.indentation()]
.into_iter()
.flatten()
.min()
}
pub fn adjust_indent(&mut self, adjust: isize) {
self.before.adjust_indent(adjust);
self.after.adjust_indent(adjust);
}
}
impl std::fmt::Display for OperatorWithSpacing {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let Self {
before,
operator,
after,
} = self;
let operator = operator.into_token_stream();
write!(f, "{before}{operator}{after}")
}
}
#[cfg(feature = "span-locations")]
pub fn whitespace_inside(group: &proc_macro2::Group) -> Option<(Whitespace, Whitespace)> {
#[cfg(not(feature = "span-locations"))]
{
_ = group;
None
}
#[cfg(feature = "span-locations")]
#[allow(clippy::incompatible_msrv)] {
let group_start = group.span().unwrap().start();
let group_end = group.span().unwrap().end();
let (content_start, content_end) = match stream_start_end_spans(group.stream()) {
Some((start_span, end_span)) => (start_span.unwrap().start(), end_span.unwrap().end()),
None => {
if group_end.line() == group_start.line() {
let spaces = group_end.column().checked_sub(group_start.column() + 2)?;
return Some((Whitespace { lines: 0, spaces }, Whitespace::new()))
} else {
let lines = group_end.line().checked_sub(group_start.line())?;
let spaces = group_end.column().checked_sub(2)?;
return Some((Whitespace { lines, spaces }, Whitespace::new()));
};
}
};
if group_start.file() != content_start.file() || group_end.file() != content_end.file() {
return None;
}
let spacing_start = if group_start.line() == content_start.line() {
let spaces = content_start.column().checked_sub(group_start.column() + 1)?;
Whitespace { lines: 0, spaces }
} else {
let lines = content_start.line().checked_sub(group_start.line())?;
let spaces = content_start.column().checked_sub(1).expect("column not one based");
Whitespace { lines, spaces }
};
let spacing_end = if content_end.line() == group_end.line() {
let spaces = group_end.column().checked_sub(content_end.column() + 1)?;
Whitespace { lines: 0, spaces }
} else {
let lines = group_end.line().checked_sub(content_end.line())?;
let spaces = group_end.column().checked_sub(2)?;
Whitespace { lines, spaces }
};
Some((spacing_start, spacing_end))
}
}
pub fn whitespace_between(a: Span, b: Span) -> Option<Whitespace> {
#[cfg(not(feature = "span-locations"))]
{
let _ = (a, b);
None
}
#[cfg(feature = "span-locations")]
#[allow(clippy::incompatible_msrv)] {
let span_a = a.unwrap().end();
let span_b = b.unwrap().start();
if span_a.file() != span_b.file() {
return None;
}
if span_a.line() == span_b.line() {
let spaces = span_b.column().checked_sub(span_a.column())?;
return Some(Whitespace { lines: 0, spaces});
}
let lines = span_b.line().checked_sub(span_a.line())?;
let spaces = span_b.column().saturating_sub(1);
Some(Whitespace { lines, spaces})
}
}
fn stream_start_end_spans(stream: proc_macro2::TokenStream) -> Option<(Span, Span)> {
let mut tokens = stream.into_iter();
let start_span = tokens.next()?.span();
let mut end_span = start_span;
for token in tokens {
end_span = token.span();
}
Some((start_span, end_span))
}
#[derive(Copy, Clone)]
pub struct Whitespace {
pub lines: usize,
pub spaces: usize,
}
impl Whitespace {
pub fn new() -> Self {
Self {
lines: 0,
spaces: 0,
}
}
#[must_use]
pub fn with_lines(self, lines: usize) -> Self {
Self {
lines,
spaces: self.spaces,
}
}
#[must_use]
pub fn with_spaces(self, spaces: usize) -> Self {
Self {
lines: self.lines,
spaces,
}
}
pub fn indentation(&self) -> Option<usize> {
if self.lines == 0 {
None
} else {
Some(self.spaces)
}
}
pub fn adjust_indent(&mut self, adjust: isize) {
if self.lines > 0 {
self.spaces = (self.spaces as isize + adjust) as usize;
}
}
}
impl From<Whitespace> for String {
fn from(value: Whitespace) -> Self {
let mut output = String::with_capacity(value.lines + value.spaces);
for _ in 0..value.lines {
output.push('\n');
}
for _ in 0..value.spaces {
output.push(' ');
}
output
}
}
impl std::fmt::Display for Whitespace {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
for _ in 0..self.lines {
f.write_str("\n")?;
}
for _ in 0..self.spaces {
f.write_str(" ")?;
}
Ok(())
}
}