git.delta.rocks / jrsonnet / refs/commits / 8bc6498a805d

difftreelog

test import formatting test-cases from rustanka

umopspmtYaroslav Bolyukin2026-02-12parent: #fc45c6c.patch.diff
in: master

45 files changed

modifiedCargo.lockdiffbeforeafterboth
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -156,6 +156,16 @@
 ]
 
 [[package]]
+name = "bstr"
+version = "1.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab"
+dependencies = [
+ "memchr",
+ "serde",
+]
+
+[[package]]
 name = "bumpalo"
 version = "3.19.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -453,6 +463,19 @@
 ]
 
 [[package]]
+name = "globset"
+version = "0.4.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "52dfc19153a48bde0cbd630453615c8151bce3a5adfac7a0aebfbf0a1e1f57e3"
+dependencies = [
+ "aho-corasick",
+ "bstr",
+ "log",
+ "regex-automata",
+ "regex-syntax",
+]
+
+[[package]]
 name = "hashbrown"
 version = "0.14.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -548,9 +571,11 @@
 checksum = "e82db8c87c7f1ccecb34ce0c24399b8a73081427f3c7c50a5d597925356115e4"
 dependencies = [
  "console",
+ "globset",
  "once_cell",
  "similar",
  "tempfile",
+ "walkdir",
 ]
 
 [[package]]
@@ -797,6 +822,12 @@
 checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039"
 
 [[package]]
+name = "log"
+version = "0.4.29"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
+
+[[package]]
 name = "logos"
 version = "0.16.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1157,6 +1188,15 @@
 checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
 
 [[package]]
+name = "same-file"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
+dependencies = [
+ "winapi-util",
+]
+
+[[package]]
 name = "saphyr-parser-bw"
 version = "0.0.607"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1491,6 +1531,16 @@
 ]
 
 [[package]]
+name = "walkdir"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
+dependencies = [
+ "same-file",
+ "winapi-util",
+]
+
+[[package]]
 name = "wasip2"
 version = "1.0.2+wasi-0.2.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1545,6 +1595,15 @@
 ]
 
 [[package]]
+name = "winapi-util"
+version = "0.1.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
+dependencies = [
+ "windows-sys 0.59.0",
+]
+
+[[package]]
 name = "windows-link"
 version = "0.2.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
modifiedCargo.tomldiffbeforeafterboth
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -38,7 +38,7 @@
 # Parsing, manifestification is implemented manually everywhere
 serde = "1.0.228"
 serde_json = "1.0.149"
-serde-saphyr = {version = "0.0.17", default-features = false}
+serde-saphyr = { version = "0.0.17", default-features = false }
 
 # Error handling
 anyhow = "1.0.101"
@@ -63,7 +63,7 @@
 
 mimallocator = "0.1.3"
 indoc = "2.0"
-insta = "1.46"
+insta = { version = "1.46", features = ["glob"] }
 tempfile = "3.24"
 pathdiff = "0.2.3"
 hashbrown = "0.16.1"
modifiedcrates/jrsonnet-formatter/src/lib.rsdiffbeforeafterboth
before · crates/jrsonnet-formatter/src/lib.rs
1use std::{any::type_name, rc::Rc};23use children::{children_between, trivia_before};4use dprint_core::formatting::{5	condition_helpers::is_multiple_lines,6	condition_resolvers::true_resolver,7	ir_helpers::{new_line_group, with_indent},8	ConditionResolver, ConditionResolverContext, LineNumber, PrintItems, PrintOptions,9};10use hi_doc::{Formatting, SnippetBuilder};11use jrsonnet_rowan_parser::{12	nodes::{13		Arg, ArgsDesc, Assertion, BinaryOperator, Bind, CompSpec, Destruct, DestructArrayPart,14		DestructRest, Expr, ExprBase, FieldName, ForSpec, IfSpec, ImportKind, Literal, Member,15		Name, Number, ObjBody, ObjLocal, ParamsDesc, SliceDesc, SourceFile, Stmt, Suffix, Text,16		UnaryOperator, Visibility,17	},18	AstNode, AstToken as _, SyntaxToken,19};2021use crate::{22	children::{trivia_after, Child, EndingComments},23	comments::{format_comments, CommentLocation},24};2526mod children;27mod comments;28#[cfg(test)]29mod tests;3031fn with_indent_eoi(cond: ConditionResolver, o: PrintItems, e: EndingComments) -> PrintItems {32	let end_comments_items = {33		let mut items = PrintItems::new();34		if e.should_start_with_newline {35			p!(&mut items, nl);36		}37		format_comments(&e.trivia, CommentLocation::EndOfItems, &mut items);38		items.into_rc_path()39	};40	let items =41		new_line_group(pi!(@i; items(o.into()) items(end_comments_items.into()))).into_rc_path();4243	let indented = with_indent(pi!(@i; nl items(items.into())));4445	pi!(@i; if_else("indented body", cond, items(indented))(str(" ") items(items.into())))46}4748pub trait Printable {49	fn print(&self, out: &mut PrintItems);50}5152macro_rules! pi {53	(@i; $($t:tt)*) => {{54		#[allow(unused_mut)]55		let mut o = dprint_core::formatting::PrintItems::new();56		pi!(@s; o: $($t)*);57		o58	}};59	(@s; $o:ident: str($e:expr $(,)?) $($t:tt)*) => {{60		$o.push_string($e.to_owned());61		pi!(@s; $o: $($t)*);62	}};63	(@s; $o:ident: string($e:expr $(,)?) $($t:tt)*) => {{64		$o.push_string($e);65		pi!(@s; $o: $($t)*);66	}};67	(@s; $o:ident: nl $($t:tt)*) => {{68		$o.push_signal(dprint_core::formatting::Signal::NewLine);69		pi!(@s; $o: $($t)*);70	}};71	(@s; $o:ident: sonl $($t:tt)*) => {{72		$o.push_signal(dprint_core::formatting::Signal::SpaceOrNewLine);73		pi!(@s; $o: $($t)*);74	}};75	(@s; $o:ident: tab $($t:tt)*) => {{76		$o.push_signal(dprint_core::formatting::Signal::Tab);77		pi!(@s; $o: $($t)*);78	}};79	(@s; $o:ident: >i $($t:tt)*) => {{80		$o.push_signal(dprint_core::formatting::Signal::StartIndent);81		pi!(@s; $o: $($t)*);82	}};83	(@s; $o:ident: <i $($t:tt)*) => {{84		$o.push_signal(dprint_core::formatting::Signal::FinishIndent);85		pi!(@s; $o: $($t)*);86	}};87	(@s; $o:ident: info($v:expr) $($t:tt)*) => {{88		$o.push_info($v);89		pi!(@s; $o: $($t)*);90	}};91	(@s; $o:ident: ln_anchor($v:expr) $($t:tt)*) => {{92		$o.push_anchor(LineNumberAnchor::new($v));93		pi!(@s; $o: $($t)*);94	}};95	(@s; $o:ident: if($s:literal, $cond:expr, $($i:tt)*) $($t:tt)*) => {{96		$o.push_condition(dprint_core::formatting::conditions::if_true(97			$s,98			$cond.clone(),99			{100				let mut o = PrintItems::new();101				p!(o, $($i)*);102				o103			},104		));105		pi!(@s; $o: $($t)*);106	}};107	(@s; $o:ident: if_else($s:literal, $cond:expr, $($i:tt)*)($($e:tt)+) $($t:tt)*) => {{108		$o.push_condition(dprint_core::formatting::conditions::if_true_or(109			$s,110			$cond.clone(),111			{112				let mut o = PrintItems::new();113				p!(o, $($i)*);114				o115			},116			{117				let mut o = PrintItems::new();118				p!(o, $($e)*);119				o120			},121		));122		pi!(@s; $o: $($t)*);123	}};124	(@s; $o:ident: if_not($s:literal, $cond:expr, $($e:tt)*) $($t:tt)*) => {{125		$o.push_condition(dprint_core::formatting::conditions::if_true_or(126			$s,127			$cond.clone(),128			{129				let o = PrintItems::new();130				o131			},132			{133				let mut o = PrintItems::new();134				p!(o, $($e)*);135				o136			},137		));138		pi!(@s; $o: $($t)*);139	}};140	(@s; $o:ident: {$expr:expr} $($t:tt)*) => {{141		$expr.print($o);142		pi!(@s; $o: $($t)*);143	}};144	(@s; $o:ident: items($expr:expr) $($t:tt)*) => {{145		$o.extend($expr);146		pi!(@s; $o: $($t)*);147	}};148	(@s; $o:ident: if ($e:expr)($($then:tt)*) $($t:tt)*) => {{149		if $e {150			pi!(@s; $o: $($then)*);151		}152		pi!(@s; $o: $($t)*);153	}};154	(@s; $o:ident: ifelse ($e:expr)($($then:tt)*)($($else:tt)*) $($t:tt)*) => {{155		if $e {156			pi!(@s; $o: $($then)*);157		} else {158			pi!(@s; $o: $($else)*);159		}160		pi!(@s; $o: $($t)*);161	}};162	(@s; $i:ident:) => {}163}164macro_rules! p {165	($o:ident, $($t:tt)*) => {166		pi!(@s; $o: $($t)*)167	};168	(&mut $o:ident, $($t:tt)*) => {169		let om = &mut $o;170		pi!(@s; om: $($t)*)171	};172}173pub(crate) use p;174pub(crate) use pi;175176impl<P> Printable for Option<P>177where178	P: Printable,179{180	fn print(&self, out: &mut PrintItems) {181		if let Some(v) = self {182			v.print(out);183		} else {184			p!(185				out,186				string(format!(187					"/*missing {}*/",188					type_name::<P>().replace("jrsonnet_rowan_parser::generated::nodes::", "")189				),)190			);191		}192	}193}194195impl Printable for SyntaxToken {196	fn print(&self, out: &mut PrintItems) {197		p!(out, string(self.to_string()));198	}199}200201impl Printable for Text {202	fn print(&self, out: &mut PrintItems) {203		p!(out, string(format!("{}", self)));204	}205}206impl Printable for Number {207	fn print(&self, out: &mut PrintItems) {208		p!(out, string(format!("{}", self)));209	}210}211212impl Printable for Name {213	fn print(&self, out: &mut PrintItems) {214		p!(out, { self.ident_lit() });215	}216}217218impl Printable for DestructRest {219	fn print(&self, out: &mut PrintItems) {220		p!(out, str("..."));221		if let Some(name) = self.into() {222			p!(out, { name });223		}224	}225}226227impl Printable for Destruct {228	fn print(&self, out: &mut PrintItems) {229		match self {230			Self::DestructFull(f) => {231				p!(out, { f.name() });232			}233			Self::DestructSkip(_) => p!(out, str("?")),234			Self::DestructArray(a) => {235				p!(out, str("[") >i nl);236				for el in a.destruct_array_parts() {237					match el {238						DestructArrayPart::DestructArrayElement(e) => {239							p!(out, {e.destruct()} str(",") nl);240						}241						DestructArrayPart::DestructRest(d) => {242							p!(out, {d} str(",") nl);243						}244					}245				}246				p!(out, <i str("]"));247			}248			Self::DestructObject(o) => {249				p!(out, str("{") >i nl);250				for item in o.destruct_object_fields() {251					p!(out, { item.field() });252					if let Some(des) = item.destruct() {253						p!(out, str(": ") {des});254					}255					if let Some(def) = item.expr() {256						p!(out, str(" = ") {def});257					}258					p!(out, str(",") nl);259				}260				if let Some(rest) = o.destruct_rest() {261					p!(out, {rest} nl);262				}263				p!(out, <i str("}"));264			}265		}266	}267}268269impl Printable for FieldName {270	fn print(&self, out: &mut PrintItems) {271		match self {272			Self::FieldNameFixed(f) => {273				if let Some(id) = f.id() {274					p!(out, { id });275				} else if let Some(str) = f.text() {276					p!(out, { str });277				} else {278					p!(out, str("/*missing FieldName*/"));279				}280			}281			Self::FieldNameDynamic(d) => {282				p!(out, str("[") {d.expr()} str("]"));283			}284		}285	}286}287288impl Printable for Visibility {289	fn print(&self, out: &mut PrintItems) {290		p!(out, string(self.to_string()));291	}292}293294impl Printable for ObjLocal {295	fn print(&self, out: &mut PrintItems) {296		p!(out, str("local ") {self.bind()});297	}298}299300impl Printable for Assertion {301	fn print(&self, out: &mut PrintItems) {302		p!(out, str("assert ") {self.condition()});303		if self.colon_token().is_some() || self.message().is_some() {304			p!(out, str(": ") {self.message()});305		}306	}307}308309impl Printable for ParamsDesc {310	fn print(&self, out: &mut PrintItems) {311		p!(out, str("(") >i nl);312		for param in self.params() {313			p!(out, { param.destruct() });314			if param.assign_token().is_some() || param.expr().is_some() {315				p!(out, str(" = ") {param.expr()});316			}317			p!(out, str(",") nl);318		}319		p!(out, <i str(")"));320	}321}322impl Printable for ArgsDesc {323	fn print(&self, out: &mut PrintItems) {324		let start = LineNumber::new("args start line");325		let end = LineNumber::new("args end line");326		let multi_line = Rc::new(move |condition_context: &mut ConditionResolverContext| {327			is_multiple_lines(condition_context, start, end)328		});329330		let (children, end_comments) = children_between::<Arg>(331			self.syntax().clone(),332			self.l_paren_token().map(Into::into).as_ref(),333			self.r_paren_token().map(Into::into).as_ref(),334			None,335		);336337		fn gen_args(children: Vec<Child<Arg>>, multi_line: ConditionResolver) -> PrintItems {338			let mut _out = PrintItems::new();339			let out = &mut _out;340341			let mut args = children.into_iter().peekable();342			while let Some(ele) = args.next() {343				if ele.should_start_with_newline {344					p!(out, nl);345				}346				format_comments(&ele.before_trivia, CommentLocation::AboveItem, out);347				let arg = ele.value;348				if arg.name().is_some() || arg.assign_token().is_some() {349					p!(out, {arg.name()} str(" = "));350				}351				p!(out, { arg.expr() });352				let has_more = args.peek().is_some();353				if has_more {354					p!(out, str(","));355				} else {356					p!(out, if("trailing comma", multi_line, str(",")));357				}358				format_comments(&ele.inline_trivia, CommentLocation::ItemInline, out);359				if has_more {360					p!(out, if_else("arg separator", multi_line, nl)(sonl));361				}362			}363			_out364		}365366		let args_items = new_line_group(gen_args(children, multi_line.clone())).into_rc_path();367		let args_indented = with_indent(pi!(@i; nl items(args_items.into())));368369		p!(out, str("(") info(start));370		p!(out, if_else("args body", multi_line, items(args_indented) nl)(items(args_items.into())));371		if end_comments.should_start_with_newline {372			p!(out, nl);373		}374		format_comments(&end_comments.trivia, CommentLocation::EndOfItems, out);375		p!(out, str(")") info(end));376	}377}378impl Printable for SliceDesc {379	fn print(&self, out: &mut PrintItems) {380		p!(out, str("["));381		if self.from().is_some() {382			p!(out, { self.from() });383		}384		p!(out, str(":"));385		if self.end().is_some() {386			p!(out, { self.end().map(|e| e.expr()) });387		}388		// Keep only one : in case if we don't need step389		if self.step().is_some() {390			p!(out, str(":") {self.step().map(|e|e.expr())});391		}392		p!(out, str("]"));393	}394}395396impl Printable for Member {397	fn print(&self, out: &mut PrintItems) {398		match self {399			Self::MemberBindStmt(b) => {400				p!(out, { b.obj_local() });401			}402			Self::MemberAssertStmt(ass) => {403				p!(out, { ass.assertion() });404			}405			Self::MemberFieldNormal(n) => {406				p!(out, {n.field_name()} if(n.plus_token().is_some())({n.plus_token()}) {n.visibility()} str(" ") {n.expr()});407			}408			Self::MemberFieldMethod(m) => {409				p!(out, {m.field_name()} {m.params_desc()} {m.visibility()} str(" ") {m.expr()});410			}411		}412	}413}414415impl Printable for ObjBody {416	fn print(&self, out: &mut PrintItems) {417		match self {418			Self::ObjBodyComp(l) => {419				let (children, mut end_comments) = children_between::<Member>(420					l.syntax().clone(),421					l.l_brace_token().map(Into::into).as_ref(),422					Some(423						&(l.comp_specs()424							.next()425							.expect("at least one spec is defined")426							.syntax()427							.clone())428						.into(),429					),430					None,431				);432				let trailing_for_comp = end_comments.extract_trailing();433				p!(out, str("{") >i nl);434				for mem in children {435					if mem.should_start_with_newline {436						p!(out, nl);437					}438					format_comments(&mem.before_trivia, CommentLocation::AboveItem, out);439					p!(out, {mem.value} str(","));440					format_comments(&mem.inline_trivia, CommentLocation::ItemInline, out);441					p!(out, nl);442				}443444				if end_comments.should_start_with_newline {445					p!(out, nl);446				}447				format_comments(&end_comments.trivia, CommentLocation::EndOfItems, out);448449				let (compspecs, end_comments) = children_between::<CompSpec>(450					l.syntax().clone(),451					l.member_comps()452						.last()453						.map(|m| m.syntax().clone())454						.map(Into::into)455						.or_else(|| l.l_brace_token().map(Into::into))456						.as_ref(),457					l.r_brace_token().map(Into::into).as_ref(),458					Some(trailing_for_comp),459				);460				for mem in compspecs {461					if mem.should_start_with_newline {462						p!(out, nl);463					}464					format_comments(&mem.before_trivia, CommentLocation::AboveItem, out);465					p!(out, { mem.value });466					format_comments(&mem.inline_trivia, CommentLocation::ItemInline, out);467				}468				if end_comments.should_start_with_newline {469					p!(out, nl);470				}471				format_comments(&end_comments.trivia, CommentLocation::EndOfItems, out);472473				p!(out, nl <i str("}"));474			}475			Self::ObjBodyMemberList(l) => {476				let (children, end_comments) = children_between::<Member>(477					l.syntax().clone(),478					l.l_brace_token().map(Into::into).as_ref(),479					l.r_brace_token().map(Into::into).as_ref(),480					None,481				);482				if children.is_empty() && end_comments.is_empty() {483					p!(out, str("{ }"));484					return;485				}486487				let source_is_multiline = children.iter().any(|c| c.triggers_multiline)488					|| end_comments.should_start_with_newline;489490				let start = LineNumber::new("obj start line");491				let end = LineNumber::new("obj end line");492				let multi_line: ConditionResolver = if source_is_multiline {493					true_resolver()494				} else {495					Rc::new(move |ctx: &mut ConditionResolverContext| {496						is_multiple_lines(ctx, start, end)497					})498				};499500				fn gen_members(501					children: Vec<Child<Member>>,502					multi_line: ConditionResolver,503				) -> PrintItems {504					let mut _out = PrintItems::new();505					let out = &mut _out;506					let mut members = children.into_iter().peekable();507					while let Some(mem) = members.next() {508						if mem.should_start_with_newline {509							p!(out, nl);510						}511						format_comments(&mem.before_trivia, CommentLocation::AboveItem, out);512						p!(out, { mem.value });513						let has_more = members.peek().is_some();514						if has_more {515							p!(out, str(","));516						} else {517							p!(out, if("trailing comma", multi_line, str(",")));518						}519						format_comments(&mem.inline_trivia, CommentLocation::ItemInline, out);520						p!(out, if_else("member separator", multi_line, nl)(sonl));521					}522					_out523				}524525				let members_items =526					new_line_group(gen_members(children, multi_line.clone())).into_rc_path();527528				let members = with_indent_eoi(multi_line, members_items.into(), end_comments);529530				p!(out, str("{") info(start));531				p!(out, items(members));532				p!(out, str("}") info(end));533			}534		}535	}536}537impl Printable for UnaryOperator {538	fn print(&self, out: &mut PrintItems) {539		p!(out, string(self.text().to_string()));540	}541}542impl Printable for BinaryOperator {543	fn print(&self, out: &mut PrintItems) {544		p!(out, string(self.text().to_string()));545	}546}547impl Printable for Bind {548	fn print(&self, out: &mut PrintItems) {549		match self {550			Self::BindDestruct(d) => {551				p!(out, {d.into()} str(" = ") {d.value()});552			}553			Self::BindFunction(f) => {554				p!(out, {f.name()} {f.params()} str(" = ") {f.value()});555			}556		}557	}558}559impl Printable for Literal {560	fn print(&self, out: &mut PrintItems) {561		p!(out, string(self.syntax().to_string()));562	}563}564impl Printable for ImportKind {565	fn print(&self, out: &mut PrintItems) {566		p!(out, string(self.syntax().to_string()));567	}568}569impl Printable for ForSpec {570	fn print(&self, out: &mut PrintItems) {571		p!(out, str("for ") {self.bind()} str(" in ") {self.expr()});572	}573}574impl Printable for IfSpec {575	fn print(&self, out: &mut PrintItems) {576		p!(out, str("if ") {self.expr()});577	}578}579impl Printable for CompSpec {580	fn print(&self, out: &mut PrintItems) {581		match self {582			Self::ForSpec(f) => f.print(out),583			Self::IfSpec(i) => i.print(out),584		}585	}586}587impl Printable for Expr {588	fn print(&self, out: &mut PrintItems) {589		let (stmts, _ending) = children_between::<Stmt>(590			self.syntax().clone(),591			None,592			self.expr_base()593				.as_ref()594				.map(ExprBase::syntax)595				.cloned()596				.map(Into::into)597				.as_ref(),598			None,599		);600		for stmt in stmts {601			p!(out, { stmt.value });602		}603		p!(out, { self.expr_base() });604		let (suffixes, _ending) = children_between::<Suffix>(605			self.syntax().clone(),606			self.expr_base()607				.as_ref()608				.map(ExprBase::syntax)609				.cloned()610				.map(Into::into)611				.as_ref(),612			None,613			None,614		);615		for suffix in suffixes {616			p!(out, { suffix.value });617		}618	}619}620impl Printable for Suffix {621	fn print(&self, out: &mut PrintItems) {622		match self {623			Self::SuffixIndex(i) => {624				if i.question_mark_token().is_some() {625					p!(out, str("?"));626				}627				p!(out, str(".") {i.index()});628			}629			Self::SuffixIndexExpr(e) => {630				if e.question_mark_token().is_some() {631					p!(out, str(".?"));632				}633				p!(out, str("[") {e.index()} str("]"));634			}635			Self::SuffixSlice(d) => {636				p!(out, { d.slice_desc() });637			}638			Self::SuffixApply(a) => {639				p!(out, { a.args_desc() });640			}641		}642	}643}644impl Printable for Stmt {645	fn print(&self, out: &mut PrintItems) {646		match self {647			Self::StmtLocal(l) => {648				let (binds, end_comments) = children_between::<Bind>(649					l.syntax().clone(),650					l.local_kw_token().map(Into::into).as_ref(),651					l.semi_token().map(Into::into).as_ref(),652					None,653				);654				if binds.len() == 1 {655					let bind = &binds[0];656					format_comments(&bind.before_trivia, CommentLocation::AboveItem, out);657					p!(out, str("local ") {bind.value});658				// TODO: keep end_comments, child.inline_trivia somehow, force multiple locals formatting in case of presence?659				} else {660					p!(out,str("local") >i nl);661					for bind in binds {662						if bind.should_start_with_newline {663							p!(out, nl);664						}665						format_comments(&bind.before_trivia, CommentLocation::AboveItem, out);666						p!(out, {bind.value} str(","));667						format_comments(&bind.inline_trivia, CommentLocation::ItemInline, out);668						p!(out, nl);669					}670					if end_comments.should_start_with_newline {671						p!(out, nl);672					}673					format_comments(&end_comments.trivia, CommentLocation::EndOfItems, out);674					p!(out,<i);675				}676				p!(out,str(";") nl);677			}678			Self::StmtAssert(a) => {679				p!(out, {a.assertion()} str(";") nl);680			}681		}682	}683}684impl Printable for ExprBase {685	fn print(&self, out: &mut PrintItems) {686		match self {687			Self::ExprBinary(b) => {688				p!(out, {b.lhs()} str(" ") {b.binary_operator()} str(" ") {b.rhs()});689			}690			Self::ExprUnary(u) => p!(out, {u.unary_operator()} {u.rhs()}),691			// Self::ExprSlice(s) => {692			// 	p!(new: {s.expr()} {s.slice_desc()})693			// }694			// Self::ExprIndex(i) => {695			// 	p!(new: {i.expr()} str(".") {i.index()})696			// }697			// Self::ExprIndexExpr(i) => p!(new: {i.base()} str("[") {i.index()} str("]")),698			// Self::ExprApply(a) => {699			// 	let mut pi = p!(new: {a.expr()} {a.args_desc()});700			// 	if a.tailstrict_kw_token().is_some() {701			// 		p!(out,str(" tailstrict"));702			// 	}703			// 	pi704			// }705			Self::ExprObjExtend(ex) => {706				p!(out, {ex.lhs_work()} str(" ") {ex.rhs_work()});707			}708			Self::ExprParened(p) => {709				p!(out, str("(") {p.expr()} str(")"));710			}711			Self::ExprString(s) => p!(out, { s.text() }),712			Self::ExprNumber(n) => p!(out, { n.number() }),713			Self::ExprArray(a) => {714				p!(out, str("[") >i nl);715				for el in a.exprs() {716					p!(out, {el} str(",") nl);717				}718				p!(out, <i str("]"));719			}720			Self::ExprObject(obj) => {721				p!(out, { obj.obj_body() });722			}723			Self::ExprArrayComp(arr) => {724				p!(out, str("[") {arr.expr()});725				for spec in arr.comp_specs() {726					p!(out, str(" ") {spec});727				}728				p!(out, str("]"));729			}730			Self::ExprImport(v) => {731				p!(out, {v.import_kind()} str(" ") {v.text()});732			}733			Self::ExprVar(n) => p!(out, { n.name() }),734			// Self::ExprLocal(l) => {735			// }736			Self::ExprIfThenElse(ite) => {737				p!(out, str("if ") {ite.cond()} str(" then ") {ite.then().map(|t| t.expr())});738				if ite.else_kw_token().is_some() || ite.else_().is_some() {739					p!(out, str(" else ") {ite.else_().map(|t| t.expr())});740				}741			}742			Self::ExprFunction(f) => p!(out, str("function") {f.params_desc()} nl {f.expr()}),743			// Self::ExprAssert(a) => p!(new: {a.assertion()} str("; ") {a.expr()}),744			Self::ExprError(e) => p!(out, str("error ") {e.expr()}),745			Self::ExprLiteral(l) => {746				p!(out, { l.literal() });747			}748		}749	}750}751752impl Printable for SourceFile {753	fn print(&self, out: &mut PrintItems) {754		let before = trivia_before(755			self.syntax().clone(),756			self.expr()757				.map(|e| e.syntax().clone())758				.map(Into::into)759				.as_ref(),760		);761		let after = trivia_after(762			self.syntax().clone(),763			self.expr()764				.map(|e| e.syntax().clone())765				.map(Into::into)766				.as_ref(),767		);768		format_comments(&before, CommentLocation::AboveItem, out);769		p!(out, {self.expr()} nl);770		format_comments(&after, CommentLocation::EndOfItems, out);771	}772}773774pub struct FormatOptions {775	// 0 for hard tabs776	pub indent: u8,777}778pub fn format(input: &str, opts: &FormatOptions) -> Result<String, SnippetBuilder> {779	let (parsed, errors) = jrsonnet_rowan_parser::parse(input);780	if !errors.is_empty() {781		let mut builder = hi_doc::SnippetBuilder::new(input);782		for error in errors {783			builder784				.error(hi_doc::Text::fragment(785					format!("{:?}", error.error),786					Formatting::default(),787				))788				.range(789					error.range.start().into()790						..=(usize::from(error.range.end()) - 1).max(error.range.start().into()),791				)792				.build();793		}794		// let snippet = builder.build();795		return Err(builder);796		// It is possible to recover from this failure, but the output may be broken, as formatter is free to skip797		// ERROR rowan nodes.798		// Recovery needs to be enabled for LSP, though.799	}800	Ok(dprint_core::formatting::format(801		|| {802			let mut out = PrintItems::new();803			parsed.print(&mut out);804			out805		},806		PrintOptions {807			indent_width: if opts.indent == 0 {808				// Reasonable max length for both 2 and 4 space sized tabs.809				3810			} else {811				opts.indent812			},813			max_width: 100,814			use_tabs: opts.indent == 0,815			new_line_text: "\n",816		},817	))818}
deletedcrates/jrsonnet-formatter/src/snapshots/jrsonnet_formatter__tests__args.snapdiffbeforeafterboth
--- a/crates/jrsonnet-formatter/src/snapshots/jrsonnet_formatter__tests__args.snap
+++ /dev/null
@@ -1,36 +0,0 @@
----
-source: crates/jrsonnet-formatter/src/tests.rs
-expression: "reformat(indoc!(\"\n\t\t\t{\n\t\t\t\tshort: aaa(1,2,3,4,5),\n\t\t\t\tlong: bbb(123123123123123123123,12312312321123123123,123123123123312123123,123123123123123123312,123123123123312321123),\n\t\t\t\tshort_in_long: bbb(aaa(1,2,3,4,5), 123123123123123123123,12312312321123123123,123123123123312123123,123123123123123123312,123123123123312321123),\n\t\t\t\tlong_in_short: aaa(1,2,3,4,5,bbb(123123123123123123123,12312312321123123123,123123123123312123123,123123123123123123312,123123123123312321123)),\n\t\t\t}\n\t\t\"))"
----
-{
-	short: aaa(1, 2, 3, 4, 5),
-	long: bbb(
-		123123123123123123123,
-		12312312321123123123,
-		123123123123312123123,
-		123123123123123123312,
-		123123123123312321123,
-	),
-	short_in_long: bbb(
-		aaa(1, 2, 3, 4, 5),
-		123123123123123123123,
-		12312312321123123123,
-		123123123123312123123,
-		123123123123123123312,
-		123123123123312321123,
-	),
-	long_in_short: aaa(
-		1,
-		2,
-		3,
-		4,
-		5,
-		bbb(
-			123123123123123123123,
-			12312312321123123123,
-			123123123123312123123,
-			123123123123123123312,
-			123123123123312321123,
-		),
-	),
-}
deletedcrates/jrsonnet-formatter/src/snapshots/jrsonnet_formatter__tests__asserts.snapdiffbeforeafterboth
--- a/crates/jrsonnet-formatter/src/snapshots/jrsonnet_formatter__tests__asserts.snap
+++ /dev/null
@@ -1,9 +0,0 @@
----
-source: crates/jrsonnet-formatter/src/tests.rs
-expression: "reformat(indoc!(\"\n\t\t\t{\n\t\t\t\tassert 1 > 0 : 'one should be greater than zero',\n\t\t\t\tassert true,\n\t\t\t\tvalue: 42,\n\t\t\t}\n\t\t\"))"
----
-{
-	assert 1 > 0: 'one should be greater than zero',
-	assert true,
-	value: 42,
-}
deletedcrates/jrsonnet-formatter/src/snapshots/jrsonnet_formatter__tests__complex_comments.snapdiffbeforeafterboth
--- a/crates/jrsonnet-formatter/src/snapshots/jrsonnet_formatter__tests__complex_comments.snap
+++ /dev/null
@@ -1,62 +0,0 @@
----
-source: crates/jrsonnet-formatter/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\n\t\t\t smallObjectWithEnding: {/*Ending comment*/},\n\t\t\t smallObjectWithFieldAndEnding: {a: 11/*Ending comment*/},\n\t\t\t smallObjectWithFieldAndEnding2: {/*Start*/a: 11/*Ending comment*/},\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: '',
-	},
-
-	smallObjectWithEnding: {
-		/* Ending comment */
-	},
-	smallObjectWithFieldAndEnding: { a: 11 /* Ending comment */ },
-	smallObjectWithFieldAndEnding2: {
-		/* Start */
-		a: 11, /* Ending comment */
-	},
-}
deletedcrates/jrsonnet-formatter/src/snapshots/jrsonnet_formatter__tests__complex_nested.snapdiffbeforeafterboth
--- a/crates/jrsonnet-formatter/src/snapshots/jrsonnet_formatter__tests__complex_nested.snap
+++ /dev/null
@@ -1,41 +0,0 @@
----
-source: crates/jrsonnet-formatter/src/tests.rs
-expression: "reformat(indoc!(\"\n\t\t\t{\n\t\t\t\tkubernetes: {\n\t\t\t\t deployment: {\n\t\t\t\t\tapiVersion: 'apps/v1',\n\t\t\t\t\tkind: 'Deployment',\n\t\t\t\t\tmetadata: {\n\t\t\t\t\t  name: 'myapp',\n\t\t\t\t\t  labels: { app: 'myapp', version: 'v1' },\n\t\t\t\t\t},\n\t\t\t\t\tspec: {\n\t\t\t\t\t  replicas: 3,\n\t\t\t\t\t  selector: { matchLabels: { app: 'myapp' } },\n\t\t\t\t\t  template: {\n\t\t\t\t\t\t metadata: { labels: { app: 'myapp' } },\n\t\t\t\t\t\t spec: {\n\t\t\t\t\t\t\tcontainers: [\n\t\t\t\t\t\t\t  {\n\t\t\t\t\t\t\t\t name: 'myapp',\n\t\t\t\t\t\t\t\t image: 'myapp:latest',\n\t\t\t\t\t\t\t\t ports: [{ containerPort: 8080 }],\n\t\t\t\t\t\t\t\t env: [\n\t\t\t\t\t\t\t\t\t{ name: 'FOO', value: 'bar' },\n\t\t\t\t\t\t\t\t\t{ name: 'BAZ', valueFrom: { secretKeyRef: { name: 'mysecret', key: 'password' } } },\n\t\t\t\t\t\t\t\t ],\n\t\t\t\t\t\t\t  },\n\t\t\t\t\t\t\t],\n\t\t\t\t\t\t },\n\t\t\t\t\t  },\n\t\t\t\t\t},\n\t\t\t\t },\n\t\t\t  },\n\t\t\t}\n\t\t\"))"
----
-{
-	kubernetes: {
-		deployment: {
-			apiVersion: 'apps/v1',
-			kind: 'Deployment',
-			metadata: {
-				name: 'myapp',
-				labels: { app: 'myapp', version: 'v1' },
-			},
-			spec: {
-				replicas: 3,
-				selector: { matchLabels: { app: 'myapp' } },
-				template: {
-					metadata: { labels: { app: 'myapp' } },
-					spec: {
-						containers: [
-							{
-								name: 'myapp',
-								image: 'myapp:latest',
-								ports: [
-									{ containerPort: 8080 },
-								],
-								env: [
-									{ name: 'FOO', value: 'bar' },
-									{
-										name: 'BAZ',
-										valueFrom: { secretKeyRef: { name: 'mysecret', key: 'password' } },
-									},
-								],
-							},
-						],
-					},
-				},
-			},
-		},
-	},
-}
deletedcrates/jrsonnet-formatter/src/snapshots/jrsonnet_formatter__tests__self_super.snapdiffbeforeafterboth
--- a/crates/jrsonnet-formatter/src/snapshots/jrsonnet_formatter__tests__self_super.snap
+++ /dev/null
@@ -1,13 +0,0 @@
----
-source: crates/jrsonnet-formatter/src/tests.rs
-expression: "reformat(indoc!(\"\n\t\t\tlocal base = {\n\t\t\t  foo: 'bar',\n\t\t\t  method():: self.foo,\n\t\t\t};\n\n\t\t\tbase {\n\t\t\t  foo: super.foo + '-extended',\n\t\t\t  result: self.method(),\n\t\t\t}\n\t\t\"))"
----
-local base = {
-	foo: 'bar',
-	method(
-	):: self.foo,
-};
-base {
-	foo: super.foo + '-extended',
-	result: self.method(),
-}
addedcrates/jrsonnet-formatter/src/snapshots/jrsonnet_formatter__tests__snapshots@args.jsonnet.snapdiffbeforeafterboth
--- /dev/null
+++ b/crates/jrsonnet-formatter/src/snapshots/jrsonnet_formatter__tests__snapshots@args.jsonnet.snap
@@ -0,0 +1,37 @@
+---
+source: crates/jrsonnet-formatter/src/tests.rs
+expression: reformat(&input)
+input_file: crates/jrsonnet-formatter/src/tests/args.jsonnet
+---
+{
+   short: aaa(1, 2, 3, 4, 5),
+   long: bbb(
+      123123123123123123123,
+      12312312321123123123,
+      123123123123312123123,
+      123123123123123123312,
+      123123123123312321123,
+   ),
+   short_in_long: bbb(
+      aaa(1, 2, 3, 4, 5),
+      123123123123123123123,
+      12312312321123123123,
+      123123123123312123123,
+      123123123123123123312,
+      123123123123312321123,
+   ),
+   long_in_short: aaa(
+      1,
+      2,
+      3,
+      4,
+      5,
+      bbb(
+         123123123123123123123,
+         12312312321123123123,
+         123123123123312123123,
+         123123123123123123312,
+         123123123123312321123,
+      ),
+   ),
+}
addedcrates/jrsonnet-formatter/src/snapshots/jrsonnet_formatter__tests__snapshots@asserts.jsonnet.snapdiffbeforeafterboth
--- /dev/null
+++ b/crates/jrsonnet-formatter/src/snapshots/jrsonnet_formatter__tests__snapshots@asserts.jsonnet.snap
@@ -0,0 +1,10 @@
+---
+source: crates/jrsonnet-formatter/src/tests.rs
+expression: reformat(&input)
+input_file: crates/jrsonnet-formatter/src/tests/asserts.jsonnet
+---
+{
+   assert 1 > 0: 'one should be greater than zero',
+   assert true,
+   value: 42,
+}
addedcrates/jrsonnet-formatter/src/snapshots/jrsonnet_formatter__tests__snapshots@basic_array.jsonnet.snapdiffbeforeafterboth
--- /dev/null
+++ b/crates/jrsonnet-formatter/src/snapshots/jrsonnet_formatter__tests__snapshots@basic_array.jsonnet.snap
@@ -0,0 +1,12 @@
+---
+source: crates/jrsonnet-formatter/src/tests.rs
+expression: reformat(&input)
+input_file: crates/jrsonnet-formatter/src/tests/basic_array.jsonnet
+---
+[
+   1,
+   2,
+   3,
+   4,
+   5,
+]
addedcrates/jrsonnet-formatter/src/snapshots/jrsonnet_formatter__tests__snapshots@basic_object.jsonnet.snapdiffbeforeafterboth
--- /dev/null
+++ b/crates/jrsonnet-formatter/src/snapshots/jrsonnet_formatter__tests__snapshots@basic_object.jsonnet.snap
@@ -0,0 +1,6 @@
+---
+source: crates/jrsonnet-formatter/src/tests.rs
+expression: reformat(&input)
+input_file: crates/jrsonnet-formatter/src/tests/basic_object.jsonnet
+---
+{ foo: 'bar', baz: 'qux', nested: { a: 1, b: 2 } }
addedcrates/jrsonnet-formatter/src/snapshots/jrsonnet_formatter__tests__snapshots@comments.jsonnet.snapdiffbeforeafterboth
--- /dev/null
+++ b/crates/jrsonnet-formatter/src/snapshots/jrsonnet_formatter__tests__snapshots@comments.jsonnet.snap
@@ -0,0 +1,16 @@
+---
+source: crates/jrsonnet-formatter/src/tests.rs
+expression: reformat(&input)
+input_file: crates/jrsonnet-formatter/src/tests/comments.jsonnet
+---
+// File header comment
+{
+   // Comment above field
+   foo: 'bar',
+   /* Block comment */
+   baz: 'qux',
+   nested: {
+      // Hash comment
+      value: 42,
+   },
+}
addedcrates/jrsonnet-formatter/src/snapshots/jrsonnet_formatter__tests__snapshots@complex_comments.jsonnet.snapdiffbeforeafterboth
--- /dev/null
+++ b/crates/jrsonnet-formatter/src/snapshots/jrsonnet_formatter__tests__snapshots@complex_comments.jsonnet.snap
@@ -0,0 +1,63 @@
+---
+source: crates/jrsonnet-formatter/src/tests.rs
+expression: reformat(&input)
+input_file: crates/jrsonnet-formatter/src/tests/complex_comments.jsonnet
+---
+{
+   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: '',
+   },
+
+   smallObjectWithEnding: {
+      /* Ending comment */
+   },
+   smallObjectWithFieldAndEnding: { a: 11 /* Ending comment */ },
+   smallObjectWithFieldAndEnding2: {
+      /* Start */
+      a: 11, /* Ending comment */
+   },
+}
addedcrates/jrsonnet-formatter/src/snapshots/jrsonnet_formatter__tests__snapshots@complex_nested.jsonnet.snapdiffbeforeafterboth
--- /dev/null
+++ b/crates/jrsonnet-formatter/src/snapshots/jrsonnet_formatter__tests__snapshots@complex_nested.jsonnet.snap
@@ -0,0 +1,42 @@
+---
+source: crates/jrsonnet-formatter/src/tests.rs
+expression: reformat(&input)
+input_file: crates/jrsonnet-formatter/src/tests/complex_nested.jsonnet
+---
+{
+   kubernetes: {
+      deployment: {
+         apiVersion: 'apps/v1',
+         kind: 'Deployment',
+         metadata: {
+            name: 'myapp',
+            labels: { app: 'myapp', version: 'v1' },
+         },
+         spec: {
+            replicas: 3,
+            selector: { matchLabels: { app: 'myapp' } },
+            template: {
+               metadata: { labels: { app: 'myapp' } },
+               spec: {
+                  containers: [
+                     {
+                        name: 'myapp',
+                        image: 'myapp:latest',
+                        ports: [
+                           { containerPort: 8080 },
+                        ],
+                        env: [
+                           { name: 'FOO', value: 'bar' },
+                           {
+                              name: 'BAZ',
+                              valueFrom: { secretKeyRef: { name: 'mysecret', key: 'password' } },
+                           },
+                        ],
+                     },
+                  ],
+               },
+            },
+         },
+      },
+   },
+}
addedcrates/jrsonnet-formatter/src/snapshots/jrsonnet_formatter__tests__snapshots@comprehensions.jsonnet.snapdiffbeforeafterboth
--- /dev/null
+++ b/crates/jrsonnet-formatter/src/snapshots/jrsonnet_formatter__tests__snapshots@comprehensions.jsonnet.snap
@@ -0,0 +1,29 @@
+---
+source: crates/jrsonnet-formatter/src/tests.rs
+expression: reformat(&input)
+input_file: crates/jrsonnet-formatter/src/tests/comprehensions.jsonnet
+---
+{
+   arr: [x for x in [
+      1,
+      2,
+      3,
+   ]],
+   filtered: [x for x in [
+      1,
+      2,
+      3,
+      4,
+      5,
+   ] if x > 2],
+   obj: {
+      [k]: v,
+      for k in [
+         'a',
+         'b',
+      ]for v in [
+         1,
+         2,
+      ]
+   },
+}
addedcrates/jrsonnet-formatter/src/snapshots/jrsonnet_formatter__tests__snapshots@conditionals.jsonnet.snapdiffbeforeafterboth
--- /dev/null
+++ b/crates/jrsonnet-formatter/src/snapshots/jrsonnet_formatter__tests__snapshots@conditionals.jsonnet.snap
@@ -0,0 +1,9 @@
+---
+source: crates/jrsonnet-formatter/src/tests.rs
+expression: reformat(&input)
+input_file: crates/jrsonnet-formatter/src/tests/conditionals.jsonnet
+---
+{
+   simple: if true then 'yes' else 'no',
+   nested: if 1 > 0 then if 2 > 1 then 'a' else 'b' else 'c',
+}
addedcrates/jrsonnet-formatter/src/snapshots/jrsonnet_formatter__tests__snapshots@functions.jsonnet.snapdiffbeforeafterboth
--- /dev/null
+++ b/crates/jrsonnet-formatter/src/snapshots/jrsonnet_formatter__tests__snapshots@functions.jsonnet.snap
@@ -0,0 +1,20 @@
+---
+source: crates/jrsonnet-formatter/src/tests.rs
+expression: reformat(&input)
+input_file: crates/jrsonnet-formatter/src/tests/functions.jsonnet
+---
+{
+   simple(
+      x,
+   ):: x * 2,
+   with_default(
+      x,
+      y = 10,
+   ):: x + y,
+   multiline(
+      a,
+      b,
+      c,
+   ):: a + b + c,
+   called: self.simple(5),
+}
addedcrates/jrsonnet-formatter/src/snapshots/jrsonnet_formatter__tests__snapshots@imports.jsonnet.snapdiffbeforeafterboth
--- /dev/null
+++ b/crates/jrsonnet-formatter/src/snapshots/jrsonnet_formatter__tests__snapshots@imports.jsonnet.snap
@@ -0,0 +1,11 @@
+---
+source: crates/jrsonnet-formatter/src/tests.rs
+expression: reformat(&input)
+input_file: crates/jrsonnet-formatter/src/tests/imports.jsonnet
+---
+local a = import 'a.libsonnet';
+local m = import 'm.libsonnet';
+local z = import 'z.libsonnet';
+{
+   result: a + m + z,
+}
addedcrates/jrsonnet-formatter/src/snapshots/jrsonnet_formatter__tests__snapshots@local_vars.jsonnet.snapdiffbeforeafterboth
--- /dev/null
+++ b/crates/jrsonnet-formatter/src/snapshots/jrsonnet_formatter__tests__snapshots@local_vars.jsonnet.snap
@@ -0,0 +1,12 @@
+---
+source: crates/jrsonnet-formatter/src/tests.rs
+expression: reformat(&input)
+input_file: crates/jrsonnet-formatter/src/tests/local_vars.jsonnet
+---
+local x = 10;
+local y = 20;
+local sum = x + y;
+{
+   local inner = 5,
+   result: sum + inner,
+}
addedcrates/jrsonnet-formatter/src/snapshots/jrsonnet_formatter__tests__snapshots@operators.jsonnet.snapdiffbeforeafterboth
--- /dev/null
+++ b/crates/jrsonnet-formatter/src/snapshots/jrsonnet_formatter__tests__snapshots@operators.jsonnet.snap
@@ -0,0 +1,18 @@
+---
+source: crates/jrsonnet-formatter/src/tests.rs
+expression: reformat(&input)
+input_file: crates/jrsonnet-formatter/src/tests/operators.jsonnet
+---
+{
+   arithmetic: 1 + 2 * 3 - 4 / 2,
+   comparison: 1 < 2 && 3 > 2 || false,
+   string_concat: 'hello' + ' ' + 'world',
+   object_concat: { a: 1 } + { b: 2 },
+   array_concat: [
+      1,
+      2,
+   ] + [
+      3,
+      4,
+   ],
+}
addedcrates/jrsonnet-formatter/src/snapshots/jrsonnet_formatter__tests__snapshots@self_super.jsonnet.snapdiffbeforeafterboth
--- /dev/null
+++ b/crates/jrsonnet-formatter/src/snapshots/jrsonnet_formatter__tests__snapshots@self_super.jsonnet.snap
@@ -0,0 +1,14 @@
+---
+source: crates/jrsonnet-formatter/src/tests.rs
+expression: reformat(&input)
+input_file: crates/jrsonnet-formatter/src/tests/self_super.jsonnet
+---
+local base = {
+   foo: 'bar',
+   method(
+   ):: self.foo,
+};
+base {
+   foo: super.foo + '-extended',
+   result: self.method(),
+}
addedcrates/jrsonnet-formatter/src/snapshots/jrsonnet_formatter__tests__snapshots@std_functions.jsonnet.snapdiffbeforeafterboth
--- /dev/null
+++ b/crates/jrsonnet-formatter/src/snapshots/jrsonnet_formatter__tests__snapshots@std_functions.jsonnet.snap
@@ -0,0 +1,22 @@
+---
+source: crates/jrsonnet-formatter/src/tests.rs
+expression: reformat(&input)
+input_file: crates/jrsonnet-formatter/src/tests/std_functions.jsonnet
+---
+{
+   length: std.length(
+      [
+         1,
+         2,
+         3,
+      ],
+   ),
+   type: std.type('hello'),
+   format: std.format(
+      'Hello, %s!',
+      [
+         'world',
+      ],
+   ),
+   manifest: std.manifestJsonEx({ foo: 'bar' }, '  '),
+}
addedcrates/jrsonnet-formatter/src/snapshots/jrsonnet_formatter__tests__snapshots@string_styles.jsonnet.snapdiffbeforeafterboth
--- /dev/null
+++ b/crates/jrsonnet-formatter/src/snapshots/jrsonnet_formatter__tests__snapshots@string_styles.jsonnet.snap
@@ -0,0 +1,14 @@
+---
+source: crates/jrsonnet-formatter/src/tests.rs
+expression: reformat(&input)
+input_file: crates/jrsonnet-formatter/src/tests/string_styles.jsonnet
+---
+{
+   double_quote: 'hello world',
+   single_quote: 'hello world',
+   escaped: 'line1\nline2',
+   multiline: |||
+       This is a
+       multiline string
+     |||,
+}
modifiedcrates/jrsonnet-formatter/src/tests.rsdiffbeforeafterboth
--- a/crates/jrsonnet-formatter/src/tests.rs
+++ b/crates/jrsonnet-formatter/src/tests.rs
@@ -1,5 +1,10 @@
+#![cfg(test)]
+
+use std::fs;
+
 use dprint_core::formatting::{PrintItems, PrintOptions};
 use indoc::indoc;
+use insta::{assert_snapshot, glob};
 
 use crate::Printable;
 
@@ -13,155 +18,18 @@
 			out
 		},
 		PrintOptions {
-			indent_width: 2,
+			indent_width: 3,
 			max_width: 100,
-			use_tabs: true,
+			use_tabs: false,
 			new_line_text: "\n",
 		},
 	)
 }
 
 #[test]
-fn complex_comments() {
-	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: '',
-          },
-
-			 smallObjectWithEnding: {/*Ending comment*/},
-			 smallObjectWithFieldAndEnding: {a: 11/*Ending comment*/},
-			 smallObjectWithFieldAndEnding2: {/*Start*/a: 11/*Ending comment*/},
-        }"
-	)));
-}
-
-#[test]
-fn args() {
-	insta::assert_snapshot!(reformat(indoc!(
-		"
-			{
-				short: aaa(1,2,3,4,5),
-				long: bbb(123123123123123123123,12312312321123123123,123123123123312123123,123123123123123123312,123123123123312321123),
-				short_in_long: bbb(aaa(1,2,3,4,5), 123123123123123123123,12312312321123123123,123123123123312123123,123123123123123123312,123123123123312321123),
-				long_in_short: aaa(1,2,3,4,5,bbb(123123123123123123123,12312312321123123123,123123123123312123123,123123123123123123312,123123123123312321123)),
-			}
-		"
-	)));
-}
-
-#[test]
-fn asserts() {
-	insta::assert_snapshot!(reformat(indoc!(
-		"
-			{
-				assert 1 > 0 : 'one should be greater than zero',
-				assert true,
-				value: 42,
-			}
-		"
-	)));
-}
-
-#[test]
-fn complex_nested() {
-	insta::assert_snapshot!(reformat(indoc!(
-		"
-			{
-				kubernetes: {
-				 deployment: {
-					apiVersion: 'apps/v1',
-					kind: 'Deployment',
-					metadata: {
-					  name: 'myapp',
-					  labels: { app: 'myapp', version: 'v1' },
-					},
-					spec: {
-					  replicas: 3,
-					  selector: { matchLabels: { app: 'myapp' } },
-					  template: {
-						 metadata: { labels: { app: 'myapp' } },
-						 spec: {
-							containers: [
-							  {
-								 name: 'myapp',
-								 image: 'myapp:latest',
-								 ports: [{ containerPort: 8080 }],
-								 env: [
-									{ name: 'FOO', value: 'bar' },
-									{ name: 'BAZ', valueFrom: { secretKeyRef: { name: 'mysecret', key: 'password' } } },
-								 ],
-							  },
-							],
-						 },
-					  },
-					},
-				 },
-			  },
-			}
-		"
-	)));
-}
-
-#[test]
-fn self_super() {
-	insta::assert_snapshot!(reformat(indoc!(
-		"
-			local base = {
-			  foo: 'bar',
-			  method():: self.foo,
-			};
-
-			base {
-			  foo: super.foo + '-extended',
-			  result: self.method(),
-			}
-		"
-	)));
+fn snapshots() {
+	glob!("tests/*.jsonnet", |path| {
+		let input = fs::read_to_string(path).expect("read test file");
+		assert_snapshot!(reformat(&input));
+	});
 }
addedcrates/jrsonnet-formatter/src/tests/args.jsonnetdiffbeforeafterboth
--- /dev/null
+++ b/crates/jrsonnet-formatter/src/tests/args.jsonnet
@@ -0,0 +1,7 @@
+
+			{
+				short: aaa(1,2,3,4,5),
+				long: bbb(123123123123123123123,12312312321123123123,123123123123312123123,123123123123123123312,123123123123312321123),
+				short_in_long: bbb(aaa(1,2,3,4,5), 123123123123123123123,12312312321123123123,123123123123312123123,123123123123123123312,123123123123312321123),
+				long_in_short: aaa(1,2,3,4,5,bbb(123123123123123123123,12312312321123123123,123123123123312123123,123123123123123123312,123123123123312321123)),
+			}
addedcrates/jrsonnet-formatter/src/tests/asserts.jsonnetdiffbeforeafterboth
--- /dev/null
+++ b/crates/jrsonnet-formatter/src/tests/asserts.jsonnet
@@ -0,0 +1,6 @@
+
+			{
+				assert 1 > 0 : 'one should be greater than zero',
+				assert true,
+				value: 42,
+			}
addedcrates/jrsonnet-formatter/src/tests/basic_array.jsonnetdiffbeforeafterboth
--- /dev/null
+++ b/crates/jrsonnet-formatter/src/tests/basic_array.jsonnet
@@ -0,0 +1 @@
+[1, 2, 3, 4, 5]
addedcrates/jrsonnet-formatter/src/tests/basic_object.jsonnetdiffbeforeafterboth
--- /dev/null
+++ b/crates/jrsonnet-formatter/src/tests/basic_object.jsonnet
@@ -0,0 +1 @@
+{ foo: 'bar', baz: 'qux', nested: { a: 1, b: 2 } }
addedcrates/jrsonnet-formatter/src/tests/comments.jsonnetdiffbeforeafterboth
--- /dev/null
+++ b/crates/jrsonnet-formatter/src/tests/comments.jsonnet
@@ -0,0 +1,11 @@
+// File header comment
+{
+  // Comment above field
+  foo: 'bar',
+  /* Block comment */
+  baz: 'qux',
+  nested: {
+    // Hash comment
+    value: 42,
+  },
+}
addedcrates/jrsonnet-formatter/src/tests/complex_comments.jsonnetdiffbeforeafterboth
--- /dev/null
+++ b/crates/jrsonnet-formatter/src/tests/complex_comments.jsonnet
@@ -0,0 +1,55 @@
+{
+		  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: '',
+          },
+
+			 smallObjectWithEnding: {/*Ending comment*/},
+			 smallObjectWithFieldAndEnding: {a: 11/*Ending comment*/},
+			 smallObjectWithFieldAndEnding2: {/*Start*/a: 11/*Ending comment*/},
+        }
addedcrates/jrsonnet-formatter/src/tests/complex_nested.jsonnetdiffbeforeafterboth
--- /dev/null
+++ b/crates/jrsonnet-formatter/src/tests/complex_nested.jsonnet
@@ -0,0 +1,33 @@
+
+			{
+				kubernetes: {
+				 deployment: {
+					apiVersion: 'apps/v1',
+					kind: 'Deployment',
+					metadata: {
+					  name: 'myapp',
+					  labels: { app: 'myapp', version: 'v1' },
+					},
+					spec: {
+					  replicas: 3,
+					  selector: { matchLabels: { app: 'myapp' } },
+					  template: {
+						 metadata: { labels: { app: 'myapp' } },
+						 spec: {
+							containers: [
+							  {
+								 name: 'myapp',
+								 image: 'myapp:latest',
+								 ports: [{ containerPort: 8080 }],
+								 env: [
+									{ name: 'FOO', value: 'bar' },
+									{ name: 'BAZ', valueFrom: { secretKeyRef: { name: 'mysecret', key: 'password' } } },
+								 ],
+							  },
+							],
+						 },
+					  },
+					},
+				 },
+			  },
+			}
addedcrates/jrsonnet-formatter/src/tests/comprehensions.jsonnetdiffbeforeafterboth
--- /dev/null
+++ b/crates/jrsonnet-formatter/src/tests/comprehensions.jsonnet
@@ -0,0 +1,5 @@
+{
+  arr: [x for x in [1, 2, 3]],
+  filtered: [x for x in [1, 2, 3, 4, 5] if x > 2],
+  obj: { [k]: v for k in ['a', 'b'] for v in [1, 2] },
+}
addedcrates/jrsonnet-formatter/src/tests/conditionals.jsonnetdiffbeforeafterboth
--- /dev/null
+++ b/crates/jrsonnet-formatter/src/tests/conditionals.jsonnet
@@ -0,0 +1,7 @@
+{
+  simple: if true then 'yes' else 'no',
+  nested: if 1 > 0 then
+    if 2 > 1 then 'a' else 'b'
+  else
+    'c',
+}
addedcrates/jrsonnet-formatter/src/tests/functions.jsonnetdiffbeforeafterboth
--- /dev/null
+++ b/crates/jrsonnet-formatter/src/tests/functions.jsonnet
@@ -0,0 +1,10 @@
+{
+  simple(x):: x * 2,
+  with_default(x, y=10):: x + y,
+  multiline(
+    a,
+    b,
+    c,
+  ):: a + b + c,
+  called: self.simple(5),
+}
addedcrates/jrsonnet-formatter/src/tests/imports.jsonnetdiffbeforeafterboth
--- /dev/null
+++ b/crates/jrsonnet-formatter/src/tests/imports.jsonnet
@@ -0,0 +1,7 @@
+local a = import 'a.libsonnet';
+local m = import 'm.libsonnet';
+local z = import 'z.libsonnet';
+
+{
+  result: a + m + z,
+}
addedcrates/jrsonnet-formatter/src/tests/local_vars.jsonnetdiffbeforeafterboth
--- /dev/null
+++ b/crates/jrsonnet-formatter/src/tests/local_vars.jsonnet
@@ -0,0 +1,8 @@
+local x = 10;
+local y = 20;
+local sum = x + y;
+
+{
+  local inner = 5,
+  result: sum + inner,
+}
addedcrates/jrsonnet-formatter/src/tests/operators.jsonnetdiffbeforeafterboth
--- /dev/null
+++ b/crates/jrsonnet-formatter/src/tests/operators.jsonnet
@@ -0,0 +1,7 @@
+{
+  arithmetic: 1 + 2 * 3 - 4 / 2,
+  comparison: 1 < 2 && 3 > 2 || false,
+  string_concat: 'hello' + ' ' + 'world',
+  object_concat: { a: 1 } + { b: 2 },
+  array_concat: [1, 2] + [3, 4],
+}
addedcrates/jrsonnet-formatter/src/tests/self_super.jsonnetdiffbeforeafterboth
--- /dev/null
+++ b/crates/jrsonnet-formatter/src/tests/self_super.jsonnet
@@ -0,0 +1,9 @@
+local base = {
+  foo: 'bar',
+  method():: self.foo,
+};
+
+base {
+  foo: super.foo + '-extended',
+  result: self.method(),
+}
addedcrates/jrsonnet-formatter/src/tests/std_functions.jsonnetdiffbeforeafterboth
--- /dev/null
+++ b/crates/jrsonnet-formatter/src/tests/std_functions.jsonnet
@@ -0,0 +1,6 @@
+{
+  length: std.length([1, 2, 3]),
+  type: std.type('hello'),
+  format: std.format('Hello, %s!', ['world']),
+  manifest: std.manifestJsonEx({ foo: 'bar' }, '  '),
+}
addedcrates/jrsonnet-formatter/src/tests/string_styles.jsonnetdiffbeforeafterboth
--- /dev/null
+++ b/crates/jrsonnet-formatter/src/tests/string_styles.jsonnet
@@ -0,0 +1,9 @@
+{
+  double_quote: 'hello world',
+  single_quote: 'hello world',
+  escaped: 'line1\nline2',
+  multiline: |||
+    This is a
+    multiline string
+  |||,
+}
modifiedcrates/jrsonnet-rowan-parser/src/snapshots/jrsonnet_rowan_parser__tests__plain_call.snapdiffbeforeafterboth
--- a/crates/jrsonnet-rowan-parser/src/snapshots/jrsonnet_rowan_parser__tests__plain_call.snap
+++ b/crates/jrsonnet-rowan-parser/src/snapshots/jrsonnet_rowan_parser__tests__plain_call.snap
@@ -5,49 +5,49 @@
 SOURCE_FILE@0..37
   EXPR@0..36
     EXPR_BINARY@0..36
-      EXPR@0..3
+      EXPR@0..31
         EXPR_VAR@0..3
           NAME@0..3
             IDENT@0..3 "std"
-      SUFFIX_INDEX@3..10
-        DOT@3..4 "."
-        NAME@4..10
-          IDENT@4..10 "substr"
-      SUFFIX_APPLY@10..31
-        ARGS_DESC@10..31
-          L_PAREN@10..11 "("
-          ARG@11..12
-            EXPR@11..12
-              EXPR_VAR@11..12
-                NAME@11..12
-                  IDENT@11..12 "a"
-          COMMA@12..13 ","
-          WHITESPACE@13..14 " "
-          ARG@14..15
-            EXPR@14..15
-              EXPR_NUMBER@14..15
-                FLOAT@14..15 "0"
-          COMMA@15..16 ","
-          WHITESPACE@16..17 " "
-          ARG@17..30
-            EXPR@17..30
-              EXPR_VAR@17..20
-                NAME@17..20
-                  IDENT@17..20 "std"
-              SUFFIX_INDEX@20..27
-                DOT@20..21 "."
-                NAME@21..27
-                  IDENT@21..27 "length"
-              SUFFIX_APPLY@27..30
-                ARGS_DESC@27..30
-                  L_PAREN@27..28 "("
-                  ARG@28..29
-                    EXPR@28..29
-                      EXPR_VAR@28..29
-                        NAME@28..29
-                          IDENT@28..29 "b"
-                  R_PAREN@29..30 ")"
-          R_PAREN@30..31 ")"
+        SUFFIX_INDEX@3..10
+          DOT@3..4 "."
+          NAME@4..10
+            IDENT@4..10 "substr"
+        SUFFIX_APPLY@10..31
+          ARGS_DESC@10..31
+            L_PAREN@10..11 "("
+            ARG@11..12
+              EXPR@11..12
+                EXPR_VAR@11..12
+                  NAME@11..12
+                    IDENT@11..12 "a"
+            COMMA@12..13 ","
+            WHITESPACE@13..14 " "
+            ARG@14..15
+              EXPR@14..15
+                EXPR_NUMBER@14..15
+                  FLOAT@14..15 "0"
+            COMMA@15..16 ","
+            WHITESPACE@16..17 " "
+            ARG@17..30
+              EXPR@17..30
+                EXPR_VAR@17..20
+                  NAME@17..20
+                    IDENT@17..20 "std"
+                SUFFIX_INDEX@20..27
+                  DOT@20..21 "."
+                  NAME@21..27
+                    IDENT@21..27 "length"
+                SUFFIX_APPLY@27..30
+                  ARGS_DESC@27..30
+                    L_PAREN@27..28 "("
+                    ARG@28..29
+                      EXPR@28..29
+                        EXPR_VAR@28..29
+                          NAME@28..29
+                            IDENT@28..29 "b"
+                    R_PAREN@29..30 ")"
+            R_PAREN@30..31 ")"
       WHITESPACE@31..32 " "
       EQ@32..34 "=="
       WHITESPACE@34..35 " "
addedcrates/jrsonnet-rowan-parser/src/snapshots/jrsonnet_rowan_parser__tests__string_block_trim.snapdiffbeforeafterboth
--- /dev/null
+++ b/crates/jrsonnet-rowan-parser/src/snapshots/jrsonnet_rowan_parser__tests__string_block_trim.snap
@@ -0,0 +1,9 @@
+---
+source: crates/jrsonnet-rowan-parser/src/tests.rs
+expression: "|||-\n\tTrimmed text block\n|||\n"
+---
+SOURCE_FILE@0..29
+  EXPR@0..28
+    EXPR_STRING@0..28
+      STRING_BLOCK@0..28 "|||-\n\tTrimmed text bl ..."
+  WHITESPACE@28..29 "\n"
modifiedcrates/jrsonnet-rowan-parser/src/string_block.rsdiffbeforeafterboth
--- a/crates/jrsonnet-rowan-parser/src/string_block.rs
+++ b/crates/jrsonnet-rowan-parser/src/string_block.rs
@@ -52,6 +52,14 @@
 			self.rest().chars().next()
 		}
 
+		fn eat_if(&mut self, f: impl Fn(char) -> bool) -> usize {
+			if self.peek().map(f).unwrap_or(false) {
+				self.index += 1;
+				return 1;
+			}
+			0
+		}
+
 		fn eat_while(&mut self, f: impl Fn(char) -> bool) -> usize {
 			if self.index == self.source.len() {
 				return 0;
@@ -133,6 +141,8 @@
 		offset: lex.span().end,
 	};
 
+	ctx.eat_if(|v| v == '-');
+
 	// Skip whitespaces
 	ctx.eat_while(|r| r == ' ' || r == '\t' || r == '\r');
 
modifiedcrates/jrsonnet-rowan-parser/src/tests.rsdiffbeforeafterboth
--- a/crates/jrsonnet-rowan-parser/src/tests.rs
+++ b/crates/jrsonnet-rowan-parser/src/tests.rs
@@ -55,7 +55,6 @@
 				let src = indoc::indoc!($test);
 				let result = process(&src);
 				insta::assert_snapshot!(stringify!($name), result, src);
-
 			}
 		)+};
 	}
@@ -204,6 +203,12 @@
 	super_nesting => r#"
 		super.a + super.b
 	"#
+
+	string_block_trim => r#"
+		|||-
+			Trimmed text block
+		|||
+	"#
 );
 
 #[test]