From 635d4e28cac4cd09f11b22fe6dcfe6f93cb77ab7 Mon Sep 17 00:00:00 2001 From: Matthew Hughes Date: Mon, 25 May 2026 20:33:07 +0100 Subject: [PATCH 1/3] Avoid passing entire config to `get_prefix_space_width` And pass only the required attribute, similarly to `wrap_str` --- src/utils.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/utils.rs b/src/utils.rs index b676803379f..58ac7b9d8fa 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -609,7 +609,7 @@ pub(crate) fn trim_left_preserve_layout( let prefix_space_width = if is_empty_line(&line) { None } else { - Some(get_prefix_space_width(config, &line)) + Some(get_prefix_space_width(&line, config.tab_spaces())) }; // just InString{Commented} in order to allow the start of a string to be indented @@ -685,12 +685,12 @@ pub(crate) fn is_empty_line(s: &str) -> bool { s.is_empty() || s.chars().all(char::is_whitespace) } -fn get_prefix_space_width(config: &Config, s: &str) -> usize { +fn get_prefix_space_width(s: &str, tab_spaces: usize) -> usize { let mut width = 0; for c in s.chars() { match c { ' ' => width += 1, - '\t' => width += config.tab_spaces(), + '\t' => width += tab_spaces, _ => return width, } } From 2b98d0656438664bca4f3f9e8f5780e688bc0562 Mon Sep 17 00:00:00 2001 From: Matthew Hughes Date: Mon, 25 May 2026 20:42:39 +0100 Subject: [PATCH 2/3] Rewrite `get_prefix_space_width` to also give the prefix end This is required so that `last_line_width` knows the width of the prefix and where the rest of the string starts. Rename the function appropriately and replace a call to `str::chars` with `str::char_indices` to make it more explicit we want an index here (though the two are equivalent in this case, since we only loop over single byte characters) --- src/utils.rs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/utils.rs b/src/utils.rs index 58ac7b9d8fa..cbc2d488462 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -609,7 +609,8 @@ pub(crate) fn trim_left_preserve_layout( let prefix_space_width = if is_empty_line(&line) { None } else { - Some(get_prefix_space_width(&line, config.tab_spaces())) + let (prefix_width, _) = get_prefix_space_width_and_end(&line, config.tab_spaces()); + Some(prefix_width) }; // just InString{Commented} in order to allow the start of a string to be indented @@ -685,16 +686,17 @@ pub(crate) fn is_empty_line(s: &str) -> bool { s.is_empty() || s.chars().all(char::is_whitespace) } -fn get_prefix_space_width(s: &str, tab_spaces: usize) -> usize { +fn get_prefix_space_width_and_end(s: &str, tab_spaces: usize) -> (usize, usize) { let mut width = 0; - for c in s.chars() { + + for (i, c) in s.char_indices() { match c { ' ' => width += 1, '\t' => width += tab_spaces, - _ => return width, + _ => return (width, i), } } - width + (width, s.len()) } pub(crate) trait NodeIdExt { From 25956303e5b79f6fc6f234f67bc9ccb3eadd5cff Mon Sep 17 00:00:00 2001 From: Matthew Hughes Date: Mon, 25 May 2026 21:12:19 +0100 Subject: [PATCH 3/3] Teach `last_line_width` about `config.tab_spaces` So that the line width is calculated as the rendered width of any trailing space plus the unicode width of the rest of the string. That is, for example, so that the line: \s\s\s\ssome_func(); Would have the same length as \tsome_func(); Assuming `tab_spaces=4`. This fixes a issue where the formatting a collection of binary operations would depend on whether hard of soft tabs were used. Specifically in the change to the call of `last_line_width` at line 135 of `src/pairs.rs`: the inconsistency above meant under some conditions when using hard tabs we could satisfy the condition on that line and drop into the block to snuggle the lines together, where with soft tabs a different width would be computed and we'd avoid snuggling them. A test has been added covering this case. --- src/chains.rs | 29 +++++-- src/closures.rs | 2 +- src/comment.rs | 12 ++- src/expr.rs | 42 +++++++--- src/items.rs | 91 +++++++++++++--------- src/lists.rs | 5 +- src/macros.rs | 7 +- src/missed_spans.rs | 5 +- src/overflow.rs | 7 +- src/pairs.rs | 18 +++-- src/string.rs | 7 +- src/types.rs | 13 +++- src/utils.rs | 28 +++++-- src/visitor.rs | 5 +- tests/target/issue_6859_hard_tab_breaks.rs | 11 +++ 15 files changed, 199 insertions(+), 83 deletions(-) create mode 100644 tests/target/issue_6859_hard_tab_breaks.rs diff --git a/src/chains.rs b/src/chains.rs index 90adb67ad43..97a79794d4a 100644 --- a/src/chains.rs +++ b/src/chains.rs @@ -566,8 +566,13 @@ impl Rewrite for Chain { formatter.format_root(&self.parent, context, shape)?; if let Some(result) = formatter.pure_root() { - return wrap_str(result, context.config.max_width(), shape) - .max_width_error(shape.width, self.parent.span); + return wrap_str( + result, + context.config.max_width(), + context.config.tab_spaces(), + shape, + ) + .max_width_error(shape.width, self.parent.span); } let first = self.children.first().unwrap_or(&self.parent); @@ -582,7 +587,13 @@ impl Rewrite for Chain { formatter.format_last_child(context, shape, child_shape)?; let result = formatter.join_rewrites(context, child_shape)?; - wrap_str(result, context.config.max_width(), shape).max_width_error(shape.width, full_span) + wrap_str( + result, + context.config.max_width(), + context.config.tab_spaces(), + shape, + ) + .max_width_error(shape.width, full_span) } } @@ -718,7 +729,7 @@ impl<'a> ChainFormatterShared<'a> { ) -> Result<(), RewriteError> { let last = self.children.last().unknown_error()?; let extendable = may_extend && last_line_extendable(&self.rewrites[0]); - let prev_last_line_width = last_line_width(&self.rewrites[0]); + let prev_last_line_width = last_line_width(&self.rewrites[0], context.config.tab_spaces()); // Total of all items excluding the last. let almost_total = if extendable { @@ -958,7 +969,8 @@ impl<'a> ChainFormatter for ChainFormatterVisual<'a> { let mut root_rewrite = parent.rewrite_result(context, parent_shape)?; let multiline = root_rewrite.contains('\n'); self.offset = if multiline { - last_line_width(&root_rewrite).saturating_sub(shape.used_width()) + last_line_width(&root_rewrite, context.config.tab_spaces()) + .saturating_sub(shape.used_width()) } else { trimmed_last_line_width(&root_rewrite) }; @@ -973,7 +985,12 @@ impl<'a> ChainFormatter for ChainFormatterVisual<'a> { .visual_indent(self.offset) .sub_width(self.offset, item.span)?; let rewrite = item.rewrite_result(context, child_shape)?; - if filtered_str_fits(&rewrite, context.config.max_width(), shape) { + if filtered_str_fits( + &rewrite, + context.config.max_width(), + context.config.tab_spaces(), + shape, + ) { root_rewrite.push_str(&rewrite); } else { // We couldn't fit in at the visual indent, try the last diff --git a/src/closures.rs b/src/closures.rs index 19cd0d9792c..6d5b43082ef 100644 --- a/src/closures.rs +++ b/src/closures.rs @@ -351,7 +351,7 @@ fn rewrite_closure_fn_decl( prefix.push_str(&ret_str); } // 1 = space between `|...|` and body. - let extra_offset = last_line_width(&prefix) + 1; + let extra_offset = last_line_width(&prefix, context.config.tab_spaces()) + 1; Ok((prefix, extra_offset)) } diff --git a/src/comment.rs b/src/comment.rs index 241934a7d3d..c0709d6a324 100644 --- a/src/comment.rs +++ b/src/comment.rs @@ -174,8 +174,9 @@ pub(crate) fn combine_strs_with_missing_comments( } else { " " }; - let mut one_line_width = - last_line_width(prev_str) + first_line_width(next_str) + first_sep.len(); + let mut one_line_width = last_line_width(prev_str, context.config.tab_spaces()) + + first_line_width(next_str) + + first_sep.len(); let config = context.config; let indent = shape.indent; @@ -207,7 +208,9 @@ pub(crate) fn combine_strs_with_missing_comments( let first_sep = if prev_str.is_empty() || missing_comment.is_empty() { Cow::from("") } else { - let one_line_width = last_line_width(prev_str) + first_line_width(&missing_comment) + 1; + let one_line_width = last_line_width(prev_str, context.config.tab_spaces()) + + first_line_width(&missing_comment) + + 1; if prefer_same_line && one_line_width <= shape.width { Cow::from(" ") } else { @@ -871,7 +874,8 @@ impl<'a> CommentRewrite<'a> { self.fmt.shape = if self.is_prev_line_multi_line { // 1 = " " - let offset = 1 + last_line_width(&self.result) - self.line_start.len(); + let offset = 1 + last_line_width(&self.result, self.fmt.config.tab_spaces()) + - self.line_start.len(); Shape { width: self.max_width.saturating_sub(offset), indent: self.fmt_indent, diff --git a/src/expr.rs b/src/expr.rs index b79137c4442..505cccbe1ef 100644 --- a/src/expr.rs +++ b/src/expr.rs @@ -279,6 +279,7 @@ pub(crate) fn format_expr( wrap_str( context.snippet(expr.span).to_owned(), context.config.max_width(), + context.config.tab_spaces(), shape, ) .max_width_error(shape.width, expr.span) @@ -597,7 +598,10 @@ fn rewrite_single_line_block( shape: Shape, ) -> RewriteResult { if let Some(block_expr) = stmt::Stmt::from_simple_block(context, block, attrs) { - let expr_shape = shape.offset_left(last_line_width(prefix), block_expr.span())?; + let expr_shape = shape.offset_left( + last_line_width(prefix, context.config.tab_spaces()), + block_expr.span(), + )?; let expr_str = block_expr.rewrite_result(context, expr_shape)?; let label_str = rewrite_label(context, label); let result = format!("{prefix}{label_str}{{ {expr_str} }}"); @@ -1097,7 +1101,7 @@ impl<'a> ControlFlow<'a> { }; let used_width = if pat_expr_string.contains('\n') { - last_line_width(&pat_expr_string) + last_line_width(&pat_expr_string, context.config.tab_spaces()) } else { // 2 = spaces after keyword and condition. label_string.len() + self.keyword.len() + pat_expr_string.len() + 2 @@ -1342,6 +1346,7 @@ pub(crate) fn rewrite_literal( _ => wrap_str( context.snippet(span).to_owned(), context.config.max_width(), + context.config.tab_spaces(), shape, ) .max_width_error(shape.width, span), @@ -1360,8 +1365,13 @@ fn rewrite_string_lit(context: &RewriteContext<'_>, span: Span, shape: Shape) -> { return Ok(string_lit.to_owned()); } else { - return wrap_str(string_lit.to_owned(), context.config.max_width(), shape) - .max_width_error(shape.width, span); + return wrap_str( + string_lit.to_owned(), + context.config.max_width(), + context.config.tab_spaces(), + shape, + ) + .max_width_error(shape.width, span); } } @@ -1402,6 +1412,7 @@ fn rewrite_int_lit( token_lit.suffix.as_ref().map_or("", |s| s.as_str()) ), context.config.max_width(), + context.config.tab_spaces(), shape, ) .max_width_error(shape.width, span); @@ -1411,6 +1422,7 @@ fn rewrite_int_lit( wrap_str( context.snippet(span).to_owned(), context.config.max_width(), + context.config.tab_spaces(), shape, ) .max_width_error(shape.width, span) @@ -1429,6 +1441,7 @@ fn rewrite_float_lit( return wrap_str( context.snippet(span).to_owned(), context.config.max_width(), + context.config.tab_spaces(), shape, ) .max_width_error(shape.width, span); @@ -1477,6 +1490,7 @@ fn rewrite_float_lit( suffix.unwrap_or(""), ), context.config.max_width(), + context.config.tab_spaces(), shape, ) .max_width_error(shape.width, span) @@ -1698,7 +1712,7 @@ fn rewrite_index( ) -> RewriteResult { let expr_str = expr.rewrite_result(context, shape)?; - let offset = last_line_width(&expr_str) + 1; + let offset = last_line_width(&expr_str, context.config.tab_spaces()) + 1; let rhs_overhead = shape.rhs_overhead(context.config); let index_shape = if expr_str.contains('\n') { Shape::legacy(context.config.max_width(), shape.indent) @@ -2220,11 +2234,12 @@ pub(crate) fn rewrite_assign_rhs_expr( rhs_kind: &RhsAssignKind<'_>, rhs_tactics: RhsTactics, ) -> RewriteResult { - let last_line_width = last_line_width(lhs).saturating_sub(if lhs.contains('\n') { - shape.indent.width() - } else { - 0 - }); + let last_line_width = + last_line_width(lhs, context.config.tab_spaces()).saturating_sub(if lhs.contains('\n') { + shape.indent.width() + } else { + 0 + }); // 1 = space between operator and rhs. let orig_shape = shape.offset_left_opt(last_line_width + 1).unwrap_or(Shape { width: 0, @@ -2321,7 +2336,12 @@ fn choose_rhs( match (orig_rhs, new_rhs) { (Ok(ref orig_rhs), Ok(ref new_rhs)) - if !filtered_str_fits(&new_rhs, context.config.max_width(), new_shape) => + if !filtered_str_fits( + &new_rhs, + context.config.max_width(), + context.config.tab_spaces(), + new_shape, + ) => { Ok(format!("{before_space_str}{orig_rhs}")) } diff --git a/src/items.rs b/src/items.rs index a2c0e8e0f50..60300222d68 100644 --- a/src/items.rs +++ b/src/items.rs @@ -107,7 +107,10 @@ impl Rewrite for ast::Local { } else { shape } - .offset_left(last_line_width(&result) + separator.len(), self.span())? + .offset_left( + last_line_width(&result, context.config.tab_spaces()) + separator.len(), + self.span(), + )? // 2 = ` =` .sub_width(2, self.span())?; @@ -437,7 +440,7 @@ impl<'a> FmtVisitor<'a> { // 2 = ` {` if self.config.brace_style() == BraceStyle::AlwaysNextLine || force_newline_brace - || last_line_width(&result) + 2 > self.shape().width + || last_line_width(&result, context.config.tab_spaces()) + 2 > self.shape().width { fn_brace_style = FnBraceStyle::NextLine } @@ -556,7 +559,7 @@ impl<'a> FmtVisitor<'a> { self.block_indent, // make a span that starts right after `enum Foo` mk_sp(ident.span.hi(), body_start), - last_line_width(&enum_header), + last_line_width(&enum_header, self.get_context().config.tab_spaces()), ) .unwrap(); self.push_str(&generics_str); @@ -810,10 +813,10 @@ pub(crate) fn format_impl( let where_budget = if result.contains('\n') { context.config.max_width() } else { - context.budget(last_line_width(&result)) + context.budget(last_line_width(&result, context.config.tab_spaces())) }; - let mut option = WhereClauseOption::snuggled(&ref_and_type); + let mut option = WhereClauseOption::snuggled(&ref_and_type, context.config.tab_spaces()); let snippet = context.snippet(item.span); let open_pos = snippet.find_uncommented("{").unknown_error()? + 1; if !contains_comment(&snippet[open_pos..]) @@ -848,7 +851,7 @@ pub(crate) fn format_impl( mk_sp(self_ty.span.hi(), hi), Shape::indented(offset, context.config), context, - last_line_width(&result), + last_line_width(&result, context.config.tab_spaces()), ) { Ok(ref missing_comment) if !missing_comment.is_empty() => { result.push_str(missing_comment); @@ -966,11 +969,17 @@ fn format_impl_ref_and_type( } let shape = if context.config.style_edition() >= StyleEdition::Edition2024 { - Shape::indented(offset + last_line_width(&result), context.config) + Shape::indented( + offset + last_line_width(&result, context.config.tab_spaces()), + context.config, + ) } else { generics_shape_from_config( context.config, - Shape::indented(offset + last_line_width(&result), context.config), + Shape::indented( + offset + last_line_width(&result, context.config.tab_spaces()), + context.config, + ), 0, item.span, )? @@ -985,7 +994,7 @@ fn format_impl_ref_and_type( ast::ImplPolarity::Negative(_) => "!", ast::ImplPolarity::Positive => "", }; - let result_len = last_line_width(&result); + let result_len = last_line_width(&result, context.config.tab_spaces()); result.push_str(&rewrite_trait_ref( context, &of_trait.trait_ref, @@ -1009,7 +1018,9 @@ fn format_impl_ref_and_type( } else { 0 }; - let used_space = last_line_width(&result) + trait_ref_overhead + curly_brace_overhead; + let used_space = last_line_width(&result, context.config.tab_spaces()) + + trait_ref_overhead + + curly_brace_overhead; // 1 = space before the type. let budget = context.budget(used_space + 1); if let Some(self_ty_str) = self_ty.rewrite(context, Shape::legacy(budget, offset)) { @@ -1032,7 +1043,7 @@ fn format_impl_ref_and_type( if of_trait.is_some() { result.push_str("for "); } - let budget = context.budget(last_line_width(&result)); + let budget = context.budget(last_line_width(&result, context.config.tab_spaces())); let type_offset = match context.config.indent_style() { IndentStyle::Visual => new_line_offset + trait_ref_overhead, IndentStyle::Block => new_line_offset, @@ -1205,13 +1216,13 @@ pub(crate) fn format_trait( if !generics.where_clause.predicates.is_empty() { let where_on_new_line = context.config.indent_style() != IndentStyle::Block; - let where_budget = context.budget(last_line_width(&result)); + let where_budget = context.budget(last_line_width(&result, context.config.tab_spaces())); let pos_before_where = if bounds.is_empty() { generics.where_clause.span.lo() } else { bounds[bounds.len() - 1].span().hi() }; - let option = WhereClauseOption::snuggled(&generics_str); + let option = WhereClauseOption::snuggled(&generics_str, context.config.tab_spaces()); let where_clause_str = rewrite_where_clause( context, &generics.where_clause, @@ -1227,7 +1238,9 @@ pub(crate) fn format_trait( // If the where-clause cannot fit on the same line, // put the where-clause on a new line if !where_clause_str.contains('\n') - && last_line_width(&result) + where_clause_str.len() + offset.width() + && last_line_width(&result, context.config.tab_spaces()) + + where_clause_str.len() + + offset.width() > context.config.comment_width() { let width = offset.block_indent + context.config.tab_spaces() - 1; @@ -1250,7 +1263,7 @@ pub(crate) fn format_trait( mk_sp(comment_lo, comment_hi), Shape::indented(offset, context.config), context, - last_line_width(&result), + last_line_width(&result, context.config.tab_spaces()), ) { Ok(ref missing_comment) if !missing_comment.is_empty() => { result.push_str(missing_comment); @@ -1267,7 +1280,8 @@ pub(crate) fn format_trait( match context.config.brace_style() { _ if last_line_contains_single_line_comment(&result) - || last_line_width(&result) + 2 > context.budget(offset.width()) => + || last_line_width(&result, context.config.tab_spaces()) + 2 + > context.budget(offset.width()) => { result.push_str(&offset.to_string_with_newline(context.config)); } @@ -1409,7 +1423,7 @@ fn format_unit_struct( offset, // make a span that starts right after `struct Foo` mk_sp(p.ident.span.hi(), hi), - last_line_width(&header_str), + last_line_width(&header_str, context.config.tab_spaces()), )? } else { String::new() @@ -1452,7 +1466,7 @@ pub(crate) fn format_struct_struct( offset, // make a span that starts right after `struct Foo` mk_sp(header_hi, body_lo), - last_line_width(&result), + last_line_width(&result, context.config.tab_spaces()), )?, None => { // 3 = ` {}`, 2 = ` {`. @@ -1536,7 +1550,7 @@ fn format_empty_struct_or_tuple( closer: &str, ) { // 3 = " {}" or "();" - let used_width = last_line_used_width(result, offset.width()) + 3; + let used_width = last_line_used_width(result, offset.width(), context.config.tab_spaces()) + 3; if used_width > context.config.max_width() { result.push_str(&offset.to_string_with_newline(context.config)) } @@ -1599,12 +1613,13 @@ fn format_tuple_struct( let where_clause_str = match struct_parts.generics { Some(generics) => { - let budget = context.budget(last_line_width(&header_str)); + let budget = context.budget(last_line_width(&header_str, context.config.tab_spaces())); let shape = Shape::legacy(budget, offset); let generics_str = rewrite_generics(context, "", generics, shape).ok()?; result.push_str(&generics_str); - let where_budget = context.budget(last_line_width(&result)); + let where_budget = + context.budget(last_line_width(&result, context.config.tab_spaces())); let option = WhereClauseOption::new(true, WhereClauseSpace::Newline); rewrite_where_clause( context, @@ -1776,8 +1791,8 @@ fn rewrite_ty( } } - let where_budget = context.budget(last_line_width(&result)); - let mut option = WhereClauseOption::snuggled(&result); + let where_budget = context.budget(last_line_width(&result, context.config.tab_spaces())); + let mut option = WhereClauseOption::snuggled(&result, context.config.tab_spaces()); if rhs.is_none() { option.suppress_comma(); } @@ -2320,7 +2335,7 @@ impl Rewrite for ast::Param { result.push_str(&before_comment); result.push_str(colon_spaces(context.config)); result.push_str(&after_comment); - let overhead = last_line_width(&result); + let overhead = last_line_width(&result, context.config.tab_spaces()); let max_width = shape .width .checked_sub(overhead) @@ -2348,7 +2363,7 @@ impl Rewrite for ast::Param { result.push_str(&before_comment); result.push_str(colon_spaces(context.config)); result.push_str(&after_comment); - let overhead = last_line_width(&result); + let overhead = last_line_width(&result, context.config.tab_spaces()); let max_width = shape .width .checked_sub(overhead) @@ -2478,7 +2493,7 @@ fn rewrite_fn_base( // 2 = `()` 2 }; - let used_width = last_line_used_width(&result, indent.width()); + let used_width = last_line_used_width(&result, indent.width(), context.config.tab_spaces()); let one_line_budget = context.budget(used_width + overhead); let shape = Shape { width: one_line_budget, @@ -2573,7 +2588,8 @@ fn rewrite_fn_base( result.push(')'); } else { result.push_str(¶m_str); - let used_width = last_line_used_width(&result, indent.width()) + first_line_width(&ret_str); + let used_width = last_line_used_width(&result, indent.width(), context.config.tab_spaces()) + + first_line_width(&ret_str); // Put the closing brace on the next line if it overflows the max width. // 1 = `)` let closing_paren_overflow_max_width = @@ -2671,11 +2687,12 @@ fn rewrite_fn_base( let ret_shape = Shape::indented(indent, context.config); ret_shape - .offset_left_opt(last_line_width(&result)) + .offset_left_opt(last_line_width(&result, context.config.tab_spaces())) .unwrap_or(ret_shape) }; - let exceeds_max_width = last_line_width(&result) + ret_str_len > context.config.max_width(); + let exceeds_max_width = last_line_width(&result, context.config.tab_spaces()) + ret_str_len + > context.config.max_width(); if multi_line_ret_str || ret_should_indent @@ -2752,7 +2769,7 @@ fn rewrite_fn_base( mk_sp(ret_span.lo(), span.hi()), shape, context, - last_line_width(&result), + last_line_width(&result, context.config.tab_spaces()), ) { Ok(ref missing_comment) if !missing_comment.is_empty() => { result.push_str(missing_comment); @@ -2801,10 +2818,10 @@ impl WhereClauseOption { } } - fn snuggled(current: &str) -> WhereClauseOption { + fn snuggled(current: &str, tab_spaces: usize) -> WhereClauseOption { WhereClauseOption { suppress_comma: false, - snuggle: if last_line_width(current) == 1 { + snuggle: if last_line_width(current, tab_spaces) == 1 { WhereClauseSpace::Space } else { WhereClauseSpace::Newline @@ -3360,8 +3377,12 @@ fn format_generics( span.lo() }; let (same_line_brace, missed_comments) = if !generics.where_clause.predicates.is_empty() { - let budget = context.budget(last_line_used_width(&result, offset.width())); - let mut option = WhereClauseOption::snuggled(&result); + let budget = context.budget(last_line_used_width( + &result, + offset.width(), + context.config.tab_spaces(), + )); + let mut option = WhereClauseOption::snuggled(&result, context.config.tab_spaces()); if brace_pos == BracePos::None { option.suppress_comma = true; } @@ -3417,7 +3438,7 @@ fn format_generics( if brace_pos == BracePos::None { return Some(result); } - let total_used_width = last_line_used_width(&result, used_width); + let total_used_width = last_line_used_width(&result, used_width, context.config.tab_spaces()); let remaining_budget = context.budget(total_used_width); // If the same line brace if forced, it indicates that we are rewriting an item with empty body, // and hence we take the closer into account as well for one line budget. diff --git a/src/lists.rs b/src/lists.rs index 9d811e5d9b5..f567a191e1f 100644 --- a/src/lists.rs +++ b/src/lists.rs @@ -425,7 +425,8 @@ where if tactic != DefinitiveListTactic::Horizontal && item.post_comment.is_some() { let comment = item.post_comment.as_ref().unwrap(); - let overhead = last_line_width(&result) + first_line_width(comment.trim()); + let overhead = last_line_width(&result, formatting.config.tab_spaces()) + + first_line_width(comment.trim()); let rewrite_post_comment = |item_max_width: &mut Option| { if item_max_width.is_none() && !last && !inner_item.contains('\n') { @@ -471,7 +472,7 @@ where let mut comment_alignment = post_comment_alignment(item_max_width, unicode_str_width(inner_item)); if first_line_width(&formatted_comment) - + last_line_width(&result) + + last_line_width(&result, formatting.config.tab_spaces()) + comment_alignment + 1 > formatting.config.max_width() diff --git a/src/macros.rs b/src/macros.rs index 2d56021069c..95cdebc5cb7 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -1360,7 +1360,12 @@ impl MacroBranch { } }; - if !filtered_str_fits(&new_body_snippet.snippet, config.max_width(), shape) { + if !filtered_str_fits( + &new_body_snippet.snippet, + config.max_width(), + context.config.tab_spaces(), + shape, + ) { return Err(RewriteError::ExceedsMaxWidth { configured_width: shape.width, span: self.span, diff --git a/src/missed_spans.rs b/src/missed_spans.rs index 2654d2464ee..8b124705ad2 100644 --- a/src/missed_spans.rs +++ b/src/missed_spans.rs @@ -266,7 +266,10 @@ impl<'a> FmtVisitor<'a> { self.block_indent } else { self.push_str(" "); - Indent::from_width(self.config, last_line_width(&self.buffer)) + Indent::from_width( + self.config, + last_line_width(&self.buffer, self.config.tab_spaces()), + ) }; let comment_width = ::std::cmp::min( diff --git a/src/overflow.rs b/src/overflow.rs index 4230c89b57b..9e9854278cf 100644 --- a/src/overflow.rs +++ b/src/overflow.rs @@ -385,7 +385,7 @@ impl<'a> Context<'a> { // 1 = "(" or ")" let one_line_shape = shape - .offset_left_opt(last_line_width(ident) + 1) + .offset_left_opt(last_line_width(ident, context.config.tab_spaces()) + 1) .and_then(|shape| shape.sub_width_opt(1)) .unwrap_or(Shape { width: 0, ..shape }); let nested_shape = shape_from_indent_style(context, shape, used_width + 2, used_width + 1); @@ -669,7 +669,10 @@ impl<'a> Context<'a> { fn wrap_items(&self, items_str: &str, shape: Shape, is_extendable: bool) -> String { let shape = Shape { - width: shape.width.saturating_sub(last_line_width(self.ident)), + width: shape.width.saturating_sub(last_line_width( + self.ident, + self.context.config.tab_spaces(), + )), ..shape }; diff --git a/src/pairs.rs b/src/pairs.rs index 48948b88b3b..b4e390f2061 100644 --- a/src/pairs.rs +++ b/src/pairs.rs @@ -86,7 +86,8 @@ fn rewrite_pairs_one_line( let prefix_len = result.len(); let last = list.list.last()?.0; - let cur_shape = base_shape.offset_left_opt(last_line_width(&result))?; + let cur_shape = + base_shape.offset_left_opt(last_line_width(&result, context.config.tab_spaces()))?; let last_rewrite = last.rewrite(context, cur_shape)?; result.push_str(&last_rewrite); @@ -102,7 +103,12 @@ fn rewrite_pairs_one_line( return None; } - wrap_str(result, context.config.max_width(), shape) + wrap_str( + result, + context.config.max_width(), + context.config.tab_spaces(), + shape, + ) } fn rewrite_pairs_multiline( @@ -132,7 +138,9 @@ fn rewrite_pairs_multiline( } else { shape.used_width() }; - if last_line_width(&result) + offset <= nested_shape.used_width() { + if last_line_width(&result, context.config.tab_spaces()) + 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)) @@ -193,7 +201,7 @@ where // Try to put both lhs and rhs on the same line. let rhs_orig_result = shape - .offset_left_opt(last_line_width(&lhs_result) + pp.infix.len()) + .offset_left_opt(last_line_width(&lhs_result, tab_spaces) + pp.infix.len()) .and_then(|s| s.sub_width_opt(pp.suffix.len())) .and_then(|rhs_shape| rhs.rewrite_result(context, rhs_shape).ok()); @@ -208,7 +216,7 @@ where .map(|first_line| first_line.ends_with('{')) .unwrap_or(false); if !rhs_result.contains('\n') || allow_same_line { - let one_line_width = last_line_width(&lhs_result) + let one_line_width = last_line_width(&lhs_result, tab_spaces) + pp.infix.len() + first_line_width(rhs_result) + pp.suffix.len(); diff --git a/src/string.rs b/src/string.rs index 59c445f904b..af90bb055c8 100644 --- a/src/string.rs +++ b/src/string.rs @@ -150,7 +150,12 @@ pub(crate) fn rewrite_string<'a>( } result.push_str(fmt.closer); - wrap_str(result, fmt.config.max_width(), fmt.shape) + wrap_str( + result, + fmt.config.max_width(), + fmt.config.tab_spaces(), + fmt.shape, + ) } /// Returns the index to the end of the URL if the split at index of the given string includes a diff --git a/src/types.rs b/src/types.rs index 94ed42c6ea5..a9f0bc5da7f 100644 --- a/src/types.rs +++ b/src/types.rs @@ -421,7 +421,10 @@ where shape.block().indent.to_string_with_newline(context.config), ) }; - if output.is_empty() || last_line_width(&args) + first_line_width(&output) <= shape.width { + if output.is_empty() + || last_line_width(&args, context.config.tab_spaces()) + first_line_width(&output) + <= shape.width + { Ok(format!("{args}{output}")) } else { Ok(format!( @@ -499,7 +502,9 @@ impl Rewrite for ast::WherePredicate { let mut result = String::with_capacity(attrs_str.len() + pred_str.len() + 1); result.push_str(&attrs_str); let pred_start = self.span.lo(); - let line_len = last_line_width(&attrs_str) + 1 + first_line_width(&pred_str); + let line_len = last_line_width(&attrs_str, context.config.tab_spaces()) + + 1 + + first_line_width(&pred_str); if let Some(last_attr) = self.attrs.last().filter(|last_attr| { contains_comment(context.snippet(mk_sp(last_attr.span.hi(), pred_start))) }) { @@ -598,7 +603,7 @@ fn rewrite_bounded_lifetime( Ok(result) } else { let colon = type_bound_colon(context); - let overhead = last_line_width(&result) + colon.len(); + let overhead = last_line_width(&result, context.config.tab_spaces()) + colon.len(); let shape = shape.sub_width(overhead, span)?; let result = format!( "{}{}{}", @@ -915,7 +920,7 @@ impl Rewrite for ast::Ty { true, )?; } else { - let used_width = last_line_width(&result); + let used_width = last_line_width(&result, context.config.tab_spaces()); let budget = shape .width .checked_sub(used_width) diff --git a/src/utils.rs b/src/utils.rs index cbc2d488462..398418b8345 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -218,15 +218,17 @@ pub(crate) fn first_line_width(s: &str) -> usize { /// The width of the last line in s. #[inline] -pub(crate) fn last_line_width(s: &str) -> usize { - unicode_str_width(s.rsplitn(2, '\n').next().unwrap_or("")) +pub(crate) fn last_line_width(s: &str, tab_spaces: usize) -> usize { + let last_line = s.rsplitn(2, '\n').next().unwrap_or(""); + let (prefix_width, prefix_end) = get_prefix_space_width_and_end(last_line, tab_spaces); + 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 { +pub(crate) fn last_line_used_width(s: &str, offset: usize, tab_spaces: usize) -> usize { if s.contains('\n') { - last_line_width(s) + last_line_width(s, tab_spaces) } else { offset + unicode_str_width(s) } @@ -400,15 +402,25 @@ macro_rules! skip_out_of_file_lines_range_visitor { // Wraps String in an Option. Returns Some when the string adheres to the // Rewrite constraints defined for the Rewrite trait and None otherwise. -pub(crate) fn wrap_str(s: String, max_width: usize, shape: Shape) -> Option { - if filtered_str_fits(&s, max_width, shape) { +pub(crate) fn wrap_str( + s: String, + max_width: usize, + tab_spaces: usize, + shape: Shape, +) -> Option { + if filtered_str_fits(&s, max_width, tab_spaces, shape) { Some(s) } else { None } } -pub(crate) fn filtered_str_fits(snippet: &str, max_width: usize, shape: Shape) -> bool { +pub(crate) fn filtered_str_fits( + snippet: &str, + max_width: usize, + tab_spaces: usize, + shape: Shape, +) -> bool { let snippet = &filter_normal_code(snippet); if !snippet.is_empty() { // First line must fits with `shape.width`. @@ -429,7 +441,7 @@ pub(crate) fn filtered_str_fits(snippet: &str, max_width: usize, shape: Shape) - } // A special check for the last line, since the caller may // place trailing characters on this line. - if last_line_width(snippet) > shape.used_width() + shape.width { + if last_line_width(snippet, tab_spaces) > shape.used_width() + shape.width { return false; } } diff --git a/src/visitor.rs b/src/visitor.rs index 4072a1d8697..7237eb4c1fe 100644 --- a/src/visitor.rs +++ b/src/visitor.rs @@ -277,7 +277,8 @@ impl<'b, 'a: 'b> FmtVisitor<'a> { let align_to_right = if unindent_comment && contains_comment(comment_snippet) { let first_lines = comment_snippet.splitn(2, '/').next().unwrap_or(""); - last_line_width(first_lines) > last_line_width(comment_snippet) + last_line_width(first_lines, config.tab_spaces()) + > last_line_width(comment_snippet, config.tab_spaces()) } else { false }; @@ -330,7 +331,7 @@ impl<'b, 'a: 'b> FmtVisitor<'a> { } else { if comment_on_same_line { // 1 = a space before `//` - let offset_len = 1 + last_line_width(&self.buffer) + let offset_len = 1 + last_line_width(&self.buffer, config.tab_spaces()) .saturating_sub(self.block_indent.width()); match comment_shape .visual_indent(offset_len) 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 + }; +}