From 5f591018119b26f0a4200eb4ac6178fb384062b9 Mon Sep 17 00:00:00 2001 From: Micha Reiser Date: Wed, 6 Sep 2023 09:10:13 +0200 Subject: [PATCH] Memoize text width (#6552) --- crates/ruff_formatter/src/arguments.rs | 2 +- crates/ruff_formatter/src/builders.rs | 56 +++------- crates/ruff_formatter/src/format_element.rs | 95 +++++++++++++--- .../src/format_element/document.rs | 35 +++--- crates/ruff_formatter/src/macros.rs | 8 +- crates/ruff_formatter/src/printer/mod.rs | 103 ++++++++++++------ .../src/comments/format.rs | 26 ++--- .../src/expression/expr_ipy_escape_command.rs | 2 +- .../src/expression/expr_name.rs | 2 +- .../src/expression/number.rs | 6 +- .../src/expression/string.rs | 23 ++-- .../src/other/identifier.rs | 2 +- .../src/statement/stmt_ipy_escape_command.rs | 2 +- crates/ruff_python_formatter/src/verbatim.rs | 35 +----- 14 files changed, 213 insertions(+), 184 deletions(-) diff --git a/crates/ruff_formatter/src/arguments.rs b/crates/ruff_formatter/src/arguments.rs index 28a1638db85c7..8fa70d73db8d8 100644 --- a/crates/ruff_formatter/src/arguments.rs +++ b/crates/ruff_formatter/src/arguments.rs @@ -129,7 +129,7 @@ mod tests { #[test] fn test_nesting() { - let mut context = FormatState::new(()); + let mut context = FormatState::new(SimpleFormatContext::default()); let mut buffer = VecBuffer::new(&mut context); write!( diff --git a/crates/ruff_formatter/src/builders.rs b/crates/ruff_formatter/src/builders.rs index 0f6962448425b..a47929a3e2a02 100644 --- a/crates/ruff_formatter/src/builders.rs +++ b/crates/ruff_formatter/src/builders.rs @@ -9,7 +9,9 @@ use Tag::*; use crate::format_element::tag::{Condition, Tag}; use crate::prelude::tag::{DedentMode, GroupMode, LabelId}; use crate::prelude::*; -use crate::{format_element, write, Argument, Arguments, FormatContext, GroupId, TextSize}; +use crate::{ + format_element, write, Argument, Arguments, FormatContext, FormatOptions, GroupId, TextSize, +}; use crate::{Buffer, VecBuffer}; /// A line break that only gets printed if the enclosing `Group` doesn't fit on a single line. @@ -348,7 +350,10 @@ pub struct Text<'a> { position: Option, } -impl Format for Text<'_> { +impl Format for Text<'_> +where + Context: FormatContext, +{ fn fmt(&self, f: &mut Formatter) -> FormatResult<()> { if let Some(source_position) = self.position { f.write_element(FormatElement::SourcePosition(source_position)); @@ -356,6 +361,7 @@ impl Format for Text<'_> { f.write_element(FormatElement::Text { text: self.text.to_string().into_boxed_str(), + text_width: TextWidth::from_text(self.text, f.options().tab_width()), }); Ok(()) @@ -369,31 +375,13 @@ impl std::fmt::Debug for Text<'_> { } /// Emits a text as it is written in the source document. Optimized to avoid allocations. -pub const fn source_text_slice( - range: TextRange, - newlines: ContainsNewlines, -) -> SourceTextSliceBuilder { - SourceTextSliceBuilder { - range, - new_lines: newlines, - } -} - -#[derive(Copy, Clone, Eq, PartialEq, Debug)] -pub enum ContainsNewlines { - /// The string contains newline characters - Yes, - /// The string contains no newline characters - No, - - /// The string may contain newline characters, search the string to determine if there are any newlines. - Detect, +pub const fn source_text_slice(range: TextRange) -> SourceTextSliceBuilder { + SourceTextSliceBuilder { range } } #[derive(Eq, PartialEq, Debug)] pub struct SourceTextSliceBuilder { range: TextRange, - new_lines: ContainsNewlines, } impl Format for SourceTextSliceBuilder @@ -405,28 +393,10 @@ where let slice = source_code.slice(self.range); debug_assert_no_newlines(slice.text(source_code)); - let contains_newlines = match self.new_lines { - ContainsNewlines::Yes => { - debug_assert!( - slice.text(source_code).contains('\n'), - "Text contains no new line characters but the caller specified that it does." - ); - true - } - ContainsNewlines::No => { - debug_assert!( - !slice.text(source_code).contains('\n'), - "Text contains new line characters but the caller specified that it does not." - ); - false - } - ContainsNewlines::Detect => slice.text(source_code).contains('\n'), - }; + let text_width = + TextWidth::from_text(slice.text(source_code), f.context().options().tab_width()); - f.write_element(FormatElement::SourceCodeSlice { - slice, - contains_newlines, - }); + f.write_element(FormatElement::SourceCodeSlice { slice, text_width }); Ok(()) } diff --git a/crates/ruff_formatter/src/format_element.rs b/crates/ruff_formatter/src/format_element.rs index ee927292cd40c..f5deac86c30d9 100644 --- a/crates/ruff_formatter/src/format_element.rs +++ b/crates/ruff_formatter/src/format_element.rs @@ -3,12 +3,14 @@ pub mod tag; use std::borrow::Cow; use std::hash::{Hash, Hasher}; +use std::num::NonZeroU32; use std::ops::Deref; use std::rc::Rc; +use unicode_width::UnicodeWidthChar; use crate::format_element::tag::{GroupMode, LabelId, Tag}; use crate::source_code::SourceCodeSlice; -use crate::TagKind; +use crate::{TabWidth, TagKind}; use ruff_text_size::TextSize; /// Language agnostic IR for formatting source code. @@ -37,13 +39,13 @@ pub enum FormatElement { Text { /// There's no need for the text to be mutable, using `Box` safes 8 bytes over `String`. text: Box, + text_width: TextWidth, }, /// Text that gets emitted as it is in the source code. Optimized to avoid any allocations. SourceCodeSlice { slice: SourceCodeSlice, - /// Whether the string contains any new line characters - contains_newlines: bool, + text_width: TextWidth, }, /// Prevents that line suffixes move past this boundary. Forces the printer to print any pending @@ -73,13 +75,10 @@ impl std::fmt::Debug for FormatElement { FormatElement::ExpandParent => write!(fmt, "ExpandParent"), FormatElement::Token { text } => fmt.debug_tuple("Token").field(text).finish(), FormatElement::Text { text, .. } => fmt.debug_tuple("DynamicText").field(text).finish(), - FormatElement::SourceCodeSlice { - slice, - contains_newlines, - } => fmt + FormatElement::SourceCodeSlice { slice, text_width } => fmt .debug_tuple("Text") .field(slice) - .field(contains_newlines) + .field(text_width) .finish(), FormatElement::LineSuffixBoundary => write!(fmt, "LineSuffixBoundary"), FormatElement::BestFitting { variants, mode } => fmt @@ -255,11 +254,8 @@ impl FormatElements for FormatElement { FormatElement::ExpandParent => true, FormatElement::Tag(Tag::StartGroup(group)) => !group.mode().is_flat(), FormatElement::Line(line_mode) => matches!(line_mode, LineMode::Hard | LineMode::Empty), - - FormatElement::Text { text, .. } => text.contains('\n'), - FormatElement::SourceCodeSlice { - contains_newlines, .. - } => *contains_newlines, + FormatElement::Text { text_width, .. } => text_width.is_multiline(), + FormatElement::SourceCodeSlice { text_width, .. } => text_width.is_multiline(), FormatElement::Interned(interned) => interned.will_break(), // Traverse into the most flat version because the content is guaranteed to expand when even // the most flat version contains some content that forces a break. @@ -403,6 +399,67 @@ pub trait FormatElements { fn end_tag(&self, kind: TagKind) -> Option<&Tag>; } +/// New-type wrapper for a single-line text unicode width. +/// Mainly to prevent access to the inner value. +/// +/// ## Representation +/// +/// Represents the width by adding 1 to the actual width so that the width can be represented by a [`NonZeroU32`], +/// allowing [`TextWidth`] or [`Option`] fit in 4 bytes rather than 8. +/// +/// This means that 2^32 can not be precisely represented and instead has the same value as 2^32-1. +/// This imprecision shouldn't matter in practice because either text are longer than any configured line width +/// and thus, the text should break. +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub struct Width(NonZeroU32); + +impl Width { + pub(crate) const fn new(width: u32) -> Self { + Width(NonZeroU32::MIN.saturating_add(width)) + } + + pub const fn value(self) -> u32 { + self.0.get() - 1 + } +} + +/// The pre-computed unicode width of a text if it is a single-line text or a marker +/// that it is a multiline text if it contains a line feed. +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub enum TextWidth { + Width(Width), + Multiline, +} + +impl TextWidth { + pub fn from_text(text: &str, tab_width: TabWidth) -> TextWidth { + let mut width = 0u32; + + for c in text.chars() { + let char_width = match c { + '\t' => tab_width.value(), + '\n' => return TextWidth::Multiline, + #[allow(clippy::cast_possible_truncation)] + c => c.width().unwrap_or(0) as u32, + }; + width += char_width; + } + + Self::Width(Width::new(width)) + } + + pub const fn width(self) -> Option { + match self { + TextWidth::Width(width) => Some(width), + TextWidth::Multiline => None, + } + } + + pub(crate) const fn is_multiline(self) -> bool { + matches!(self, TextWidth::Multiline) + } +} + #[cfg(test)] mod tests { @@ -430,19 +487,21 @@ mod sizes { // be recomputed at a later point in time? // You reduced the size of a format element? Excellent work! + use super::{BestFittingVariants, Interned, TextWidth}; use static_assertions::assert_eq_size; assert_eq_size!(ruff_text_size::TextRange, [u8; 8]); - assert_eq_size!(crate::prelude::tag::VerbatimKind, [u8; 8]); - assert_eq_size!(crate::prelude::Interned, [u8; 16]); - assert_eq_size!(crate::format_element::BestFittingVariants, [u8; 16]); + assert_eq_size!(TextWidth, [u8; 4]); + assert_eq_size!(super::tag::VerbatimKind, [u8; 8]); + assert_eq_size!(Interned, [u8; 16]); + assert_eq_size!(BestFittingVariants, [u8; 16]); #[cfg(not(debug_assertions))] assert_eq_size!(crate::SourceCodeSlice, [u8; 8]); #[cfg(not(debug_assertions))] - assert_eq_size!(crate::format_element::Tag, [u8; 16]); + assert_eq_size!(super::Tag, [u8; 16]); #[cfg(not(debug_assertions))] - assert_eq_size!(crate::FormatElement, [u8; 24]); + assert_eq_size!(super::FormatElement, [u8; 24]); } diff --git a/crates/ruff_formatter/src/format_element/document.rs b/crates/ruff_formatter/src/format_element/document.rs index ae6761296c4d1..0ae0a333ea6fe 100644 --- a/crates/ruff_formatter/src/format_element/document.rs +++ b/crates/ruff_formatter/src/format_element/document.rs @@ -104,10 +104,11 @@ impl Document { expands = false; continue; } - FormatElement::Text { text, .. } => text.contains('\n'), - FormatElement::SourceCodeSlice { - contains_newlines, .. - } => *contains_newlines, + FormatElement::Text { + text: _, + text_width, + } => text_width.is_multiline(), + FormatElement::SourceCodeSlice { text_width, .. } => text_width.is_multiline(), FormatElement::ExpandParent | FormatElement::Line(LineMode::Hard | LineMode::Empty) => true, _ => false, @@ -259,11 +260,16 @@ impl Format> for &[FormatElement] { | FormatElement::Text { .. } | FormatElement::SourceCodeSlice { .. }) => { fn write_escaped(element: &FormatElement, f: &mut Formatter) { - let text = match element { - FormatElement::Token { text } => text, - FormatElement::Text { text } => text.as_ref(), - FormatElement::SourceCodeSlice { slice, .. } => { - slice.text(f.context().source_code()) + let (text, text_width) = match element { + #[allow(clippy::cast_possible_truncation)] + FormatElement::Token { text } => { + (*text, TextWidth::Width(Width::new(text.len() as u32))) + } + FormatElement::Text { text, text_width } => { + (text.as_ref(), *text_width) + } + FormatElement::SourceCodeSlice { slice, text_width } => { + (slice.text(f.context().source_code()), *text_width) } _ => unreachable!(), }; @@ -271,6 +277,7 @@ impl Format> for &[FormatElement] { if text.contains('"') { f.write_element(FormatElement::Text { text: text.replace('"', r#"\""#).into(), + text_width, }); } else { f.write_element(element.clone()); @@ -854,15 +861,9 @@ mod tests { [group(&format_args![ token("("), soft_block_indent(&format_args![ - source_text_slice( - TextRange::at(TextSize::new(0), TextSize::new(19)), - ContainsNewlines::No - ), + source_text_slice(TextRange::at(TextSize::new(0), TextSize::new(19)),), space(), - source_text_slice( - TextRange::at(TextSize::new(20), TextSize::new(28)), - ContainsNewlines::No - ), + source_text_slice(TextRange::at(TextSize::new(20), TextSize::new(28)),), ]) ])] ) diff --git a/crates/ruff_formatter/src/macros.rs b/crates/ruff_formatter/src/macros.rs index d3745d1d3c582..97a4cc696115d 100644 --- a/crates/ruff_formatter/src/macros.rs +++ b/crates/ruff_formatter/src/macros.rs @@ -343,15 +343,15 @@ mod tests { struct TestFormat; - impl Format<()> for TestFormat { - fn fmt(&self, f: &mut Formatter<()>) -> FormatResult<()> { + impl Format for TestFormat { + fn fmt(&self, f: &mut Formatter) -> FormatResult<()> { write!(f, [token("test")]) } } #[test] fn test_single_element() { - let mut state = FormatState::new(()); + let mut state = FormatState::new(SimpleFormatContext::default()); let mut buffer = VecBuffer::new(&mut state); write![&mut buffer, [TestFormat]].unwrap(); @@ -364,7 +364,7 @@ mod tests { #[test] fn test_multiple_elements() { - let mut state = FormatState::new(()); + let mut state = FormatState::new(SimpleFormatContext::default()); let mut buffer = VecBuffer::new(&mut state); write![ diff --git a/crates/ruff_formatter/src/printer/mod.rs b/crates/ruff_formatter/src/printer/mod.rs index 803cfa07dafe8..3c3c7e970a35a 100644 --- a/crates/ruff_formatter/src/printer/mod.rs +++ b/crates/ruff_formatter/src/printer/mod.rs @@ -9,8 +9,8 @@ use ruff_text_size::{Ranged, TextLen, TextSize}; use crate::format_element::document::Document; use crate::format_element::tag::{Condition, GroupMode}; use crate::format_element::{BestFittingMode, BestFittingVariants, LineMode, PrintMode}; -use crate::prelude::tag; use crate::prelude::tag::{DedentMode, Tag, TagKind, VerbatimKind}; +use crate::prelude::{tag, TextWidth}; use crate::printer::call_stack::{ CallStack, FitsCallStack, PrintCallStack, PrintElementArgs, StackFrame, }; @@ -96,10 +96,22 @@ impl<'a> Printer<'a> { match element { FormatElement::Space => self.print_text(Text::Token(" "), None), FormatElement::Token { text } => self.print_text(Text::Token(text), None), - FormatElement::Text { text } => self.print_text(Text::Text(text), None), - FormatElement::SourceCodeSlice { slice, .. } => { + FormatElement::Text { text, text_width } => self.print_text( + Text::Text { + text, + text_width: *text_width, + }, + None, + ), + FormatElement::SourceCodeSlice { slice, text_width } => { let text = slice.text(self.source_code); - self.print_text(Text::Text(text), Some(slice.range())); + self.print_text( + Text::Text { + text, + text_width: *text_width, + }, + Some(slice.range()), + ); } FormatElement::Line(line_mode) => { if args.mode().is_flat() @@ -395,9 +407,17 @@ impl<'a> Printer<'a> { self.state.buffer.push_str(token); self.state.line_width += token.len() as u32; } - Text::Text(text) => { - for char in text.chars() { - self.print_char(char); + Text::Text { + text, + text_width: width, + } => { + if let Some(width) = width.width() { + self.state.buffer.push_str(text); + self.state.line_width += width.value(); + } else { + for char in text.chars() { + self.print_char(char); + } } } } @@ -1086,10 +1106,24 @@ impl<'a, 'print> FitsMeasurer<'a, 'print> { } FormatElement::Token { text } => return Ok(self.fits_text(Text::Token(text), args)), - FormatElement::Text { text, .. } => return Ok(self.fits_text(Text::Text(text), args)), - FormatElement::SourceCodeSlice { slice, .. } => { + FormatElement::Text { text, text_width } => { + return Ok(self.fits_text( + Text::Text { + text, + text_width: *text_width, + }, + args, + )) + } + FormatElement::SourceCodeSlice { slice, text_width } => { let text = slice.text(self.printer.source_code); - return Ok(self.fits_text(Text::Text(text), args)); + return Ok(self.fits_text( + Text::Text { + text, + text_width: *text_width, + }, + args, + )); } FormatElement::LineSuffixBoundary => { if self.state.has_line_suffix { @@ -1307,27 +1341,31 @@ impl<'a, 'print> FitsMeasurer<'a, 'print> { Text::Token(token) => { self.state.line_width += token.len() as u32; } - Text::Text(text) => { - for c in text.chars() { - let char_width = match c { - '\t' => self.options().tab_width.value(), - '\n' => { - if self.must_be_flat { - return Fits::No; - } - match args.measure_mode() { - MeasureMode::FirstLine => return Fits::Yes, - MeasureMode::AllLines => { - self.state.line_width = 0; - continue; + Text::Text { text, text_width } => { + if let Some(width) = text_width.width() { + self.state.line_width += width.value(); + } else { + for c in text.chars() { + let char_width = match c { + '\t' => self.options().tab_width.value(), + '\n' => { + if self.must_be_flat { + return Fits::No; + } + match args.measure_mode() { + MeasureMode::FirstLine => return Fits::Yes, + MeasureMode::AllLines => { + self.state.line_width = 0; + continue; + } } } - } - // SAFETY: A u32 is sufficient to format files <= 4GB - #[allow(clippy::cast_possible_truncation)] - c => c.width().unwrap_or(0) as u32, - }; - self.state.line_width += char_width; + // SAFETY: A u32 is sufficient to format files <= 4GB + #[allow(clippy::cast_possible_truncation)] + c => c.width().unwrap_or(0) as u32, + }; + self.state.line_width += char_width; + } } } } @@ -1451,7 +1489,10 @@ enum Text<'a> { /// ASCII only text that contains no line breaks or tab characters. Token(&'a str), /// Arbitrary text. May contain `\n` line breaks, tab characters, or unicode characters. - Text(&'a str), + Text { + text: &'a str, + text_width: TextWidth, + }, } #[cfg(test)] @@ -1669,7 +1710,7 @@ two lines`, #[test] fn test_fill_breaks() { - let mut state = FormatState::new(()); + let mut state = FormatState::new(SimpleFormatContext::default()); let mut buffer = VecBuffer::new(&mut state); let mut formatter = Formatter::new(&mut buffer); diff --git a/crates/ruff_python_formatter/src/comments/format.rs b/crates/ruff_python_formatter/src/comments/format.rs index 96bf58089a7be..794f21a54a075 100644 --- a/crates/ruff_python_formatter/src/comments/format.rs +++ b/crates/ruff_python_formatter/src/comments/format.rs @@ -1,7 +1,5 @@ use std::borrow::Cow; -use unicode_width::UnicodeWidthChar; - use ruff_formatter::{format_args, write, FormatError, FormatOptions, SourceCode}; use ruff_python_ast::node::{AnyNodeRef, AstNode}; use ruff_python_trivia::{lines_after, lines_after_ignoring_trivia, lines_before}; @@ -377,16 +375,12 @@ impl Format> for FormatTrailingEndOfLineComment<'_> { 0 } else { // Start with 2 because of the two leading spaces. - let mut width = 2; - - // SAFETY: The formatted file is <= 4GB, and each comment should as well. - #[allow(clippy::cast_possible_truncation)] - for c in normalized_comment.chars() { - width += match c { - '\t' => f.options().tab_width().value(), - c => c.width().unwrap_or(0) as u32, - } - } + let width = 2u32.saturating_add( + TextWidth::from_text(&normalized_comment, f.options().tab_width()) + .width() + .expect("Expected comment not to contain any newlines") + .value(), + ); width }; @@ -430,11 +424,9 @@ pub(crate) struct FormatNormalizedComment<'a> { impl Format> for FormatNormalizedComment<'_> { fn fmt(&self, f: &mut Formatter) -> FormatResult<()> { match self.comment { - Cow::Borrowed(borrowed) => source_text_slice( - TextRange::at(self.range.start(), borrowed.text_len()), - ContainsNewlines::No, - ) - .fmt(f), + Cow::Borrowed(borrowed) => { + source_text_slice(TextRange::at(self.range.start(), borrowed.text_len())).fmt(f) + } Cow::Owned(ref owned) => { write!( diff --git a/crates/ruff_python_formatter/src/expression/expr_ipy_escape_command.rs b/crates/ruff_python_formatter/src/expression/expr_ipy_escape_command.rs index e225a8eaae77e..7e084055ac7a7 100644 --- a/crates/ruff_python_formatter/src/expression/expr_ipy_escape_command.rs +++ b/crates/ruff_python_formatter/src/expression/expr_ipy_escape_command.rs @@ -8,6 +8,6 @@ pub struct FormatExprIpyEscapeCommand; impl FormatNodeRule for FormatExprIpyEscapeCommand { fn fmt_fields(&self, item: &ExprIpyEscapeCommand, f: &mut PyFormatter) -> FormatResult<()> { - source_text_slice(item.range(), ContainsNewlines::No).fmt(f) + source_text_slice(item.range()).fmt(f) } } diff --git a/crates/ruff_python_formatter/src/expression/expr_name.rs b/crates/ruff_python_formatter/src/expression/expr_name.rs index e0585f55abee5..b47021cf5439e 100644 --- a/crates/ruff_python_formatter/src/expression/expr_name.rs +++ b/crates/ruff_python_formatter/src/expression/expr_name.rs @@ -21,7 +21,7 @@ impl FormatNodeRule for FormatExprName { .text(f.context().source_code()) ); - write!(f, [source_text_slice(*range, ContainsNewlines::No)]) + write!(f, [source_text_slice(*range)]) } fn fmt_dangling_comments( diff --git a/crates/ruff_python_formatter/src/expression/number.rs b/crates/ruff_python_formatter/src/expression/number.rs index 8bf6d266a8b32..130640f48ff1a 100644 --- a/crates/ruff_python_formatter/src/expression/number.rs +++ b/crates/ruff_python_formatter/src/expression/number.rs @@ -24,7 +24,7 @@ impl Format> for FormatInt<'_> { let normalized = normalize_integer(content); match normalized { - Cow::Borrowed(_) => source_text_slice(range, ContainsNewlines::No).fmt(f), + Cow::Borrowed(_) => source_text_slice(range).fmt(f), Cow::Owned(normalized) => text(&normalized, Some(range.start())).fmt(f), } } @@ -49,7 +49,7 @@ impl Format> for FormatFloat<'_> { let normalized = normalize_floating_number(content); match normalized { - Cow::Borrowed(_) => source_text_slice(range, ContainsNewlines::No).fmt(f), + Cow::Borrowed(_) => source_text_slice(range).fmt(f), Cow::Owned(normalized) => text(&normalized, Some(range.start())).fmt(f), } } @@ -75,7 +75,7 @@ impl Format> for FormatComplex<'_> { match normalized { Cow::Borrowed(_) => { - source_text_slice(range.sub_end(TextSize::from(1)), ContainsNewlines::No).fmt(f)?; + source_text_slice(range.sub_end(TextSize::from(1))).fmt(f)?; } Cow::Owned(normalized) => { text(&normalized, Some(range.start())).fmt(f)?; diff --git a/crates/ruff_python_formatter/src/expression/string.rs b/crates/ruff_python_formatter/src/expression/string.rs index e6165942ba9f4..ac0a0387c6edb 100644 --- a/crates/ruff_python_formatter/src/expression/string.rs +++ b/crates/ruff_python_formatter/src/expression/string.rs @@ -314,7 +314,7 @@ impl FormatStringPart { impl Format> for FormatStringPart { fn fmt(&self, f: &mut PyFormatter) -> FormatResult<()> { - let (normalized, contains_newlines) = normalize_string( + let normalized = normalize_string( f.context().locator().slice(self.range), self.preferred_quotes, self.is_raw_string, @@ -323,7 +323,7 @@ impl Format> for FormatStringPart { write!(f, [self.prefix, self.preferred_quotes])?; match normalized { Cow::Borrowed(_) => { - source_text_slice(self.range(), contains_newlines).fmt(f)?; + source_text_slice(self.range()).fmt(f)?; } Cow::Owned(normalized) => { text(&normalized, Some(self.start())).fmt(f)?; @@ -604,11 +604,7 @@ impl Format> for StringQuotes { /// with the provided `style`. /// /// Returns the normalized string and whether it contains new lines. -fn normalize_string( - input: &str, - quotes: StringQuotes, - is_raw: bool, -) -> (Cow, ContainsNewlines) { +fn normalize_string(input: &str, quotes: StringQuotes, is_raw: bool) -> Cow { // The normalized string if `input` is not yet normalized. // `output` must remain empty if `input` is already normalized. let mut output = String::new(); @@ -616,8 +612,6 @@ fn normalize_string( // If `last_index` is `0` at the end, then the input is already normalized and can be returned as is. let mut last_index = 0; - let mut newlines = ContainsNewlines::No; - let style = quotes.style; let preferred_quote = style.as_char(); let opposite_quote = style.invert().as_char(); @@ -638,9 +632,6 @@ fn normalize_string( } last_index = index + '\r'.len_utf8(); - newlines = ContainsNewlines::Yes; - } else if c == '\n' { - newlines = ContainsNewlines::Yes; } else if !quotes.triple && !is_raw { if c == '\\' { if let Some(next) = input.as_bytes().get(index + 1).copied().map(char::from) { @@ -675,7 +666,7 @@ fn normalize_string( Cow::Owned(output) }; - (normalized, newlines) + normalized } /// For docstring indentation, black counts spaces as 1 and tabs by increasing the indentation up @@ -792,7 +783,7 @@ fn format_docstring(string_part: &FormatStringPart, f: &mut PyFormatter) -> Form return string_part.fmt(f); } - let (normalized, _) = normalize_string( + let normalized = normalize_string( locator.slice(string_part), string_part.preferred_quotes, string_part.is_raw_string, @@ -837,7 +828,7 @@ fn format_docstring(string_part: &FormatStringPart, f: &mut PyFormatter) -> Form let trimmed_line_range = TextRange::at(offset, trim_end.text_len()).add_start(leading_whitespace); if already_normalized { - source_text_slice(trimmed_line_range, ContainsNewlines::No).fmt(f)?; + source_text_slice(trimmed_line_range).fmt(f)?; } else { text(trim_both, Some(trimmed_line_range.start())).fmt(f)?; } @@ -954,7 +945,7 @@ fn format_docstring_line( let trimmed_line_range = TextRange::at(offset, trim_end.text_len()).add_start(stripped_indentation); if already_normalized { - source_text_slice(trimmed_line_range, ContainsNewlines::No).fmt(f)?; + source_text_slice(trimmed_line_range).fmt(f)?; } else { // All indents are ascii spaces, so the slicing is correct text( diff --git a/crates/ruff_python_formatter/src/other/identifier.rs b/crates/ruff_python_formatter/src/other/identifier.rs index b889589302210..ae3dd98f67000 100644 --- a/crates/ruff_python_formatter/src/other/identifier.rs +++ b/crates/ruff_python_formatter/src/other/identifier.rs @@ -8,7 +8,7 @@ pub struct FormatIdentifier; impl FormatRule> for FormatIdentifier { fn fmt(&self, item: &Identifier, f: &mut PyFormatter) -> FormatResult<()> { - source_text_slice(item.range(), ContainsNewlines::No).fmt(f) + source_text_slice(item.range()).fmt(f) } } diff --git a/crates/ruff_python_formatter/src/statement/stmt_ipy_escape_command.rs b/crates/ruff_python_formatter/src/statement/stmt_ipy_escape_command.rs index 50add1990ffde..b41305f42abe8 100644 --- a/crates/ruff_python_formatter/src/statement/stmt_ipy_escape_command.rs +++ b/crates/ruff_python_formatter/src/statement/stmt_ipy_escape_command.rs @@ -9,7 +9,7 @@ pub struct FormatStmtIpyEscapeCommand; impl FormatNodeRule for FormatStmtIpyEscapeCommand { fn fmt_fields(&self, item: &StmtIpyEscapeCommand, f: &mut PyFormatter) -> FormatResult<()> { - source_text_slice(item.range(), ContainsNewlines::No).fmt(f) + source_text_slice(item.range()).fmt(f) } fn is_suppressed( diff --git a/crates/ruff_python_formatter/src/verbatim.rs b/crates/ruff_python_formatter/src/verbatim.rs index f40723de99ee7..deaa8cf4c5a0e 100644 --- a/crates/ruff_python_formatter/src/verbatim.rs +++ b/crates/ruff_python_formatter/src/verbatim.rs @@ -709,7 +709,7 @@ impl Format> for FormatVerbatimStatementRange { } } else { // Non empty line, write the text of the line - verbatim_text(trimmed_line_range, logical_line.contains_newlines).fmt(f)?; + verbatim_text(trimmed_line_range).fmt(f)?; // Write the line separator that terminates the line, except if it is the last line (that isn't separated by a hard line break). if logical_line.has_trailing_newline { @@ -760,7 +760,6 @@ where fn next(&mut self) -> Option { let mut parens = 0u32; - let mut contains_newlines = ContainsNewlines::No; let (content_end, full_end) = loop { match self.lexer.next() { @@ -768,18 +767,12 @@ where Tok::Newline => break (range.start(), range.end()), // Ignore if inside an expression Tok::NonLogicalNewline if parens == 0 => break (range.start(), range.end()), - Tok::NonLogicalNewline => { - contains_newlines = ContainsNewlines::Yes; - } Tok::Lbrace | Tok::Lpar | Tok::Lsqb => { parens = parens.saturating_add(1); } Tok::Rbrace | Tok::Rpar | Tok::Rsqb => { parens = parens.saturating_sub(1); } - Tok::String { value, .. } if value.contains(['\n', '\r']) => { - contains_newlines = ContainsNewlines::Yes; - } _ => {} }, None => { @@ -790,7 +783,6 @@ where self.last_line_end = self.content_end; Some(Ok(LogicalLine { content_range: TextRange::new(content_start, self.content_end), - contains_newlines: ContainsNewlines::No, has_trailing_newline: false, })) } else { @@ -810,7 +802,6 @@ where Some(Ok(LogicalLine { content_range: TextRange::new(line_start, content_end), - contains_newlines, has_trailing_newline: true, })) } @@ -822,8 +813,6 @@ impl FusedIterator for LogicalLinesIter where I: Iterator(item: T, contains_newlines: ContainsNewlines) -> VerbatimText +fn verbatim_text(item: T) -> VerbatimText where T: Ranged, { VerbatimText { verbatim_range: item.range(), - contains_newlines, } } @@ -859,13 +846,7 @@ impl Format> for VerbatimText { match normalize_newlines(f.context().locator().slice(self.verbatim_range), ['\r']) { Cow::Borrowed(_) => { - write!( - f, - [source_text_slice( - self.verbatim_range, - self.contains_newlines - )] - )?; + write!(f, [source_text_slice(self.verbatim_range,)])?; } Cow::Owned(cleaned) => { write!( @@ -924,7 +905,7 @@ impl Format> for FormatSuppressedNode<'_> { f, [ leading_comments(node_comments.leading), - verbatim_text(self.node, ContainsNewlines::Detect), + verbatim_text(self.node), trailing_comments(node_comments.trailing) ] ) @@ -937,13 +918,7 @@ pub(crate) fn write_suppressed_clause_header( f: &mut PyFormatter, ) -> FormatResult<()> { // Write the outer comments and format the node as verbatim - write!( - f, - [verbatim_text( - header.range(f.context().source())?, - ContainsNewlines::Detect - )] - )?; + write!(f, [verbatim_text(header.range(f.context().source())?)])?; let comments = f.context().comments(); header.visit(&mut |child| {