1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
// Chain helpers on the main Printer for the chain renderer.
//
// These let the chain builder/printer delegate back to the main Printer for
// expression building and comment handling. Formerly the `ChainPrinter` /
// `SymbolLookup` traits — collapsed to inherent methods, since `Printer` was
// their only implementor.
use super::printing::ComputedBracket;
use super::types::ChainCall;
use crate::ast;
use crate::printer::comments::CommentVec;
use crate::printer::{CommentSpacing, Printer};
use tsv_lang::doc::DocBuf;
use tsv_lang::doc::arena::{DocArena, DocId};
use tsv_lang::{ClassifiedComments, Comment, Span};
impl<'a> Printer<'a> {
pub(crate) fn arena(&self) -> &DocArena {
self.arena
}
pub(crate) fn ident_doc(&self, name: ast::IdentName<'_>, name_start: u32) -> DocId {
self.ident_name_doc(name, name_start)
}
pub(crate) fn in_for_init(&self) -> bool {
self.in_for_init.get()
}
pub(crate) fn print_call_args(
&self,
call: &ast::CallExpression<'_>,
span: Span,
facts: ChainCall,
) -> DocId {
self.build_call_args_doc_for_chain(call, span, facts)
}
pub(crate) fn print_call_args_expanded(
&self,
call: &ast::CallExpression<'_>,
span: Span,
facts: ChainCall,
) -> DocId {
self.build_call_args_doc_for_chain_expanded(call, span, facts)
}
pub(crate) fn build_chain_block_comments_doc(
&self,
start: u32,
end: u32,
spacing: CommentSpacing,
) -> DocId {
let block_comments: CommentVec<'_> = self
.comments_to_emit_between(start, end)
.filter(|c| c.is_block)
.collect();
self.format_block_comments(&block_comments, spacing)
}
pub(crate) fn build_computed_member_line_comment_bracket(
&self,
open: &'static str,
bracket: ComputedBracket,
prop_start: u32,
prop_end: u32,
bracket_end: u32,
inner: DocId,
) -> Option<DocId> {
let inside_start = bracket.inside;
// Only the break path — a line comment before the index or after it (before
// `]`). A block-only or comment-free bracket falls through to the caller.
let body_trailing_line_comment = self.has_line_comments_between(prop_end, bracket_end);
if !self.has_line_comments_between(inside_start, prop_start) && !body_trailing_line_comment
{
return None;
}
let d = self.d();
// Build the body (index + any index→`]` trailing comments), then hand it to the
// shared bracket-break helper (it owns the `[`→index line-comment prefix and the
// break shell, mirroring the computed-key bracket). `[`→index: a `[`-line comment
// is pulled onto the `[` line, an own-line one keeps its line (blank-preserving).
// index→`]`: a same-line comment trails the index, an own-line one keeps its line.
let mut body_parts = DocBuf::new();
body_parts.push(inner);
if self.has_comments_to_emit_between(prop_end, bracket_end) {
self.push_deferred_run_end(&mut body_parts, inner);
}
let mut prev = prop_end;
for comment in self.comments_to_emit_between(prop_end, bracket_end) {
if self.is_same_line(prev, comment.span.start) {
body_parts.push(d.text(" "));
} else {
body_parts.push(d.hardline());
}
body_parts.push(self.build_comment_doc(comment));
prev = comment.span.end;
}
Some(self.build_bracket_line_comment_break(
open,
bracket.bracket_pos(),
prop_start,
body_trailing_line_comment,
d.concat(&body_parts),
))
}
pub(crate) fn get_property_span(&self, expr: &ast::Expression<'_>) -> Span {
expr.span()
}
pub(crate) fn is_expression_statement(&self) -> bool {
self.is_expression_statement.get()
}
pub(crate) fn clear_expression_statement(&self) {
self.is_expression_statement.set(false);
}
pub(crate) fn chain_has_comments(&self) -> bool {
self.chain_has_comments.get()
}
pub(crate) fn set_chain_has_comments(&self, has_comments: bool) -> bool {
let prev = self.chain_has_comments.get();
self.chain_has_comments.set(has_comments);
prev
}
pub(crate) fn restore_chain_has_comments(&self, prev: bool) {
self.chain_has_comments.set(prev);
}
/// The chain gaps' one classification seam — every chain consumer (the builder's
/// gap ownership, the dot/computed print paths, `push_gap_comments_and_break`)
/// classifies through here, so they take the ADVANCING trailing anchor together
/// and the gap's two sides cannot drift into a double-print
/// (`ClassifiedComments::from_index_advancing` — see its ⚠️ for why the advance
/// is not `from_range`'s one behavior).
pub(crate) fn classify_comments(&self, start: u32, end: u32) -> ClassifiedComments<'_> {
ClassifiedComments::from_index_advancing(
self.comment_free_gap.comments(),
self.first_index_between(start, end),
start,
end,
self.source.as_bytes(),
self.comment_line_breaks,
)
}
/// A chain gap's trail scan opening at `start` in its next-pass reading
/// ([`tsv_lang::GapTrailScan::fixed`]) — for a predicate that walks the gap itself (a
/// different axis, an early exit) and reads it as the NEXT pass will, where a comment
/// on a multi-line block's closing line stands on a line the block's own newline
/// began. It shares the emitter's `)` arm ([`Self::classify_comments`] walks
/// [`tsv_lang::GapTrailScan::new`]), so neither can read a comment printed after a
/// kept pair's `)` as own-line. Feed it the gap's comments in source order through
/// [`Self::chain_gap_trail`].
pub(crate) const fn chain_gap_trail_scan_fixed(&self, start: u32) -> tsv_lang::GapTrailScan {
tsv_lang::GapTrailScan::fixed(start)
}
/// A chain gap's trail scan opening at `start` in the emitter's own reading
/// ([`tsv_lang::GapTrailScan::new`], the scan [`Self::classify_comments`] walks).
pub(crate) const fn chain_gap_trail_scan(&self, start: u32) -> tsv_lang::GapTrailScan {
tsv_lang::GapTrailScan::new(start)
}
/// How `comment`, the next comment of the gap `scan` walks, stands — see
/// [`tsv_lang::GapTrailScan`].
pub(crate) fn chain_gap_trail(
&self,
scan: &mut tsv_lang::GapTrailScan,
comment: &Comment,
) -> tsv_lang::GapTrail {
scan.read(comment, self.source.as_bytes(), self.comment_line_breaks)
}
/// A chain node's gap classification, honoring the node's own
/// [`super::ChainNode::paren_gap_skip`] hole.
///
/// The common node has no hole and this is exactly [`Self::classify_comments`]. A
/// node whose gap was widened back over a stripped grouping paren has one, and its
/// claim is the two regions either side of it — the paren prefix and its own gap —
/// classified separately so each is read against its own anchor.
///
/// ⚠️ **Every layout predicate asked about the same gap must honor the hole too**, and
/// they all do it through one seam — [`super::printing::chain_gap_any`], whose ⚠️ carries
/// the argument. This emitter keeps its own spelling only because it builds a
/// `ClassifiedComments` rather than answering a `bool`, and an unconditional second
/// classify would cost every hole-less gap (the common case) for nothing.
pub(crate) fn classify_chain_gap(
&self,
start: u32,
end: u32,
skip: Option<Span>,
) -> ClassifiedComments<'_> {
let Some(skip) = skip else {
return self.classify_comments(start, end);
};
let mut classified = self.classify_comments(start, skip.start);
classified.append(self.classify_comments(skip.end, end));
classified
}
/// A gap's same-line block run, each comment behind a space
/// (`method() /* c */`). The named entry for
/// [`format_block_comments`](Self::format_block_comments) at `Leading` spacing,
/// so the spacing rule lives once.
pub(crate) fn build_trailing_block_doc(&self, comments: &[&Comment]) -> DocId {
self.format_block_comments(comments, CommentSpacing::Leading)
}
/// A chain gap's trailing line comments, deferred via `line_suffix` — the run
/// rides to the end of the output line and is flushed by whatever break the
/// caller emits next.
///
/// **The caller must emit that break.** A `line_suffix_boundary` here would flush
/// the run *and end the line itself* (see `tsv_lang`'s `render_line_node`), so a
/// caller that also breaks — every chain gap does, `build_chain_line_break` being
/// a hardline at minimum — would render a blank line under the comment. Prettier
/// plants its boundary at a member lookup whose following `softline` may collapse
/// (`print/member.js`); a chain gap is the case where it may not.
pub(crate) fn build_deferred_line_comments_doc(&self, comments: &[&Comment]) -> DocId {
let d = self.d();
if comments.is_empty() {
return d.empty();
}
let mut parts = DocBuf::with_capacity(comments.len());
for comment in comments {
parts.push(self.build_trailing_line_comment_doc(comment));
}
d.concat(&parts)
}
pub(crate) fn get_source(&self) -> &str {
self.source
}
}
impl<'a> Printer<'a> {
/// Format an already-filtered slice of **block** comments, each with `spacing`
/// applied to its outer edges (` /* c */` after a chain element, `/* c */ ` before
/// one). Every comment gets the space — a chain gap's block run is never at line
/// start — so this is the plain-`true` consumer of
/// [`Printer::push_block_comment_spaced`], which owns what the spacing means.
pub(crate) fn format_block_comments(
&self,
block_comments: &[&Comment],
spacing: CommentSpacing,
) -> DocId {
let d = self.d();
if block_comments.is_empty() {
return d.empty();
}
let mut parts = DocBuf::new();
for comment in block_comments {
self.push_block_comment_spaced(&mut parts, comment, spacing, true);
}
d.concat(&parts)
}
}