difftreelog
feat(fmt) preserve comments in locals
in: master
3 files changed
cmds/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>,
cmds/jrsonnet-fmt/src/comments.rsdiffbeforeafterboth1use 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}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}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"};
{