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

difftreelog

feat(fmt) preserve comments in locals

Yaroslav Bolyukin2022-06-22parent: #016538a.patch.diff
in: master

3 files changed

modifiedcmds/jrsonnet-fmt/src/children.rsdiffbeforeafterboth
--- a/cmds/jrsonnet-fmt/src/children.rs
+++ b/cmds/jrsonnet-fmt/src/children.rs
@@ -40,10 +40,9 @@
 	}
 	let mut iter = node.children_with_tokens().peekable();
 	while iter.peek() != start {
-		// println!("Skipped {}");
-		dbg!(&iter.next());
+		iter.next();
 	}
-	dbg!(&iter.next());
+	iter.next();
 	let mut out = Vec::new();
 	for item in iter {
 		if let Some(trivia) = item.as_token().cloned().and_then(Trivia::cast) {
@@ -59,6 +58,36 @@
 	out
 }
 
+pub fn trivia_between(
+	node: SyntaxNode,
+	start: Option<&SyntaxElement>,
+	end: Option<&SyntaxElement>,
+) -> ChildTrivia {
+	let mut iter = node.children_with_tokens().peekable();
+	while iter.peek() != start {
+		iter.next();
+	}
+	iter.next();
+
+	let loose = start.is_none() || end.is_none();
+
+	let mut out = Vec::new();
+	for item in iter.take_while(|i| Some(i) != end) {
+		if let Some(trivia) = item.as_token().cloned().and_then(Trivia::cast) {
+			out.push(trivia);
+		} else if loose {
+			break;
+		} else {
+			assert!(
+				TS![, ;].contains(item.kind()) || item.kind() == ERROR,
+				"silently eaten token: {:?}",
+				item.kind()
+			)
+		}
+	}
+	out
+}
+
 pub fn children_between<T: AstNode + Debug>(
 	node: SyntaxNode,
 	start: Option<&SyntaxElement>,
modifiedcmds/jrsonnet-fmt/src/comments.rsdiffbeforeafterboth
before · cmds/jrsonnet-fmt/src/comments.rs
1use dprint_core::formatting::PrintItems;2use jrsonnet_rowan_parser::{nodes::TriviaKind, AstToken};34use crate::{children::ChildTrivia, p, pi};56pub enum CommentLocation {7	/// Above local, field, other things8	AboveItem,9	/// After item10	ItemInline,11	/// After all items in object12	EndOfItems,13}1415pub fn format_comments(comments: &ChildTrivia, loc: CommentLocation) -> PrintItems {16	let mut pi = p!(new:);1718	for c in comments {19		match c.kind() {20			TriviaKind::Whitespace => {}21			TriviaKind::MultiLineComment => {22				let mut text = c23					.text()24					.strip_prefix("/*")25					.expect("ml comment starts with /*")26					.strip_suffix("*/")27					.expect("ml comment ends with */");28				// doc-style comment, /**29				let doc = if text.starts_with('*') {30					text = &text[1..];31					true32				} else {33					false34				};35				// Is comment starts with text immediatly, i.e /*text36				let mut immediate_start = true;37				let mut lines = text38					.split('\n')39					.map(|l| l.trim_end())40					.skip_while(|l| {41						if l.is_empty() {42							immediate_start = false;43							true44						} else {45							false46						}47					})48					.collect::<Vec<_>>();49				while lines.last().map(|l| l.is_empty()).unwrap_or(false) {50					lines.pop();51				}52				if lines.len() == 1 && !doc {53					p!(pi: str("/* ") str(lines[0].trim()) str(" */") nl)54				} else if !lines.is_empty() {55					fn common_ws_prefix<'a>(a: &'a str, b: &str) -> &'a str {56						let offset = a57							.bytes()58							.zip(b.bytes())59							.take_while(|(a, b)| a == b && (a.is_ascii_whitespace() || *a == b'*'))60							.count();61						&a[..offset]62					}63					// First line is not empty, extract ws prefix of it64					let mut common_ws_padding = if immediate_start && lines.len() > 1 {65						common_ws_prefix(lines[1], lines[1])66					} else {67						common_ws_prefix(lines[0], lines[0])68					};69					for line in lines70						.iter()71						.skip(if immediate_start { 2 } else { 1 })72						.filter(|l| !l.is_empty())73					{74						common_ws_padding = common_ws_prefix(common_ws_padding, line);75					}76					for line in lines77						.iter_mut()78						.skip(if immediate_start { 1 } else { 0 })79						.filter(|l| !l.is_empty())80					{81						*line = line82							.strip_prefix(common_ws_padding)83							.expect("all non-empty lines start with this padding");84					}8586					p!(pi: str("/*"));87					if doc {88						p!(pi: str("*"));89					}90					p!(pi: nl);91					for mut line in lines {92						if doc {93							p!(pi: str(" *"));94						}95						if line.is_empty() {96							p!(pi: nl);97						} else {98							if doc {99								p!(pi: str(" "));100							}101							while let Some(new_line) = line.strip_prefix('\t') {102								if doc {103									p!(pi: str("    "));104								} else {105									p!(pi: tab);106								}107								line = new_line;108							}109							p!(pi: str(line) nl)110						}111					}112					if doc {113						p!(pi: str(" "));114					}115					p!(pi: str("*/") nl)116				}117			}118			// TODO: Keep common padding for multiple continous lines of single-line comments119			// I.e120			// ```121			// #  Line1122			// #    Line2123			// ```124			// Should be reformatted as125			// ```126			// # Line1127			// #   Line2128			// ```129			// But currently comment formatter is not aware of continous comment lines, and reformats it as130			// ```131			// # Line1132			// # Line2133			// ```134			TriviaKind::SingleLineHashComment => {135				if matches!(loc, CommentLocation::ItemInline) {136					p!(pi: str(" "))137				}138				p!(pi: str("# ") str(c.text().strip_prefix('#').expect("hash comment starts with #").trim()));139				if !matches!(loc, CommentLocation::ItemInline) {140					p!(pi: nl)141				}142			}143			TriviaKind::SingleLineSlashComment => {144				if matches!(loc, CommentLocation::ItemInline) {145					p!(pi: str(" "))146				}147				p!(pi: str("// ") str(c.text().strip_prefix("//").expect("comment starts with //").trim()));148				if !matches!(loc, CommentLocation::ItemInline) {149					p!(pi: nl)150				}151			}152			// Garbage in - garbage out153			TriviaKind::ErrorCommentTooShort => p!(pi: str("/*/")),154			TriviaKind::ErrorCommentUnterminated => p!(pi: str(c.text())),155		}156	}157158	pi159}
after · cmds/jrsonnet-fmt/src/comments.rs
1use dprint_core::formatting::PrintItems;2use jrsonnet_rowan_parser::{nodes::TriviaKind, AstToken};34use crate::{children::ChildTrivia, p, pi};56pub enum CommentLocation {7	/// Above local, field, other things8	AboveItem,9	/// After item10	ItemInline,11	/// After all items in object12	EndOfItems,13}1415#[must_use]16pub fn format_comments(comments: &ChildTrivia, loc: CommentLocation) -> PrintItems {17	let mut pi = p!(new:);1819	for c in comments {20		match c.kind() {21			TriviaKind::Whitespace => {}22			TriviaKind::MultiLineComment => {23				let mut text = c24					.text()25					.strip_prefix("/*")26					.expect("ml comment starts with /*")27					.strip_suffix("*/")28					.expect("ml comment ends with */");29				// doc-style comment, /**30				let doc = if text.starts_with('*') {31					text = &text[1..];32					true33				} else {34					false35				};36				// Is comment starts with text immediatly, i.e /*text37				let mut immediate_start = true;38				let mut lines = text39					.split('\n')40					.map(|l| l.trim_end())41					.skip_while(|l| {42						if l.is_empty() {43							immediate_start = false;44							true45						} else {46							false47						}48					})49					.collect::<Vec<_>>();50				while lines.last().map(|l| l.is_empty()).unwrap_or(false) {51					lines.pop();52				}53				if lines.len() == 1 && !doc {54					p!(pi: str("/* ") str(lines[0].trim()) str(" */") nl)55				} else if !lines.is_empty() {56					fn common_ws_prefix<'a>(a: &'a str, b: &str) -> &'a str {57						let offset = a58							.bytes()59							.zip(b.bytes())60							.take_while(|(a, b)| a == b && (a.is_ascii_whitespace() || *a == b'*'))61							.count();62						&a[..offset]63					}64					// First line is not empty, extract ws prefix of it65					let mut common_ws_padding = if immediate_start && lines.len() > 1 {66						common_ws_prefix(lines[1], lines[1])67					} else {68						common_ws_prefix(lines[0], lines[0])69					};70					for line in lines71						.iter()72						.skip(if immediate_start { 2 } else { 1 })73						.filter(|l| !l.is_empty())74					{75						common_ws_padding = common_ws_prefix(common_ws_padding, line);76					}77					for line in lines78						.iter_mut()79						.skip(if immediate_start { 1 } else { 0 })80						.filter(|l| !l.is_empty())81					{82						*line = line83							.strip_prefix(common_ws_padding)84							.expect("all non-empty lines start with this padding");85					}8687					p!(pi: str("/*"));88					if doc {89						p!(pi: str("*"));90					}91					p!(pi: nl);92					for mut line in lines {93						if doc {94							p!(pi: str(" *"));95						}96						if line.is_empty() {97							p!(pi: nl);98						} else {99							if doc {100								p!(pi: str(" "));101							}102							while let Some(new_line) = line.strip_prefix('\t') {103								if doc {104									p!(pi: str("    "));105								} else {106									p!(pi: tab);107								}108								line = new_line;109							}110							p!(pi: str(line) nl)111						}112					}113					if doc {114						p!(pi: str(" "));115					}116					p!(pi: str("*/") nl)117				}118			}119			// TODO: Keep common padding for multiple continous lines of single-line comments120			// I.e121			// ```122			// #  Line1123			// #    Line2124			// ```125			// Should be reformatted as126			// ```127			// # Line1128			// #   Line2129			// ```130			// But currently comment formatter is not aware of continous comment lines, and reformats it as131			// ```132			// # Line1133			// # Line2134			// ```135			TriviaKind::SingleLineHashComment => {136				if matches!(loc, CommentLocation::ItemInline) {137					p!(pi: str(" "))138				}139				p!(pi: str("# ") str(c.text().strip_prefix('#').expect("hash comment starts with #").trim()));140				if !matches!(loc, CommentLocation::ItemInline) {141					p!(pi: nl)142				}143			}144			TriviaKind::SingleLineSlashComment => {145				if matches!(loc, CommentLocation::ItemInline) {146					p!(pi: str(" "))147				}148				p!(pi: str("// ") str(c.text().strip_prefix("//").expect("comment starts with //").trim()));149				if !matches!(loc, CommentLocation::ItemInline) {150					p!(pi: nl)151				}152			}153			// Garbage in - garbage out154			TriviaKind::ErrorCommentTooShort => p!(pi: str("/*/")),155			TriviaKind::ErrorCommentUnterminated => p!(pi: str(c.text())),156		}157	}158159	pi160}
modifiedcmds/jrsonnet-fmt/src/main.rsdiffbeforeafterboth
--- a/cmds/jrsonnet-fmt/src/main.rs
+++ b/cmds/jrsonnet-fmt/src/main.rs
@@ -13,7 +13,7 @@
 };
 
 use crate::{
-	children::{should_start_with_newline, trivia_after},
+	children::{should_start_with_newline, trivia_after, trivia_between},
 	comments::{format_comments, CommentLocation},
 };
 
@@ -436,11 +436,45 @@
 			}
 			Expr::ExprVar(n) => p!(new: {n.name()}),
 			Expr::ExprLocal(l) => {
-				let mut pi = p!(new: str("local") >i nl);
-				for bind in l.binds() {
-					p!(pi: {bind} str(",") nl);
+				let mut pi = p!(new:);
+				let (binds, end_comments) = children_between::<Bind>(
+					l.syntax().clone(),
+					l.local_kw_token().map(Into::into).as_ref(),
+					l.semi_token().map(Into::into).as_ref(),
+				);
+				if binds.len() == 1 {
+					let bind = &binds[0];
+					p!(pi: items(format_comments(&bind.before_trivia, CommentLocation::AboveItem)));
+					p!(pi: str("local ") {bind.value});
+				// TODO: keep end_comments, child.inline_trivia somehow, force multiple locals formatting in case of presence?
+				} else {
+					p!(pi: str("local") >i nl);
+					for bind in binds {
+						if bind.needs_newline_above() {
+							p!(pi: nl);
+						}
+						p!(pi: items(format_comments(&bind.before_trivia, CommentLocation::AboveItem)));
+						p!(pi: {bind.value} str(";"));
+						p!(pi: items(format_comments(&bind.inline_trivia, CommentLocation::ItemInline)) nl);
+					}
+					// TODO: needs_newline_above end_comments
+					p!(pi: items(format_comments(&end_comments, CommentLocation::EndOfItems)));
+					p!(pi: <i);
 				}
-				p!(pi: <i str(";") nl {l.expr()});
+				p!(pi: str(";") nl);
+
+				let expr_comments = trivia_between(
+					l.syntax().clone(),
+					l.semi_token().map(Into::into).as_ref(),
+					l.expr()
+						.map(|e| e.syntax().clone())
+						.map(Into::into)
+						.as_ref(),
+				);
+				p!(pi: items(format_comments(&expr_comments, CommentLocation::AboveItem)));
+
+				// TODO: needs_newline_above expr
+				p!(pi: {l.expr()});
 				pi
 			}
 			Expr::ExprIfThenElse(ite) => {
@@ -515,6 +549,25 @@
 
 		local unary = !a;
 
+		local
+			//   I am comment
+			singleLocalWithItemComment = 1,
+		;
+
+		// Comment between local and expression
+
+		local
+			a = 1, //   Inline
+			// Comment above b
+			b = 4,
+
+			// c needs some space
+			c = 5,
+
+			// Comment after everything
+		;
+
+
 		local Template = {z: "foo"};
 
 		{