Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 2 additions & 5 deletions crates/tsv_debug/src/cli/commands/gap_audit_known.txt
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
# the gate rather than being pinned.
#
# Format: KIND<TAB>SHAPE<TAB>PAYLOADS
# shapes: 642
# shapes: 639
DROPPED !)⟨⟩!. annotation,block,jsdoc_cast
DROPPED #⟨⟩IDENT annotation,block,jsdoc_cast,line,multiline
DROPPED #⟨⟩\ annotation,block,jsdoc_cast,line,multiline
Expand Down Expand Up @@ -168,7 +168,7 @@ DROPPED ,{}⟨⟩); annotation,block,jsdoc_cast,multiline
DROPPED ,⟨⟩, annotation,block,jsdoc_cast
DROPPED ,⟨⟩,, annotation,block,jsdoc_cast
DROPPED ,⟨⟩/* annotation,block,jsdoc_cast,line,multiline
DROPPED ,⟨⟩// annotation,block,jsdoc_cast,multiline
DROPPED ,⟨⟩// annotation,block,jsdoc_cast
DROPPED ,⟨⟩>( annotation,block,jsdoc_cast,multiline
DROPPED ,⟨⟩>() annotation,block,jsdoc_cast,multiline
DROPPED ,⟨⟩>(/ annotation,block,jsdoc_cast,multiline
Expand Down Expand Up @@ -292,7 +292,6 @@ DROPPED IDENT⟨⟩--) annotation,block,jsdoc_cast
DROPPED IDENT⟨⟩--; annotation,block,jsdoc_cast
DROPPED IDENT⟨⟩/ annotation,block,jsdoc_cast,line,multiline
DROPPED IDENT⟨⟩/* line,multiline
DROPPED IDENT⟨⟩/*, multiline
DROPPED IDENT⟨⟩: annotation,block,jsdoc_cast,line,multiline
DROPPED IDENT⟨⟩:[. annotation,block,jsdoc_cast,line,multiline
DROPPED IDENT⟨⟩= annotation,block,jsdoc_cast,line,multiline
Expand Down Expand Up @@ -459,7 +458,6 @@ DROPPED })⟨⟩) annotation,block,jsdoc_cast,line,multiline
DROPPED })⟨⟩); annotation,block,jsdoc_cast,multiline
DROPPED })⟨⟩␣ annotation,block,jsdoc_cast,multiline
DROPPED }*#⟨⟩IDENT annotation,block,jsdoc_cast,line,multiline
DROPPED },⟨⟩// multiline
DROPPED },⟨⟩>( annotation,block,jsdoc_cast,multiline
DROPPED },⟨⟩>() annotation,block,jsdoc_cast,multiline
DROPPED },⟨⟩␣ annotation,block,jsdoc_cast,multiline
Expand Down Expand Up @@ -526,7 +524,6 @@ DROPPED ␣⟨⟩... multiline
DROPPED ␣⟨⟩/ annotation,block,jsdoc_cast,line,multiline
DROPPED ␣⟨⟩/* annotation,block,jsdoc_cast,line,multiline
DROPPED ␣⟨⟩/** multiline
DROPPED ␣⟨⟩/*, multiline
DROPPED ␣⟨⟩// annotation,block,jsdoc_cast,line,multiline
DROPPED ␣⟨⟩: annotation,block,jsdoc_cast,line,multiline
DROPPED ␣⟨⟩; annotation,block,jsdoc_cast,line,multiline
Expand Down
25 changes: 18 additions & 7 deletions crates/tsv_ts/src/printer/expressions/patterns.rs
Original file line number Diff line number Diff line change
Expand Up @@ -417,14 +417,25 @@ impl<'a> Printer<'a> {
let mut parts = DocBuf::new();
let mut last_pos = prev_end;
for comment in comments_to_emit_in_range(self.comments, prev_end, boundary) {
if self.is_same_line(prev_end, comment.span.start) {
continue;
}
if self.has_blank_line_between(last_pos, comment.span.start) {
parts.push(d.literalline());
if self.is_same_line(last_pos, comment.span.start) {
// Same source line as the preceding content — e.g. a line comment
// trailing a *multiline* block's closing line (`*/ // c`). The
// per-element loop keys its same-line capture on the element's end,
// so a comment sitting on the block's *closing* line (a different
// line than the element) was never captured there; keep it inline
// here rather than dropping it or forcing it onto its own line.
// Keying the test on the running `last_pos` (not the original
// `prev_end`) also keeps two same-line dangling comments together
// (`/* a */ // b`).
parts.push(d.text(" "));
parts.push(self.build_comment_doc(comment));
} else {
if self.has_blank_line_between(last_pos, comment.span.start) {
parts.push(d.literalline());
}
parts.push(d.hardline());
parts.push(self.build_comment_doc(comment));
}
parts.push(d.hardline());
parts.push(self.build_comment_doc(comment));
last_pos = comment.span.end;
}
d.concat(&parts)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -897,6 +897,7 @@ impl<'a> Printer<'a> {
) -> DocId {
let d = self.d();
let left_start = self.get_for_in_of_left_start(left);
let binding_start = self.get_for_in_of_binding_start(left);
let left_end = self.get_for_in_of_left_end(left);
let right_start = right.span().start;
let right_end = right.span().end;
Expand All @@ -913,6 +914,7 @@ impl<'a> Printer<'a> {
let keyword_pos = self
.find_keyword_position(left_end, right_start, keyword)
.unwrap_or(left_end);
let keyword_end = keyword_pos + keyword.len() as u32;

// Preserve comments between keywords and `(`
// for await: two gaps — for-to-await and await-to-paren
Expand Down Expand Up @@ -940,14 +942,23 @@ impl<'a> Printer<'a> {
self.build_keyword_paren_comments(for_keyword_end, open_paren)
};

// Check for line comments in the header - if present, use breaking layout
// We check from open paren to close paren
// Check for line comments in the header's *structural gaps* — if present,
// use the breaking layout that preserves them where the author placed them.
// Only gap comments count: a `//` *inside* the binding or the iterable
// expression is that expression's own content (printed by its doc) and must
// NOT force the header open — prettier keeps the head inline and only the
// iterable breaks. See `in_of_iterable_line_comment`.
let close = close_paren.unwrap_or(right_end + 1);
let has_line_comments = if let Some(open) = open_paren {
self.has_line_comments_between(open + 1, close)
} else {
self.has_line_comments_between(left_start, close)
};
let has_line_comments = self.for_in_of_header_has_structural_line_comments(
open_paren,
binding_start,
left_end,
keyword_pos,
keyword_end,
right_start,
right_end,
close,
);

// Build the `for ... (` opening once — shared by both the inline and the
// breaking (line-comment) layouts, so each preserves any `for`-to-`(`
Expand Down Expand Up @@ -1000,7 +1011,6 @@ impl<'a> Printer<'a> {
}

// Comments after the keyword, before right
let keyword_end = keyword_pos + keyword.len() as u32;
let has_comment =
self.append_for_in_of_block_comments(&mut parts, keyword_end, right_start);
if !has_comment {
Expand All @@ -1022,6 +1032,47 @@ impl<'a> Printer<'a> {
d.group(d.concat(&parts))
}

/// Whether a `//` line comment sits in one of the for-in/for-of header's
/// *structural gaps* — after `(` up to the binding target (covering the
/// declaration keyword→binding gap, `const // c⏎x`), between the binding and
/// the `in`/`of` keyword, between the keyword and the iterable, or between the
/// iterable and `)`.
///
/// Deliberately excludes the interiors of the binding *pattern*
/// (`[binding_start, left_end)`) and the iterable (`[right_start, right_end)`): a
/// line comment *inside* the iterable (`for (const x of [\n\t'a' // c\n])`) — or
/// inside a destructuring binding — is that expression's own content, printed by
/// its doc, and must not force the header-breaking layout
/// (`build_for_in_of_with_line_comments`). Prettier keeps the head inline and
/// only that expression breaks. A gap comment, by contrast, has no other line
/// break to ride, so inline the `//` would swallow the following header tokens —
/// tsv breaks the header and preserves it in place. See the
/// `in_of_iterable_line_comment` (excluded) and
/// `of_in_keyword_binding_line_comment_prettier_divergence` (keyword→binding gap,
/// included) fixtures.
#[allow(clippy::too_many_arguments)]
fn for_in_of_header_has_structural_line_comments(
&self,
open_paren: Option<u32>,
binding_start: u32,
left_end: u32,
keyword_pos: u32,
keyword_end: u32,
right_start: u32,
right_end: u32,
close: u32,
) -> bool {
// `(` → binding target (spans the declaration keyword + its keyword→binding
// gap). Absent open paren (degenerate header) has no locatable gap.
open_paren.is_some_and(|open| self.has_line_comments_between(open + 1, binding_start))
// binding → keyword
|| self.has_line_comments_between(left_end, keyword_pos)
// keyword → iterable
|| self.has_line_comments_between(keyword_end, right_start)
// iterable → `)`
|| self.has_line_comments_between(right_end, close)
}

/// Build for-in/for-of statement with line comments preserved in their positions
///
/// This is our divergence from Prettier - we preserve line comments where
Expand Down Expand Up @@ -1193,6 +1244,23 @@ impl<'a> Printer<'a> {
}
}

/// Get the start position of the for-in/for-of binding *target* — the first
/// declarator's pattern for a `VariableDeclaration` left (`const [a, b]` → the
/// `[`), or the pattern itself for a bare `Pattern` left. Unlike
/// `get_for_in_of_left_start` (which points at the `const`/`let` keyword), this
/// skips the declaration kind so the keyword→binding gap (`const // c⏎x`) reads
/// as a header-structural position while the binding pattern's own interior does
/// not — see `for_in_of_header_has_structural_line_comments`.
fn get_for_in_of_binding_start(&self, left: &internal::ForInOfLeft<'_>) -> u32 {
match left {
internal::ForInOfLeft::VariableDeclaration(decl) => decl
.declarations
.first()
.map_or(decl.span.start, |declarator| declarator.id.span().start),
internal::ForInOfLeft::Pattern(expr) => expr.span().start,
}
}

/// Append inline block comments for for-in/for-of statements.
/// Emits ` comment` for each block comment, plus trailing ` ` if any were added.
/// Own-line comments normalize to inline. Line comments are skipped (handled by
Expand Down
Loading