difftreelog
feat(fmt) preserve newline above last comment
in: master
3 files changed
cmds/jrsonnet-fmt/src/children.rsdiffbeforeafterboth1// 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}cmds/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
}
cmds/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!(