1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
use crate::js::auxiliary::template_element::TemplateElementOptions;
use crate::js::lists::template_element_list::{TemplateElementIndention, TemplateElementLayout};
use crate::prelude::*;
use biome_formatter::printer::Printer;
use biome_formatter::{
format_args, write, CstFormatContext, FormatOptions, RemoveSoftLinesBuffer, VecBuffer,
};
use biome_js_syntax::{AnyJsTemplateElement, JsTemplateElementList};
use biome_text_size::{TextRange, TextSize};
use std::cmp;
use unicode_width::UnicodeWidthStr;
#[derive(Debug)]
enum EachTemplateElement {
/// A significant value in the test each table. It's a row element.
Column(EachTemplateColumn),
/// Indicates the end of the current row.
LineBreak,
}
/// Row element containing the column information.
#[derive(Debug)]
struct EachTemplateColumn {
/// Formatted text of the column.
text: String,
/// Formatted text width.
width: TextSize,
/// Corresponding range for the text to replace it.
range: TextRange,
/// Indicates the line break in the text.
will_break: bool,
}
impl EachTemplateColumn {
fn new(text: String, range: TextRange, will_break: bool) -> Self {
let width = TextSize::try_from(text.width())
.expect("integer overflow while converting a text width to `TextSize`");
EachTemplateColumn {
text,
width,
range,
will_break,
}
}
}
struct EachTemplateTableBuilder {
/// Holds information about the current row.
current_row: EachTemplateCurrentRow,
/// Information about all rows.
rows: Vec<EachTemplateRow>,
/// Contains the maximum length of each column of all rows.
columns_width: Vec<TextSize>,
/// Elements for formatting.
elements: Vec<EachTemplateElement>,
}
impl EachTemplateTableBuilder {
fn new() -> Self {
EachTemplateTableBuilder {
current_row: EachTemplateCurrentRow::new(),
rows: Vec::new(),
columns_width: Vec::new(),
elements: Vec::new(),
}
}
/// Adds a new item to the buffer.
fn entry(&mut self, element: EachTemplateElement) {
match &element {
EachTemplateElement::Column(column) => {
if column.will_break {
self.current_row.has_line_break_column = true;
}
// if there was no column with a line break, then add width of the current column to the buffer
if !self.current_row.has_line_break_column {
self.current_row.column_widths.push(column.width);
}
}
EachTemplateElement::LineBreak => {
self.next_row();
}
}
self.elements.push(element);
}
/// Advance the table state to a new row.
/// Merge the current row columns width with the table ones if row doesn't contain a line break column.
fn next_row(&mut self) {
if !self.current_row.has_line_break_column {
let table_column_width_iter = self.columns_width.iter_mut();
let mut row_column_width_iter = self.current_row.column_widths.iter();
// find the maximum length between the table and the current row
for table_column_width in table_column_width_iter {
let row_column_width = match row_column_width_iter.next() {
Some(width) => width,
_ => break,
};
*table_column_width = cmp::max(*table_column_width, *row_column_width);
}
// add the remaining items to the buffer
self.columns_width.extend(row_column_width_iter);
}
// save information about the row to buffer
self.rows.push(EachTemplateRow {
has_line_break_column: self.current_row.has_line_break_column,
});
self.current_row.reset();
}
fn finish(mut self) -> EachTemplateTable {
self.next_row();
EachTemplateTable {
rows: self.rows,
columns_width: self.columns_width,
elements: self.elements,
}
}
}
#[derive(Debug)]
pub(crate) struct EachTemplateTable {
/// Information about all rows.
rows: Vec<EachTemplateRow>,
/// Contains the maximum length of each column of all rows.
columns_width: Vec<TextSize>,
/// Elements for formatting.
elements: Vec<EachTemplateElement>,
}
#[derive(Debug)]
struct EachTemplateCurrentRow {
/// Contains the maximum length of the current column.
column_widths: Vec<TextSize>,
/// Whether the current row contains a column with a line break.
has_line_break_column: bool,
}
impl EachTemplateCurrentRow {
fn new() -> Self {
EachTemplateCurrentRow {
column_widths: Vec::new(),
has_line_break_column: false,
}
}
/// Reset the state of the current row when moving to the next line.
fn reset(&mut self) {
self.column_widths.clear();
self.has_line_break_column = false;
}
}
#[derive(Debug)]
struct EachTemplateRow {
/// Whether the current row contains a column with a line break.
has_line_break_column: bool,
}
/// Separator between columns in a row.
struct EachTemplateSeparator;
impl Format<JsFormatContext> for EachTemplateSeparator {
fn fmt(&self, f: &mut Formatter<JsFormatContext>) -> FormatResult<()> {
write!(f, [text("|")])
}
}
impl EachTemplateTable {
pub(crate) fn from(list: &JsTemplateElementList, f: &mut JsFormatter) -> FormatResult<Self> {
let mut iter = list.into_iter().peekable();
let mut builder = EachTemplateTableBuilder::new();
// the table must have a header
// e.g. a | b | expected
let header = match iter.next() {
Some(AnyJsTemplateElement::JsTemplateChunkElement(header)) => header,
// we check this case in `is_test_each_pattern` and `is_test_each_pattern_elements` functions
_ => return Err(FormatError::SyntaxError),
};
// It's safe to mark the header as checked here because we check that node doesn't have any trivia
// when we call `is_test_each_pattern`
f.context()
.comments()
.mark_suppression_checked(header.syntax());
write!(f, [format_removed(&header.template_chunk_token()?)])?;
let header = header.template_chunk_token()?;
// split the header to get columns
for column in header.text_trimmed().split_terminator('|') {
let text = column.trim().to_string();
let range = header.text_range();
let column = EachTemplateColumn::new(text, range, false);
builder.entry(EachTemplateElement::Column(column));
}
builder.entry(EachTemplateElement::LineBreak);
while let Some(element) = iter.next() {
match element {
AnyJsTemplateElement::JsTemplateChunkElement(element) => {
// It's safe to mark the element as checked here because we check that node doesn't have any trivia
// when we call `is_test_each_pattern`
f.context()
.comments()
.mark_suppression_checked(element.syntax());
write!(f, [format_removed(&element.template_chunk_token()?)])?;
let has_line_break = element
.template_chunk_token()?
.text_trimmed()
.contains('\n');
let is_last = iter.peek().is_none();
// go to the next line if the current element contains a line break
if has_line_break && !is_last {
builder.entry(EachTemplateElement::LineBreak);
}
}
AnyJsTemplateElement::JsTemplateElement(element) => {
let mut vec_buffer = VecBuffer::new(f.state_mut());
// The softline buffer replaces all softline breaks with a space or removes it entirely
// to "mimic" an infinite print width
let mut buffer = RemoveSoftLinesBuffer::new(&mut vec_buffer);
let mut recording = buffer.start_recording();
let options = TemplateElementOptions {
after_new_line: false,
indention: TemplateElementIndention::default(),
layout: TemplateElementLayout::Fit,
};
// print the current column with infinite print width
write!(recording, [element.format().with_options(options)])?;
let recorded = recording.stop();
// whether there was a line break when formatting the column
let will_break = recorded.will_break();
let root = Document::from(vec_buffer.into_vec());
let range = element.range();
let print_options = f.options().as_print_options();
let printed = Printer::new(print_options).print(&root)?;
let text = printed.into_code();
let column = EachTemplateColumn::new(text, range, will_break);
builder.entry(EachTemplateElement::Column(column));
}
}
}
let each_table = builder.finish();
Ok(each_table)
}
}
impl Format<JsFormatContext> for EachTemplateTable {
fn fmt(&self, f: &mut Formatter<JsFormatContext>) -> FormatResult<()> {
let table_content = format_with(|f| {
let mut current_column: usize = 0;
let mut current_row: usize = 0;
let mut iter = self.elements.iter().peekable();
write!(f, [hard_line_break()])?;
while let Some(element) = iter.next() {
let next_item = iter.peek();
let is_last = next_item.is_none();
let is_last_in_row =
matches!(next_item, Some(EachTemplateElement::LineBreak)) || is_last;
match element {
EachTemplateElement::Column(column) => {
let mut text = column.text.to_string();
if current_column != 0 && (!is_last_in_row || !text.is_empty()) {
text = std::format!(" {text}");
}
// align the column based on the maximum column width in the table
if !is_last_in_row {
if !self.rows[current_row].has_line_break_column {
let column_width = self
.columns_width
.get(current_column)
.copied()
.unwrap_or_default();
let padding = " ".repeat(
column_width
.checked_sub(column.width)
.unwrap_or_default()
.into(),
);
text.push_str(&padding);
}
text.push(' ');
}
write!(f, [dynamic_text(&text, column.range.start())])?;
if !is_last_in_row {
write!(f, [EachTemplateSeparator])?;
}
current_column += 1;
}
EachTemplateElement::LineBreak => {
current_column = 0;
current_row += 1;
if !is_last {
write!(f, [hard_line_break()])?;
}
}
}
}
Ok(())
});
write!(f, [indent(&format_args!(table_content)), hard_line_break()])
}
}