Skip to content

Commit aad06a6

Browse files
denbezrukovDenis.Bezrukov
authored and
Denis.Bezrukov
committed
fix(rome_js_formatter): in JSX, some spaces are removed rome#3211
1 parent de1392c commit aad06a6

File tree

5 files changed

+329
-27
lines changed

5 files changed

+329
-27
lines changed

crates/rome_js_formatter/src/jsx/lists/child_list.rs

+9-3
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ use crate::utils::jsx::{
66
use crate::JsFormatter;
77
use rome_formatter::{format_args, write, FormatRuleWithOptions, VecBuffer};
88
use rome_js_syntax::{JsxAnyChild, JsxChildList};
9+
use rome_rowan::TextSize;
910

1011
#[derive(Debug, Clone, Default)]
1112
pub struct FormatJsxChildList {
@@ -142,7 +143,7 @@ impl FormatRule<JsxChildList> for FormatJsxChildList {
142143
// Any child that isn't text
143144
JsxChild::NonText(non_text) => {
144145
let line_mode = match children_iter.peek() {
145-
Some(JsxChild::Newline | JsxChild::Word(_) | JsxChild::Whitespace) => {
146+
Some(JsxChild::Word(word)) => {
146147
// Break if the current or next element is a self closing element
147148
// ```javascript
148149
// <pre className="h-screen overflow-y-scroll" />adefg
@@ -152,16 +153,21 @@ impl FormatRule<JsxChildList> for FormatJsxChildList {
152153
// <pre className="h-screen overflow-y-scroll" />
153154
// adefg
154155
// ```
155-
if matches!(non_text, JsxAnyChild::JsxSelfClosingElement(_)) {
156+
if matches!(non_text, JsxAnyChild::JsxSelfClosingElement(_))
157+
&& word.len() != TextSize::from(1)
158+
{
156159
Some(LineMode::Hard)
157160
} else {
158161
Some(LineMode::Soft)
159162
}
160163
}
161164

162165
// Add a hard line break if what comes after the element is not a text or is all whitespace
163-
Some(_) => Some(LineMode::Hard),
166+
Some(JsxChild::NonText(_)) => Some(LineMode::Hard),
164167

168+
Some(JsxChild::Newline | JsxChild::Whitespace | JsxChild::EmptyLine) => {
169+
None
170+
}
165171
// Don't insert trailing line breaks
166172
None => None,
167173
};

crates/rome_js_formatter/src/lib.rs

+2-1
Original file line numberDiff line numberDiff line change
@@ -751,7 +751,8 @@ function() {
751751
// use this test check if your snippet prints as you wish, without using a snapshot
752752
fn quick_test() {
753753
let src = r#"
754-
type C = B & (C | A) & B;
754+
const a = <><b/>a{' '}{" "}{" "}{" "}c{" "}{" "}{" "}{" "}{" "}{" "}</>;
755+
755756
756757
"#;
757758
let syntax = SourceType::tsx();

crates/rome_js_formatter/src/utils/jsx.rs

+100-18
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ impl Format<JsFormatContext> for JsxSpace {
122122
write![
123123
formatter,
124124
[
125-
if_group_breaks(&format_args![JsxRawSpace, hard_line_break()]),
125+
if_group_breaks(&format_args![JsxRawSpace, soft_line_break()]),
126126
if_group_fits_on_line(&space())
127127
]
128128
]
@@ -173,7 +173,7 @@ pub(crate) fn jsx_split_children<I>(children: I) -> SyntaxResult<Vec<JsxChild>>
173173
where
174174
I: IntoIterator<Item = JsxAnyChild>,
175175
{
176-
let mut result = Vec::new();
176+
let mut builder = JsxSplitChildrenBuilder::default();
177177

178178
for child in children.into_iter() {
179179
match child {
@@ -203,15 +203,15 @@ where
203203
// </div>
204204
// ```
205205
if newlines > 1 {
206-
result.push(JsxChild::EmptyLine);
206+
builder.entry(JsxChild::EmptyLine);
207207
}
208208

209209
continue;
210210
}
211211

212-
result.push(JsxChild::Newline)
213-
} else if !matches!(result.last(), Some(JsxChild::Whitespace)) {
214-
result.push(JsxChild::Whitespace)
212+
builder.entry(JsxChild::Newline)
213+
} else {
214+
builder.entry(JsxChild::Whitespace)
215215
}
216216
}
217217
_ => unreachable!(),
@@ -224,15 +224,15 @@ where
224224
// Only handle trailing whitespace. Words must always be joined by new lines
225225
if chunks.peek().is_none() {
226226
if whitespace.contains('\n') {
227-
result.push(JsxChild::Newline);
227+
builder.entry(JsxChild::Newline);
228228
} else {
229-
result.push(JsxChild::Whitespace)
229+
builder.entry(JsxChild::Whitespace)
230230
}
231231
}
232232
}
233233

234234
(relative_start, JsxTextChunk::Word(word)) => {
235-
result.push(JsxChild::Word(JsxWord {
235+
builder.entry(JsxChild::Word(JsxWord {
236236
text: value_token
237237
.token_text()
238238
.slice(TextRange::at(relative_start, word.text_len())),
@@ -245,23 +245,61 @@ where
245245

246246
JsxAnyChild::JsxExpressionChild(child) => {
247247
if is_whitespace_jsx_expression(&child) {
248-
match result.last() {
249-
Some(JsxChild::Whitespace) => {
250-
// Ignore
251-
}
252-
_ => result.push(JsxChild::Whitespace),
253-
}
248+
builder.entry(JsxChild::Whitespace)
254249
} else {
255-
result.push(JsxChild::NonText(child.into()))
250+
builder.entry(JsxChild::NonText(child.into()))
256251
}
257252
}
258253
child => {
259-
result.push(JsxChild::NonText(child));
254+
builder.entry(JsxChild::NonText(child));
255+
}
256+
}
257+
}
258+
259+
Ok(builder.finish())
260+
}
261+
262+
#[derive(Debug, Default)]
263+
struct JsxSplitChildrenBuilder {
264+
pending: Option<JsxChild>,
265+
buffer: Vec<JsxChild>,
266+
}
267+
268+
impl JsxSplitChildrenBuilder {
269+
fn is_last_whitespace(&self) -> bool {
270+
matches!(self.buffer.last(), Some(JsxChild::Whitespace))
271+
}
272+
273+
fn entry(&mut self, child: JsxChild) {
274+
if let Some(pending) = self.pending.take() {
275+
if matches!(
276+
pending,
277+
JsxChild::EmptyLine | JsxChild::Newline | JsxChild::Whitespace
278+
) {
279+
if !matches!(child, JsxChild::Whitespace) && !self.is_last_whitespace() {
280+
self.buffer.push(pending)
281+
}
282+
} else {
283+
self.buffer.push(pending);
260284
}
261285
}
286+
287+
self.pending = Some(child);
262288
}
263289

264-
Ok(result)
290+
fn finish(mut self) -> Vec<JsxChild> {
291+
if let Some(pending) = self.pending.take() {
292+
if matches!(pending, JsxChild::Whitespace) {
293+
if !self.is_last_whitespace() {
294+
self.buffer.push(pending)
295+
}
296+
} else {
297+
self.buffer.push(pending);
298+
}
299+
}
300+
301+
self.buffer
302+
}
265303
}
266304

267305
#[derive(Debug, Clone, Eq, PartialEq)]
@@ -328,6 +366,12 @@ pub(crate) struct JsxWord {
328366
source_position: TextSize,
329367
}
330368

369+
impl JsxWord {
370+
pub fn len(&self) -> TextSize {
371+
self.text.len()
372+
}
373+
}
374+
331375
impl Format<JsFormatContext> for JsxWord {
332376
fn fmt(&self, f: &mut Formatter<JsFormatContext>) -> FormatResult<()> {
333377
f.write_element(FormatElement::Text(Text::SyntaxTokenTextSlice {
@@ -594,6 +638,44 @@ mod tests {
594638
assert_eq!(children[3], JsxChild::Whitespace);
595639
}
596640

641+
#[test]
642+
fn split_children_remove_in_row_jsx_whitespaces() {
643+
let child_list = parse_jsx_children(r#"a{' '}{' '}{' '}c{" "}{' '}{" "}"#);
644+
645+
let children = jsx_split_children(&child_list).unwrap();
646+
647+
assert_eq!(
648+
4,
649+
children.len(),
650+
"Expected to contain four elements. Actual:\n{children:#?} "
651+
);
652+
assert_word(&children[0], "a");
653+
assert_eq!(children[1], JsxChild::Whitespace);
654+
assert_word(&children[2], "c");
655+
assert_eq!(children[3], JsxChild::Whitespace);
656+
}
657+
658+
#[test]
659+
fn split_children_remove_new_line_before_jsx_whitespaces() {
660+
let child_list = parse_jsx_children(
661+
r#"a
662+
{' '}c{" "}
663+
"#,
664+
);
665+
666+
let children = jsx_split_children(&child_list).unwrap();
667+
668+
assert_eq!(
669+
4,
670+
children.len(),
671+
"Expected to contain four elements. Actual:\n{children:#?} "
672+
);
673+
assert_word(&children[0], "a");
674+
assert_eq!(children[1], JsxChild::Whitespace);
675+
assert_word(&children[2], "c");
676+
assert_eq!(children[3], JsxChild::Whitespace);
677+
}
678+
597679
fn assert_word(child: &JsxChild, text: &str) {
598680
match child {
599681
JsxChild::Word(word) => {

crates/rome_js_formatter/tests/specs/jsx/element.jsx

+69
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,75 @@ c = <div>a{' '}{' '}{' '}{' '}{' '}{' '}{' '}{' '}b{' '}{' '}{' '}{' '}{' '}{' '
4141

4242
c2 = <div>a{' '}{' '}{' '}{' '}{' '}{' '}{' '}{' '}<div></div>content{' '}{' '}{' '}{' '}{' '}{' '}</div>;
4343

44+
// this group should fit one line jsx whitespaces are hidden
45+
b =
46+
<div>
47+
<a></a>
48+
49+
{' '}
50+
51+
<a></a>
52+
53+
{' '}
54+
1
55+
</div>;
56+
57+
// this group should break first tag and show only first jsx whitespace
58+
b1 =
59+
<div>
60+
<a>
61+
{`
62+
12312
63+
12312
64+
`}
65+
</a>
66+
67+
{' '}
68+
69+
<a></a>
70+
71+
{' '}
72+
1
73+
</div>;
74+
75+
// this group fit one line and hide jsx whitespace
76+
b2 =
77+
<>
78+
<a>123
79+
</a>
80+
81+
{' '}
82+
1
83+
</>;
84+
85+
// this group break group and show jsx whitespace
86+
b3 =
87+
<>
88+
<a>{`
89+
123`}
90+
</a>
91+
92+
{' '}
93+
1
94+
</>;
95+
96+
// one length text should insert soft line break
97+
b4 = <><pre />a</>;
98+
// longer than one length text should insert hard line break
99+
b5 = <><pre />ab</>;
100+
101+
// one length text should insert soft line break show last jsx whitespace
102+
b6 = <><b/>a{' '}bb{" "}cc{" "}dd{" "}ee{" "}{" "}{" "}{" "}{" "}{" "}</>;
103+
// longer than one length text should insert hard line break
104+
b7 = <><b/>longer{' '}bb{" "}cc{" "}dd{" "}ee{" "}{" "}{" "}{" "}{" "}{" "}</>;
105+
106+
const b8 = <div>
107+
Text <a data-very-long-prop-breakline-rome-playground data-other>
108+
some link
109+
</a>{' '}
110+
| some other text,{' '}
111+
</div>;
112+
44113
<div><div></div><a> jumps over the lazy dog </a></div>;
45114

46115
const Essay = () => <div>The films of Wong Kar-Wai exemplify the synthesis of French New Wave cinema—specifically the unrelenting

0 commit comments

Comments
 (0)