git.delta.rocks / jrsonnet / refs/commits / d14f5fd9b388

difftreelog

refactor split pretty-printer into a library crate

tvsrnnvwYaroslav Bolyukin2026-02-08parent: #6e67554.patch.diff
in: master

14 files changed

modifiedCargo.lockdiffbeforeafterboth
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -631,16 +631,23 @@
 version = "0.5.0-pre97"
 dependencies = [
  "clap",
- "dprint-core",
  "hi-doc",
  "indoc",
- "insta",
- "jrsonnet-rowan-parser",
+ "jrsonnet-formatter",
  "tempfile",
  "thiserror",
 ]
 
 [[package]]
+name = "jrsonnet-formatter"
+version = "0.5.0-pre97"
+dependencies = [
+ "dprint-core",
+ "hi-doc",
+ "jrsonnet-rowan-parser",
+]
+
+[[package]]
 name = "jrsonnet-gcmodule"
 version = "0.4.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
modifiedCargo.tomldiffbeforeafterboth
before · Cargo.toml
1[workspace]2members = ["crates/*", "bindings/jsonnet", "cmds/*", "tests", "xtask"]3default-members = ["cmds/jrsonnet"]4resolver = "2"56[workspace.package]7authors = ["Yaroslav Bolyukin <iam@lach.pw>"]8edition = "2021"9license = "MIT"10repository = "https://github.com/CertainLach/jrsonnet"11version = "0.5.0-pre97"1213[workspace.dependencies]14jrsonnet-evaluator = { path = "./crates/jrsonnet-evaluator", version = "0.5.0-pre97" }15jrsonnet-macros = { path = "./crates/jrsonnet-macros", version = "0.5.0-pre97" }16jrsonnet-parser = { path = "./crates/jrsonnet-parser", version = "0.5.0-pre97" }17jrsonnet-rowan-parser = { path = "./crates/jrsonnet-rowan-parser", version = "0.5.0-pre97" }18jrsonnet-interner = { path = "./crates/jrsonnet-interner", version = "0.5.0-pre97" }19jrsonnet-stdlib = { path = "./crates/jrsonnet-stdlib", version = "0.5.0-pre97" }20jrsonnet-cli = { path = "./crates/jrsonnet-cli", version = "0.5.0-pre97" }21jrsonnet-types = { path = "./crates/jrsonnet-types", version = "0.5.0-pre97" }22jrsonnet-gcmodule = { version = "0.4.1" }23# Diagnostics.24# hi-doc is my library, which handles text formatting very well, but isn't polished enough yet25# Previous implementation was based on annotate-snippets, which I don't like for many reasons.26#27# I'm against using miette, because I want to reuse data between interpreter and annotations, yet miette28#   and other libraries want to handle spans etc by itself, which is okay for compiler diagnostics, but is29#   bad for interpreter, where interpreter and parser are paired much closer.30hi-doc = "0.3.0"31annotate-snippets = "0.12.11"3233# CLI34clap = "4.5"35clap_complete = "4.5"3637# Parsing, manifestification is implemented manually everywhere38serde = "1.0.228"39serde_json = "1.0.149"40serde-saphyr = {version = "0.0.17", default-features = false}4142# Error handling43anyhow = "1.0.101"44thiserror = "2.0.18"4546# Code formatting47dprint-core = "0.67.4"4849# Stdlib hashing functions50md5 = "0.8.0"51sha1 = "0.10.6"52sha2 = "0.10.9"53sha3 = "0.10.8"5455# Source code parsing.56# Jrsonnet has two parsers for jsonnet - one is for execution, and another is for better parsing diagnostics/lints/LSP.57# First (and fast one) is based on peg, second is based on rowan.58peg = "0.8.5"59logos = "0.16.1"60ungrammar = "1.16.1"61rowan = "0.16.1"6263mimallocator = "0.1.3"64indoc = "2.0"65insta = "1.46"66tempfile = "3.24"67pathdiff = "0.2.3"68hashbrown = "0.16.1"69static_assertions = "1.1"70rustc-hash = "2.1"71num-bigint = "0.4.6"72strsim = "0.11.1"73proc-macro2 = "1.0"74quote = "1.0"75syn = "2.0"76drop_bomb = "0.1.5"77base64 = "0.22.1"78indexmap = "2.13.0"79itertools = "0.14.0"80xshell = "0.2.7"8182lsp-server = "0.7.9"83lsp-types = "0.97.0"8485regex = "1.12"86lru = "0.16.3"8788json-structural-diff = "0.2.0"89syn-dissect-closure = "0.1.0"9091[workspace.lints.rust]92unsafe_op_in_unsafe_fn = "deny"9394# TODO: add docs everywhere95# missing_doc = "warn"9697elided_lifetimes_in_paths = "allow"98explicit_outlives_requirements = "allow"99noop_method_call = "allow"100single_use_lifetimes = "allow"101variant_size_differences = "allow"102macro_expanded_macro_exports_accessed_by_absolute_paths = "allow"103104[workspace.lints.rustdoc]105all = "warn"106107[workspace.lints.clippy]108all = { level = "warn", priority = -1 }109nursery = { level = "warn", priority = -1 }110pedantic = { level = "warn", priority = -1 }111112ptr_arg = "allow"113# Too verbose114must_use_candidate = "allow"115# A lot of functions pass around errors thrown by code116missing_errors_doc = "allow"117# A lot of pointers have interior Rc118needless_pass_by_value = "allow"119# Its fine120wildcard_imports = "allow"121enum_glob_use = "allow"122module_name_repetitions = "allow"123# TODO: fix individual issues, however this works as intended almost everywhere124cast_precision_loss = "allow"125cast_possible_wrap = "allow"126cast_possible_truncation = "allow"127cast_sign_loss = "allow"128# False positives129# https://github.com/rust-lang/rust-clippy/issues/6902130use_self = "allow"131# https://github.com/rust-lang/rust-clippy/issues/8539132iter_with_drain = "allow"133type_repetition_in_bounds = "allow"134# ci is being run with nightly, but library should work on stable135missing_const_for_fn = "allow"136# too many false-positives with .expect() calls137missing_panics_doc = "allow"138# false positive for IStr type. There is an configuration option for139# such cases, but it doesn't work:140# https://github.com/rust-lang/rust-clippy/issues/9801141mutable_key_type = "allow"142# false positives143redundant_pub_crate = "allow"144# Sometimes code is fancier without that145manual_let_else = "allow"146# Something is broken about that lint, can't be allowed for147# codegenerated-stdlib block148similar_names = "allow"149150#[profile.test]151#opt-level = 1152153[profile.release]154opt-level = 3155lto = "fat"156codegen-units = 1157debug = 0158panic = "abort"159strip = true160161[profile.releasedebug]162inherits = "release"163debug = 2164panic = "unwind"165strip = false
after · Cargo.toml
1[workspace]2members = ["crates/*", "bindings/jsonnet", "cmds/*", "tests", "xtask"]3default-members = ["cmds/jrsonnet"]4resolver = "2"56[workspace.package]7authors = ["Yaroslav Bolyukin <iam@lach.pw>"]8edition = "2021"9license = "MIT"10repository = "https://github.com/CertainLach/jrsonnet"11version = "0.5.0-pre97"1213[workspace.dependencies]14jrsonnet-evaluator = { path = "./crates/jrsonnet-evaluator", version = "0.5.0-pre97" }15jrsonnet-macros = { path = "./crates/jrsonnet-macros", version = "0.5.0-pre97" }16jrsonnet-parser = { path = "./crates/jrsonnet-parser", version = "0.5.0-pre97" }17jrsonnet-rowan-parser = { path = "./crates/jrsonnet-rowan-parser", version = "0.5.0-pre97" }18jrsonnet-interner = { path = "./crates/jrsonnet-interner", version = "0.5.0-pre97" }19jrsonnet-stdlib = { path = "./crates/jrsonnet-stdlib", version = "0.5.0-pre97" }20jrsonnet-cli = { path = "./crates/jrsonnet-cli", version = "0.5.0-pre97" }21jrsonnet-types = { path = "./crates/jrsonnet-types", version = "0.5.0-pre97" }22jrsonnet-formatter = { path = "./crates/jrsonnet-formatter", version = "0.5.0-pre97" }23jrsonnet-gcmodule = { version = "0.4.1" }24# Diagnostics.25# hi-doc is my library, which handles text formatting very well, but isn't polished enough yet26# Previous implementation was based on annotate-snippets, which I don't like for many reasons.27#28# I'm against using miette, because I want to reuse data between interpreter and annotations, yet miette29#   and other libraries want to handle spans etc by itself, which is okay for compiler diagnostics, but is30#   bad for interpreter, where interpreter and parser are paired much closer.31hi-doc = "0.3.0"32annotate-snippets = "0.12.11"3334# CLI35clap = "4.5"36clap_complete = "4.5"3738# Parsing, manifestification is implemented manually everywhere39serde = "1.0.228"40serde_json = "1.0.149"41serde-saphyr = {version = "0.0.17", default-features = false}4243# Error handling44anyhow = "1.0.101"45thiserror = "2.0.18"4647# Code formatting48dprint-core = "0.67.4"4950# Stdlib hashing functions51md5 = "0.8.0"52sha1 = "0.10.6"53sha2 = "0.10.9"54sha3 = "0.10.8"5556# Source code parsing.57# Jrsonnet has two parsers for jsonnet - one is for execution, and another is for better parsing diagnostics/lints/LSP.58# First (and fast one) is based on peg, second is based on rowan.59peg = "0.8.5"60logos = "0.16.1"61ungrammar = "1.16.1"62rowan = "0.16.1"6364mimallocator = "0.1.3"65indoc = "2.0"66insta = "1.46"67tempfile = "3.24"68pathdiff = "0.2.3"69hashbrown = "0.16.1"70static_assertions = "1.1"71rustc-hash = "2.1"72num-bigint = "0.4.6"73strsim = "0.11.1"74proc-macro2 = "1.0"75quote = "1.0"76syn = "2.0"77drop_bomb = "0.1.5"78base64 = "0.22.1"79indexmap = "2.13.0"80itertools = "0.14.0"81xshell = "0.2.7"8283lsp-server = "0.7.9"84lsp-types = "0.97.0"8586regex = "1.12"87lru = "0.16.3"8889json-structural-diff = "0.2.0"90syn-dissect-closure = "0.1.0"9192[workspace.lints.rust]93unsafe_op_in_unsafe_fn = "deny"9495# TODO: add docs everywhere96# missing_doc = "warn"9798elided_lifetimes_in_paths = "allow"99explicit_outlives_requirements = "allow"100noop_method_call = "allow"101single_use_lifetimes = "allow"102variant_size_differences = "allow"103macro_expanded_macro_exports_accessed_by_absolute_paths = "allow"104105[workspace.lints.rustdoc]106all = "warn"107108[workspace.lints.clippy]109all = { level = "warn", priority = -1 }110nursery = { level = "warn", priority = -1 }111pedantic = { level = "warn", priority = -1 }112113ptr_arg = "allow"114# Too verbose115must_use_candidate = "allow"116# A lot of functions pass around errors thrown by code117missing_errors_doc = "allow"118# A lot of pointers have interior Rc119needless_pass_by_value = "allow"120# Its fine121wildcard_imports = "allow"122enum_glob_use = "allow"123module_name_repetitions = "allow"124# TODO: fix individual issues, however this works as intended almost everywhere125cast_precision_loss = "allow"126cast_possible_wrap = "allow"127cast_possible_truncation = "allow"128cast_sign_loss = "allow"129# False positives130# https://github.com/rust-lang/rust-clippy/issues/6902131use_self = "allow"132# https://github.com/rust-lang/rust-clippy/issues/8539133iter_with_drain = "allow"134type_repetition_in_bounds = "allow"135# ci is being run with nightly, but library should work on stable136missing_const_for_fn = "allow"137# too many false-positives with .expect() calls138missing_panics_doc = "allow"139# false positive for IStr type. There is an configuration option for140# such cases, but it doesn't work:141# https://github.com/rust-lang/rust-clippy/issues/9801142mutable_key_type = "allow"143# false positives144redundant_pub_crate = "allow"145# Sometimes code is fancier without that146manual_let_else = "allow"147# Something is broken about that lint, can't be allowed for148# codegenerated-stdlib block149similar_names = "allow"150151#[profile.test]152#opt-level = 1153154[profile.release]155opt-level = 3156lto = "fat"157codegen-units = 1158debug = 0159panic = "abort"160strip = true161162[profile.releasedebug]163inherits = "release"164debug = 2165panic = "unwind"166strip = false
modifiedcmds/jrsonnet-fmt/Cargo.tomldiffbeforeafterboth
--- a/cmds/jrsonnet-fmt/Cargo.toml
+++ b/cmds/jrsonnet-fmt/Cargo.toml
@@ -10,11 +10,9 @@
 workspace = true
 
 [dependencies]
-dprint-core.workspace = true
-jrsonnet-rowan-parser.workspace = true
-insta.workspace = true
 indoc.workspace = true
 hi-doc.workspace = true
 clap = { workspace = true, features = ["derive"] }
 tempfile.workspace = true
 thiserror.workspace = true
+jrsonnet-formatter.workspace = true
deletedcmds/jrsonnet-fmt/src/children.rsdiffbeforeafterboth
--- a/cmds/jrsonnet-fmt/src/children.rs
+++ /dev/null
@@ -1,251 +0,0 @@
-// TODO: Return errors as trivia
-
-use std::{fmt::Debug, mem};
-
-use jrsonnet_rowan_parser::{
-	nodes::{CustomError, Trivia, TriviaKind},
-	AstNode, AstToken, SyntaxElement, SyntaxNode, TS,
-};
-
-pub type ChildTrivia = Vec<Result<Trivia, String>>;
-
-/// Node should have no non-trivia tokens before element
-pub fn trivia_before(node: SyntaxNode, end: Option<&SyntaxElement>) -> ChildTrivia {
-	let mut out = Vec::new();
-	for item in node.children_with_tokens() {
-		if Some(&item) == end {
-			break;
-		}
-
-		if let Some(trivia) = item.as_token().cloned().and_then(Trivia::cast) {
-			out.push(Ok(trivia));
-		} else if CustomError::can_cast(item.kind()) {
-			out.push(Err(item.to_string()));
-		} else if end.is_none() {
-			break;
-		} else {
-			assert!(
-				TS![, ;].contains(item.kind()),
-				"silently eaten token: {:?}",
-				item.kind()
-			);
-		}
-	}
-	out
-}
-/// Node should have no non-trivia tokens after element
-pub fn trivia_after(node: SyntaxNode, start: Option<&SyntaxElement>) -> ChildTrivia {
-	if start.is_none() {
-		return Vec::new();
-	}
-	let mut iter = node.children_with_tokens().peekable();
-	while iter.peek() != start {
-		iter.next();
-	}
-	iter.next();
-	let mut out = Vec::new();
-	for item in iter {
-		if let Some(trivia) = item.as_token().cloned().and_then(Trivia::cast) {
-			out.push(Ok(trivia));
-		} else if CustomError::can_cast(item.kind()) {
-			out.push(Err(item.to_string()));
-		} else {
-			assert!(
-				TS![, ;].contains(item.kind()),
-				"silently eaten token: {:?}",
-				item.kind()
-			);
-		}
-	}
-	out
-}
-
-pub fn children_between<T: AstNode + Debug>(
-	node: SyntaxNode,
-	start: Option<&SyntaxElement>,
-	end: Option<&SyntaxElement>,
-	trailing: Option<ChildTrivia>,
-) -> (Vec<Child<T>>, EndingComments) {
-	let mut iter = node.children_with_tokens().peekable();
-	if start.is_some() {
-		while iter.peek() != start {
-			iter.next();
-		}
-		iter.next();
-	}
-	children(
-		iter.take_while(|i| Some(i) != end),
-		start.is_none() && end.is_none(),
-		trailing,
-	)
-}
-
-pub fn should_start_with_newline(prev_inline: Option<&ChildTrivia>, tt: &ChildTrivia) -> bool {
-	count_newlines_before(tt)
-		+ prev_inline
-			.map(count_newlines_after)
-			.unwrap_or_default()
-
-		// First for previous item end, second for current item
-		>= 2
-}
-
-fn count_newlines_before(tt: &ChildTrivia) -> usize {
-	let mut nl_count = 0;
-	for t in tt {
-		match t {
-			Ok(t) => match t.kind() {
-				TriviaKind::Whitespace => {
-					nl_count += t.text().bytes().filter(|b| *b == b'\n').count();
-				}
-				_ => break,
-			},
-			Err(_) => {
-				nl_count += 1;
-			}
-		}
-	}
-	nl_count
-}
-fn count_newlines_after(tt: &ChildTrivia) -> usize {
-	let mut nl_count = 0;
-	for t in tt.iter().rev() {
-		match t {
-			Ok(t) => match t.kind() {
-				TriviaKind::Whitespace => {
-					nl_count += t.text().bytes().filter(|b| *b == b'\n').count();
-				}
-				TriviaKind::SingleLineHashComment | TriviaKind::SingleLineSlashComment => {
-					nl_count += 1;
-					break;
-				}
-				_ => {}
-			},
-			Err(_) => nl_count += 1,
-		}
-	}
-	nl_count
-}
-
-pub fn children<T: AstNode + Debug>(
-	items: impl Iterator<Item = SyntaxElement>,
-	loose: bool,
-	mut trailing: Option<ChildTrivia>,
-) -> (Vec<Child<T>>, EndingComments) {
-	let mut out = Vec::new();
-	let mut current_child = None::<Child<T>>;
-	let mut next = ChildTrivia::new();
-	// Previous element ended, do not add more inline comments
-	let mut started_next = false;
-	let mut had_some = false;
-
-	for item in items {
-		if let Some(value) = item.as_node().cloned().and_then(T::cast) {
-			let before_trivia = if let Some(trailing) = trailing.take() {
-				assert!(next.is_empty());
-				trailing
-			} else {
-				mem::take(&mut next)
-			};
-			let last_child = current_child.replace(Child {
-				// First item should not start with newline
-				should_start_with_newline: had_some
-					&& should_start_with_newline(
-						current_child.as_ref().map(|c| &c.inline_trivia),
-						&before_trivia,
-					),
-				before_trivia,
-				value,
-				inline_trivia: Vec::new(),
-			});
-			if let Some(last_child) = last_child {
-				out.push(last_child);
-			}
-			had_some = true;
-			started_next = false;
-		} else if let Some(trivia) = item.as_token().cloned().and_then(Trivia::cast) {
-			let is_single_line_comment = trivia.kind() == TriviaKind::SingleLineHashComment
-				|| trivia.kind() == TriviaKind::SingleLineSlashComment;
-			if trailing.is_some() {
-				// Someone have already parsed trivia for us
-				continue;
-			} else if started_next
-				|| current_child.is_none()
-				|| trivia.text().contains('\n') && !is_single_line_comment
-			{
-				next.push(Ok(trivia.clone()));
-				started_next = true;
-			} else {
-				let cur = current_child.as_mut().expect("checked not none");
-				cur.inline_trivia.push(Ok(trivia));
-				if is_single_line_comment {
-					started_next = true;
-				}
-			}
-			had_some = true;
-		} else if CustomError::can_cast(item.kind()) {
-			next.push(Err(item.to_string()));
-		} else if loose {
-			if had_some {
-				break;
-			}
-			started_next = true;
-		} else {
-			assert!(
-				TS![, ;].contains(item.kind()),
-				"silently eaten token: {:?}",
-				item.kind()
-			);
-		}
-	}
-
-	let ending_comments = EndingComments {
-		should_start_with_newline: should_start_with_newline(
-			current_child.as_ref().map(|c| &c.inline_trivia),
-			&next,
-		),
-		trivia: next,
-	};
-
-	if let Some(current_child) = current_child {
-		out.push(current_child);
-	}
-
-	(out, ending_comments)
-}
-
-#[derive(Debug)]
-pub struct Child<T> {
-	/// If this child has two newlines above in source code, so it needs to have it in the output
-	pub should_start_with_newline: bool,
-	/// Comment before item, i.e
-	///
-	/// ```ignore
-	/// // Comment
-	/// item
-	/// ```
-	pub before_trivia: ChildTrivia,
-	pub value: T,
-	/// Comment after line, but located at same line
-	///
-	/// ```ignore
-	/// item1, // Inline comment
-	/// // Not inline comment
-	/// item2,
-	/// ```
-	pub inline_trivia: ChildTrivia,
-}
-
-pub struct EndingComments {
-	/// If this child has two newlines above in source code, so it needs to have it in the output
-	pub should_start_with_newline: bool,
-	pub trivia: ChildTrivia,
-}
-impl EndingComments {
-	pub fn is_empty(&self) -> bool {
-		!self.should_start_with_newline && self.trivia.is_empty()
-	}
-	pub fn extract_trailing(&mut self) -> ChildTrivia {
-		mem::take(&mut self.trivia)
-	}
-}
deletedcmds/jrsonnet-fmt/src/comments.rsdiffbeforeafterboth
--- a/cmds/jrsonnet-fmt/src/comments.rs
+++ /dev/null
@@ -1,181 +0,0 @@
-use std::string::String;
-
-use dprint_core::formatting::PrintItems;
-use jrsonnet_rowan_parser::{nodes::TriviaKind, AstToken};
-
-use crate::{children::ChildTrivia, p, pi};
-
-pub enum CommentLocation {
-	/// Above local, field, other things
-	AboveItem,
-	/// After item
-	ItemInline,
-	/// After all items in object
-	EndOfItems,
-}
-
-#[allow(clippy::too_many_lines, clippy::cognitive_complexity)]
-pub fn format_comments(comments: &ChildTrivia, loc: CommentLocation, out: &mut PrintItems) {
-	for c in comments {
-		let Ok(c) = c else {
-			let mut text = c.as_ref().unwrap_err() as &str;
-			while !text.is_empty() {
-				let pos = text.find(['\n', '\t']).unwrap_or(text.len());
-				let sliced = &text[..pos];
-				p!(out, string(sliced.to_string()));
-				text = &text[pos..];
-				if !text.is_empty() {
-					match text.as_bytes()[0] {
-						b'\n' => p!(out, nl),
-						b'\t' => p!(out, tab),
-						_ => unreachable!(),
-					}
-					text = &text[1..];
-				}
-			}
-			continue;
-		};
-		match c.kind() {
-			TriviaKind::Whitespace => {}
-			TriviaKind::MultiLineComment => {
-				let mut text = c
-					.text()
-					.strip_prefix("/*")
-					.expect("ml comment starts with /*")
-					.strip_suffix("*/")
-					.expect("ml comment ends with */");
-				// doc-style comment, /**
-				let doc = if text.starts_with('*') {
-					text = &text[1..];
-					true
-				} else {
-					false
-				};
-				// Is comment starts with text immediatly, i.e /*text
-				let mut immediate_start = true;
-				let mut lines = text
-					.split('\n')
-					.map(|l| l.trim_end().to_string())
-					.skip_while(|l| {
-						if l.is_empty() {
-							immediate_start = false;
-							true
-						} else {
-							false
-						}
-					})
-					.collect::<Vec<_>>();
-				while lines.last().is_some_and(String::is_empty) {
-					lines.pop();
-				}
-				if lines.len() == 1 && !doc {
-					if matches!(loc, CommentLocation::ItemInline) {
-						p!(out, str(" "));
-					}
-					p!(out, str("/* ") string(lines[0].trim().to_string()) str(" */") nl);
-				} else if !lines.is_empty() {
-					fn common_ws_prefix<'a>(a: &'a str, b: &str) -> &'a str {
-						let offset = a
-							.bytes()
-							.zip(b.bytes())
-							.take_while(|(a, b)| a == b && (a.is_ascii_whitespace() || *a == b'*'))
-							.count();
-						&a[..offset]
-					}
-					// First line is not empty, extract ws prefix of it
-					let mut common_ws_padding = (if immediate_start && lines.len() > 1 {
-						common_ws_prefix(&lines[1], &lines[1])
-					} else {
-						common_ws_prefix(&lines[0], &lines[0])
-					})
-					.to_string();
-					for line in lines
-						.iter()
-						.skip(if immediate_start { 2 } else { 1 })
-						.filter(|l| !l.is_empty())
-					{
-						common_ws_padding = common_ws_prefix(&common_ws_padding, line).to_string();
-					}
-					for line in lines
-						.iter_mut()
-						.skip(usize::from(immediate_start))
-						.filter(|l| !l.is_empty())
-					{
-						*line = line
-							.strip_prefix(&common_ws_padding)
-							.expect("all non-empty lines start with this padding")
-							.to_string();
-					}
-
-					p!(out, str("/*"));
-					if doc {
-						p!(out, str("*"));
-					}
-					p!(out, nl);
-					for mut line in lines {
-						if doc {
-							p!(out, str(" *"));
-						}
-						if line.is_empty() {
-							p!(out, nl);
-						} else {
-							if doc {
-								p!(out, str(" "));
-							}
-							while let Some(new_line) = line.strip_prefix('\t') {
-								if doc {
-									p!(out, str("    "));
-								} else {
-									p!(out, tab);
-								}
-								line = new_line.to_string();
-							}
-							p!(out, string(line.to_string()) nl);
-						}
-					}
-					if doc {
-						p!(out, str(" "));
-					}
-					p!(out, str("*/") nl);
-				}
-			}
-			// TODO: Keep common padding for multiple continous lines of single-line comments
-			// I.e
-			// ```
-			// #  Line1
-			// #    Line2
-			// ```
-			// Should be reformatted as
-			// ```
-			// # Line1
-			// #   Line2
-			// ```
-			// But currently comment formatter is not aware of continous comment lines, and reformats it as
-			// ```
-			// # Line1
-			// # Line2
-			// ```
-			TriviaKind::SingleLineHashComment => {
-				if matches!(loc, CommentLocation::ItemInline) {
-					p!(out, str(" "));
-				}
-				p!(out, str("# ") string(c.text().strip_prefix('#').expect("hash comment starts with #").trim().to_string()));
-				if !matches!(loc, CommentLocation::ItemInline) {
-					p!(out, nl);
-				}
-			}
-			TriviaKind::SingleLineSlashComment => {
-				if matches!(loc, CommentLocation::ItemInline) {
-					p!(out, str(" "));
-				}
-				p!(out, str("// ") string(c.text().strip_prefix("//").expect("comment starts with //").trim().to_string()));
-				if !matches!(loc, CommentLocation::ItemInline) {
-					p!(out, nl);
-				}
-			}
-			// Garbage in - garbage out
-			TriviaKind::ErrorCommentTooShort => p!(out, str("/*/")),
-			TriviaKind::ErrorCommentUnterminated => p!(out, string(c.text().to_string())),
-		}
-	}
-}
modifiedcmds/jrsonnet-fmt/src/main.rsdiffbeforeafterboth
--- a/cmds/jrsonnet-fmt/src/main.rs
+++ b/cmds/jrsonnet-fmt/src/main.rs
@@ -1,755 +1,7 @@
-use std::{
-	any::type_name,
-	fs,
-	io::{self, Write},
-	path::PathBuf,
-	process,
-	rc::Rc,
-};
+use std::{fs, io};
 
-use children::{children_between, trivia_before};
 use clap::Parser;
-use dprint_core::formatting::{
-	condition_helpers::is_multiple_lines, condition_resolvers::true_resolver,
-	ConditionResolverContext, LineNumber, PrintItems, PrintOptions,
-};
-use hi_doc::Formatting;
-use jrsonnet_rowan_parser::{
-	nodes::{
-		Arg, ArgsDesc, Assertion, BinaryOperator, Bind, CompSpec, Destruct, DestructArrayPart,
-		DestructRest, Expr, ExprBase, FieldName, ForSpec, IfSpec, ImportKind, Literal, Member,
-		Name, Number, ObjBody, ObjLocal, ParamsDesc, SliceDesc, SourceFile, Stmt, Suffix, Text,
-		UnaryOperator, Visibility,
-	},
-	AstNode, AstToken as _, SyntaxToken,
-};
-
-use crate::{
-	children::trivia_after,
-	comments::{format_comments, CommentLocation},
-};
-
-mod children;
-mod comments;
-#[cfg(test)]
-mod tests;
-
-pub trait Printable {
-	fn print(&self, out: &mut PrintItems);
-}
-
-macro_rules! pi {
-	(@i; $($t:tt)*) => {{
-		#[allow(unused_mut)]
-		let mut o = dprint_core::formatting::PrintItems::new();
-		pi!(@s; o: $($t)*);
-		o
-	}};
-	(@s; $o:ident: str($e:expr $(,)?) $($t:tt)*) => {{
-		$o.push_string($e.to_owned());
-		pi!(@s; $o: $($t)*);
-	}};
-	(@s; $o:ident: string($e:expr $(,)?) $($t:tt)*) => {{
-		$o.push_string($e);
-		pi!(@s; $o: $($t)*);
-	}};
-	(@s; $o:ident: nl $($t:tt)*) => {{
-		$o.push_signal(dprint_core::formatting::Signal::NewLine);
-		pi!(@s; $o: $($t)*);
-	}};
-	(@s; $o:ident: tab $($t:tt)*) => {{
-		$o.push_signal(dprint_core::formatting::Signal::Tab);
-		pi!(@s; $o: $($t)*);
-	}};
-	(@s; $o:ident: >i $($t:tt)*) => {{
-		$o.push_signal(dprint_core::formatting::Signal::StartIndent);
-		pi!(@s; $o: $($t)*);
-	}};
-	(@s; $o:ident: <i $($t:tt)*) => {{
-		$o.push_signal(dprint_core::formatting::Signal::FinishIndent);
-		pi!(@s; $o: $($t)*);
-	}};
-	(@s; $o:ident: info($v:expr) $($t:tt)*) => {{
-		$o.push_info($v);
-		pi!(@s; $o: $($t)*);
-	}};
-	(@s; $o:ident: if($s:literal, $cond:expr, $($i:tt)*) $($t:tt)*) => {{
-		$o.push_condition(dprint_core::formatting::conditions::if_true(
-			$s,
-			$cond.clone(),
-			{
-				let mut o = PrintItems::new();
-				p!(o, $($i)*);
-				o
-			},
-		));
-		pi!(@s; $o: $($t)*);
-	}};
-	(@s; $o:ident: if_else($s:literal, $cond:expr, $($i:tt)*)($($e:tt)+) $($t:tt)*) => {{
-		$o.push_condition(dprint_core::formatting::conditions::if_true_or(
-			$s,
-			$cond.clone(),
-			{
-				let mut o = PrintItems::new();
-				p!(o, $($i)*);
-				o
-			},
-			{
-				let mut o = PrintItems::new();
-				p!(o, $($e)*);
-				o
-			},
-		));
-		pi!(@s; $o: $($t)*);
-	}};
-	(@s; $o:ident: if_not($s:literal, $cond:expr, $($e:tt)*) $($t:tt)*) => {{
-		$o.push_condition(dprint_core::formatting::conditions::if_true_or(
-			$s,
-			$cond.clone(),
-			{
-				let o = PrintItems::new();
-				o
-			},
-			{
-				let mut o = PrintItems::new();
-				p!(o, $($e)*);
-				o
-			},
-		));
-		pi!(@s; $o: $($t)*);
-	}};
-	(@s; $o:ident: {$expr:expr} $($t:tt)*) => {{
-		$expr.print($o);
-		pi!(@s; $o: $($t)*);
-	}};
-	(@s; $o:ident: items($expr:expr) $($t:tt)*) => {{
-		$o.extend($expr);
-		pi!(@s; $o: $($t)*);
-	}};
-	(@s; $o:ident: if ($e:expr)($($then:tt)*) $($t:tt)*) => {{
-		if $e {
-			pi!(@s; $o: $($then)*);
-		}
-		pi!(@s; $o: $($t)*);
-	}};
-	(@s; $o:ident: ifelse ($e:expr)($($then:tt)*)($($else:tt)*) $($t:tt)*) => {{
-		if $e {
-			pi!(@s; $o: $($then)*);
-		} else {
-			pi!(@s; $o: $($else)*);
-		}
-		pi!(@s; $o: $($t)*);
-	}};
-	(@s; $i:ident:) => {}
-}
-macro_rules! p {
-	($o:ident, $($t:tt)*) => {
-		pi!(@s; $o: $($t)*)
-	};
-}
-pub(crate) use p;
-pub(crate) use pi;
-
-impl<P> Printable for Option<P>
-where
-	P: Printable,
-{
-	fn print(&self, out: &mut PrintItems) {
-		if let Some(v) = self {
-			v.print(out);
-		} else {
-			p!(
-				out,
-				string(format!(
-					"/*missing {}*/",
-					type_name::<P>().replace("jrsonnet_rowan_parser::generated::nodes::", "")
-				),)
-			);
-		}
-	}
-}
-
-impl Printable for SyntaxToken {
-	fn print(&self, out: &mut PrintItems) {
-		p!(out, string(self.to_string()));
-	}
-}
-
-impl Printable for Text {
-	fn print(&self, out: &mut PrintItems) {
-		p!(out, string(format!("{}", self)));
-	}
-}
-impl Printable for Number {
-	fn print(&self, out: &mut PrintItems) {
-		p!(out, string(format!("{}", self)));
-	}
-}
-
-impl Printable for Name {
-	fn print(&self, out: &mut PrintItems) {
-		p!(out, { self.ident_lit() });
-	}
-}
-
-impl Printable for DestructRest {
-	fn print(&self, out: &mut PrintItems) {
-		p!(out, str("..."));
-		if let Some(name) = self.into() {
-			p!(out, { name });
-		}
-	}
-}
-
-impl Printable for Destruct {
-	fn print(&self, out: &mut PrintItems) {
-		match self {
-			Self::DestructFull(f) => {
-				p!(out, { f.name() });
-			}
-			Self::DestructSkip(_) => p!(out, str("?")),
-			Self::DestructArray(a) => {
-				p!(out, str("[") >i nl);
-				for el in a.destruct_array_parts() {
-					match el {
-						DestructArrayPart::DestructArrayElement(e) => {
-							p!(out, {e.destruct()} str(",") nl);
-						}
-						DestructArrayPart::DestructRest(d) => {
-							p!(out, {d} str(",") nl);
-						}
-					}
-				}
-				p!(out, <i str("]"));
-			}
-			Self::DestructObject(o) => {
-				p!(out, str("{") >i nl);
-				for item in o.destruct_object_fields() {
-					p!(out, { item.field() });
-					if let Some(des) = item.destruct() {
-						p!(out, str(": ") {des});
-					}
-					if let Some(def) = item.expr() {
-						p!(out, str(" = ") {def});
-					}
-					p!(out, str(",") nl);
-				}
-				if let Some(rest) = o.destruct_rest() {
-					p!(out, {rest} nl);
-				}
-				p!(out, <i str("}"));
-			}
-		}
-	}
-}
-
-impl Printable for FieldName {
-	fn print(&self, out: &mut PrintItems) {
-		match self {
-			Self::FieldNameFixed(f) => {
-				if let Some(id) = f.id() {
-					p!(out, { id });
-				} else if let Some(str) = f.text() {
-					p!(out, { str });
-				} else {
-					p!(out, str("/*missing FieldName*/"));
-				}
-			}
-			Self::FieldNameDynamic(d) => {
-				p!(out, str("[") {d.expr()} str("]"));
-			}
-		}
-	}
-}
-
-impl Printable for Visibility {
-	fn print(&self, out: &mut PrintItems) {
-		p!(out, string(self.to_string()));
-	}
-}
-
-impl Printable for ObjLocal {
-	fn print(&self, out: &mut PrintItems) {
-		p!(out, str("local ") {self.bind()});
-	}
-}
-
-impl Printable for Assertion {
-	fn print(&self, out: &mut PrintItems) {
-		p!(out, str("assert ") {self.condition()});
-		if self.colon_token().is_some() || self.message().is_some() {
-			p!(out, str(": ") {self.message()});
-		}
-	}
-}
-
-impl Printable for ParamsDesc {
-	fn print(&self, out: &mut PrintItems) {
-		p!(out, str("(") >i nl);
-		for param in self.params() {
-			p!(out, { param.destruct() });
-			if param.assign_token().is_some() || param.expr().is_some() {
-				p!(out, str(" = ") {param.expr()});
-			}
-			p!(out, str(",") nl);
-		}
-		p!(out, <i str(")"));
-	}
-}
-impl Printable for ArgsDesc {
-	fn print(&self, out: &mut PrintItems) {
-		let start = LineNumber::new("start");
-		let end = LineNumber::new("end");
-		let multi_line = Rc::new(move |condition_context: &mut ConditionResolverContext| {
-			is_multiple_lines(condition_context, start, end).map(|v| !v)
-		});
-		p!(out, str("(") info(start) if("start args", multi_line, >i nl));
-		let (children, end_comments) = children_between::<Arg>(
-			self.syntax().clone(),
-			self.l_paren_token().map(Into::into).as_ref(),
-			self.r_paren_token().map(Into::into).as_ref(),
-			None,
-		);
-		let mut args = children.into_iter().peekable();
-		while let Some(ele) = args.next() {
-			if ele.should_start_with_newline {
-				p!(out, nl);
-			}
-			format_comments(&ele.before_trivia, CommentLocation::AboveItem, out);
-			let arg = ele.value;
-			if arg.name().is_some() || arg.assign_token().is_some() {
-				p!(out, {arg.name()} str(" = "));
-			}
-			let comma_between = if args.peek().is_some() {
-				true_resolver()
-			} else {
-				multi_line.clone()
-			};
-			p!(out, {arg.expr()} if("arg comma", comma_between, str(",") if_not("between args", multi_line, str(" "))));
-			format_comments(&ele.inline_trivia, CommentLocation::ItemInline, out);
-			p!(out, if("between args", multi_line, nl));
-		}
-		if end_comments.should_start_with_newline {
-			p!(out, nl);
-		}
-		format_comments(&end_comments.trivia, CommentLocation::EndOfItems, out);
-		p!(out, if("end args", multi_line, <i info(end)) str(")"));
-	}
-}
-impl Printable for SliceDesc {
-	fn print(&self, out: &mut PrintItems) {
-		p!(out, str("["));
-		if self.from().is_some() {
-			p!(out, { self.from() });
-		}
-		p!(out, str(":"));
-		if self.end().is_some() {
-			p!(out, { self.end().map(|e| e.expr()) });
-		}
-		// Keep only one : in case if we don't need step
-		if self.step().is_some() {
-			p!(out, str(":") {self.step().map(|e|e.expr())});
-		}
-		p!(out, str("]"));
-	}
-}
-
-impl Printable for Member {
-	fn print(&self, out: &mut PrintItems) {
-		match self {
-			Self::MemberBindStmt(b) => {
-				p!(out, { b.obj_local() });
-			}
-			Self::MemberAssertStmt(ass) => {
-				p!(out, { ass.assertion() });
-			}
-			Self::MemberFieldNormal(n) => {
-				p!(out, {n.field_name()} if(n.plus_token().is_some())({n.plus_token()}) {n.visibility()} str(" ") {n.expr()});
-			}
-			Self::MemberFieldMethod(m) => {
-				p!(out, {m.field_name()} {m.params_desc()} {m.visibility()} str(" ") {m.expr()});
-			}
-		}
-	}
-}
-
-impl Printable for ObjBody {
-	fn print(&self, out: &mut PrintItems) {
-		match self {
-			Self::ObjBodyComp(l) => {
-				let (children, mut end_comments) = children_between::<Member>(
-					l.syntax().clone(),
-					l.l_brace_token().map(Into::into).as_ref(),
-					Some(
-						&(l.comp_specs()
-							.next()
-							.expect("at least one spec is defined")
-							.syntax()
-							.clone())
-						.into(),
-					),
-					None,
-				);
-				let trailing_for_comp = end_comments.extract_trailing();
-				p!(out, str("{") >i nl);
-				for mem in children {
-					if mem.should_start_with_newline {
-						p!(out, nl);
-					}
-					format_comments(&mem.before_trivia, CommentLocation::AboveItem, out);
-					p!(out, {mem.value} str(","));
-					format_comments(&mem.inline_trivia, CommentLocation::ItemInline, out);
-					p!(out, nl);
-				}
-
-				if end_comments.should_start_with_newline {
-					p!(out, nl);
-				}
-				format_comments(&end_comments.trivia, CommentLocation::EndOfItems, out);
-
-				let (compspecs, end_comments) = children_between::<CompSpec>(
-					l.syntax().clone(),
-					l.member_comps()
-						.last()
-						.map(|m| m.syntax().clone())
-						.map(Into::into)
-						.or_else(|| l.l_brace_token().map(Into::into))
-						.as_ref(),
-					l.r_brace_token().map(Into::into).as_ref(),
-					Some(trailing_for_comp),
-				);
-				for mem in compspecs {
-					if mem.should_start_with_newline {
-						p!(out, nl);
-					}
-					format_comments(&mem.before_trivia, CommentLocation::AboveItem, out);
-					p!(out, { mem.value });
-					format_comments(&mem.inline_trivia, CommentLocation::ItemInline, out);
-				}
-				if end_comments.should_start_with_newline {
-					p!(out, nl);
-				}
-				format_comments(&end_comments.trivia, CommentLocation::EndOfItems, out);
-
-				p!(out, nl <i str("}"));
-			}
-			Self::ObjBodyMemberList(l) => {
-				let (children, end_comments) = children_between::<Member>(
-					l.syntax().clone(),
-					l.l_brace_token().map(Into::into).as_ref(),
-					l.r_brace_token().map(Into::into).as_ref(),
-					None,
-				);
-				if children.is_empty() && end_comments.is_empty() {
-					p!(out, str("{ }"));
-					return;
-				}
-				p!(out, str("{") >i nl);
-				for (i, mem) in children.into_iter().enumerate() {
-					if mem.should_start_with_newline && i != 0 {
-						p!(out, nl);
-					}
-					format_comments(&mem.before_trivia, CommentLocation::AboveItem, out);
-					p!(out, {mem.value} str(","));
-					format_comments(&mem.inline_trivia, CommentLocation::ItemInline, out);
-					p!(out, nl);
-				}
-
-				if end_comments.should_start_with_newline {
-					p!(out, nl);
-				}
-				format_comments(&end_comments.trivia, CommentLocation::EndOfItems, out);
-				p!(out, <i str("}"));
-			}
-		}
-	}
-}
-impl Printable for UnaryOperator {
-	fn print(&self, out: &mut PrintItems) {
-		p!(out, string(self.text().to_string()));
-	}
-}
-impl Printable for BinaryOperator {
-	fn print(&self, out: &mut PrintItems) {
-		p!(out, string(self.text().to_string()));
-	}
-}
-impl Printable for Bind {
-	fn print(&self, out: &mut PrintItems) {
-		match self {
-			Self::BindDestruct(d) => {
-				p!(out, {d.into()} str(" = ") {d.value()});
-			}
-			Self::BindFunction(f) => {
-				p!(out, {f.name()} {f.params()} str(" = ") {f.value()});
-			}
-		}
-	}
-}
-impl Printable for Literal {
-	fn print(&self, out: &mut PrintItems) {
-		p!(out, string(self.syntax().to_string()));
-	}
-}
-impl Printable for ImportKind {
-	fn print(&self, out: &mut PrintItems) {
-		p!(out, string(self.syntax().to_string()));
-	}
-}
-impl Printable for ForSpec {
-	fn print(&self, out: &mut PrintItems) {
-		p!(out, str("for ") {self.bind()} str(" in ") {self.expr()});
-	}
-}
-impl Printable for IfSpec {
-	fn print(&self, out: &mut PrintItems) {
-		p!(out, str("if ") {self.expr()});
-	}
-}
-impl Printable for CompSpec {
-	fn print(&self, out: &mut PrintItems) {
-		match self {
-			Self::ForSpec(f) => f.print(out),
-			Self::IfSpec(i) => i.print(out),
-		}
-	}
-}
-impl Printable for Expr {
-	fn print(&self, out: &mut PrintItems) {
-		let (stmts, _ending) = children_between::<Stmt>(
-			self.syntax().clone(),
-			None,
-			self.expr_base()
-				.as_ref()
-				.map(ExprBase::syntax)
-				.cloned()
-				.map(Into::into)
-				.as_ref(),
-			None,
-		);
-		for stmt in stmts {
-			p!(out, { stmt.value });
-		}
-		p!(out, { self.expr_base() });
-		let (suffixes, _ending) = children_between::<Suffix>(
-			self.syntax().clone(),
-			self.expr_base()
-				.as_ref()
-				.map(ExprBase::syntax)
-				.cloned()
-				.map(Into::into)
-				.as_ref(),
-			None,
-			None,
-		);
-		for suffix in suffixes {
-			p!(out, { suffix.value });
-		}
-	}
-}
-impl Printable for Suffix {
-	fn print(&self, out: &mut PrintItems) {
-		match self {
-			Self::SuffixIndex(i) => {
-				if i.question_mark_token().is_some() {
-					p!(out, str("?"));
-				}
-				p!(out, str(".") {i.index()});
-			}
-			Self::SuffixIndexExpr(e) => {
-				if e.question_mark_token().is_some() {
-					p!(out, str(".?"));
-				}
-				p!(out, str("[") {e.index()} str("]"));
-			}
-			Self::SuffixSlice(d) => {
-				p!(out, { d.slice_desc() });
-			}
-			Self::SuffixApply(a) => {
-				p!(out, { a.args_desc() });
-			}
-		}
-	}
-}
-impl Printable for Stmt {
-	fn print(&self, out: &mut PrintItems) {
-		match self {
-			Self::StmtLocal(l) => {
-				let (binds, end_comments) = children_between::<Bind>(
-					l.syntax().clone(),
-					l.local_kw_token().map(Into::into).as_ref(),
-					l.semi_token().map(Into::into).as_ref(),
-					None,
-				);
-				if binds.len() == 1 {
-					let bind = &binds[0];
-					format_comments(&bind.before_trivia, CommentLocation::AboveItem, out);
-					p!(out, str("local ") {bind.value});
-				// TODO: keep end_comments, child.inline_trivia somehow, force multiple locals formatting in case of presence?
-				} else {
-					p!(out,str("local") >i nl);
-					for bind in binds {
-						if bind.should_start_with_newline {
-							p!(out, nl);
-						}
-						format_comments(&bind.before_trivia, CommentLocation::AboveItem, out);
-						p!(out, {bind.value} str(","));
-						format_comments(&bind.inline_trivia, CommentLocation::ItemInline, out);
-						p!(out, nl);
-					}
-					if end_comments.should_start_with_newline {
-						p!(out, nl);
-					}
-					format_comments(&end_comments.trivia, CommentLocation::EndOfItems, out);
-					p!(out,<i);
-				}
-				p!(out,str(";") nl);
-			}
-			Self::StmtAssert(a) => {
-				p!(out, {a.assertion()} str(";") nl);
-			}
-		}
-	}
-}
-impl Printable for ExprBase {
-	fn print(&self, out: &mut PrintItems) {
-		match self {
-			Self::ExprBinary(b) => {
-				p!(out, {b.lhs_work()} str(" ") {b.binary_operator()} str(" ") {b.rhs_work()});
-			}
-			Self::ExprUnary(u) => p!(out, {u.unary_operator()} {u.rhs()}),
-			// Self::ExprSlice(s) => {
-			// 	p!(new: {s.expr()} {s.slice_desc()})
-			// }
-			// Self::ExprIndex(i) => {
-			// 	p!(new: {i.expr()} str(".") {i.index()})
-			// }
-			// Self::ExprIndexExpr(i) => p!(new: {i.base()} str("[") {i.index()} str("]")),
-			// Self::ExprApply(a) => {
-			// 	let mut pi = p!(new: {a.expr()} {a.args_desc()});
-			// 	if a.tailstrict_kw_token().is_some() {
-			// 		p!(out,str(" tailstrict"));
-			// 	}
-			// 	pi
-			// }
-			Self::ExprObjExtend(ex) => {
-				p!(out, {ex.lhs_work()} str(" ") {ex.rhs_work()});
-			}
-			Self::ExprParened(p) => {
-				p!(out, str("(") {p.expr()} str(")"));
-			}
-			Self::ExprString(s) => p!(out, { s.text() }),
-			Self::ExprNumber(n) => p!(out, { n.number() }),
-			Self::ExprArray(a) => {
-				p!(out, str("[") >i nl);
-				for el in a.exprs() {
-					p!(out, {el} str(",") nl);
-				}
-				p!(out, <i str("]"));
-			}
-			Self::ExprObject(obj) => {
-				p!(out, { obj.obj_body() });
-			}
-			Self::ExprArrayComp(arr) => {
-				p!(out, str("[") {arr.expr()});
-				for spec in arr.comp_specs() {
-					p!(out, str(" ") {spec});
-				}
-				p!(out, str("]"));
-			}
-			Self::ExprImport(v) => {
-				p!(out, {v.import_kind()} str(" ") {v.text()});
-			}
-			Self::ExprVar(n) => p!(out, { n.name() }),
-			// Self::ExprLocal(l) => {
-			// }
-			Self::ExprIfThenElse(ite) => {
-				p!(out, str("if ") {ite.cond()} str(" then ") {ite.then().map(|t| t.expr())});
-				if ite.else_kw_token().is_some() || ite.else_().is_some() {
-					p!(out, str(" else ") {ite.else_().map(|t| t.expr())});
-				}
-			}
-			Self::ExprFunction(f) => p!(out, str("function") {f.params_desc()} nl {f.expr()}),
-			// Self::ExprAssert(a) => p!(new: {a.assertion()} str("; ") {a.expr()}),
-			Self::ExprError(e) => p!(out, str("error ") {e.expr()}),
-			Self::ExprLiteral(l) => {
-				p!(out, { l.literal() });
-			}
-		}
-	}
-}
-
-impl Printable for SourceFile {
-	fn print(&self, out: &mut PrintItems) {
-		let before = trivia_before(
-			self.syntax().clone(),
-			self.expr()
-				.map(|e| e.syntax().clone())
-				.map(Into::into)
-				.as_ref(),
-		);
-		let after = trivia_after(
-			self.syntax().clone(),
-			self.expr()
-				.map(|e| e.syntax().clone())
-				.map(Into::into)
-				.as_ref(),
-		);
-		format_comments(&before, CommentLocation::AboveItem, out);
-		p!(out, {self.expr()} nl);
-		format_comments(&after, CommentLocation::EndOfItems, out);
-	}
-}
-
-struct FormatOptions {
-	// 0 for hard tabs
-	indent: u8,
-}
-fn format(input: &str, opts: &FormatOptions) -> Option<String> {
-	let (parsed, errors) = jrsonnet_rowan_parser::parse(input);
-	if !errors.is_empty() {
-		let mut builder = hi_doc::SnippetBuilder::new(input);
-		for error in errors {
-			builder
-				.error(hi_doc::Text::fragment(
-					format!("{:?}", error.error),
-					Formatting::default(),
-				))
-				.range(
-					error.range.start().into()
-						..=(usize::from(error.range.end()) - 1).max(error.range.start().into()),
-				)
-				.build();
-		}
-		let snippet = builder.build();
-		let ansi = hi_doc::source_to_ansi(&snippet);
-		eprintln!("{ansi}");
-		// It is possible to recover from this failure, but the output may be broken, as formatter is free to skip
-		// ERROR rowan nodes.
-		// Recovery needs to be enabled for LSP, though.
-		//
-		// TODO: Verify how formatter interacts in cases of missing positional values, i.e `if cond then /*missing Expr*/ else residual`.
-		return None;
-	}
-	Some(dprint_core::formatting::format(
-		|| {
-			let mut out = PrintItems::new();
-			parsed.print(&mut out);
-			out
-		},
-		PrintOptions {
-			indent_width: if opts.indent == 0 {
-				// Reasonable max length for both 2 and 4 space sized tabs.
-				3
-			} else {
-				opts.indent
-			},
-			max_width: 100,
-			use_tabs: opts.indent == 0,
-			new_line_text: "\n",
-		},
-	))
-}
+use jrsonnet_formatter::{format, FormatOptions};
 
 #[derive(Parser)]
 #[allow(clippy::struct_excessive_bools)]
@@ -768,9 +20,7 @@
 	#[arg(long)]
 	test: bool,
 	/// Number of spaces to indent with
-	///
-	/// 0 for guess from input (default), and use hard tabs if unable to guess.
-	#[arg(long, default_value = "0")]
+	#[arg(long, default_value = "2")]
 	indent: u8,
 	/// Force hard tab for indentation
 	#[arg(long)]
@@ -819,7 +69,7 @@
 	let mut convergence_tmp;
 	// https://github.com/dprint/dprint/pull/423
 	loop {
-		let Some(reformatted) = format(
+		let reformatted = match format(
 			&formatted,
 			&FormatOptions {
 				indent: if opts.indent == 0 || opts.hard_tabs {
@@ -828,8 +78,14 @@
 					opts.indent
 				},
 			},
-		) else {
-			return Err(Error::Parse);
+		) {
+			Ok(v) => v,
+			Err(e) => {
+				let snippet = e.build();
+				let ansi = hi_doc::source_to_ansi(&snippet);
+				eprintln!("{ansi}");
+				return Err(Error::Parse);
+			}
 		};
 		convergence_tmp = reformatted.trim().to_owned();
 		if formatted == convergence_tmp {
deletedcmds/jrsonnet-fmt/src/snapshots/jrsonnet_fmt__tests__complex_comments_snapshot.snapdiffbeforeafterboth
--- a/cmds/jrsonnet-fmt/src/snapshots/jrsonnet_fmt__tests__complex_comments_snapshot.snap
+++ /dev/null
@@ -1,53 +0,0 @@
----
-source: cmds/jrsonnet-fmt/src/tests.rs
-expression: "reformat(indoc!(\"{\n\t\t  comments: {\n\t\t\t_: '',\n\t\t\t//     Plain comment\n\t\t\ta: '',\n\n\t\t\t#    Plain comment with empty line before\n\t\t\tb: '',\n\t\t\t/*Single-line multiline comment\n\n\t\t\t*/\n\t\t\tc: '',\n\n\t\t\t/**Single-line multiline doc comment\n\n\t\t\t*/\n\t\t\tc: '',\n\n\t\t\t/**Multiline doc\n\t\t\tComment\n\t\t\t*/\n\t\t\tc: '',\n\n\t\t\t/*\n\n\tMulti-line\n\n\tcomment\n\t\t\t*/\n\t\t\td: '',\n\n\t\t\te: '', // Inline comment\n\n\t\t\tk: '',\n\n\t\t\t// Text after everything\n\t\t  },\n\t\t  comments2: {\n\t\t\tk: '',\n\t\t\t// Text after everything, but no newline above\n\t\t  },\n          spacing: {\n            a: '',\n\n            b: '',\n          },\n          noSpacing: {\n            a: '',\n            b: '',\n          },\n        }\"))"
----
-{
-	comments: {
-		_: '',
-		// Plain comment
-		a: '',
-
-		# Plain comment with empty line before
-		b: '',
-		/* Single-line multiline comment */
-		c: '',
-
-		/**
-		 * Single-line multiline doc comment
-		 */
-		c: '',
-
-		/**
-		 * Multiline doc
-		 * Comment
-		 */
-		c: '',
-
-		/*
-		Multi-line
-
-		comment
-		*/
-		d: '',
-
-		e: '', // Inline comment
-
-		k: '',
-
-		// Text after everything
-	},
-	comments2: {
-		k: '',
-		// Text after everything, but no newline above
-	},
-	spacing: {
-		a: '',
-
-		b: '',
-	},
-	noSpacing: {
-		a: '',
-		b: '',
-	},
-}
deletedcmds/jrsonnet-fmt/src/tests.rsdiffbeforeafterboth
--- a/cmds/jrsonnet-fmt/src/tests.rs
+++ /dev/null
@@ -1,79 +0,0 @@
-use dprint_core::formatting::{PrintItems, PrintOptions};
-use indoc::indoc;
-
-use crate::Printable;
-
-fn reformat(input: &str) -> String {
-	let (source, _) = jrsonnet_rowan_parser::parse(input);
-
-	dprint_core::formatting::format(
-		|| {
-			let mut out = PrintItems::new();
-			source.print(&mut out);
-			out
-		},
-		PrintOptions {
-			indent_width: 2,
-			max_width: 100,
-			use_tabs: true,
-			new_line_text: "\n",
-		},
-	)
-}
-
-#[test]
-fn complex_comments_snapshot() {
-	insta::assert_snapshot!(reformat(indoc!(
-		"{
-		  comments: {
-			_: '',
-			//     Plain comment
-			a: '',
-
-			#    Plain comment with empty line before
-			b: '',
-			/*Single-line multiline comment
-
-			*/
-			c: '',
-
-			/**Single-line multiline doc comment
-
-			*/
-			c: '',
-
-			/**Multiline doc
-			Comment
-			*/
-			c: '',
-
-			/*
-
-	Multi-line
-
-	comment
-			*/
-			d: '',
-
-			e: '', // Inline comment
-
-			k: '',
-
-			// Text after everything
-		  },
-		  comments2: {
-			k: '',
-			// Text after everything, but no newline above
-		  },
-          spacing: {
-            a: '',
-
-            b: '',
-          },
-          noSpacing: {
-            a: '',
-            b: '',
-          },
-        }"
-	)));
-}
addedcrates/jrsonnet-formatter/Cargo.tomldiffbeforeafterboth
--- /dev/null
+++ b/crates/jrsonnet-formatter/Cargo.toml
@@ -0,0 +1,15 @@
+[package]
+name = "jrsonnet-formatter"
+authors.workspace = true
+edition.workspace = true
+license.workspace = true
+repository.workspace = true
+version.workspace = true
+
+[dependencies]
+dprint-core.workspace = true
+hi-doc.workspace = true
+jrsonnet-rowan-parser.workspace = true
+
+[lints]
+workspace = true
addedcrates/jrsonnet-formatter/src/children.rsdiffbeforeafterboth
--- /dev/null
+++ b/crates/jrsonnet-formatter/src/children.rs
@@ -0,0 +1,251 @@
+// TODO: Return errors as trivia
+
+use std::{fmt::Debug, mem};
+
+use jrsonnet_rowan_parser::{
+	nodes::{CustomError, Trivia, TriviaKind},
+	AstNode, AstToken, SyntaxElement, SyntaxNode, TS,
+};
+
+pub type ChildTrivia = Vec<Result<Trivia, String>>;
+
+/// Node should have no non-trivia tokens before element
+pub fn trivia_before(node: SyntaxNode, end: Option<&SyntaxElement>) -> ChildTrivia {
+	let mut out = Vec::new();
+	for item in node.children_with_tokens() {
+		if Some(&item) == end {
+			break;
+		}
+
+		if let Some(trivia) = item.as_token().cloned().and_then(Trivia::cast) {
+			out.push(Ok(trivia));
+		} else if CustomError::can_cast(item.kind()) {
+			out.push(Err(item.to_string()));
+		} else if end.is_none() {
+			break;
+		} else {
+			assert!(
+				TS![, ;].contains(item.kind()),
+				"silently eaten token: {:?}",
+				item.kind()
+			);
+		}
+	}
+	out
+}
+/// Node should have no non-trivia tokens after element
+pub fn trivia_after(node: SyntaxNode, start: Option<&SyntaxElement>) -> ChildTrivia {
+	if start.is_none() {
+		return Vec::new();
+	}
+	let mut iter = node.children_with_tokens().peekable();
+	while iter.peek() != start {
+		iter.next();
+	}
+	iter.next();
+	let mut out = Vec::new();
+	for item in iter {
+		if let Some(trivia) = item.as_token().cloned().and_then(Trivia::cast) {
+			out.push(Ok(trivia));
+		} else if CustomError::can_cast(item.kind()) {
+			out.push(Err(item.to_string()));
+		} else {
+			assert!(
+				TS![, ;].contains(item.kind()),
+				"silently eaten token: {:?}",
+				item.kind()
+			);
+		}
+	}
+	out
+}
+
+pub fn children_between<T: AstNode + Debug>(
+	node: SyntaxNode,
+	start: Option<&SyntaxElement>,
+	end: Option<&SyntaxElement>,
+	trailing: Option<ChildTrivia>,
+) -> (Vec<Child<T>>, EndingComments) {
+	let mut iter = node.children_with_tokens().peekable();
+	if start.is_some() {
+		while iter.peek() != start {
+			iter.next();
+		}
+		iter.next();
+	}
+	children(
+		iter.take_while(|i| Some(i) != end),
+		start.is_none() && end.is_none(),
+		trailing,
+	)
+}
+
+pub fn should_start_with_newline(prev_inline: Option<&ChildTrivia>, tt: &ChildTrivia) -> bool {
+	count_newlines_before(tt)
+		+ prev_inline
+			.map(count_newlines_after)
+			.unwrap_or_default()
+
+		// First for previous item end, second for current item
+		>= 2
+}
+
+fn count_newlines_before(tt: &ChildTrivia) -> usize {
+	let mut nl_count = 0;
+	for t in tt {
+		match t {
+			Ok(t) => match t.kind() {
+				TriviaKind::Whitespace => {
+					nl_count += t.text().bytes().filter(|b| *b == b'\n').count();
+				}
+				_ => break,
+			},
+			Err(_) => {
+				nl_count += 1;
+			}
+		}
+	}
+	nl_count
+}
+fn count_newlines_after(tt: &ChildTrivia) -> usize {
+	let mut nl_count = 0;
+	for t in tt.iter().rev() {
+		match t {
+			Ok(t) => match t.kind() {
+				TriviaKind::Whitespace => {
+					nl_count += t.text().bytes().filter(|b| *b == b'\n').count();
+				}
+				TriviaKind::SingleLineHashComment | TriviaKind::SingleLineSlashComment => {
+					nl_count += 1;
+					break;
+				}
+				_ => {}
+			},
+			Err(_) => nl_count += 1,
+		}
+	}
+	nl_count
+}
+
+pub fn children<T: AstNode + Debug>(
+	items: impl Iterator<Item = SyntaxElement>,
+	loose: bool,
+	mut trailing: Option<ChildTrivia>,
+) -> (Vec<Child<T>>, EndingComments) {
+	let mut out = Vec::new();
+	let mut current_child = None::<Child<T>>;
+	let mut next = ChildTrivia::new();
+	// Previous element ended, do not add more inline comments
+	let mut started_next = false;
+	let mut had_some = false;
+
+	for item in items {
+		if let Some(value) = item.as_node().cloned().and_then(T::cast) {
+			let before_trivia = if let Some(trailing) = trailing.take() {
+				assert!(next.is_empty());
+				trailing
+			} else {
+				mem::take(&mut next)
+			};
+			let last_child = current_child.replace(Child {
+				// First item should not start with newline
+				should_start_with_newline: had_some
+					&& should_start_with_newline(
+						current_child.as_ref().map(|c| &c.inline_trivia),
+						&before_trivia,
+					),
+				before_trivia,
+				value,
+				inline_trivia: Vec::new(),
+			});
+			if let Some(last_child) = last_child {
+				out.push(last_child);
+			}
+			had_some = true;
+			started_next = false;
+		} else if let Some(trivia) = item.as_token().cloned().and_then(Trivia::cast) {
+			let is_single_line_comment = trivia.kind() == TriviaKind::SingleLineHashComment
+				|| trivia.kind() == TriviaKind::SingleLineSlashComment;
+			if trailing.is_some() {
+				// Someone have already parsed trivia for us
+				continue;
+			} else if started_next
+				|| current_child.is_none()
+				|| trivia.text().contains('\n') && !is_single_line_comment
+			{
+				next.push(Ok(trivia.clone()));
+				started_next = true;
+			} else {
+				let cur = current_child.as_mut().expect("checked not none");
+				cur.inline_trivia.push(Ok(trivia));
+				if is_single_line_comment {
+					started_next = true;
+				}
+			}
+			had_some = true;
+		} else if CustomError::can_cast(item.kind()) {
+			next.push(Err(item.to_string()));
+		} else if loose {
+			if had_some {
+				break;
+			}
+			started_next = true;
+		} else {
+			assert!(
+				TS![, ;].contains(item.kind()),
+				"silently eaten token: {:?}",
+				item.kind()
+			);
+		}
+	}
+
+	let ending_comments = EndingComments {
+		should_start_with_newline: should_start_with_newline(
+			current_child.as_ref().map(|c| &c.inline_trivia),
+			&next,
+		),
+		trivia: next,
+	};
+
+	if let Some(current_child) = current_child {
+		out.push(current_child);
+	}
+
+	(out, ending_comments)
+}
+
+#[derive(Debug)]
+pub struct Child<T> {
+	/// If this child has two newlines above in source code, so it needs to have it in the output
+	pub should_start_with_newline: bool,
+	/// Comment before item, i.e
+	///
+	/// ```ignore
+	/// // Comment
+	/// item
+	/// ```
+	pub before_trivia: ChildTrivia,
+	pub value: T,
+	/// Comment after line, but located at same line
+	///
+	/// ```ignore
+	/// item1, // Inline comment
+	/// // Not inline comment
+	/// item2,
+	/// ```
+	pub inline_trivia: ChildTrivia,
+}
+
+pub struct EndingComments {
+	/// If this child has two newlines above in source code, so it needs to have it in the output
+	pub should_start_with_newline: bool,
+	pub trivia: ChildTrivia,
+}
+impl EndingComments {
+	pub fn is_empty(&self) -> bool {
+		!self.should_start_with_newline && self.trivia.is_empty()
+	}
+	pub fn extract_trailing(&mut self) -> ChildTrivia {
+		mem::take(&mut self.trivia)
+	}
+}
addedcrates/jrsonnet-formatter/src/comments.rsdiffbeforeafterboth
--- /dev/null
+++ b/crates/jrsonnet-formatter/src/comments.rs
@@ -0,0 +1,181 @@
+use std::string::String;
+
+use dprint_core::formatting::PrintItems;
+use jrsonnet_rowan_parser::{nodes::TriviaKind, AstToken};
+
+use crate::{children::ChildTrivia, p, pi};
+
+pub enum CommentLocation {
+	/// Above local, field, other things
+	AboveItem,
+	/// After item
+	ItemInline,
+	/// After all items in object
+	EndOfItems,
+}
+
+#[allow(clippy::too_many_lines, clippy::cognitive_complexity)]
+pub fn format_comments(comments: &ChildTrivia, loc: CommentLocation, out: &mut PrintItems) {
+	for c in comments {
+		let Ok(c) = c else {
+			let mut text = c.as_ref().unwrap_err() as &str;
+			while !text.is_empty() {
+				let pos = text.find(['\n', '\t']).unwrap_or(text.len());
+				let sliced = &text[..pos];
+				p!(out, string(sliced.to_string()));
+				text = &text[pos..];
+				if !text.is_empty() {
+					match text.as_bytes()[0] {
+						b'\n' => p!(out, nl),
+						b'\t' => p!(out, tab),
+						_ => unreachable!(),
+					}
+					text = &text[1..];
+				}
+			}
+			continue;
+		};
+		match c.kind() {
+			TriviaKind::Whitespace => {}
+			TriviaKind::MultiLineComment => {
+				let mut text = c
+					.text()
+					.strip_prefix("/*")
+					.expect("ml comment starts with /*")
+					.strip_suffix("*/")
+					.expect("ml comment ends with */");
+				// doc-style comment, /**
+				let doc = if text.starts_with('*') {
+					text = &text[1..];
+					true
+				} else {
+					false
+				};
+				// Is comment starts with text immediatly, i.e /*text
+				let mut immediate_start = true;
+				let mut lines = text
+					.split('\n')
+					.map(|l| l.trim_end().to_string())
+					.skip_while(|l| {
+						if l.is_empty() {
+							immediate_start = false;
+							true
+						} else {
+							false
+						}
+					})
+					.collect::<Vec<_>>();
+				while lines.last().is_some_and(String::is_empty) {
+					lines.pop();
+				}
+				if lines.len() == 1 && !doc {
+					if matches!(loc, CommentLocation::ItemInline) {
+						p!(out, str(" "));
+					}
+					p!(out, str("/* ") string(lines[0].trim().to_string()) str(" */") nl);
+				} else if !lines.is_empty() {
+					fn common_ws_prefix<'a>(a: &'a str, b: &str) -> &'a str {
+						let offset = a
+							.bytes()
+							.zip(b.bytes())
+							.take_while(|(a, b)| a == b && (a.is_ascii_whitespace() || *a == b'*'))
+							.count();
+						&a[..offset]
+					}
+					// First line is not empty, extract ws prefix of it
+					let mut common_ws_padding = (if immediate_start && lines.len() > 1 {
+						common_ws_prefix(&lines[1], &lines[1])
+					} else {
+						common_ws_prefix(&lines[0], &lines[0])
+					})
+					.to_string();
+					for line in lines
+						.iter()
+						.skip(if immediate_start { 2 } else { 1 })
+						.filter(|l| !l.is_empty())
+					{
+						common_ws_padding = common_ws_prefix(&common_ws_padding, line).to_string();
+					}
+					for line in lines
+						.iter_mut()
+						.skip(usize::from(immediate_start))
+						.filter(|l| !l.is_empty())
+					{
+						*line = line
+							.strip_prefix(&common_ws_padding)
+							.expect("all non-empty lines start with this padding")
+							.to_string();
+					}
+
+					p!(out, str("/*"));
+					if doc {
+						p!(out, str("*"));
+					}
+					p!(out, nl);
+					for mut line in lines {
+						if doc {
+							p!(out, str(" *"));
+						}
+						if line.is_empty() {
+							p!(out, nl);
+						} else {
+							if doc {
+								p!(out, str(" "));
+							}
+							while let Some(new_line) = line.strip_prefix('\t') {
+								if doc {
+									p!(out, str("    "));
+								} else {
+									p!(out, tab);
+								}
+								line = new_line.to_string();
+							}
+							p!(out, string(line.to_string()) nl);
+						}
+					}
+					if doc {
+						p!(out, str(" "));
+					}
+					p!(out, str("*/") nl);
+				}
+			}
+			// TODO: Keep common padding for multiple continous lines of single-line comments
+			// I.e
+			// ```
+			// #  Line1
+			// #    Line2
+			// ```
+			// Should be reformatted as
+			// ```
+			// # Line1
+			// #   Line2
+			// ```
+			// But currently comment formatter is not aware of continous comment lines, and reformats it as
+			// ```
+			// # Line1
+			// # Line2
+			// ```
+			TriviaKind::SingleLineHashComment => {
+				if matches!(loc, CommentLocation::ItemInline) {
+					p!(out, str(" "));
+				}
+				p!(out, str("# ") string(c.text().strip_prefix('#').expect("hash comment starts with #").trim().to_string()));
+				if !matches!(loc, CommentLocation::ItemInline) {
+					p!(out, nl);
+				}
+			}
+			TriviaKind::SingleLineSlashComment => {
+				if matches!(loc, CommentLocation::ItemInline) {
+					p!(out, str(" "));
+				}
+				p!(out, str("// ") string(c.text().strip_prefix("//").expect("comment starts with //").trim().to_string()));
+				if !matches!(loc, CommentLocation::ItemInline) {
+					p!(out, nl);
+				}
+			}
+			// Garbage in - garbage out
+			TriviaKind::ErrorCommentTooShort => p!(out, str("/*/")),
+			TriviaKind::ErrorCommentUnterminated => p!(out, string(c.text().to_string())),
+		}
+	}
+}
addedcrates/jrsonnet-formatter/src/lib.rsdiffbeforeafterboth
--- /dev/null
+++ b/crates/jrsonnet-formatter/src/lib.rs
@@ -0,0 +1,740 @@
+use std::{any::type_name, rc::Rc};
+
+use children::{children_between, trivia_before};
+use dprint_core::formatting::{
+	condition_helpers::is_multiple_lines, condition_resolvers::true_resolver,
+	ConditionResolverContext, LineNumber, PrintItems, PrintOptions,
+};
+use hi_doc::{Formatting, SnippetBuilder};
+use jrsonnet_rowan_parser::{
+	nodes::{
+		Arg, ArgsDesc, Assertion, BinaryOperator, Bind, CompSpec, Destruct, DestructArrayPart,
+		DestructRest, Expr, ExprBase, FieldName, ForSpec, IfSpec, ImportKind, Literal, Member,
+		Name, Number, ObjBody, ObjLocal, ParamsDesc, SliceDesc, SourceFile, Stmt, Suffix, Text,
+		UnaryOperator, Visibility,
+	},
+	AstNode, AstToken as _, SyntaxToken,
+};
+
+use crate::{
+	children::trivia_after,
+	comments::{format_comments, CommentLocation},
+};
+
+mod children;
+mod comments;
+#[cfg(test)]
+mod tests;
+
+pub trait Printable {
+	fn print(&self, out: &mut PrintItems);
+}
+
+macro_rules! pi {
+	(@i; $($t:tt)*) => {{
+		#[allow(unused_mut)]
+		let mut o = dprint_core::formatting::PrintItems::new();
+		pi!(@s; o: $($t)*);
+		o
+	}};
+	(@s; $o:ident: str($e:expr $(,)?) $($t:tt)*) => {{
+		$o.push_string($e.to_owned());
+		pi!(@s; $o: $($t)*);
+	}};
+	(@s; $o:ident: string($e:expr $(,)?) $($t:tt)*) => {{
+		$o.push_string($e);
+		pi!(@s; $o: $($t)*);
+	}};
+	(@s; $o:ident: nl $($t:tt)*) => {{
+		$o.push_signal(dprint_core::formatting::Signal::NewLine);
+		pi!(@s; $o: $($t)*);
+	}};
+	(@s; $o:ident: tab $($t:tt)*) => {{
+		$o.push_signal(dprint_core::formatting::Signal::Tab);
+		pi!(@s; $o: $($t)*);
+	}};
+	(@s; $o:ident: >i $($t:tt)*) => {{
+		$o.push_signal(dprint_core::formatting::Signal::StartIndent);
+		pi!(@s; $o: $($t)*);
+	}};
+	(@s; $o:ident: <i $($t:tt)*) => {{
+		$o.push_signal(dprint_core::formatting::Signal::FinishIndent);
+		pi!(@s; $o: $($t)*);
+	}};
+	(@s; $o:ident: info($v:expr) $($t:tt)*) => {{
+		$o.push_info($v);
+		pi!(@s; $o: $($t)*);
+	}};
+	(@s; $o:ident: if($s:literal, $cond:expr, $($i:tt)*) $($t:tt)*) => {{
+		$o.push_condition(dprint_core::formatting::conditions::if_true(
+			$s,
+			$cond.clone(),
+			{
+				let mut o = PrintItems::new();
+				p!(o, $($i)*);
+				o
+			},
+		));
+		pi!(@s; $o: $($t)*);
+	}};
+	(@s; $o:ident: if_else($s:literal, $cond:expr, $($i:tt)*)($($e:tt)+) $($t:tt)*) => {{
+		$o.push_condition(dprint_core::formatting::conditions::if_true_or(
+			$s,
+			$cond.clone(),
+			{
+				let mut o = PrintItems::new();
+				p!(o, $($i)*);
+				o
+			},
+			{
+				let mut o = PrintItems::new();
+				p!(o, $($e)*);
+				o
+			},
+		));
+		pi!(@s; $o: $($t)*);
+	}};
+	(@s; $o:ident: if_not($s:literal, $cond:expr, $($e:tt)*) $($t:tt)*) => {{
+		$o.push_condition(dprint_core::formatting::conditions::if_true_or(
+			$s,
+			$cond.clone(),
+			{
+				let o = PrintItems::new();
+				o
+			},
+			{
+				let mut o = PrintItems::new();
+				p!(o, $($e)*);
+				o
+			},
+		));
+		pi!(@s; $o: $($t)*);
+	}};
+	(@s; $o:ident: {$expr:expr} $($t:tt)*) => {{
+		$expr.print($o);
+		pi!(@s; $o: $($t)*);
+	}};
+	(@s; $o:ident: items($expr:expr) $($t:tt)*) => {{
+		$o.extend($expr);
+		pi!(@s; $o: $($t)*);
+	}};
+	(@s; $o:ident: if ($e:expr)($($then:tt)*) $($t:tt)*) => {{
+		if $e {
+			pi!(@s; $o: $($then)*);
+		}
+		pi!(@s; $o: $($t)*);
+	}};
+	(@s; $o:ident: ifelse ($e:expr)($($then:tt)*)($($else:tt)*) $($t:tt)*) => {{
+		if $e {
+			pi!(@s; $o: $($then)*);
+		} else {
+			pi!(@s; $o: $($else)*);
+		}
+		pi!(@s; $o: $($t)*);
+	}};
+	(@s; $i:ident:) => {}
+}
+macro_rules! p {
+	($o:ident, $($t:tt)*) => {
+		pi!(@s; $o: $($t)*)
+	};
+}
+pub(crate) use p;
+pub(crate) use pi;
+
+impl<P> Printable for Option<P>
+where
+	P: Printable,
+{
+	fn print(&self, out: &mut PrintItems) {
+		if let Some(v) = self {
+			v.print(out);
+		} else {
+			p!(
+				out,
+				string(format!(
+					"/*missing {}*/",
+					type_name::<P>().replace("jrsonnet_rowan_parser::generated::nodes::", "")
+				),)
+			);
+		}
+	}
+}
+
+impl Printable for SyntaxToken {
+	fn print(&self, out: &mut PrintItems) {
+		p!(out, string(self.to_string()));
+	}
+}
+
+impl Printable for Text {
+	fn print(&self, out: &mut PrintItems) {
+		p!(out, string(format!("{}", self)));
+	}
+}
+impl Printable for Number {
+	fn print(&self, out: &mut PrintItems) {
+		p!(out, string(format!("{}", self)));
+	}
+}
+
+impl Printable for Name {
+	fn print(&self, out: &mut PrintItems) {
+		p!(out, { self.ident_lit() });
+	}
+}
+
+impl Printable for DestructRest {
+	fn print(&self, out: &mut PrintItems) {
+		p!(out, str("..."));
+		if let Some(name) = self.into() {
+			p!(out, { name });
+		}
+	}
+}
+
+impl Printable for Destruct {
+	fn print(&self, out: &mut PrintItems) {
+		match self {
+			Self::DestructFull(f) => {
+				p!(out, { f.name() });
+			}
+			Self::DestructSkip(_) => p!(out, str("?")),
+			Self::DestructArray(a) => {
+				p!(out, str("[") >i nl);
+				for el in a.destruct_array_parts() {
+					match el {
+						DestructArrayPart::DestructArrayElement(e) => {
+							p!(out, {e.destruct()} str(",") nl);
+						}
+						DestructArrayPart::DestructRest(d) => {
+							p!(out, {d} str(",") nl);
+						}
+					}
+				}
+				p!(out, <i str("]"));
+			}
+			Self::DestructObject(o) => {
+				p!(out, str("{") >i nl);
+				for item in o.destruct_object_fields() {
+					p!(out, { item.field() });
+					if let Some(des) = item.destruct() {
+						p!(out, str(": ") {des});
+					}
+					if let Some(def) = item.expr() {
+						p!(out, str(" = ") {def});
+					}
+					p!(out, str(",") nl);
+				}
+				if let Some(rest) = o.destruct_rest() {
+					p!(out, {rest} nl);
+				}
+				p!(out, <i str("}"));
+			}
+		}
+	}
+}
+
+impl Printable for FieldName {
+	fn print(&self, out: &mut PrintItems) {
+		match self {
+			Self::FieldNameFixed(f) => {
+				if let Some(id) = f.id() {
+					p!(out, { id });
+				} else if let Some(str) = f.text() {
+					p!(out, { str });
+				} else {
+					p!(out, str("/*missing FieldName*/"));
+				}
+			}
+			Self::FieldNameDynamic(d) => {
+				p!(out, str("[") {d.expr()} str("]"));
+			}
+		}
+	}
+}
+
+impl Printable for Visibility {
+	fn print(&self, out: &mut PrintItems) {
+		p!(out, string(self.to_string()));
+	}
+}
+
+impl Printable for ObjLocal {
+	fn print(&self, out: &mut PrintItems) {
+		p!(out, str("local ") {self.bind()});
+	}
+}
+
+impl Printable for Assertion {
+	fn print(&self, out: &mut PrintItems) {
+		p!(out, str("assert ") {self.condition()});
+		if self.colon_token().is_some() || self.message().is_some() {
+			p!(out, str(": ") {self.message()});
+		}
+	}
+}
+
+impl Printable for ParamsDesc {
+	fn print(&self, out: &mut PrintItems) {
+		p!(out, str("(") >i nl);
+		for param in self.params() {
+			p!(out, { param.destruct() });
+			if param.assign_token().is_some() || param.expr().is_some() {
+				p!(out, str(" = ") {param.expr()});
+			}
+			p!(out, str(",") nl);
+		}
+		p!(out, <i str(")"));
+	}
+}
+impl Printable for ArgsDesc {
+	fn print(&self, out: &mut PrintItems) {
+		let start = LineNumber::new("start");
+		let end = LineNumber::new("end");
+		let multi_line = Rc::new(move |condition_context: &mut ConditionResolverContext| {
+			is_multiple_lines(condition_context, start, end).map(|v| !v)
+		});
+		p!(out, str("(") info(start) if("start args", multi_line, >i nl));
+		let (children, end_comments) = children_between::<Arg>(
+			self.syntax().clone(),
+			self.l_paren_token().map(Into::into).as_ref(),
+			self.r_paren_token().map(Into::into).as_ref(),
+			None,
+		);
+		let mut args = children.into_iter().peekable();
+		while let Some(ele) = args.next() {
+			if ele.should_start_with_newline {
+				p!(out, nl);
+			}
+			format_comments(&ele.before_trivia, CommentLocation::AboveItem, out);
+			let arg = ele.value;
+			if arg.name().is_some() || arg.assign_token().is_some() {
+				p!(out, {arg.name()} str(" = "));
+			}
+			let comma_between = if args.peek().is_some() {
+				true_resolver()
+			} else {
+				multi_line.clone()
+			};
+			p!(out, {arg.expr()} if("arg comma", comma_between, str(",") if_not("between args", multi_line, str(" "))));
+			format_comments(&ele.inline_trivia, CommentLocation::ItemInline, out);
+			p!(out, if("between args", multi_line, nl));
+		}
+		if end_comments.should_start_with_newline {
+			p!(out, nl);
+		}
+		format_comments(&end_comments.trivia, CommentLocation::EndOfItems, out);
+		p!(out, if("end args", multi_line, <i info(end)) str(")"));
+	}
+}
+impl Printable for SliceDesc {
+	fn print(&self, out: &mut PrintItems) {
+		p!(out, str("["));
+		if self.from().is_some() {
+			p!(out, { self.from() });
+		}
+		p!(out, str(":"));
+		if self.end().is_some() {
+			p!(out, { self.end().map(|e| e.expr()) });
+		}
+		// Keep only one : in case if we don't need step
+		if self.step().is_some() {
+			p!(out, str(":") {self.step().map(|e|e.expr())});
+		}
+		p!(out, str("]"));
+	}
+}
+
+impl Printable for Member {
+	fn print(&self, out: &mut PrintItems) {
+		match self {
+			Self::MemberBindStmt(b) => {
+				p!(out, { b.obj_local() });
+			}
+			Self::MemberAssertStmt(ass) => {
+				p!(out, { ass.assertion() });
+			}
+			Self::MemberFieldNormal(n) => {
+				p!(out, {n.field_name()} if(n.plus_token().is_some())({n.plus_token()}) {n.visibility()} str(" ") {n.expr()});
+			}
+			Self::MemberFieldMethod(m) => {
+				p!(out, {m.field_name()} {m.params_desc()} {m.visibility()} str(" ") {m.expr()});
+			}
+		}
+	}
+}
+
+impl Printable for ObjBody {
+	fn print(&self, out: &mut PrintItems) {
+		match self {
+			Self::ObjBodyComp(l) => {
+				let (children, mut end_comments) = children_between::<Member>(
+					l.syntax().clone(),
+					l.l_brace_token().map(Into::into).as_ref(),
+					Some(
+						&(l.comp_specs()
+							.next()
+							.expect("at least one spec is defined")
+							.syntax()
+							.clone())
+						.into(),
+					),
+					None,
+				);
+				let trailing_for_comp = end_comments.extract_trailing();
+				p!(out, str("{") >i nl);
+				for mem in children {
+					if mem.should_start_with_newline {
+						p!(out, nl);
+					}
+					format_comments(&mem.before_trivia, CommentLocation::AboveItem, out);
+					p!(out, {mem.value} str(","));
+					format_comments(&mem.inline_trivia, CommentLocation::ItemInline, out);
+					p!(out, nl);
+				}
+
+				if end_comments.should_start_with_newline {
+					p!(out, nl);
+				}
+				format_comments(&end_comments.trivia, CommentLocation::EndOfItems, out);
+
+				let (compspecs, end_comments) = children_between::<CompSpec>(
+					l.syntax().clone(),
+					l.member_comps()
+						.last()
+						.map(|m| m.syntax().clone())
+						.map(Into::into)
+						.or_else(|| l.l_brace_token().map(Into::into))
+						.as_ref(),
+					l.r_brace_token().map(Into::into).as_ref(),
+					Some(trailing_for_comp),
+				);
+				for mem in compspecs {
+					if mem.should_start_with_newline {
+						p!(out, nl);
+					}
+					format_comments(&mem.before_trivia, CommentLocation::AboveItem, out);
+					p!(out, { mem.value });
+					format_comments(&mem.inline_trivia, CommentLocation::ItemInline, out);
+				}
+				if end_comments.should_start_with_newline {
+					p!(out, nl);
+				}
+				format_comments(&end_comments.trivia, CommentLocation::EndOfItems, out);
+
+				p!(out, nl <i str("}"));
+			}
+			Self::ObjBodyMemberList(l) => {
+				let (children, end_comments) = children_between::<Member>(
+					l.syntax().clone(),
+					l.l_brace_token().map(Into::into).as_ref(),
+					l.r_brace_token().map(Into::into).as_ref(),
+					None,
+				);
+				if children.is_empty() && end_comments.is_empty() {
+					p!(out, str("{ }"));
+					return;
+				}
+				p!(out, str("{") >i nl);
+				for (i, mem) in children.into_iter().enumerate() {
+					if mem.should_start_with_newline && i != 0 {
+						p!(out, nl);
+					}
+					format_comments(&mem.before_trivia, CommentLocation::AboveItem, out);
+					p!(out, {mem.value} str(","));
+					format_comments(&mem.inline_trivia, CommentLocation::ItemInline, out);
+					p!(out, nl);
+				}
+
+				if end_comments.should_start_with_newline {
+					p!(out, nl);
+				}
+				format_comments(&end_comments.trivia, CommentLocation::EndOfItems, out);
+				p!(out, <i str("}"));
+			}
+		}
+	}
+}
+impl Printable for UnaryOperator {
+	fn print(&self, out: &mut PrintItems) {
+		p!(out, string(self.text().to_string()));
+	}
+}
+impl Printable for BinaryOperator {
+	fn print(&self, out: &mut PrintItems) {
+		p!(out, string(self.text().to_string()));
+	}
+}
+impl Printable for Bind {
+	fn print(&self, out: &mut PrintItems) {
+		match self {
+			Self::BindDestruct(d) => {
+				p!(out, {d.into()} str(" = ") {d.value()});
+			}
+			Self::BindFunction(f) => {
+				p!(out, {f.name()} {f.params()} str(" = ") {f.value()});
+			}
+		}
+	}
+}
+impl Printable for Literal {
+	fn print(&self, out: &mut PrintItems) {
+		p!(out, string(self.syntax().to_string()));
+	}
+}
+impl Printable for ImportKind {
+	fn print(&self, out: &mut PrintItems) {
+		p!(out, string(self.syntax().to_string()));
+	}
+}
+impl Printable for ForSpec {
+	fn print(&self, out: &mut PrintItems) {
+		p!(out, str("for ") {self.bind()} str(" in ") {self.expr()});
+	}
+}
+impl Printable for IfSpec {
+	fn print(&self, out: &mut PrintItems) {
+		p!(out, str("if ") {self.expr()});
+	}
+}
+impl Printable for CompSpec {
+	fn print(&self, out: &mut PrintItems) {
+		match self {
+			Self::ForSpec(f) => f.print(out),
+			Self::IfSpec(i) => i.print(out),
+		}
+	}
+}
+impl Printable for Expr {
+	fn print(&self, out: &mut PrintItems) {
+		let (stmts, _ending) = children_between::<Stmt>(
+			self.syntax().clone(),
+			None,
+			self.expr_base()
+				.as_ref()
+				.map(ExprBase::syntax)
+				.cloned()
+				.map(Into::into)
+				.as_ref(),
+			None,
+		);
+		for stmt in stmts {
+			p!(out, { stmt.value });
+		}
+		p!(out, { self.expr_base() });
+		let (suffixes, _ending) = children_between::<Suffix>(
+			self.syntax().clone(),
+			self.expr_base()
+				.as_ref()
+				.map(ExprBase::syntax)
+				.cloned()
+				.map(Into::into)
+				.as_ref(),
+			None,
+			None,
+		);
+		for suffix in suffixes {
+			p!(out, { suffix.value });
+		}
+	}
+}
+impl Printable for Suffix {
+	fn print(&self, out: &mut PrintItems) {
+		match self {
+			Self::SuffixIndex(i) => {
+				if i.question_mark_token().is_some() {
+					p!(out, str("?"));
+				}
+				p!(out, str(".") {i.index()});
+			}
+			Self::SuffixIndexExpr(e) => {
+				if e.question_mark_token().is_some() {
+					p!(out, str(".?"));
+				}
+				p!(out, str("[") {e.index()} str("]"));
+			}
+			Self::SuffixSlice(d) => {
+				p!(out, { d.slice_desc() });
+			}
+			Self::SuffixApply(a) => {
+				p!(out, { a.args_desc() });
+			}
+		}
+	}
+}
+impl Printable for Stmt {
+	fn print(&self, out: &mut PrintItems) {
+		match self {
+			Self::StmtLocal(l) => {
+				let (binds, end_comments) = children_between::<Bind>(
+					l.syntax().clone(),
+					l.local_kw_token().map(Into::into).as_ref(),
+					l.semi_token().map(Into::into).as_ref(),
+					None,
+				);
+				if binds.len() == 1 {
+					let bind = &binds[0];
+					format_comments(&bind.before_trivia, CommentLocation::AboveItem, out);
+					p!(out, str("local ") {bind.value});
+				// TODO: keep end_comments, child.inline_trivia somehow, force multiple locals formatting in case of presence?
+				} else {
+					p!(out,str("local") >i nl);
+					for bind in binds {
+						if bind.should_start_with_newline {
+							p!(out, nl);
+						}
+						format_comments(&bind.before_trivia, CommentLocation::AboveItem, out);
+						p!(out, {bind.value} str(","));
+						format_comments(&bind.inline_trivia, CommentLocation::ItemInline, out);
+						p!(out, nl);
+					}
+					if end_comments.should_start_with_newline {
+						p!(out, nl);
+					}
+					format_comments(&end_comments.trivia, CommentLocation::EndOfItems, out);
+					p!(out,<i);
+				}
+				p!(out,str(";") nl);
+			}
+			Self::StmtAssert(a) => {
+				p!(out, {a.assertion()} str(";") nl);
+			}
+		}
+	}
+}
+impl Printable for ExprBase {
+	fn print(&self, out: &mut PrintItems) {
+		match self {
+			Self::ExprBinary(b) => {
+				p!(out, {b.lhs_work()} str(" ") {b.binary_operator()} str(" ") {b.rhs_work()});
+			}
+			Self::ExprUnary(u) => p!(out, {u.unary_operator()} {u.rhs()}),
+			// Self::ExprSlice(s) => {
+			// 	p!(new: {s.expr()} {s.slice_desc()})
+			// }
+			// Self::ExprIndex(i) => {
+			// 	p!(new: {i.expr()} str(".") {i.index()})
+			// }
+			// Self::ExprIndexExpr(i) => p!(new: {i.base()} str("[") {i.index()} str("]")),
+			// Self::ExprApply(a) => {
+			// 	let mut pi = p!(new: {a.expr()} {a.args_desc()});
+			// 	if a.tailstrict_kw_token().is_some() {
+			// 		p!(out,str(" tailstrict"));
+			// 	}
+			// 	pi
+			// }
+			Self::ExprObjExtend(ex) => {
+				p!(out, {ex.lhs_work()} str(" ") {ex.rhs_work()});
+			}
+			Self::ExprParened(p) => {
+				p!(out, str("(") {p.expr()} str(")"));
+			}
+			Self::ExprString(s) => p!(out, { s.text() }),
+			Self::ExprNumber(n) => p!(out, { n.number() }),
+			Self::ExprArray(a) => {
+				p!(out, str("[") >i nl);
+				for el in a.exprs() {
+					p!(out, {el} str(",") nl);
+				}
+				p!(out, <i str("]"));
+			}
+			Self::ExprObject(obj) => {
+				p!(out, { obj.obj_body() });
+			}
+			Self::ExprArrayComp(arr) => {
+				p!(out, str("[") {arr.expr()});
+				for spec in arr.comp_specs() {
+					p!(out, str(" ") {spec});
+				}
+				p!(out, str("]"));
+			}
+			Self::ExprImport(v) => {
+				p!(out, {v.import_kind()} str(" ") {v.text()});
+			}
+			Self::ExprVar(n) => p!(out, { n.name() }),
+			// Self::ExprLocal(l) => {
+			// }
+			Self::ExprIfThenElse(ite) => {
+				p!(out, str("if ") {ite.cond()} str(" then ") {ite.then().map(|t| t.expr())});
+				if ite.else_kw_token().is_some() || ite.else_().is_some() {
+					p!(out, str(" else ") {ite.else_().map(|t| t.expr())});
+				}
+			}
+			Self::ExprFunction(f) => p!(out, str("function") {f.params_desc()} nl {f.expr()}),
+			// Self::ExprAssert(a) => p!(new: {a.assertion()} str("; ") {a.expr()}),
+			Self::ExprError(e) => p!(out, str("error ") {e.expr()}),
+			Self::ExprLiteral(l) => {
+				p!(out, { l.literal() });
+			}
+		}
+	}
+}
+
+impl Printable for SourceFile {
+	fn print(&self, out: &mut PrintItems) {
+		let before = trivia_before(
+			self.syntax().clone(),
+			self.expr()
+				.map(|e| e.syntax().clone())
+				.map(Into::into)
+				.as_ref(),
+		);
+		let after = trivia_after(
+			self.syntax().clone(),
+			self.expr()
+				.map(|e| e.syntax().clone())
+				.map(Into::into)
+				.as_ref(),
+		);
+		format_comments(&before, CommentLocation::AboveItem, out);
+		p!(out, {self.expr()} nl);
+		format_comments(&after, CommentLocation::EndOfItems, out);
+	}
+}
+
+pub struct FormatOptions {
+	// 0 for hard tabs
+	pub indent: u8,
+}
+pub fn format(input: &str, opts: &FormatOptions) -> Result<String, SnippetBuilder> {
+	let (parsed, errors) = jrsonnet_rowan_parser::parse(input);
+	if !errors.is_empty() {
+		let mut builder = hi_doc::SnippetBuilder::new(input);
+		for error in errors {
+			builder
+				.error(hi_doc::Text::fragment(
+					format!("{:?}", error.error),
+					Formatting::default(),
+				))
+				.range(
+					error.range.start().into()
+						..=(usize::from(error.range.end()) - 1).max(error.range.start().into()),
+				)
+				.build();
+		}
+		// let snippet = builder.build();
+		return Err(builder);
+		// It is possible to recover from this failure, but the output may be broken, as formatter is free to skip
+		// ERROR rowan nodes.
+		// Recovery needs to be enabled for LSP, though.
+	}
+	Ok(dprint_core::formatting::format(
+		|| {
+			let mut out = PrintItems::new();
+			parsed.print(&mut out);
+			out
+		},
+		PrintOptions {
+			indent_width: if opts.indent == 0 {
+				// Reasonable max length for both 2 and 4 space sized tabs.
+				3
+			} else {
+				opts.indent
+			},
+			max_width: 100,
+			use_tabs: opts.indent == 0,
+			new_line_text: "\n",
+		},
+	))
+}
addedcrates/jrsonnet-formatter/src/snapshots/jrsonnet_fmt__tests__complex_comments_snapshot.snapdiffbeforeafterboth
--- /dev/null
+++ b/crates/jrsonnet-formatter/src/snapshots/jrsonnet_fmt__tests__complex_comments_snapshot.snap
@@ -0,0 +1,53 @@
+---
+source: cmds/jrsonnet-fmt/src/tests.rs
+expression: "reformat(indoc!(\"{\n\t\t  comments: {\n\t\t\t_: '',\n\t\t\t//     Plain comment\n\t\t\ta: '',\n\n\t\t\t#    Plain comment with empty line before\n\t\t\tb: '',\n\t\t\t/*Single-line multiline comment\n\n\t\t\t*/\n\t\t\tc: '',\n\n\t\t\t/**Single-line multiline doc comment\n\n\t\t\t*/\n\t\t\tc: '',\n\n\t\t\t/**Multiline doc\n\t\t\tComment\n\t\t\t*/\n\t\t\tc: '',\n\n\t\t\t/*\n\n\tMulti-line\n\n\tcomment\n\t\t\t*/\n\t\t\td: '',\n\n\t\t\te: '', // Inline comment\n\n\t\t\tk: '',\n\n\t\t\t// Text after everything\n\t\t  },\n\t\t  comments2: {\n\t\t\tk: '',\n\t\t\t// Text after everything, but no newline above\n\t\t  },\n          spacing: {\n            a: '',\n\n            b: '',\n          },\n          noSpacing: {\n            a: '',\n            b: '',\n          },\n        }\"))"
+---
+{
+	comments: {
+		_: '',
+		// Plain comment
+		a: '',
+
+		# Plain comment with empty line before
+		b: '',
+		/* Single-line multiline comment */
+		c: '',
+
+		/**
+		 * Single-line multiline doc comment
+		 */
+		c: '',
+
+		/**
+		 * Multiline doc
+		 * Comment
+		 */
+		c: '',
+
+		/*
+		Multi-line
+
+		comment
+		*/
+		d: '',
+
+		e: '', // Inline comment
+
+		k: '',
+
+		// Text after everything
+	},
+	comments2: {
+		k: '',
+		// Text after everything, but no newline above
+	},
+	spacing: {
+		a: '',
+
+		b: '',
+	},
+	noSpacing: {
+		a: '',
+		b: '',
+	},
+}
addedcrates/jrsonnet-formatter/src/tests.rsdiffbeforeafterboth
--- /dev/null
+++ b/crates/jrsonnet-formatter/src/tests.rs
@@ -0,0 +1,79 @@
+use dprint_core::formatting::{PrintItems, PrintOptions};
+use indoc::indoc;
+
+use crate::Printable;
+
+fn reformat(input: &str) -> String {
+	let (source, _) = jrsonnet_rowan_parser::parse(input);
+
+	dprint_core::formatting::format(
+		|| {
+			let mut out = PrintItems::new();
+			source.print(&mut out);
+			out
+		},
+		PrintOptions {
+			indent_width: 2,
+			max_width: 100,
+			use_tabs: true,
+			new_line_text: "\n",
+		},
+	)
+}
+
+#[test]
+fn complex_comments_snapshot() {
+	insta::assert_snapshot!(reformat(indoc!(
+		"{
+		  comments: {
+			_: '',
+			//     Plain comment
+			a: '',
+
+			#    Plain comment with empty line before
+			b: '',
+			/*Single-line multiline comment
+
+			*/
+			c: '',
+
+			/**Single-line multiline doc comment
+
+			*/
+			c: '',
+
+			/**Multiline doc
+			Comment
+			*/
+			c: '',
+
+			/*
+
+	Multi-line
+
+	comment
+			*/
+			d: '',
+
+			e: '', // Inline comment
+
+			k: '',
+
+			// Text after everything
+		  },
+		  comments2: {
+			k: '',
+			// Text after everything, but no newline above
+		  },
+          spacing: {
+            a: '',
+
+            b: '',
+          },
+          noSpacing: {
+            a: '',
+            b: '',
+          },
+        }"
+	)));
+}