diff --git a/Sources/SwiftFormat/Core/Trivia+Convenience.swift b/Sources/SwiftFormat/Core/Trivia+Convenience.swift index c1906ab6c..138204231 100644 --- a/Sources/SwiftFormat/Core/Trivia+Convenience.swift +++ b/Sources/SwiftFormat/Core/Trivia+Convenience.swift @@ -80,13 +80,19 @@ extension Trivia { }) } - /// Returns `true` if this trivia contains any backslashes (used for multiline string newline - /// suppression). - var containsBackslashes: Bool { - return contains( - where: { - if case .backslashes = $0 { return true } - return false - }) + /// Returns the prefix of this trivia that corresponds to the backslash and pound signs used to + /// represent a non-line-break continuation of a multiline string, or nil if the trivia does not + /// represent such a continuation. + var multilineStringContinuation: String? { + var result = "" + for piece in pieces { + switch piece { + case .backslashes, .pounds: + piece.write(to: &result) + default: + break + } + } + return result.isEmpty ? nil : result } } diff --git a/Sources/SwiftFormat/PrettyPrint/TokenStreamCreator.swift b/Sources/SwiftFormat/PrettyPrint/TokenStreamCreator.swift index 6b9e74874..d5be22648 100644 --- a/Sources/SwiftFormat/PrettyPrint/TokenStreamCreator.swift +++ b/Sources/SwiftFormat/PrettyPrint/TokenStreamCreator.swift @@ -2637,11 +2637,13 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { emitSegmentTextTokens(segmentText[...]) } - if node.trailingTrivia.containsBackslashes && !config.reflowMultilineStringLiterals.isAlways { - // Segments with trailing backslashes won't end with a literal newline; the backslash is - // considered trivia. To preserve the original text and wrapping, we need to manually render - // the backslash and a break into the token stream. - appendToken(.syntax("\\")) + if !config.reflowMultilineStringLiterals.isAlways, + let continuation = node.trailingTrivia.multilineStringContinuation + { + // Segments with trailing backslashes won't end with a literal newline; the backslash and any + // `#` delimiters for raw strings are considered trivia. To preserve the original text and + // wrapping, we need to manually render them break into the token stream. + appendToken(.syntax(continuation)) appendToken(.break(breakKind, newlines: .hard(count: 1))) } return .skipChildren diff --git a/Tests/SwiftFormatTests/PrettyPrint/StringTests.swift b/Tests/SwiftFormatTests/PrettyPrint/StringTests.swift index 2a785bdb9..5b5172a28 100644 --- a/Tests/SwiftFormatTests/PrettyPrint/StringTests.swift +++ b/Tests/SwiftFormatTests/PrettyPrint/StringTests.swift @@ -739,4 +739,41 @@ final class StringTests: PrettyPrintTestCase { assertPrettyPrintEqual(input: input, expected: input + "\n", linelength: 20) } + + func testMultilineStringWithContinuations() { + let input = + ##""" + let someString = + """ + lines \ + \nare \ + short. + """ + let someString = + #""" + lines \# + \#nare \# + short. + """# + """## + + let expected = + ##""" + let someString = + """ + lines \ + \nare \ + short. + """ + let someString = + #""" + lines \# + \#nare \# + short. + """# + + """## + + assertPrettyPrintEqual(input: input, expected: expected, linelength: 30) + } }