git.delta.rocks / jrsonnet / refs/commits / d32a788bb470

difftreelog

feat(fmt) reformat text block

lvqwxktuYaroslav Bolyukin2026-02-12parent: #8bc6498.patch.diff
in: master

6 files changed

modifiedcrates/jrsonnet-formatter/src/lib.rsdiffbeforeafterboth
9};9};
10use hi_doc::{Formatting, SnippetBuilder};10use hi_doc::{Formatting, SnippetBuilder};
11use jrsonnet_rowan_parser::{11use jrsonnet_rowan_parser::{
12 collect_lexed_str_block,
12 nodes::{13 nodes::{
13 Arg, ArgsDesc, Assertion, BinaryOperator, Bind, CompSpec, Destruct, DestructArrayPart,14 Arg, ArgsDesc, Assertion, BinaryOperator, Bind, CompSpec, Destruct, DestructArrayPart,
14 DestructRest, Expr, ExprBase, FieldName, ForSpec, IfSpec, ImportKind, Literal, Member,15 DestructRest, Expr, ExprBase, FieldName, ForSpec, IfSpec, ImportKind, Literal, Member,
83 $o.push_signal(dprint_core::formatting::Signal::FinishIndent);84 $o.push_signal(dprint_core::formatting::Signal::FinishIndent);
84 pi!(@s; $o: $($t)*);85 pi!(@s; $o: $($t)*);
85 }};86 }};
87 (@s; $o:ident: >ii $($t:tt)*) => {{
88 $o.push_signal(dprint_core::formatting::Signal::StartIgnoringIndent);
89 pi!(@s; $o: $($t)*);
90 }};
91 (@s; $o:ident: <ii $($t:tt)*) => {{
92 $o.push_signal(dprint_core::formatting::Signal::FinishIgnoringIndent);
93 pi!(@s; $o: $($t)*);
94 }};
86 (@s; $o:ident: info($v:expr) $($t:tt)*) => {{95 (@s; $o:ident: info($v:expr) $($t:tt)*) => {{
87 $o.push_info($v);96 $o.push_info($v);
88 pi!(@s; $o: $($t)*);97 pi!(@s; $o: $($t)*);
201 fn print(&self, out: &mut PrintItems) {210 fn print(&self, out: &mut PrintItems) {
202 if matches!(self.kind(), TextKind::StringBlock) {211 if matches!(self.kind(), TextKind::StringBlock) {
203 let text = self.text();212 let text = self.text();
204213 let mut text = collect_lexed_str_block(&text[3..])
214 .expect("formatting is not performed on code with parsing errors");
215
216 if text.truncate && text.lines.ends_with(&[""]) {
217 text.truncate = false;
218 text.lines.pop();
219 }
220
221 p!(out, str("|||"));
222 if text.truncate {
223 p!(out, str("-"));
224 }
225 p!(out, nl > i);
205 for (i, ele) in text.split("\n").enumerate() {226 for ele in text.lines {
206 if i != 0 {227 if ele.is_empty() {
207 p!(out, nl);228 p!(out, >ii nl <ii);
208 }229 } else {
209 // TODO: Trim and recreate whitespace
210 p!(out, string(ele.to_string()));230 p!(out, string(ele.to_string()) nl);
231 }
211 }232 }
233 p!(out, <i str("|||"));
234
212 return;235 return;
213 }236 }
modifiedcrates/jrsonnet-formatter/src/snapshots/jrsonnet_formatter__tests__snapshots@string_styles.jsonnet.snapdiffbeforeafterboth
8 single_quote: 'hello world',8 single_quote: 'hello world',
9 escaped: 'line1\nline2',9 escaped: 'line1\nline2',
10 multiline: |||10 multiline: |||
11 This is a11 This is a
12
13 multiline string
14 |||,
15 multiline_truncated: |||-
16 This is a
17
12 multiline string18 multiline string with truncated newline
13 |||,19 |||,
20 multiline_to_truncated: |||
21 This is a
22
23 multiline string with to-be truncated newline
24 |||,
14}25}
1526
modifiedcrates/jrsonnet-formatter/src/tests.rsdiffbeforeafterboth
3use std::fs;3use std::fs;
44
5use dprint_core::formatting::{PrintItems, PrintOptions};5use dprint_core::formatting::{PrintItems, PrintOptions};
6use indoc::indoc;
7use insta::{assert_snapshot, glob};6use insta::{assert_snapshot, glob};
87
9use crate::Printable;8use crate::Printable;
modifiedcrates/jrsonnet-formatter/src/tests/string_styles.jsonnetdiffbeforeafterboth
4 escaped: 'line1\nline2',4 escaped: 'line1\nline2',
5 multiline: |||5 multiline: |||
6 This is a6 This is a
7
7 multiline string8 multiline string
8 |||,9 |||,
10 multiline_truncated: |||-
11 This is a
12
13 multiline string with truncated newline
14 |||,
15 multiline_to_truncated: |||-
16 This is a
17
18 multiline string with to-be truncated newline
19
20 |||,
9}21}
1022
modifiedcrates/jrsonnet-rowan-parser/src/lib.rsdiffbeforeafterboth
22pub use generated::{nodes, syntax_kinds::SyntaxKind};22pub use generated::{nodes, syntax_kinds::SyntaxKind};
23pub use language::*;23pub use language::*;
24pub use token_set::SyntaxKindSet;24pub use token_set::SyntaxKindSet;
25pub use string_block::{collect_lexed_str_block, CollectStrBlock};
2526
26use self::{27use self::{
27 ast::support,28 ast::support,
modifiedcrates/jrsonnet-rowan-parser/src/string_block.rsdiffbeforeafterboth
6 MissingIndent,6 MissingIndent,
7}7}
8
9use std::ops::Range;
108
11use logos::Lexer;9use logos::Lexer;
12use StringBlockError::*;10use StringBlockError::*;
1311
14use crate::SyntaxKind;12use crate::SyntaxKind;
1513
16pub fn lex_str_block_test(lex: &mut Lexer<SyntaxKind>) {14pub(crate) fn lex_str_block_test<'d>(lex: &mut Lexer<'d, SyntaxKind>) {
17 let _ = lex_str_block(lex);15 let _ = lex_str_block(lex);
18}16}
17
18pub(crate) struct Context<'a> {
19 source: &'a str,
20 index: usize,
21}
22
23impl<'a> Context<'a> {
24 fn rest(&self) -> &'a str {
25 &self.source[self.index..]
26 }
27
28 fn next(&mut self) -> Option<char> {
29 if self.index == self.source.len() {
30 return None;
31 }
32
33 match self.rest().chars().next() {
34 None => None,
35 Some(c) => {
36 self.index += c.len_utf8();
37 Some(c)
38 }
39 }
40 }
41
42 fn peek(&self) -> Option<char> {
43 if self.index == self.source.len() {
44 return None;
45 }
46
47 self.rest().chars().next()
48 }
49
50 fn eat_if(&mut self, f: impl Fn(char) -> bool) -> usize {
51 if self.peek().map(f).unwrap_or(false) {
52 self.index += 1;
53 return 1;
54 }
55 0
56 }
57
58 fn eat_while(&mut self, f: impl Fn(char) -> bool) -> usize {
59 if self.index == self.source.len() {
60 return 0;
61 }
62
63 let next_char = self.rest().char_indices().find(|(_, c)| !f(*c));
64
65 match next_char {
66 None => {
67 let diff = self.source.len() - self.index;
68 self.index = self.source.len();
69 diff
70 }
71 Some((idx, _)) => {
72 self.index += idx;
73 idx
74 }
75 }
76 }
77
78 fn skip(&mut self, len: usize) {
79 self.index = match self.index + len {
80 n if n > self.source.len() => self.source.len(),
81 n => n,
82 };
83 }
84}
85
86// Check that b has at least the same whitespace prefix as a and returns the
87// amount of this whitespace, otherwise returns 0. If a has no whitespace
88// prefix than return 0.
89fn check_whitespace(a: &str, b: &str) -> usize {
90 let a = a.as_bytes();
91 let b = b.as_bytes();
92
93 for i in 0..a.len() {
94 if a[i] != b' ' && a[i] != b'\t' {
95 // a has run out of whitespace and b matched up to this point. Return result.
96 return i;
97 }
98
99 if i >= b.len() {
100 // We ran off the edge of b while a still has whitespace. Return 0 as failure.
101 return 0;
102 }
103
104 if a[i] != b[i] {
105 // a has whitespace but b does not. Return 0 as failure.
106 return 0;
107 }
108 }
109
110 // We ran off the end of a and b kept up
111 a.len()
112}
113
114pub(crate) trait StrBlockLexCtx<'d> {
115 fn remainder(&self) -> &'d str;
116 fn eat_error(&mut self, ctx: &Context<'d>);
117 fn bump_pos(&mut self, s: usize);
118 fn mark_truncating(&mut self);
119 fn mark_line(&mut self, line: &'d str);
120}
121
122impl<'d> StrBlockLexCtx<'d> for Lexer<'d, SyntaxKind> {
123 fn remainder(&self) -> &'d str {
124 self.remainder()
125 }
126 fn eat_error(&mut self, ctx: &Context<'d>) {
127 let end_index = ctx
128 .rest()
129 .find("|||")
130 .map_or_else(|| ctx.rest().len(), |v| v + 3);
131 self.bump(ctx.index + end_index);
132 }
133 fn bump_pos(&mut self, s: usize) {
134 self.bump(s);
135 }
136 fn mark_truncating(&mut self) {
137 // Lexer test doesn't collect anything
138 }
139 fn mark_line(&mut self, _line: &'d str) {
140 // Lexer test doesn't collect anything
141 }
142}
143
144pub fn collect_lexed_str_block<'s>(
145 input: &'s str,
146) -> Result<CollectStrBlock<'s>, StringBlockError> {
147 let mut collect = CollectStrBlock {
148 truncate: false,
149 lines: vec![],
150 input,
151 offset: 0,
152 };
153 lex_str_block(&mut collect)?;
154 Ok(collect)
155}
156
157pub struct CollectStrBlock<'s> {
158 pub truncate: bool,
159 pub lines: Vec<&'s str>,
160 input: &'s str,
161 offset: usize,
162}
163
164impl<'d> StrBlockLexCtx<'d> for CollectStrBlock<'d> {
165 fn remainder(&self) -> &'d str {
166 self.input
167 }
168
169 fn eat_error(&mut self, _ctx: &Context<'d>) {
170 // Error will be returned, no need to record it here
171 }
172
173 fn bump_pos(&mut self, s: usize) {
174 self.offset += s;
175 }
176
177 fn mark_truncating(&mut self) {
178 self.truncate = true;
179 }
180
181 fn mark_line(&mut self, line: &'d str) {
182 self.lines.push(line)
183 }
184}
19185
20#[allow(clippy::too_many_lines)]186pub(crate) fn lex_str_block<'a>(lex: &mut impl StrBlockLexCtx<'a>) -> Result<(), StringBlockError> {
21pub fn lex_str_block(lex: &mut Lexer<SyntaxKind>) -> Result<(), StringBlockError> {187 // debug_assert_eq!(lex.slice(), "|||");
22 struct Context<'a> {
23 source: &'a str,
24 index: usize,
25 offset: usize,
26 }
27
28 impl<'a> Context<'a> {
29 fn rest(&self) -> &'a str {
30 &self.source[self.index..]
31 }
32
33 fn next(&mut self) -> Option<char> {
34 if self.index == self.source.len() {
35 return None;
36 }
37
38 match self.rest().chars().next() {
39 None => None,
40 Some(c) => {
41 self.index += c.len_utf8();
42 Some(c)
43 }
44 }
45 }
46
47 fn peek(&self) -> Option<char> {
48 if self.index == self.source.len() {
49 return None;
50 }
51
52 self.rest().chars().next()
53 }
54
55 fn eat_if(&mut self, f: impl Fn(char) -> bool) -> usize {
56 if self.peek().map(f).unwrap_or(false) {
57 self.index += 1;
58 return 1;
59 }
60 0
61 }
62
63 fn eat_while(&mut self, f: impl Fn(char) -> bool) -> usize {
64 if self.index == self.source.len() {
65 return 0;
66 }
67
68 let next_char = self.rest().char_indices().find(|(_, c)| !f(*c));
69
70 match next_char {
71 None => {
72 let diff = self.source.len() - self.index;
73 self.index = self.source.len();
74 diff
75 }
76 Some((idx, _)) => {
77 self.index += idx;
78 idx
79 }
80 }
81 }
82
83 fn skip(&mut self, len: usize) {
84 self.index = match self.index + len {
85 n if n > self.source.len() => self.source.len(),
86 n => n,
87 };
88 }
89
90 #[allow(clippy::range_plus_one)]
91 fn pos(&self) -> Range<usize> {
92 if self.index == self.source.len() {
93 self.offset + self.index..self.offset + self.index
94 } else {
95 // TODO: char size
96 self.offset + self.index..self.offset + self.index + 1
97 }
98 }
99 }
100
101 // Check that b has at least the same whitespace prefix as a and returns the
102 // amount of this whitespace, otherwise returns 0. If a has no whitespace
103 // prefix than return 0.
104 fn check_whitespace(a: &str, b: &str) -> usize {
105 let a = a.as_bytes();
106 let b = b.as_bytes();
107
108 for i in 0..a.len() {
109 if a[i] != b' ' && a[i] != b'\t' {
110 // a has run out of whitespace and b matched up to this point. Return result.
111 return i;
112 }
113
114 if i >= b.len() {
115 // We ran off the edge of b while a still has whitespace. Return 0 as failure.
116 return 0;
117 }
118
119 if a[i] != b[i] {
120 // a has whitespace but b does not. Return 0 as failure.
121 return 0;
122 }
123 }
124
125 // We ran off the end of a and b kept up
126 a.len()
127 }
128
129 fn guess_token_end_and_bump<'a>(lex: &mut Lexer<'a, SyntaxKind>, ctx: &Context<'a>) {
130 let end_index = ctx
131 .rest()
132 .find("|||")
133 .map_or_else(|| ctx.rest().len(), |v| v + 3);
134 lex.bump(ctx.index + end_index);
135 }
136
137 debug_assert_eq!(lex.slice(), "|||");
138 let mut ctx = Context {188 let mut ctx = Context::<'a> {
139 source: lex.remainder(),189 source: lex.remainder(),
140 index: 0,190 index: 0,
141 offset: lex.span().end,
142 };191 };
143192
144 ctx.eat_if(|v| v == '-');193 if ctx.eat_if(|v| v == '-') != 0 {
194 lex.mark_truncating();
195 }
145196
146 // Skip whitespaces197 // Skip whitespaces
147 ctx.eat_while(|r| r == ' ' || r == '\t' || r == '\r');198 ctx.eat_while(|r| r == ' ' || r == '\t' || r == '\r');
150 match ctx.next() {201 match ctx.next() {
151 Some('\n') => (),202 Some('\n') => (),
152 None => {203 None => {
153 guess_token_end_and_bump(lex, &ctx);204 lex.eat_error(&ctx);
154 return Err(UnexpectedEnd);205 return Err(UnexpectedEnd);
155 }206 }
156 // Text block requires new line after |||.207 // Text block requires new line after |||.
157 Some(_) => {208 Some(_) => {
158 guess_token_end_and_bump(lex, &ctx);209 lex.eat_error(&ctx);
159 return Err(MissingNewLine);210 return Err(MissingNewLine);
160 }211 }
161 }212 }
170221
171 if num_whitespace == 0 {222 if num_whitespace == 0 {
172 // Text block's first line must start with whitespace223 // Text block's first line must start with whitespace
173 guess_token_end_and_bump(lex, &ctx);224 lex.eat_error(&ctx);
174 return Err(MissingIndent);225 return Err(MissingIndent);
175 }226 }
176227
177 loop {228 loop {
178 debug_assert_ne!(num_whitespace, 0, "Unexpected value for num_whitespace");229 debug_assert_ne!(num_whitespace, 0, "Unexpected value for num_whitespace");
179 ctx.skip(num_whitespace);230 ctx.skip(num_whitespace);
180231
232 let line_start = ctx.index;
233 let mut line_size = 0;
181 loop {234 loop {
182 match ctx.next() {235 match ctx.next() {
183 None => {236 None => {
184 guess_token_end_and_bump(lex, &ctx);237 lex.eat_error(&ctx);
185 return Err(UnexpectedEnd);238 return Err(UnexpectedEnd);
186 }239 }
187 Some('\n') => break,240 Some('\n') => {
241 lex.mark_line(&ctx.source[line_start..line_start + line_size]);
242 break;
243 }
188 Some(_) => (),244 Some(c) => {
245 line_size += c.len_utf8();
246 }
189 }247 }
190 }248 }
191249
192 // Skip any blank lines250 // Skip any blank lines
193 while ctx.peek() == Some('\n') {251 while ctx.peek() == Some('\n') {
252 lex.mark_line("");
194 ctx.next();253 ctx.next();
195 }254 }
196255
206 }265 }
207266
208 if !ctx.rest().starts_with("|||") {267 if !ctx.rest().starts_with("|||") {
209 // Text block not terminated with |||
210 let pos = ctx.pos();
211 if pos.is_empty() {268 if ctx.rest().is_empty() {
212 // eof
213 lex.bump(ctx.index);269 lex.bump_pos(ctx.index);
214 return Err(UnexpectedEnd);270 return Err(UnexpectedEnd);
215 }271 }
216
217 guess_token_end_and_bump(lex, &ctx);272 lex.eat_error(&ctx);
218 return Err(MissingTermination);273 return Err(MissingTermination);
219 }274 }
220275
224 }279 }
225 }280 }
226281
227 lex.bump(ctx.index);282 lex.bump_pos(ctx.index);
228 Ok(())283 Ok(())
229}284}
230285