From c8fd0cf341fd2386de1031c7c02d06cd9e872172 Mon Sep 17 00:00:00 2001 From: Matthew Hughes Date: Wed, 29 Apr 2026 17:11:38 +0100 Subject: [PATCH] Fix wrong indenting with hard tabs in binop pairs When rewriting pairs of multi-line binary operations, we would check the length of a line to see if we can snuggle the current line into the previous one. We compute the length of the previous line using `last_line_width` which is not aware of indentation widths (i.e. `config.tab_spaces`), so if we were formatting something like: if some_long_name { foo } | if some_other_name { bar } Then when we get to the line for `| if some_other_name {` we check the previous line, which is: * `\s\s\s\s\s\s\s\s}' if we are not using hard tabs (and 1 tab = 4 spaces), and * `\t\t}` if we are using hard-tabs `last_line_width` would return 9 for the first one, and 3 for the second. Meaning if we're using hard tabs we could conclude that it should fit on the previous line, leading to inconsistent behaviour between the two. To fix this, create a version of `last_line_width` that is aware of `config.tab_spaces`. --- src/pairs.rs | 5 +++-- src/utils.rs | 22 ++++++++++++++++++++++ tests/target/issue_6859_hard_tab_breaks.rs | 11 +++++++++++ 3 files changed, 36 insertions(+), 2 deletions(-) create mode 100644 tests/target/issue_6859_hard_tab_breaks.rs diff --git a/src/pairs.rs b/src/pairs.rs index 48948b88b3b..736e28c5b51 100644 --- a/src/pairs.rs +++ b/src/pairs.rs @@ -7,7 +7,8 @@ use crate::rewrite::{Rewrite, RewriteContext, RewriteErrorExt, RewriteResult}; use crate::shape::Shape; use crate::spanned::Spanned; use crate::utils::{ - first_line_width, is_single_line, last_line_width, trimmed_last_line_width, wrap_str, + first_line_width, is_single_line, last_line_width, last_line_width_cfg, + trimmed_last_line_width, wrap_str, }; /// Sigils that decorate a binop pair. @@ -132,7 +133,7 @@ fn rewrite_pairs_multiline( } else { shape.used_width() }; - if last_line_width(&result) + offset <= nested_shape.used_width() { + if last_line_width_cfg(context.config, &result) + offset <= nested_shape.used_width() { // We must snuggle the next line onto the previous line to avoid an orphan. if let Some(line_shape) = shape.offset_left_opt(s.len() + 2 + trimmed_last_line_width(&result)) diff --git a/src/utils.rs b/src/utils.rs index b676803379f..4c838751dc6 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -222,6 +222,14 @@ pub(crate) fn last_line_width(s: &str) -> usize { unicode_str_width(s.rsplitn(2, '\n').next().unwrap_or("")) } +/// The width of the last line in s. +#[inline] +pub(crate) fn last_line_width_cfg(config: &Config, s: &str) -> usize { + let last_line = s.rsplitn(2, '\n').next().unwrap_or(""); + let (prefix_width, prefix_end) = get_prefix_space_width2(&config, last_line); + prefix_width + unicode_str_width(&last_line[prefix_end..]) +} + /// The total used width of the last line. #[inline] pub(crate) fn last_line_used_width(s: &str, offset: usize) -> usize { @@ -697,6 +705,20 @@ fn get_prefix_space_width(config: &Config, s: &str) -> usize { width } +fn get_prefix_space_width2(config: &Config, s: &str) -> (usize, usize) { + let mut width = 0; + let mut prefix_end = 0; + for c in s.chars() { + match c { + ' ' => width += 1, + '\t' => width += config.tab_spaces(), + _ => return (width, prefix_end), + } + prefix_end += 1; + } + (width, prefix_end) +} + pub(crate) trait NodeIdExt { fn root() -> Self; } diff --git a/tests/target/issue_6859_hard_tab_breaks.rs b/tests/target/issue_6859_hard_tab_breaks.rs new file mode 100644 index 00000000000..c83dc3637b8 --- /dev/null +++ b/tests/target/issue_6859_hard_tab_breaks.rs @@ -0,0 +1,11 @@ +// rustfmt-hard_tabs: true + +fn testing() { + let _ = some_long_name + | if some_other_long_name { + foo + } + | if some_other_name { + bar + }; +}