diff --git a/Cargo.lock b/Cargo.lock index 8043bb4875..6221b28476 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2354,6 +2354,7 @@ dependencies = [ "similar", "smallvec", "thiserror", + "unicode-width", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index f8c0a066de..7100aeec35 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -49,6 +49,7 @@ toml = { version = "0.7.2" } tracing = "0.1.37" tracing-indicatif = "0.3.4" tracing-subscriber = { version = "0.3.17", features = ["env-filter"] } +unicode-width = "0.1.10" wsl = { version = "0.1.0" } # v1.0.1 diff --git a/crates/ruff/Cargo.toml b/crates/ruff/Cargo.toml index a65f8ca7f3..dd8f42778b 100644 --- a/crates/ruff/Cargo.toml +++ b/crates/ruff/Cargo.toml @@ -75,7 +75,7 @@ strum_macros = { workspace = true } thiserror = { version = "1.0.43" } toml = { workspace = true } typed-arena = { version = "2.0.2" } -unicode-width = { version = "0.1.10" } +unicode-width = { workspace = true } unicode_names2 = { version = "0.6.0", git = "https://github.com/youknowone/unicode_names2.git", rev = "4ce16aa85cbcdd9cc830410f1a72ef9a235f2fde" } wsl = { version = "0.1.0" } diff --git a/crates/ruff_formatter/Cargo.toml b/crates/ruff_formatter/Cargo.toml index 2f9c52ee06..a4cf178196 100644 --- a/crates/ruff_formatter/Cargo.toml +++ b/crates/ruff_formatter/Cargo.toml @@ -17,9 +17,9 @@ drop_bomb = { version = "0.1.5" } rustc-hash = { workspace = true } schemars = { workspace = true, optional = true } serde = { workspace = true, optional = true } -tracing = { version = "0.1.37", default-features = false, features = ["std"] } -unicode-width = { version = "0.1.10" } static_assertions = "1.1.0" +tracing = { version = "0.1.37", default-features = false, features = ["std"] } +unicode-width = { workspace = true } [dev-dependencies] insta = { workspace = true } diff --git a/crates/ruff_python_formatter/Cargo.toml b/crates/ruff_python_formatter/Cargo.toml index b57a90faed..637276bab1 100644 --- a/crates/ruff_python_formatter/Cargo.toml +++ b/crates/ruff_python_formatter/Cargo.toml @@ -30,6 +30,7 @@ rustc-hash = { workspace = true } serde = { workspace = true, optional = true } smallvec = { workspace = true } thiserror = { workspace = true } +unicode-width = { workspace = true } [dev-dependencies] ruff_formatter = { path = "../ruff_formatter", features = ["serde"] } diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/.editorconfig b/crates/ruff_python_formatter/resources/test/fixtures/ruff/.editorconfig new file mode 100644 index 0000000000..ddc5dc593f --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/.editorconfig @@ -0,0 +1,3 @@ +[mixed_space_and_tab.py] +generated_code = true +ij_formatter_enabled = false \ No newline at end of file diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/fmt_on_off/comments.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/fmt_on_off/comments.py index 0a8118079f..ede6493669 100644 --- a/crates/ruff_python_formatter/resources/test/fixtures/ruff/fmt_on_off/comments.py +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/fmt_on_off/comments.py @@ -10,8 +10,13 @@ a + b # a trailing comment def test(): pass - # trailing comment that falls into the verbatim range + # under indent + def nested(): + ... + + # trailing comment that falls into the verbatim range + # trailing outer comment # fmt: on a + b diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/fmt_on_off/fmt_off_docstring.options.json b/crates/ruff_python_formatter/resources/test/fixtures/ruff/fmt_on_off/fmt_off_docstring.options.json new file mode 100644 index 0000000000..e662f72879 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/fmt_on_off/fmt_off_docstring.options.json @@ -0,0 +1,8 @@ +[ + { + "indent_style": { "Space": 4 } + }, + { + "indent_style": { "Space": 2 } + } +] diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/fmt_on_off/indent.options.json b/crates/ruff_python_formatter/resources/test/fixtures/ruff/fmt_on_off/indent.options.json new file mode 100644 index 0000000000..8f229d10a5 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/fmt_on_off/indent.options.json @@ -0,0 +1,11 @@ +[ + { + "indent_style": { "Space": 4 } + }, + { + "indent_style": { "Space": 1 } + }, + { + "indent_style": "Tab" + } +] diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/fmt_on_off/indent.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/fmt_on_off/indent.py new file mode 100644 index 0000000000..c0cb6c1849 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/fmt_on_off/indent.py @@ -0,0 +1,55 @@ +def test(): + # fmt: off + a_very_small_indent + ( +not_fixed + ) + + if True: + pass + more + # fmt: on + + formatted + + def test(): + a_small_indent + # fmt: off +# fix under-indented comments + (or_the_inner_expression + +expressions + ) + + if True: + pass + # fmt: on + + +# fmt: off +def test(): + pass + + # It is necessary to indent comments because the following fmt: on comment because it otherwise becomes a trailing comment + # of the `test` function if the "proper" indentation is larger than 2 spaces. + # fmt: on + +disabled + formatting; + +# fmt: on + +formatted; + +def test(): + pass + # fmt: off + """A multiline strings + that should not get formatted""" + + "A single quoted multiline \ + string" + + disabled + formatting; + +# fmt: on + +formatted; diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/fmt_on_off/mixed_space_and_tab.options.json b/crates/ruff_python_formatter/resources/test/fixtures/ruff/fmt_on_off/mixed_space_and_tab.options.json new file mode 100644 index 0000000000..e40788162c --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/fmt_on_off/mixed_space_and_tab.options.json @@ -0,0 +1,11 @@ +[ + { + "indent_style": { "Space": 4 } + }, + { + "indent_style": { "Space": 2 } + }, + { + "indent_style": "Tab" + } +] diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/fmt_on_off/mixed_space_and_tab.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/fmt_on_off/mixed_space_and_tab.py new file mode 100644 index 0000000000..0cf93824c1 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/fmt_on_off/mixed_space_and_tab.py @@ -0,0 +1,15 @@ +def test(): + # fmt: off + a_very_small_indent + ( +not_fixed + ) + + if True: +# Fun tab, space, tab, space. Followed by space, tab, tab, space + pass + more + else: + other + # fmt: on + diff --git a/crates/ruff_python_formatter/src/lib.rs b/crates/ruff_python_formatter/src/lib.rs index 2a704ae242..6c124f7d90 100644 --- a/crates/ruff_python_formatter/src/lib.rs +++ b/crates/ruff_python_formatter/src/lib.rs @@ -255,31 +255,16 @@ if True: #[ignore] #[test] fn quick_test() { - let src = r#" -with ( - [ - "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "bbbbbbbbbb", - "cccccccccccccccccccccccccccccccccccccccccc", - dddddddddddddddddddddddddddddddd, - ] as example1, - aaaaaaaaaaaaaaaaaaaaaaaaaa - + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb - + cccccccccccccccccccccccccccc - + ddddddddddddddddd as example2, - CtxManager2() as example2, - CtxManager2() as example2, - CtxManager2() as example2, -): - ... + let src = r#"def test(): + # fmt: off -with [ - "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "bbbbbbbbbb", - "cccccccccccccccccccccccccccccccccccccccccc", - dddddddddddddddddddddddddddddddd, -] as example1, aaaaaaaaaaaaaaaaaaaaaaaaaa * bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb * cccccccccccccccccccccccccccc + ddddddddddddddddd as example2, CtxManager222222222222222() as example2: - ... + a + b + + + + # suppressed comments + +a + b # formatted "#; // Tokenize once @@ -304,9 +289,9 @@ with [ // Use `dbg_write!(f, []) instead of `write!(f, [])` in your formatting code to print some IR // inside of a `Format` implementation // use ruff_formatter::FormatContext; - // dbg!(formatted + // formatted // .document() - // .display(formatted.context().source_code())); + // .display(formatted.context().source_code()); // // dbg!(formatted // .context() diff --git a/crates/ruff_python_formatter/src/verbatim.rs b/crates/ruff_python_formatter/src/verbatim.rs index c96a35c59c..825ff63aca 100644 --- a/crates/ruff_python_formatter/src/verbatim.rs +++ b/crates/ruff_python_formatter/src/verbatim.rs @@ -1,11 +1,16 @@ use std::borrow::Cow; use std::iter::FusedIterator; -use ruff_formatter::write; +use unicode_width::UnicodeWidthStr; + +use ruff_formatter::{write, FormatError}; use ruff_python_ast::node::AnyNodeRef; use ruff_python_ast::{Ranged, Stmt}; +use ruff_python_parser::lexer::{lex_starts_at, LexResult}; +use ruff_python_parser::{Mode, Tok}; use ruff_python_trivia::lines_before; -use ruff_text_size::TextRange; +use ruff_source_file::Locator; +use ruff_text_size::{TextRange, TextSize}; use crate::comments::format::{empty_lines, format_comment}; use crate::comments::{leading_comments, trailing_comments, SourceComment}; @@ -80,6 +85,7 @@ pub(crate) fn write_suppressed_statements_starting_with_trailing_comment<'a>( ) -> FormatResult<&'a Stmt> { let comments = f.context().comments().clone(); let source = f.context().source(); + let indentation = Indentation::from_stmt(last_formatted.statement(), source); let trailing_node_comments = comments.trailing_comments(last_formatted); let mut trailing_comment_ranges = @@ -131,10 +137,13 @@ pub(crate) fn write_suppressed_statements_starting_with_trailing_comment<'a>( write!( f, [ - verbatim_text(TextRange::new( - format_off_comment.end(), - format_on_comment.start(), - )), + FormatVerbatimStatementRange { + verbatim_range: TextRange::new( + format_off_comment.end(), + format_on_comment.start(), + ), + indentation + }, trailing_comments(std::slice::from_ref(format_on_comment)), trailing_comments(formatted_comments), ] @@ -163,7 +172,7 @@ pub(crate) fn write_suppressed_statements_starting_with_trailing_comment<'a>( // All comments in this range are suppressed SuppressionComments::Suppressed { comments: _ } => {} - // SAFETY: Unreachable because the function returns as soon as we reach the end of the suppressed range + // SAFETY: Unreachable because the function returns as soon as it reaches the end of the suppressed range SuppressionComments::SuppressionStarts { .. } | SuppressionComments::Formatted { .. } => unreachable!(), } @@ -195,7 +204,11 @@ pub(crate) fn write_suppressed_statements_starting_with_trailing_comment<'a>( // # a trailing comment // ``` else if let Some(last_comment) = trailing_node_comments.last() { - verbatim_text(TextRange::new(format_off_comment.end(), last_comment.end())).fmt(f)?; + FormatVerbatimStatementRange { + verbatim_range: TextRange::new(format_off_comment.end(), last_comment.end()), + indentation, + } + .fmt(f)?; Ok(last_formatted.statement()) } // The suppression comment is the very last code in the block. There's nothing more to format. @@ -226,10 +239,10 @@ fn write_suppressed_statements<'a>( let comments = f.context().comments().clone(); let source = f.context().source(); - // TODO(micha) Fixup indent let mut statement = first_suppressed; let mut leading_node_comments = first_suppressed_leading_comments; let mut format_off_comment = format_off_comment; + let indentation = Indentation::from_stmt(first_suppressed.statement(), source); loop { for range in CommentRangeIter::in_suppression(leading_node_comments, source) { @@ -266,10 +279,13 @@ fn write_suppressed_statements<'a>( write!( f, [ - verbatim_text(TextRange::new( - format_off_comment.end(), - format_on_comment.start(), - )), + FormatVerbatimStatementRange { + verbatim_range: TextRange::new( + format_off_comment.end(), + format_on_comment.start(), + ), + indentation + }, leading_comments(std::slice::from_ref(format_on_comment)), leading_comments(formatted_comments), ] @@ -343,10 +359,13 @@ fn write_suppressed_statements<'a>( write!( f, [ - verbatim_text(TextRange::new( - format_off_comment.end(), - format_on_comment.start() - )), + FormatVerbatimStatementRange { + verbatim_range: TextRange::new( + format_off_comment.end(), + format_on_comment.start() + ), + indentation + }, format_comment(format_on_comment), hard_line_break(), trailing_comments(formatted_comments), @@ -380,7 +399,11 @@ fn write_suppressed_statements<'a>( .last() .map_or(statement.end(), Ranged::end); - verbatim_text(TextRange::new(format_off_comment.end(), end)).fmt(f)?; + FormatVerbatimStatementRange { + verbatim_range: TextRange::new(format_off_comment.end(), end), + indentation, + } + .fmt(f)?; return Ok(statement.statement()); } @@ -573,33 +596,283 @@ impl Format> for TrailingFormatOffComment<'_> { } } -struct VerbatimText(TextRange); +/// Stores the indentation of a statement by storing the number of indentation characters. +/// Storing the number of indentation characters is sufficient because: +/// * Two indentations are equal if they result in the same column, regardless of the used tab size. +/// This implementation makes use of this fact and assumes a tab size of 1. +/// * The source document is correctly indented because it is valid Python code (or the formatter would have failed parsing the code). +#[derive(Copy, Clone)] +struct Indentation(u32); -fn verbatim_text(item: T) -> VerbatimText +impl Indentation { + fn from_stmt(stmt: &Stmt, source: &str) -> Indentation { + let line_start = Locator::new(source).line_start(stmt.start()); + + let mut indentation = 0u32; + for c in source[TextRange::new(line_start, stmt.start())].chars() { + if is_indent_whitespace(c) { + indentation += 1; + } else { + break; + } + } + + Indentation(indentation) + } + + fn trim_indent(self, ranged: impl Ranged, source: &str) -> TextRange { + let range = ranged.range(); + let mut start_offset = TextSize::default(); + + for c in source[range].chars().take(self.0 as usize) { + if is_indent_whitespace(c) { + start_offset += TextSize::new(1); + } else { + break; + } + } + + TextRange::new(range.start() + start_offset, range.end()) + } +} + +/// Returns `true` for a space or tab character. +/// +/// This is different than [`is_python_whitespace`] in that it returns `false` for a form feed character. +/// Form feed characters are excluded because they should be preserved in the suppressed output. +const fn is_indent_whitespace(c: char) -> bool { + matches!(c, ' ' | '\t') +} + +/// Formats a verbatim range where the top-level nodes are statements (or statement-level comments). +/// +/// Formats each statement as written in the source code, but adds the right indentation to match +/// the indentation of formatted statements: +/// +/// ```python +/// def test(): +/// print("formatted") +/// # fmt: off +/// ( +/// not_formatted + b +/// ) +/// # fmt: on +/// ``` +/// +/// Gets formatted as +/// +/// ```python +/// def test(): +/// print("formatted") +/// # fmt: off +/// ( +/// not_formatted + b +/// ) +/// # fmt: on +/// ``` +/// +/// Notice how the `not_formatted + b` expression statement gets the same indentation as the `print` statement above, +/// but the indentation of the expression remains unchanged. It changes the indentation to: +/// * Prevent syntax errors because of different indentation levels between formatted and suppressed statements. +/// * Align with the `fmt: skip` where statements are indented as well, but inner expressions are formatted as is. +struct FormatVerbatimStatementRange { + verbatim_range: TextRange, + indentation: Indentation, +} + +impl Format> for FormatVerbatimStatementRange { + fn fmt(&self, f: &mut Formatter>) -> FormatResult<()> { + let lexer = lex_starts_at( + &f.context().source()[self.verbatim_range], + Mode::Module, + self.verbatim_range.start(), + ); + + let logical_lines = LogicalLinesIter::new(lexer, self.verbatim_range); + let mut first = true; + + for logical_line in logical_lines { + let logical_line = logical_line?; + + let trimmed_line_range = self + .indentation + .trim_indent(&logical_line, f.context().source()); + + // A line without any content, write an empty line, except for the first or last (indent only) line. + if trimmed_line_range.is_empty() { + if logical_line.has_trailing_newline { + if first { + hard_line_break().fmt(f)?; + } else { + empty_line().fmt(f)?; + } + } + } else { + // Non empty line, write the text of the line + verbatim_text(trimmed_line_range, logical_line.contains_newlines).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 { + // Insert an empty line if the text is non-empty but all characters have a width of zero. + // This is necessary to work around the fact that the Printer omits hard line breaks if the line width is 0. + // The alternative is to "fix" the printer and explicitly track the width and whether the line is empty. + // There's currently no use case for zero-width content outside of the verbatim context (and, form feeds are a Python specific speciality). + // It, therefore, feels wrong to add additional complexity to the very hot `Printer::print_char` function, + // to work around this special case. Therefore, work around the Printer behavior here, in the cold verbatim-formatting. + if f.context().source()[trimmed_line_range].width() == 0 { + empty_line().fmt(f)?; + } else { + hard_line_break().fmt(f)?; + } + } + } + + first = false; + } + + Ok(()) + } +} + +struct LogicalLinesIter { + lexer: I, + // The end of the last logical line + last_line_end: TextSize, + // The position where the content to lex ends. + content_end: TextSize, +} + +impl LogicalLinesIter { + fn new(lexer: I, verbatim_range: TextRange) -> Self { + Self { + lexer, + last_line_end: verbatim_range.start(), + content_end: verbatim_range.end(), + } + } +} + +impl Iterator for LogicalLinesIter +where + I: Iterator, +{ + type Item = FormatResult; + + 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() { + Some(Ok((token, range))) => match token { + 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 => { + // Returns any content that comes after the last newline. This is mainly whitespace + // or characters that the `Lexer` skips, like a form-feed character. + return if self.last_line_end < self.content_end { + let content_start = self.last_line_end; + 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 { + None + }; + } + Some(Err(_)) => { + return Some(Err(FormatError::syntax_error( + "Unexpected token when lexing verbatim statement range.", + ))) + } + } + }; + + let line_start = self.last_line_end; + self.last_line_end = full_end; + + Some(Ok(LogicalLine { + content_range: TextRange::new(line_start, content_end), + contains_newlines, + has_trailing_newline: true, + })) + } +} + +impl FusedIterator for LogicalLinesIter where I: Iterator {} + +/// A logical line or a comment (or form feed only) line +struct LogicalLine { + /// The range of this lines content (excluding the trailing newline) + content_range: TextRange, + /// Whether the content in `content_range` contains any newlines. + contains_newlines: ContainsNewlines, + /// Does this logical line have a trailing newline or does it just happen to be the last line. + has_trailing_newline: bool, +} + +impl Ranged for LogicalLine { + fn range(&self) -> TextRange { + self.content_range + } +} + +struct VerbatimText { + verbatim_range: TextRange, + contains_newlines: ContainsNewlines, +} + +fn verbatim_text(item: T, contains_newlines: ContainsNewlines) -> VerbatimText where T: Ranged, { - VerbatimText(item.range()) + VerbatimText { + verbatim_range: item.range(), + contains_newlines, + } } impl Format> for VerbatimText { fn fmt(&self, f: &mut PyFormatter) -> FormatResult<()> { f.write_element(FormatElement::Tag(Tag::StartVerbatim( tag::VerbatimKind::Verbatim { - length: self.0.len(), + length: self.verbatim_range.len(), }, )))?; - match normalize_newlines(f.context().locator().slice(self.0), ['\r']) { + match normalize_newlines(f.context().locator().slice(self.verbatim_range), ['\r']) { Cow::Borrowed(_) => { - write!(f, [source_text_slice(self.0, ContainsNewlines::Detect)])?; + write!( + f, + [source_text_slice( + self.verbatim_range, + self.contains_newlines + )] + )?; } Cow::Owned(cleaned) => { write!( f, [ - dynamic_text(&cleaned, Some(self.0.start())), - source_position(self.0.end()) + dynamic_text(&cleaned, Some(self.verbatim_range.start())), + source_position(self.verbatim_range.end()) ] )?; } diff --git a/crates/ruff_python_formatter/tests/snapshots/format@fmt_on_off__comments.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@fmt_on_off__comments.py.snap index f961fc36e8..ad353b6d2f 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@fmt_on_off__comments.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@fmt_on_off__comments.py.snap @@ -16,8 +16,13 @@ a + b # a trailing comment def test(): pass - # trailing comment that falls into the verbatim range + # under indent + def nested(): + ... + + # trailing comment that falls into the verbatim range + # trailing outer comment # fmt: on a + b @@ -43,8 +48,13 @@ a + b # a trailing comment def test(): pass - # trailing comment that falls into the verbatim range + # under indent + def nested(): + ... + + # trailing comment that falls into the verbatim range + # trailing outer comment # fmt: on a + b diff --git a/crates/ruff_python_formatter/tests/snapshots/format@fmt_on_off__fmt_off_docstring.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@fmt_on_off__fmt_off_docstring.py.snap index 396dd028e3..dd1d9c6714 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@fmt_on_off__fmt_off_docstring.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@fmt_on_off__fmt_off_docstring.py.snap @@ -25,7 +25,15 @@ def test(): and_this + gets + formatted + too ``` -## Output +## Outputs +### Output 1 +``` +indent-style = Spaces, size: 4 +line-width = 88 +quote-style = Double +magic-trailing-comma = Respect +``` + ```py def test(): # fmt: off @@ -49,4 +57,35 @@ def test(): ``` +### Output 2 +``` +indent-style = Spaces, size: 2 +line-width = 88 +quote-style = Double +magic-trailing-comma = Respect +``` + +```py +def test(): + # fmt: off + """ This docstring does not + get formatted + """ + + # fmt: on + + but + this + does + + +def test(): + # fmt: off + # just for fun + # fmt: on + # leading comment + """This docstring gets formatted""" # trailing comment + + and_this + gets + formatted + too +``` + + diff --git a/crates/ruff_python_formatter/tests/snapshots/format@fmt_on_off__indent.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@fmt_on_off__indent.py.snap new file mode 100644 index 0000000000..37ea15222f --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/format@fmt_on_off__indent.py.snap @@ -0,0 +1,272 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/fmt_on_off/indent.py +--- +## Input +```py +def test(): + # fmt: off + a_very_small_indent + ( +not_fixed + ) + + if True: + pass + more + # fmt: on + + formatted + + def test(): + a_small_indent + # fmt: off +# fix under-indented comments + (or_the_inner_expression + +expressions + ) + + if True: + pass + # fmt: on + + +# fmt: off +def test(): + pass + + # It is necessary to indent comments because the following fmt: on comment because it otherwise becomes a trailing comment + # of the `test` function if the "proper" indentation is larger than 2 spaces. + # fmt: on + +disabled + formatting; + +# fmt: on + +formatted; + +def test(): + pass + # fmt: off + """A multiline strings + that should not get formatted""" + + "A single quoted multiline \ + string" + + disabled + formatting; + +# fmt: on + +formatted; +``` + +## Outputs +### Output 1 +``` +indent-style = Spaces, size: 4 +line-width = 88 +quote-style = Double +magic-trailing-comma = Respect +``` + +```py +def test(): + # fmt: off + a_very_small_indent + ( +not_fixed + ) + + if True: + pass + more + # fmt: on + + formatted + + def test(): + a_small_indent + # fmt: off + # fix under-indented comments + (or_the_inner_expression + +expressions + ) + + if True: + pass + # fmt: on + + +# fmt: off +def test(): + pass + + # It is necessary to indent comments because the following fmt: on comment because it otherwise becomes a trailing comment + # of the `test` function if the "proper" indentation is larger than 2 spaces. + # fmt: on + +disabled + formatting; + +# fmt: on + +formatted + + +def test(): + pass + # fmt: off + """A multiline strings + that should not get formatted""" + + "A single quoted multiline \ + string" + + disabled + formatting + + +# fmt: on + +formatted +``` + + +### Output 2 +``` +indent-style = Spaces, size: 1 +line-width = 88 +quote-style = Double +magic-trailing-comma = Respect +``` + +```py +def test(): + # fmt: off + a_very_small_indent + ( +not_fixed + ) + + if True: + pass + more + # fmt: on + + formatted + + def test(): + a_small_indent + # fmt: off + # fix under-indented comments + (or_the_inner_expression + +expressions + ) + + if True: + pass + # fmt: on + + +# fmt: off +def test(): + pass + + # It is necessary to indent comments because the following fmt: on comment because it otherwise becomes a trailing comment + # of the `test` function if the "proper" indentation is larger than 2 spaces. + # fmt: on + +disabled + formatting; + +# fmt: on + +formatted + + +def test(): + pass + # fmt: off + """A multiline strings + that should not get formatted""" + + "A single quoted multiline \ + string" + + disabled + formatting + + +# fmt: on + +formatted +``` + + +### Output 3 +``` +indent-style = Tab +line-width = 88 +quote-style = Double +magic-trailing-comma = Respect +``` + +```py +def test(): + # fmt: off + a_very_small_indent + ( +not_fixed + ) + + if True: + pass + more + # fmt: on + + formatted + + def test(): + a_small_indent + # fmt: off + # fix under-indented comments + (or_the_inner_expression + +expressions + ) + + if True: + pass + # fmt: on + + +# fmt: off +def test(): + pass + + # It is necessary to indent comments because the following fmt: on comment because it otherwise becomes a trailing comment + # of the `test` function if the "proper" indentation is larger than 2 spaces. + # fmt: on + +disabled + formatting; + +# fmt: on + +formatted + + +def test(): + pass + # fmt: off + """A multiline strings + that should not get formatted""" + + "A single quoted multiline \ + string" + + disabled + formatting + + +# fmt: on + +formatted +``` + + + diff --git a/crates/ruff_python_formatter/tests/snapshots/format@fmt_on_off__mixed_space_and_tab.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@fmt_on_off__mixed_space_and_tab.py.snap new file mode 100644 index 0000000000..1027b24e41 --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/format@fmt_on_off__mixed_space_and_tab.py.snap @@ -0,0 +1,103 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/fmt_on_off/mixed_space_and_tab.py +--- +## Input +```py +def test(): + # fmt: off + a_very_small_indent + ( +not_fixed + ) + + if True: +# Fun tab, space, tab, space. Followed by space, tab, tab, space + pass + more + else: + other + # fmt: on + +``` + +## Outputs +### Output 1 +``` +indent-style = Spaces, size: 4 +line-width = 88 +quote-style = Double +magic-trailing-comma = Respect +``` + +```py +def test(): + # fmt: off + a_very_small_indent + ( +not_fixed + ) + + if True: + # Fun tab, space, tab, space. Followed by space, tab, tab, space + pass + more + else: + other +# fmt: on +``` + + +### Output 2 +``` +indent-style = Spaces, size: 2 +line-width = 88 +quote-style = Double +magic-trailing-comma = Respect +``` + +```py +def test(): + # fmt: off + a_very_small_indent + ( +not_fixed + ) + + if True: + # Fun tab, space, tab, space. Followed by space, tab, tab, space + pass + more + else: + other +# fmt: on +``` + + +### Output 3 +``` +indent-style = Tab +line-width = 88 +quote-style = Double +magic-trailing-comma = Respect +``` + +```py +def test(): + # fmt: off + a_very_small_indent + ( +not_fixed + ) + + if True: + # Fun tab, space, tab, space. Followed by space, tab, tab, space + pass + more + else: + other +# fmt: on +``` + + +