use super::types::{ChainGroup, ChainNode, NonNullGap, is_numeric_index};
use crate::ast::{self, Expression, ExpressionKind};
use crate::printer::expressions::ChainBaseTernary;
use crate::printer::ignore::FrozenOperandPair;
use crate::printer::{
ArrowChainContext, LeadingGlue, ParenContext, Printer, ShareTag, is_curried_arrow_chain,
needs_parens,
};
use std::cell::Cell;
use tsv_lang::doc::{
DocBuf,
arena::{DocArena, DocId},
};
use tsv_lang::printing::has_blank_line_between_strict;
use tsv_lang::{Comment, Span};
#[derive(Clone, Copy)]
enum BaseShape {
Hug,
Hang,
TernaryDirect,
}
impl BaseShape {
fn of(
expr: &Expression<'_>,
base_ternary: Option<ChainBaseTernary>,
followed_by_non_null: bool,
) -> Self {
if let Some(ternary) = base_ternary.filter(|t| !t.expands) {
return if ternary.direct && !followed_by_non_null {
Self::TernaryDirect
} else {
Self::Hug
};
}
match &expr.kind {
ExpressionKind::ArrowFunctionExpression(_) | ExpressionKind::FunctionExpression(_) => {
Self::Hug
}
_ => Self::Hang,
}
}
fn apply(self, printer: &Printer<'_>, content: DocId) -> DocId {
let d = printer.arena();
match self {
Self::Hug => content,
Self::Hang => printer.build_expanding_parens_body_doc(content),
Self::TernaryDirect => d.group(d.concat(&[content, d.softline()])),
}
}
}
pub(crate) fn print_node_inner<'a>(
node: &ChainNode<'a>,
printer: &Printer<'_>,
expanded: bool,
skip_comments: bool,
) -> DocId {
let d = printer.arena();
match node {
ChainNode::Base {
expr,
needs_parens,
paren_leading_start,
paren_comment_end,
followed_by_non_null,
} => {
let base_start = expr.span().start;
let base_end = expr.span().end;
let trailing_gap = || {
paren_comment_end.map(|end| (base_end, end)).or_else(|| {
printer.owned_pair_trailing_gap(base_end, paren_leading_start.is_some())
})
};
if let Some(frozen) = printer.frozen_chain_base_span(expr) {
let slice = printer.build_frozen_expression_doc(expr, frozen);
if let Some(paren_start) = *paren_leading_start {
return printer.build_owned_required_pair_doc(
(paren_start, base_start),
trailing_gap(),
slice,
")",
);
}
return printer.wrap_frozen_position_pair(
expr,
FrozenOperandPair::Position {
ctx: ParenContext::ChainBase,
required: *needs_parens,
},
slice,
);
}
if *needs_parens {
let base_ternary = printer.chain_base_ternary(expr);
let tag = if base_ternary.is_some_and(|ternary| ternary.expands) {
ShareTag::ChainExpandedParenBase
} else {
ShareTag::ChainParenBase
};
let key = printer.chain_share_key(expr, tag);
printer.chain_shared_doc(key, || {
let trailing_gap = trailing_gap();
let callee_chain = is_curried_arrow_chain(expr)
&& paren_leading_start.is_some_and(|start| {
printer.pair_gaps_are_comment_free((start, base_start), trailing_gap)
});
let inner_memo = Cell::new(None);
let inner = || -> DocId {
if let Some(doc) = inner_memo.get() {
return doc;
}
let doc = if callee_chain {
printer
.build_with_arrow_chain_context(ArrowChainContext::Callee, || {
printer.build_expression_doc(expr)
})
} else {
printer.build_expression_doc(expr)
};
inner_memo.set(Some(doc));
doc
};
let claimed = |build: &dyn Fn() -> DocId| {
printer.build_value_with_outermost_owned_comment(expr, build)
};
let leading_gap = (paren_leading_start.unwrap_or(base_start), base_start);
let multiline_lead = printer.value_claims_outermost_owned_comment(expr)
|| printer
.comments_to_emit_between(leading_gap.0, leading_gap.1)
.any(|comment| comment.multiline);
let shape = match BaseShape::of(expr, base_ternary, *followed_by_non_null) {
BaseShape::Hang if multiline_lead => BaseShape::Hug,
shape => shape,
};
printer.build_shaped_owned_required_pair_doc(
leading_gap,
trailing_gap,
|| claimed(&inner),
|lead, trailing| {
if let BaseShape::TernaryDirect = shape {
let body = claimed(&|| {
shape.apply(
printer,
printer.concat_pair_runs(None, inner(), trailing),
)
});
return printer.concat_pair_runs(lead, body, None);
}
let content = claimed(&|| match (&expr.kind, shape) {
(ExpressionKind::BinaryExpression(binary), BaseShape::Hang) => {
printer.build_binary_chain_for_parens(binary, expr.span)
}
_ => inner(),
});
shape.apply(printer, printer.concat_pair_runs(lead, content, trailing))
},
")",
)
})
} else {
let leaf = matches!(
expr.kind,
ExpressionKind::Identifier(_)
| ExpressionKind::Literal(_)
| ExpressionKind::ThisExpression(_)
);
let key = if leaf {
None
} else {
printer.chain_share_key(expr, ShareTag::ChainBase)
};
printer.chain_shared_doc(key, || printer.build_expression_doc(expr))
}
}
ChainNode::Call { call, span, facts } => {
if expanded {
printer.print_call_args_expanded(call, *span, *facts)
} else {
printer.print_call_args(call, *span, *facts)
}
}
ChainNode::Member {
property,
optional,
object_end,
property_start,
paren_gap_skip,
} => print_member_access(
printer,
*property,
MemberSpans {
name_start: *property_start,
object_end: *object_end,
property_start: *property_start,
paren_gap_skip: *paren_gap_skip,
},
*optional,
false,
skip_comments,
),
ChainNode::PrivateMember {
property,
optional,
object_end,
property_start,
name_start,
paren_gap_skip,
} => print_member_access(
printer,
*property,
MemberSpans {
name_start: *name_start,
object_end: *object_end,
property_start: *property_start,
paren_gap_skip: *paren_gap_skip,
},
*optional,
true,
skip_comments,
),
ChainNode::ComputedMember {
expr,
optional,
object_end,
bracket_end,
paren_gap_skip,
} => {
let raw_inner = printer.build_expression_doc_claiming_outermost(expr);
let inner = if needs_parens(
expr,
ParenContext::ComputedPropertyKey,
printer.in_for_init(),
) {
d.parens(raw_inner)
} else {
raw_inner
};
#[cfg(debug_assertions)]
if d.will_break(inner) {
printer.computed_index_breaks.set(true);
}
let open = if *optional { "?.[" } else { "[" };
let breakable = !is_numeric_index(expr);
if !printer.chain_has_comments() {
return computed_lookup_doc(printer, open, inner, breakable);
}
let prop_span = printer.get_property_span(expr);
let bracket = find_computed_bracket(printer.get_source(), *object_end, prop_span.start);
let bracket_doc = if let Some(broken) = printer
.build_computed_member_line_comment_bracket(
open,
bracket,
prop_span.start,
prop_span.end,
*bracket_end,
inner,
) {
broken
} else {
let leading_comments_doc = printer.build_chain_block_comments_doc(
bracket.inside,
prop_span.start,
crate::printer::CommentSpacing::Trailing,
);
let trailing_comments_doc = printer.build_chain_block_comments_doc(
prop_span.end,
*bracket_end,
crate::printer::CommentSpacing::Leading,
);
let inner_with_comments =
d.concat(&[leading_comments_doc, inner, trailing_comments_doc]);
let has_inside_comments = printer
.has_comments_on_page_between(bracket.inside, prop_span.start)
|| printer.has_comments_on_page_between(prop_span.end, *bracket_end);
computed_lookup_doc(
printer,
open,
inner_with_comments,
breakable || has_inside_comments,
)
};
if skip_comments {
return bracket_doc;
}
let pre_bracket =
printer.classify_chain_gap(*object_end, bracket.open, *paren_gap_skip);
if pre_bracket.has_line_comments() {
let mut parts = DocBuf::new();
push_gap_comments_and_break(
&mut parts,
printer,
*object_end,
bracket.open,
*paren_gap_skip,
);
parts.push(bracket_doc);
return d.concat(&parts);
}
let glued_doc = printer.build_trailing_block_doc(&pre_bracket.trailing_block);
if !pre_bracket.leading_block.is_empty() {
let anchor = pre_bracket
.trailing_block
.last()
.map_or(*object_end, |c| c.span.end);
let mut parts = DocBuf::new();
parts.push(glued_doc);
push_break_and_leading_run(
&mut parts,
printer,
anchor,
bracket.open,
&pre_bracket.leading_in_source_order(),
);
parts.push(bracket_doc);
return d.concat(&parts);
}
d.concat(&[glued_doc, bracket_doc])
}
ChainNode::NonNull {
gap:
NonNullGap::Bang {
operand_end,
bang_end,
},
} if printer.chain_has_comments() => {
let comments = printer.build_chain_block_comments_doc(
*operand_end,
*bang_end,
crate::printer::CommentSpacing::Leading,
);
d.concat(&[comments, d.text("!")])
}
ChainNode::NonNull { .. } => d.text("!"),
}
}
pub(crate) fn print_node<'a>(node: &ChainNode<'a>, printer: &Printer<'_>) -> DocId {
print_node_inner(node, printer, false, false)
}
pub(crate) fn print_group<'a>(group: &ChainGroup<'a>, printer: &Printer<'_>) -> DocId {
print_group_inner(group, printer, false, false)
}
pub(crate) fn print_group_expanded<'a>(group: &ChainGroup<'a>, printer: &Printer<'_>) -> DocId {
print_group_inner(group, printer, true, false)
}
pub(crate) fn print_group_skip_first_comments<'a>(
group: &ChainGroup<'a>,
printer: &Printer<'_>,
) -> DocId {
print_group_inner(group, printer, false, true)
}
pub(crate) fn print_group_expanded_skip_first_comments<'a>(
group: &ChainGroup<'a>,
printer: &Printer<'_>,
) -> DocId {
print_group_inner(group, printer, true, true)
}
fn print_group_inner<'a>(
group: &ChainGroup<'a>,
printer: &Printer<'_>,
expanded: bool,
skip_first_comments: bool,
) -> DocId {
let d = printer.arena();
let doc =
if printer.chain_ungroups_args.get() && group.nodes.iter().any(ChainNode::ungroups_args) {
print_curried_group(group, printer, expanded, skip_first_comments)
} else {
d.concat_iter(
group
.nodes
.iter()
.enumerate()
.map(|(i, n)| print_group_node(n, i, printer, expanded, skip_first_comments)),
)
};
if !expanded && group_forces_list_break(group, printer) {
return d.group_break(doc);
}
doc
}
#[inline]
fn print_group_node(
node: &ChainNode<'_>,
i: usize,
printer: &Printer<'_>,
expanded: bool,
skip_first_comments: bool,
) -> DocId {
let skip_comments = skip_first_comments && i == 0 && node.is_member();
print_node_inner(node, printer, expanded, skip_comments)
}
#[inline(never)]
fn print_curried_group(
group: &ChainGroup<'_>,
printer: &Printer<'_>,
expanded: bool,
skip_first_comments: bool,
) -> DocId {
let d = printer.arena();
let mut parts = DocBuf::new();
for (i, node) in group.nodes.iter().enumerate() {
let doc = print_group_node(node, i, printer, expanded, skip_first_comments);
let closes_group = i > 0
&& group.nodes[i - 1].is_call()
&& matches!(node, ChainNode::Call { facts, .. } if facts.own_call_layout);
if closes_group {
let prefix = d.concat(&parts);
parts.clear();
parts.push(d.group(d.concat(&[prefix, doc])));
} else {
parts.push(doc);
}
}
d.concat(&parts)
}
fn group_forces_list_break(group: &ChainGroup<'_>, printer: &Printer<'_>) -> bool {
!printer.forced_list_elements.borrow().is_empty()
&& group
.nodes
.iter()
.filter_map(ChainNode::as_call_expression)
.flat_map(|call| call.arguments.iter())
.any(|arg| printer.span_holds_forced_list_break(arg.span()))
}
pub(crate) fn member_lookup_group(d: &DocArena, lookup: DocId) -> DocId {
d.group(d.indent(d.concat(&[d.softline(), lookup])))
}
fn computed_lookup_doc(
printer: &Printer<'_>,
open: &'static str,
index: DocId,
breakable: bool,
) -> DocId {
let d = printer.arena();
if !breakable {
return d.concat(&[d.text(open), index, d.text("]")]);
}
let indented = d.indent(d.concat(&[d.softline(), index]));
d.group(d.concat(&[d.text(open), indented, d.softline(), d.text("]")]))
}
struct MemberSpans {
name_start: u32,
object_end: u32,
property_start: u32,
paren_gap_skip: Option<Span>,
}
fn print_member_access(
printer: &Printer<'_>,
property: ast::IdentName<'_>,
spans: MemberSpans,
optional: bool,
is_private: bool,
skip_comments: bool,
) -> DocId {
let d = printer.arena();
let prop_doc = printer.ident_doc(property, spans.name_start);
let member_doc = match (optional, is_private) {
(false, false) => d.concat(&[d.text("."), prop_doc]),
(true, false) => d.concat(&[d.text("?."), prop_doc]),
(false, true) => d.concat(&[d.text(".#"), prop_doc]),
(true, true) => d.concat(&[d.text("?.#"), prop_doc]),
};
if skip_comments {
return member_doc;
}
if !printer.chain_has_comments() {
return member_doc;
}
let classified =
printer.classify_chain_gap(spans.object_end, spans.property_start, spans.paren_gap_skip);
let trailing_block = printer.build_trailing_block_doc(&classified.trailing_block);
let trailing_line = printer.build_deferred_line_comments_doc(&classified.trailing_line);
let leading_line = printer.build_deferred_line_comments_doc(&classified.leading_line);
let leading_block = printer.build_trailing_block_doc(&classified.leading_block);
d.concat(&[
trailing_block,
trailing_line,
leading_block,
leading_line,
member_doc,
])
}
pub(crate) fn has_inside_bracket_comments<'a>(node: &ChainNode<'a>, printer: &Printer<'_>) -> bool {
if let ChainNode::ComputedMember {
expr,
object_end,
bracket_end,
..
} = node
{
let prop_span = printer.get_property_span(expr);
let inside =
find_computed_bracket(printer.get_source(), *object_end, prop_span.start).inside;
printer.has_comments_on_page_between(inside, printer.left_spine_printed_start(expr))
|| printer.has_comments_on_page_between(prop_span.end, *bracket_end)
} else {
false
}
}
pub(crate) fn find_bracket_position(source: &str, start: u32, end: u32) -> u32 {
find_computed_bracket(source, start, end).open
}
#[derive(Debug, Clone, Copy)]
pub(crate) struct ComputedBracket {
pub open: u32,
pub inside: u32,
}
impl ComputedBracket {
pub(crate) const fn bracket_pos(self) -> u32 {
self.inside - 1
}
}
pub(crate) fn find_computed_bracket(source: &str, start: u32, end: u32) -> ComputedBracket {
let bytes = source.as_bytes();
let start_pos = start as usize;
let end_pos = end as usize;
let mut i = start_pos;
while i < end_pos {
if let Some(new_i) = tsv_lang::source_scan::skip_comment(bytes, i, end_pos) {
i = new_i;
continue;
}
if bytes[i] == b'?' && i + 2 < end_pos && bytes[i + 1] == b'.' && bytes[i + 2] == b'[' {
return ComputedBracket {
open: i as u32,
inside: (i + 3) as u32,
};
}
if bytes[i] == b'[' {
return ComputedBracket {
open: i as u32,
inside: (i + 1) as u32,
};
}
i += 1;
}
ComputedBracket {
open: start,
inside: start,
}
}
pub(crate) fn push_gap_comments_and_break(
parts: &mut DocBuf,
printer: &Printer<'_>,
object_end: u32,
property_start: u32,
paren_gap_skip: Option<Span>,
) {
if !printer.chain_has_comments() {
parts.push(build_chain_line_break(printer, object_end, property_start));
return;
}
let classified = printer.classify_chain_gap(object_end, property_start, paren_gap_skip);
parts.push(printer.build_trailing_block_doc(&classified.trailing_block));
parts.push(printer.build_deferred_line_comments_doc(&classified.trailing_line));
push_break_and_leading_run(
parts,
printer,
object_end,
property_start,
&classified.leading_in_source_order(),
);
}
fn push_break_and_leading_run(
parts: &mut DocBuf,
printer: &Printer<'_>,
object_end: u32,
property_start: u32,
leading: &[&Comment],
) {
parts.push(build_chain_line_break(printer, object_end, property_start));
if let Some(first) = leading.first() {
if has_blank_line_between_strict(printer.get_source(), object_end, first.span.start) {
parts.push(printer.arena().hardline());
}
printer.push_leading_comment_run(
parts,
leading.iter().copied(),
property_start,
LeadingGlue::AdjacentOwnLine,
);
}
}
pub(crate) fn node_comment_gap(node: &ChainNode<'_>, printer: &Printer<'_>) -> Option<(u32, u32)> {
let (object_end, property_start) = node.comment_range()?;
if matches!(node, ChainNode::ComputedMember { .. }) && printer.chain_has_comments() {
let bracket_open = find_bracket_position(printer.get_source(), object_end, property_start);
return Some((object_end, bracket_open));
}
Some((object_end, property_start))
}
#[expect(clippy::inline_always)]
#[inline(always)]
pub(crate) fn chain_gap_share_any(
node: &ChainNode<'_>,
source: &str,
mut f: impl FnMut(u32, u32) -> bool,
) -> bool {
let Some(gap) = node.comment_range() else {
return false;
};
if !chain_gap_any(gap, node.paren_gap_skip(), &mut f) {
return false;
}
match node {
ChainNode::ComputedMember {
expr, object_end, ..
} => {
let open = find_bracket_position(source, *object_end, expr.span().start);
chain_gap_any((gap.0, open), node.paren_gap_skip(), f)
}
_ => true,
}
}
pub(crate) fn chain_gap_any(
gap: (u32, u32),
skip: Option<Span>,
mut f: impl FnMut(u32, u32) -> bool,
) -> bool {
let (start, end) = gap;
let Some(skip) = skip else {
return f(start, end);
};
f(start, skip.start) || f(skip.end, end)
}
pub(crate) fn group_comment_gap(
group: &ChainGroup<'_>,
printer: &Printer<'_>,
) -> Option<(u32, u32)> {
node_comment_gap(group.nodes.first()?, printer)
}
pub(crate) fn build_chain_line_break(
printer: &Printer<'_>,
object_end: u32,
property_start: u32,
) -> DocId {
let d = printer.arena();
let source = printer.get_source();
let has_comments = printer.has_comments_to_emit_between(object_end, property_start);
if !has_comments && has_blank_line_between_strict(source, object_end, property_start) {
d.concat(&[d.literalline(), d.hardline()])
} else {
d.hardline()
}
}