difftreelog
refactor split pretty-printer into a library crate
in: master
14 files changed
Cargo.lockdiffbeforeafterboth--- a/Cargo.lock
+++ b/Cargo.lock
@@ -631,16 +631,23 @@
version = "0.5.0-pre97"
dependencies = [
"clap",
- "dprint-core",
"hi-doc",
"indoc",
- "insta",
- "jrsonnet-rowan-parser",
+ "jrsonnet-formatter",
"tempfile",
"thiserror",
]
[[package]]
+name = "jrsonnet-formatter"
+version = "0.5.0-pre97"
+dependencies = [
+ "dprint-core",
+ "hi-doc",
+ "jrsonnet-rowan-parser",
+]
+
+[[package]]
name = "jrsonnet-gcmodule"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
Cargo.tomldiffbeforeafterboth--- a/Cargo.toml
+++ b/Cargo.toml
@@ -19,6 +19,7 @@
jrsonnet-stdlib = { path = "./crates/jrsonnet-stdlib", version = "0.5.0-pre97" }
jrsonnet-cli = { path = "./crates/jrsonnet-cli", version = "0.5.0-pre97" }
jrsonnet-types = { path = "./crates/jrsonnet-types", version = "0.5.0-pre97" }
+jrsonnet-formatter = { path = "./crates/jrsonnet-formatter", version = "0.5.0-pre97" }
jrsonnet-gcmodule = { version = "0.4.1" }
# Diagnostics.
# hi-doc is my library, which handles text formatting very well, but isn't polished enough yet
cmds/jrsonnet-fmt/Cargo.tomldiffbeforeafterboth--- a/cmds/jrsonnet-fmt/Cargo.toml
+++ b/cmds/jrsonnet-fmt/Cargo.toml
@@ -10,11 +10,9 @@
workspace = true
[dependencies]
-dprint-core.workspace = true
-jrsonnet-rowan-parser.workspace = true
-insta.workspace = true
indoc.workspace = true
hi-doc.workspace = true
clap = { workspace = true, features = ["derive"] }
tempfile.workspace = true
thiserror.workspace = true
+jrsonnet-formatter.workspace = true
cmds/jrsonnet-fmt/src/children.rsdiffbeforeafterboth--- a/cmds/jrsonnet-fmt/src/children.rs
+++ /dev/null
@@ -1,251 +0,0 @@
-// TODO: Return errors as trivia
-
-use std::{fmt::Debug, mem};
-
-use jrsonnet_rowan_parser::{
- nodes::{CustomError, Trivia, TriviaKind},
- AstNode, AstToken, SyntaxElement, SyntaxNode, TS,
-};
-
-pub type ChildTrivia = Vec<Result<Trivia, String>>;
-
-/// Node should have no non-trivia tokens before element
-pub fn trivia_before(node: SyntaxNode, end: Option<&SyntaxElement>) -> ChildTrivia {
- let mut out = Vec::new();
- for item in node.children_with_tokens() {
- if Some(&item) == end {
- break;
- }
-
- if let Some(trivia) = item.as_token().cloned().and_then(Trivia::cast) {
- out.push(Ok(trivia));
- } else if CustomError::can_cast(item.kind()) {
- out.push(Err(item.to_string()));
- } else if end.is_none() {
- break;
- } else {
- assert!(
- TS![, ;].contains(item.kind()),
- "silently eaten token: {:?}",
- item.kind()
- );
- }
- }
- out
-}
-/// Node should have no non-trivia tokens after element
-pub fn trivia_after(node: SyntaxNode, start: Option<&SyntaxElement>) -> ChildTrivia {
- if start.is_none() {
- return Vec::new();
- }
- let mut iter = node.children_with_tokens().peekable();
- while iter.peek() != start {
- 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) {
- out.push(Ok(trivia));
- } else if CustomError::can_cast(item.kind()) {
- out.push(Err(item.to_string()));
- } else {
- assert!(
- TS![, ;].contains(item.kind()),
- "silently eaten token: {:?}",
- item.kind()
- );
- }
- }
- out
-}
-
-pub fn children_between<T: AstNode + Debug>(
- node: SyntaxNode,
- start: Option<&SyntaxElement>,
- end: Option<&SyntaxElement>,
- trailing: Option<ChildTrivia>,
-) -> (Vec<Child<T>>, EndingComments) {
- let mut iter = node.children_with_tokens().peekable();
- if start.is_some() {
- while iter.peek() != start {
- iter.next();
- }
- iter.next();
- }
- children(
- iter.take_while(|i| Some(i) != end),
- start.is_none() && end.is_none(),
- trailing,
- )
-}
-
-pub fn should_start_with_newline(prev_inline: Option<&ChildTrivia>, tt: &ChildTrivia) -> bool {
- count_newlines_before(tt)
- + prev_inline
- .map(count_newlines_after)
- .unwrap_or_default()
-
- // First for previous item end, second for current item
- >= 2
-}
-
-fn count_newlines_before(tt: &ChildTrivia) -> usize {
- let mut nl_count = 0;
- for t in tt {
- match t {
- Ok(t) => match t.kind() {
- TriviaKind::Whitespace => {
- nl_count += t.text().bytes().filter(|b| *b == b'\n').count();
- }
- _ => break,
- },
- Err(_) => {
- nl_count += 1;
- }
- }
- }
- nl_count
-}
-fn count_newlines_after(tt: &ChildTrivia) -> usize {
- let mut nl_count = 0;
- for t in tt.iter().rev() {
- match t {
- Ok(t) => match t.kind() {
- TriviaKind::Whitespace => {
- nl_count += t.text().bytes().filter(|b| *b == b'\n').count();
- }
- TriviaKind::SingleLineHashComment | TriviaKind::SingleLineSlashComment => {
- nl_count += 1;
- break;
- }
- _ => {}
- },
- Err(_) => nl_count += 1,
- }
- }
- nl_count
-}
-
-pub fn children<T: AstNode + Debug>(
- items: impl Iterator<Item = SyntaxElement>,
- loose: bool,
- mut trailing: Option<ChildTrivia>,
-) -> (Vec<Child<T>>, EndingComments) {
- let mut out = Vec::new();
- let mut current_child = None::<Child<T>>;
- let mut next = ChildTrivia::new();
- // Previous element ended, do not add more inline comments
- let mut started_next = false;
- let mut had_some = false;
-
- for item in items {
- if let Some(value) = item.as_node().cloned().and_then(T::cast) {
- let before_trivia = if let Some(trailing) = trailing.take() {
- assert!(next.is_empty());
- trailing
- } else {
- mem::take(&mut next)
- };
- let last_child = current_child.replace(Child {
- // First item should not start with newline
- should_start_with_newline: had_some
- && should_start_with_newline(
- current_child.as_ref().map(|c| &c.inline_trivia),
- &before_trivia,
- ),
- before_trivia,
- value,
- inline_trivia: Vec::new(),
- });
- if let Some(last_child) = last_child {
- out.push(last_child);
- }
- had_some = true;
- started_next = false;
- } else if let Some(trivia) = item.as_token().cloned().and_then(Trivia::cast) {
- let is_single_line_comment = trivia.kind() == TriviaKind::SingleLineHashComment
- || trivia.kind() == TriviaKind::SingleLineSlashComment;
- if trailing.is_some() {
- // Someone have already parsed trivia for us
- continue;
- } else if started_next
- || current_child.is_none()
- || trivia.text().contains('\n') && !is_single_line_comment
- {
- next.push(Ok(trivia.clone()));
- started_next = true;
- } else {
- let cur = current_child.as_mut().expect("checked not none");
- cur.inline_trivia.push(Ok(trivia));
- if is_single_line_comment {
- started_next = true;
- }
- }
- had_some = true;
- } else if CustomError::can_cast(item.kind()) {
- next.push(Err(item.to_string()));
- } else if loose {
- if had_some {
- break;
- }
- started_next = true;
- } else {
- assert!(
- TS![, ;].contains(item.kind()),
- "silently eaten token: {:?}",
- item.kind()
- );
- }
- }
-
- let ending_comments = EndingComments {
- should_start_with_newline: should_start_with_newline(
- current_child.as_ref().map(|c| &c.inline_trivia),
- &next,
- ),
- trivia: next,
- };
-
- if let Some(current_child) = current_child {
- out.push(current_child);
- }
-
- (out, ending_comments)
-}
-
-#[derive(Debug)]
-pub struct Child<T> {
- /// If this child has two newlines above in source code, so it needs to have it in the output
- pub should_start_with_newline: bool,
- /// Comment before item, i.e
- ///
- /// ```ignore
- /// // Comment
- /// item
- /// ```
- pub before_trivia: ChildTrivia,
- pub value: T,
- /// Comment after line, but located at same line
- ///
- /// ```ignore
- /// item1, // Inline comment
- /// // Not inline comment
- /// item2,
- /// ```
- pub inline_trivia: ChildTrivia,
-}
-
-pub struct EndingComments {
- /// If this child has two newlines above in source code, so it needs to have it in the output
- pub should_start_with_newline: bool,
- pub trivia: ChildTrivia,
-}
-impl EndingComments {
- pub fn is_empty(&self) -> bool {
- !self.should_start_with_newline && self.trivia.is_empty()
- }
- pub fn extract_trailing(&mut self) -> ChildTrivia {
- mem::take(&mut self.trivia)
- }
-}
cmds/jrsonnet-fmt/src/comments.rsdiffbeforeafterboth--- a/cmds/jrsonnet-fmt/src/comments.rs
+++ /dev/null
@@ -1,181 +0,0 @@
-use std::string::String;
-
-use dprint_core::formatting::PrintItems;
-use jrsonnet_rowan_parser::{nodes::TriviaKind, AstToken};
-
-use crate::{children::ChildTrivia, p, pi};
-
-pub enum CommentLocation {
- /// Above local, field, other things
- AboveItem,
- /// After item
- ItemInline,
- /// After all items in object
- EndOfItems,
-}
-
-#[allow(clippy::too_many_lines, clippy::cognitive_complexity)]
-pub fn format_comments(comments: &ChildTrivia, loc: CommentLocation, out: &mut PrintItems) {
- for c in comments {
- let Ok(c) = c else {
- let mut text = c.as_ref().unwrap_err() as &str;
- while !text.is_empty() {
- let pos = text.find(['\n', '\t']).unwrap_or(text.len());
- let sliced = &text[..pos];
- p!(out, string(sliced.to_string()));
- text = &text[pos..];
- if !text.is_empty() {
- match text.as_bytes()[0] {
- b'\n' => p!(out, nl),
- b'\t' => p!(out, tab),
- _ => unreachable!(),
- }
- text = &text[1..];
- }
- }
- continue;
- };
- match c.kind() {
- TriviaKind::Whitespace => {}
- TriviaKind::MultiLineComment => {
- let mut text = c
- .text()
- .strip_prefix("/*")
- .expect("ml comment starts with /*")
- .strip_suffix("*/")
- .expect("ml comment ends with */");
- // doc-style comment, /**
- let doc = if text.starts_with('*') {
- text = &text[1..];
- true
- } else {
- false
- };
- // Is comment starts with text immediatly, i.e /*text
- let mut immediate_start = true;
- let mut lines = text
- .split('\n')
- .map(|l| l.trim_end().to_string())
- .skip_while(|l| {
- if l.is_empty() {
- immediate_start = false;
- true
- } else {
- false
- }
- })
- .collect::<Vec<_>>();
- while lines.last().is_some_and(String::is_empty) {
- lines.pop();
- }
- if lines.len() == 1 && !doc {
- if matches!(loc, CommentLocation::ItemInline) {
- p!(out, str(" "));
- }
- p!(out, str("/* ") string(lines[0].trim().to_string()) str(" */") nl);
- } else if !lines.is_empty() {
- fn common_ws_prefix<'a>(a: &'a str, b: &str) -> &'a str {
- let offset = a
- .bytes()
- .zip(b.bytes())
- .take_while(|(a, b)| a == b && (a.is_ascii_whitespace() || *a == b'*'))
- .count();
- &a[..offset]
- }
- // First line is not empty, extract ws prefix of it
- let mut common_ws_padding = (if immediate_start && lines.len() > 1 {
- common_ws_prefix(&lines[1], &lines[1])
- } else {
- common_ws_prefix(&lines[0], &lines[0])
- })
- .to_string();
- for line in lines
- .iter()
- .skip(if immediate_start { 2 } else { 1 })
- .filter(|l| !l.is_empty())
- {
- common_ws_padding = common_ws_prefix(&common_ws_padding, line).to_string();
- }
- for line in lines
- .iter_mut()
- .skip(usize::from(immediate_start))
- .filter(|l| !l.is_empty())
- {
- *line = line
- .strip_prefix(&common_ws_padding)
- .expect("all non-empty lines start with this padding")
- .to_string();
- }
-
- p!(out, str("/*"));
- if doc {
- p!(out, str("*"));
- }
- p!(out, nl);
- for mut line in lines {
- if doc {
- p!(out, str(" *"));
- }
- if line.is_empty() {
- p!(out, nl);
- } else {
- if doc {
- p!(out, str(" "));
- }
- while let Some(new_line) = line.strip_prefix('\t') {
- if doc {
- p!(out, str(" "));
- } else {
- p!(out, tab);
- }
- line = new_line.to_string();
- }
- p!(out, string(line.to_string()) nl);
- }
- }
- if doc {
- p!(out, str(" "));
- }
- p!(out, str("*/") nl);
- }
- }
- // TODO: Keep common padding for multiple continous lines of single-line comments
- // I.e
- // ```
- // # Line1
- // # Line2
- // ```
- // Should be reformatted as
- // ```
- // # Line1
- // # Line2
- // ```
- // But currently comment formatter is not aware of continous comment lines, and reformats it as
- // ```
- // # Line1
- // # Line2
- // ```
- TriviaKind::SingleLineHashComment => {
- if matches!(loc, CommentLocation::ItemInline) {
- p!(out, str(" "));
- }
- p!(out, str("# ") string(c.text().strip_prefix('#').expect("hash comment starts with #").trim().to_string()));
- if !matches!(loc, CommentLocation::ItemInline) {
- p!(out, nl);
- }
- }
- TriviaKind::SingleLineSlashComment => {
- if matches!(loc, CommentLocation::ItemInline) {
- p!(out, str(" "));
- }
- p!(out, str("// ") string(c.text().strip_prefix("//").expect("comment starts with //").trim().to_string()));
- if !matches!(loc, CommentLocation::ItemInline) {
- p!(out, nl);
- }
- }
- // Garbage in - garbage out
- TriviaKind::ErrorCommentTooShort => p!(out, str("/*/")),
- TriviaKind::ErrorCommentUnterminated => p!(out, string(c.text().to_string())),
- }
- }
-}
cmds/jrsonnet-fmt/src/main.rsdiffbeforeafterboth1use std::{2 any::type_name,3 fs,4 io::{self, Write},5 path::PathBuf,6 process,7 rc::Rc,8};910use children::{children_between, trivia_before};11use clap::Parser;12use dprint_core::formatting::{13 condition_helpers::is_multiple_lines, condition_resolvers::true_resolver,14 ConditionResolverContext, LineNumber, PrintItems, PrintOptions,15};16use hi_doc::Formatting;17use jrsonnet_rowan_parser::{18 nodes::{19 Arg, ArgsDesc, Assertion, BinaryOperator, Bind, CompSpec, Destruct, DestructArrayPart,20 DestructRest, Expr, ExprBase, FieldName, ForSpec, IfSpec, ImportKind, Literal, Member,21 Name, Number, ObjBody, ObjLocal, ParamsDesc, SliceDesc, SourceFile, Stmt, Suffix, Text,22 UnaryOperator, Visibility,23 },24 AstNode, AstToken as _, SyntaxToken,25};2627use crate::{28 children::trivia_after,29 comments::{format_comments, CommentLocation},30};3132mod children;33mod comments;34#[cfg(test)]35mod tests;3637pub trait Printable {38 fn print(&self, out: &mut PrintItems);39}4041macro_rules! pi {42 (@i; $($t:tt)*) => {{43 #[allow(unused_mut)]44 let mut o = dprint_core::formatting::PrintItems::new();45 pi!(@s; o: $($t)*);46 o47 }};48 (@s; $o:ident: str($e:expr $(,)?) $($t:tt)*) => {{49 $o.push_string($e.to_owned());50 pi!(@s; $o: $($t)*);51 }};52 (@s; $o:ident: string($e:expr $(,)?) $($t:tt)*) => {{53 $o.push_string($e);54 pi!(@s; $o: $($t)*);55 }};56 (@s; $o:ident: nl $($t:tt)*) => {{57 $o.push_signal(dprint_core::formatting::Signal::NewLine);58 pi!(@s; $o: $($t)*);59 }};60 (@s; $o:ident: tab $($t:tt)*) => {{61 $o.push_signal(dprint_core::formatting::Signal::Tab);62 pi!(@s; $o: $($t)*);63 }};64 (@s; $o:ident: >i $($t:tt)*) => {{65 $o.push_signal(dprint_core::formatting::Signal::StartIndent);66 pi!(@s; $o: $($t)*);67 }};68 (@s; $o:ident: <i $($t:tt)*) => {{69 $o.push_signal(dprint_core::formatting::Signal::FinishIndent);70 pi!(@s; $o: $($t)*);71 }};72 (@s; $o:ident: info($v:expr) $($t:tt)*) => {{73 $o.push_info($v);74 pi!(@s; $o: $($t)*);75 }};76 (@s; $o:ident: if($s:literal, $cond:expr, $($i:tt)*) $($t:tt)*) => {{77 $o.push_condition(dprint_core::formatting::conditions::if_true(78 $s,79 $cond.clone(),80 {81 let mut o = PrintItems::new();82 p!(o, $($i)*);83 o84 },85 ));86 pi!(@s; $o: $($t)*);87 }};88 (@s; $o:ident: if_else($s:literal, $cond:expr, $($i:tt)*)($($e:tt)+) $($t:tt)*) => {{89 $o.push_condition(dprint_core::formatting::conditions::if_true_or(90 $s,91 $cond.clone(),92 {93 let mut o = PrintItems::new();94 p!(o, $($i)*);95 o96 },97 {98 let mut o = PrintItems::new();99 p!(o, $($e)*);100 o101 },102 ));103 pi!(@s; $o: $($t)*);104 }};105 (@s; $o:ident: if_not($s:literal, $cond:expr, $($e:tt)*) $($t:tt)*) => {{106 $o.push_condition(dprint_core::formatting::conditions::if_true_or(107 $s,108 $cond.clone(),109 {110 let o = PrintItems::new();111 o112 },113 {114 let mut o = PrintItems::new();115 p!(o, $($e)*);116 o117 },118 ));119 pi!(@s; $o: $($t)*);120 }};121 (@s; $o:ident: {$expr:expr} $($t:tt)*) => {{122 $expr.print($o);123 pi!(@s; $o: $($t)*);124 }};125 (@s; $o:ident: items($expr:expr) $($t:tt)*) => {{126 $o.extend($expr);127 pi!(@s; $o: $($t)*);128 }};129 (@s; $o:ident: if ($e:expr)($($then:tt)*) $($t:tt)*) => {{130 if $e {131 pi!(@s; $o: $($then)*);132 }133 pi!(@s; $o: $($t)*);134 }};135 (@s; $o:ident: ifelse ($e:expr)($($then:tt)*)($($else:tt)*) $($t:tt)*) => {{136 if $e {137 pi!(@s; $o: $($then)*);138 } else {139 pi!(@s; $o: $($else)*);140 }141 pi!(@s; $o: $($t)*);142 }};143 (@s; $i:ident:) => {}144}145macro_rules! p {146 ($o:ident, $($t:tt)*) => {147 pi!(@s; $o: $($t)*)148 };149}150pub(crate) use p;151pub(crate) use pi;152153impl<P> Printable for Option<P>154where155 P: Printable,156{157 fn print(&self, out: &mut PrintItems) {158 if let Some(v) = self {159 v.print(out);160 } else {161 p!(162 out,163 string(format!(164 "/*missing {}*/",165 type_name::<P>().replace("jrsonnet_rowan_parser::generated::nodes::", "")166 ),)167 );168 }169 }170}171172impl Printable for SyntaxToken {173 fn print(&self, out: &mut PrintItems) {174 p!(out, string(self.to_string()));175 }176}177178impl Printable for Text {179 fn print(&self, out: &mut PrintItems) {180 p!(out, string(format!("{}", self)));181 }182}183impl Printable for Number {184 fn print(&self, out: &mut PrintItems) {185 p!(out, string(format!("{}", self)));186 }187}188189impl Printable for Name {190 fn print(&self, out: &mut PrintItems) {191 p!(out, { self.ident_lit() });192 }193}194195impl Printable for DestructRest {196 fn print(&self, out: &mut PrintItems) {197 p!(out, str("..."));198 if let Some(name) = self.into() {199 p!(out, { name });200 }201 }202}203204impl Printable for Destruct {205 fn print(&self, out: &mut PrintItems) {206 match self {207 Self::DestructFull(f) => {208 p!(out, { f.name() });209 }210 Self::DestructSkip(_) => p!(out, str("?")),211 Self::DestructArray(a) => {212 p!(out, str("[") >i nl);213 for el in a.destruct_array_parts() {214 match el {215 DestructArrayPart::DestructArrayElement(e) => {216 p!(out, {e.destruct()} str(",") nl);217 }218 DestructArrayPart::DestructRest(d) => {219 p!(out, {d} str(",") nl);220 }221 }222 }223 p!(out, <i str("]"));224 }225 Self::DestructObject(o) => {226 p!(out, str("{") >i nl);227 for item in o.destruct_object_fields() {228 p!(out, { item.field() });229 if let Some(des) = item.destruct() {230 p!(out, str(": ") {des});231 }232 if let Some(def) = item.expr() {233 p!(out, str(" = ") {def});234 }235 p!(out, str(",") nl);236 }237 if let Some(rest) = o.destruct_rest() {238 p!(out, {rest} nl);239 }240 p!(out, <i str("}"));241 }242 }243 }244}245246impl Printable for FieldName {247 fn print(&self, out: &mut PrintItems) {248 match self {249 Self::FieldNameFixed(f) => {250 if let Some(id) = f.id() {251 p!(out, { id });252 } else if let Some(str) = f.text() {253 p!(out, { str });254 } else {255 p!(out, str("/*missing FieldName*/"));256 }257 }258 Self::FieldNameDynamic(d) => {259 p!(out, str("[") {d.expr()} str("]"));260 }261 }262 }263}264265impl Printable for Visibility {266 fn print(&self, out: &mut PrintItems) {267 p!(out, string(self.to_string()));268 }269}270271impl Printable for ObjLocal {272 fn print(&self, out: &mut PrintItems) {273 p!(out, str("local ") {self.bind()});274 }275}276277impl Printable for Assertion {278 fn print(&self, out: &mut PrintItems) {279 p!(out, str("assert ") {self.condition()});280 if self.colon_token().is_some() || self.message().is_some() {281 p!(out, str(": ") {self.message()});282 }283 }284}285286impl Printable for ParamsDesc {287 fn print(&self, out: &mut PrintItems) {288 p!(out, str("(") >i nl);289 for param in self.params() {290 p!(out, { param.destruct() });291 if param.assign_token().is_some() || param.expr().is_some() {292 p!(out, str(" = ") {param.expr()});293 }294 p!(out, str(",") nl);295 }296 p!(out, <i str(")"));297 }298}299impl Printable for ArgsDesc {300 fn print(&self, out: &mut PrintItems) {301 let start = LineNumber::new("start");302 let end = LineNumber::new("end");303 let multi_line = Rc::new(move |condition_context: &mut ConditionResolverContext| {304 is_multiple_lines(condition_context, start, end).map(|v| !v)305 });306 p!(out, str("(") info(start) if("start args", multi_line, >i nl));307 let (children, end_comments) = children_between::<Arg>(308 self.syntax().clone(),309 self.l_paren_token().map(Into::into).as_ref(),310 self.r_paren_token().map(Into::into).as_ref(),311 None,312 );313 let mut args = children.into_iter().peekable();314 while let Some(ele) = args.next() {315 if ele.should_start_with_newline {316 p!(out, nl);317 }318 format_comments(&ele.before_trivia, CommentLocation::AboveItem, out);319 let arg = ele.value;320 if arg.name().is_some() || arg.assign_token().is_some() {321 p!(out, {arg.name()} str(" = "));322 }323 let comma_between = if args.peek().is_some() {324 true_resolver()325 } else {326 multi_line.clone()327 };328 p!(out, {arg.expr()} if("arg comma", comma_between, str(",") if_not("between args", multi_line, str(" "))));329 format_comments(&ele.inline_trivia, CommentLocation::ItemInline, out);330 p!(out, if("between args", multi_line, nl));331 }332 if end_comments.should_start_with_newline {333 p!(out, nl);334 }335 format_comments(&end_comments.trivia, CommentLocation::EndOfItems, out);336 p!(out, if("end args", multi_line, <i info(end)) str(")"));337 }338}339impl Printable for SliceDesc {340 fn print(&self, out: &mut PrintItems) {341 p!(out, str("["));342 if self.from().is_some() {343 p!(out, { self.from() });344 }345 p!(out, str(":"));346 if self.end().is_some() {347 p!(out, { self.end().map(|e| e.expr()) });348 }349 // Keep only one : in case if we don't need step350 if self.step().is_some() {351 p!(out, str(":") {self.step().map(|e|e.expr())});352 }353 p!(out, str("]"));354 }355}356357impl Printable for Member {358 fn print(&self, out: &mut PrintItems) {359 match self {360 Self::MemberBindStmt(b) => {361 p!(out, { b.obj_local() });362 }363 Self::MemberAssertStmt(ass) => {364 p!(out, { ass.assertion() });365 }366 Self::MemberFieldNormal(n) => {367 p!(out, {n.field_name()} if(n.plus_token().is_some())({n.plus_token()}) {n.visibility()} str(" ") {n.expr()});368 }369 Self::MemberFieldMethod(m) => {370 p!(out, {m.field_name()} {m.params_desc()} {m.visibility()} str(" ") {m.expr()});371 }372 }373 }374}375376impl Printable for ObjBody {377 fn print(&self, out: &mut PrintItems) {378 match self {379 Self::ObjBodyComp(l) => {380 let (children, mut end_comments) = children_between::<Member>(381 l.syntax().clone(),382 l.l_brace_token().map(Into::into).as_ref(),383 Some(384 &(l.comp_specs()385 .next()386 .expect("at least one spec is defined")387 .syntax()388 .clone())389 .into(),390 ),391 None,392 );393 let trailing_for_comp = end_comments.extract_trailing();394 p!(out, str("{") >i nl);395 for mem in children {396 if mem.should_start_with_newline {397 p!(out, nl);398 }399 format_comments(&mem.before_trivia, CommentLocation::AboveItem, out);400 p!(out, {mem.value} str(","));401 format_comments(&mem.inline_trivia, CommentLocation::ItemInline, out);402 p!(out, nl);403 }404405 if end_comments.should_start_with_newline {406 p!(out, nl);407 }408 format_comments(&end_comments.trivia, CommentLocation::EndOfItems, out);409410 let (compspecs, end_comments) = children_between::<CompSpec>(411 l.syntax().clone(),412 l.member_comps()413 .last()414 .map(|m| m.syntax().clone())415 .map(Into::into)416 .or_else(|| l.l_brace_token().map(Into::into))417 .as_ref(),418 l.r_brace_token().map(Into::into).as_ref(),419 Some(trailing_for_comp),420 );421 for mem in compspecs {422 if mem.should_start_with_newline {423 p!(out, nl);424 }425 format_comments(&mem.before_trivia, CommentLocation::AboveItem, out);426 p!(out, { mem.value });427 format_comments(&mem.inline_trivia, CommentLocation::ItemInline, out);428 }429 if end_comments.should_start_with_newline {430 p!(out, nl);431 }432 format_comments(&end_comments.trivia, CommentLocation::EndOfItems, out);433434 p!(out, nl <i str("}"));435 }436 Self::ObjBodyMemberList(l) => {437 let (children, end_comments) = children_between::<Member>(438 l.syntax().clone(),439 l.l_brace_token().map(Into::into).as_ref(),440 l.r_brace_token().map(Into::into).as_ref(),441 None,442 );443 if children.is_empty() && end_comments.is_empty() {444 p!(out, str("{ }"));445 return;446 }447 p!(out, str("{") >i nl);448 for (i, mem) in children.into_iter().enumerate() {449 if mem.should_start_with_newline && i != 0 {450 p!(out, nl);451 }452 format_comments(&mem.before_trivia, CommentLocation::AboveItem, out);453 p!(out, {mem.value} str(","));454 format_comments(&mem.inline_trivia, CommentLocation::ItemInline, out);455 p!(out, nl);456 }457458 if end_comments.should_start_with_newline {459 p!(out, nl);460 }461 format_comments(&end_comments.trivia, CommentLocation::EndOfItems, out);462 p!(out, <i str("}"));463 }464 }465 }466}467impl Printable for UnaryOperator {468 fn print(&self, out: &mut PrintItems) {469 p!(out, string(self.text().to_string()));470 }471}472impl Printable for BinaryOperator {473 fn print(&self, out: &mut PrintItems) {474 p!(out, string(self.text().to_string()));475 }476}477impl Printable for Bind {478 fn print(&self, out: &mut PrintItems) {479 match self {480 Self::BindDestruct(d) => {481 p!(out, {d.into()} str(" = ") {d.value()});482 }483 Self::BindFunction(f) => {484 p!(out, {f.name()} {f.params()} str(" = ") {f.value()});485 }486 }487 }488}489impl Printable for Literal {490 fn print(&self, out: &mut PrintItems) {491 p!(out, string(self.syntax().to_string()));492 }493}494impl Printable for ImportKind {495 fn print(&self, out: &mut PrintItems) {496 p!(out, string(self.syntax().to_string()));497 }498}499impl Printable for ForSpec {500 fn print(&self, out: &mut PrintItems) {501 p!(out, str("for ") {self.bind()} str(" in ") {self.expr()});502 }503}504impl Printable for IfSpec {505 fn print(&self, out: &mut PrintItems) {506 p!(out, str("if ") {self.expr()});507 }508}509impl Printable for CompSpec {510 fn print(&self, out: &mut PrintItems) {511 match self {512 Self::ForSpec(f) => f.print(out),513 Self::IfSpec(i) => i.print(out),514 }515 }516}517impl Printable for Expr {518 fn print(&self, out: &mut PrintItems) {519 let (stmts, _ending) = children_between::<Stmt>(520 self.syntax().clone(),521 None,522 self.expr_base()523 .as_ref()524 .map(ExprBase::syntax)525 .cloned()526 .map(Into::into)527 .as_ref(),528 None,529 );530 for stmt in stmts {531 p!(out, { stmt.value });532 }533 p!(out, { self.expr_base() });534 let (suffixes, _ending) = children_between::<Suffix>(535 self.syntax().clone(),536 self.expr_base()537 .as_ref()538 .map(ExprBase::syntax)539 .cloned()540 .map(Into::into)541 .as_ref(),542 None,543 None,544 );545 for suffix in suffixes {546 p!(out, { suffix.value });547 }548 }549}550impl Printable for Suffix {551 fn print(&self, out: &mut PrintItems) {552 match self {553 Self::SuffixIndex(i) => {554 if i.question_mark_token().is_some() {555 p!(out, str("?"));556 }557 p!(out, str(".") {i.index()});558 }559 Self::SuffixIndexExpr(e) => {560 if e.question_mark_token().is_some() {561 p!(out, str(".?"));562 }563 p!(out, str("[") {e.index()} str("]"));564 }565 Self::SuffixSlice(d) => {566 p!(out, { d.slice_desc() });567 }568 Self::SuffixApply(a) => {569 p!(out, { a.args_desc() });570 }571 }572 }573}574impl Printable for Stmt {575 fn print(&self, out: &mut PrintItems) {576 match self {577 Self::StmtLocal(l) => {578 let (binds, end_comments) = children_between::<Bind>(579 l.syntax().clone(),580 l.local_kw_token().map(Into::into).as_ref(),581 l.semi_token().map(Into::into).as_ref(),582 None,583 );584 if binds.len() == 1 {585 let bind = &binds[0];586 format_comments(&bind.before_trivia, CommentLocation::AboveItem, out);587 p!(out, str("local ") {bind.value});588 // TODO: keep end_comments, child.inline_trivia somehow, force multiple locals formatting in case of presence?589 } else {590 p!(out,str("local") >i nl);591 for bind in binds {592 if bind.should_start_with_newline {593 p!(out, nl);594 }595 format_comments(&bind.before_trivia, CommentLocation::AboveItem, out);596 p!(out, {bind.value} str(","));597 format_comments(&bind.inline_trivia, CommentLocation::ItemInline, out);598 p!(out, nl);599 }600 if end_comments.should_start_with_newline {601 p!(out, nl);602 }603 format_comments(&end_comments.trivia, CommentLocation::EndOfItems, out);604 p!(out,<i);605 }606 p!(out,str(";") nl);607 }608 Self::StmtAssert(a) => {609 p!(out, {a.assertion()} str(";") nl);610 }611 }612 }613}614impl Printable for ExprBase {615 fn print(&self, out: &mut PrintItems) {616 match self {617 Self::ExprBinary(b) => {618 p!(out, {b.lhs_work()} str(" ") {b.binary_operator()} str(" ") {b.rhs_work()});619 }620 Self::ExprUnary(u) => p!(out, {u.unary_operator()} {u.rhs()}),621 // Self::ExprSlice(s) => {622 // p!(new: {s.expr()} {s.slice_desc()})623 // }624 // Self::ExprIndex(i) => {625 // p!(new: {i.expr()} str(".") {i.index()})626 // }627 // Self::ExprIndexExpr(i) => p!(new: {i.base()} str("[") {i.index()} str("]")),628 // Self::ExprApply(a) => {629 // let mut pi = p!(new: {a.expr()} {a.args_desc()});630 // if a.tailstrict_kw_token().is_some() {631 // p!(out,str(" tailstrict"));632 // }633 // pi634 // }635 Self::ExprObjExtend(ex) => {636 p!(out, {ex.lhs_work()} str(" ") {ex.rhs_work()});637 }638 Self::ExprParened(p) => {639 p!(out, str("(") {p.expr()} str(")"));640 }641 Self::ExprString(s) => p!(out, { s.text() }),642 Self::ExprNumber(n) => p!(out, { n.number() }),643 Self::ExprArray(a) => {644 p!(out, str("[") >i nl);645 for el in a.exprs() {646 p!(out, {el} str(",") nl);647 }648 p!(out, <i str("]"));649 }650 Self::ExprObject(obj) => {651 p!(out, { obj.obj_body() });652 }653 Self::ExprArrayComp(arr) => {654 p!(out, str("[") {arr.expr()});655 for spec in arr.comp_specs() {656 p!(out, str(" ") {spec});657 }658 p!(out, str("]"));659 }660 Self::ExprImport(v) => {661 p!(out, {v.import_kind()} str(" ") {v.text()});662 }663 Self::ExprVar(n) => p!(out, { n.name() }),664 // Self::ExprLocal(l) => {665 // }666 Self::ExprIfThenElse(ite) => {667 p!(out, str("if ") {ite.cond()} str(" then ") {ite.then().map(|t| t.expr())});668 if ite.else_kw_token().is_some() || ite.else_().is_some() {669 p!(out, str(" else ") {ite.else_().map(|t| t.expr())});670 }671 }672 Self::ExprFunction(f) => p!(out, str("function") {f.params_desc()} nl {f.expr()}),673 // Self::ExprAssert(a) => p!(new: {a.assertion()} str("; ") {a.expr()}),674 Self::ExprError(e) => p!(out, str("error ") {e.expr()}),675 Self::ExprLiteral(l) => {676 p!(out, { l.literal() });677 }678 }679 }680}681682impl Printable for SourceFile {683 fn print(&self, out: &mut PrintItems) {684 let before = trivia_before(685 self.syntax().clone(),686 self.expr()687 .map(|e| e.syntax().clone())688 .map(Into::into)689 .as_ref(),690 );691 let after = trivia_after(692 self.syntax().clone(),693 self.expr()694 .map(|e| e.syntax().clone())695 .map(Into::into)696 .as_ref(),697 );698 format_comments(&before, CommentLocation::AboveItem, out);699 p!(out, {self.expr()} nl);700 format_comments(&after, CommentLocation::EndOfItems, out);701 }702}703704struct FormatOptions {705 // 0 for hard tabs706 indent: u8,707}708fn format(input: &str, opts: &FormatOptions) -> Option<String> {709 let (parsed, errors) = jrsonnet_rowan_parser::parse(input);710 if !errors.is_empty() {711 let mut builder = hi_doc::SnippetBuilder::new(input);712 for error in errors {713 builder714 .error(hi_doc::Text::fragment(715 format!("{:?}", error.error),716 Formatting::default(),717 ))718 .range(719 error.range.start().into()720 ..=(usize::from(error.range.end()) - 1).max(error.range.start().into()),721 )722 .build();723 }724 let snippet = builder.build();725 let ansi = hi_doc::source_to_ansi(&snippet);726 eprintln!("{ansi}");727 // It is possible to recover from this failure, but the output may be broken, as formatter is free to skip728 // ERROR rowan nodes.729 // Recovery needs to be enabled for LSP, though.730 //731 // TODO: Verify how formatter interacts in cases of missing positional values, i.e `if cond then /*missing Expr*/ else residual`.732 return None;733 }734 Some(dprint_core::formatting::format(735 || {736 let mut out = PrintItems::new();737 parsed.print(&mut out);738 out739 },740 PrintOptions {741 indent_width: if opts.indent == 0 {742 // Reasonable max length for both 2 and 4 space sized tabs.743 3744 } else {745 opts.indent746 },747 max_width: 100,748 use_tabs: opts.indent == 0,749 new_line_text: "\n",750 },751 ))752}753754#[derive(Parser)]755#[allow(clippy::struct_excessive_bools)]756struct Opts {757 /// Treat input as code, reformat it instead of reading file.758 #[clap(long, short = 'e')]759 exec: bool,760 /// Path to be reformatted if `--exec` if unset, otherwise code itself.761 input: String,762 /// Replace code with formatted in-place, instead of printing it to stdout.763 /// Only applicable if `--exec` is unset.764 #[clap(long, short = 'i')]765 in_place: bool,766767 /// Exit with error if formatted does not match input768 #[arg(long)]769 test: bool,770 /// Number of spaces to indent with771 ///772 /// 0 for guess from input (default), and use hard tabs if unable to guess.773 #[arg(long, default_value = "0")]774 indent: u8,775 /// Force hard tab for indentation776 #[arg(long)]777 hard_tabs: bool,778779 /// Debug option: how many times to call reformatting in case of unstable dprint output resolution.780 ///781 /// 0 for not retrying to reformat.782 #[arg(long, default_value = "0")]783 conv_limit: usize,784}785786#[derive(thiserror::Error, Debug)]787enum Error {788 #[error("--in-place is incompatible with --exec")]789 InPlaceExec,790 #[error("io: {0}")]791 Io(#[from] io::Error),792 #[error("persist: {0}")]793 Persist(#[from] tempfile::PersistError),794 #[error("parsing failed, refusing to reformat corrupted input")]795 Parse,796}797798fn main_result() -> Result<(), Error> {799 eprintln!("jrsonnet-fmt is a prototype of a jsonnet code formatter, do not expect it to produce meaningful results right now.");800 eprintln!("It is not expected for its output to match other implementations, it will be completly separate implementation with maybe different name.");801 let mut opts = Opts::parse();802 let input = if opts.exec {803 if opts.in_place {804 return Err(Error::InPlaceExec);805 }806 opts.input.clone()807 } else {808 fs::read_to_string(&opts.input)?809 };810811 if opts.indent == 0 {812 // Sane default.813 // TODO: Implement actual guessing.814 opts.hard_tabs = true;815 }816817 let mut iteration = 0;818 let mut formatted = input.clone();819 let mut convergence_tmp;820 // https://github.com/dprint/dprint/pull/423821 loop {822 let Some(reformatted) = format(823 &formatted,824 &FormatOptions {825 indent: if opts.indent == 0 || opts.hard_tabs {826 0827 } else {828 opts.indent829 },830 },831 ) else {832 return Err(Error::Parse);833 };834 convergence_tmp = reformatted.trim().to_owned();835 if formatted == convergence_tmp {836 break;837 }838 formatted = convergence_tmp;839 if opts.conv_limit == 0 {840 break;841 }842 iteration += 1;843 assert!(iteration <= opts.conv_limit, "formatting not converged");844 }845 formatted.push('\n');846 if opts.test && formatted != input {847 process::exit(1);848 }849 if opts.in_place {850 let path = PathBuf::from(opts.input);851 let mut temp = tempfile::NamedTempFile::new_in(path.parent().expect(852 "not failed during read, this path is not a directory, and there is a parent",853 ))?;854 temp.write_all(formatted.as_bytes())?;855 temp.flush()?;856 temp.persist(&path)?;857 } else {858 print!("{formatted}");859 }860 Ok(())861}862863fn main() {864 if let Err(e) = main_result() {865 eprintln!("{e}");866 process::exit(1);867 }868}1use std::{fs, io};23use clap::Parser;4use jrsonnet_formatter::{format, FormatOptions};56#[derive(Parser)]7#[allow(clippy::struct_excessive_bools)]8struct Opts {9 /// Treat input as code, reformat it instead of reading file.10 #[clap(long, short = 'e')]11 exec: bool,12 /// Path to be reformatted if `--exec` if unset, otherwise code itself.13 input: String,14 /// Replace code with formatted in-place, instead of printing it to stdout.15 /// Only applicable if `--exec` is unset.16 #[clap(long, short = 'i')]17 in_place: bool,1819 /// Exit with error if formatted does not match input20 #[arg(long)]21 test: bool,22 /// Number of spaces to indent with23 #[arg(long, default_value = "2")]24 indent: u8,25 /// Force hard tab for indentation26 #[arg(long)]27 hard_tabs: bool,2829 /// Debug option: how many times to call reformatting in case of unstable dprint output resolution.30 ///31 /// 0 for not retrying to reformat.32 #[arg(long, default_value = "0")]33 conv_limit: usize,34}3536#[derive(thiserror::Error, Debug)]37enum Error {38 #[error("--in-place is incompatible with --exec")]39 InPlaceExec,40 #[error("io: {0}")]41 Io(#[from] io::Error),42 #[error("persist: {0}")]43 Persist(#[from] tempfile::PersistError),44 #[error("parsing failed, refusing to reformat corrupted input")]45 Parse,46}4748fn main_result() -> Result<(), Error> {49 eprintln!("jrsonnet-fmt is a prototype of a jsonnet code formatter, do not expect it to produce meaningful results right now.");50 eprintln!("It is not expected for its output to match other implementations, it will be completly separate implementation with maybe different name.");51 let mut opts = Opts::parse();52 let input = if opts.exec {53 if opts.in_place {54 return Err(Error::InPlaceExec);55 }56 opts.input.clone()57 } else {58 fs::read_to_string(&opts.input)?59 };6061 if opts.indent == 0 {62 // Sane default.63 // TODO: Implement actual guessing.64 opts.hard_tabs = true;65 }6667 let mut iteration = 0;68 let mut formatted = input.clone();69 let mut convergence_tmp;70 // https://github.com/dprint/dprint/pull/42371 loop {72 let reformatted = match format(73 &formatted,74 &FormatOptions {75 indent: if opts.indent == 0 || opts.hard_tabs {76 077 } else {78 opts.indent79 },80 },81 ) {82 Ok(v) => v,83 Err(e) => {84 let snippet = e.build();85 let ansi = hi_doc::source_to_ansi(&snippet);86 eprintln!("{ansi}");87 return Err(Error::Parse);88 }89 };90 convergence_tmp = reformatted.trim().to_owned();91 if formatted == convergence_tmp {92 break;93 }94 formatted = convergence_tmp;95 if opts.conv_limit == 0 {96 break;97 }98 iteration += 1;99 assert!(iteration <= opts.conv_limit, "formatting not converged");100 }101 formatted.push('\n');102 if opts.test && formatted != input {103 process::exit(1);104 }105 if opts.in_place {106 let path = PathBuf::from(opts.input);107 let mut temp = tempfile::NamedTempFile::new_in(path.parent().expect(108 "not failed during read, this path is not a directory, and there is a parent",109 ))?;110 temp.write_all(formatted.as_bytes())?;111 temp.flush()?;112 temp.persist(&path)?;113 } else {114 print!("{formatted}");115 }116 Ok(())117}118119fn main() {120 if let Err(e) = main_result() {121 eprintln!("{e}");122 process::exit(1);123 }124}cmds/jrsonnet-fmt/src/snapshots/jrsonnet_fmt__tests__complex_comments_snapshot.snapdiffbeforeafterboth--- a/cmds/jrsonnet-fmt/src/snapshots/jrsonnet_fmt__tests__complex_comments_snapshot.snap
+++ /dev/null
@@ -1,53 +0,0 @@
----
-source: cmds/jrsonnet-fmt/src/tests.rs
-expression: "reformat(indoc!(\"{\n\t\t comments: {\n\t\t\t_: '',\n\t\t\t// Plain comment\n\t\t\ta: '',\n\n\t\t\t# Plain comment with empty line before\n\t\t\tb: '',\n\t\t\t/*Single-line multiline comment\n\n\t\t\t*/\n\t\t\tc: '',\n\n\t\t\t/**Single-line multiline doc comment\n\n\t\t\t*/\n\t\t\tc: '',\n\n\t\t\t/**Multiline doc\n\t\t\tComment\n\t\t\t*/\n\t\t\tc: '',\n\n\t\t\t/*\n\n\tMulti-line\n\n\tcomment\n\t\t\t*/\n\t\t\td: '',\n\n\t\t\te: '', // Inline comment\n\n\t\t\tk: '',\n\n\t\t\t// Text after everything\n\t\t },\n\t\t comments2: {\n\t\t\tk: '',\n\t\t\t// Text after everything, but no newline above\n\t\t },\n spacing: {\n a: '',\n\n b: '',\n },\n noSpacing: {\n a: '',\n b: '',\n },\n }\"))"
----
-{
- comments: {
- _: '',
- // Plain comment
- a: '',
-
- # Plain comment with empty line before
- b: '',
- /* Single-line multiline comment */
- c: '',
-
- /**
- * Single-line multiline doc comment
- */
- c: '',
-
- /**
- * Multiline doc
- * Comment
- */
- c: '',
-
- /*
- Multi-line
-
- comment
- */
- d: '',
-
- e: '', // Inline comment
-
- k: '',
-
- // Text after everything
- },
- comments2: {
- k: '',
- // Text after everything, but no newline above
- },
- spacing: {
- a: '',
-
- b: '',
- },
- noSpacing: {
- a: '',
- b: '',
- },
-}
cmds/jrsonnet-fmt/src/tests.rsdiffbeforeafterboth--- a/cmds/jrsonnet-fmt/src/tests.rs
+++ /dev/null
@@ -1,79 +0,0 @@
-use dprint_core::formatting::{PrintItems, PrintOptions};
-use indoc::indoc;
-
-use crate::Printable;
-
-fn reformat(input: &str) -> String {
- let (source, _) = jrsonnet_rowan_parser::parse(input);
-
- dprint_core::formatting::format(
- || {
- let mut out = PrintItems::new();
- source.print(&mut out);
- out
- },
- PrintOptions {
- indent_width: 2,
- max_width: 100,
- use_tabs: true,
- new_line_text: "\n",
- },
- )
-}
-
-#[test]
-fn complex_comments_snapshot() {
- insta::assert_snapshot!(reformat(indoc!(
- "{
- comments: {
- _: '',
- // Plain comment
- a: '',
-
- # Plain comment with empty line before
- b: '',
- /*Single-line multiline comment
-
- */
- c: '',
-
- /**Single-line multiline doc comment
-
- */
- c: '',
-
- /**Multiline doc
- Comment
- */
- c: '',
-
- /*
-
- Multi-line
-
- comment
- */
- d: '',
-
- e: '', // Inline comment
-
- k: '',
-
- // Text after everything
- },
- comments2: {
- k: '',
- // Text after everything, but no newline above
- },
- spacing: {
- a: '',
-
- b: '',
- },
- noSpacing: {
- a: '',
- b: '',
- },
- }"
- )));
-}
crates/jrsonnet-formatter/Cargo.tomldiffbeforeafterboth--- /dev/null
+++ b/crates/jrsonnet-formatter/Cargo.toml
@@ -0,0 +1,15 @@
+[package]
+name = "jrsonnet-formatter"
+authors.workspace = true
+edition.workspace = true
+license.workspace = true
+repository.workspace = true
+version.workspace = true
+
+[dependencies]
+dprint-core.workspace = true
+hi-doc.workspace = true
+jrsonnet-rowan-parser.workspace = true
+
+[lints]
+workspace = true
crates/jrsonnet-formatter/src/children.rsdiffbeforeafterboth--- /dev/null
+++ b/crates/jrsonnet-formatter/src/children.rs
@@ -0,0 +1,251 @@
+// TODO: Return errors as trivia
+
+use std::{fmt::Debug, mem};
+
+use jrsonnet_rowan_parser::{
+ nodes::{CustomError, Trivia, TriviaKind},
+ AstNode, AstToken, SyntaxElement, SyntaxNode, TS,
+};
+
+pub type ChildTrivia = Vec<Result<Trivia, String>>;
+
+/// Node should have no non-trivia tokens before element
+pub fn trivia_before(node: SyntaxNode, end: Option<&SyntaxElement>) -> ChildTrivia {
+ let mut out = Vec::new();
+ for item in node.children_with_tokens() {
+ if Some(&item) == end {
+ break;
+ }
+
+ if let Some(trivia) = item.as_token().cloned().and_then(Trivia::cast) {
+ out.push(Ok(trivia));
+ } else if CustomError::can_cast(item.kind()) {
+ out.push(Err(item.to_string()));
+ } else if end.is_none() {
+ break;
+ } else {
+ assert!(
+ TS![, ;].contains(item.kind()),
+ "silently eaten token: {:?}",
+ item.kind()
+ );
+ }
+ }
+ out
+}
+/// Node should have no non-trivia tokens after element
+pub fn trivia_after(node: SyntaxNode, start: Option<&SyntaxElement>) -> ChildTrivia {
+ if start.is_none() {
+ return Vec::new();
+ }
+ let mut iter = node.children_with_tokens().peekable();
+ while iter.peek() != start {
+ 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) {
+ out.push(Ok(trivia));
+ } else if CustomError::can_cast(item.kind()) {
+ out.push(Err(item.to_string()));
+ } else {
+ assert!(
+ TS![, ;].contains(item.kind()),
+ "silently eaten token: {:?}",
+ item.kind()
+ );
+ }
+ }
+ out
+}
+
+pub fn children_between<T: AstNode + Debug>(
+ node: SyntaxNode,
+ start: Option<&SyntaxElement>,
+ end: Option<&SyntaxElement>,
+ trailing: Option<ChildTrivia>,
+) -> (Vec<Child<T>>, EndingComments) {
+ let mut iter = node.children_with_tokens().peekable();
+ if start.is_some() {
+ while iter.peek() != start {
+ iter.next();
+ }
+ iter.next();
+ }
+ children(
+ iter.take_while(|i| Some(i) != end),
+ start.is_none() && end.is_none(),
+ trailing,
+ )
+}
+
+pub fn should_start_with_newline(prev_inline: Option<&ChildTrivia>, tt: &ChildTrivia) -> bool {
+ count_newlines_before(tt)
+ + prev_inline
+ .map(count_newlines_after)
+ .unwrap_or_default()
+
+ // First for previous item end, second for current item
+ >= 2
+}
+
+fn count_newlines_before(tt: &ChildTrivia) -> usize {
+ let mut nl_count = 0;
+ for t in tt {
+ match t {
+ Ok(t) => match t.kind() {
+ TriviaKind::Whitespace => {
+ nl_count += t.text().bytes().filter(|b| *b == b'\n').count();
+ }
+ _ => break,
+ },
+ Err(_) => {
+ nl_count += 1;
+ }
+ }
+ }
+ nl_count
+}
+fn count_newlines_after(tt: &ChildTrivia) -> usize {
+ let mut nl_count = 0;
+ for t in tt.iter().rev() {
+ match t {
+ Ok(t) => match t.kind() {
+ TriviaKind::Whitespace => {
+ nl_count += t.text().bytes().filter(|b| *b == b'\n').count();
+ }
+ TriviaKind::SingleLineHashComment | TriviaKind::SingleLineSlashComment => {
+ nl_count += 1;
+ break;
+ }
+ _ => {}
+ },
+ Err(_) => nl_count += 1,
+ }
+ }
+ nl_count
+}
+
+pub fn children<T: AstNode + Debug>(
+ items: impl Iterator<Item = SyntaxElement>,
+ loose: bool,
+ mut trailing: Option<ChildTrivia>,
+) -> (Vec<Child<T>>, EndingComments) {
+ let mut out = Vec::new();
+ let mut current_child = None::<Child<T>>;
+ let mut next = ChildTrivia::new();
+ // Previous element ended, do not add more inline comments
+ let mut started_next = false;
+ let mut had_some = false;
+
+ for item in items {
+ if let Some(value) = item.as_node().cloned().and_then(T::cast) {
+ let before_trivia = if let Some(trailing) = trailing.take() {
+ assert!(next.is_empty());
+ trailing
+ } else {
+ mem::take(&mut next)
+ };
+ let last_child = current_child.replace(Child {
+ // First item should not start with newline
+ should_start_with_newline: had_some
+ && should_start_with_newline(
+ current_child.as_ref().map(|c| &c.inline_trivia),
+ &before_trivia,
+ ),
+ before_trivia,
+ value,
+ inline_trivia: Vec::new(),
+ });
+ if let Some(last_child) = last_child {
+ out.push(last_child);
+ }
+ had_some = true;
+ started_next = false;
+ } else if let Some(trivia) = item.as_token().cloned().and_then(Trivia::cast) {
+ let is_single_line_comment = trivia.kind() == TriviaKind::SingleLineHashComment
+ || trivia.kind() == TriviaKind::SingleLineSlashComment;
+ if trailing.is_some() {
+ // Someone have already parsed trivia for us
+ continue;
+ } else if started_next
+ || current_child.is_none()
+ || trivia.text().contains('\n') && !is_single_line_comment
+ {
+ next.push(Ok(trivia.clone()));
+ started_next = true;
+ } else {
+ let cur = current_child.as_mut().expect("checked not none");
+ cur.inline_trivia.push(Ok(trivia));
+ if is_single_line_comment {
+ started_next = true;
+ }
+ }
+ had_some = true;
+ } else if CustomError::can_cast(item.kind()) {
+ next.push(Err(item.to_string()));
+ } else if loose {
+ if had_some {
+ break;
+ }
+ started_next = true;
+ } else {
+ assert!(
+ TS![, ;].contains(item.kind()),
+ "silently eaten token: {:?}",
+ item.kind()
+ );
+ }
+ }
+
+ let ending_comments = EndingComments {
+ should_start_with_newline: should_start_with_newline(
+ current_child.as_ref().map(|c| &c.inline_trivia),
+ &next,
+ ),
+ trivia: next,
+ };
+
+ if let Some(current_child) = current_child {
+ out.push(current_child);
+ }
+
+ (out, ending_comments)
+}
+
+#[derive(Debug)]
+pub struct Child<T> {
+ /// If this child has two newlines above in source code, so it needs to have it in the output
+ pub should_start_with_newline: bool,
+ /// Comment before item, i.e
+ ///
+ /// ```ignore
+ /// // Comment
+ /// item
+ /// ```
+ pub before_trivia: ChildTrivia,
+ pub value: T,
+ /// Comment after line, but located at same line
+ ///
+ /// ```ignore
+ /// item1, // Inline comment
+ /// // Not inline comment
+ /// item2,
+ /// ```
+ pub inline_trivia: ChildTrivia,
+}
+
+pub struct EndingComments {
+ /// If this child has two newlines above in source code, so it needs to have it in the output
+ pub should_start_with_newline: bool,
+ pub trivia: ChildTrivia,
+}
+impl EndingComments {
+ pub fn is_empty(&self) -> bool {
+ !self.should_start_with_newline && self.trivia.is_empty()
+ }
+ pub fn extract_trailing(&mut self) -> ChildTrivia {
+ mem::take(&mut self.trivia)
+ }
+}
crates/jrsonnet-formatter/src/comments.rsdiffbeforeafterboth--- /dev/null
+++ b/crates/jrsonnet-formatter/src/comments.rs
@@ -0,0 +1,181 @@
+use std::string::String;
+
+use dprint_core::formatting::PrintItems;
+use jrsonnet_rowan_parser::{nodes::TriviaKind, AstToken};
+
+use crate::{children::ChildTrivia, p, pi};
+
+pub enum CommentLocation {
+ /// Above local, field, other things
+ AboveItem,
+ /// After item
+ ItemInline,
+ /// After all items in object
+ EndOfItems,
+}
+
+#[allow(clippy::too_many_lines, clippy::cognitive_complexity)]
+pub fn format_comments(comments: &ChildTrivia, loc: CommentLocation, out: &mut PrintItems) {
+ for c in comments {
+ let Ok(c) = c else {
+ let mut text = c.as_ref().unwrap_err() as &str;
+ while !text.is_empty() {
+ let pos = text.find(['\n', '\t']).unwrap_or(text.len());
+ let sliced = &text[..pos];
+ p!(out, string(sliced.to_string()));
+ text = &text[pos..];
+ if !text.is_empty() {
+ match text.as_bytes()[0] {
+ b'\n' => p!(out, nl),
+ b'\t' => p!(out, tab),
+ _ => unreachable!(),
+ }
+ text = &text[1..];
+ }
+ }
+ continue;
+ };
+ match c.kind() {
+ TriviaKind::Whitespace => {}
+ TriviaKind::MultiLineComment => {
+ let mut text = c
+ .text()
+ .strip_prefix("/*")
+ .expect("ml comment starts with /*")
+ .strip_suffix("*/")
+ .expect("ml comment ends with */");
+ // doc-style comment, /**
+ let doc = if text.starts_with('*') {
+ text = &text[1..];
+ true
+ } else {
+ false
+ };
+ // Is comment starts with text immediatly, i.e /*text
+ let mut immediate_start = true;
+ let mut lines = text
+ .split('\n')
+ .map(|l| l.trim_end().to_string())
+ .skip_while(|l| {
+ if l.is_empty() {
+ immediate_start = false;
+ true
+ } else {
+ false
+ }
+ })
+ .collect::<Vec<_>>();
+ while lines.last().is_some_and(String::is_empty) {
+ lines.pop();
+ }
+ if lines.len() == 1 && !doc {
+ if matches!(loc, CommentLocation::ItemInline) {
+ p!(out, str(" "));
+ }
+ p!(out, str("/* ") string(lines[0].trim().to_string()) str(" */") nl);
+ } else if !lines.is_empty() {
+ fn common_ws_prefix<'a>(a: &'a str, b: &str) -> &'a str {
+ let offset = a
+ .bytes()
+ .zip(b.bytes())
+ .take_while(|(a, b)| a == b && (a.is_ascii_whitespace() || *a == b'*'))
+ .count();
+ &a[..offset]
+ }
+ // First line is not empty, extract ws prefix of it
+ let mut common_ws_padding = (if immediate_start && lines.len() > 1 {
+ common_ws_prefix(&lines[1], &lines[1])
+ } else {
+ common_ws_prefix(&lines[0], &lines[0])
+ })
+ .to_string();
+ for line in lines
+ .iter()
+ .skip(if immediate_start { 2 } else { 1 })
+ .filter(|l| !l.is_empty())
+ {
+ common_ws_padding = common_ws_prefix(&common_ws_padding, line).to_string();
+ }
+ for line in lines
+ .iter_mut()
+ .skip(usize::from(immediate_start))
+ .filter(|l| !l.is_empty())
+ {
+ *line = line
+ .strip_prefix(&common_ws_padding)
+ .expect("all non-empty lines start with this padding")
+ .to_string();
+ }
+
+ p!(out, str("/*"));
+ if doc {
+ p!(out, str("*"));
+ }
+ p!(out, nl);
+ for mut line in lines {
+ if doc {
+ p!(out, str(" *"));
+ }
+ if line.is_empty() {
+ p!(out, nl);
+ } else {
+ if doc {
+ p!(out, str(" "));
+ }
+ while let Some(new_line) = line.strip_prefix('\t') {
+ if doc {
+ p!(out, str(" "));
+ } else {
+ p!(out, tab);
+ }
+ line = new_line.to_string();
+ }
+ p!(out, string(line.to_string()) nl);
+ }
+ }
+ if doc {
+ p!(out, str(" "));
+ }
+ p!(out, str("*/") nl);
+ }
+ }
+ // TODO: Keep common padding for multiple continous lines of single-line comments
+ // I.e
+ // ```
+ // # Line1
+ // # Line2
+ // ```
+ // Should be reformatted as
+ // ```
+ // # Line1
+ // # Line2
+ // ```
+ // But currently comment formatter is not aware of continous comment lines, and reformats it as
+ // ```
+ // # Line1
+ // # Line2
+ // ```
+ TriviaKind::SingleLineHashComment => {
+ if matches!(loc, CommentLocation::ItemInline) {
+ p!(out, str(" "));
+ }
+ p!(out, str("# ") string(c.text().strip_prefix('#').expect("hash comment starts with #").trim().to_string()));
+ if !matches!(loc, CommentLocation::ItemInline) {
+ p!(out, nl);
+ }
+ }
+ TriviaKind::SingleLineSlashComment => {
+ if matches!(loc, CommentLocation::ItemInline) {
+ p!(out, str(" "));
+ }
+ p!(out, str("// ") string(c.text().strip_prefix("//").expect("comment starts with //").trim().to_string()));
+ if !matches!(loc, CommentLocation::ItemInline) {
+ p!(out, nl);
+ }
+ }
+ // Garbage in - garbage out
+ TriviaKind::ErrorCommentTooShort => p!(out, str("/*/")),
+ TriviaKind::ErrorCommentUnterminated => p!(out, string(c.text().to_string())),
+ }
+ }
+}
crates/jrsonnet-formatter/src/lib.rsdiffbeforeafterboth--- /dev/null
+++ b/crates/jrsonnet-formatter/src/lib.rs
@@ -0,0 +1,740 @@
+use std::{any::type_name, rc::Rc};
+
+use children::{children_between, trivia_before};
+use dprint_core::formatting::{
+ condition_helpers::is_multiple_lines, condition_resolvers::true_resolver,
+ ConditionResolverContext, LineNumber, PrintItems, PrintOptions,
+};
+use hi_doc::{Formatting, SnippetBuilder};
+use jrsonnet_rowan_parser::{
+ nodes::{
+ Arg, ArgsDesc, Assertion, BinaryOperator, Bind, CompSpec, Destruct, DestructArrayPart,
+ DestructRest, Expr, ExprBase, FieldName, ForSpec, IfSpec, ImportKind, Literal, Member,
+ Name, Number, ObjBody, ObjLocal, ParamsDesc, SliceDesc, SourceFile, Stmt, Suffix, Text,
+ UnaryOperator, Visibility,
+ },
+ AstNode, AstToken as _, SyntaxToken,
+};
+
+use crate::{
+ children::trivia_after,
+ comments::{format_comments, CommentLocation},
+};
+
+mod children;
+mod comments;
+#[cfg(test)]
+mod tests;
+
+pub trait Printable {
+ fn print(&self, out: &mut PrintItems);
+}
+
+macro_rules! pi {
+ (@i; $($t:tt)*) => {{
+ #[allow(unused_mut)]
+ let mut o = dprint_core::formatting::PrintItems::new();
+ pi!(@s; o: $($t)*);
+ o
+ }};
+ (@s; $o:ident: str($e:expr $(,)?) $($t:tt)*) => {{
+ $o.push_string($e.to_owned());
+ pi!(@s; $o: $($t)*);
+ }};
+ (@s; $o:ident: string($e:expr $(,)?) $($t:tt)*) => {{
+ $o.push_string($e);
+ pi!(@s; $o: $($t)*);
+ }};
+ (@s; $o:ident: nl $($t:tt)*) => {{
+ $o.push_signal(dprint_core::formatting::Signal::NewLine);
+ pi!(@s; $o: $($t)*);
+ }};
+ (@s; $o:ident: tab $($t:tt)*) => {{
+ $o.push_signal(dprint_core::formatting::Signal::Tab);
+ pi!(@s; $o: $($t)*);
+ }};
+ (@s; $o:ident: >i $($t:tt)*) => {{
+ $o.push_signal(dprint_core::formatting::Signal::StartIndent);
+ pi!(@s; $o: $($t)*);
+ }};
+ (@s; $o:ident: <i $($t:tt)*) => {{
+ $o.push_signal(dprint_core::formatting::Signal::FinishIndent);
+ pi!(@s; $o: $($t)*);
+ }};
+ (@s; $o:ident: info($v:expr) $($t:tt)*) => {{
+ $o.push_info($v);
+ pi!(@s; $o: $($t)*);
+ }};
+ (@s; $o:ident: if($s:literal, $cond:expr, $($i:tt)*) $($t:tt)*) => {{
+ $o.push_condition(dprint_core::formatting::conditions::if_true(
+ $s,
+ $cond.clone(),
+ {
+ let mut o = PrintItems::new();
+ p!(o, $($i)*);
+ o
+ },
+ ));
+ pi!(@s; $o: $($t)*);
+ }};
+ (@s; $o:ident: if_else($s:literal, $cond:expr, $($i:tt)*)($($e:tt)+) $($t:tt)*) => {{
+ $o.push_condition(dprint_core::formatting::conditions::if_true_or(
+ $s,
+ $cond.clone(),
+ {
+ let mut o = PrintItems::new();
+ p!(o, $($i)*);
+ o
+ },
+ {
+ let mut o = PrintItems::new();
+ p!(o, $($e)*);
+ o
+ },
+ ));
+ pi!(@s; $o: $($t)*);
+ }};
+ (@s; $o:ident: if_not($s:literal, $cond:expr, $($e:tt)*) $($t:tt)*) => {{
+ $o.push_condition(dprint_core::formatting::conditions::if_true_or(
+ $s,
+ $cond.clone(),
+ {
+ let o = PrintItems::new();
+ o
+ },
+ {
+ let mut o = PrintItems::new();
+ p!(o, $($e)*);
+ o
+ },
+ ));
+ pi!(@s; $o: $($t)*);
+ }};
+ (@s; $o:ident: {$expr:expr} $($t:tt)*) => {{
+ $expr.print($o);
+ pi!(@s; $o: $($t)*);
+ }};
+ (@s; $o:ident: items($expr:expr) $($t:tt)*) => {{
+ $o.extend($expr);
+ pi!(@s; $o: $($t)*);
+ }};
+ (@s; $o:ident: if ($e:expr)($($then:tt)*) $($t:tt)*) => {{
+ if $e {
+ pi!(@s; $o: $($then)*);
+ }
+ pi!(@s; $o: $($t)*);
+ }};
+ (@s; $o:ident: ifelse ($e:expr)($($then:tt)*)($($else:tt)*) $($t:tt)*) => {{
+ if $e {
+ pi!(@s; $o: $($then)*);
+ } else {
+ pi!(@s; $o: $($else)*);
+ }
+ pi!(@s; $o: $($t)*);
+ }};
+ (@s; $i:ident:) => {}
+}
+macro_rules! p {
+ ($o:ident, $($t:tt)*) => {
+ pi!(@s; $o: $($t)*)
+ };
+}
+pub(crate) use p;
+pub(crate) use pi;
+
+impl<P> Printable for Option<P>
+where
+ P: Printable,
+{
+ fn print(&self, out: &mut PrintItems) {
+ if let Some(v) = self {
+ v.print(out);
+ } else {
+ p!(
+ out,
+ string(format!(
+ "/*missing {}*/",
+ type_name::<P>().replace("jrsonnet_rowan_parser::generated::nodes::", "")
+ ),)
+ );
+ }
+ }
+}
+
+impl Printable for SyntaxToken {
+ fn print(&self, out: &mut PrintItems) {
+ p!(out, string(self.to_string()));
+ }
+}
+
+impl Printable for Text {
+ fn print(&self, out: &mut PrintItems) {
+ p!(out, string(format!("{}", self)));
+ }
+}
+impl Printable for Number {
+ fn print(&self, out: &mut PrintItems) {
+ p!(out, string(format!("{}", self)));
+ }
+}
+
+impl Printable for Name {
+ fn print(&self, out: &mut PrintItems) {
+ p!(out, { self.ident_lit() });
+ }
+}
+
+impl Printable for DestructRest {
+ fn print(&self, out: &mut PrintItems) {
+ p!(out, str("..."));
+ if let Some(name) = self.into() {
+ p!(out, { name });
+ }
+ }
+}
+
+impl Printable for Destruct {
+ fn print(&self, out: &mut PrintItems) {
+ match self {
+ Self::DestructFull(f) => {
+ p!(out, { f.name() });
+ }
+ Self::DestructSkip(_) => p!(out, str("?")),
+ Self::DestructArray(a) => {
+ p!(out, str("[") >i nl);
+ for el in a.destruct_array_parts() {
+ match el {
+ DestructArrayPart::DestructArrayElement(e) => {
+ p!(out, {e.destruct()} str(",") nl);
+ }
+ DestructArrayPart::DestructRest(d) => {
+ p!(out, {d} str(",") nl);
+ }
+ }
+ }
+ p!(out, <i str("]"));
+ }
+ Self::DestructObject(o) => {
+ p!(out, str("{") >i nl);
+ for item in o.destruct_object_fields() {
+ p!(out, { item.field() });
+ if let Some(des) = item.destruct() {
+ p!(out, str(": ") {des});
+ }
+ if let Some(def) = item.expr() {
+ p!(out, str(" = ") {def});
+ }
+ p!(out, str(",") nl);
+ }
+ if let Some(rest) = o.destruct_rest() {
+ p!(out, {rest} nl);
+ }
+ p!(out, <i str("}"));
+ }
+ }
+ }
+}
+
+impl Printable for FieldName {
+ fn print(&self, out: &mut PrintItems) {
+ match self {
+ Self::FieldNameFixed(f) => {
+ if let Some(id) = f.id() {
+ p!(out, { id });
+ } else if let Some(str) = f.text() {
+ p!(out, { str });
+ } else {
+ p!(out, str("/*missing FieldName*/"));
+ }
+ }
+ Self::FieldNameDynamic(d) => {
+ p!(out, str("[") {d.expr()} str("]"));
+ }
+ }
+ }
+}
+
+impl Printable for Visibility {
+ fn print(&self, out: &mut PrintItems) {
+ p!(out, string(self.to_string()));
+ }
+}
+
+impl Printable for ObjLocal {
+ fn print(&self, out: &mut PrintItems) {
+ p!(out, str("local ") {self.bind()});
+ }
+}
+
+impl Printable for Assertion {
+ fn print(&self, out: &mut PrintItems) {
+ p!(out, str("assert ") {self.condition()});
+ if self.colon_token().is_some() || self.message().is_some() {
+ p!(out, str(": ") {self.message()});
+ }
+ }
+}
+
+impl Printable for ParamsDesc {
+ fn print(&self, out: &mut PrintItems) {
+ p!(out, str("(") >i nl);
+ for param in self.params() {
+ p!(out, { param.destruct() });
+ if param.assign_token().is_some() || param.expr().is_some() {
+ p!(out, str(" = ") {param.expr()});
+ }
+ p!(out, str(",") nl);
+ }
+ p!(out, <i str(")"));
+ }
+}
+impl Printable for ArgsDesc {
+ fn print(&self, out: &mut PrintItems) {
+ let start = LineNumber::new("start");
+ let end = LineNumber::new("end");
+ let multi_line = Rc::new(move |condition_context: &mut ConditionResolverContext| {
+ is_multiple_lines(condition_context, start, end).map(|v| !v)
+ });
+ p!(out, str("(") info(start) if("start args", multi_line, >i nl));
+ let (children, end_comments) = children_between::<Arg>(
+ self.syntax().clone(),
+ self.l_paren_token().map(Into::into).as_ref(),
+ self.r_paren_token().map(Into::into).as_ref(),
+ None,
+ );
+ let mut args = children.into_iter().peekable();
+ while let Some(ele) = args.next() {
+ if ele.should_start_with_newline {
+ p!(out, nl);
+ }
+ format_comments(&ele.before_trivia, CommentLocation::AboveItem, out);
+ let arg = ele.value;
+ if arg.name().is_some() || arg.assign_token().is_some() {
+ p!(out, {arg.name()} str(" = "));
+ }
+ let comma_between = if args.peek().is_some() {
+ true_resolver()
+ } else {
+ multi_line.clone()
+ };
+ p!(out, {arg.expr()} if("arg comma", comma_between, str(",") if_not("between args", multi_line, str(" "))));
+ format_comments(&ele.inline_trivia, CommentLocation::ItemInline, out);
+ p!(out, if("between args", multi_line, nl));
+ }
+ if end_comments.should_start_with_newline {
+ p!(out, nl);
+ }
+ format_comments(&end_comments.trivia, CommentLocation::EndOfItems, out);
+ p!(out, if("end args", multi_line, <i info(end)) str(")"));
+ }
+}
+impl Printable for SliceDesc {
+ fn print(&self, out: &mut PrintItems) {
+ p!(out, str("["));
+ if self.from().is_some() {
+ p!(out, { self.from() });
+ }
+ p!(out, str(":"));
+ if self.end().is_some() {
+ p!(out, { self.end().map(|e| e.expr()) });
+ }
+ // Keep only one : in case if we don't need step
+ if self.step().is_some() {
+ p!(out, str(":") {self.step().map(|e|e.expr())});
+ }
+ p!(out, str("]"));
+ }
+}
+
+impl Printable for Member {
+ fn print(&self, out: &mut PrintItems) {
+ match self {
+ Self::MemberBindStmt(b) => {
+ p!(out, { b.obj_local() });
+ }
+ Self::MemberAssertStmt(ass) => {
+ p!(out, { ass.assertion() });
+ }
+ Self::MemberFieldNormal(n) => {
+ p!(out, {n.field_name()} if(n.plus_token().is_some())({n.plus_token()}) {n.visibility()} str(" ") {n.expr()});
+ }
+ Self::MemberFieldMethod(m) => {
+ p!(out, {m.field_name()} {m.params_desc()} {m.visibility()} str(" ") {m.expr()});
+ }
+ }
+ }
+}
+
+impl Printable for ObjBody {
+ fn print(&self, out: &mut PrintItems) {
+ match self {
+ Self::ObjBodyComp(l) => {
+ let (children, mut end_comments) = children_between::<Member>(
+ l.syntax().clone(),
+ l.l_brace_token().map(Into::into).as_ref(),
+ Some(
+ &(l.comp_specs()
+ .next()
+ .expect("at least one spec is defined")
+ .syntax()
+ .clone())
+ .into(),
+ ),
+ None,
+ );
+ let trailing_for_comp = end_comments.extract_trailing();
+ p!(out, str("{") >i nl);
+ for mem in children {
+ if mem.should_start_with_newline {
+ p!(out, nl);
+ }
+ format_comments(&mem.before_trivia, CommentLocation::AboveItem, out);
+ p!(out, {mem.value} str(","));
+ format_comments(&mem.inline_trivia, CommentLocation::ItemInline, out);
+ p!(out, nl);
+ }
+
+ if end_comments.should_start_with_newline {
+ p!(out, nl);
+ }
+ format_comments(&end_comments.trivia, CommentLocation::EndOfItems, out);
+
+ let (compspecs, end_comments) = children_between::<CompSpec>(
+ l.syntax().clone(),
+ l.member_comps()
+ .last()
+ .map(|m| m.syntax().clone())
+ .map(Into::into)
+ .or_else(|| l.l_brace_token().map(Into::into))
+ .as_ref(),
+ l.r_brace_token().map(Into::into).as_ref(),
+ Some(trailing_for_comp),
+ );
+ for mem in compspecs {
+ if mem.should_start_with_newline {
+ p!(out, nl);
+ }
+ format_comments(&mem.before_trivia, CommentLocation::AboveItem, out);
+ p!(out, { mem.value });
+ format_comments(&mem.inline_trivia, CommentLocation::ItemInline, out);
+ }
+ if end_comments.should_start_with_newline {
+ p!(out, nl);
+ }
+ format_comments(&end_comments.trivia, CommentLocation::EndOfItems, out);
+
+ p!(out, nl <i str("}"));
+ }
+ Self::ObjBodyMemberList(l) => {
+ let (children, end_comments) = children_between::<Member>(
+ l.syntax().clone(),
+ l.l_brace_token().map(Into::into).as_ref(),
+ l.r_brace_token().map(Into::into).as_ref(),
+ None,
+ );
+ if children.is_empty() && end_comments.is_empty() {
+ p!(out, str("{ }"));
+ return;
+ }
+ p!(out, str("{") >i nl);
+ for (i, mem) in children.into_iter().enumerate() {
+ if mem.should_start_with_newline && i != 0 {
+ p!(out, nl);
+ }
+ format_comments(&mem.before_trivia, CommentLocation::AboveItem, out);
+ p!(out, {mem.value} str(","));
+ format_comments(&mem.inline_trivia, CommentLocation::ItemInline, out);
+ p!(out, nl);
+ }
+
+ if end_comments.should_start_with_newline {
+ p!(out, nl);
+ }
+ format_comments(&end_comments.trivia, CommentLocation::EndOfItems, out);
+ p!(out, <i str("}"));
+ }
+ }
+ }
+}
+impl Printable for UnaryOperator {
+ fn print(&self, out: &mut PrintItems) {
+ p!(out, string(self.text().to_string()));
+ }
+}
+impl Printable for BinaryOperator {
+ fn print(&self, out: &mut PrintItems) {
+ p!(out, string(self.text().to_string()));
+ }
+}
+impl Printable for Bind {
+ fn print(&self, out: &mut PrintItems) {
+ match self {
+ Self::BindDestruct(d) => {
+ p!(out, {d.into()} str(" = ") {d.value()});
+ }
+ Self::BindFunction(f) => {
+ p!(out, {f.name()} {f.params()} str(" = ") {f.value()});
+ }
+ }
+ }
+}
+impl Printable for Literal {
+ fn print(&self, out: &mut PrintItems) {
+ p!(out, string(self.syntax().to_string()));
+ }
+}
+impl Printable for ImportKind {
+ fn print(&self, out: &mut PrintItems) {
+ p!(out, string(self.syntax().to_string()));
+ }
+}
+impl Printable for ForSpec {
+ fn print(&self, out: &mut PrintItems) {
+ p!(out, str("for ") {self.bind()} str(" in ") {self.expr()});
+ }
+}
+impl Printable for IfSpec {
+ fn print(&self, out: &mut PrintItems) {
+ p!(out, str("if ") {self.expr()});
+ }
+}
+impl Printable for CompSpec {
+ fn print(&self, out: &mut PrintItems) {
+ match self {
+ Self::ForSpec(f) => f.print(out),
+ Self::IfSpec(i) => i.print(out),
+ }
+ }
+}
+impl Printable for Expr {
+ fn print(&self, out: &mut PrintItems) {
+ let (stmts, _ending) = children_between::<Stmt>(
+ self.syntax().clone(),
+ None,
+ self.expr_base()
+ .as_ref()
+ .map(ExprBase::syntax)
+ .cloned()
+ .map(Into::into)
+ .as_ref(),
+ None,
+ );
+ for stmt in stmts {
+ p!(out, { stmt.value });
+ }
+ p!(out, { self.expr_base() });
+ let (suffixes, _ending) = children_between::<Suffix>(
+ self.syntax().clone(),
+ self.expr_base()
+ .as_ref()
+ .map(ExprBase::syntax)
+ .cloned()
+ .map(Into::into)
+ .as_ref(),
+ None,
+ None,
+ );
+ for suffix in suffixes {
+ p!(out, { suffix.value });
+ }
+ }
+}
+impl Printable for Suffix {
+ fn print(&self, out: &mut PrintItems) {
+ match self {
+ Self::SuffixIndex(i) => {
+ if i.question_mark_token().is_some() {
+ p!(out, str("?"));
+ }
+ p!(out, str(".") {i.index()});
+ }
+ Self::SuffixIndexExpr(e) => {
+ if e.question_mark_token().is_some() {
+ p!(out, str(".?"));
+ }
+ p!(out, str("[") {e.index()} str("]"));
+ }
+ Self::SuffixSlice(d) => {
+ p!(out, { d.slice_desc() });
+ }
+ Self::SuffixApply(a) => {
+ p!(out, { a.args_desc() });
+ }
+ }
+ }
+}
+impl Printable for Stmt {
+ fn print(&self, out: &mut PrintItems) {
+ match self {
+ Self::StmtLocal(l) => {
+ 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(),
+ None,
+ );
+ if binds.len() == 1 {
+ let bind = &binds[0];
+ format_comments(&bind.before_trivia, CommentLocation::AboveItem, out);
+ p!(out, str("local ") {bind.value});
+ // TODO: keep end_comments, child.inline_trivia somehow, force multiple locals formatting in case of presence?
+ } else {
+ p!(out,str("local") >i nl);
+ for bind in binds {
+ if bind.should_start_with_newline {
+ p!(out, nl);
+ }
+ format_comments(&bind.before_trivia, CommentLocation::AboveItem, out);
+ p!(out, {bind.value} str(","));
+ format_comments(&bind.inline_trivia, CommentLocation::ItemInline, out);
+ p!(out, nl);
+ }
+ if end_comments.should_start_with_newline {
+ p!(out, nl);
+ }
+ format_comments(&end_comments.trivia, CommentLocation::EndOfItems, out);
+ p!(out,<i);
+ }
+ p!(out,str(";") nl);
+ }
+ Self::StmtAssert(a) => {
+ p!(out, {a.assertion()} str(";") nl);
+ }
+ }
+ }
+}
+impl Printable for ExprBase {
+ fn print(&self, out: &mut PrintItems) {
+ match self {
+ Self::ExprBinary(b) => {
+ p!(out, {b.lhs_work()} str(" ") {b.binary_operator()} str(" ") {b.rhs_work()});
+ }
+ Self::ExprUnary(u) => p!(out, {u.unary_operator()} {u.rhs()}),
+ // Self::ExprSlice(s) => {
+ // p!(new: {s.expr()} {s.slice_desc()})
+ // }
+ // Self::ExprIndex(i) => {
+ // p!(new: {i.expr()} str(".") {i.index()})
+ // }
+ // Self::ExprIndexExpr(i) => p!(new: {i.base()} str("[") {i.index()} str("]")),
+ // Self::ExprApply(a) => {
+ // let mut pi = p!(new: {a.expr()} {a.args_desc()});
+ // if a.tailstrict_kw_token().is_some() {
+ // p!(out,str(" tailstrict"));
+ // }
+ // pi
+ // }
+ Self::ExprObjExtend(ex) => {
+ p!(out, {ex.lhs_work()} str(" ") {ex.rhs_work()});
+ }
+ Self::ExprParened(p) => {
+ p!(out, str("(") {p.expr()} str(")"));
+ }
+ Self::ExprString(s) => p!(out, { s.text() }),
+ Self::ExprNumber(n) => p!(out, { n.number() }),
+ Self::ExprArray(a) => {
+ p!(out, str("[") >i nl);
+ for el in a.exprs() {
+ p!(out, {el} str(",") nl);
+ }
+ p!(out, <i str("]"));
+ }
+ Self::ExprObject(obj) => {
+ p!(out, { obj.obj_body() });
+ }
+ Self::ExprArrayComp(arr) => {
+ p!(out, str("[") {arr.expr()});
+ for spec in arr.comp_specs() {
+ p!(out, str(" ") {spec});
+ }
+ p!(out, str("]"));
+ }
+ Self::ExprImport(v) => {
+ p!(out, {v.import_kind()} str(" ") {v.text()});
+ }
+ Self::ExprVar(n) => p!(out, { n.name() }),
+ // Self::ExprLocal(l) => {
+ // }
+ Self::ExprIfThenElse(ite) => {
+ p!(out, str("if ") {ite.cond()} str(" then ") {ite.then().map(|t| t.expr())});
+ if ite.else_kw_token().is_some() || ite.else_().is_some() {
+ p!(out, str(" else ") {ite.else_().map(|t| t.expr())});
+ }
+ }
+ Self::ExprFunction(f) => p!(out, str("function") {f.params_desc()} nl {f.expr()}),
+ // Self::ExprAssert(a) => p!(new: {a.assertion()} str("; ") {a.expr()}),
+ Self::ExprError(e) => p!(out, str("error ") {e.expr()}),
+ Self::ExprLiteral(l) => {
+ p!(out, { l.literal() });
+ }
+ }
+ }
+}
+
+impl Printable for SourceFile {
+ fn print(&self, out: &mut PrintItems) {
+ let before = trivia_before(
+ self.syntax().clone(),
+ self.expr()
+ .map(|e| e.syntax().clone())
+ .map(Into::into)
+ .as_ref(),
+ );
+ let after = trivia_after(
+ self.syntax().clone(),
+ self.expr()
+ .map(|e| e.syntax().clone())
+ .map(Into::into)
+ .as_ref(),
+ );
+ format_comments(&before, CommentLocation::AboveItem, out);
+ p!(out, {self.expr()} nl);
+ format_comments(&after, CommentLocation::EndOfItems, out);
+ }
+}
+
+pub struct FormatOptions {
+ // 0 for hard tabs
+ pub indent: u8,
+}
+pub fn format(input: &str, opts: &FormatOptions) -> Result<String, SnippetBuilder> {
+ let (parsed, errors) = jrsonnet_rowan_parser::parse(input);
+ if !errors.is_empty() {
+ let mut builder = hi_doc::SnippetBuilder::new(input);
+ for error in errors {
+ builder
+ .error(hi_doc::Text::fragment(
+ format!("{:?}", error.error),
+ Formatting::default(),
+ ))
+ .range(
+ error.range.start().into()
+ ..=(usize::from(error.range.end()) - 1).max(error.range.start().into()),
+ )
+ .build();
+ }
+ // let snippet = builder.build();
+ return Err(builder);
+ // It is possible to recover from this failure, but the output may be broken, as formatter is free to skip
+ // ERROR rowan nodes.
+ // Recovery needs to be enabled for LSP, though.
+ }
+ Ok(dprint_core::formatting::format(
+ || {
+ let mut out = PrintItems::new();
+ parsed.print(&mut out);
+ out
+ },
+ PrintOptions {
+ indent_width: if opts.indent == 0 {
+ // Reasonable max length for both 2 and 4 space sized tabs.
+ 3
+ } else {
+ opts.indent
+ },
+ max_width: 100,
+ use_tabs: opts.indent == 0,
+ new_line_text: "\n",
+ },
+ ))
+}
crates/jrsonnet-formatter/src/snapshots/jrsonnet_fmt__tests__complex_comments_snapshot.snapdiffbeforeafterboth--- /dev/null
+++ b/crates/jrsonnet-formatter/src/snapshots/jrsonnet_fmt__tests__complex_comments_snapshot.snap
@@ -0,0 +1,53 @@
+---
+source: cmds/jrsonnet-fmt/src/tests.rs
+expression: "reformat(indoc!(\"{\n\t\t comments: {\n\t\t\t_: '',\n\t\t\t// Plain comment\n\t\t\ta: '',\n\n\t\t\t# Plain comment with empty line before\n\t\t\tb: '',\n\t\t\t/*Single-line multiline comment\n\n\t\t\t*/\n\t\t\tc: '',\n\n\t\t\t/**Single-line multiline doc comment\n\n\t\t\t*/\n\t\t\tc: '',\n\n\t\t\t/**Multiline doc\n\t\t\tComment\n\t\t\t*/\n\t\t\tc: '',\n\n\t\t\t/*\n\n\tMulti-line\n\n\tcomment\n\t\t\t*/\n\t\t\td: '',\n\n\t\t\te: '', // Inline comment\n\n\t\t\tk: '',\n\n\t\t\t// Text after everything\n\t\t },\n\t\t comments2: {\n\t\t\tk: '',\n\t\t\t// Text after everything, but no newline above\n\t\t },\n spacing: {\n a: '',\n\n b: '',\n },\n noSpacing: {\n a: '',\n b: '',\n },\n }\"))"
+---
+{
+ comments: {
+ _: '',
+ // Plain comment
+ a: '',
+
+ # Plain comment with empty line before
+ b: '',
+ /* Single-line multiline comment */
+ c: '',
+
+ /**
+ * Single-line multiline doc comment
+ */
+ c: '',
+
+ /**
+ * Multiline doc
+ * Comment
+ */
+ c: '',
+
+ /*
+ Multi-line
+
+ comment
+ */
+ d: '',
+
+ e: '', // Inline comment
+
+ k: '',
+
+ // Text after everything
+ },
+ comments2: {
+ k: '',
+ // Text after everything, but no newline above
+ },
+ spacing: {
+ a: '',
+
+ b: '',
+ },
+ noSpacing: {
+ a: '',
+ b: '',
+ },
+}
crates/jrsonnet-formatter/src/tests.rsdiffbeforeafterboth--- /dev/null
+++ b/crates/jrsonnet-formatter/src/tests.rs
@@ -0,0 +1,79 @@
+use dprint_core::formatting::{PrintItems, PrintOptions};
+use indoc::indoc;
+
+use crate::Printable;
+
+fn reformat(input: &str) -> String {
+ let (source, _) = jrsonnet_rowan_parser::parse(input);
+
+ dprint_core::formatting::format(
+ || {
+ let mut out = PrintItems::new();
+ source.print(&mut out);
+ out
+ },
+ PrintOptions {
+ indent_width: 2,
+ max_width: 100,
+ use_tabs: true,
+ new_line_text: "\n",
+ },
+ )
+}
+
+#[test]
+fn complex_comments_snapshot() {
+ insta::assert_snapshot!(reformat(indoc!(
+ "{
+ comments: {
+ _: '',
+ // Plain comment
+ a: '',
+
+ # Plain comment with empty line before
+ b: '',
+ /*Single-line multiline comment
+
+ */
+ c: '',
+
+ /**Single-line multiline doc comment
+
+ */
+ c: '',
+
+ /**Multiline doc
+ Comment
+ */
+ c: '',
+
+ /*
+
+ Multi-line
+
+ comment
+ */
+ d: '',
+
+ e: '', // Inline comment
+
+ k: '',
+
+ // Text after everything
+ },
+ comments2: {
+ k: '',
+ // Text after everything, but no newline above
+ },
+ spacing: {
+ a: '',
+
+ b: '',
+ },
+ noSpacing: {
+ a: '',
+ b: '',
+ },
+ }"
+ )));
+}