diff --git a/Configurations.md b/Configurations.md index 37cb7474130..5c45eb120ad 100644 --- a/Configurations.md +++ b/Configurations.md @@ -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. diff --git a/src/config/mod.rs b/src/config/mod.rs index 8c04363b1fd..35f0986b7b1 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -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"; @@ -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 diff --git a/src/config/options.rs b/src/config/options.rs index 3b91021813c..4e2b8033d4d 100644 --- a/src/config/options.rs +++ b/src/config/options.rs @@ -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, +} diff --git a/src/expr.rs b/src/expr.rs index ced382c4915..eb0b6712043 100644 --- a/src/expr.rs +++ b/src/expr.rs @@ -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, @@ -1945,6 +1947,8 @@ fn choose_rhs( 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)) } _ => { @@ -1958,26 +1962,76 @@ fn choose_rhs( .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)), } } }