difftreelog
feat(fmt) preserve comments in locals
in: master
3 files changed
cmds/jrsonnet-fmt/src/children.rsdiffbeforeafterboth1// TODO: Return errors as trivia23use std::{fmt::Debug, marker::PhantomData, 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 // println!("Skipped {}");44 dbg!(&iter.next());45 }46 dbg!(&iter.next());47 let mut out = Vec::new();48 for item in iter {49 if let Some(trivia) = item.as_token().cloned().and_then(Trivia::cast) {50 out.push(trivia);51 } else {52 assert!(53 TS![, ;].contains(item.kind()) || item.kind() == ERROR,54 "silently eaten token: {:?}",55 item.kind()56 )57 }58 }59 out60}6162pub fn children_between<T: AstNode + Debug>(63 node: SyntaxNode,64 start: Option<&SyntaxElement>,65 end: Option<&SyntaxElement>,66) -> (Vec<Child<T>>, ChildTrivia) {67 let mut iter = node.children_with_tokens().peekable();68 while iter.peek() != start {69 iter.next();70 }71 iter.next();72 children(73 iter.take_while(|i| Some(i) != end),74 start.is_none() || end.is_none(),75 )76}7778pub fn should_start_with_newline(tt: &ChildTrivia) -> bool {79 // First for previous item end80 count_newlines_before(tt) >= 281}8283fn count_newlines_before(tt: &ChildTrivia) -> usize {84 let mut nl_count = 0;85 for t in tt {86 match t.kind() {87 TriviaKind::Whitespace => {88 nl_count += t.text().bytes().filter(|b| *b == b'\n').count();89 }90 _ => break,91 }92 }93 nl_count94}95fn count_newlines_after(tt: &ChildTrivia) -> usize {96 let mut nl_count = 0;97 for t in tt.iter().rev() {98 match t.kind() {99 TriviaKind::Whitespace => {100 nl_count += t.text().bytes().filter(|b| *b == b'\n').count();101 }102 TriviaKind::SingleLineHashComment => {103 nl_count += 1;104 break;105 }106 TriviaKind::SingleLineSlashComment => {107 nl_count += 1;108 break;109 }110 _ => {}111 }112 }113 nl_count114}115116pub fn children<'a, T: AstNode + Debug>(117 items: impl Iterator<Item = SyntaxElement>,118 loose: bool,119) -> (Vec<Child<T>>, ChildTrivia) {120 let mut out = Vec::new();121 let mut current_child = None::<Child<T>>;122 let mut next = ChildTrivia::new();123 // Previous element ended, do not add more inline comments124 let mut started_next = false;125 let mut had_some = false;126127 for item in items {128 if let Some(value) = item.as_node().cloned().and_then(T::cast) {129 let before_trivia = mem::take(&mut next);130 let last_child = current_child.replace(Child {131 newlines_above: if had_some {132 count_newlines_before(&before_trivia)133 + current_child134 .as_ref()135 .map(|c| count_newlines_after(&c.inline_trivia))136 .unwrap_or_default()137 } else {138 0139 },140 before_trivia,141 value,142 inline_trivia: Vec::new(),143 });144 if let Some(last_child) = last_child {145 out.push(last_child)146 }147 had_some = true;148 started_next = false;149 } else if let Some(trivia) = item.as_token().cloned().and_then(Trivia::cast) {150 let is_single_line_comment = trivia.kind() == TriviaKind::SingleLineHashComment151 || trivia.kind() == TriviaKind::SingleLineSlashComment;152 if started_next153 || current_child.is_none()154 || trivia.text().contains('\n') && !is_single_line_comment155 {156 next.push(trivia.clone());157 started_next = true;158 } else {159 let cur = current_child.as_mut().expect("checked not none");160 cur.inline_trivia.push(trivia);161 if is_single_line_comment {162 started_next = true;163 }164 }165 had_some = true;166 } else if loose {167 if had_some {168 break;169 }170 started_next = true;171 } else {172 assert!(173 TS![, ;].contains(item.kind()) || item.kind() == ERROR,174 "silently eaten token: {:?}",175 item.kind()176 )177 }178 }179180 if let Some(current_child) = current_child {181 out.push(current_child);182 }183184 (out, next)185}186187#[derive(Debug)]188pub struct Child<T> {189 newlines_above: usize,190 /// Comment before item, i.e191 ///192 /// ```ignore193 /// // Comment194 /// item195 /// ```196 pub before_trivia: ChildTrivia,197 pub value: T,198 /// Comment after line, but located at same line199 ///200 /// ```ignore201 /// item1, // Inline comment202 /// // Not inline comment203 /// item2,204 /// ```205 pub inline_trivia: ChildTrivia,206}207208impl<T> Child<T> {209 /// If this child has two newlines above in source code, so it needs to have it in output210 pub fn needs_newline_above(&self) -> bool {211 // First line for end of previous item212 self.newlines_above >= 2213 }214}cmds/jrsonnet-fmt/src/comments.rsdiffbeforeafterboth--- a/cmds/jrsonnet-fmt/src/comments.rs
+++ b/cmds/jrsonnet-fmt/src/comments.rs
@@ -12,6 +12,7 @@
EndOfItems,
}
+#[must_use]
pub fn format_comments(comments: &ChildTrivia, loc: CommentLocation) -> PrintItems {
let mut pi = p!(new:);
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},
+ 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"};
{