use super::{CommentSpacing, CommentVec, LeadingGlue, Printer, RunLeadingBlank, ShareSite};
use crate::ast;
use crate::printer::ParenContext;
use crate::printer::chain::{
chain_mid_chain_paren_gaps, chain_paren_leading_gap, tag_paren_leading_start,
};
use crate::printer::expressions::conditional::ternary_test_needs_parens;
use crate::printer::expressions::operators::SeqLayout;
use crate::printer::ignore::FrozenOperandPair;
use crate::printer::needs_parens::needs_parens;
use crate::printer::statements::TerminatorGap;
use smallvec::{SmallVec, smallvec};
use tsv_lang::Span;
use tsv_lang::doc::DocBuf;
use tsv_lang::doc::arena::DocId;
use tsv_lang::range_too_narrow_for_a_comment;
use tsv_lang::source_scan::{
TriviaProfile, find_char_skipping_comments, has_newline_after_position,
has_newline_before_position, skip_trivia,
};
#[derive(Clone, Copy, PartialEq, Eq)]
enum ShellTail {
StatementTerminator,
ForClauseSeparator,
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum ReparsedRun {
SingleLine,
AnyLines,
}
pub(crate) struct ParenLeadingValue {
pub paren_line: DocBuf,
pub value: DocId,
pub forces_break: bool,
}
fn left_side_child<'e>(expr: &'e ast::Expression<'e>) -> Option<&'e ast::Expression<'e>> {
use ast::ExpressionKind;
Some(match &expr.kind {
ExpressionKind::CallExpression(call) => call.callee,
ExpressionKind::MemberExpression(member) => member.object,
ExpressionKind::TSNonNullExpression(non_null) => non_null.expression,
ExpressionKind::TaggedTemplateExpression(tagged) => tagged.tag,
ExpressionKind::ConditionalExpression(cond) => cond.test,
ExpressionKind::BinaryExpression(binary) => binary.left,
ExpressionKind::AssignmentExpression(assign) => assign.left,
ExpressionKind::SequenceExpression(seq) => seq.expressions.first()?,
_ => return None,
})
}
fn left_side_child_is_parenthesized(
expr: &ast::Expression<'_>,
child: &ast::Expression<'_>,
in_for_init: bool,
) -> bool {
use ast::ExpressionKind;
let ctx = match &expr.kind {
ExpressionKind::MemberExpression(_) => ParenContext::ChainBase,
ExpressionKind::CallExpression(_) => ParenContext::Callee,
ExpressionKind::TaggedTemplateExpression(_) => ParenContext::TaggedTemplateTag,
ExpressionKind::TSNonNullExpression(_) => ParenContext::NonNull,
ExpressionKind::AssignmentExpression(assign) => ParenContext::AssignmentTarget {
operator: assign.operator,
},
ExpressionKind::BinaryExpression(binary) => ParenContext::BinaryLeft {
parent_op: binary.operator,
},
ExpressionKind::ConditionalExpression(_) => return ternary_test_needs_parens(child),
_ => return false,
};
needs_parens(child, ctx, in_for_init)
}
pub(crate) fn leading_regex_start(expr: &ast::Expression<'_>) -> Option<u32> {
use ast::ExpressionKind;
let mut node = expr;
loop {
match &node.kind {
ExpressionKind::RegexLiteral(_) => return Some(node.span().start),
ExpressionKind::SequenceExpression(_) => return None,
_ => {}
}
let (child, parenthesized) = printed_left_spine_step(node, false)?;
if parenthesized {
return None;
}
node = child;
}
}
pub(in crate::printer) fn printed_left_spine_step<'e>(
node: &'e ast::Expression<'e>,
in_for_init: bool,
) -> Option<(&'e ast::Expression<'e>, bool)> {
use ast::ExpressionKind;
Some(match &node.kind {
ExpressionKind::TSAsExpression(cast) => (
cast.expression,
needs_parens(cast.expression, ParenContext::TypeAssertion, in_for_init),
),
ExpressionKind::TSSatisfiesExpression(cast) => (
cast.expression,
needs_parens(cast.expression, ParenContext::TypeAssertion, in_for_init),
),
ExpressionKind::TSInstantiationExpression(inst) => (
inst.expression,
needs_parens(
inst.expression,
ParenContext::InstantiationExpression { source_pair: false },
in_for_init,
),
),
ExpressionKind::UpdateExpression(update) if !update.prefix => (
update.argument,
needs_parens(
update.argument,
ParenContext::UpdateArgument { postfix: true },
in_for_init,
),
),
_ => {
let child = left_side_child(node)?;
(
child,
left_side_child_is_parenthesized(node, child, in_for_init),
)
}
})
}
fn shell_content_end(expr: &ast::Expression<'_>) -> u32 {
match &expr.kind {
ast::ExpressionKind::SequenceExpression(seq) => seq
.expressions
.last()
.map_or_else(|| expr.span().end, |last| last.span().end),
_ => expr.span().end,
}
}
pub(crate) fn paren_pair_keeps_leading_run(expr: &ast::Expression<'_>) -> bool {
matches!(
expr.kind,
ast::ExpressionKind::ArrowFunctionExpression(_)
| ast::ExpressionKind::FunctionExpression(_)
)
}
pub(crate) fn next_significant_byte(source: &str, start: u32, end: u32) -> Option<usize> {
let bytes = source.as_bytes();
let end = end as usize;
let mut i = start as usize;
while i < end {
if let Some(next) = skip_trivia(bytes, i, end, TriviaProfile::COMMENTS) {
i = next;
} else if let Some(ws) = js_whitespace_at(source, i) {
i += ws;
} else {
return Some(i);
}
}
None
}
pub(crate) fn js_whitespace_at(source: &str, i: usize) -> Option<usize> {
let byte = *source.as_bytes().get(i)?;
if byte < 0x80 {
return tsv_lang::is_js_whitespace(char::from(byte)).then_some(1);
}
let c = source.get(i..)?.chars().next()?;
tsv_lang::is_js_whitespace(c).then(|| c.len_utf8())
}
pub(crate) fn paren_shell_close_after(source: &str, operand_end: u32) -> Option<u32> {
let bytes = source.as_bytes();
let pos = next_significant_byte(source, operand_end, bytes.len() as u32)?;
(bytes[pos] == b')').then_some(pos as u32 + 1)
}
pub(crate) fn left_spine_shell_is_comment_free(source: &str, start: u32) -> bool {
let bytes = source.as_bytes();
let mut i = start as usize;
while let Some(&byte) = bytes.get(i) {
match byte {
b'(' | b' ' | b'\t' | b'\n' | b'\r' | 0x0B | 0x0C => i += 1,
_ => return byte != b'/' && byte.is_ascii(),
}
}
true
}
pub(super) fn left_spine_has_no_shell(source: &str, expr: &ast::Expression<'_>) -> bool {
source.as_bytes().get(expr.span().start as usize) != Some(&b'(')
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) struct LeftSpineClaim {
pub(crate) claimer: Span,
pub(crate) region: Span,
}
pub(crate) struct HoistedLeftSpine<'p, 'a, 'e>(Option<LeftSpineSteps<'p, 'a, 'e>>);
impl<'e> Iterator for HoistedLeftSpine<'_, '_, 'e> {
type Item = (&'e ast::Expression<'e>, &'e ast::Expression<'e>);
fn next(&mut self) -> Option<Self::Item> {
let step = self.0.as_mut()?.next();
match step {
Some(step) if !step.mid_chain => Some((step.node, step.child)),
_ => {
self.0 = None;
None
}
}
}
}
#[derive(Clone, Copy)]
pub(crate) struct LeftSpineStep<'e> {
pub(crate) node: &'e ast::Expression<'e>,
pub(crate) child: &'e ast::Expression<'e>,
pub(crate) mid_chain: bool,
}
pub(crate) struct LeftSpineSteps<'p, 'a, 'e> {
printer: &'p Printer<'a>,
node: Option<&'e ast::Expression<'e>>,
chain_root: Option<&'e ast::Expression<'e>>,
mid_chain_gaps: Option<SmallVec<[(u32, u32); 2]>>,
}
impl<'e> Iterator for LeftSpineSteps<'_, '_, 'e> {
type Item = LeftSpineStep<'e>;
fn next(&mut self) -> Option<Self::Item> {
let node = self.node.take()?;
let child = self.printer.hoisted_left_side_child(node)?;
let mut mid_chain = false;
if matches!(
node.kind,
ast::ExpressionKind::MemberExpression(_) | ast::ExpressionKind::CallExpression(_)
) {
let root = *self.chain_root.get_or_insert(node);
let (shell_start, child_start) = (node.span().start, child.span().start);
mid_chain = shell_start < child_start
&& self
.printer
.comments_in_source_between(shell_start, child_start)
.next()
.is_some()
&& self
.mid_chain_gaps
.get_or_insert_with(|| {
chain_mid_chain_paren_gaps(root, self.printer.linearize_input())
})
.contains(&(shell_start, child_start));
} else if !(self.chain_root.is_some()
&& matches!(node.kind, ast::ExpressionKind::TSNonNullExpression(_)))
{
self.chain_root = None;
self.mid_chain_gaps = None;
}
self.node = Some(child);
Some(LeftSpineStep {
node,
child,
mid_chain,
})
}
}
pub(crate) enum AsiOperandShell {
Shell(DocId),
Bare(Option<DocId>),
}
impl<'a> Printer<'a> {
pub(crate) fn hoisted_left_side_child<'e>(
&self,
expr: &'e ast::Expression<'e>,
) -> Option<&'e ast::Expression<'e>> {
use ast::ExpressionKind;
if let Some(operand) = self.bare_cast_operand(expr) {
return Some(operand);
}
if let ExpressionKind::TSInstantiationExpression(inst) = &expr.kind {
return (!self.instantiation_operand_prints_pair(inst, expr.span))
.then_some(inst.expression);
}
let child = left_side_child(expr)?;
let link_step = || {
self.chain_flattened_non_null_operand(child)
.unwrap_or(child)
};
match &expr.kind {
ExpressionKind::MemberExpression(_) | ExpressionKind::CallExpression(_) => {
chain_paren_leading_gap(expr, self.comments)
.is_none()
.then(link_step)
}
ExpressionKind::TSNonNullExpression(non_null) => {
(chain_paren_leading_gap(expr, self.comments).is_none()
&& !self.needs_parens(non_null.expression, ParenContext::NonNull))
.then(link_step)
}
ExpressionKind::TaggedTemplateExpression(tagged) => {
tag_paren_leading_start(tagged, expr.span)
.is_none()
.then_some(child)
}
ExpressionKind::AssignmentExpression(assign) => (!self.needs_parens(
assign.left,
ParenContext::AssignmentTarget {
operator: assign.operator,
},
))
.then_some(child),
_ => Some(child),
}
}
fn chain_flattened_non_null_operand<'e>(
&self,
child: &'e ast::Expression<'e>,
) -> Option<&'e ast::Expression<'e>> {
let ast::ExpressionKind::TSNonNullExpression(non_null) = &child.kind else {
return None;
};
(chain_paren_leading_gap(child, self.comments).is_none()
&& self.needs_parens(non_null.expression, ParenContext::NonNull))
.then_some(non_null.expression)
}
fn bare_cast_operand<'e>(
&self,
expr: &'e ast::Expression<'e>,
) -> Option<&'e ast::Expression<'e>> {
use ast::ExpressionKind;
let (operand, boundary) = match &expr.kind {
ExpressionKind::TSAsExpression(cast) => {
(cast.expression, cast.type_annotation.span().start)
}
ExpressionKind::TSSatisfiesExpression(cast) => {
(cast.expression, cast.type_annotation.span().start)
}
ExpressionKind::UpdateExpression(update) if !update.prefix => {
(update.argument, expr.span().end)
}
_ => return None,
};
let (_, parenthesized) = printed_left_spine_step(expr, self.in_for_init.get())?;
let shell_free = |start: u32, end: u32| {
start >= end || self.comments_in_source_between(start, end).next().is_none()
};
(!parenthesized
&& shell_free(expr.span().start, operand.span().start)
&& shell_free(operand.span().end, boundary))
.then_some(operand)
}
pub(crate) fn left_shell_paren_is_emitted(
&self,
expr: &ast::Expression<'_>,
child: &ast::Expression<'_>,
) -> bool {
expr.span().start != child.span().start
&& left_side_child_is_parenthesized(expr, child, self.in_for_init.get())
}
pub(crate) fn hoisted_left_spine<'p, 'e>(
&'p self,
expr: &'e ast::Expression<'e>,
) -> HoistedLeftSpine<'p, 'a, 'e> {
HoistedLeftSpine(Some(self.left_spine_steps(expr)))
}
pub(crate) fn left_spine_steps<'p, 'e>(
&'p self,
expr: &'e ast::Expression<'e>,
) -> LeftSpineSteps<'p, 'a, 'e> {
LeftSpineSteps {
printer: self,
node: Some(expr),
chain_root: None,
mid_chain_gaps: None,
}
}
pub(crate) fn left_spine_group_start(&self, node: &ast::Expression<'_>) -> Option<u32> {
match &node.kind {
ast::ExpressionKind::BinaryExpression(binary) => {
let mut link = binary;
while let Some(left) = self.flattenable_left(link) {
link = left;
}
Some(link.left.span().start)
}
_ => None,
}
}
pub(crate) fn comment_takes_line_in_group(&self, comment: &ast::Comment) -> bool {
!comment.is_block
|| comment.multiline
|| has_newline_before_position(self.source, comment.span.start)
}
pub(crate) fn stripped_left_side_child<'e>(
&self,
expr: &'e ast::Expression<'e>,
) -> Option<&'e ast::Expression<'e>> {
let child = self.hoisted_left_side_child(expr)?;
(!self.left_shell_paren_is_emitted(expr, child)).then_some(child)
}
pub(crate) fn stripped_left_spine_leaf<'e>(
&self,
expr: &'e ast::Expression<'e>,
) -> &'e ast::Expression<'e> {
let mut node = expr;
while let Some(child) = self.stripped_left_side_child(node) {
node = child;
}
node
}
pub(crate) fn left_spine_shell_has_own_line_comment(&self, expr: &ast::Expression<'_>) -> bool {
if left_spine_shell_is_comment_free(self.source, expr.span().start) {
return false;
}
let mut group_start = None;
self.left_spine_steps(expr).any(
|LeftSpineStep {
node,
child,
mid_chain,
}| {
let shell_start = node.span().start;
let child_start = child.span().start;
let in_group = group_start.is_some_and(|start| shell_start >= start);
if group_start.is_none() && !mid_chain {
group_start = self.left_spine_group_start(node);
}
shell_start < child_start
&& self
.comments_in_source_between(shell_start, child_start)
.any(|c| {
(!mid_chain || c.owned_by_node)
&& has_newline_after_position(self.source, c.span.end)
&& (!in_group || self.comment_takes_line_in_group(c))
})
},
)
}
#[inline]
pub(crate) fn left_spine_indentable_leads_value(&self, value: &ast::Expression<'_>) -> bool {
!left_spine_has_no_shell(self.source, value)
&& self.left_spine_indentable_leads_value_walk(value)
}
#[inline(never)]
fn left_spine_indentable_leads_value_walk(&self, value: &ast::Expression<'_>) -> bool {
if left_spine_shell_is_comment_free(self.source, value.span().start) {
return false;
}
self.left_spine_steps(value).any(|step| {
let shell_start = step.node.span().start;
let child_start = step.child.span().start;
shell_start < child_start
&& self
.comments_in_source_between(shell_start, child_start)
.any(|c| {
(!step.mid_chain || c.owned_by_node) && self.block_comment_is_indentable(c)
})
})
}
pub(crate) fn left_spine_printed_start(&self, expr: &ast::Expression<'_>) -> u32 {
if left_spine_has_no_shell(self.source, expr) {
return expr.span().start;
}
self.hoisted_left_spine(expr)
.last()
.map_or(expr, |(_, leaf)| leaf)
.span()
.start
}
#[inline]
pub(crate) fn left_spine_run_claim(
&self,
node: Span,
start: u32,
child: &ast::Expression<'_>,
child_in_pair: impl FnOnce() -> bool,
) -> Option<LeftSpineClaim> {
if left_spine_has_no_shell(self.source, child) {
return None;
}
self.left_spine_run_claim_walk(node, start, child, child_in_pair)
}
#[inline(never)]
fn left_spine_run_claim_walk(
&self,
node: Span,
start: u32,
child: &ast::Expression<'_>,
child_in_pair: impl FnOnce() -> bool,
) -> Option<LeftSpineClaim> {
if left_spine_shell_is_comment_free(self.source, child.span().start)
|| self
.claimed_left_spine_run
.get()
.is_some_and(|claim| claim.region.start <= start && start < claim.region.end)
|| !self.has_comments_to_emit_between(start, child.span().end)
{
return None;
}
let mut below_pair = child_in_pair().then(|| child.span().start);
let mut leaf = child;
for (node, next) in self.hoisted_left_spine(child) {
if !matches!(
node.kind,
ast::ExpressionKind::MemberExpression(_)
| ast::ExpressionKind::CallExpression(_)
| ast::ExpressionKind::TSNonNullExpression(_)
| ast::ExpressionKind::BinaryExpression(_)
| ast::ExpressionKind::TaggedTemplateExpression(_)
| ast::ExpressionKind::ConditionalExpression(_)
| ast::ExpressionKind::SequenceExpression(_)
| ast::ExpressionKind::TSAsExpression(_)
| ast::ExpressionKind::TSSatisfiesExpression(_)
| ast::ExpressionKind::UpdateExpression(_)
| ast::ExpressionKind::TSInstantiationExpression(_)
) {
break;
}
if below_pair.is_none()
&& left_side_child_is_parenthesized(node, next, self.in_for_init.get())
{
below_pair = Some(next.span().start);
}
leaf = next;
}
let leaf_start = leaf.span().start;
let below_pair = below_pair?;
(below_pair < leaf_start
&& self.has_comments_to_emit_between(below_pair, leaf_start)
&& !self.left_spine_region_stays_per_site(start, leaf_start))
.then(|| LeftSpineClaim {
claimer: node,
region: Span::new(start, leaf_start),
})
}
fn left_spine_region_stays_per_site(&self, start: u32, end: u32) -> bool {
self.comments_in_source_between(start, end).any(|c| {
self.is_honored_directive(c)
|| (c.is_block && tsv_lang::is_tree_shaking_annotation(c.content(self.source)))
})
}
#[expect(clippy::inline_always)]
#[inline(always)]
pub(crate) fn with_left_spine_claim<T>(
&self,
claim: Option<LeftSpineClaim>,
build: impl FnOnce() -> T,
) -> T {
let saved = claim.map(|claim| self.claimed_left_spine_run.replace(Some(claim)));
let out = build();
if let Some(saved) = saved {
self.claimed_left_spine_run.set(saved);
}
out
}
pub(crate) fn build_paren_leading_value_doc(
&self,
open_paren_end: u32,
value_start: u32,
value_doc: DocId,
) -> ParenLeadingValue {
let (paren_line, pull_pos) = match self.paren_line_share_anchor(open_paren_end, value_start)
{
Some(paren) => {
let (prefix, pull_pos) = self.delimiter_line_comment_prefix(paren, value_start);
(prefix.into_iter().collect(), pull_pos)
}
None => (DocBuf::new(), None),
};
let run = self.build_leading_comment_run_with_break(
open_paren_end,
value_start,
LeadingGlue::AdjacentStrippedParen,
pull_pos,
);
let (value, run_breaks) = match run {
Some((run, force_break)) => (self.d().concat(&[run, value_doc]), force_break),
None => (value_doc, false),
};
ParenLeadingValue {
forces_break: run_breaks || pull_pos.is_some(),
paren_line,
value,
}
}
fn paren_line_share_anchor(&self, gap_start: u32, value_start: u32) -> Option<u32> {
let paren = find_char_skipping_comments(
self.source.as_bytes(),
gap_start as usize,
value_start as usize,
b'(',
)? as u32;
self.comments_to_emit_between(gap_start, value_start)
.next()
.is_some_and(|first| first.span.start > paren)
.then_some(paren)
}
pub(crate) fn append_trailing_paren_comments(
&self,
parts: &mut DocBuf,
argument_end: u32,
span_end: u32,
) -> bool {
let mut deferred_run = false;
let mut prev_end = argument_end;
for comment in self.comments_to_emit_between(argument_end, span_end) {
let own_line = self.comment_has_newline_between(prev_end, comment.span.start);
parts.push(if own_line {
self.build_trailing_comment_doc_own_line_blank(
comment,
self.previous_line_is_empty(
self.blank_scan_start(prev_end, comment.span.start),
comment.span.start,
),
)
} else if deferred_run || !comment.is_block {
self.build_trailing_line_comment_doc(comment)
} else {
self.build_trailing_comment_doc(comment)
});
deferred_run |= own_line || !comment.is_block;
prev_end = comment.span.end;
}
if deferred_run {
self.deferred_paren_runs
.set(self.deferred_paren_runs.get() + 1);
}
deferred_run
}
pub(crate) fn build_list_element_doc(
&self,
element_start: u32,
build: impl FnOnce() -> DocId,
) -> DocId {
let before = self.deferred_paren_runs.get();
let doc = build();
if self.deferred_paren_runs.get() == before {
return doc;
}
self.deferred_paren_runs.set(before);
self.forced_list_elements.borrow_mut().push(element_start);
let d = self.d();
d.concat(&[doc, d.flush_break()])
}
pub(crate) fn push_deferred_run_end(&self, parts: &mut DocBuf, value: DocId) {
let d = self.d();
if d.holds_line_suffix(value) {
let pending = d.pending_run_at_end(value, self.source);
parts.push(d.embed_end(pending, false).doc());
}
}
pub(crate) fn span_holds_forced_list_break(&self, span: Span) -> bool {
self.forced_list_elements
.borrow()
.iter()
.any(|&start| span.start <= start && start < span.end)
}
pub(crate) fn with_line_ending_construct<T>(&self, build: impl FnOnce() -> T) -> T {
let before = self.deferred_paren_runs.get();
let built = build();
self.deferred_paren_runs.set(before);
built
}
pub(in crate::printer) fn split_terminator_gap_comments(
&self,
parts: &mut DocBuf,
argument_end: u32,
span_end: u32,
operand_parens_printed: bool,
gap: TerminatorGap,
) -> DocBuf {
let clause_tail = gap.clause_tail();
let d = self.d();
let mut deferred = DocBuf::new();
let mut prev_end = argument_end;
let mut run_deferred = false;
let claim_end = self.node_terminator_claim_end(argument_end, span_end, gap);
for comment in self.comments_to_emit_between(argument_end, claim_end) {
let same_line = !self.has_newline_between(prev_end, comment.span.start);
let operand_enclosed =
same_line && self.gap_has_close_paren(comment.span.end, span_end);
if comment.is_block && operand_enclosed && operand_parens_printed {
parts.push(d.text(" "));
parts.push(self.build_comment_doc(comment));
} else if same_line {
deferred.push(if run_deferred {
self.build_trailing_line_comment_doc(comment)
} else {
self.build_trailing_comment_doc(comment)
});
run_deferred |= !comment.is_block;
} else {
let blank = self.has_blank_line_between(prev_end, comment.span.start);
deferred.push(match clause_tail {
Some(dedent) => self.build_clause_tail_comment_doc(comment, blank, dedent),
None => self.build_trailing_comment_doc_own_line_blank(comment, blank),
});
run_deferred = true;
}
prev_end = comment.span.end;
}
deferred
}
pub(crate) fn gap_has_close_paren(&self, start: u32, end: u32) -> bool {
find_char_skipping_comments(self.source.as_bytes(), start as usize, end as usize, b')')
.is_some()
}
pub(crate) fn append_paren_interior_trailing_comments(
&self,
parts: &mut DocBuf,
interior: Span,
) {
let d = self.d();
let argument_end = interior.start;
for comment in self.comments_to_emit_between(argument_end, interior.end) {
if self.has_newline_between(argument_end, comment.span.start) {
continue;
}
if comment.is_block {
parts.push(d.text(" "));
parts.push(self.build_comment_doc(comment));
} else {
parts
.push(d.line_suffix(d.concat(&[d.text(" "), self.build_comment_doc(comment)])));
}
}
}
pub(crate) fn paren_interior_own_line_comments(
&self,
interior: Option<Span>,
) -> CommentVec<'_> {
let Some(interior) = interior else {
return CommentVec::new();
};
let arg_end = interior.start;
self.comments_to_emit_between(arg_end, interior.end)
.filter(|c| self.has_newline_between(arg_end, c.span.start))
.collect()
}
pub(crate) fn paren_interior_separates_run(&self, interior: Option<Span>) -> bool {
!self.paren_interior_own_line_comments(interior).is_empty()
|| self.paren_interior_defers_line_comment(interior)
}
pub(crate) fn push_element_share_and_run<'c>(
&self,
parts: &mut DocBuf,
interior: Option<Span>,
site: ShareSite,
run: impl IntoIterator<Item = &'c ast::Comment>,
) -> bool {
self.push_element_share_and_run_with_blanks(parts, interior, site, true, run)
}
pub(crate) fn push_element_share_and_run_with_blanks<'c>(
&self,
parts: &mut DocBuf,
interior: Option<Span>,
site: ShareSite,
preserve_blanks: bool,
run: impl IntoIterator<Item = &'c ast::Comment>,
) -> bool {
let share = self.paren_interior_own_line_comments(interior);
let line_ends_in_line_comment = share.last().map_or_else(
|| self.paren_interior_defers_line_comment(interior),
|last| !last.is_block,
);
let pushed =
self.push_paren_interior_own_line_comments(parts, interior, site, preserve_blanks);
let run: CommentVec<'_> = run.into_iter().collect();
let Some(first) = run.first() else {
return pushed;
};
let run_has_line_comment = run.last().is_some_and(|c| !c.is_block);
let fresh_line =
line_ends_in_line_comment || (site == ShareSite::NextElement && !run_has_line_comment);
if fresh_line {
let scan_from = first.span.start;
self.push_trailing_comment_run(parts, run.iter().copied(), scan_from);
} else {
let d = self.d();
for comment in &run {
if comment.is_block {
parts.push(d.text(" "));
parts.push(self.build_comment_doc(comment));
} else {
parts.push(self.build_trailing_line_comment_doc(comment));
}
}
}
pushed
}
pub(crate) fn comment_hugs_next_item(&self, comment_end: u32, next_pos: u32) -> bool {
self.is_same_line(comment_end, next_pos)
|| super::calls::has_stripped_paren_gap(self.source, comment_end, next_pos)
}
pub(crate) fn blank_before_next_item(&self, from: u32, next_start: u32) -> bool {
let next = self
.comments_in_source_between(from, next_start)
.next()
.map_or(next_start, |c| c.span.start);
tsv_lang::printing::has_blank_line_between_strict(self.source, from, next)
}
pub(crate) fn run_leads_next_item(
&self,
run: &[&ast::Comment],
comma: Option<u32>,
next_start: u32,
) -> bool {
let (Some(first), Some(last)) = (run.first(), run.last()) else {
return false;
};
if !run.iter().all(|c| c.is_block) || self.blank_before_next_item(last.span.end, next_start)
{
return false;
}
if self.comment_hugs_next(last) {
return true;
}
let before_comma = comma.is_some_and(|comma| last.span.start < comma);
before_comma && !self.line_holds_only_closers_before(first.span.start)
}
fn line_holds_only_closers_before(&self, pos: u32) -> bool {
self.source.as_bytes()[..pos as usize]
.iter()
.rev()
.take_while(|&&b| b != b'\n' && b != b'\r')
.all(|&b| matches!(b, b' ' | b'\t' | b')'))
}
pub(crate) fn push_run_leading_next_item(
&self,
parts: &mut DocBuf,
run: &[&ast::Comment],
next_start: u32,
) {
let d = self.d();
for (i, comment) in run.iter().enumerate() {
parts.push(self.build_comment_doc(comment));
let next = run.get(i + 1).copied().or_else(|| {
self.comments_to_emit_between(comment.span.end, next_start)
.next()
});
parts.push(if self.directive_follows(next) {
d.hardline()
} else {
d.text(" ")
});
}
}
pub(crate) fn paren_interior_forces_expansion(&self, interior: Option<Span>) -> bool {
interior.is_some_and(|interior| {
let arg_end = interior.start;
self.comments_to_emit_between(arg_end, interior.end)
.any(|c| !c.is_block || self.has_newline_between(arg_end, c.span.start))
})
}
pub(crate) fn any_paren_interior_forces_expansion(
&self,
elements: &[&ast::Expression<'_>],
) -> bool {
elements
.iter()
.any(|e| self.paren_interior_forces_expansion(e.paren_interior()))
}
pub(crate) fn paren_interior_defers_line_comment(&self, interior: Option<Span>) -> bool {
interior.is_some_and(|interior| {
let arg_end = interior.start;
self.comments_to_emit_between(arg_end, interior.end)
.any(|c| !c.is_block && !self.has_newline_between(arg_end, c.span.start))
})
}
pub(crate) fn push_paren_interior_own_line_comments(
&self,
parts: &mut DocBuf,
interior: Option<Span>,
site: ShareSite,
preserve_blanks: bool,
) -> bool {
let Some(interior) = interior else {
return false;
};
let comments = self.paren_interior_own_line_comments(Some(interior));
let mut prev_end = interior.start;
let mut prev_comment: Option<&ast::Comment> = None;
for comment in &comments {
if site == ShareSite::Closer
&& self.trailing_run_hugs_previous(prev_comment, comment.span.start)
{
parts.push(self.d().text(" "));
} else if preserve_blanks {
self.push_blank_preserving_hardline(parts, prev_end, comment.span.start);
} else {
parts.push(self.d().hardline());
}
parts.push(self.build_comment_doc(comment));
prev_end = comment.span.end;
prev_comment = Some(comment);
}
!comments.is_empty()
}
#[inline]
pub(crate) fn has_trailing_paren_comments(&self, expr_end: u32, boundary_end: u32) -> bool {
!range_too_narrow_for_a_comment(expr_end, boundary_end)
&& self.has_trailing_paren_comments_wide(expr_end, boundary_end)
}
#[inline(never)]
fn has_trailing_paren_comments_wide(&self, expr_end: u32, boundary_end: u32) -> bool {
self.has_comments_on_page_between(expr_end, boundary_end)
&& self
.collapsed_grouping_close(expr_end, boundary_end)
.is_some_and(|close| self.has_comments_on_page_between(expr_end, close))
}
pub(in crate::printer) fn collapsed_grouping_close(
&self,
expr_end: u32,
boundary_end: u32,
) -> Option<u32> {
let bytes = self.source.as_bytes();
let mut i = expr_end;
let mut close = None;
while let Some(pos) = self.next_significant_byte(i, boundary_end) {
if bytes[pos] != b')' {
break;
}
close = Some(pos as u32);
i = pos as u32 + 1;
}
close
}
fn build_shell_sequence_doc(
&self,
seq: &ast::SequenceExpression<'_>,
span: Span,
expr_end: u32,
boundary_end: u32,
layout: SeqLayout,
) -> DocId {
let grouping_close = self
.collapsed_grouping_close(expr_end, boundary_end)
.unwrap_or(boundary_end);
self.build_sequence_doc_value(seq, span, grouping_close, layout)
}
pub(in crate::printer) fn next_significant_byte(&self, start: u32, end: u32) -> Option<usize> {
next_significant_byte(self.source, start, end)
}
pub(crate) fn asi_gap_needs_parens(&self, expr_end: u32, boundary_end: u32) -> bool {
if !self.has_comments_on_page_between(expr_end, boundary_end) {
return false;
}
let Some(close) = self.collapsed_grouping_close(expr_end, boundary_end) else {
return false;
};
self.has_line_spanning_comments_to_emit_between(expr_end, close)
}
fn redundant_shell_holds_line_spanning_owned_comment(
&self,
leading_start: u32,
expr: &ast::Expression<'_>,
operand_ctx: ParenContext,
) -> bool {
!matches!(expr.kind, ast::ExpressionKind::SequenceExpression(_))
&& !self.needs_parens(expr, operand_ctx)
&& self
.comments_in_source_between(leading_start, expr.span().start)
.any(|c| c.multiline)
}
pub(crate) fn build_asi_operand_shell_doc(
&self,
node_start: u32,
expr: &ast::Expression<'_>,
boundary_end: u32,
operand_ctx: ParenContext,
) -> AsiOperandShell {
let expr_start = expr.span().start;
let expr_end = shell_content_end(expr);
let open = find_char_skipping_comments(
self.source.as_bytes(),
node_start as usize,
expr_start as usize,
b'(',
)
.map(|p| p as u32);
let leading_start = open.map_or(node_start, |p| p + 1);
let has_leading = open.is_some()
&& (self.has_comments_to_emit_between(leading_start, expr_start)
|| self.redundant_shell_holds_line_spanning_owned_comment(
leading_start,
expr,
operand_ctx,
));
let needs_trailing = self.asi_gap_needs_parens(expr_end, boundary_end);
if !has_leading && !needs_trailing {
return AsiOperandShell::Bare(None);
}
if !needs_trailing
&& self.leading_run_reparses_owned(leading_start, expr_start, ReparsedRun::SingleLine)
{
let run = self
.build_inline_comments_between_doc_trailing_space_opt(leading_start, expr_start);
if let (ast::ExpressionKind::SequenceExpression(seq), Some(run)) = (&expr.kind, run) {
let d = self.d();
let mut pair: DocBuf = smallvec![
d.text("("),
run,
self.build_sequence_doc_bare(seq, expr.span, expr_end),
d.text(")"),
];
if let Some(trailing) =
self.build_inline_comments_between_doc_opt(expr_end, boundary_end)
{
pair.push(trailing);
}
return AsiOperandShell::Shell(d.concat(&pair));
}
return AsiOperandShell::Bare(run);
}
let close = self.collapsed_grouping_close(expr_end, boundary_end);
let inner_end = close.map_or(boundary_end, |c| c + 1);
if matches!(expr.kind, ast::ExpressionKind::SequenceExpression(_)) && !has_leading {
let seq =
self.build_expression_doc_keep_paren_comments(expr, inner_end, SeqLayout::Aligned);
return AsiOperandShell::Shell(self.append_shell_outside_run(seq, close, boundary_end));
}
self.expr_stmt_paren_target.set(None);
let (paren_trailing, run_start) =
self.split_located_paren_glued_run(leading_start, open, expr_start);
let leading_run = self.build_rhs_comments_opt(run_start, expr_start);
let mut body = DocBuf::new();
if let Some(run) = leading_run {
body.push(run);
}
body.push(match &expr.kind {
ast::ExpressionKind::SequenceExpression(seq) => {
self.build_sequence_doc_bare(seq, expr.span, expr_end)
}
_ => self.build_left_spine_operand_doc(
leading_start,
expr,
FrozenOperandPair::Emitted,
|| self.build_expression_doc_claiming_outermost(expr),
),
});
if let Some((trailing, _needs_break)) =
self.trailing_paren_comment_parts(expr_end, inner_end)
{
body.extend(trailing);
}
let shell = self.compose_expanded_shell_doc(paren_trailing, &body, ")");
AsiOperandShell::Shell(self.append_shell_outside_run(shell, close, boundary_end))
}
fn leading_run_reparses_owned(
&self,
gap_start: u32,
operand_start: u32,
lines: ReparsedRun,
) -> bool {
let mut run = self.comments_in_source_between(gap_start, operand_start);
let (Some(comment), None) = (run.next(), run.next()) else {
return false;
};
comment.is_block
&& (!comment.multiline || lines == ReparsedRun::AnyLines)
&& !comment.owned_by_node
&& self.comment_hugs_next(comment)
}
fn append_shell_outside_run(
&self,
shell: DocId,
close: Option<u32>,
boundary_end: u32,
) -> DocId {
let Some(start) = close.map(|c| c + 1).filter(|&start| {
start < boundary_end && self.has_comments_to_emit_between(start, boundary_end)
}) else {
return shell;
};
self.d().concat(&[
shell,
self.build_inline_comments_between_doc(start, boundary_end),
])
}
pub(in crate::printer) fn compose_expanded_shell_doc(
&self,
paren_trailing: Option<DocId>,
body: &DocBuf,
close: &'static str,
) -> DocId {
let d = self.d();
let open_doc = match paren_trailing {
Some(comment) => d.concat(&[d.text("("), comment]),
None => d.text("("),
};
d.concat(&[
open_doc,
d.indent_hardline(d.concat(body)),
d.hardline(),
d.text(close),
])
}
pub(in crate::printer) fn build_leading_run_expanded_shell_doc(
&self,
gap_start: u32,
operand_start: u32,
trailing_gap: Option<(u32, u32)>,
inner_doc: DocId,
close: &'static str,
) -> DocId {
let (paren_trailing, run_start) =
self.split_open_delimiter_glued_run(gap_start, operand_start);
let mut body = DocBuf::new();
if let Some(run) = self.build_rhs_comments_opt(run_start, operand_start) {
body.push(run);
}
body.push(inner_doc);
if let Some((start, end)) = trailing_gap {
self.push_shell_trailing_run(&mut body, start, end);
}
self.compose_expanded_shell_doc(paren_trailing, &body, close)
}
fn push_shell_trailing_run(&self, body: &mut DocBuf, start: u32, end: u32) {
if self.has_line_comments_between(start, end) {
self.push_anchored_trailing_run(body, start, end, RunLeadingBlank::Keep);
} else if self.has_comments_to_emit_between(start, end) {
body.push(self.build_chain_block_comments_doc(start, end, CommentSpacing::Leading));
}
}
pub(in crate::printer) fn build_required_pair_leading_shell_doc(
&self,
gap_start: u32,
operand_start: u32,
trailing_gap: Option<(u32, u32)>,
inner: DocId,
close: &'static str,
) -> Option<DocId> {
self.required_pair_takes_leading_shell(gap_start, operand_start)
.then(|| {
self.build_leading_run_expanded_shell_doc(
gap_start,
operand_start,
trailing_gap,
inner,
close,
)
})
}
fn required_pair_takes_leading_shell(&self, gap_start: u32, operand_start: u32) -> bool {
self.has_comments_to_emit_between(gap_start, operand_start)
&& self.has_newline_between(gap_start, operand_start)
&& !self.leading_run_reparses_owned(gap_start, operand_start, ReparsedRun::AnyLines)
}
pub(in crate::printer) fn paren_shell_close_after(&self, operand_end: u32) -> Option<u32> {
paren_shell_close_after(self.source, operand_end)
}
pub(in crate::printer) fn owned_pair_trailing_gap(
&self,
operand_end: u32,
keeps_interior: bool,
) -> Option<(u32, u32)> {
keeps_interior
.then(|| self.paren_shell_close_after(operand_end))
.flatten()
.map(|close| (operand_end, close))
}
pub(in crate::printer) fn pair_gaps_are_comment_free(
&self,
leading: (u32, u32),
trailing: Option<(u32, u32)>,
) -> bool {
!self.has_comments_on_page_between(leading.0, leading.1)
&& trailing.is_none_or(|(start, end)| !self.has_comments_on_page_between(start, end))
}
pub(in crate::printer) fn gap_start_after_owned_pair(
operand_end: u32,
trailing_gap: Option<(u32, u32)>,
) -> u32 {
trailing_gap.map_or(operand_end, |(_, close)| close)
}
pub(in crate::printer) fn build_owned_required_pair_doc(
&self,
leading_gap: (u32, u32),
trailing_gap: Option<(u32, u32)>,
body: DocId,
close: &'static str,
) -> DocId {
self.build_shaped_owned_required_pair_doc(
leading_gap,
trailing_gap,
|| body,
|lead, trailing| self.concat_pair_runs(lead, body, trailing),
close,
)
}
pub(in crate::printer) fn concat_pair_runs(
&self,
lead: Option<DocId>,
content: DocId,
trailing: Option<DocId>,
) -> DocId {
match (lead, trailing) {
(None, None) => content,
(Some(lead), None) => self.d().concat(&[lead, content]),
(None, Some(trailing)) => self.d().concat(&[content, trailing]),
(Some(lead), Some(trailing)) => self.d().concat(&[lead, content, trailing]),
}
}
pub(in crate::printer) fn build_shaped_owned_required_pair_doc(
&self,
leading_gap: (u32, u32),
trailing_gap: Option<(u32, u32)>,
plain: impl FnOnce() -> DocId,
fold_shaped: impl FnOnce(Option<DocId>, Option<DocId>) -> DocId,
close: &'static str,
) -> DocId {
let d = self.d();
let (gap_start, operand_start) = leading_gap;
if self.required_pair_takes_leading_shell(gap_start, operand_start) {
return self.build_leading_run_expanded_shell_doc(
gap_start,
operand_start,
trailing_gap,
plain(),
close,
);
}
let lead = || self.build_rhs_comments_opt(gap_start, operand_start);
let mut trailing = None;
if let Some((start, end)) = trailing_gap {
if self.has_line_spanning_comments_to_emit_between(start, end) {
let body = plain();
let body = lead().map_or(body, |lead| d.concat(&[lead, body]));
return self.build_trailing_run_expanded_shell_doc(start, end, body, close);
} else if self.has_comments_to_emit_between(start, end) {
trailing =
Some(self.build_chain_block_comments_doc(start, end, CommentSpacing::Leading));
}
}
d.concat(&[d.text("("), fold_shaped(lead(), trailing), d.text(close)])
}
pub(in crate::printer) fn build_shell_operand_doc(
&self,
node_start: u32,
target: &ast::Expression<'_>,
context: ParenContext,
) -> DocId {
self.build_shell_operand_doc_with(node_start, target, context, || {
self.build_expression_doc(target)
})
}
pub(in crate::printer) fn build_shell_operand_doc_with(
&self,
node_start: u32,
target: &ast::Expression<'_>,
context: ParenContext,
build: impl FnOnce() -> DocId,
) -> DocId {
let d = self.d();
let target_start = target.span().start;
let needs_parens = self.needs_parens(target, context);
if !self.has_comments_to_emit_between(node_start, target_start) {
let inner = build();
return if needs_parens { d.parens(inner) } else { inner };
}
let open = find_char_skipping_comments(
self.source.as_bytes(),
node_start as usize,
target_start as usize,
b'(',
)
.map(|p| p as u32);
let leading_start = open.map_or(node_start, |p| p + 1);
let inner = build();
if needs_parens
&& let Some(shell) = self.build_required_pair_leading_shell_doc(
leading_start,
target_start,
None,
inner,
")",
)
{
return shell;
}
let lead = self.removed_paren_comments_opt(
Span::new(node_start, target.span().end),
leading_start,
target_start,
);
let core = match lead {
Some(lead) => d.concat(&[lead, inner]),
None => inner,
};
if needs_parens { d.parens(core) } else { core }
}
pub(crate) fn build_expression_doc_with_paren_comments(
&self,
expr: &ast::Expression<'_>,
boundary_end: u32,
position_parens: bool,
) -> DocId {
self.build_shell_value_doc(
expr,
boundary_end,
self.shell_tail(boundary_end),
position_parens,
None,
)
}
fn shell_tail(&self, boundary_end: u32) -> ShellTail {
if self.shell_closes_for_clause(boundary_end) {
ShellTail::ForClauseSeparator
} else {
ShellTail::StatementTerminator
}
}
pub(crate) fn shell_closes_for_clause(&self, boundary_end: u32) -> bool {
self.for_clause_end.get() == Some(boundary_end)
}
pub(crate) fn shell_supplies_for_init_pair(
&self,
expr: &ast::Expression<'_>,
boundary: Option<u32>,
) -> bool {
boundary.is_some_and(|end| {
self.shell_closes_for_clause(end) || self.shell_value_keeps_own_parens(expr, end, false)
})
}
pub(crate) fn build_frozen_value_shell_doc(
&self,
expr: &ast::Expression<'_>,
frozen: Span,
boundary_end: u32,
position_parens: bool,
) -> DocId {
self.build_shell_value_doc(
expr,
boundary_end,
self.shell_tail(boundary_end),
position_parens,
Some(frozen),
)
}
pub(crate) fn shell_value_keeps_own_parens(
&self,
expr: &ast::Expression<'_>,
boundary_end: u32,
position_parens: bool,
) -> bool {
self.value_shell_forced_open(expr, boundary_end)
|| (position_parens && self.value_shell_pair_in_question(expr, boundary_end))
}
fn value_shell_pair_in_question(&self, expr: &ast::Expression<'_>, boundary_end: u32) -> bool {
!matches!(expr.kind, ast::ExpressionKind::SequenceExpression(_))
&& self.has_trailing_paren_comments(expr.span().end, boundary_end)
}
pub(crate) fn wrap_value_position_parens(
&self,
expr: &ast::Expression<'_>,
boundary_end: u32,
position_parens: bool,
inner: DocId,
) -> DocId {
if position_parens
&& !self.shell_value_keeps_own_parens(expr, boundary_end, position_parens)
{
self.d().parens(inner)
} else {
inner
}
}
fn shell_gap_retains_parens(
&self,
expr_end: u32,
boundary_end: u32,
position_parens: bool,
) -> bool {
position_parens || self.shell_gap_holds_unplaceable_comment(expr_end, boundary_end)
}
fn shell_gap_holds_unplaceable_comment(&self, expr_end: u32, boundary_end: u32) -> bool {
self.comments_on_page_between(expr_end, boundary_end)
.any(|c| !c.is_block || self.has_newline_between(expr_end, c.span.start))
}
pub(in crate::printer) fn value_shell_forced_open(
&self,
expr: &ast::Expression<'_>,
boundary_end: u32,
) -> bool {
self.value_shell_pair_in_question(expr, boundary_end)
&& self.shell_gap_holds_unplaceable_comment(expr.span().end, boundary_end)
}
pub(crate) fn build_for_init_value_doc(
&self,
expr: &ast::Expression<'_>,
boundary_end: u32,
position_parens: bool,
frozen: Option<Span>,
) -> DocId {
self.build_shell_value_doc(
expr,
boundary_end,
ShellTail::ForClauseSeparator,
position_parens,
frozen,
)
}
pub(crate) fn build_for_clause_operand_doc(
&self,
expr: &ast::Expression<'_>,
boundary_end: u32,
frozen: Option<Span>,
) -> DocId {
self.build_for_init_value_doc(expr, boundary_end, false, frozen)
}
fn build_shell_inner_doc(&self, expr: &ast::Expression<'_>, frozen: Option<Span>) -> DocId {
match frozen {
Some(frozen) => self.build_frozen_expression_doc(expr, frozen),
None => self.build_expression_doc(expr),
}
}
pub(in crate::printer) fn shell_trailing_gap_start(
&self,
expr: &ast::Expression<'_>,
boundary_end: u32,
frozen: Option<Span>,
printer_owns_grouping: bool,
) -> u32 {
let span_end = expr.span().end;
let unfrozen = if matches!(expr.kind, ast::ExpressionKind::SequenceExpression(_))
&& (printer_owns_grouping
|| self
.collapsed_grouping_close(span_end, boundary_end)
.is_some())
{
shell_content_end(expr)
} else {
span_end
};
Self::element_claim_anchor(frozen, unfrozen)
}
fn build_shell_value_doc(
&self,
expr: &ast::Expression<'_>,
boundary_end: u32,
tail: ShellTail,
position_parens: bool,
frozen: Option<Span>,
) -> DocId {
let expr_end = self.shell_trailing_gap_start(expr, boundary_end, frozen, false);
let wrap_in = |doc: DocId| match (tail, frozen) {
(ShellTail::ForClauseSeparator, Some(frozen)) => {
self.wrap_frozen_for_init_in(expr, frozen, position_parens, doc)
}
(ShellTail::ForClauseSeparator, None) => self.wrap_for_init_in(expr, doc),
(ShellTail::StatementTerminator, _) => doc,
};
if tail == ShellTail::StatementTerminator
&& frozen.is_none()
&& self.statement_tail_may_take(boundary_end)
&& let Some(doc) =
self.build_handed_shell_value_doc(expr, expr_end, boundary_end, position_parens)
{
return doc;
}
if !self.has_trailing_paren_comments(expr_end, boundary_end) {
return wrap_in(self.build_shell_inner_doc(expr, frozen));
}
if let ast::ExpressionKind::SequenceExpression(seq) = &expr.kind {
return match frozen {
Some(frozen) => self.build_frozen_kept_paren_doc(frozen, boundary_end),
None if tail == ShellTail::ForClauseSeparator
&& self.shell_gap_holds_unplaceable_comment(expr_end, boundary_end) =>
{
let inner = self.build_sequence_doc_bare(seq, expr.span, expr_end);
self.build_kept_paren_shell_doc(inner, expr_end, boundary_end)
.unwrap_or_else(|| self.d().parens(inner))
}
None => self.build_shell_sequence_doc(
seq,
expr.span,
expr_end,
boundary_end,
SeqLayout::Aligned,
),
};
}
if let Some(frozen) = frozen
&& self.shell_gap_retains_parens(expr_end, boundary_end, position_parens)
{
return self.build_frozen_kept_paren_doc(frozen, boundary_end);
}
self.build_stripped_shell_tail_doc_with(
expr,
expr_end,
boundary_end,
tail,
position_parens,
|| wrap_in(self.build_shell_inner_doc(expr, frozen)),
)
}
#[cold]
#[inline(never)]
fn build_handed_shell_value_doc(
&self,
expr: &ast::Expression<'_>,
expr_end: u32,
boundary_end: u32,
position_parens: bool,
) -> Option<DocId> {
if matches!(expr.kind, ast::ExpressionKind::SequenceExpression(_)) {
return None;
}
let retains = self.shell_gap_retains_parens(expr_end, boundary_end, position_parens);
let region = self.shell_statement_tail_region(expr, boundary_end, retains)?;
Some(self.with_statement_tail_claim(region, || self.build_expression_doc(expr)))
}
pub(in crate::printer) fn build_stripped_shell_tail_doc(
&self,
expr: &ast::Expression<'_>,
expr_end: u32,
boundary_end: u32,
inner: impl FnOnce() -> DocId,
) -> DocId {
let tail = self.shell_tail(boundary_end);
if tail == ShellTail::StatementTerminator
&& self.statement_tail_may_take(boundary_end)
&& let Some(region) = self.shell_statement_tail_region(
expr,
boundary_end,
self.shell_gap_retains_parens(expr_end, boundary_end, false),
)
{
return self.with_statement_tail_claim(region, inner);
}
self.build_stripped_shell_tail_doc_with(expr, expr_end, boundary_end, tail, false, inner)
}
fn build_stripped_shell_tail_doc_with(
&self,
expr: &ast::Expression<'_>,
expr_end: u32,
boundary_end: u32,
tail: ShellTail,
position_parens: bool,
inner: impl FnOnce() -> DocId,
) -> DocId {
if !self.has_trailing_paren_comments(expr_end, boundary_end) {
return inner();
}
if self.shell_gap_retains_parens(expr_end, boundary_end, position_parens) {
return self.build_expression_doc_keep_paren_comments(
expr,
boundary_end,
SeqLayout::Aligned,
);
}
let d = self.d();
let inner = inner();
if tail == ShellTail::StatementTerminator
&& self.shell_meets_statement_terminator(boundary_end)
{
let mut parts: DocBuf = smallvec![inner];
for comment in self.comments_to_emit_between(expr_end, boundary_end) {
let suffix = d.concat(&[d.text(" "), self.build_comment_doc(comment)]);
parts.push(d.line_suffix(suffix));
}
return d.concat(&parts);
}
let comments = self.build_comments_between(expr_end, boundary_end, CommentSpacing::Leading);
d.concat(&[inner, comments])
}
fn shell_meets_statement_terminator(&self, boundary_end: u32) -> bool {
let bytes = self.source.as_bytes();
self.next_significant_byte(boundary_end, bytes.len() as u32)
.is_some_and(|pos| bytes[pos] == b';')
}
pub(crate) fn trailing_paren_comment_parts(
&self,
expr_end: u32,
boundary_end: u32,
) -> Option<(DocBuf, bool)> {
if !self.has_trailing_paren_comments(expr_end, boundary_end) {
return None;
}
let d = self.d();
let needs_break = self
.comments_to_emit_between(expr_end, boundary_end)
.any(|c| {
!c.is_block || c.multiline || self.has_newline_between(expr_end, c.span.start)
});
let mut parts = DocBuf::new();
let mut prev_end = expr_end;
for comment in self.comments_to_emit_between(expr_end, boundary_end) {
if self.has_newline_between(prev_end, comment.span.start) {
parts.push(d.hardline());
} else {
parts.push(d.text(" "));
}
parts.push(self.build_comment_doc(comment));
prev_end = comment.span.end;
}
Some((parts, needs_break))
}
pub(in crate::printer) fn build_expression_doc_keep_paren_comments(
&self,
expr: &ast::Expression<'_>,
boundary_end: u32,
layout: SeqLayout,
) -> DocId {
let expr_end = expr.span().end;
if let ast::ExpressionKind::SequenceExpression(seq) = &expr.kind {
return self.build_shell_sequence_doc(seq, expr.span, expr_end, boundary_end, layout);
}
let inner = self.build_expression_doc(expr);
self.build_kept_paren_shell_doc(inner, expr_end, boundary_end)
.unwrap_or(inner)
}
fn build_kept_paren_shell_doc(
&self,
inner: DocId,
expr_end: u32,
boundary_end: u32,
) -> Option<DocId> {
let d = self.d();
let (comment_parts, needs_break) =
self.trailing_paren_comment_parts(expr_end, boundary_end)?;
Some(if needs_break {
let mut indent_parts: DocBuf = smallvec![d.hardline(), inner];
indent_parts.extend(comment_parts);
d.concat(&[
d.text("("),
d.indent(d.concat(&indent_parts)),
d.hardline(),
d.text(")"),
])
} else {
let mut parts: DocBuf = smallvec![d.text("("), inner];
parts.extend(comment_parts);
parts.push(d.text(")"));
d.concat(&parts)
})
}
pub(crate) fn build_frozen_kept_paren_doc(&self, frozen: Span, boundary_end: u32) -> DocId {
let inner = self.build_frozen_node_doc(frozen);
self.build_kept_paren_shell_doc(inner, frozen.end, boundary_end)
.unwrap_or_else(|| self.d().parens(inner))
}
pub(crate) fn promote_comments_before_operator(
&self,
start: u32,
op_pos: u32,
) -> Option<(DocId, u32)> {
let d = self.d();
let mut promoted_parts = DocBuf::new();
let mut last_promoted_end = start;
for comment in self.comments_to_emit_between(start, op_pos) {
if comment.is_block {
promoted_parts.push(d.text(" "));
promoted_parts.push(self.build_comment_doc(comment));
last_promoted_end = comment.span.end;
}
}
if promoted_parts.is_empty() {
None
} else {
Some((d.concat(&promoted_parts), last_promoted_end))
}
}
pub(crate) fn find_operator_in_source(
&self,
start: u32,
end: u32,
operator: &str,
) -> Option<u32> {
let bytes = self.source.as_bytes();
let op_bytes = operator.as_bytes();
let op_len = op_bytes.len();
let end_usize = end as usize;
let mut i = start as usize;
while i + op_len <= end_usize {
if let Some(past_comment) = tsv_lang::source_scan::skip_comment(bytes, i, end_usize) {
i = past_comment;
continue;
}
if &bytes[i..i + op_len] == op_bytes {
return Some(i as u32);
}
i += 1;
}
None
}
#[inline]
pub(crate) fn prepend_removed_paren_comments(
&self,
node: Span,
outer_start: u32,
inner_start: u32,
doc: DocId,
) -> DocId {
self.prepend_opt(
self.removed_paren_comments_opt(node, outer_start, inner_start),
doc,
)
}
pub(crate) fn removed_paren_comments_opt(
&self,
node: Span,
outer_start: u32,
inner_start: u32,
) -> Option<DocId> {
let mut outer_start = outer_start;
if let Some(claim) = self.claimed_left_spine_run.get() {
if claim.claimer == node {
return self
.build_rhs_comments_opt(claim.region.start, claim.region.end.max(inner_start));
}
if claim.region.start <= outer_start && outer_start < claim.region.end {
outer_start = claim.region.end;
}
}
(outer_start < inner_start)
.then(|| self.build_rhs_comments_opt(outer_start, inner_start))
.flatten()
}
pub(crate) fn build_value_paren_line_comment_shell(
&self,
value_doc: DocId,
value_end: u32,
span_end: u32,
) -> Option<(DocId, u32)> {
let close = self.value_paren_line_comment_close(value_end, span_end)?;
let shell = self.build_paren_operand_comment_doc(value_end, close, value_doc, ")")?;
Some((shell, Self::past_grouping_close(close, span_end)))
}
pub(crate) fn value_paren_line_comment_close(
&self,
value_end: u32,
span_end: u32,
) -> Option<u32> {
let close = self.retained_grouping_close(value_end, span_end)?;
self.has_line_comments_between(value_end, close)
.then_some(close)
}
pub(crate) fn past_grouping_close(close: u32, span_end: u32) -> u32 {
close.saturating_add(1).min(span_end)
}
pub(crate) fn build_paren_operand_comment_doc(
&self,
start: u32,
end: u32,
operand: DocId,
close: &'static str,
) -> Option<DocId> {
let d = self.d();
if self.has_line_spanning_comments_to_emit_between(start, end) {
return Some(self.build_trailing_run_expanded_shell_doc(start, end, operand, close));
}
if self.has_comments_to_emit_between(start, end) {
let trailing = self.build_chain_block_comments_doc(start, end, CommentSpacing::Leading);
return Some(d.concat(&[d.text("("), operand, trailing, d.text(close)]));
}
None
}
fn build_trailing_run_expanded_shell_doc(
&self,
start: u32,
end: u32,
operand: DocId,
close: &'static str,
) -> DocId {
let mut body = DocBuf::with_capacity(3);
body.push(operand);
self.push_anchored_trailing_run(&mut body, start, end, RunLeadingBlank::Keep);
self.compose_expanded_shell_doc(None, &body, close)
}
}