Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(js_formatter): refactor indention for arrow chains #1136

Merged
merged 2 commits into from
Dec 12, 2023
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -65,12 +65,7 @@ impl FormatNodeRule<JsArrowFunctionExpression> for FormatJsArrowFunctionExpressi
write!(
f,
[
format_signature(
&arrow,
self.options.call_arg_layout.is_some(),
false,
true
),
format_signature(&arrow, self.options.call_arg_layout.is_some(), true),
space(),
arrow.fat_arrow_token().format()
]
Expand Down Expand Up @@ -211,7 +206,6 @@ impl FormatNodeRule<JsArrowFunctionExpression> for FormatJsArrowFunctionExpressi
fn format_signature(
arrow: &JsArrowFunctionExpression,
is_first_or_last_call_argument: bool,
ancestor_call_expr_or_logical_expr: bool,
is_first_in_chain: bool,
) -> impl Format<JsFormatContext> + '_ {
format_with(move |f| {
Expand Down Expand Up @@ -255,11 +249,7 @@ fn format_signature(
}
}
AnyJsArrowFunctionParameters::JsParameters(params) => {
if ancestor_call_expr_or_logical_expr && !is_first_or_last_call_argument {
write!(f, [group(&dedent(&params.format()))])?;
} else {
write!(f, [params.format()])?;
}
write!(f, [params.format()])?;
}
};

Expand Down Expand Up @@ -477,12 +467,6 @@ impl Format<JsFormatContext> for ArrowChain {
let tail_body = tail.body()?;
let is_assignment_rhs = self.options.assignment_layout.is_some();
let is_grouped_call_arg_layout = self.options.call_arg_layout.is_some();
let ancestor_call_expr_or_logical_expr = head.syntax().ancestors().any(|ancestor| {
matches!(
ancestor.kind(),
JsSyntaxKind::JS_CALL_EXPRESSION | JsSyntaxKind::JS_LOGICAL_EXPRESSION
)
});

// If this chain is the callee in a parent call expression, then we
// want it to break onto a new line to clearly show that the arrow
Expand Down Expand Up @@ -533,11 +517,20 @@ impl Format<JsFormatContext> for ArrowChain {
Some(AssignmentLikeLayout::ChainTailArrowFunction)
);

let format_arrow_signatures = format_with(|f| {
if is_callee || is_assignment_rhs {
write!(f, [soft_line_break()])?;
}
// Arrow chains as callees or as the right side of an assignment
// indent the entire signature chain a single level and do _not_
// indent a second level for additional signatures after the first:
// const foo =
// (a) =>
// (b) =>
// (c) =>
// 0;
// This tracks that state and is used to prevent the insertion of
// additional indents under `format_arrow_signatures`, then also to
// add the outer indent under `format_inner`.
let has_initial_indent = is_callee || is_assignment_rhs;

let format_arrow_signatures = format_with(|f| {
let join_signatures = format_with(|f: &mut JsFormatter| {
let mut is_first_in_chain = true;
for arrow in self.arrows() {
Expand Down Expand Up @@ -568,17 +561,24 @@ impl Format<JsFormatContext> for ArrowChain {
}
}

write!(
f,
[format_signature(
arrow,
is_grouped_call_arg_layout,
ancestor_call_expr_or_logical_expr,
is_first_in_chain,
)]
)?;

is_first_in_chain = false;
let formatted_signature =
format_signature(arrow, is_grouped_call_arg_layout, is_first_in_chain);

// Arrow chains indent a second level for every item other than the first:
// (a) =>
// (b) =>
// (c) =>
// 0
// Because the chain is printed as a flat list, each entry needs to set
// its own indention. This ensures that the first item keeps the same
// level as the surrounding content, and then each subsequent item has
// one additional level, as shown above.
if is_first_in_chain || has_initial_indent {
is_first_in_chain = false;
write!(f, [formatted_signature])?;
} else {
write!(f, [indent(&formatted_signature)])?;
}

// The arrow of the tail is formatted outside of the group to ensure it never
// breaks from the body
Expand Down Expand Up @@ -688,16 +688,26 @@ impl Format<JsFormatContext> for ArrowChain {
let group_id = f.group_id("arrow-chain");

let format_inner = format_once(|f| {
write!(
f,
[
group(&indent(&format_arrow_signatures))
if has_initial_indent {
write!(
f,
[group(&indent(&format_args![
soft_line_break(),
format_arrow_signatures
]))
.with_group_id(Some(group_id))
.should_expand(break_signatures)]
)?;
} else {
write!(
f,
[group(&format_arrow_signatures)
.with_group_id(Some(group_id))
.should_expand(break_signatures),
space(),
tail.fat_arrow_token().format(),
]
)?;
.should_expand(break_signatures)]
)?;
};

write!(f, [space(), tail.fat_arrow_token().format()])?;

if is_grouped_call_arg_layout {
write!(f, [group(&format_tail_body)])?;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@

// Parameters that break over multiple lines should match indention
// with the opening parenthesis.
// The second signature and onward are indented a second level.
a.b(
(a) =>
(b) =>
(
c = "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdef",
d = "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdef",
) =>
(e) =>
0,
);

// In assignments, the first signature is indented, and all subsequent
// signatures indent to the same level, not a second level in.
// Block body statements also indent an additional level from the last
// signature.
x = (bifornCringerMoshedPerplexSawder) => ((askTrovenaBeenaDependsRowans, glimseGlyphsHazardNoopsTieTie) => (f00) => {
averredBathersBoxroomBuggyNurl();
} // BOOM
)

// Chained assignments with arrow functions indent an additional level from the
// last assignment, then all subsequent signatures are at the same level
const bifornCringer1 =
askTrovenaBeenaDepends =
glimseGlyphs =
(argumentOne, argumentTwo) => (restOfTheArguments12345678) => {
return "baz";
};

const bifornCringer2 =
askTrovenaBeenaDepends =
glimseGlyphs =
(argumentOne, argumentTwo) => (restOfTheArguments12345678) => (yetAnotherArgumentPastLineWidth) => {
return "baz";
};

// Same-line bodies that end up expanding only indent a single time.
import('./someComponent').then(({default: TheComponent}) => (props) => (
<TheComponent {...someInjectedProps} />
));
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
---
source: crates/biome_formatter_test/src/snapshot_builder.rs
info: js/module/arrow/curried_indents.js
---

# Input

```js

// Parameters that break over multiple lines should match indention
// with the opening parenthesis.
// The second signature and onward are indented a second level.
a.b(
(a) =>
(b) =>
(
c = "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdef",
d = "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdef",
) =>
(e) =>
0,
);

// In assignments, the first signature is indented, and all subsequent
// signatures indent to the same level, not a second level in.
// Block body statements also indent an additional level from the last
// signature.
x = (bifornCringerMoshedPerplexSawder) => ((askTrovenaBeenaDependsRowans, glimseGlyphsHazardNoopsTieTie) => (f00) => {
averredBathersBoxroomBuggyNurl();
} // BOOM
)

// Chained assignments with arrow functions indent an additional level from the
// last assignment, then all subsequent signatures are at the same level
const bifornCringer1 =
askTrovenaBeenaDepends =
glimseGlyphs =
(argumentOne, argumentTwo) => (restOfTheArguments12345678) => {
return "baz";
};

const bifornCringer2 =
askTrovenaBeenaDepends =
glimseGlyphs =
(argumentOne, argumentTwo) => (restOfTheArguments12345678) => (yetAnotherArgumentPastLineWidth) => {
return "baz";
};

// Same-line bodies that end up expanding only indent a single time.
import('./someComponent').then(({default: TheComponent}) => (props) => (
<TheComponent {...someInjectedProps} />
));
```


=============================

# Outputs

## Output 1

-----
Indent style: Tab
Indent width: 2
Line ending: LF
Line width: 80
Quote style: Double Quotes
JSX quote style: Double Quotes
Quote properties: As needed
Trailing comma: All
Semicolons: Always
Arrow parentheses: Always
Bracket spacing: true
Bracket same line: false
-----

```js
// Parameters that break over multiple lines should match indention
// with the opening parenthesis.
// The second signature and onward are indented a second level.
a.b(
(a) =>
(b) =>
(
c = "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdef",
d = "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdef",
) =>
(e) =>
0,
);

// In assignments, the first signature is indented, and all subsequent
// signatures indent to the same level, not a second level in.
// Block body statements also indent an additional level from the last
// signature.
x =
(bifornCringerMoshedPerplexSawder) =>
(askTrovenaBeenaDependsRowans, glimseGlyphsHazardNoopsTieTie) =>
(f00) => {
averredBathersBoxroomBuggyNurl();
}; // BOOM

// Chained assignments with arrow functions indent an additional level from the
// last assignment, then all subsequent signatures are at the same level
const bifornCringer1 =
(askTrovenaBeenaDepends =
glimseGlyphs =
(argumentOne, argumentTwo) => (restOfTheArguments12345678) => {
return "baz";
});

const bifornCringer2 =
(askTrovenaBeenaDepends =
glimseGlyphs =
(argumentOne, argumentTwo) =>
(restOfTheArguments12345678) =>
(yetAnotherArgumentPastLineWidth) => {
return "baz";
});

// Same-line bodies that end up expanding only indent a single time.
import("./someComponent").then(({ default: TheComponent }) => (props) => (
<TheComponent {...someInjectedProps} />
));
```

## Output 2

-----
Indent style: Tab
Indent width: 2
Line ending: LF
Line width: 80
Quote style: Double Quotes
JSX quote style: Double Quotes
Quote properties: As needed
Trailing comma: All
Semicolons: Always
Arrow parentheses: As needed
Bracket spacing: true
Bracket same line: false
-----

```js
// Parameters that break over multiple lines should match indention
// with the opening parenthesis.
// The second signature and onward are indented a second level.
a.b(
a =>
b =>
(
c = "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdef",
d = "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdef",
) =>
e =>
0,
);

// In assignments, the first signature is indented, and all subsequent
// signatures indent to the same level, not a second level in.
// Block body statements also indent an additional level from the last
// signature.
x =
bifornCringerMoshedPerplexSawder =>
(askTrovenaBeenaDependsRowans, glimseGlyphsHazardNoopsTieTie) =>
f00 => {
averredBathersBoxroomBuggyNurl();
}; // BOOM

// Chained assignments with arrow functions indent an additional level from the
// last assignment, then all subsequent signatures are at the same level
const bifornCringer1 =
(askTrovenaBeenaDepends =
glimseGlyphs =
(argumentOne, argumentTwo) => restOfTheArguments12345678 => {
return "baz";
});

const bifornCringer2 =
(askTrovenaBeenaDepends =
glimseGlyphs =
(argumentOne, argumentTwo) =>
restOfTheArguments12345678 =>
yetAnotherArgumentPastLineWidth => {
return "baz";
});

// Same-line bodies that end up expanding only indent a single time.
import("./someComponent").then(({ default: TheComponent }) => props => (
<TheComponent {...someInjectedProps} />
));
```