Skip to content

Add configuration option for wrapping of assignment right-hand side #4886

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

Open
wants to merge 1 commit into
base: stbu
Choose a base branch
from
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
63 changes: 63 additions & 0 deletions Configurations.md
Original file line number Diff line number Diff line change
Expand Up @@ -1992,6 +1992,69 @@ use dolor;
use sit;
```

## `righthand_indentation_strategy`

Controls how the right-hand side of an assignment should be formatted if the expression does not fit on a single line. If the expression fits on the same line, this option is ignored.

- **Default value**: `Heuristic`
- **Possible values**: `Heuristic`, `SameLineAsLHS`, `NewlineIndentRHS`
- **Stable**: No

#### `Heuristic` (default):

Use a heuristic approach to determine whether or not an expression should be on the same line as the left-hand side or moved to the next line.

```rust
fn main() {
let foo = bar().baz();

let bar: SomeWideResult + Send + Sync =
some_long_function_call().some_even_longer_function_call();

let baz = vec![1, 2, 3, 4, 5, 6, 7]
.into_iter()
.map(|x| x + 1)
.fold(0, |sum, i| sum + 1);
}
```

#### `SameLineAsLHS`:

If there is some valid formatting that allows part of the expression to be on the same line as the left-hand side, prefer that over a newline-indent.

```rust
fn main() {
let foo = bar().baz();

let bar: SomeWideResult + Send + Sync = some_long_function_call()
.some_even_longer_function_call();

let baz = vec![1, 2, 3, 4, 5, 6, 7]
.into_iter()
.map(|x| x + 1)
.fold(0, |sum, i| sum + 1);
}
```

#### `NewlineIndentRHS`

If there is some valid formatting that allows the expression to be placed indented on the next line, prefer that over placing it next to the left-hand side.

```rust
fn main() {
let foo = bar().baz();

let bar: SomeWideResult + Send + Sync =
some_long_function_call().some_even_longer_function_call();

let baz =
vec![1, 2, 3, 4, 5, 6, 7]
.into_iter()
.map(|x| x + 1)
.fold(0, |sum, i| sum + 1);
}
```

## `group_imports`

Controls the strategy for how imports are grouped together.
Expand Down
4 changes: 4 additions & 0 deletions src/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,9 @@ create_config! {
inline_attribute_width: usize, 0, false,
"Write an item and its attribute on the same line \
if their combined width is below a threshold";
righthand_indentation_strategy: RightHandIndentationStrategy,
RightHandIndentationStrategy::Heuristic, false,
"Determines how the right-hand side of an assignment and pattern is indented.";

// Options that can change the source code beyond whitespace/blocks (somewhat linty things)
merge_derives: bool, true, true, "Merge multiple `#[derive(...)]` into a single one";
Expand Down Expand Up @@ -604,6 +607,7 @@ blank_lines_lower_bound = 0
edition = "2015"
version = "One"
inline_attribute_width = 0
righthand_indentation_strategy = "Heuristic"
merge_derives = true
use_try_shorthand = false
use_field_init_shorthand = false
Expand Down
41 changes: 41 additions & 0 deletions src/config/options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -442,3 +442,44 @@ pub enum MatchArmLeadingPipe {
/// Preserve any existing leading pipes
Preserve,
}

/// Controls how "right-hand" expressions (assignment and match bodies)
/// should be rendered if the right-hand side does not fit on a single line
/// but might possibly fit on a single line if we newline-indent.
#[config_type]
pub enum RightHandIndentationStrategy {
/// Use the `prefer_next_line` heuristic (default behavior, equivalent to old
/// rustfmt versions that did not support this option).
///
/// let foo =
/// bar().baz().boo();
///
/// | SomeEnum =>
/// bar().baz().boo()
Heuristic,
/// If the expression doesn't fit on a single line, split it but leave the first
/// line on the same line as the left-hand (even if indenting may have the expression
/// fit entirely):
///
/// let foo = bar()
/// .baz()
/// .boo();
///
/// | SomeEnum => bar()
/// .baz()
/// .boo()
SameLineAsLHS,
/// If the expression doesn't fit on a single line, split it and indent the first
/// line on the line below the left-hand.
///
/// let foo =
/// bar()
/// .baz()
/// .boo();
///
/// | SomeEnum =>
/// bar()
/// .baz()
/// .boo()
NewlineIndentRHS,
}
88 changes: 71 additions & 17 deletions src/expr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ use crate::comment::{
rewrite_missing_comment, CharClasses, FindUncommented,
};
use crate::config::lists::*;
use crate::config::{Config, ControlBraceStyle, IndentStyle, Version};
use crate::config::{
Config, ControlBraceStyle, IndentStyle, RightHandIndentationStrategy, Version,
};
use crate::lists::{
definitive_tactic, itemize_list, shape_for_tactic, struct_lit_formatting, struct_lit_shape,
struct_lit_tactic, write_list, ListFormatting, Separator,
Expand Down Expand Up @@ -1945,6 +1947,8 @@ fn choose_rhs<R: Rewrite>(
Some(ref new_str)
if !new_str.contains('\n') && unicode_str_width(new_str) <= shape.width =>
{
// The entire expression fits on the same line, so no need to attempt
// a rendering on the new line.
Some(format!(" {}", new_str))
}
_ => {
Expand All @@ -1958,26 +1962,76 @@ fn choose_rhs<R: Rewrite>(
.to_string_with_newline(context.config);
let before_space_str = if has_rhs_comment { "" } else { " " };

match (orig_rhs, new_rhs) {
(Some(ref orig_rhs), Some(ref new_rhs))
if wrap_str(new_rhs.clone(), context.config.max_width(), new_shape)
.is_none() =>
{
// We've now tried to lay out the expression both on the same line as the
// lhs (in `orig_rhs`) and on the next line with an indent (`new_rhs`).
// We now need to figure out how we're going to render this based on how
// the user has configured rhs rendering. Before that, some trivial cases.

// We had a correct layout for the same line, but not indented. This should
// rarely happen, but might happen if the lhs is less wide than the configured
// indentation width (meaning the rhs effectively has more room on the same
// line than it does on the next line). Just use the one that we know works.
if orig_rhs.is_some() && new_rhs.is_none() {
// TODO: If the user has `NewlineIndentRHS` configured, should we return
// none instead of defaulting to the same line (even though the user doesn't
// want that?). Should we instead expand the width of the newline until we
// can fit something inside it?
return Some(format!("{}{}", before_space_str, orig_rhs.unwrap()));
}

// We had a correct layout for the indented line, but not for the original one.
// This is effectively the reverse of the above condition.
if orig_rhs.is_none() && new_rhs.is_some() {
// TODO: If the user has `SameLineAsLHS` configured, should we return none
// instead of defaulting to the same line? Should we instead expand the
// width of the lhs line until we can fit something inside it?
return Some(format!("{}{}", new_indent_str, new_rhs.unwrap()));
}

// We couldn't render any of the two conditions...
if orig_rhs.is_none() && orig_rhs.is_none() {
// ...however, the caller has allowed us to exceed the maximum width.
if rhs_tactics == RhsTactics::AllowOverflow {
let shape = shape.infinite_width();

return expr
.rewrite(context, shape)
.map(|s| format!("{}{}", before_space_str, s));
}

// We aren't allowed to overflow but we can't render either variant.
return None;
}

// We're now in a situation where we know for sure that both same-line and
// new-line rendering has resulted in a solution.
let orig_rhs = orig_rhs.as_ref().unwrap();
let new_rhs = new_rhs.as_ref().unwrap();

// If we're not able to wrap the new right hand side within the width configured,
// instead fall back to the original that fits on the same line.
if wrap_str(new_rhs.clone(), context.config.max_width(), new_shape).is_none() {
// TODO: Same issue with `NewlineIndentRHS` discussed earlier.
return Some(format!("{}{}", before_space_str, orig_rhs));
}

// Now we need to do things differently based on what the user has configured.
match context.config.righthand_indentation_strategy() {
RightHandIndentationStrategy::Heuristic => {
// Heuristic/old approach. Check whether we should prefer the next line,
// and then return either the next line or the current line.
if prefer_next_line(orig_rhs, new_rhs, rhs_tactics) {
Some(format!("{}{}", new_indent_str, new_rhs))
} else {
Some(format!("{}{}", before_space_str, orig_rhs))
}
}
RightHandIndentationStrategy::SameLineAsLHS => {
Some(format!("{}{}", before_space_str, orig_rhs))
}
(Some(ref orig_rhs), Some(ref new_rhs))
if prefer_next_line(orig_rhs, new_rhs, rhs_tactics) =>
{
RightHandIndentationStrategy::NewlineIndentRHS => {
Some(format!("{}{}", new_indent_str, new_rhs))
}
(None, Some(ref new_rhs)) => Some(format!("{}{}", new_indent_str, new_rhs)),
(None, None) if rhs_tactics == RhsTactics::AllowOverflow => {
let shape = shape.infinite_width();
expr.rewrite(context, shape)
.map(|s| format!("{}{}", before_space_str, s))
}
(None, None) => None,
(Some(orig_rhs), _) => Some(format!("{}{}", before_space_str, orig_rhs)),
}
}
}
Expand Down