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

difftreelog

feat(fmt) reformat text block

lvqwxktuYaroslav Bolyukin2026-02-12parent: #8bc6498.patch.diff
in: master

6 files changed

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		TextKind, 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;28mod tests;2930fn with_indent_eoi(cond: ConditionResolver, o: PrintItems, e: EndingComments) -> PrintItems {31	let end_comments_items = {32		let mut items = PrintItems::new();33		if e.should_start_with_newline {34			p!(&mut items, nl);35		}36		format_comments(&e.trivia, CommentLocation::EndOfItems, &mut items);37		items.into_rc_path()38	};39	let items =40		new_line_group(pi!(@i; items(o.into()) items(end_comments_items.into()))).into_rc_path();4142	let indented = with_indent(pi!(@i; nl items(items.into())));4344	pi!(@i; if_else("indented body", cond, items(indented))(str(" ") items(items.into())))45}4647pub trait Printable {48	fn print(&self, out: &mut PrintItems);49}5051macro_rules! pi {52	(@i; $($t:tt)*) => {{53		#[allow(unused_mut)]54		let mut o = dprint_core::formatting::PrintItems::new();55		pi!(@s; o: $($t)*);56		o57	}};58	(@s; $o:ident: str($e:expr $(,)?) $($t:tt)*) => {{59		$o.push_string($e.to_owned());60		pi!(@s; $o: $($t)*);61	}};62	(@s; $o:ident: string($e:expr $(,)?) $($t:tt)*) => {{63		$o.push_string($e);64		pi!(@s; $o: $($t)*);65	}};66	(@s; $o:ident: nl $($t:tt)*) => {{67		$o.push_signal(dprint_core::formatting::Signal::NewLine);68		pi!(@s; $o: $($t)*);69	}};70	(@s; $o:ident: sonl $($t:tt)*) => {{71		$o.push_signal(dprint_core::formatting::Signal::SpaceOrNewLine);72		pi!(@s; $o: $($t)*);73	}};74	(@s; $o:ident: tab $($t:tt)*) => {{75		$o.push_signal(dprint_core::formatting::Signal::Tab);76		pi!(@s; $o: $($t)*);77	}};78	(@s; $o:ident: >i $($t:tt)*) => {{79		$o.push_signal(dprint_core::formatting::Signal::StartIndent);80		pi!(@s; $o: $($t)*);81	}};82	(@s; $o:ident: <i $($t:tt)*) => {{83		$o.push_signal(dprint_core::formatting::Signal::FinishIndent);84		pi!(@s; $o: $($t)*);85	}};86	(@s; $o:ident: info($v:expr) $($t:tt)*) => {{87		$o.push_info($v);88		pi!(@s; $o: $($t)*);89	}};90	(@s; $o:ident: ln_anchor($v:expr) $($t:tt)*) => {{91		$o.push_anchor(LineNumberAnchor::new($v));92		pi!(@s; $o: $($t)*);93	}};94	(@s; $o:ident: if($s:literal, $cond:expr, $($i:tt)*) $($t:tt)*) => {{95		$o.push_condition(dprint_core::formatting::conditions::if_true(96			$s,97			$cond.clone(),98			{99				let mut o = PrintItems::new();100				p!(o, $($i)*);101				o102			},103		));104		pi!(@s; $o: $($t)*);105	}};106	(@s; $o:ident: if_else($s:literal, $cond:expr, $($i:tt)*)($($e:tt)+) $($t:tt)*) => {{107		$o.push_condition(dprint_core::formatting::conditions::if_true_or(108			$s,109			$cond.clone(),110			{111				let mut o = PrintItems::new();112				p!(o, $($i)*);113				o114			},115			{116				let mut o = PrintItems::new();117				p!(o, $($e)*);118				o119			},120		));121		pi!(@s; $o: $($t)*);122	}};123	(@s; $o:ident: if_not($s:literal, $cond:expr, $($e:tt)*) $($t:tt)*) => {{124		$o.push_condition(dprint_core::formatting::conditions::if_true_or(125			$s,126			$cond.clone(),127			{128				let o = PrintItems::new();129				o130			},131			{132				let mut o = PrintItems::new();133				p!(o, $($e)*);134				o135			},136		));137		pi!(@s; $o: $($t)*);138	}};139	(@s; $o:ident: {$expr:expr} $($t:tt)*) => {{140		$expr.print($o);141		pi!(@s; $o: $($t)*);142	}};143	(@s; $o:ident: items($expr:expr) $($t:tt)*) => {{144		$o.extend($expr);145		pi!(@s; $o: $($t)*);146	}};147	(@s; $o:ident: if ($e:expr)($($then:tt)*) $($t:tt)*) => {{148		if $e {149			pi!(@s; $o: $($then)*);150		}151		pi!(@s; $o: $($t)*);152	}};153	(@s; $o:ident: ifelse ($e:expr)($($then:tt)*)($($else:tt)*) $($t:tt)*) => {{154		if $e {155			pi!(@s; $o: $($then)*);156		} else {157			pi!(@s; $o: $($else)*);158		}159		pi!(@s; $o: $($t)*);160	}};161	(@s; $i:ident:) => {}162}163macro_rules! p {164	($o:ident, $($t:tt)*) => {165		pi!(@s; $o: $($t)*)166	};167	(&mut $o:ident, $($t:tt)*) => {168		let om = &mut $o;169		pi!(@s; om: $($t)*)170	};171}172pub(crate) use p;173pub(crate) use pi;174175impl<P> Printable for Option<P>176where177	P: Printable,178{179	fn print(&self, out: &mut PrintItems) {180		if let Some(v) = self {181			v.print(out);182		} else {183			p!(184				out,185				string(format!(186					"/*missing {}*/",187					type_name::<P>().replace("jrsonnet_rowan_parser::generated::nodes::", "")188				),)189			);190		}191	}192}193194impl Printable for SyntaxToken {195	fn print(&self, out: &mut PrintItems) {196		p!(out, string(self.to_string()));197	}198}199200impl Printable for Text {201	fn print(&self, out: &mut PrintItems) {202		if matches!(self.kind(), TextKind::StringBlock) {203			let text = self.text();204205			for (i, ele) in text.split("\n").enumerate() {206				if i != 0 {207					p!(out, nl);208				}209				// TODO: Trim and recreate whitespace210				p!(out, string(ele.to_string()));211			}212			return;213		}214		p!(out, string(format!("{}", self)));215	}216}217impl Printable for Number {218	fn print(&self, out: &mut PrintItems) {219		p!(out, string(format!("{}", self)));220	}221}222223impl Printable for Name {224	fn print(&self, out: &mut PrintItems) {225		p!(out, { self.ident_lit() });226	}227}228229impl Printable for DestructRest {230	fn print(&self, out: &mut PrintItems) {231		p!(out, str("..."));232		if let Some(name) = self.into() {233			p!(out, { name });234		}235	}236}237238impl Printable for Destruct {239	fn print(&self, out: &mut PrintItems) {240		match self {241			Self::DestructFull(f) => {242				p!(out, { f.name() });243			}244			Self::DestructSkip(_) => p!(out, str("?")),245			Self::DestructArray(a) => {246				p!(out, str("[") >i nl);247				for el in a.destruct_array_parts() {248					match el {249						DestructArrayPart::DestructArrayElement(e) => {250							p!(out, {e.destruct()} str(",") nl);251						}252						DestructArrayPart::DestructRest(d) => {253							p!(out, {d} str(",") nl);254						}255					}256				}257				p!(out, <i str("]"));258			}259			Self::DestructObject(o) => {260				p!(out, str("{") >i nl);261				for item in o.destruct_object_fields() {262					p!(out, { item.field() });263					if let Some(des) = item.destruct() {264						p!(out, str(": ") {des});265					}266					if let Some(def) = item.expr() {267						p!(out, str(" = ") {def});268					}269					p!(out, str(",") nl);270				}271				if let Some(rest) = o.destruct_rest() {272					p!(out, {rest} nl);273				}274				p!(out, <i str("}"));275			}276		}277	}278}279280impl Printable for FieldName {281	fn print(&self, out: &mut PrintItems) {282		match self {283			Self::FieldNameFixed(f) => {284				if let Some(id) = f.id() {285					p!(out, { id });286				} else if let Some(str) = f.text() {287					p!(out, { str });288				} else {289					p!(out, str("/*missing FieldName*/"));290				}291			}292			Self::FieldNameDynamic(d) => {293				p!(out, str("[") {d.expr()} str("]"));294			}295		}296	}297}298299impl Printable for Visibility {300	fn print(&self, out: &mut PrintItems) {301		p!(out, string(self.to_string()));302	}303}304305impl Printable for ObjLocal {306	fn print(&self, out: &mut PrintItems) {307		p!(out, str("local ") {self.bind()});308	}309}310311impl Printable for Assertion {312	fn print(&self, out: &mut PrintItems) {313		p!(out, str("assert ") {self.condition()});314		if self.colon_token().is_some() || self.message().is_some() {315			p!(out, str(": ") {self.message()});316		}317	}318}319320impl Printable for ParamsDesc {321	fn print(&self, out: &mut PrintItems) {322		p!(out, str("(") >i nl);323		for param in self.params() {324			p!(out, { param.destruct() });325			if param.assign_token().is_some() || param.expr().is_some() {326				p!(out, str(" = ") {param.expr()});327			}328			p!(out, str(",") nl);329		}330		p!(out, <i str(")"));331	}332}333impl Printable for ArgsDesc {334	fn print(&self, out: &mut PrintItems) {335		let start = LineNumber::new("args start line");336		let end = LineNumber::new("args end line");337		let multi_line = Rc::new(move |condition_context: &mut ConditionResolverContext| {338			is_multiple_lines(condition_context, start, end)339		});340341		let (children, end_comments) = children_between::<Arg>(342			self.syntax().clone(),343			self.l_paren_token().map(Into::into).as_ref(),344			self.r_paren_token().map(Into::into).as_ref(),345			None,346		);347348		fn gen_args(children: Vec<Child<Arg>>, multi_line: ConditionResolver) -> PrintItems {349			let mut _out = PrintItems::new();350			let out = &mut _out;351352			let mut args = children.into_iter().peekable();353			while let Some(ele) = args.next() {354				if ele.should_start_with_newline {355					p!(out, nl);356				}357				format_comments(&ele.before_trivia, CommentLocation::AboveItem, out);358				let arg = ele.value;359				if arg.name().is_some() || arg.assign_token().is_some() {360					p!(out, {arg.name()} str(" = "));361				}362				p!(out, { arg.expr() });363				let has_more = args.peek().is_some();364				if has_more {365					p!(out, str(","));366				} else {367					p!(out, if("trailing comma", multi_line, str(",")));368				}369				format_comments(&ele.inline_trivia, CommentLocation::ItemInline, out);370				if has_more {371					p!(out, if_else("arg separator", multi_line, nl)(sonl));372				}373			}374			_out375		}376377		let args_items = new_line_group(gen_args(children, multi_line.clone())).into_rc_path();378		let args_indented = with_indent(pi!(@i; nl items(args_items.into())));379380		p!(out, str("(") info(start));381		p!(out, if_else("args body", multi_line, items(args_indented) nl)(items(args_items.into())));382		if end_comments.should_start_with_newline {383			p!(out, nl);384		}385		format_comments(&end_comments.trivia, CommentLocation::EndOfItems, out);386		p!(out, str(")") info(end));387	}388}389impl Printable for SliceDesc {390	fn print(&self, out: &mut PrintItems) {391		p!(out, str("["));392		if self.from().is_some() {393			p!(out, { self.from() });394		}395		p!(out, str(":"));396		if self.end().is_some() {397			p!(out, { self.end().map(|e| e.expr()) });398		}399		// Keep only one : in case if we don't need step400		if self.step().is_some() {401			p!(out, str(":") {self.step().map(|e|e.expr())});402		}403		p!(out, str("]"));404	}405}406407impl Printable for Member {408	fn print(&self, out: &mut PrintItems) {409		match self {410			Self::MemberBindStmt(b) => {411				p!(out, { b.obj_local() });412			}413			Self::MemberAssertStmt(ass) => {414				p!(out, { ass.assertion() });415			}416			Self::MemberFieldNormal(n) => {417				p!(out, {n.field_name()} if(n.plus_token().is_some())({n.plus_token()}) {n.visibility()} str(" ") {n.expr()});418			}419			Self::MemberFieldMethod(m) => {420				p!(out, {m.field_name()} {m.params_desc()} {m.visibility()} str(" ") {m.expr()});421			}422		}423	}424}425426impl Printable for ObjBody {427	fn print(&self, out: &mut PrintItems) {428		match self {429			Self::ObjBodyComp(l) => {430				let (children, mut end_comments) = children_between::<Member>(431					l.syntax().clone(),432					l.l_brace_token().map(Into::into).as_ref(),433					Some(434						&(l.comp_specs()435							.next()436							.expect("at least one spec is defined")437							.syntax()438							.clone())439						.into(),440					),441					None,442				);443				let trailing_for_comp = end_comments.extract_trailing();444				p!(out, str("{") >i nl);445				for mem in children {446					if mem.should_start_with_newline {447						p!(out, nl);448					}449					format_comments(&mem.before_trivia, CommentLocation::AboveItem, out);450					p!(out, {mem.value} str(","));451					format_comments(&mem.inline_trivia, CommentLocation::ItemInline, out);452					p!(out, nl);453				}454455				if end_comments.should_start_with_newline {456					p!(out, nl);457				}458				format_comments(&end_comments.trivia, CommentLocation::EndOfItems, out);459460				let (compspecs, end_comments) = children_between::<CompSpec>(461					l.syntax().clone(),462					l.member_comps()463						.last()464						.map(|m| m.syntax().clone())465						.map(Into::into)466						.or_else(|| l.l_brace_token().map(Into::into))467						.as_ref(),468					l.r_brace_token().map(Into::into).as_ref(),469					Some(trailing_for_comp),470				);471				for mem in compspecs {472					if mem.should_start_with_newline {473						p!(out, nl);474					}475					format_comments(&mem.before_trivia, CommentLocation::AboveItem, out);476					p!(out, { mem.value });477					format_comments(&mem.inline_trivia, CommentLocation::ItemInline, out);478				}479				if end_comments.should_start_with_newline {480					p!(out, nl);481				}482				format_comments(&end_comments.trivia, CommentLocation::EndOfItems, out);483484				p!(out, nl <i str("}"));485			}486			Self::ObjBodyMemberList(l) => {487				let (children, end_comments) = children_between::<Member>(488					l.syntax().clone(),489					l.l_brace_token().map(Into::into).as_ref(),490					l.r_brace_token().map(Into::into).as_ref(),491					None,492				);493				if children.is_empty() && end_comments.is_empty() {494					p!(out, str("{ }"));495					return;496				}497498				let source_is_multiline = children.iter().any(|c| c.triggers_multiline)499					|| end_comments.should_start_with_newline;500501				let start = LineNumber::new("obj start line");502				let end = LineNumber::new("obj end line");503				let multi_line: ConditionResolver = if source_is_multiline {504					true_resolver()505				} else {506					Rc::new(move |ctx: &mut ConditionResolverContext| {507						is_multiple_lines(ctx, start, end)508					})509				};510511				fn gen_members(512					children: Vec<Child<Member>>,513					multi_line: ConditionResolver,514				) -> PrintItems {515					let mut _out = PrintItems::new();516					let out = &mut _out;517					let mut members = children.into_iter().peekable();518					while let Some(mem) = members.next() {519						if mem.should_start_with_newline {520							p!(out, nl);521						}522						format_comments(&mem.before_trivia, CommentLocation::AboveItem, out);523						p!(out, { mem.value });524						let has_more = members.peek().is_some();525						if has_more {526							p!(out, str(","));527						} else {528							p!(out, if("trailing comma", multi_line, str(",")));529						}530						format_comments(&mem.inline_trivia, CommentLocation::ItemInline, out);531						p!(out, if_else("member separator", multi_line, nl)(sonl));532					}533					_out534				}535536				let members_items =537					new_line_group(gen_members(children, multi_line.clone())).into_rc_path();538539				let members = with_indent_eoi(multi_line, members_items.into(), end_comments);540541				p!(out, str("{") info(start));542				p!(out, items(members));543				p!(out, str("}") info(end));544			}545		}546	}547}548impl Printable for UnaryOperator {549	fn print(&self, out: &mut PrintItems) {550		p!(out, string(self.text().to_string()));551	}552}553impl Printable for BinaryOperator {554	fn print(&self, out: &mut PrintItems) {555		p!(out, string(self.text().to_string()));556	}557}558impl Printable for Bind {559	fn print(&self, out: &mut PrintItems) {560		match self {561			Self::BindDestruct(d) => {562				p!(out, {d.into()} str(" = ") {d.value()});563			}564			Self::BindFunction(f) => {565				p!(out, {f.name()} {f.params()} str(" = ") {f.value()});566			}567		}568	}569}570impl Printable for Literal {571	fn print(&self, out: &mut PrintItems) {572		p!(out, string(self.syntax().to_string()));573	}574}575impl Printable for ImportKind {576	fn print(&self, out: &mut PrintItems) {577		p!(out, string(self.syntax().to_string()));578	}579}580impl Printable for ForSpec {581	fn print(&self, out: &mut PrintItems) {582		p!(out, str("for ") {self.bind()} str(" in ") {self.expr()});583	}584}585impl Printable for IfSpec {586	fn print(&self, out: &mut PrintItems) {587		p!(out, str("if ") {self.expr()});588	}589}590impl Printable for CompSpec {591	fn print(&self, out: &mut PrintItems) {592		match self {593			Self::ForSpec(f) => f.print(out),594			Self::IfSpec(i) => i.print(out),595		}596	}597}598impl Printable for Expr {599	fn print(&self, out: &mut PrintItems) {600		let (stmts, _ending) = children_between::<Stmt>(601			self.syntax().clone(),602			None,603			self.expr_base()604				.as_ref()605				.map(ExprBase::syntax)606				.cloned()607				.map(Into::into)608				.as_ref(),609			None,610		);611		for stmt in stmts {612			p!(out, { stmt.value });613		}614		p!(out, { self.expr_base() });615		let (suffixes, _ending) = children_between::<Suffix>(616			self.syntax().clone(),617			self.expr_base()618				.as_ref()619				.map(ExprBase::syntax)620				.cloned()621				.map(Into::into)622				.as_ref(),623			None,624			None,625		);626		for suffix in suffixes {627			p!(out, { suffix.value });628		}629	}630}631impl Printable for Suffix {632	fn print(&self, out: &mut PrintItems) {633		match self {634			Self::SuffixIndex(i) => {635				if i.question_mark_token().is_some() {636					p!(out, str("?"));637				}638				p!(out, str(".") {i.index()});639			}640			Self::SuffixIndexExpr(e) => {641				if e.question_mark_token().is_some() {642					p!(out, str(".?"));643				}644				p!(out, str("[") {e.index()} str("]"));645			}646			Self::SuffixSlice(d) => {647				p!(out, { d.slice_desc() });648			}649			Self::SuffixApply(a) => {650				p!(out, { a.args_desc() });651			}652		}653	}654}655impl Printable for Stmt {656	fn print(&self, out: &mut PrintItems) {657		match self {658			Self::StmtLocal(l) => {659				let (binds, end_comments) = children_between::<Bind>(660					l.syntax().clone(),661					l.local_kw_token().map(Into::into).as_ref(),662					l.semi_token().map(Into::into).as_ref(),663					None,664				);665				if binds.len() == 1 {666					let bind = &binds[0];667					format_comments(&bind.before_trivia, CommentLocation::AboveItem, out);668					p!(out, str("local ") {bind.value});669				// TODO: keep end_comments, child.inline_trivia somehow, force multiple locals formatting in case of presence?670				} else {671					p!(out,str("local") >i nl);672					for bind in binds {673						if bind.should_start_with_newline {674							p!(out, nl);675						}676						format_comments(&bind.before_trivia, CommentLocation::AboveItem, out);677						p!(out, {bind.value} str(","));678						format_comments(&bind.inline_trivia, CommentLocation::ItemInline, out);679						p!(out, nl);680					}681					if end_comments.should_start_with_newline {682						p!(out, nl);683					}684					format_comments(&end_comments.trivia, CommentLocation::EndOfItems, out);685					p!(out,<i);686				}687				p!(out,str(";") nl);688			}689			Self::StmtAssert(a) => {690				p!(out, {a.assertion()} str(";") nl);691			}692		}693	}694}695impl Printable for ExprBase {696	fn print(&self, out: &mut PrintItems) {697		match self {698			Self::ExprBinary(b) => {699				p!(out, {b.lhs()} str(" ") {b.binary_operator()} str(" ") {b.rhs()});700			}701			Self::ExprUnary(u) => p!(out, {u.unary_operator()} {u.rhs()}),702			// Self::ExprSlice(s) => {703			// 	p!(new: {s.expr()} {s.slice_desc()})704			// }705			// Self::ExprIndex(i) => {706			// 	p!(new: {i.expr()} str(".") {i.index()})707			// }708			// Self::ExprIndexExpr(i) => p!(new: {i.base()} str("[") {i.index()} str("]")),709			// Self::ExprApply(a) => {710			// 	let mut pi = p!(new: {a.expr()} {a.args_desc()});711			// 	if a.tailstrict_kw_token().is_some() {712			// 		p!(out,str(" tailstrict"));713			// 	}714			// 	pi715			// }716			Self::ExprObjExtend(ex) => {717				p!(out, {ex.lhs_work()} str(" ") {ex.rhs_work()});718			}719			Self::ExprParened(p) => {720				p!(out, str("(") {p.expr()} str(")"));721			}722			Self::ExprString(s) => p!(out, { s.text() }),723			Self::ExprNumber(n) => p!(out, { n.number() }),724			Self::ExprArray(a) => {725				p!(out, str("[") >i nl);726				for el in a.exprs() {727					p!(out, {el} str(",") nl);728				}729				p!(out, <i str("]"));730			}731			Self::ExprObject(obj) => {732				p!(out, { obj.obj_body() });733			}734			Self::ExprArrayComp(arr) => {735				p!(out, str("[") {arr.expr()});736				for spec in arr.comp_specs() {737					p!(out, str(" ") {spec});738				}739				p!(out, str("]"));740			}741			Self::ExprImport(v) => {742				p!(out, {v.import_kind()} str(" ") {v.text()});743			}744			Self::ExprVar(n) => p!(out, { n.name() }),745			// Self::ExprLocal(l) => {746			// }747			Self::ExprIfThenElse(ite) => {748				p!(out, str("if ") {ite.cond()} str(" then ") {ite.then().map(|t| t.expr())});749				if ite.else_kw_token().is_some() || ite.else_().is_some() {750					p!(out, str(" else ") {ite.else_().map(|t| t.expr())});751				}752			}753			Self::ExprFunction(f) => p!(out, str("function") {f.params_desc()} nl {f.expr()}),754			// Self::ExprAssert(a) => p!(new: {a.assertion()} str("; ") {a.expr()}),755			Self::ExprError(e) => p!(out, str("error ") {e.expr()}),756			Self::ExprLiteral(l) => {757				p!(out, { l.literal() });758			}759		}760	}761}762763impl Printable for SourceFile {764	fn print(&self, out: &mut PrintItems) {765		let before = trivia_before(766			self.syntax().clone(),767			self.expr()768				.map(|e| e.syntax().clone())769				.map(Into::into)770				.as_ref(),771		);772		let after = trivia_after(773			self.syntax().clone(),774			self.expr()775				.map(|e| e.syntax().clone())776				.map(Into::into)777				.as_ref(),778		);779		format_comments(&before, CommentLocation::AboveItem, out);780		p!(out, {self.expr()} nl);781		format_comments(&after, CommentLocation::EndOfItems, out);782	}783}784785pub struct FormatOptions {786	// 0 for hard tabs787	pub indent: u8,788}789pub fn format(input: &str, opts: &FormatOptions) -> Result<String, SnippetBuilder> {790	let (parsed, errors) = jrsonnet_rowan_parser::parse(input);791	if !errors.is_empty() {792		let mut builder = hi_doc::SnippetBuilder::new(input);793		for error in errors {794			builder795				.error(hi_doc::Text::fragment(796					format!("{:?}", error.error),797					Formatting::default(),798				))799				.range(800					error.range.start().into()801						..=(usize::from(error.range.end()) - 1).max(error.range.start().into()),802				)803				.build();804		}805		// let snippet = builder.build();806		return Err(builder);807		// It is possible to recover from this failure, but the output may be broken, as formatter is free to skip808		// ERROR rowan nodes.809		// Recovery needs to be enabled for LSP, though.810	}811	Ok(dprint_core::formatting::format(812		|| {813			let mut out = PrintItems::new();814			parsed.print(&mut out);815			out816		},817		PrintOptions {818			indent_width: if opts.indent == 0 {819				// Reasonable max length for both 2 and 4 space sized tabs.820				3821			} else {822				opts.indent823			},824			max_width: 100,825			use_tabs: opts.indent == 0,826			new_line_text: "\n",827		},828	))829}
modifiedcrates/jrsonnet-formatter/src/snapshots/jrsonnet_formatter__tests__snapshots@string_styles.jsonnet.snapdiffbeforeafterboth
--- a/crates/jrsonnet-formatter/src/snapshots/jrsonnet_formatter__tests__snapshots@string_styles.jsonnet.snap
+++ b/crates/jrsonnet-formatter/src/snapshots/jrsonnet_formatter__tests__snapshots@string_styles.jsonnet.snap
@@ -8,7 +8,18 @@
    single_quote: 'hello world',
    escaped: 'line1\nline2',
    multiline: |||
-       This is a
-       multiline string
-     |||,
+      This is a
+
+      multiline string
+   |||,
+   multiline_truncated: |||-
+      This is a
+
+      multiline string with truncated newline
+   |||,
+   multiline_to_truncated: |||
+      This is a
+
+      multiline string with to-be truncated newline
+   |||,
 }
modifiedcrates/jrsonnet-formatter/src/tests.rsdiffbeforeafterboth
--- a/crates/jrsonnet-formatter/src/tests.rs
+++ b/crates/jrsonnet-formatter/src/tests.rs
@@ -3,7 +3,6 @@
 use std::fs;
 
 use dprint_core::formatting::{PrintItems, PrintOptions};
-use indoc::indoc;
 use insta::{assert_snapshot, glob};
 
 use crate::Printable;
modifiedcrates/jrsonnet-formatter/src/tests/string_styles.jsonnetdiffbeforeafterboth
--- a/crates/jrsonnet-formatter/src/tests/string_styles.jsonnet
+++ b/crates/jrsonnet-formatter/src/tests/string_styles.jsonnet
@@ -4,6 +4,18 @@
   escaped: 'line1\nline2',
   multiline: |||
     This is a
+
     multiline string
   |||,
+  multiline_truncated: |||-
+    This is a
+
+    multiline string with truncated newline
+  |||,
+  multiline_to_truncated: |||-
+    This is a
+
+    multiline string with to-be truncated newline
+
+  |||,
 }
modifiedcrates/jrsonnet-rowan-parser/src/lib.rsdiffbeforeafterboth
--- a/crates/jrsonnet-rowan-parser/src/lib.rs
+++ b/crates/jrsonnet-rowan-parser/src/lib.rs
@@ -22,6 +22,7 @@
 pub use generated::{nodes, syntax_kinds::SyntaxKind};
 pub use language::*;
 pub use token_set::SyntaxKindSet;
+pub use string_block::{collect_lexed_str_block, CollectStrBlock};
 
 use self::{
 	ast::support,
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
@@ -6,142 +6,193 @@
 	MissingIndent,
 }
 
-use std::ops::Range;
-
 use logos::Lexer;
 use StringBlockError::*;
 
 use crate::SyntaxKind;
 
-pub fn lex_str_block_test(lex: &mut Lexer<SyntaxKind>) {
+pub(crate) fn lex_str_block_test<'d>(lex: &mut Lexer<'d, SyntaxKind>) {
 	let _ = lex_str_block(lex);
 }
 
-#[allow(clippy::too_many_lines)]
-pub fn lex_str_block(lex: &mut Lexer<SyntaxKind>) -> Result<(), StringBlockError> {
-	struct Context<'a> {
-		source: &'a str,
-		index: usize,
-		offset: usize,
+pub(crate) struct Context<'a> {
+	source: &'a str,
+	index: usize,
+}
+
+impl<'a> Context<'a> {
+	fn rest(&self) -> &'a str {
+		&self.source[self.index..]
 	}
 
-	impl<'a> Context<'a> {
-		fn rest(&self) -> &'a str {
-			&self.source[self.index..]
+	fn next(&mut self) -> Option<char> {
+		if self.index == self.source.len() {
+			return None;
 		}
 
-		fn next(&mut self) -> Option<char> {
-			if self.index == self.source.len() {
-				return None;
+		match self.rest().chars().next() {
+			None => None,
+			Some(c) => {
+				self.index += c.len_utf8();
+				Some(c)
 			}
+		}
+	}
 
-			match self.rest().chars().next() {
-				None => None,
-				Some(c) => {
-					self.index += c.len_utf8();
-					Some(c)
-				}
-			}
+	fn peek(&self) -> Option<char> {
+		if self.index == self.source.len() {
+			return None;
 		}
 
-		fn peek(&self) -> Option<char> {
-			if self.index == self.source.len() {
-				return None;
-			}
+		self.rest().chars().next()
+	}
 
-			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_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;
 		}
 
-		fn eat_while(&mut self, f: impl Fn(char) -> bool) -> usize {
-			if self.index == self.source.len() {
-				return 0;
-			}
+		let next_char = self.rest().char_indices().find(|(_, c)| !f(*c));
 
-			let next_char = self.rest().char_indices().find(|(_, c)| !f(*c));
-
-			match next_char {
-				None => {
-					let diff = self.source.len() - self.index;
-					self.index = self.source.len();
-					diff
-				}
-				Some((idx, _)) => {
-					self.index += idx;
-					idx
-				}
+		match next_char {
+			None => {
+				let diff = self.source.len() - self.index;
+				self.index = self.source.len();
+				diff
 			}
-		}
-
-		fn skip(&mut self, len: usize) {
-			self.index = match self.index + len {
-				n if n > self.source.len() => self.source.len(),
-				n => n,
-			};
-		}
-
-		#[allow(clippy::range_plus_one)]
-		fn pos(&self) -> Range<usize> {
-			if self.index == self.source.len() {
-				self.offset + self.index..self.offset + self.index
-			} else {
-				// TODO: char size
-				self.offset + self.index..self.offset + self.index + 1
+			Some((idx, _)) => {
+				self.index += idx;
+				idx
 			}
 		}
 	}
 
-	// Check that b has at least the same whitespace prefix as a and returns the
-	// amount of this whitespace, otherwise returns 0.  If a has no whitespace
-	// prefix than return 0.
-	fn check_whitespace(a: &str, b: &str) -> usize {
-		let a = a.as_bytes();
-		let b = b.as_bytes();
+	fn skip(&mut self, len: usize) {
+		self.index = match self.index + len {
+			n if n > self.source.len() => self.source.len(),
+			n => n,
+		};
+	}
+}
 
-		for i in 0..a.len() {
-			if a[i] != b' ' && a[i] != b'\t' {
-				// a has run out of whitespace and b matched up to this point. Return result.
-				return i;
-			}
+// Check that b has at least the same whitespace prefix as a and returns the
+// amount of this whitespace, otherwise returns 0.  If a has no whitespace
+// prefix than return 0.
+fn check_whitespace(a: &str, b: &str) -> usize {
+	let a = a.as_bytes();
+	let b = b.as_bytes();
 
-			if i >= b.len() {
-				// We ran off the edge of b while a still has whitespace. Return 0 as failure.
-				return 0;
-			}
+	for i in 0..a.len() {
+		if a[i] != b' ' && a[i] != b'\t' {
+			// a has run out of whitespace and b matched up to this point. Return result.
+			return i;
+		}
 
-			if a[i] != b[i] {
-				// a has whitespace but b does not. Return 0 as failure.
-				return 0;
-			}
+		if i >= b.len() {
+			// We ran off the edge of b while a still has whitespace. Return 0 as failure.
+			return 0;
 		}
 
-		// We ran off the end of a and b kept up
-		a.len()
+		if a[i] != b[i] {
+			// a has whitespace but b does not. Return 0 as failure.
+			return 0;
+		}
 	}
 
-	fn guess_token_end_and_bump<'a>(lex: &mut Lexer<'a, SyntaxKind>, ctx: &Context<'a>) {
+	// We ran off the end of a and b kept up
+	a.len()
+}
+
+pub(crate) trait StrBlockLexCtx<'d> {
+	fn remainder(&self) -> &'d str;
+	fn eat_error(&mut self, ctx: &Context<'d>);
+	fn bump_pos(&mut self, s: usize);
+	fn mark_truncating(&mut self);
+	fn mark_line(&mut self, line: &'d str);
+}
+
+impl<'d> StrBlockLexCtx<'d> for Lexer<'d, SyntaxKind> {
+	fn remainder(&self) -> &'d str {
+		self.remainder()
+	}
+	fn eat_error(&mut self, ctx: &Context<'d>) {
 		let end_index = ctx
 			.rest()
 			.find("|||")
 			.map_or_else(|| ctx.rest().len(), |v| v + 3);
-		lex.bump(ctx.index + end_index);
+		self.bump(ctx.index + end_index);
 	}
+	fn bump_pos(&mut self, s: usize) {
+		self.bump(s);
+	}
+	fn mark_truncating(&mut self) {
+		// Lexer test doesn't collect anything
+	}
+	fn mark_line(&mut self, _line: &'d str) {
+		// Lexer test doesn't collect anything
+	}
+}
 
-	debug_assert_eq!(lex.slice(), "|||");
-	let mut ctx = Context {
+pub fn collect_lexed_str_block<'s>(
+	input: &'s str,
+) -> Result<CollectStrBlock<'s>, StringBlockError> {
+	let mut collect = CollectStrBlock {
+		truncate: false,
+		lines: vec![],
+		input,
+		offset: 0,
+	};
+	lex_str_block(&mut collect)?;
+	Ok(collect)
+}
+
+pub struct CollectStrBlock<'s> {
+	pub truncate: bool,
+	pub lines: Vec<&'s str>,
+	input: &'s str,
+	offset: usize,
+}
+
+impl<'d> StrBlockLexCtx<'d> for CollectStrBlock<'d> {
+	fn remainder(&self) -> &'d str {
+		self.input
+	}
+
+	fn eat_error(&mut self, _ctx: &Context<'d>) {
+		// Error will be returned, no need to record it here
+	}
+
+	fn bump_pos(&mut self, s: usize) {
+		self.offset += s;
+	}
+
+	fn mark_truncating(&mut self) {
+		self.truncate = true;
+	}
+
+	fn mark_line(&mut self, line: &'d str) {
+		self.lines.push(line)
+	}
+}
+
+pub(crate) fn lex_str_block<'a>(lex: &mut impl StrBlockLexCtx<'a>) -> Result<(), StringBlockError> {
+	// debug_assert_eq!(lex.slice(), "|||");
+	let mut ctx = Context::<'a> {
 		source: lex.remainder(),
 		index: 0,
-		offset: lex.span().end,
 	};
 
-	ctx.eat_if(|v| v == '-');
+	if ctx.eat_if(|v| v == '-') != 0 {
+		lex.mark_truncating();
+	}
 
 	// Skip whitespaces
 	ctx.eat_while(|r| r == ' ' || r == '\t' || r == '\r');
@@ -150,12 +201,12 @@
 	match ctx.next() {
 		Some('\n') => (),
 		None => {
-			guess_token_end_and_bump(lex, &ctx);
+			lex.eat_error(&ctx);
 			return Err(UnexpectedEnd);
 		}
 		// Text block requires new line after |||.
 		Some(_) => {
-			guess_token_end_and_bump(lex, &ctx);
+			lex.eat_error(&ctx);
 			return Err(MissingNewLine);
 		}
 	}
@@ -170,7 +221,7 @@
 
 	if num_whitespace == 0 {
 		// Text block's first line must start with whitespace
-		guess_token_end_and_bump(lex, &ctx);
+		lex.eat_error(&ctx);
 		return Err(MissingIndent);
 	}
 
@@ -178,19 +229,27 @@
 		debug_assert_ne!(num_whitespace, 0, "Unexpected value for num_whitespace");
 		ctx.skip(num_whitespace);
 
+		let line_start = ctx.index;
+		let mut line_size = 0;
 		loop {
 			match ctx.next() {
 				None => {
-					guess_token_end_and_bump(lex, &ctx);
+					lex.eat_error(&ctx);
 					return Err(UnexpectedEnd);
 				}
-				Some('\n') => break,
-				Some(_) => (),
+				Some('\n') => {
+					lex.mark_line(&ctx.source[line_start..line_start + line_size]);
+					break;
+				}
+				Some(c) => {
+					line_size += c.len_utf8();
+				}
 			}
 		}
 
 		// Skip any blank lines
 		while ctx.peek() == Some('\n') {
+			lex.mark_line("");
 			ctx.next();
 		}
 
@@ -206,15 +265,11 @@
 			}
 
 			if !ctx.rest().starts_with("|||") {
-				// Text block not terminated with |||
-				let pos = ctx.pos();
-				if pos.is_empty() {
-					// eof
-					lex.bump(ctx.index);
+				if ctx.rest().is_empty() {
+					lex.bump_pos(ctx.index);
 					return Err(UnexpectedEnd);
 				}
-
-				guess_token_end_and_bump(lex, &ctx);
+				lex.eat_error(&ctx);
 				return Err(MissingTermination);
 			}
 
@@ -224,6 +279,6 @@
 		}
 	}
 
-	lex.bump(ctx.index);
+	lex.bump_pos(ctx.index);
 	Ok(())
 }