Skip to content

Commit 479f7a8

Browse files
authored
feat: add jsx.bracketPosition (#443)
1 parent 3e76cd1 commit 479f7a8

File tree

8 files changed

+249
-34
lines changed

8 files changed

+249
-34
lines changed

deployment/schema.json

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,21 @@
9999
"description": ""
100100
}]
101101
},
102+
"jsx.bracketPosition": {
103+
"description": "If the end angle bracket of a jsx open element or self closing element should be on the same or next line when the attributes span multiple lines.",
104+
"type": "string",
105+
"default": "nextLine",
106+
"oneOf": [{
107+
"const": "maintain",
108+
"description": "Maintains the position of the end angle bracket."
109+
}, {
110+
"const": "sameLine",
111+
"description": "Forces the end angle bracket to be on the same line."
112+
}, {
113+
"const": "nextLine",
114+
"description": "Forces the end angle bracket to be on the next line."
115+
}]
116+
},
102117
"newLineKind": {
103118
"description": "The kind of newline to use.",
104119
"type": "string",

src/configuration/builder.rs

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,14 @@ impl ConfigurationBuilder {
128128
self.insert("jsx.forceNewLinesSurroundingContent", value.into())
129129
}
130130

131+
/// If the end angle bracket of a jsx open element or self closing element
132+
/// should be on the same or next line when the attributes span multiple lines.
133+
///
134+
/// Default: `nextLine`
135+
pub fn jsx_bracket_position(&mut self, value: SameOrNextLinePosition) -> &mut Self {
136+
self.insert("jsx.bracketPosition", value.to_string().into())
137+
}
138+
131139
/// Whether statements should end in a semi-colon.
132140
///
133141
/// Default: `SemiColons::Prefer`
@@ -173,7 +181,7 @@ impl ConfigurationBuilder {
173181
/// Where to place the expression of a statement that could possibly be on one line (ex. `if (true) console.log(5);`).
174182
///
175183
/// Default: SingleBodyPosition::Maintain
176-
pub fn single_body_position(&mut self, value: SingleBodyPosition) -> &mut Self {
184+
pub fn single_body_position(&mut self, value: SameOrNextLinePosition) -> &mut Self {
177185
self.insert("singleBodyPosition", value.to_string().into())
178186
}
179187

@@ -769,23 +777,23 @@ impl ConfigurationBuilder {
769777

770778
/* single body position */
771779

772-
pub fn if_statement_single_body_position(&mut self, value: SingleBodyPosition) -> &mut Self {
780+
pub fn if_statement_single_body_position(&mut self, value: SameOrNextLinePosition) -> &mut Self {
773781
self.insert("ifStatement.singleBodyPosition", value.to_string().into())
774782
}
775783

776-
pub fn for_statement_single_body_position(&mut self, value: SingleBodyPosition) -> &mut Self {
784+
pub fn for_statement_single_body_position(&mut self, value: SameOrNextLinePosition) -> &mut Self {
777785
self.insert("forStatement.singleBodyPosition", value.to_string().into())
778786
}
779787

780-
pub fn for_in_statement_single_body_position(&mut self, value: SingleBodyPosition) -> &mut Self {
788+
pub fn for_in_statement_single_body_position(&mut self, value: SameOrNextLinePosition) -> &mut Self {
781789
self.insert("forInStatement.singleBodyPosition", value.to_string().into())
782790
}
783791

784-
pub fn for_of_statement_single_body_position(&mut self, value: SingleBodyPosition) -> &mut Self {
792+
pub fn for_of_statement_single_body_position(&mut self, value: SameOrNextLinePosition) -> &mut Self {
785793
self.insert("forOfStatement.singleBodyPosition", value.to_string().into())
786794
}
787795

788-
pub fn while_statement_single_body_position(&mut self, value: SingleBodyPosition) -> &mut Self {
796+
pub fn while_statement_single_body_position(&mut self, value: SameOrNextLinePosition) -> &mut Self {
789797
self.insert("whileStatement.singleBodyPosition", value.to_string().into())
790798
}
791799

@@ -1038,11 +1046,12 @@ mod tests {
10381046
.jsx_quote_style(JsxQuoteStyle::PreferSingle)
10391047
.jsx_multi_line_parens(JsxMultiLineParens::Never)
10401048
.jsx_force_new_lines_surrounding_content(true)
1049+
.jsx_bracket_position(SameOrNextLinePosition::Maintain)
10411050
.semi_colons(SemiColons::Prefer)
10421051
.brace_position(BracePosition::NextLine)
10431052
.next_control_flow_position(NextControlFlowPosition::SameLine)
10441053
.operator_position(OperatorPosition::SameLine)
1045-
.single_body_position(SingleBodyPosition::SameLine)
1054+
.single_body_position(SameOrNextLinePosition::SameLine)
10461055
.trailing_commas(TrailingCommas::Never)
10471056
.use_braces(UseBraces::WhenNotSingleLine)
10481057
.quote_props(QuoteProps::AsNeeded)
@@ -1122,11 +1131,11 @@ mod tests {
11221131
.conditional_expression_operator_position(OperatorPosition::SameLine)
11231132
.conditional_type_operator_position(OperatorPosition::SameLine)
11241133
/* single body position */
1125-
.if_statement_single_body_position(SingleBodyPosition::SameLine)
1126-
.for_statement_single_body_position(SingleBodyPosition::SameLine)
1127-
.for_in_statement_single_body_position(SingleBodyPosition::SameLine)
1128-
.for_of_statement_single_body_position(SingleBodyPosition::SameLine)
1129-
.while_statement_single_body_position(SingleBodyPosition::SameLine)
1134+
.if_statement_single_body_position(SameOrNextLinePosition::SameLine)
1135+
.for_statement_single_body_position(SameOrNextLinePosition::SameLine)
1136+
.for_in_statement_single_body_position(SameOrNextLinePosition::SameLine)
1137+
.for_of_statement_single_body_position(SameOrNextLinePosition::SameLine)
1138+
.while_statement_single_body_position(SameOrNextLinePosition::SameLine)
11301139
/* trailing commas */
11311140
.arguments_trailing_commas(TrailingCommas::Never)
11321141
.parameters_trailing_commas(TrailingCommas::Never)
@@ -1219,7 +1228,7 @@ mod tests {
12191228
.while_statement_space_around(true);
12201229

12211230
let inner_config = config.get_inner_config();
1222-
assert_eq!(inner_config.len(), 172);
1231+
assert_eq!(inner_config.len(), 173);
12231232
let diagnostics = resolve_config(inner_config, &resolve_global_config(ConfigKeyMap::new(), &Default::default()).config).diagnostics;
12241233
assert_eq!(diagnostics.len(), 0);
12251234
}

src/configuration/resolve_config.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ pub fn resolve_config(config: ConfigKeyMap, global_config: &GlobalConfiguration)
3636
let brace_position = get_value(&mut config, "bracePosition", BracePosition::SameLineUnlessHanging, &mut diagnostics);
3737
let next_control_flow_position = get_value(&mut config, "nextControlFlowPosition", NextControlFlowPosition::SameLine, &mut diagnostics);
3838
let operator_position = get_value(&mut config, "operatorPosition", OperatorPosition::NextLine, &mut diagnostics);
39-
let single_body_position = get_value(&mut config, "singleBodyPosition", SingleBodyPosition::Maintain, &mut diagnostics);
39+
let single_body_position = get_value(&mut config, "singleBodyPosition", SameOrNextLinePosition::Maintain, &mut diagnostics);
4040
let trailing_commas = get_value(&mut config, "trailingCommas", TrailingCommas::OnlyMultiLine, &mut diagnostics);
4141
let use_braces = get_value(&mut config, "useBraces", UseBraces::WhenNotSingleLine, &mut diagnostics);
4242
let prefer_hanging = get_value(&mut config, "preferHanging", false, &mut diagnostics);
@@ -83,6 +83,7 @@ pub fn resolve_config(config: ConfigKeyMap, global_config: &GlobalConfiguration)
8383
jsx_quote_style: get_value(&mut config, "jsx.quoteStyle", quote_style.to_jsx_quote_style(), &mut diagnostics),
8484
jsx_multi_line_parens: get_value(&mut config, "jsx.multiLineParens", JsxMultiLineParens::Prefer, &mut diagnostics),
8585
jsx_force_new_lines_surrounding_content: get_value(&mut config, "jsx.forceNewLinesSurroundingContent", false, &mut diagnostics),
86+
jsx_bracket_position: get_value(&mut config, "jsx.bracketPosition", SameOrNextLinePosition::NextLine, &mut diagnostics),
8687
member_expression_line_per_expression: get_value(&mut config, "memberExpression.linePerExpression", false, &mut diagnostics),
8788
type_literal_separator_kind_single_line: get_value(
8889
&mut config,

src/configuration/types.rs

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -109,10 +109,10 @@ pub enum OperatorPosition {
109109

110110
generate_str_to_from![OperatorPosition, [Maintain, "maintain"], [SameLine, "sameLine"], [NextLine, "nextLine"]];
111111

112-
/// Where to place the expression of a statement that could possibly be on one line (ex. `if (true) console.log(5);`).
112+
/// Where to place a node that could be on the same line or next line.
113113
#[derive(Clone, PartialEq, Copy, Serialize, Deserialize)]
114114
#[serde(rename_all = "camelCase")]
115-
pub enum SingleBodyPosition {
115+
pub enum SameOrNextLinePosition {
116116
/// Maintains the position of the expression.
117117
Maintain,
118118
/// Forces the whole statement to be on one line.
@@ -121,7 +121,7 @@ pub enum SingleBodyPosition {
121121
NextLine,
122122
}
123123

124-
generate_str_to_from![SingleBodyPosition, [Maintain, "maintain"], [SameLine, "sameLine"], [NextLine, "nextLine"]];
124+
generate_str_to_from![SameOrNextLinePosition, [Maintain, "maintain"], [SameLine, "sameLine"], [NextLine, "nextLine"]];
125125

126126
/// If braces should be used or not in certain scenarios.
127127
#[derive(Clone, PartialEq, Copy, Serialize, Deserialize)]
@@ -289,6 +289,8 @@ pub struct Configuration {
289289
pub jsx_multi_line_parens: JsxMultiLineParens,
290290
#[serde(rename = "jsx.forceNewLinesSurroundingContent")]
291291
pub jsx_force_new_lines_surrounding_content: bool,
292+
#[serde(rename = "jsx.bracketPosition")]
293+
pub jsx_bracket_position: SameOrNextLinePosition,
292294
#[serde(rename = "memberExpression.linePerExpression")]
293295
pub member_expression_line_per_expression: bool,
294296
#[serde(rename = "typeLiteral.separatorKind.singleLine")]
@@ -420,15 +422,15 @@ pub struct Configuration {
420422
pub conditional_type_operator_position: OperatorPosition,
421423
/* single body position */
422424
#[serde(rename = "ifStatement.singleBodyPosition")]
423-
pub if_statement_single_body_position: SingleBodyPosition,
425+
pub if_statement_single_body_position: SameOrNextLinePosition,
424426
#[serde(rename = "forStatement.singleBodyPosition")]
425-
pub for_statement_single_body_position: SingleBodyPosition,
427+
pub for_statement_single_body_position: SameOrNextLinePosition,
426428
#[serde(rename = "forInStatement.singleBodyPosition")]
427-
pub for_in_statement_single_body_position: SingleBodyPosition,
429+
pub for_in_statement_single_body_position: SameOrNextLinePosition,
428430
#[serde(rename = "forOfStatement.singleBodyPosition")]
429-
pub for_of_statement_single_body_position: SingleBodyPosition,
431+
pub for_of_statement_single_body_position: SameOrNextLinePosition,
430432
#[serde(rename = "whileStatement.singleBodyPosition")]
431-
pub while_statement_single_body_position: SingleBodyPosition,
433+
pub while_statement_single_body_position: SameOrNextLinePosition,
432434
/* trailing commas */
433435
#[serde(rename = "arguments.trailingCommas")]
434436
pub arguments_trailing_commas: TrailingCommas,

src/generation/generate.rs

Lines changed: 29 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -694,7 +694,7 @@ fn gen_catch_clause<'a>(node: &'a CatchClause, context: &mut Context<'a>) -> Pri
694694

695695
let try_stmt = node.parent();
696696
let single_body_position = if try_stmt.finalizer.is_some() {
697-
Some(SingleBodyPosition::NextLine)
697+
Some(SameOrNextLinePosition::NextLine)
698698
} else {
699699
None
700700
};
@@ -3583,6 +3583,7 @@ fn gen_jsx_namespaced_name<'a>(node: &'a JSXNamespacedName, context: &mut Contex
35833583
fn gen_jsx_opening_element<'a>(node: &'a JSXOpeningElement, context: &mut Context<'a>) -> PrintItems {
35843584
let space_before_self_closing_tag_slash = context.config.jsx_element_space_before_self_closing_tag_slash;
35853585
let force_use_new_lines = get_force_is_multi_line(node, context);
3586+
let prefer_newline_before_close_bracket = get_should_prefer_newline_before_close_bracket(node, context);
35863587
let start_lsil = LineStartIndentLevel::new("openingElementStart");
35873588
let mut items = PrintItems::new();
35883589

@@ -3601,6 +3602,8 @@ fn gen_jsx_opening_element<'a>(node: &'a JSXOpeningElement, context: &mut Contex
36013602
items.push_str(" ");
36023603
}
36033604
} else if !node.attrs.is_empty() {
3605+
let mut multi_line_options = ir_helpers::MultiLineOptions::surround_newlines_indented();
3606+
multi_line_options.newline_at_end = prefer_newline_before_close_bracket;
36043607
items.extend(gen_separated_values(
36053608
GenSeparatedValuesParams {
36063609
nodes: node.attrs.iter().map(|p| NodeOrSeparator::Node(p.into())).collect(),
@@ -3611,7 +3614,7 @@ fn gen_jsx_opening_element<'a>(node: &'a JSXOpeningElement, context: &mut Contex
36113614
single_line_space_at_start: true,
36123615
single_line_space_at_end,
36133616
custom_single_line_separator: None,
3614-
multi_line_options: ir_helpers::MultiLineOptions::surround_newlines_indented(),
3617+
multi_line_options,
36153618
force_possible_newline_at_start: false,
36163619
node_sorter: None,
36173620
},
@@ -3633,12 +3636,12 @@ fn gen_jsx_opening_element<'a>(node: &'a JSXOpeningElement, context: &mut Contex
36333636
}
36343637

36353638
if node.self_closing() {
3636-
if node.attrs.is_empty() && space_before_self_closing_tag_slash {
3639+
if (node.attrs.is_empty() || !prefer_newline_before_close_bracket) && space_before_self_closing_tag_slash {
36373640
items.push_str(""); // force current line indentation
36383641
items.extend(space_if_not_start_line());
36393642
}
36403643
items.push_str("/");
3641-
} else if context.config.jsx_attributes_prefer_hanging {
3644+
} else if context.config.jsx_attributes_prefer_hanging && prefer_newline_before_close_bracket {
36423645
items.push_condition(conditions::new_line_if_hanging(start_lsil, None));
36433646
}
36443647
items.push_str(">");
@@ -3655,6 +3658,20 @@ fn gen_jsx_opening_element<'a>(node: &'a JSXOpeningElement, context: &mut Contex
36553658
}
36563659
}
36573660

3661+
fn get_should_prefer_newline_before_close_bracket(node: &JSXOpeningElement, context: &mut Context) -> bool {
3662+
match context.config.jsx_bracket_position {
3663+
SameOrNextLinePosition::Maintain => {
3664+
if let Some(last_attr) = node.attrs.last() {
3665+
last_attr.end_line_fast(context.program) < node.end_line_fast(context.program)
3666+
} else {
3667+
false
3668+
}
3669+
}
3670+
SameOrNextLinePosition::NextLine => true,
3671+
SameOrNextLinePosition::SameLine => false,
3672+
}
3673+
}
3674+
36583675
fn is_jsx_attr_with_string(node: &JSXAttrOrSpread) -> bool {
36593676
if let JSXAttrOrSpread::JSXAttr(attrib) = node {
36603677
if let Some(value) = attrib.value {
@@ -4986,7 +5003,7 @@ fn gen_try_stmt<'a>(node: &'a TryStmt, context: &mut Context<'a>) -> PrintItems
49865003
body_node: node.block.into(),
49875004
use_braces: UseBraces::Always, // braces required
49885005
brace_position: context.config.try_statement_brace_position,
4989-
single_body_position: Some(SingleBodyPosition::NextLine),
5006+
single_body_position: Some(SameOrNextLinePosition::NextLine),
49905007
requires_braces_condition_ref: None,
49915008
start_header_info: None,
49925009
end_header_info: None,
@@ -5030,7 +5047,7 @@ fn gen_try_stmt<'a>(node: &'a TryStmt, context: &mut Context<'a>) -> PrintItems
50305047
body_node: finalizer.into(),
50315048
use_braces: UseBraces::Always, // braces required
50325049
brace_position,
5033-
single_body_position: Some(SingleBodyPosition::NextLine),
5050+
single_body_position: Some(SameOrNextLinePosition::NextLine),
50345051
requires_braces_condition_ref: None,
50355052
start_header_info: None,
50365053
end_header_info: None,
@@ -7949,7 +7966,7 @@ struct GenHeaderWithConditionalBraceBodyOptions<'a> {
79497966
generated_header: PrintItems,
79507967
use_braces: UseBraces,
79517968
brace_position: BracePosition,
7952-
single_body_position: Option<SingleBodyPosition>,
7969+
single_body_position: Option<SameOrNextLinePosition>,
79537970
requires_braces_condition_ref: Option<ConditionReference>,
79547971
}
79557972

@@ -7998,7 +8015,7 @@ struct GenConditionalBraceBodyOptions<'a> {
79988015
body_node: Node<'a>,
79998016
use_braces: UseBraces,
80008017
brace_position: BracePosition,
8001-
single_body_position: Option<SingleBodyPosition>,
8018+
single_body_position: Option<SameOrNextLinePosition>,
80028019
requires_braces_condition_ref: Option<ConditionReference>,
80038020
start_header_info: Option<(LineNumber, LineStartIndentLevel)>,
80048021
end_header_info: Option<LineNumber>,
@@ -8240,19 +8257,19 @@ fn gen_conditional_brace_body<'a>(opts: GenConditionalBraceBodyOptions<'a>, cont
82408257
fn get_should_use_new_line<'a>(
82418258
body_node: Node,
82428259
body_should_be_multi_line: bool,
8243-
single_body_position: &Option<SingleBodyPosition>,
8260+
single_body_position: &Option<SameOrNextLinePosition>,
82448261
context: &mut Context<'a>,
82458262
) -> bool {
82468263
if body_should_be_multi_line {
82478264
return true;
82488265
}
82498266
if let Some(single_body_position) = single_body_position {
82508267
return match single_body_position {
8251-
SingleBodyPosition::Maintain => {
8268+
SameOrNextLinePosition::Maintain => {
82528269
get_body_stmt_start_line(body_node, context) > body_node.previous_token_fast(context.program).start_line_fast(context.program)
82538270
}
8254-
SingleBodyPosition::NextLine => true,
8255-
SingleBodyPosition::SameLine => {
8271+
SameOrNextLinePosition::NextLine => true,
8272+
SameOrNextLinePosition::SameLine => {
82568273
if let Node::BlockStmt(block_stmt) = body_node {
82578274
if block_stmt.stmts.len() != 1 {
82588275
return true;
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
-- file.tsx --
2+
~~ lineWidth: 50, jsx.bracketPosition: maintain ~~
3+
== should maintain the bracket position when there's attributes ==
4+
const t = (
5+
<Test
6+
prop={5}
7+
other={10}>
8+
Test
9+
</Test>
10+
);
11+
const u = (
12+
<Test
13+
prop={5}
14+
other={10}
15+
>
16+
</Test>
17+
);
18+
const v = (
19+
<Test
20+
>
21+
</Test>
22+
);
23+
const w = (
24+
<Test
25+
prop={5}
26+
other={10}
27+
/>
28+
);
29+
const x = (
30+
<Test
31+
prop={5}
32+
other={10} />
33+
);
34+
35+
[expect]
36+
const t = (
37+
<Test
38+
prop={5}
39+
other={10}>
40+
Test
41+
</Test>
42+
);
43+
const u = (
44+
<Test
45+
prop={5}
46+
other={10}
47+
>
48+
</Test>
49+
);
50+
const v = (
51+
<Test>
52+
</Test>
53+
);
54+
const w = (
55+
<Test
56+
prop={5}
57+
other={10}
58+
/>
59+
);
60+
const x = (
61+
<Test
62+
prop={5}
63+
other={10} />
64+
);

0 commit comments

Comments
 (0)