11pub ( crate ) use any:: AnyString ;
22pub ( crate ) use normalize:: { normalize_string, NormalizedString , StringNormalizer } ;
3- use ruff_formatter:: format_args;
3+ use ruff_formatter:: { format_args, write } ;
44use ruff_python_ast:: str:: Quote ;
55use ruff_python_ast:: {
66 str_prefix:: { AnyStringPrefix , StringLiteralPrefix } ,
77 AnyStringFlags , StringFlags ,
88} ;
9- use ruff_text_size :: Ranged ;
9+ use std :: borrow :: Cow ;
1010
1111use crate :: comments:: { leading_comments, trailing_comments} ;
1212use crate :: expression:: parentheses:: in_parentheses_only_soft_line_break_or_space;
1313use crate :: other:: f_string:: FormatFString ;
1414use crate :: other:: string_literal:: StringLiteralKind ;
1515use crate :: prelude:: * ;
16+ use crate :: preview:: is_f_string_formatting_enabled;
1617use crate :: string:: any:: AnyStringPart ;
1718use crate :: string:: normalize:: QuoteMetadata ;
1819use 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