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
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

> a formatter, parser, and future linter + more for Svelte, TypeScript, and CSS

High-performance Rust parser as a drop-in replacement for Svelte's modern parser (acorn + acorn-typescript), paired with a formatter that took Prettier as its initial guide and still tracks it closely for the common case — while making deliberate, cataloged choices to diverge where tsv's own judgment is more defensible.
High-performance Rust parser as a drop-in replacement for Svelte's modern parser (acorn + acorn-typescript), paired with a formatter that took Prettier as its initial guide and still tracks it for the common case — while making deliberate, cataloged choices to diverge where tsv's own judgment is more defensible.

**Non-configurable by design**: formatting options are fixed at Prettier's defaults except printWidth=100, useTabs=true, singleQuote=true, and bracketSpacing=false — no config files, CLI flags, or runtime options, ever (opinionated like `gofmt` and Black). See [Configuration](#configuration).

Expand Down
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
> a formatter, parser, and future linter + more for Svelte, TypeScript, and CSS - [tsv.fuz.dev](https://tsv.fuz.dev/)

tsv is a toolchain for Svelte, TypeScript, and CSS, written in Rust.
The first release has a near-[Prettier](https://prettier.io/) formatter that
closely follows [prettier-plugin-svelte](https://github.com/sveltejs/prettier-plugin-svelte),
The first release has a near-[Prettier](https://prettier.io/) formatter,
similar to [prettier-plugin-svelte](https://github.com/sveltejs/prettier-plugin-svelte),
and a drop-in replacement for [Svelte](https://svelte.dev/)'s parser +
[acorn](https://github.com/acornjs/acorn) +
[acorn-typescript](https://github.com/sveltejs/acorn-typescript).
Expand Down Expand Up @@ -62,8 +62,8 @@ from anything that speaks C FFI. Deno's FFI is used in the benchmarks.
## Design

- supports Svelte, TypeScript, CSS, JS, and HTML
- formatting tracks Prettier and prettier-plugin-svelte closely, but intentionally diverges
in some cases - see [docs/conformance_prettier.md](docs/conformance_prettier.md)
- formatting tracks Prettier and prettier-plugin-svelte for the common case, but intentionally
diverges in some cases - see [docs/conformance_prettier.md](docs/conformance_prettier.md)
- tsv can generate a public JSON AST that should exactly match
Svelte 5's modern AST with acorn and acorn-typescript
(see [docs/conformance_svelte.md](docs/conformance_svelte.md)),
Expand Down
46 changes: 46 additions & 0 deletions crates/tsv_ts/src/printer/calls/arg_comments.rs
Original file line number Diff line number Diff line change
Expand Up @@ -446,6 +446,11 @@ where
/// - `leading`: Comments on their own lines (not on same line as reference_pos)
///
/// Uses `SmallVec` to avoid heap allocations for the common case (0-2 comments per range).
// TODO: this same-line-vs-own-line partition + its emit_* helpers are the call-arg
// instance of a rule also implemented in `conditional.rs` split_pre_operator_comments
// and `chain/builder/helpers.rs` push_gap_comments_and_break. Three parallel copies;
// unify if/when the Printer/ChainPrinter trait split and the differing emission
// shapes (operator / comma / dot) allow.
pub(crate) struct PartitionedComments<'a> {
pub trailing_line: SmallVec<[&'a internal::Comment; 2]>,
pub trailing_block: SmallVec<[&'a internal::Comment; 2]>,
Expand Down Expand Up @@ -538,6 +543,47 @@ impl<'a> PartitionedComments<'a> {
}
}

/// Emit a non-last arg's trailing comments split around its comma, then push the
/// comma itself: before-comma block comments trail the arg (`arg /* c */,`),
/// after-comma blocks and the same-line line comment follow the comma
/// (`arg, /* c */ // c2`). The caller adds the line break after.
///
/// Unlike [`emit_trailing_comments`] (which the caller invokes *after* pushing
/// the comma, so every block lands after it), this keeps a before-comma block in
/// its authored position. Shared by the `new`-argument non-last paths
/// (`build_new_doc_with_wrapping` and `build_args_with_blank_lines`) so they
/// can't drift — both used to relocate the block past the comma.
pub fn emit_trailing_comments_around_comma(
&self,
parts: &mut Vec<DocId>,
printer: &Printer<'_>,
arg_end: u32,
next_arg_start: u32,
) {
let d = printer.d();
let comma_pos = find_comma_pos(printer.source, arg_end, next_arg_start);
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));
}
}
}
for comment in &self.trailing_line {
parts.push(printer.build_trailing_line_comment_doc(comment));
}
}

/// Emit own-line ("leading") comments after the last argument, past its
/// trailing comma — each on its own line (hardline before).
///
Expand Down
49 changes: 35 additions & 14 deletions crates/tsv_ts/src/printer/calls/arg_wrapping.rs
Original file line number Diff line number Diff line change
Expand Up @@ -702,15 +702,11 @@ pub(crate) fn build_args_joined_with_comments(
);
let comma_pos = find_comma_pos(printer.source, arg_end, next_arg_start);

if pc.has_trailing_line() {
// Trailing line comments always force hardline: `arg, // comment\n`
parts.push(d.text(","));
for comment in &pc.trailing_line {
parts.push(d.text(" "));
parts.push(printer.build_comment_doc(comment));
}
parts.push(d.hardline());
} else if pc.has_trailing_block() {
if pc.has_trailing_line() || pc.has_trailing_block() {
// Block and line comments are emitted together (not either/or) so an
// arg carrying both — `a /* c */, // c2` — never drops the block.
let has_line = pc.has_trailing_line();

// Before-comma block comments: `arg /* c */,`
if let Some(cpos) = comma_pos {
for comment in &pc.trailing_block {
Expand All @@ -721,8 +717,26 @@ pub(crate) fn build_args_joined_with_comments(
}
}
parts.push(d.text(","));
if use_hardline {
// Hardline: break first, comment starts next line

if has_line {
// A line comment runs to EOL and forces a hardline. After-comma
// blocks and the line comment stay on the comma line, in order:
// `arg, /* after */ // comment`.
if let Some(cpos) = comma_pos {
for comment in &pc.trailing_block {
if is_comment_after_comma(comment, cpos) {
parts.push(d.text(" "));
parts.push(printer.build_comment_doc(comment));
}
}
}
for comment in &pc.trailing_line {
parts.push(d.text(" "));
parts.push(printer.build_comment_doc(comment));
}
parts.push(d.hardline());
} else if use_hardline {
// Block-only, hardline: break first, comment starts next line
parts.push(d.hardline());
if let Some(cpos) = comma_pos {
for comment in &pc.trailing_block {
Expand All @@ -733,7 +747,7 @@ pub(crate) fn build_args_joined_with_comments(
}
}
} else {
// Soft: comment stays inline after comma, break follows
// Block-only, soft: comment stays inline after comma, break follows
if let Some(cpos) = comma_pos {
for comment in &pc.trailing_block {
if is_comment_after_comma(comment, cpos) {
Expand Down Expand Up @@ -943,8 +957,15 @@ pub(super) fn build_args_with_blank_lines(
next_start,
);

arg_parts.push(d.text(","));
pc.emit_trailing_comments(&mut arg_parts, printer);
// Split trailing comments around the comma (shared with the `new`
// non-last path) so a before-comma block stays put instead of being
// relocated past the comma.
pc.emit_trailing_comments_around_comma(
&mut arg_parts,
printer,
arg_end,
next_start,
);

let next_has_blank = pc.has_blank_line_in_gap(
printer.source,
Expand Down
117 changes: 65 additions & 52 deletions crates/tsv_ts/src/printer/calls/call_formatting.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1093,23 +1093,13 @@ pub(super) fn build_call_doc_with_wrapping(
force_expansion = true;
}

if pc.has_trailing_line() {
// Trailing line comments: comma, comment, hardline. The comment
// goes through `line_suffix` (zero width) so it never counts
// against the argument's own group — a long trailing comment
// can't force a binary/conditional arg to break (prettier's
// `lineSuffix`). It still renders after the comma at end-of-line.
force_expansion = true;
arg_parts.push(d.text(","));
for comment in &pc.trailing_line {
arg_parts.push(printer.build_trailing_line_comment_doc(comment));
}
if has_blank_line {
arg_parts.push(d.literalline());
}
arg_parts.push(d.hardline());
} else if pc.has_trailing_block() {
// Trailing block comments: place relative to comma based on source position
if pc.has_trailing_line() || pc.has_trailing_block() {
// Trailing comments after this arg, in source order. Block and
// line comments are emitted together (not either/or) so an arg
// carrying both — `a /* c */, // c2` — never drops the block.
let has_line = pc.has_trailing_line();

// Before-comma block comments trail the arg, before the comma.
if let Some(cpos) = comma_pos {
for comment in &pc.trailing_block {
if is_comment_before_comma(comment, cpos) {
Expand All @@ -1119,10 +1109,25 @@ pub(super) fn build_call_doc_with_wrapping(
}
}
arg_parts.push(d.text(","));

// Same-line line comments, after the comma. The comment goes
// through `line_suffix` (zero width) so it never counts against
// the argument's own group — a long trailing comment can't force
// a binary/conditional arg to break (prettier's `lineSuffix`). It
// forces the call to expand and renders after the comma at EOL.
if has_line {
force_expansion = true;
for comment in &pc.trailing_line {
arg_parts.push(printer.build_trailing_line_comment_doc(comment));
}
}
if has_blank_line {
arg_parts.push(d.literalline());
}
arg_parts.push(d.line());
// A line comment forces a hard break (it runs to EOL); a
// block-only arg uses a soft line so it can stay inline.
arg_parts.push(if has_line { d.hardline() } else { d.line() });

// After-comma block comments (e.g., `arg1, /** @type {T} */ arg2`)
// go AFTER the line break so they stay with the next arg when breaking.
if let Some(cpos) = comma_pos {
Expand Down Expand Up @@ -1176,21 +1181,36 @@ pub(super) fn build_call_doc_with_wrapping(
paren_close,
);

// Own-line comments (block or line) after the last arg (before closing
// paren). These appear as siblings after the trailing comma, forcing
// expansion. Also handles spread with stripped parens via effective_arg_end.
if !pc.leading.is_empty() {
force_expansion = true;
pc.emit_last_arg_dangling_comments(
&mut arg_parts,
printer,
&mut has_trailing_comma_on_last,
);
// Trailing comments after the last arg, before the closing paren, in
// source order: same-line block comments first (split around the source
// comma), then the same-line line comment (after the comma, via
// `line_suffix`), then own-line comments (each on its own line). Emitting
// same-line comments before own-line ones — and never dropping a block —
// avoids merging consecutive comments onto one line (which reverses their
// order) and content loss. The `new`/member-chain last-arg paths do the
// same via the shared emit_* helpers; this path keeps its own loop only
// for the block comma-split (`b /* c */,` vs past the trailing comma).

// (1) Same-line block comments: before-comma blocks trail the arg, after-
// comma blocks are preserved past the trailing comma. Don't force
// expansion on their own — let width/source newlines decide.
// e.g., fn({short} /* c */) stays inline, fn({long...} /* c */) expands.
let comma_pos = find_comma_pos(printer.source, effective_arg_end, paren_close);
for comment in &pc.trailing_block {
if comma_pos.is_some_and(|cp| is_comment_after_comma(comment, cp)) {
last_after_comma.push(d.text(" "));
last_after_comma.push(printer.build_comment_doc(comment));
} else {
arg_parts.push(d.text(" "));
arg_parts.push(printer.build_comment_doc(comment));
}
}

// (2) Same-line line comment, after the comma, via `line_suffix`.
if pc.has_trailing_line() {
if !has_trailing_comma_on_last {
arg_parts.push(d.text(","));
has_trailing_comma_on_last = true;
}

// Build comment docs: " // comment" for each
Expand All @@ -1199,34 +1219,27 @@ pub(super) fn build_call_doc_with_wrapping(
.iter()
.flat_map(|c| [d.text(" "), printer.build_comment_doc(c)])
.collect();
let comments = d.concat(&comment_docs);

// Line comments always force the CALL to expand - the newline after the
// comment means the call must break to multiple lines.
// comment means the call must break to multiple lines. A trailing line
// comment never counts toward width (prettier's `lineSuffix`), so the
// argument's own group (array/object, binary, conditional, …) can stay
// inline even when the comment exceeds print_width; force_expansion
// ensures the call expands.
force_expansion = true;
arg_parts.push(d.line_suffix(d.concat(&comment_docs)));
}

// A trailing line comment never counts toward width (prettier's
// `lineSuffix`), so the argument's own group (array/object, binary,
// conditional, …) can stay inline even when the comment exceeds
// print_width. The force_expansion above ensures the call expands.
arg_parts.push(d.line_suffix(comments));
has_trailing_comma_on_last = true;
} else if pc.has_trailing_block() {
// Trailing block comments: place relative to the source comma.
// Before-comma stay after the arg; after-comma are preserved past
// the trailing comma (emitted by the wrappers below). Don't force
// expansion - let content decide based on width/source newlines.
// e.g., fn({short} /* c */) stays inline, fn({long...} /* c */) expands
let comma_pos = find_comma_pos(printer.source, effective_arg_end, paren_close);
for comment in &pc.trailing_block {
if comma_pos.is_some_and(|cp| is_comment_after_comma(comment, cp)) {
last_after_comma.push(d.text(" "));
last_after_comma.push(printer.build_comment_doc(comment));
} else {
arg_parts.push(d.text(" "));
arg_parts.push(printer.build_comment_doc(comment));
}
}
// (3) Own-line comments (block or line) after the last arg, before the
// closing paren — emitted after the trailing comma, each on its own line.
// Also handles spread with stripped parens via effective_arg_end.
if !pc.leading.is_empty() {
force_expansion = true;
pc.emit_last_arg_dangling_comments(
&mut arg_parts,
printer,
&mut has_trailing_comma_on_last,
);
}
}
}
Expand Down
Loading