git.delta.rocks / jrsonnet / refs/commits / 443991edefaa

difftreelog

feat(fmt) preserve newline above last comment

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

3 files changed

modifiedcmds/jrsonnet-fmt/src/children.rsdiffbeforeafterboth
after · cmds/jrsonnet-fmt/src/children.rs
1// TODO: Return errors as trivia23use std::{fmt::Debug, mem};45use jrsonnet_rowan_parser::{6	nodes::{Trivia, TriviaKind},7	AstNode, AstToken, SyntaxElement,8	SyntaxKind::*,9	SyntaxNode, TS,10};1112pub type ChildTrivia = Vec<Trivia>;1314/// Node should have no non-trivia tokens before element15pub fn trivia_before(node: SyntaxNode, end: Option<&SyntaxElement>) -> ChildTrivia {16	let mut out = Vec::new();17	for item in node.children_with_tokens() {18		if Some(&item) == end {19			break;20		}2122		if let Some(trivia) = item.as_token().cloned().and_then(Trivia::cast) {23			out.push(trivia);24		} else if end.is_none() {25			break;26		} else {27			assert!(28				TS![, ;].contains(item.kind()) || item.kind() == ERROR,29				"silently eaten token: {:?}",30				item.kind()31			)32		}33	}34	out35}36/// Node should have no non-trivia tokens after element37pub fn trivia_after(node: SyntaxNode, start: Option<&SyntaxElement>) -> ChildTrivia {38	if start.is_none() {39		return Vec::new();40	}41	let mut iter = node.children_with_tokens().peekable();42	while iter.peek() != start {43		iter.next();44	}45	iter.next();46	let mut out = Vec::new();47	for item in iter {48		if let Some(trivia) = item.as_token().cloned().and_then(Trivia::cast) {49			out.push(trivia);50		} else {51			assert!(52				TS![, ;].contains(item.kind()) || item.kind() == ERROR,53				"silently eaten token: {:?}",54				item.kind()55			)56		}57	}58	out59}6061pub fn trivia_between(62	node: SyntaxNode,63	start: Option<&SyntaxElement>,64	end: Option<&SyntaxElement>,65) -> EndingComments {66	let mut iter = node.children_with_tokens().peekable();67	while iter.peek() != start {68		iter.next();69	}70	iter.next();7172	let loose = start.is_none() || end.is_none();7374	let mut out = Vec::new();75	for item in iter.take_while(|i| Some(i) != end) {76		if let Some(trivia) = item.as_token().cloned().and_then(Trivia::cast) {77			out.push(trivia);78		} else if loose {79			break;80		} else {81			assert!(82				TS![, ;].contains(item.kind()) || item.kind() == ERROR,83				"silently eaten token: {:?}",84				item.kind()85			)86		}87	}88	EndingComments {89		should_start_with_newline: should_start_with_newline(None, &out),90		trivia: out,91	}92}9394pub fn children_between<T: AstNode + Debug>(95	node: SyntaxNode,96	start: Option<&SyntaxElement>,97	end: Option<&SyntaxElement>,98) -> (Vec<Child<T>>, EndingComments) {99	let mut iter = node.children_with_tokens().peekable();100	while iter.peek() != start {101		iter.next();102	}103	iter.next();104	children(105		iter.take_while(|i| Some(i) != end),106		start.is_none() || end.is_none(),107	)108}109110pub fn should_start_with_newline(prev_inline: Option<&ChildTrivia>, tt: &ChildTrivia) -> bool {111	count_newlines_before(tt)112		+ prev_inline113			.map(count_newlines_after)114			.unwrap_or_default()115116		// First for previous item end, second for current item117		>= 2118}119120fn count_newlines_before(tt: &ChildTrivia) -> usize {121	let mut nl_count = 0;122	for t in tt {123		match t.kind() {124			TriviaKind::Whitespace => {125				nl_count += t.text().bytes().filter(|b| *b == b'\n').count();126			}127			_ => break,128		}129	}130	nl_count131}132fn count_newlines_after(tt: &ChildTrivia) -> usize {133	let mut nl_count = 0;134	for t in tt.iter().rev() {135		match t.kind() {136			TriviaKind::Whitespace => {137				nl_count += t.text().bytes().filter(|b| *b == b'\n').count();138			}139			TriviaKind::SingleLineHashComment => {140				nl_count += 1;141				break;142			}143			TriviaKind::SingleLineSlashComment => {144				nl_count += 1;145				break;146			}147			_ => {}148		}149	}150	nl_count151}152153pub fn children<T: AstNode + Debug>(154	items: impl Iterator<Item = SyntaxElement>,155	loose: bool,156) -> (Vec<Child<T>>, EndingComments) {157	let mut out = Vec::new();158	let mut current_child = None::<Child<T>>;159	let mut next = ChildTrivia::new();160	// Previous element ended, do not add more inline comments161	let mut started_next = false;162	let mut had_some = false;163164	for item in items {165		if let Some(value) = item.as_node().cloned().and_then(T::cast) {166			let before_trivia = mem::take(&mut next);167			let last_child = current_child.replace(Child {168				// First item should not start with newline169				should_start_with_newline: had_some170					&& should_start_with_newline(171						current_child.as_ref().map(|c| &c.inline_trivia),172						&before_trivia,173					),174				before_trivia,175				value,176				inline_trivia: Vec::new(),177			});178			if let Some(last_child) = last_child {179				out.push(last_child)180			}181			had_some = true;182			started_next = false;183		} else if let Some(trivia) = item.as_token().cloned().and_then(Trivia::cast) {184			let is_single_line_comment = trivia.kind() == TriviaKind::SingleLineHashComment185				|| trivia.kind() == TriviaKind::SingleLineSlashComment;186			if started_next187				|| current_child.is_none()188				|| trivia.text().contains('\n') && !is_single_line_comment189			{190				next.push(trivia.clone());191				started_next = true;192			} else {193				let cur = current_child.as_mut().expect("checked not none");194				cur.inline_trivia.push(trivia);195				if is_single_line_comment {196					started_next = true;197				}198			}199			had_some = true;200		} else if loose {201			if had_some {202				break;203			}204			started_next = true;205		} else {206			assert!(207				TS![, ;].contains(item.kind()) || item.kind() == ERROR,208				"silently eaten token: {:?}",209				item.kind()210			)211		}212	}213214	let ending_comments = EndingComments {215		should_start_with_newline: should_start_with_newline(216			current_child.as_ref().map(|c| &c.inline_trivia),217			&next,218		),219		trivia: next,220	};221222	if let Some(current_child) = current_child {223		out.push(current_child);224	}225226	(out, ending_comments)227}228229#[derive(Debug)]230pub struct Child<T> {231	/// If this child has two newlines above in source code, so it needs to have it in the output232	pub should_start_with_newline: bool,233	/// Comment before item, i.e234	///235	/// ```ignore236	/// // Comment237	/// item238	/// ```239	pub before_trivia: ChildTrivia,240	pub value: T,241	/// Comment after line, but located at same line242	///243	/// ```ignore244	/// item1, // Inline comment245	/// // Not inline comment246	/// item2,247	/// ```248	pub inline_trivia: ChildTrivia,249}250251pub struct EndingComments {252	/// If this child has two newlines above in source code, so it needs to have it in the output253	pub should_start_with_newline: bool,254	pub trivia: ChildTrivia,255}
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, trivia_between},
+	children::{trivia_after, trivia_between},
 	comments::{format_comments, CommentLocation},
 };
 
@@ -294,7 +294,7 @@
 					l.r_brace_token().map(Into::into).as_ref(),
 				);
 				for mem in children.into_iter() {
-					if mem.needs_newline_above() {
+					if mem.should_start_with_newline {
 						p!(pi: nl);
 					}
 					p!(pi: items(format_comments(&mem.before_trivia, CommentLocation::AboveItem)));
@@ -314,11 +314,10 @@
 					p!(pi: nl)
 				}
 
-				// TODO: implement same thing as needs_newline_above, but for end comments
-				if should_start_with_newline(&end_comments) {
+				if end_comments.should_start_with_newline {
 					p!(pi: nl);
 				}
-				p!(pi: items(format_comments(&end_comments, CommentLocation::EndOfItems)));
+				p!(pi: items(format_comments(&end_comments.trivia, CommentLocation::EndOfItems)));
 				p!(pi: <i str("}"));
 				pi
 			}
@@ -450,15 +449,17 @@
 				} else {
 					p!(pi: str("local") >i nl);
 					for bind in binds {
-						if bind.needs_newline_above() {
+						if bind.should_start_with_newline {
 							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)));
+					if end_comments.should_start_with_newline {
+						p!(pi: nl)
+					}
+					p!(pi: items(format_comments(&end_comments.trivia, CommentLocation::EndOfItems)));
 					p!(pi: <i);
 				}
 				p!(pi: str(";") nl);
@@ -471,9 +472,11 @@
 						.map(Into::into)
 						.as_ref(),
 				);
-				p!(pi: items(format_comments(&expr_comments, CommentLocation::AboveItem)));
 
-				// TODO: needs_newline_above expr
+				if expr_comments.should_start_with_newline {
+					p!(pi: nl);
+				}
+				p!(pi: items(format_comments(&expr_comments.trivia, CommentLocation::AboveItem)));
 				p!(pi: {l.expr()});
 				pi
 			}
modifiedcmds/jrsonnet-fmt/src/tests.rsdiffbeforeafterboth
--- a/cmds/jrsonnet-fmt/src/tests.rs
+++ b/cmds/jrsonnet-fmt/src/tests.rs
@@ -20,7 +20,8 @@
 macro_rules! assert_formatted {
 	($input:literal, $output:literal) => {
 		let formatted = reformat(indoc!($input));
-		let expected = indoc!($output);
+		let mut expected = indoc!($output).to_owned();
+		expected.push('\n');
 		if formatted != expected {
 			panic!(
 				"bad formatting, expected\n```\n{formatted}\n```\nto be equal to\n```\n{expected}\n```",
@@ -49,7 +50,6 @@
 	);
 }
 
-// Fails
 #[test]
 fn last_comment_respects_spacing_with_inline_comment_above() {
 	assert_formatted!(