use smallvec::SmallVec;
use super::super::{LeadingGlue, Printer};
use crate::ast;
use crate::printer::comments::ShareSite;
use tsv_lang::Span;
use tsv_lang::doc::DocBuf;
use tsv_lang::doc::arena::DocId;
impl<'a> Printer<'a> {
pub(super) fn open_inter_arg_gap(
&self,
parts: &mut DocBuf,
prev_arg: &ast::Expression<'_>,
interior: Option<Span>,
next_arg_start: u32,
) -> InterArgGap<'a> {
let mut pc =
PartitionedComments::for_routed_arg_gap(self, prev_arg.span().end, next_arg_start);
if !self.paren_interior_separates_run(interior) {
pc.emit_trailing_comments_around_comma(parts, self);
return InterArgGap {
forces_expansion: false,
comments: pc,
};
}
pc.lead_run_blocks_behind_interior(self);
parts.push(self.d().text(","));
self.push_element_share_and_run(parts, interior, ShareSite::NextElement, pc.trailing_run());
InterArgGap {
forces_expansion: true,
comments: pc,
}
}
pub(super) fn arg_paren_interior(
&self,
container_start: u32,
args: &[&ast::Expression<'_>],
i: usize,
) -> Option<Span> {
let interior = args[i].paren_interior()?;
self.args_frozen_span(container_start, args, i)
.is_none()
.then_some(interior)
}
pub(super) fn inter_arg_gap_has_comments(
&self,
prev_arg: &ast::Expression<'_>,
next_arg_start: u32,
) -> bool {
self.has_comments_to_emit_between(prev_arg.span().end, next_arg_start)
|| self.paren_interior_forces_expansion(prev_arg.paren_interior())
}
}
pub(super) struct InterArgGap<'a> {
pub comments: PartitionedComments<'a>,
pub forces_expansion: bool,
}
pub(super) fn build_empty_args_parens_doc(
printer: &Printer<'_>,
search_from: u32,
paren_close: u32,
prefix: DocId,
) -> DocId {
let d = printer.d();
let paren_pos = printer
.find_char_outside_comments(search_from, paren_close, b'(')
.unwrap_or(paren_close);
let parens = if paren_pos == paren_close {
d.concat(&[prefix, d.text(")")])
} else {
printer.build_empty_parens_inline_with_comments_doc(paren_pos, paren_close, prefix)
};
printer.build_line_split_gap_doc(search_from, paren_pos, parens)
}
#[inline]
fn find_comma_pos(source: &str, start: u32, end: u32) -> Option<usize> {
let bytes = source.as_bytes();
let (s, e) = (start as usize, end as usize);
let mut i = s;
while i < e {
match bytes[i] {
b',' => return Some(i),
b'/' if i + 1 < e && bytes[i + 1] == b'*' => {
i += 2;
while i + 1 < e && !(bytes[i] == b'*' && bytes[i + 1] == b'/') {
i += 1;
}
i += 2;
}
b'/' if i + 1 < e && bytes[i + 1] == b'/' => {
i += 2;
while i < e && bytes[i] != b'\n' {
i += 1;
}
}
_ => i += 1,
}
}
None
}
#[inline]
pub(crate) fn skip_stripped_open_paren(source: &str, from: u32, to: u32) -> u32 {
if from >= to {
return to;
}
let slice = &source[from as usize..to as usize];
for (i, byte) in slice.bytes().enumerate() {
if byte == b'(' {
return from + i as u32 + 1;
}
if !byte.is_ascii_whitespace() {
break;
}
}
to
}
#[inline]
fn is_comment_before_comma(comment: &ast::Comment, comma_pos: usize) -> bool {
(comment.span.start as usize) < comma_pos
}
#[inline]
fn is_comment_after_comma(comment: &ast::Comment, comma_pos: usize) -> bool {
(comment.span.start as usize) > comma_pos
}
pub(super) fn build_arg_gap_docs(
printer: &Printer<'_>,
arg_end: u32,
next_arg_start: u32,
) -> (DocBuf, DocBuf) {
let pc = PartitionedComments::for_routed_arg_gap(printer, arg_end, next_arg_start);
let mut through_comma = DocBuf::new();
pc.emit_trailing_comments_around_comma(&mut through_comma, printer);
let mut leading = DocBuf::new();
pc.emit_leading_comments_inline_aware(&mut leading, printer);
(through_comma, leading)
}
pub(super) fn has_inter_argument_comments(
call: &ast::CallExpression<'_>,
printer: &Printer<'_>,
) -> bool {
has_inter_argument_comments_slice(call.arguments, printer)
}
pub(super) fn has_inter_argument_comments_slice(
arguments: &[&ast::Expression<'_>],
printer: &Printer<'_>,
) -> bool {
if arguments.len() < 2 {
return false;
}
arguments
.windows(2)
.any(|pair| printer.has_comments_to_emit_between(pair[0].span().end, pair[1].span().start))
}
pub(super) fn any_arg_empty_line(
arguments: &[&ast::Expression<'_>],
printer: &Printer<'_>,
) -> bool {
arguments
.windows(2)
.any(|pair| printer.is_next_line_empty(pair[0].span().end, pair[1].span().start))
}
pub(crate) fn has_stripped_paren_gap(source: &str, start: u32, end: u32) -> bool {
let s = start as usize;
let e = end as usize;
if s >= e || e > source.len() {
return false;
}
let gap = &source[s..e];
if !gap
.bytes()
.all(|b| matches!(b, b' ' | b'\t' | b'\n' | b'\r' | b'(' | b')'))
{
return false;
}
match gap.bytes().position(|b| b == b'(' || b == b')') {
Some(pos) => !gap.as_bytes()[..pos]
.iter()
.any(|&b| b == b'\n' || b == b'\r'),
None => false,
}
}
pub(super) fn should_force_expansion_for_comments(
printer: &Printer<'_>,
start: u32,
next_code_pos: u32,
) -> bool {
if printer.has_line_comments_between(start, next_code_pos) {
return true;
}
for comment in printer.comments_to_emit_between(start, next_code_pos) {
if comment.is_block && printer.block_comment_owns_its_line(comment, true) {
return true;
}
}
false
}
pub(super) fn any_arg_gap_has_comment_on_page(
arguments: &[&ast::Expression<'_>],
printer: &Printer<'_>,
paren_open: u32,
call_end: u32,
) -> bool {
let mut prev_end = paren_open;
for arg in arguments {
if printer.has_comments_on_page_between(prev_end, arg.span().start) {
return true;
}
prev_end = arg.span().end;
}
printer.has_comments_on_page_between(prev_end, call_end)
}
pub(super) fn any_comment_forces_expansion(
call: &ast::CallExpression<'_>,
span: Span,
printer: &Printer<'_>,
paren_open: u32,
) -> bool {
any_comment_forces_expansion_slice(call.arguments, printer, paren_open, span.end)
}
pub(super) fn any_comment_forces_expansion_slice(
arguments: &[&ast::Expression<'_>],
printer: &Printer<'_>,
paren_open: u32,
call_end: u32,
) -> bool {
if arguments.is_empty() {
return false;
}
let first_arg_start = arguments[0].span().start;
if printer.has_comments_to_emit_between(paren_open, first_arg_start)
&& should_force_expansion_for_comments(printer, paren_open, first_arg_start)
{
return true;
}
for (i, arg) in arguments.iter().enumerate() {
if printer.paren_interior_forces_expansion(arg.paren_interior()) {
return true;
}
let arg_end = arg.span().end;
let next_boundary = if i < arguments.len() - 1 {
arguments[i + 1].span().start
} else {
call_end
};
if !printer.has_comments_to_emit_between(arg_end, next_boundary) {
continue;
}
let forces = if i < arguments.len() - 1 {
should_force_expansion_for_comments(printer, arg_end, next_boundary)
} else {
printer.has_line_comments_between(arg_end, next_boundary)
|| printer.has_own_line_block_comment_before_closer(arg_end, next_boundary)
};
if forces {
return true;
}
}
false
}
pub(super) fn last_arg_has_comments(
arguments: &[&ast::Expression<'_>],
printer: &Printer<'_>,
call_end: u32,
paren_open: u32,
) -> bool {
let Some(last) = arguments.last() else {
return false;
};
let last_start = last.span().start;
if arguments.len() >= 2 {
let prev_end = arguments[arguments.len() - 2].span().end;
if let Some(cp) = find_comma_pos(printer.source, prev_end, last_start)
&& printer.has_comments_on_page_between((cp + 1) as u32, last_start)
{
return true;
}
} else {
if printer.has_comments_on_page_between(paren_open, last_start) {
return true;
}
}
printer.has_comments_to_emit_between(last.span().end, call_end)
}
pub(super) fn first_arg_has_any_comments(
arguments: &[&ast::Expression<'_>],
printer: &Printer<'_>,
paren_open: u32,
) -> bool {
if arguments.is_empty() {
return false;
}
let first = &arguments[0];
if printer.has_comments_on_page_between(paren_open, first.span().start) {
return true;
}
if let Some(second) = arguments.get(1) {
let gap =
PartitionedComments::for_routed_arg_gap(printer, first.span().end, second.span().start);
return gap.has_trailing_comments();
}
false
}
pub(super) fn has_trailing_comments_on_args(
call: &ast::CallExpression<'_>,
span: Span,
printer: &Printer<'_>,
) -> bool {
has_trailing_line_comments_slice(call.arguments, span.end, printer)
}
pub(super) fn has_trailing_line_comments_slice(
arguments: &[&ast::Expression<'_>],
call_span_end: u32,
printer: &Printer<'_>,
) -> bool {
has_trailing_comments_slice_impl(arguments, call_span_end, |start, end| {
printer.has_line_comments_between(start, end)
})
}
pub(super) fn emit_first_arg_leading_comments(
printer: &Printer<'_>,
paren_line: &mut DocBuf,
parts: &mut DocBuf,
paren_open: u32,
first_arg_start: u32,
) {
if !printer.has_comments_to_emit_between(paren_open, first_arg_start) {
return;
}
let pc = PartitionedComments::new(printer, paren_open, first_arg_start);
if pc.has_trailing_line() {
pc.emit_trailing_comments(paren_line, printer);
pc.emit_leading_comments_inline_aware(parts, printer);
} else {
pc.emit_unpulled_gap(parts, printer);
}
}
pub(super) fn emit_last_arg_trailing_comments(
printer: &Printer<'_>,
parts: &mut DocBuf,
last_arg: &ast::Expression<'_>,
interior: Option<Span>,
paren_close: u32,
) {
let arg_end = last_arg.span().end;
let pc = printer
.has_comments_to_emit_between(arg_end, paren_close)
.then(|| PartitionedComments::for_closer_gap(printer, arg_end, paren_close));
printer.push_element_share_and_run(
parts,
interior,
ShareSite::Closer,
pc.iter().flat_map(PartitionedComments::trailing_run),
);
if let Some(pc) = &pc {
pc.emit_dangling_comments(parts, printer);
}
}
pub(super) fn has_trailing_comments_slice(
arguments: &[&ast::Expression<'_>],
call_span_end: u32,
printer: &Printer<'_>,
) -> bool {
has_trailing_comments_slice_impl(arguments, call_span_end, |start, end| {
printer.has_comments_to_emit_between(start, end)
})
}
#[inline]
fn has_trailing_comments_slice_impl<F>(
arguments: &[&ast::Expression<'_>],
call_span_end: u32,
has_comments: F,
) -> bool
where
F: Fn(u32, u32) -> bool,
{
if arguments.is_empty() {
return false;
}
for (i, arg) in arguments.iter().enumerate() {
let arg_end = arg.span().end;
let next_boundary = if i < arguments.len() - 1 {
arguments[i + 1].span().start
} else {
call_span_end
};
if has_comments(arg_end, next_boundary) {
return true;
}
}
false
}
pub(crate) struct PartitionedComments<'a> {
pub trailing_line: Option<&'a ast::Comment>,
pub trailing_block: SmallVec<[&'a ast::Comment; 2]>,
pub leading: SmallVec<[&'a ast::Comment; 2]>,
leads: SmallVec<[&'a ast::Comment; 2]>,
behind_interior: bool,
start: u32,
end: u32,
}
impl<'a> PartitionedComments<'a> {
pub(crate) fn new(printer: &Printer<'a>, start: u32, end: u32) -> Self {
let classified = tsv_lang::ClassifiedComments::from_index(
printer.comment_free_gap.comments(),
printer.first_index_between(start, end),
start,
end,
printer.source.as_bytes(),
printer.comment_line_breaks,
);
let leading = classified.leading_in_source_order();
debug_assert!(classified.trailing_line.len() <= 1);
Self {
trailing_line: classified.trailing_line.first().copied(),
trailing_block: classified.trailing_block,
leading,
leads: SmallVec::new(),
behind_interior: false,
start,
end,
}
}
fn for_item_gap(printer: &Printer<'a>, start: u32, end: u32) -> Self {
Self::from_trailing_run(
printer.trailing_comment_run(start, end),
printer,
start,
end,
)
}
pub(super) fn for_routed_arg_gap(printer: &Printer<'a>, start: u32, end: u32) -> Self {
let mut pc = Self::for_item_gap(printer, start, end);
pc.route_after_comma_hugging_to_leading(printer);
pc
}
pub(crate) fn for_closer_gap(printer: &Printer<'a>, item_end: u32, end: u32) -> Self {
Self::from_trailing_run(
printer.closer_trailing_comment_run(item_end, end),
printer,
item_end,
end,
)
}
fn from_trailing_run(
run: impl Iterator<Item = &'a ast::Comment>,
printer: &Printer<'a>,
start: u32,
end: u32,
) -> Self {
let mut trailing_line: Option<&'a ast::Comment> = None;
let mut trailing_block: SmallVec<[&'a ast::Comment; 2]> = SmallVec::new();
let mut run_end = start;
for comment in run {
if comment.is_block {
trailing_block.push(comment);
} else {
debug_assert!(trailing_line.is_none());
trailing_line = Some(comment);
}
run_end = comment.span.end;
}
let leading = printer.comments_to_emit_between(run_end, end).collect();
Self {
trailing_line,
trailing_block,
leading,
leads: SmallVec::new(),
behind_interior: false,
start,
end,
}
}
fn route_after_comma_hugging_to_leading(&mut self, printer: &Printer<'_>) {
let Some(comma_pos) = find_comma_pos(printer.source, self.start, self.end) else {
return;
};
let mut kept: SmallVec<[&'a ast::Comment; 2]> = SmallVec::new();
for comment in self.trailing_block.drain(..) {
if is_comment_after_comma(comment, comma_pos)
&& printer.comment_hugs_next_item(comment.span.end, self.end)
{
self.leading.push(comment);
} else {
kept.push(comment);
}
}
self.trailing_block = kept;
}
pub(crate) fn has_trailing_line(&self) -> bool {
self.trailing_line.is_some()
}
pub(super) fn forces_closer_break(&self) -> bool {
self.has_trailing_line() || !self.leading.is_empty()
}
pub(super) fn trailing_run(&self) -> impl Iterator<Item = &'a ast::Comment> + '_ {
self.trailing_block
.iter()
.chain(self.trailing_line.iter())
.copied()
}
pub(super) fn lead_run_blocks_behind_interior(&mut self, printer: &Printer<'_>) {
self.behind_interior = true;
if self.trailing_line.is_none()
&& printer.run_leads_next_item(
&self.trailing_block,
find_comma_pos(printer.source, self.start, self.end).map(|c| c as u32),
self.end,
)
{
self.leads = std::mem::take(&mut self.trailing_block);
}
}
pub(super) fn is_behind_interior(&self) -> bool {
self.behind_interior
}
pub(crate) fn has_trailing_block(&self) -> bool {
!self.trailing_block.is_empty()
}
pub(super) fn has_trailing_comments(&self) -> bool {
self.has_trailing_block() || self.has_trailing_line()
}
pub(crate) fn pulls_to_delimiter_line(&self, printer: &Printer<'_>) -> bool {
self.has_trailing_comments()
&& should_force_expansion_for_comments(printer, self.start, self.end)
}
pub(crate) fn emit_delimiter_line_pull(&self, parts: &mut DocBuf, printer: &Printer<'_>) {
self.emit_trailing_comments(parts, printer);
if let Some(pulled_end) = self.trailing_end() {
printer.push_delimiter_glued_blank(parts, pulled_end, self.end);
}
}
pub(super) fn emit_pulled_gap(
&self,
paren_line: &mut DocBuf,
parts: &mut DocBuf,
printer: &Printer<'_>,
) {
self.emit_delimiter_line_pull(paren_line, printer);
self.emit_leading_comments_inline_aware(parts, printer);
}
pub(super) fn emit_unpulled_gap(&self, parts: &mut DocBuf, printer: &Printer<'_>) -> bool {
printer.push_leading_comment_run(
parts,
self.trailing_block.iter().chain(&self.leading).copied(),
self.end,
LeadingGlue::AdjacentStrippedParen,
)
}
pub(super) fn has_blank_line_in_gap(&self, printer: &Printer<'_>) -> bool {
let check_end = if !self.leading.is_empty() {
self.leading[0].span.start
} else {
self.end
};
if self.behind_interior {
let from = self
.trailing_run()
.chain(self.leads.iter().copied())
.map(|c| c.span.end)
.max()
.unwrap_or(self.start);
return printer.blank_before_next_item(from, check_end);
}
printer.is_next_line_empty(self.start, check_end)
}
pub(super) fn trailing_end(&self) -> Option<u32> {
self.trailing_block
.iter()
.chain(self.trailing_line.iter())
.map(|c| c.span.end)
.max()
}
fn emit_trailing_comments(&self, parts: &mut DocBuf, printer: &Printer<'_>) {
let d = printer.d();
for comment in &self.trailing_block {
parts.push(d.text(" "));
parts.push(printer.build_comment_doc(comment));
}
if let Some(comment) = self.trailing_line {
parts.push(printer.build_trailing_line_comment_doc(comment));
}
}
pub(super) fn emit_trailing_comments_around_comma(
&self,
parts: &mut DocBuf,
printer: &Printer<'_>,
) {
let d = printer.d();
let comma_pos = find_comma_pos(printer.source, self.start, self.end);
if let Some(cpos) = comma_pos {
for comment in &self.trailing_block {
if is_comment_before_comma(comment, cpos) {
parts.push(d.text(" "));
parts.push(printer.build_comment_doc(comment));
}
}
}
parts.push(d.text(","));
if let Some(cpos) = comma_pos {
for comment in &self.trailing_block {
if is_comment_after_comma(comment, cpos) {
parts.push(d.text(" "));
parts.push(printer.build_comment_doc(comment));
}
}
}
if let Some(comment) = self.trailing_line {
parts.push(printer.build_trailing_line_comment_doc(comment));
}
}
pub(super) fn emit_dangling_comments(&self, parts: &mut DocBuf, printer: &Printer<'_>) {
printer.push_trailing_comment_run(parts, self.leading.iter().copied(), self.start);
}
pub(crate) fn emit_last_arg_comments(&self, parts: &mut DocBuf, printer: &Printer<'_>) {
self.emit_trailing_comments(parts, printer);
self.emit_dangling_comments(parts, printer);
}
pub(super) fn emit_leading_comments_inline_aware(
&self,
parts: &mut DocBuf,
printer: &Printer<'_>,
) {
printer.push_run_leading_next_item(parts, &self.leads, self.end);
printer.push_leading_comment_run(
parts,
self.leading.iter().copied(),
self.end,
LeadingGlue::AdjacentStrippedParen,
);
}
}