diff --git a/deployment/schema.json b/deployment/schema.json index 1d410a78..2b965260 100644 --- a/deployment/schema.json +++ b/deployment/schema.json @@ -345,18 +345,6 @@ "description": "Maintains the line breaks as written by the programmer." }] }, - "conditionalExpression.linePerExpression": { - "description": "Whether to force a line per expression when spanning multiple lines.", - "type": "boolean", - "default": true, - "oneOf": [{ - "const": true, - "description": "Formats with each part on a new line." - }, { - "const": false, - "description": "Maintains the line breaks as written by the programmer." - }] - }, "memberExpression.linePerExpression": { "description": "Whether to force a line per expression when spanning multiple lines.", "type": "boolean", @@ -820,6 +808,54 @@ "binaryExpression.linePerExpression": { "$ref": "#/definitions/binaryExpression.linePerExpression" }, + "conditionalExpression.linePerExpression": { + "description": "Whether to force a line per expression when spanning multiple lines.", + "type": "boolean", + "default": true, + "oneOf": [{ + "const": true, + "description": "Formats with each part on a new line." + }, { + "const": false, + "description": "Maintains the line breaks as written by the programmer." + }] + }, + "conditionalType.linePerExpression": { + "description": "Whether to force a line per expression when spanning multiple lines.", + "type": "boolean", + "default": false, + "oneOf": [{ + "const": true, + "description": "Formats with each part on a new line." + }, { + "const": false, + "description": "Uses the default behaviour." + }] + }, + "conditionalExpression.useNestedIndentation": { + "description": "Whether to use nested indentation within conditional expressions.", + "type": "boolean", + "default": true, + "oneOf": [{ + "const": true, + "description": "Uses nested indentation in the true and false expressions." + }, { + "const": false, + "description": "Don't use nested indentation." + }] + }, + "conditionalType.useNestedIndentation": { + "description": "Whether to use nested indentation within conditional types.", + "type": "boolean", + "default": true, + "oneOf": [{ + "const": true, + "description": "Uses nested indentation in the true and false expressions." + }, { + "const": false, + "description": "Don't use nested indentation." + }] + }, "jsx.bracketPosition": { "$ref": "#/definitions/jsx.bracketPosition" }, diff --git a/src/configuration/builder.rs b/src/configuration/builder.rs index b6aa475d..e7210127 100644 --- a/src/configuration/builder.rs +++ b/src/configuration/builder.rs @@ -484,12 +484,20 @@ impl ConfigurationBuilder { /// Whether to force a line per expression when spanning multiple lines. /// - /// * `true` - Formats with each part on a new line. - /// * `false` (default) - Maintains the line breaks as written by the programmer. + /// * `true` (default) - Formats with each part on a new line. + /// * `false` - Maintains the line breaks as written by the programmer. pub fn conditional_expression_line_per_expression(&mut self, value: bool) -> &mut Self { self.insert("conditionalExpression.linePerExpression", value.into()) } + /// Whether to force a line per expression when spanning multiple lines. + /// + /// * `true` - Formats with each part on a new line. + /// * `false` (default) - Maintains the line breaks as written by the programmer. + pub fn conditional_type_line_per_expression(&mut self, value: bool) -> &mut Self { + self.insert("conditionalType.linePerExpression", value.into()) + } + /// Whether to force a line per expression when spanning multiple lines. /// /// * `true` - Formats with each part on a new line. @@ -747,6 +755,16 @@ impl ConfigurationBuilder { self.insert("whileStatement.preferHanging", value.into()) } + /* situational indentation */ + + pub fn conditional_expression_use_nested_indentation(&mut self, value: bool) -> &mut Self { + self.insert("conditionalExpression.useNestedIndentation", value.into()) + } + + pub fn conditional_type_use_nested_indentation(&mut self, value: bool) -> &mut Self { + self.insert("conditionalType.useNestedIndentation", value.into()) + } + /* force single line */ pub fn export_declaration_force_single_line(&mut self, value: bool) -> &mut Self { @@ -1088,6 +1106,7 @@ mod tests { .arrow_function_use_parentheses(UseParentheses::Maintain) .binary_expression_line_per_expression(false) .conditional_expression_line_per_expression(true) + .conditional_type_line_per_expression(true) .member_expression_line_per_expression(false) .type_literal_separator_kind(SemiColonOrComma::Comma) .type_literal_separator_kind_single_line(SemiColonOrComma::Comma) @@ -1148,6 +1167,9 @@ mod tests { .union_and_intersection_type_prefer_hanging(true) .variable_statement_prefer_hanging(true) .while_statement_prefer_hanging(true) + /* situational indentation */ + .conditional_expression_use_nested_indentation(false) + .conditional_type_use_nested_indentation(false) /* member spacing */ .enum_declaration_member_spacing(MemberSpacing::Maintain) /* next control flow position */ @@ -1259,7 +1281,7 @@ mod tests { .while_statement_space_around(true); let inner_config = config.get_inner_config(); - assert_eq!(inner_config.len(), 177); + assert_eq!(inner_config.len(), 180); let diagnostics = resolve_config(inner_config, &resolve_global_config(ConfigKeyMap::new(), &Default::default()).config).diagnostics; assert_eq!(diagnostics.len(), 0); } diff --git a/src/configuration/resolve_config.rs b/src/configuration/resolve_config.rs index 67ac3c59..c2f30e24 100644 --- a/src/configuration/resolve_config.rs +++ b/src/configuration/resolve_config.rs @@ -96,6 +96,7 @@ pub fn resolve_config(config: ConfigKeyMap, global_config: &GlobalConfiguration) arrow_function_use_parentheses: get_value(&mut config, "arrowFunction.useParentheses", UseParentheses::Maintain, &mut diagnostics), binary_expression_line_per_expression: get_value(&mut config, "binaryExpression.linePerExpression", false, &mut diagnostics), conditional_expression_line_per_expression: get_value(&mut config, "conditionalExpression.linePerExpression", true, &mut diagnostics), + conditional_type_line_per_expression: get_value(&mut config, "conditionalType.linePerExpression", false, &mut diagnostics), jsx_quote_style: get_value(&mut config, "jsx.quoteStyle", quote_style.to_jsx_quote_style(), &mut diagnostics), jsx_multi_line_parens: get_value(&mut config, "jsx.multiLineParens", JsxMultiLineParens::Prefer, &mut diagnostics), jsx_force_new_lines_surrounding_content: get_value(&mut config, "jsx.forceNewLinesSurroundingContent", false, &mut diagnostics), @@ -170,6 +171,9 @@ pub fn resolve_config(config: ConfigKeyMap, global_config: &GlobalConfiguration) union_and_intersection_type_prefer_hanging: get_value(&mut config, "unionAndIntersectionType.preferHanging", prefer_hanging, &mut diagnostics), variable_statement_prefer_hanging: get_value(&mut config, "variableStatement.preferHanging", prefer_hanging, &mut diagnostics), while_statement_prefer_hanging: get_value(&mut config, "whileStatement.preferHanging", prefer_hanging, &mut diagnostics), + /* situational indentation */ + conditional_expression_use_nested_indentation: get_value(&mut config, "conditionalExpression.useNestedIndentation", false, &mut diagnostics), + conditional_type_use_nested_indentation: get_value(&mut config, "conditionalType.useNestedIndentation", false, &mut diagnostics), /* member spacing */ enum_declaration_member_spacing: get_value(&mut config, "enumDeclaration.memberSpacing", MemberSpacing::Maintain, &mut diagnostics), /* next control flow position */ diff --git a/src/configuration/types.rs b/src/configuration/types.rs index 4d1aa8fc..b32ff32f 100644 --- a/src/configuration/types.rs +++ b/src/configuration/types.rs @@ -297,6 +297,8 @@ pub struct Configuration { pub binary_expression_line_per_expression: bool, #[serde(rename = "conditionalExpression.linePerExpression")] pub conditional_expression_line_per_expression: bool, + #[serde(rename = "conditionalType.linePerExpression")] + pub conditional_type_line_per_expression: bool, #[serde(rename = "jsx.quoteStyle")] pub jsx_quote_style: JsxQuoteStyle, #[serde(rename = "jsx.multiLineParens")] @@ -419,6 +421,11 @@ pub struct Configuration { pub variable_statement_prefer_hanging: bool, #[serde(rename = "whileStatement.preferHanging")] pub while_statement_prefer_hanging: bool, + /* situational indentation */ + #[serde(rename = "conditionalExpression.useNestedIndentation")] + pub conditional_expression_use_nested_indentation: bool, + #[serde(rename = "conditionalType.useNestedIndentation")] + pub conditional_type_use_nested_indentation: bool, /* member spacing */ #[serde(rename = "enumDeclaration.memberSpacing")] pub enum_declaration_member_spacing: MemberSpacing, diff --git a/src/generation/generate.rs b/src/generation/generate.rs index 7c113cde..d8c535a2 100644 --- a/src/generation/generate.rs +++ b/src/generation/generate.rs @@ -2185,9 +2185,13 @@ fn gen_conditional_expr<'a>(node: &'a CondExpr, context: &mut Context<'a>) -> Pr let colon_token = context.token_finder.get_first_operator_after(&node.cons, ":").unwrap(); let line_per_expression = context.config.conditional_expression_line_per_expression; let has_newline_test_cons = node_helpers::get_use_new_lines_for_nodes(&node.test, &node.cons, context.program); - let has_newline_const_alt = node_helpers::get_use_new_lines_for_nodes(&node.cons, &node.alt, context.program); - let mut force_test_cons_newline = !context.config.conditional_expression_prefer_single_line && has_newline_test_cons; - let mut force_cons_alt_newline = !context.config.conditional_expression_prefer_single_line && has_newline_const_alt; + let has_newline_cons_alt = node_helpers::get_use_new_lines_for_nodes(&node.cons, &node.alt, context.program); + let use_new_lines_for_nested_conditional = context.config.conditional_expression_use_nested_indentation && { + node.parent().kind() == NodeKind::CondExpr || node.cons.kind() == NodeKind::CondExpr || node.alt.kind() == NodeKind::CondExpr + }; + let mut force_test_cons_newline = + has_newline_test_cons && (!context.config.conditional_expression_prefer_single_line || use_new_lines_for_nested_conditional); + let mut force_cons_alt_newline = has_newline_cons_alt && (!context.config.conditional_expression_prefer_single_line || use_new_lines_for_nested_conditional); if line_per_expression && (force_test_cons_newline || force_cons_alt_newline) { // for line per expression, if one is true then both should be true force_test_cons_newline = true; @@ -2295,7 +2299,7 @@ fn gen_conditional_expr<'a>(node: &'a CondExpr, context: &mut Context<'a>) -> Pr items }; - if top_most_data.is_top_most { + if top_most_data.is_top_most || use_new_lines_for_nested_conditional { items.push_condition(conditions::indent_if_start_of_line(cons_and_alt_items)); } else { items.push_condition(indent_if_sol_and_same_indent_as_top_most(cons_and_alt_items, top_most_data.il)); @@ -5251,10 +5255,18 @@ fn gen_array_type<'a>(node: &'a TsArrayType, context: &mut Context<'a>) -> Print } fn gen_conditional_type<'a>(node: &'a TsConditionalType, context: &mut Context<'a>) -> PrintItems { - let use_new_lines = - !context.config.conditional_type_prefer_single_line && node_helpers::get_use_new_lines_for_nodes(&node.true_type, &node.false_type, context.program); let top_most_data = get_top_most_data(node, context); let is_parent_conditional_type = node.parent().kind() == NodeKind::TsConditionalType; + let line_per_expression = context.config.conditional_type_line_per_expression; + let use_new_lines_for_nested_conditional = context.config.conditional_type_use_nested_indentation && { + is_parent_conditional_type || node.true_type.kind() == NodeKind::TsConditionalType || node.false_type.kind() == NodeKind::TsConditionalType + }; + let prefer_single_line = context.config.conditional_type_prefer_single_line; + let has_newline_extends_true = node_helpers::get_use_new_lines_for_nodes(&node.extends_type, &node.true_type, context.program); + let has_newline_true_false = node_helpers::get_use_new_lines_for_nodes(&node.true_type, &node.false_type, context.program); + let force_newline_extends_true = has_newline_extends_true && (!prefer_single_line || use_new_lines_for_nested_conditional); + let force_newline_true_false = has_newline_true_false && (!prefer_single_line || use_new_lines_for_nested_conditional); + let mut items = PrintItems::new(); let before_false_ln = LineNumber::new("beforeFalse"); let question_token = context.token_finder.get_first_operator_after(&node.extends_type, "?").unwrap(); @@ -5282,10 +5294,17 @@ fn gen_conditional_type<'a>(node: &'a TsConditionalType, context: &mut Context<' items }))); - if question_comment_items.previous_lines.is_empty() { - items.push_signal(Signal::SpaceOrNewLine); - } else { + if !question_comment_items.previous_lines.is_empty() { items.extend(question_comment_items.previous_lines); + } else if line_per_expression { + items.push_condition(conditions::new_line_if_multiple_lines_space_or_new_line_otherwise( + top_most_data.ln, + Some(before_false_ln), + )); + } else if force_newline_extends_true { + items.push_signal(Signal::NewLine); + } else { + items.push_signal(Signal::SpaceOrNewLine); } items.push_condition({ @@ -5303,18 +5322,23 @@ fn gen_conditional_type<'a>(node: &'a TsConditionalType, context: &mut Context<' items } .into_rc_path(); - if_true_or( - "isStartOfLineIndentElseQueue", - condition_resolvers::is_start_of_line(), - with_indent(inner_items.into()), - with_queued_indent(inner_items.into()), + + Condition::new( + "isStartOfLineAndTopLevelOrNestedConditionalIndentElseQueue", + ConditionProperties { + condition: Rc::new(move |context| { + Some(context.writer_info.is_start_of_line() && (!is_parent_conditional_type || use_new_lines_for_nested_conditional)) + }), + true_path: Some(with_indent(inner_items.into())), + false_path: Some(with_queued_indent(inner_items.into())), + }, ) }); items.extend(colon_comment_items.previous_lines); // false type - if use_new_lines { + if force_newline_true_false { items.push_signal(Signal::NewLine); } else { items.push_condition(conditions::new_line_if_multiple_lines_space_or_new_line_otherwise( @@ -5342,7 +5366,11 @@ fn gen_conditional_type<'a>(node: &'a TsConditionalType, context: &mut Context<' items }; - if is_parent_conditional_type { + // Unless `use_nested_indentation` is enabled, we keep the indentation the same. e.g.: + // type A = T extends string ? 'string' + // : T extends number ? 'number' + // : T extends boolean ? 'boolean'; + if !use_new_lines_for_nested_conditional && is_parent_conditional_type { items.extend(false_type_generated); } else { items.push_condition(conditions::indent_if_start_of_line(false_type_generated)); diff --git a/tests/specs/expressions/ConditionalExpression/ConditionalExpression_UseNestedIndentation_True.txt b/tests/specs/expressions/ConditionalExpression/ConditionalExpression_UseNestedIndentation_True.txt new file mode 100644 index 00000000..cd243087 --- /dev/null +++ b/tests/specs/expressions/ConditionalExpression/ConditionalExpression_UseNestedIndentation_True.txt @@ -0,0 +1,38 @@ +~~ lineWidth: 50, conditionalExpression.useNestedIndentation: true ~~ +== should handle nested indentation on a basic ternary / conditional expression == +const value = is_prod +? do1() +: is_laptop +? do2() +: do3(); + +[expect] +const value = is_prod + ? do1() + : is_laptop + ? do2() + : do3(); + +== should keep on single line if fits on a single line == +const value = a ? b ? c : d : e; + +[expect] +const value = a ? b ? c : d : e; + +== should go multi-line when exceeding the line width == +const value = testing ? this ? out : testing : exceedsWidth; + +[expect] +const value = testing + ? this ? out : testing + : exceedsWidth; + +== should both go multi-line when exceeding the line width twice == +const value = testing ? this ? out : testingTestingTestingTestingTesting : exceedsWidth; + +[expect] +const value = testing + ? this + ? out + : testingTestingTestingTestingTesting + : exceedsWidth; diff --git a/tests/specs/types/ConditionalType/ConditionalType_All.txt b/tests/specs/types/ConditionalType/ConditionalType_All.txt index 04bcb9eb..0593dca8 100644 --- a/tests/specs/types/ConditionalType/ConditionalType_All.txt +++ b/tests/specs/types/ConditionalType/ConditionalType_All.txt @@ -25,7 +25,7 @@ type Type = T extends string ? number [expect] type Type = T extends string ? number : T extends hereIsAVeryLongExtendsClause - ? testingThis + ? testingThis : boolean; == should not line break before the extends keyword == diff --git a/tests/specs/types/ConditionalType/ConditionalType_LinePerExpression_True.txt b/tests/specs/types/ConditionalType/ConditionalType_LinePerExpression_True.txt new file mode 100644 index 00000000..0b997d3e --- /dev/null +++ b/tests/specs/types/ConditionalType/ConditionalType_LinePerExpression_True.txt @@ -0,0 +1,20 @@ +~~ lineWidth: 80, conditionalType.linePerExpression: true ~~ +== should use a line per expression when multiLine == +type Test = A extends B + ? C extends D + ? c + : testingTesting + : testingTestingTestingTesting; + +[expect] +type Test = A extends B + ? C extends D + ? c + : testingTesting + : testingTestingTestingTesting; + +== should keep on single line if fits on a single line == +type Test = A extends B ? C extends D ? c : d : e; + +[expect] +type Test = A extends B ? C extends D ? c : d : e; diff --git a/tests/specs/types/ConditionalType/ConditionalType_UseNestedIndentation_LinePerExpression_True.txt b/tests/specs/types/ConditionalType/ConditionalType_UseNestedIndentation_LinePerExpression_True.txt new file mode 100644 index 00000000..cdca49a5 --- /dev/null +++ b/tests/specs/types/ConditionalType/ConditionalType_UseNestedIndentation_LinePerExpression_True.txt @@ -0,0 +1,26 @@ +~~ lineWidth: 80, conditionalType.useNestedIndentation: true, conditionalType.linePerExpression: true ~~ +== should handle nested indentation with extends and mapped types == +export type R = S extends Record> ? { [K in keyof S]: ReturnType } : S extends T +? _T extends Array +? I +: _T : S extends SZ ? _T : S extends DZ +? _T : never; + +[expect] +export type R = S extends Record> + ? { [K in keyof S]: ReturnType } + : S extends T + ? _T extends Array + ? I + : _T + : S extends SZ + ? _T + : S extends DZ + ? _T + : never; + +== should keep on single line if fits on a single line == +type Test = A extends B ? C extends D ? c : d : e; + +[expect] +type Test = A extends B ? C extends D ? c : d : e; diff --git a/tests/specs/types/ConditionalType/ConditionalType_UseNestedIndentation_True.txt b/tests/specs/types/ConditionalType/ConditionalType_UseNestedIndentation_True.txt new file mode 100644 index 00000000..e614ce1f --- /dev/null +++ b/tests/specs/types/ConditionalType/ConditionalType_UseNestedIndentation_True.txt @@ -0,0 +1,25 @@ +~~ lineWidth: 80, conditionalType.useNestedIndentation: true ~~ +== should handle nested indentation with extends and mapped types == +export type R = S extends Record> ? { [K in keyof S]: ReturnType } : S extends T +? _T extends Array +? I +: _T : S extends SZ ? _T : S extends DZ +? _T : never; + +[expect] +export type R = S extends Record> + ? { [K in keyof S]: ReturnType } + : S extends T + ? _T extends Array + ? I + : _T + : S extends SZ ? _T + : S extends DZ + ? _T + : never; + +== should keep on single line if fits on a single line == +type Test = A extends B ? C extends D ? c : d : e; + +[expect] +type Test = A extends B ? C extends D ? c : d : e;