Skip to content

Commit 0a86ffe

Browse files
committed
First kind of working solution
1 parent 22cb7d4 commit 0a86ffe

File tree

9 files changed

+217
-88
lines changed

9 files changed

+217
-88
lines changed

crates/ruff_python_formatter/src/expression/expr_string_literal.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ impl NeedsParentheses for ExprStringLiteral {
4646
_parent: AnyNodeRef,
4747
context: &PyFormatContext,
4848
) -> OptionalParentheses {
49+
// TODO: This is complicated. We should only return multiline if we *know* that
50+
// it won't fit because of how assignment works.
4951
if self.value.is_implicit_concatenated() {
5052
OptionalParentheses::Multiline
5153
} else if AnyString::String(self).is_multiline(context.source()) {

crates/ruff_python_formatter/src/lib.rs

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,9 @@ mod tests {
160160
use ruff_python_trivia::CommentRanges;
161161
use ruff_text_size::{TextRange, TextSize};
162162

163-
use crate::{format_module_ast, format_module_source, format_range, PyFormatOptions};
163+
use crate::{
164+
format_module_ast, format_module_source, format_range, PreviewMode, PyFormatOptions,
165+
};
164166

165167
/// Very basic test intentionally kept very similar to the CLI
166168
#[test]
@@ -188,13 +190,10 @@ if True:
188190
#[test]
189191
fn quick_test() {
190192
let source = r#"
191-
def main() -> None:
192-
if True:
193-
some_very_long_variable_name_abcdefghijk = Foo()
194-
some_very_long_variable_name_abcdefghijk = some_very_long_variable_name_abcdefghijk[
195-
some_very_long_variable_name_abcdefghijk.some_very_long_attribute_name
196-
== "This is a very long string abcdefghijk"
197-
]
193+
(
194+
f'{one}'
195+
f'{two}'
196+
)
198197
199198
"#;
200199
let source_type = PySourceType::Python;
@@ -203,7 +202,8 @@ def main() -> None:
203202
let source_path = "code_inline.py";
204203
let parsed = parse(source, source_type.as_mode()).unwrap();
205204
let comment_ranges = CommentRanges::from(parsed.tokens());
206-
let options = PyFormatOptions::from_extension(Path::new(source_path));
205+
let options = PyFormatOptions::from_extension(Path::new(source_path))
206+
.with_preview(PreviewMode::Enabled);
207207
let formatted = format_module_ast(&parsed, &comment_ranges, source, options).unwrap();
208208

209209
// Uncomment the `dbg` to print the IR.

crates/ruff_python_formatter/src/other/f_string.rs

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -76,14 +76,9 @@ impl Format<PyFormatContext<'_>> for FormatFString<'_> {
7676
let quotes = StringQuotes::from(string_kind);
7777
write!(f, [string_kind.prefix(), quotes])?;
7878

79-
f.join()
80-
.entries(
81-
self.value
82-
.elements
83-
.iter()
84-
.map(|element| FormatFStringElement::new(element, context)),
85-
)
86-
.finish()?;
79+
for element in &self.value.elements {
80+
FormatFStringElement::new(element, context).fmt(f)?;
81+
}
8782

8883
// Ending quote
8984
quotes.fmt(f)

crates/ruff_python_formatter/src/other/f_string_element.rs

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -231,11 +231,9 @@ impl Format<PyFormatContext<'_>> for FormatFStringExpressionElement<'_> {
231231
if let Some(format_spec) = format_spec.as_deref() {
232232
token(":").fmt(f)?;
233233

234-
f.join()
235-
.entries(format_spec.elements.iter().map(|element| {
236-
FormatFStringElement::new(element, self.context.f_string())
237-
}))
238-
.finish()?;
234+
for element in &format_spec.elements {
235+
FormatFStringElement::new(element, self.context.f_string()).fmt(f)?;
236+
}
239237

240238
// These trailing comments can only occur if the format specifier is
241239
// present. For example,

crates/ruff_python_formatter/src/string/any.rs

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use memchr::memchr2;
44

55
use ruff_python_ast::{
66
self as ast, AnyNodeRef, AnyStringFlags, Expr, ExprBytesLiteral, ExprFString,
7-
ExprStringLiteral, ExpressionRef, StringFlags, StringLiteral,
7+
ExprStringLiteral, ExpressionRef, FStringElement, FStringPart, StringFlags, StringLiteral,
88
};
99
use ruff_source_file::Locator;
1010
use ruff_text_size::{Ranged, TextRange};
@@ -68,12 +68,11 @@ impl<'a> AnyString<'a> {
6868

6969
pub(crate) fn is_multiline(self, source: &str) -> bool {
7070
match self {
71-
AnyString::String(_) | AnyString::Bytes(_) => {
72-
self.parts()
73-
.next()
74-
.is_some_and(|part| part.flags().is_triple_quoted())
75-
&& memchr2(b'\n', b'\r', source[self.range()].as_bytes()).is_some()
76-
}
71+
AnyString::String(_) | AnyString::Bytes(_) => self.parts().any(|part| {
72+
part.flags().is_triple_quoted()
73+
&& memchr2(b'\n', b'\r', source[part.range()].as_bytes()).is_some()
74+
}),
75+
// TODO: Should this only test string literals and expressions (if they contain any newline)?
7776
AnyString::FString(fstring) => {
7877
memchr2(b'\n', b'\r', source[fstring.range].as_bytes()).is_some()
7978
}

crates/ruff_python_formatter/src/string/mod.rs

Lines changed: 56 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,19 @@
11
pub(crate) use any::AnyString;
22
pub(crate) use normalize::{normalize_string, NormalizedString, StringNormalizer};
3-
use ruff_formatter::format_args;
3+
use ruff_formatter::{format_args, write};
44
use ruff_python_ast::str::Quote;
55
use ruff_python_ast::{
66
str_prefix::{AnyStringPrefix, StringLiteralPrefix},
77
AnyStringFlags, StringFlags,
88
};
9-
use ruff_text_size::Ranged;
9+
use std::borrow::Cow;
1010

1111
use crate::comments::{leading_comments, trailing_comments};
1212
use crate::expression::parentheses::in_parentheses_only_soft_line_break_or_space;
1313
use crate::other::f_string::FormatFString;
1414
use crate::other::string_literal::StringLiteralKind;
1515
use crate::prelude::*;
16+
use crate::preview::is_f_string_formatting_enabled;
1617
use crate::string::any::AnyStringPart;
1718
use crate::string::normalize::QuoteMetadata;
1819
use crate::QuoteStyle;
@@ -69,7 +70,7 @@ impl<'a> FormatImplicitConcatenatedString<'a> {
6970

7071
// Don't merge multiline strings because that's pointless, a multiline string can
7172
// never fit on a single line.
72-
if self.string.is_multiline(context.source()) {
73+
if !self.string.is_fstring() && self.string.is_multiline(context.source()) {
7374
return None;
7475
}
7576

@@ -81,14 +82,17 @@ impl<'a> FormatImplicitConcatenatedString<'a> {
8182

8283
// TODO unify quote styles.
8384
// Possibly run directly on entire string?
85+
let first_part = self.string.parts().next()?;
8486

85-
for part in self.string.parts() {
86-
let Ok(preferred_quote) = Quote::try_from(normalizer.preferred_quote_style(part))
87-
else {
88-
// TOOD: Handle preserve in some way or another
89-
return None;
90-
};
87+
// Only determining the preferred quote for the first string is sufficient
88+
// because we don't support joining triple quoted strings with non triple quoted strings.
89+
let Ok(preferred_quote) = Quote::try_from(normalizer.preferred_quote_style(first_part))
90+
else {
91+
// TODO: Handle preserve
92+
return None;
93+
};
9194

95+
for part in self.string.parts() {
9296
// Again, this takes a StringPart and not a `AnyStringPart`.
9397
let part_quote_metadata = QuoteMetadata::from_part(part, preferred_quote, context);
9498

@@ -100,7 +104,7 @@ impl<'a> FormatImplicitConcatenatedString<'a> {
100104
}
101105
}
102106

103-
Some(merged_quotes?.choose(Quote::Double))
107+
Some(merged_quotes?.choose(preferred_quote))
104108
}
105109
}
106110

@@ -109,12 +113,6 @@ impl Format<PyFormatContext<'_>> for FormatImplicitConcatenatedString<'_> {
109113
let comments = f.context().comments().clone();
110114
let quoting = self.string.quoting(f.context().locator());
111115

112-
// let cant_collapse = self.string.is_multiline(f.context().source())
113-
// || self.string.parts().any(|part| {
114-
// let part_comments = comments.leading_dangling_trailing(&part);
115-
// part_comments.has_leading() || part_comments.has_trailing()
116-
// });
117-
118116
let format_expanded = format_with(|f| {
119117
let mut joiner = f.join_with(in_parentheses_only_soft_line_break_or_space());
120118
for part in self.string.parts() {
@@ -145,7 +143,48 @@ impl Format<PyFormatContext<'_>> for FormatImplicitConcatenatedString<'_> {
145143
joiner.finish()
146144
});
147145

148-
format_expanded.fmt(f)
146+
if let Some(collapsed_quotes) = dbg!(self.merged_prefix(f.context())) {
147+
let format_flat = format_with(|f| {
148+
let mut parts = self.string.parts();
149+
150+
let Some(first_part) = parts.next() else {
151+
return Ok(());
152+
};
153+
154+
// TODO handle different prefixes, e.g. f-string and non fstrings. We should probably handle this
155+
// inside of `merged_prefix`
156+
let flags = first_part.flags().with_quote_style(collapsed_quotes);
157+
let quotes = StringQuotes::from(flags);
158+
159+
write!(f, [flags.prefix(), quotes])?;
160+
161+
for part in self.string.parts() {
162+
let content = f.context().locator().slice(part.content_range());
163+
let normalized = normalize_string(
164+
content,
165+
0,
166+
flags,
167+
is_f_string_formatting_enabled(f.context()),
168+
);
169+
match normalized {
170+
Cow::Borrowed(_) => source_text_slice(part.content_range()).fmt(f)?,
171+
Cow::Owned(normalized) => text(&normalized).fmt(f)?,
172+
}
173+
}
174+
175+
quotes.fmt(f)
176+
});
177+
178+
write!(
179+
f,
180+
[
181+
if_group_fits_on_line(&format_flat),
182+
if_group_breaks(&format_expanded)
183+
]
184+
)
185+
} else {
186+
format_expanded.fmt(f)
187+
}
149188
}
150189
}
151190

0 commit comments

Comments
 (0)