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
--- a/crates/jrsonnet-formatter/src/lib.rs
+++ b/crates/jrsonnet-formatter/src/lib.rs
@@ -9,6 +9,7 @@
 };
 use hi_doc::{Formatting, SnippetBuilder};
 use jrsonnet_rowan_parser::{
+	collect_lexed_str_block,
 	nodes::{
 		Arg, ArgsDesc, Assertion, BinaryOperator, Bind, CompSpec, Destruct, DestructArrayPart,
 		DestructRest, Expr, ExprBase, FieldName, ForSpec, IfSpec, ImportKind, Literal, Member,
@@ -83,6 +84,14 @@
 		$o.push_signal(dprint_core::formatting::Signal::FinishIndent);
 		pi!(@s; $o: $($t)*);
 	}};
+	(@s; $o:ident: >ii $($t:tt)*) => {{
+		$o.push_signal(dprint_core::formatting::Signal::StartIgnoringIndent);
+		pi!(@s; $o: $($t)*);
+	}};
+	(@s; $o:ident: <ii $($t:tt)*) => {{
+		$o.push_signal(dprint_core::formatting::Signal::FinishIgnoringIndent);
+		pi!(@s; $o: $($t)*);
+	}};
 	(@s; $o:ident: info($v:expr) $($t:tt)*) => {{
 		$o.push_info($v);
 		pi!(@s; $o: $($t)*);
@@ -201,14 +210,28 @@
 	fn print(&self, out: &mut PrintItems) {
 		if matches!(self.kind(), TextKind::StringBlock) {
 			let text = self.text();
+			let mut text = collect_lexed_str_block(&text[3..])
+				.expect("formatting is not performed on code with parsing errors");
 
-			for (i, ele) in text.split("\n").enumerate() {
-				if i != 0 {
-					p!(out, nl);
+			if text.truncate && text.lines.ends_with(&[""]) {
+				text.truncate = false;
+				text.lines.pop();
+			}
+
+			p!(out, str("|||"));
+			if text.truncate {
+				p!(out, str("-"));
+			}
+			p!(out, nl > i);
+			for ele in text.lines {
+				if ele.is_empty() {
+					p!(out, >ii nl <ii);
+				} else {
+					p!(out, string(ele.to_string()) nl);
 				}
-				// TODO: Trim and recreate whitespace
-				p!(out, string(ele.to_string()));
 			}
+			p!(out, <i str("|||"));
+
 			return;
 		}
 		p!(out, string(format!("{}", self)));
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
before · crates/jrsonnet-rowan-parser/src/string_block.rs
1#[derive(Clone, Copy, Debug, PartialEq, Eq)]2pub enum StringBlockError {3	UnexpectedEnd,4	MissingNewLine,5	MissingTermination,6	MissingIndent,7}89use std::ops::Range;1011use logos::Lexer;12use StringBlockError::*;1314use crate::SyntaxKind;1516pub fn lex_str_block_test(lex: &mut Lexer<SyntaxKind>) {17	let _ = lex_str_block(lex);18}1920#[allow(clippy::too_many_lines)]21pub fn lex_str_block(lex: &mut Lexer<SyntaxKind>) -> Result<(), StringBlockError> {22	struct Context<'a> {23		source: &'a str,24		index: usize,25		offset: usize,26	}2728	impl<'a> Context<'a> {29		fn rest(&self) -> &'a str {30			&self.source[self.index..]31		}3233		fn next(&mut self) -> Option<char> {34			if self.index == self.source.len() {35				return None;36			}3738			match self.rest().chars().next() {39				None => None,40				Some(c) => {41					self.index += c.len_utf8();42					Some(c)43				}44			}45		}4647		fn peek(&self) -> Option<char> {48			if self.index == self.source.len() {49				return None;50			}5152			self.rest().chars().next()53		}5455		fn eat_if(&mut self, f: impl Fn(char) -> bool) -> usize {56			if self.peek().map(f).unwrap_or(false) {57				self.index += 1;58				return 1;59			}60			061		}6263		fn eat_while(&mut self, f: impl Fn(char) -> bool) -> usize {64			if self.index == self.source.len() {65				return 0;66			}6768			let next_char = self.rest().char_indices().find(|(_, c)| !f(*c));6970			match next_char {71				None => {72					let diff = self.source.len() - self.index;73					self.index = self.source.len();74					diff75				}76				Some((idx, _)) => {77					self.index += idx;78					idx79				}80			}81		}8283		fn skip(&mut self, len: usize) {84			self.index = match self.index + len {85				n if n > self.source.len() => self.source.len(),86				n => n,87			};88		}8990		#[allow(clippy::range_plus_one)]91		fn pos(&self) -> Range<usize> {92			if self.index == self.source.len() {93				self.offset + self.index..self.offset + self.index94			} else {95				// TODO: char size96				self.offset + self.index..self.offset + self.index + 197			}98		}99	}100101	// Check that b has at least the same whitespace prefix as a and returns the102	// amount of this whitespace, otherwise returns 0.  If a has no whitespace103	// prefix than return 0.104	fn check_whitespace(a: &str, b: &str) -> usize {105		let a = a.as_bytes();106		let b = b.as_bytes();107108		for i in 0..a.len() {109			if a[i] != b' ' && a[i] != b'\t' {110				// a has run out of whitespace and b matched up to this point. Return result.111				return i;112			}113114			if i >= b.len() {115				// We ran off the edge of b while a still has whitespace. Return 0 as failure.116				return 0;117			}118119			if a[i] != b[i] {120				// a has whitespace but b does not. Return 0 as failure.121				return 0;122			}123		}124125		// We ran off the end of a and b kept up126		a.len()127	}128129	fn guess_token_end_and_bump<'a>(lex: &mut Lexer<'a, SyntaxKind>, ctx: &Context<'a>) {130		let end_index = ctx131			.rest()132			.find("|||")133			.map_or_else(|| ctx.rest().len(), |v| v + 3);134		lex.bump(ctx.index + end_index);135	}136137	debug_assert_eq!(lex.slice(), "|||");138	let mut ctx = Context {139		source: lex.remainder(),140		index: 0,141		offset: lex.span().end,142	};143144	ctx.eat_if(|v| v == '-');145146	// Skip whitespaces147	ctx.eat_while(|r| r == ' ' || r == '\t' || r == '\r');148149	// Skip \n150	match ctx.next() {151		Some('\n') => (),152		None => {153			guess_token_end_and_bump(lex, &ctx);154			return Err(UnexpectedEnd);155		}156		// Text block requires new line after |||.157		Some(_) => {158			guess_token_end_and_bump(lex, &ctx);159			return Err(MissingNewLine);160		}161	}162163	// Process leading blank lines before calculating string block indent164	while ctx.peek() == Some('\n') {165		ctx.next();166	}167168	let mut num_whitespace = check_whitespace(ctx.rest(), ctx.rest());169	let str_block_indent = &ctx.rest()[..num_whitespace];170171	if num_whitespace == 0 {172		// Text block's first line must start with whitespace173		guess_token_end_and_bump(lex, &ctx);174		return Err(MissingIndent);175	}176177	loop {178		debug_assert_ne!(num_whitespace, 0, "Unexpected value for num_whitespace");179		ctx.skip(num_whitespace);180181		loop {182			match ctx.next() {183				None => {184					guess_token_end_and_bump(lex, &ctx);185					return Err(UnexpectedEnd);186				}187				Some('\n') => break,188				Some(_) => (),189			}190		}191192		// Skip any blank lines193		while ctx.peek() == Some('\n') {194			ctx.next();195		}196197		// Look at the next line198		num_whitespace = check_whitespace(str_block_indent, ctx.rest());199		if num_whitespace == 0 {200			// End of the text block201			// let mut term_indent = String::with_capacity(num_whitespace);202			while let Some(' ' | '\t') = ctx.peek() {203				// term_indent.push(204				ctx.next().unwrap();205				// );206			}207208			if !ctx.rest().starts_with("|||") {209				// Text block not terminated with |||210				let pos = ctx.pos();211				if pos.is_empty() {212					// eof213					lex.bump(ctx.index);214					return Err(UnexpectedEnd);215				}216217				guess_token_end_and_bump(lex, &ctx);218				return Err(MissingTermination);219			}220221			// Skip '|||'222			ctx.skip(3);223			break;224		}225	}226227	lex.bump(ctx.index);228	Ok(())229}
after · crates/jrsonnet-rowan-parser/src/string_block.rs
1#[derive(Clone, Copy, Debug, PartialEq, Eq)]2pub enum StringBlockError {3	UnexpectedEnd,4	MissingNewLine,5	MissingTermination,6	MissingIndent,7}89use logos::Lexer;10use StringBlockError::*;1112use crate::SyntaxKind;1314pub(crate) fn lex_str_block_test<'d>(lex: &mut Lexer<'d, SyntaxKind>) {15	let _ = lex_str_block(lex);16}1718pub(crate) struct Context<'a> {19	source: &'a str,20	index: usize,21}2223impl<'a> Context<'a> {24	fn rest(&self) -> &'a str {25		&self.source[self.index..]26	}2728	fn next(&mut self) -> Option<char> {29		if self.index == self.source.len() {30			return None;31		}3233		match self.rest().chars().next() {34			None => None,35			Some(c) => {36				self.index += c.len_utf8();37				Some(c)38			}39		}40	}4142	fn peek(&self) -> Option<char> {43		if self.index == self.source.len() {44			return None;45		}4647		self.rest().chars().next()48	}4950	fn eat_if(&mut self, f: impl Fn(char) -> bool) -> usize {51		if self.peek().map(f).unwrap_or(false) {52			self.index += 1;53			return 1;54		}55		056	}5758	fn eat_while(&mut self, f: impl Fn(char) -> bool) -> usize {59		if self.index == self.source.len() {60			return 0;61		}6263		let next_char = self.rest().char_indices().find(|(_, c)| !f(*c));6465		match next_char {66			None => {67				let diff = self.source.len() - self.index;68				self.index = self.source.len();69				diff70			}71			Some((idx, _)) => {72				self.index += idx;73				idx74			}75		}76	}7778	fn skip(&mut self, len: usize) {79		self.index = match self.index + len {80			n if n > self.source.len() => self.source.len(),81			n => n,82		};83	}84}8586// Check that b has at least the same whitespace prefix as a and returns the87// amount of this whitespace, otherwise returns 0.  If a has no whitespace88// prefix than return 0.89fn check_whitespace(a: &str, b: &str) -> usize {90	let a = a.as_bytes();91	let b = b.as_bytes();9293	for i in 0..a.len() {94		if a[i] != b' ' && a[i] != b'\t' {95			// a has run out of whitespace and b matched up to this point. Return result.96			return i;97		}9899		if i >= b.len() {100			// We ran off the edge of b while a still has whitespace. Return 0 as failure.101			return 0;102		}103104		if a[i] != b[i] {105			// a has whitespace but b does not. Return 0 as failure.106			return 0;107		}108	}109110	// We ran off the end of a and b kept up111	a.len()112}113114pub(crate) trait StrBlockLexCtx<'d> {115	fn remainder(&self) -> &'d str;116	fn eat_error(&mut self, ctx: &Context<'d>);117	fn bump_pos(&mut self, s: usize);118	fn mark_truncating(&mut self);119	fn mark_line(&mut self, line: &'d str);120}121122impl<'d> StrBlockLexCtx<'d> for Lexer<'d, SyntaxKind> {123	fn remainder(&self) -> &'d str {124		self.remainder()125	}126	fn eat_error(&mut self, ctx: &Context<'d>) {127		let end_index = ctx128			.rest()129			.find("|||")130			.map_or_else(|| ctx.rest().len(), |v| v + 3);131		self.bump(ctx.index + end_index);132	}133	fn bump_pos(&mut self, s: usize) {134		self.bump(s);135	}136	fn mark_truncating(&mut self) {137		// Lexer test doesn't collect anything138	}139	fn mark_line(&mut self, _line: &'d str) {140		// Lexer test doesn't collect anything141	}142}143144pub fn collect_lexed_str_block<'s>(145	input: &'s str,146) -> Result<CollectStrBlock<'s>, StringBlockError> {147	let mut collect = CollectStrBlock {148		truncate: false,149		lines: vec![],150		input,151		offset: 0,152	};153	lex_str_block(&mut collect)?;154	Ok(collect)155}156157pub struct CollectStrBlock<'s> {158	pub truncate: bool,159	pub lines: Vec<&'s str>,160	input: &'s str,161	offset: usize,162}163164impl<'d> StrBlockLexCtx<'d> for CollectStrBlock<'d> {165	fn remainder(&self) -> &'d str {166		self.input167	}168169	fn eat_error(&mut self, _ctx: &Context<'d>) {170		// Error will be returned, no need to record it here171	}172173	fn bump_pos(&mut self, s: usize) {174		self.offset += s;175	}176177	fn mark_truncating(&mut self) {178		self.truncate = true;179	}180181	fn mark_line(&mut self, line: &'d str) {182		self.lines.push(line)183	}184}185186pub(crate) fn lex_str_block<'a>(lex: &mut impl StrBlockLexCtx<'a>) -> Result<(), StringBlockError> {187	// debug_assert_eq!(lex.slice(), "|||");188	let mut ctx = Context::<'a> {189		source: lex.remainder(),190		index: 0,191	};192193	if ctx.eat_if(|v| v == '-') != 0 {194		lex.mark_truncating();195	}196197	// Skip whitespaces198	ctx.eat_while(|r| r == ' ' || r == '\t' || r == '\r');199200	// Skip \n201	match ctx.next() {202		Some('\n') => (),203		None => {204			lex.eat_error(&ctx);205			return Err(UnexpectedEnd);206		}207		// Text block requires new line after |||.208		Some(_) => {209			lex.eat_error(&ctx);210			return Err(MissingNewLine);211		}212	}213214	// Process leading blank lines before calculating string block indent215	while ctx.peek() == Some('\n') {216		ctx.next();217	}218219	let mut num_whitespace = check_whitespace(ctx.rest(), ctx.rest());220	let str_block_indent = &ctx.rest()[..num_whitespace];221222	if num_whitespace == 0 {223		// Text block's first line must start with whitespace224		lex.eat_error(&ctx);225		return Err(MissingIndent);226	}227228	loop {229		debug_assert_ne!(num_whitespace, 0, "Unexpected value for num_whitespace");230		ctx.skip(num_whitespace);231232		let line_start = ctx.index;233		let mut line_size = 0;234		loop {235			match ctx.next() {236				None => {237					lex.eat_error(&ctx);238					return Err(UnexpectedEnd);239				}240				Some('\n') => {241					lex.mark_line(&ctx.source[line_start..line_start + line_size]);242					break;243				}244				Some(c) => {245					line_size += c.len_utf8();246				}247			}248		}249250		// Skip any blank lines251		while ctx.peek() == Some('\n') {252			lex.mark_line("");253			ctx.next();254		}255256		// Look at the next line257		num_whitespace = check_whitespace(str_block_indent, ctx.rest());258		if num_whitespace == 0 {259			// End of the text block260			// let mut term_indent = String::with_capacity(num_whitespace);261			while let Some(' ' | '\t') = ctx.peek() {262				// term_indent.push(263				ctx.next().unwrap();264				// );265			}266267			if !ctx.rest().starts_with("|||") {268				if ctx.rest().is_empty() {269					lex.bump_pos(ctx.index);270					return Err(UnexpectedEnd);271				}272				lex.eat_error(&ctx);273				return Err(MissingTermination);274			}275276			// Skip '|||'277			ctx.skip(3);278			break;279		}280	}281282	lex.bump_pos(ctx.index);283	Ok(())284}