Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions deployment/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,18 @@
"description": "Forces no braces when the header is one line and body is one line. Otherwise forces braces."
}]
},
"ternaryIndentStyle": {
"description": "How ternary conditional expressions should be indented.",
"type": "string",
"default": "align",
"oneOf": [{
"const": "align",
"description": "Align the ? and : operators under the condition."
}, {
"const": "structural",
"description": "Use structural indentation for the ternary expression."
}]
},
"bracePosition": {
"description": "Where to place the opening brace.",
"type": "string",
Expand Down Expand Up @@ -1181,6 +1193,9 @@
"conditionalExpression.operatorPosition": {
"$ref": "#/definitions/operatorPosition"
},
"conditionalExpression.indentStyle": {
"$ref": "#/definitions/ternaryIndentStyle"
},
"conditionalType.operatorPosition": {
"$ref": "#/definitions/operatorPosition"
},
Expand Down
7 changes: 6 additions & 1 deletion src/configuration/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -822,6 +822,10 @@ impl ConfigurationBuilder {
self.insert("conditionalExpression.operatorPosition", value.to_string().into())
}

pub fn conditional_expression_indent_style(&mut self, value: TernaryIndentStyle) -> &mut Self {
self.insert("conditionalExpression.indentStyle", value.to_string().into())
}

pub fn conditional_type_operator_position(&mut self, value: OperatorPosition) -> &mut Self {
self.insert("conditionalType.operatorPosition", value.to_string().into())
}
Expand Down Expand Up @@ -1122,6 +1126,7 @@ mod tests {
.arrow_function_use_parentheses(UseParentheses::Maintain)
.binary_expression_line_per_expression(false)
.conditional_expression_line_per_expression(true)
.conditional_expression_indent_style(TernaryIndentStyle::Align)
.member_expression_line_per_expression(false)
.type_literal_separator_kind(SemiColonOrComma::Comma)
.type_literal_separator_kind_single_line(SemiColonOrComma::Comma)
Expand Down Expand Up @@ -1297,7 +1302,7 @@ mod tests {
.while_statement_space_around(true);

let inner_config = config.get_inner_config();
assert_eq!(inner_config.len(), 182);
assert_eq!(inner_config.len(), 183);
let diagnostics = resolve_config(inner_config, &Default::default()).diagnostics;
assert_eq!(diagnostics.len(), 0);
}
Expand Down
1 change: 1 addition & 0 deletions src/configuration/resolve_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,7 @@ pub fn resolve_config(config: ConfigKeyMap, global_config: &GlobalConfiguration)
/* operator position */
binary_expression_operator_position: get_value(&mut config, "binaryExpression.operatorPosition", operator_position, &mut diagnostics),
conditional_expression_operator_position: get_value(&mut config, "conditionalExpression.operatorPosition", operator_position, &mut diagnostics),
conditional_expression_indent_style: get_value(&mut config, "conditionalExpression.indentStyle", TernaryIndentStyle::Align, &mut diagnostics),
conditional_type_operator_position: get_value(&mut config, "conditionalType.operatorPosition", operator_position, &mut diagnostics),
/* single body position */
if_statement_single_body_position: get_value(&mut config, "ifStatement.singleBodyPosition", single_body_position, &mut diagnostics),
Expand Down
15 changes: 15 additions & 0 deletions src/configuration/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,19 @@ pub enum OperatorPosition {

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

/// How to indent ternary expression branches.
#[derive(Clone, PartialEq, Copy, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum TernaryIndentStyle {
/// Align all ternary branches at the same indentation level (original behavior).
Align,
/// Add structural indentation for nested ternary branches.
Structural,
}

generate_str_to_from![TernaryIndentStyle, [Align, "align"], [Structural, "structural"]];


/// Where to place a node that could be on the same line or next line.
#[derive(Clone, PartialEq, Copy, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
Expand Down Expand Up @@ -466,6 +479,8 @@ pub struct Configuration {
pub binary_expression_operator_position: OperatorPosition,
#[serde(rename = "conditionalExpression.operatorPosition")]
pub conditional_expression_operator_position: OperatorPosition,
#[serde(rename = "conditionalExpression.indentStyle")]
pub conditional_expression_indent_style: TernaryIndentStyle,
#[serde(rename = "conditionalType.operatorPosition")]
pub conditional_type_operator_position: OperatorPosition,
/* single body position */
Expand Down
103 changes: 72 additions & 31 deletions src/generation/generate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2348,6 +2348,7 @@ fn gen_class_expr<'a>(node: &ClassExpr<'a>, context: &mut Context<'a>) -> PrintI
}

fn gen_conditional_expr<'a>(node: &CondExpr<'a>, context: &mut Context<'a>) -> PrintItems {
let is_structural_mode = context.config.conditional_expression_indent_style == TernaryIndentStyle::Structural;
let question_token = context.token_finder.get_first_operator_after(&node.test, "?").unwrap();
let colon_token = context.token_finder.get_first_operator_after(&node.cons, ":").unwrap();
let line_per_expression = context.config.conditional_expression_line_per_expression;
Expand Down Expand Up @@ -2376,14 +2377,18 @@ fn gen_conditional_expr<'a>(node: &CondExpr<'a>, context: &mut Context<'a>) -> P

let top_most_il = top_most_data.il;

items.extend(ir_helpers::new_line_group(with_queued_indent({
items.extend({
let mut items = gen_node(node.test.into(), context);
if question_position == OperatorPosition::SameLine {
items.push_sc(sc!(" ?"));
}
items.extend(question_comment_items.trailing_line);
items
})));
if is_structural_mode && no_conditional_or_alternate(node.into()) {
items
} else {
ir_helpers::new_line_group(with_queued_indent(items))
}
});

items.extend(question_comment_items.previous_lines);

Expand Down Expand Up @@ -2416,19 +2421,32 @@ fn gen_conditional_expr<'a>(node: &CondExpr<'a>, context: &mut Context<'a>) -> P
items
});

items.push_condition({
let mut items = PrintItems::new();
items.extend(question_comment_items.leading_line);
let cons_and_alt_section = {
let mut section_items = PrintItems::new();
section_items.extend(question_comment_items.leading_line);
if question_position == OperatorPosition::NextLine {
items.push_sc(sc!("? "));
section_items.push_sc(sc!("? "));
}
items.extend(gen_node(node.cons.into(), context));
let cons_items = if is_structural_mode && top_most_data.is_top_most {
ir_helpers::new_line_group(with_queued_indent(gen_node(node.cons.into(), context)))
} else {
gen_node(node.cons.into(), context)
};
section_items.extend(cons_items);
if colon_position == OperatorPosition::SameLine {
items.push_sc(sc!(" :"));
section_items.push_sc(sc!(" :"));
}
items.extend(colon_comment_items.trailing_line);
indent_if_sol_and_same_indent_as_top_most(ir_helpers::new_line_group(items), top_most_il)
});
section_items.extend(colon_comment_items.trailing_line);
section_items
};
if is_structural_mode {
items.extend(cons_and_alt_section);
} else {
items.push_condition(indent_if_sol_and_same_indent_as_top_most(
ir_helpers::new_line_group(cons_and_alt_section),
top_most_il,
));
}

items.extend(colon_comment_items.previous_lines);

Expand All @@ -2443,16 +2461,19 @@ fn gen_conditional_expr<'a>(node: &CondExpr<'a>, context: &mut Context<'a>) -> P
items.push_signal(Signal::SpaceOrNewLine);
}

items.push_condition({
let mut items = PrintItems::new();
items.extend(colon_comment_items.leading_line);
if colon_position == OperatorPosition::NextLine {
items.push_sc(sc!(": "));
}
items.push_info(before_alternate_ln);
items.extend(gen_node(node.alt.into(), context));
indent_if_sol_and_same_indent_as_top_most(ir_helpers::new_line_group(items), top_most_il)
});
let mut alt_items = PrintItems::new();
alt_items.extend(colon_comment_items.leading_line);
if colon_position == OperatorPosition::NextLine {
alt_items.push_sc(sc!(": "));
}
alt_items.push_info(before_alternate_ln);
if is_structural_mode {
alt_items.extend(ir_helpers::new_line_group(with_queued_indent(gen_node(node.alt.into(), context))));
items.extend(alt_items);
} else {
alt_items.extend(gen_node(node.alt.into(), context));
items.push_condition(indent_if_sol_and_same_indent_as_top_most(ir_helpers::new_line_group(alt_items), top_most_il));
}
items.push_info(end_ln);

if let Some(reevaluation) = multi_line_reevaluation {
Expand All @@ -2476,20 +2497,40 @@ fn gen_conditional_expr<'a>(node: &CondExpr<'a>, context: &mut Context<'a>) -> P
is_top_most: bool,
}

fn no_conditional_or_alternate(node: Node) -> bool {
match node.parent() {
Some(Node::CondExpr(parent_conditional)) => parent_conditional.alt.range() == node.range(),
_ => true,
}
}

fn get_top_most_data<'a>(node: &CondExpr<'a>, context: &mut Context<'a>) -> TopMostData {
// The "top most" node in nested conditionals follows the ancestors up through
// the alternate expressions.
// The "top most" node in nested conditionals follows the ancestors up through the alternate expressions.
let mut top_most_node = node;

// In structural mode, skip over wrapper nodes (parens, type assertions, etc.) to find the top-most conditional in the chain
let mut current_start = node.start();
let is_structural_mode = context.config.conditional_expression_indent_style == TernaryIndentStyle::Structural;
for ancestor in context.parent_stack.iter() {
if let Node::CondExpr(parent) = ancestor {
if parent.alt.start() == top_most_node.start() {
top_most_node = parent;
} else {
break;
match ancestor {
node @ (Node::CondExpr(_) | Node::ParenExpr(_) | Node::TsAsExpr(_) | Node::TsSatisfiesExpr(_) | Node::TsNonNullExpr(_)) => {
let (outer_start, inner_start) = match node {
Node::CondExpr(parent) => (parent.start(), parent.alt.start()),
Node::ParenExpr(paren) if is_structural_mode => (paren.start(), paren.expr.start()),
Node::TsAsExpr(as_expr) if is_structural_mode => (as_expr.start(), as_expr.expr.start()),
Node::TsSatisfiesExpr(satisfies_expr) if is_structural_mode => (satisfies_expr.start(), satisfies_expr.expr.start()),
Node::TsNonNullExpr(non_null_expr) if is_structural_mode => (non_null_expr.start(), non_null_expr.expr.start()),
_ => break,
};
if inner_start != current_start {
break;
}
if let Node::CondExpr(parent) = node {
top_most_node = parent;
}
current_start = outer_start;
}
} else {
break;
_ => break,
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
~~ conditionalExpression.indentStyle: align ~~
== should handle simple ternary with align indentation ==
const result = condition
? value1
: value2;

[expect]
const result = condition
? value1
: value2;

== should handle nested ternary with align indentation ==
const result = condition1
? value1
: condition2
? value2
: condition3
? value3
: value4;

[expect]
const result = condition1
? value1
: condition2
? value2
: condition3
? value3
: value4;

== should handle deeply nested ternary ==
const test = this.log(`long text here`)
? this.log(`another long text`)
: this.log(`third long text`)
? this.log(`fourth long text`)
? this.log(`fifth long text`)
: this.log(`sixth long text`)
: this.log(`seventh long text`);

[expect]
const test = this.log(`long text here`)
? this.log(`another long text`)
: this.log(`third long text`)
? this.log(`fourth long text`)
? this.log(`fifth long text`)
: this.log(`sixth long text`)
: this.log(`seventh long text`);

== should handle ternary with function calls ==
const value = isValid(data)
? processData(data)
: hasFallback(data)
? getFallback(data)
: getDefault();

[expect]
const value = isValid(data)
? processData(data)
: hasFallback(data)
? getFallback(data)
: getDefault();

== should handle parenthesized nested ternary ==
const result = condition1
? value1
: (condition2
? value2
: condition3
? value3
: value4);

[expect]
const result = condition1
? value1
: (condition2
? value2
: condition3
? value3
: value4);

== should handle nested ternary wrapped in assertion ==
const outcome = condition1
? value1
: ((condition2
? value2
: condition3
? value3
: value4) as Result);

[expect]
const outcome = condition1
? value1
: ((condition2
? value2
: condition3
? value3
: value4) as Result);

== should handle ternary in else branch ==
if (condition) {
doSomething();
} else {
const result = check1
? value1
: check2
? value2
: value3;
}

[expect]
if (condition) {
doSomething();
} else {
const result = check1
? value1
: check2
? value2
: value3;
}
Loading