diff --git a/Sources/SwiftFormat/API/Configuration+Default.swift b/Sources/SwiftFormat/API/Configuration+Default.swift index ca57700bb..d18164f39 100644 --- a/Sources/SwiftFormat/API/Configuration+Default.swift +++ b/Sources/SwiftFormat/API/Configuration+Default.swift @@ -40,5 +40,6 @@ extension Configuration { self.spacesAroundRangeFormationOperators = false self.noAssignmentInExpressions = NoAssignmentInExpressionsConfiguration() self.multiElementCollectionTrailingCommas = true + self.reflowMultilineStringLiterals = .never } } diff --git a/Sources/SwiftFormat/API/Configuration.swift b/Sources/SwiftFormat/API/Configuration.swift index 19af3de57..c6836ab8a 100644 --- a/Sources/SwiftFormat/API/Configuration.swift +++ b/Sources/SwiftFormat/API/Configuration.swift @@ -45,6 +45,7 @@ public struct Configuration: Codable, Equatable { case spacesAroundRangeFormationOperators case noAssignmentInExpressions case multiElementCollectionTrailingCommas + case reflowMultilineStringLiterals } /// A dictionary containing the default enabled/disabled states of rules, keyed by the rules' @@ -194,6 +195,71 @@ public struct Configuration: Codable, Equatable { /// ``` public var multiElementCollectionTrailingCommas: Bool + /// Determines how multiline string literals should reflow when formatted. + public enum MultilineStringReflowBehavior: Codable { + /// Never reflow multiline string literals. + case never + /// Reflow lines in string literal that exceed the maximum line length. For example with a line length of 10: + /// ```swift + /// """ + /// an escape\ + /// line break + /// a hard line break + /// """ + /// ``` + /// will be formatted as: + /// ```swift + /// """ + /// an esacpe\ + /// line break + /// a hard \ + /// line break + /// """ + /// ``` + /// The existing `\` is left in place, but the line over line length is broken. + case onlyLinesOverLength + /// Always reflow multiline string literals, this will ignore existing escaped newlines in the literal and reflow each line. Hard linebreaks are still respected. + /// For example, with a line length of 10: + /// ```swift + /// """ + /// one \ + /// word \ + /// a line. + /// this is too long. + /// """ + /// ``` + /// will be formatted as: + /// ```swift + /// """ + /// one word \ + /// a line. + /// this is \ + /// too long. + /// """ + /// ``` + case always + + var isNever: Bool { + switch self { + case .never: + return true + default: + return false + } + } + + var isAlways: Bool { + switch self { + case .always: + return true + default: + return false + } + } + } + + public var reflowMultilineStringLiterals: MultilineStringReflowBehavior + /// Creates a new `Configuration` by loading it from a configuration file. public init(contentsOf url: URL) throws { let data = try Data(contentsOf: url) @@ -287,6 +353,10 @@ public struct Configuration: Codable, Equatable { Bool.self, forKey: .multiElementCollectionTrailingCommas) ?? defaults.multiElementCollectionTrailingCommas + self.reflowMultilineStringLiterals = + try container.decodeIfPresent(MultilineStringReflowBehavior.self, forKey: .reflowMultilineStringLiterals) + ?? defaults.reflowMultilineStringLiterals + // If the `rules` key is not present at all, default it to the built-in set // so that the behavior is the same as if the configuration had been // default-initialized. To get an empty rules dictionary, one can explicitly @@ -321,6 +391,7 @@ public struct Configuration: Codable, Equatable { try container.encode(indentSwitchCaseLabels, forKey: .indentSwitchCaseLabels) try container.encode(noAssignmentInExpressions, forKey: .noAssignmentInExpressions) try container.encode(multiElementCollectionTrailingCommas, forKey: .multiElementCollectionTrailingCommas) + try container.encode(reflowMultilineStringLiterals, forKey: .reflowMultilineStringLiterals) try container.encode(rules, forKey: .rules) } diff --git a/Sources/SwiftFormat/PrettyPrint/PrettyPrint.swift b/Sources/SwiftFormat/PrettyPrint/PrettyPrint.swift index ada574f1c..8ad1c04c9 100644 --- a/Sources/SwiftFormat/PrettyPrint/PrettyPrint.swift +++ b/Sources/SwiftFormat/PrettyPrint/PrettyPrint.swift @@ -214,7 +214,6 @@ public class PrettyPrinter { switch token { case .contextualBreakingStart: activeBreakingContexts.append(ActiveBreakingContext(lineNumber: outputBuffer.lineNumber)) - // Discard the last finished breaking context to keep it from effecting breaks inside of the // new context. The discarded context has already either had an impact on the contextual break // after it or there was no relevant contextual break, so it's safe to discard. @@ -414,7 +413,7 @@ public class PrettyPrinter { var overrideBreakingSuppressed = false switch newline { - case .elective: break + case .elective, .escaped: break case .soft(_, let discretionary): // A discretionary newline (i.e. from the source) should create a line break even if the // rules for breaking are disabled. @@ -429,6 +428,10 @@ public class PrettyPrinter { let suppressBreaking = isBreakingSuppressed && !overrideBreakingSuppressed if !suppressBreaking && (!canFit(length) || mustBreak) { currentLineIsContinuation = isContinuationIfBreakFires + if case .escaped = newline { + outputBuffer.enqueueSpaces(size) + outputBuffer.write("\\") + } outputBuffer.writeNewlines(newline) lastBreak = true } else { @@ -594,19 +597,51 @@ public class PrettyPrinter { // Break lengths are equal to its size plus the token or group following it. Calculate the // length of any prior break tokens. case .break(_, let size, let newline): - if let index = delimIndexStack.last, case .break = tokens[index] { - lengths[index] += total + if let index = delimIndexStack.last, case .break(_, _, let lastNewline) = tokens[index] { + /// If the last break and this break are both `.escaped` we add an extra 1 to the total for the last `.escaped` break. + /// This is to handle situations where adding the `\` for an escaped line break would put us over the line length. + /// For example, consider the token sequence: + /// `[.syntax("this fits"), .break(.escaped), .syntax("this fits in line length"), .break(.escaped)]` + /// The naive layout of these tokens will incorrectly print as: + /// """ + /// this fits this fits in line length \ + /// """ + /// which will be too long because of the '\' character. Instead we have to print it as: + /// """ + /// this fits \ + /// this fits in line length + /// """ + /// + /// While not prematurely inserting a line in situations where a hard line break is occurring, such as: + /// + /// `[.syntax("some text"), .break(.escaped), .syntax("this is exactly the right length"), .break(.hard)]` + /// + /// We want this to print as: + /// """ + /// some text this is exactly the right length + /// """ + /// and not: + /// """ + /// some text \ + /// this is exactly the right length + /// """ + if case .escaped = newline, case .escaped = lastNewline { + lengths[index] += total + 1 + } else { + lengths[index] += total + } delimIndexStack.removeLast() } lengths.append(-total) delimIndexStack.append(i) - if case .elective = newline { - total += size - } else { - // `size` is never used in this case, because the break always fires. Use `maxLineLength` - // to ensure enclosing groups are large enough to force preceding breaks to fire. - total += maxLineLength + switch newline { + case .elective, .escaped: + total += size + default: + // `size` is never used in this case, because the break always fires. Use `maxLineLength` + // to ensure enclosing groups are large enough to force preceding breaks to fire. + total += maxLineLength } // Space tokens have a length equal to its size. diff --git a/Sources/SwiftFormat/PrettyPrint/PrettyPrintBuffer.swift b/Sources/SwiftFormat/PrettyPrint/PrettyPrintBuffer.swift index 02a2a7ac6..6c3402a00 100644 --- a/Sources/SwiftFormat/PrettyPrint/PrettyPrintBuffer.swift +++ b/Sources/SwiftFormat/PrettyPrint/PrettyPrintBuffer.swift @@ -81,6 +81,8 @@ struct PrettyPrintBuffer { numberToPrint = min(count, maximumBlankLines + 1) - consecutiveNewlineCount case .hard(let count): numberToPrint = count + case .escaped: + numberToPrint = 1 } guard numberToPrint > 0 else { return } diff --git a/Sources/SwiftFormat/PrettyPrint/Token.swift b/Sources/SwiftFormat/PrettyPrint/Token.swift index 277317c62..654f09c54 100644 --- a/Sources/SwiftFormat/PrettyPrint/Token.swift +++ b/Sources/SwiftFormat/PrettyPrint/Token.swift @@ -147,6 +147,10 @@ enum NewlineBehavior { /// newlines and the configured maximum number of blank lines. case hard(count: Int) + /// Break onto a new line is allowed if neccessary. If a line break is emitted, it will be escaped with a '\', and this breaks whitespace will be printed prior to the + /// escaped line break. This is useful in multiline strings where we don't want newlines printed in syntax to appear in the literal. + case escaped + /// An elective newline that respects discretionary newlines from the user-entered text. static let elective = NewlineBehavior.elective(ignoresDiscretionary: false) diff --git a/Sources/SwiftFormat/PrettyPrint/TokenStreamCreator.swift b/Sources/SwiftFormat/PrettyPrint/TokenStreamCreator.swift index e88f1dccb..e7ae27201 100644 --- a/Sources/SwiftFormat/PrettyPrint/TokenStreamCreator.swift +++ b/Sources/SwiftFormat/PrettyPrint/TokenStreamCreator.swift @@ -15,7 +15,7 @@ import SwiftOperators import SwiftSyntax fileprivate extension AccessorBlockSyntax { - /// Assuming that the accessor only contains an implicit getter (i.e. no + /// Assuming that the accessor only contains an implicit getter (i.e. no /// `get` or `set`), return the code block items in that getter. var getterCodeBlockItems: CodeBlockItemListSyntax { guard case .getter(let codeBlockItemList) = self.accessors else { @@ -1437,9 +1437,9 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { override func visit(_ node: ReturnClauseSyntax) -> SyntaxVisitorContinueKind { if node.parent?.is(FunctionTypeSyntax.self) ?? false { - // `FunctionTypeSyntax` used to not use `ReturnClauseSyntax` and had - // slightly different formatting behavior than the normal - // `ReturnClauseSyntax`. To maintain the previous formatting behavior, + // `FunctionTypeSyntax` used to not use `ReturnClauseSyntax` and had + // slightly different formatting behavior than the normal + // `ReturnClauseSyntax`. To maintain the previous formatting behavior, // add a special case. before(node.arrow, tokens: .break) before(node.type.firstToken(viewMode: .sourceAccurate), tokens: .break) @@ -1819,7 +1819,7 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { override func visit(_ node: OriginallyDefinedInAttributeArgumentsSyntax) -> SyntaxVisitorContinueKind { after(node.colon.lastToken(viewMode: .sourceAccurate), tokens: .break(.same, size: 1)) after(node.comma.lastToken(viewMode: .sourceAccurate), tokens: .break(.same, size: 1)) - return .visitChildren + return .visitChildren } override func visit(_ node: DocumentationAttributeArgumentSyntax) -> SyntaxVisitorContinueKind { @@ -2446,6 +2446,13 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { if !node.segments.isEmpty { before(node.closingQuote, tokens: .break(breakKind, newlines: .hard(count: 1))) } + if shouldFormatterIgnore(node: Syntax(node)) { + appendFormatterIgnored(node: Syntax(node)) + // Mirror the tokens we'd normally append on '"""' + appendTrailingTrivia(node.closingQuote) + appendAfterTokensAndTrailingComments(node.closingQuote) + return .skipChildren + } } return .visitChildren } @@ -2460,38 +2467,92 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { return .visitChildren } - override func visit(_ node: StringSegmentSyntax) -> SyntaxVisitorContinueKind { - // Looks up the correct break kind based on prior context. - func breakKind() -> BreakKind { - if let stringLiteralSegments = node.parent?.as(StringLiteralSegmentListSyntax.self), - let stringLiteralExpr = stringLiteralSegments.parent?.as(StringLiteralExprSyntax.self) - { - return pendingMultilineStringBreakKinds[stringLiteralExpr, default: .same] + // Insert an `.escaped` break token after each series of whitespace in a substring + private func emitMultilineSegmentTextTokens(breakKind: BreakKind, segment: Substring) { + var currentWord = [Unicode.Scalar]() + var currentBreak = [Unicode.Scalar]() + + func emitWord() { + if !currentWord.isEmpty { + var str = "" + str.unicodeScalars.append(contentsOf: currentWord) + appendToken(.syntax(str)) + currentWord = [] + } + } + func emitBreak() { + if !currentBreak.isEmpty { + // We append this as a syntax, instead of a `.space`, so that it is always included in the output. + var str = "" + str.unicodeScalars.append(contentsOf: currentBreak) + appendToken(.syntax(str)) + appendToken(.break(breakKind, size: 0, newlines: .escaped)) + currentBreak = [] + } + } + + for scalar in segment.unicodeScalars { + // We don't have to worry about newlines occurring in segments. + // Either a segment will end in a newline character or the newline will be in trivia. + if scalar.properties.isWhitespace { + emitWord() + currentBreak.append(scalar) } else { - return .same + emitBreak() + currentWord.append(scalar) } } + // Only one of these will actually do anything based on whether our last char was whitespace or not. + emitWord() + emitBreak() + } + + override func visit(_ node: StringSegmentSyntax) -> SyntaxVisitorContinueKind { + // Looks up the correct break kind based on prior context. + let stringLiteralParent = + node.parent? + .as(StringLiteralSegmentListSyntax.self)? + .parent? + .as(StringLiteralExprSyntax.self) + let breakKind = stringLiteralParent.map { + pendingMultilineStringBreakKinds[$0, default: .same] + } ?? .same + + let isMultiLineString = + stringLiteralParent?.openingQuote.tokenKind == .multilineStringQuote + // We don't reflow raw strings, so treat them as if they weren't multiline + && stringLiteralParent?.openingPounds == nil + + let emitSegmentTextTokens = + // If our configure reflow behavior is never, always use the single line emit segment text tokens. + isMultiLineString && !config.reflowMultilineStringLiterals.isNever + ? { (segment) in self.emitMultilineSegmentTextTokens(breakKind: breakKind, segment: segment) } + // For single line strings we don't allow line breaks, so emit the string as a single `.syntax` token + : { (segment) in self.appendToken(.syntax(String(segment))) } + let segmentText = node.content.text if segmentText.hasSuffix("\n") { // If this is a multiline string segment, it will end in a newline. Remove the newline and // append the rest of the string, followed by a break if it's not the last line before the // closing quotes. (The `StringLiteralExpr` above does the closing break.) let remainder = node.content.text.dropLast() + if !remainder.isEmpty { - appendToken(.syntax(String(remainder))) + // Replace each space in the segment text by an elective break of size 1 + emitSegmentTextTokens(remainder) } - appendToken(.break(breakKind(), newlines: .hard(count: 1))) + appendToken(.break(breakKind, newlines: .hard(count: 1))) } else { - appendToken(.syntax(segmentText)) + emitSegmentTextTokens(segmentText[...]) } - if node.trailingTrivia.containsBackslashes { + 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("\\")) - appendToken(.break(breakKind(), newlines: .hard(count: 1))) + appendToken(.break(breakKind, newlines: .hard(count: 1))) } return .skipChildren } @@ -3198,7 +3259,8 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { // There must be a break with a soft newline after the comment, but it's impossible to // know which kind of break must be used. Adding this newline is deferred until the // comment is added to the token stream. - ]) + ] + ) case .blockComment(let text): return ( @@ -3487,12 +3549,18 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { if let last = tokens.last { switch (last, token) { + case (.break(_, _, .escaped), _), (_, .break(_, _, .escaped)): + lastBreakIndex = tokens.endIndex + // Don't allow merging for .escaped breaks + canMergeNewlinesIntoLastBreak = false + tokens.append(token) + return case (.break(let breakKind, _, .soft(1, _)), .comment(let c2, _)) where breakAllowsCommentMerge(breakKind) && (c2.kind == .docLine || c2.kind == .line): // we are search for the pattern of [line comment] - [soft break 1] - [line comment] // where the comment type is the same; these can be merged into a single comment if let nextToLast = tokens.dropLast().last, - case let .comment(c1, false) = nextToLast, + case let .comment(c1, false) = nextToLast, c1.kind == c2.kind { var mergedComment = c1 @@ -4291,6 +4359,10 @@ extension NewlineBehavior { // `lhs` is either also elective or a required newline, which overwrites elective. return lhs + case (.escaped, _): + return rhs + case (_, .escaped): + return lhs case (.soft(let lhsCount, let lhsDiscretionary), .soft(let rhsCount, let rhsDiscretionary)): let mergedCount: Int if lhsDiscretionary && rhsDiscretionary { diff --git a/Tests/SwiftFormatTests/PrettyPrint/StringTests.swift b/Tests/SwiftFormatTests/PrettyPrint/StringTests.swift index a9cc75f96..3c9f937e7 100644 --- a/Tests/SwiftFormatTests/PrettyPrint/StringTests.swift +++ b/Tests/SwiftFormatTests/PrettyPrint/StringTests.swift @@ -1,3 +1,5 @@ +@_spi(Rules) @_spi(Testing) import SwiftFormat + final class StringTests: PrettyPrintTestCase { func testStrings() { let input = @@ -22,6 +24,173 @@ final class StringTests: PrettyPrintTestCase { assertPrettyPrintEqual(input: input, expected: expected, linelength: 35) } + func testLongMultilinestringIsWrapped() { + let input = + #""" + let someString = """ + this string's total lengths will be longer than the column limit even though its individual lines are as well, whoops. + """ + """# + + let expected = + #""" + let someString = """ + this string's total \ + lengths will be longer \ + than the column limit even \ + though its individual \ + lines are as well, whoops. + """ + + """# + + var config = Configuration() + config.reflowMultilineStringLiterals = .onlyLinesOverLength + assertPrettyPrintEqual( + input: input, + expected: expected, + linelength: 30, + configuration: config) + } + + func testMultilineStringIsNotReformattedWithIgnore() { + let input = + #""" + let someString = + // swift-format-ignore + """ + lines \ + are \ + short. + """ + """# + + let expected = + #""" + let someString = + // swift-format-ignore + """ + lines \ + are \ + short. + """ + + """# + + assertPrettyPrintEqual(input: input, expected: expected, linelength: 30) + } + + func testMultilineStringIsNotReformattedWithReflowDisabled() { + let input = + #""" + let someString = + """ + lines \ + are \ + short. + """ + """# + + let expected = + #""" + let someString = + """ + lines \ + are \ + short. + """ + + """# + + var config = Configuration() + config.reflowMultilineStringLiterals = .onlyLinesOverLength + assertPrettyPrintEqual(input: input, expected: expected, linelength: 30, configuration: config) + } + + func testMultilineStringWithInterpolations() { + let input = + #""" + if true { + guard let opt else { + functionCall(""" + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec rutrum libero \(2) \(testVariable) ids risus placerat imperdiet. Praesent fringilla vel nisi sed fermentum. In vitae purus feugiat, euismod nulla in, rhoncus leo. Suspendisse feugiat sapien lobortis facilisis malesuada. Aliquam feugiat suscipit accumsan. Praesent tempus fermentum est, vel blandit mi pretium a. Proin in posuere sapien. Nunc tincidunt efficitur ante id fermentum. + """) + } + } + """# + + let expected = + #""" + if true { + guard let opt else { + functionCall( + """ + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec rutrum libero \(2) \ + \(testVariable) ids risus placerat imperdiet. Praesent fringilla vel nisi sed fermentum. In \ + vitae purus feugiat, euismod nulla in, rhoncus leo. Suspendisse feugiat sapien lobortis \ + facilisis malesuada. Aliquam feugiat suscipit accumsan. Praesent tempus fermentum est, vel \ + blandit mi pretium a. Proin in posuere sapien. Nunc tincidunt efficitur ante id fermentum. + """) + } + } + + """# + + var config = Configuration() + config.reflowMultilineStringLiterals = .onlyLinesOverLength + assertPrettyPrintEqual(input: input, expected: expected, linelength: 100, configuration: config) + } + + func testMutlilineStringsRespectsHardLineBreaks() { + let input = + #""" + """ + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec rutrum libero ids risus placerat imperdiet. Praesent fringilla vel nisi sed fermentum. In vitae purus feugiat, euismod nulla in, rhoncus leo. + Suspendisse feugiat sapien lobortis facilisis malesuada. Aliquam feugiat suscipit accumsan. Praesent tempus fermentum est, vel blandit mi pretium a. Proin in posuere sapien. Nunc tincidunt efficitur ante id fermentum. + """ + """# + + let expected = + #""" + """ + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec rutrum libero ids risus placerat \ + imperdiet. Praesent fringilla vel nisi sed fermentum. In vitae purus feugiat, euismod nulla in, \ + rhoncus leo. + Suspendisse feugiat sapien lobortis facilisis malesuada. Aliquam feugiat suscipit accumsan. \ + Praesent tempus fermentum est, vel blandit mi pretium a. Proin in posuere sapien. Nunc tincidunt \ + efficitur ante id fermentum. + """ + + """# + + var config = Configuration() + config.reflowMultilineStringLiterals = .onlyLinesOverLength + assertPrettyPrintEqual(input: input, expected: expected, linelength: 100, configuration: config) + } + + func testMultilineStringsWrapAroundInterpolations() { + let input = + #""" + """ + An interpolation should be treated as a single "word" and can't be broken up \(aLongVariableName + anotherLongVariableName), so no line breaks should be available within the expr. + """ + """# + + let expected = + #""" + """ + An interpolation should be treated as a single "word" and can't be broken up \ + \(aLongVariableName + anotherLongVariableName), so no line breaks should be available within the \ + expr. + """ + + """# + + var config = Configuration() + config.reflowMultilineStringLiterals = .onlyLinesOverLength + assertPrettyPrintEqual(input: input, expected: expected, linelength: 100, configuration: config) + } + func testMultilineStringOpenQuotesDoNotWrapIfStringIsVeryLong() { let input = #""" @@ -102,6 +271,27 @@ final class StringTests: PrettyPrintTestCase { assertPrettyPrintEqual(input: input, expected: expected, linelength: 25) } + func testMultilineStringWithWordLongerThanLineLength() { + let input = + #""" + """ + there isn't an opportunity to break up this long url: https://www.cool-math-games.org/games/id?=01913310-b7c3-77d8-898e-300ccd451ea8 + """ + """# + let expected = + #""" + """ + there isn't an opportunity to break up this long url: \ + https://www.cool-math-games.org/games/id?=01913310-b7c3-77d8-898e-300ccd451ea8 + """ + + """# + + var config = Configuration() + config.reflowMultilineStringLiterals = .onlyLinesOverLength + assertPrettyPrintEqual(input: input, expected: expected, linelength: 70, configuration: config) + } + func testMultilineStringInterpolations() { let input = #""" @@ -218,7 +408,7 @@ final class StringTests: PrettyPrintTestCase { assertPrettyPrintEqual(input: input, expected: expected, linelength: 25) } - func testMultilineStringPreservesTrailingBackslashes() { + func testMultilineStringReflowsTrailingBackslashes() { let input = #""" let x = """ @@ -235,14 +425,54 @@ final class StringTests: PrettyPrintTestCase { let x = """ there should be \ backslashes at \ - the end of \ - every line \ - except this one + the end of every \ + line except this \ + one """ """# - assertPrettyPrintEqual(input: input, expected: expected, linelength: 20) + var config = Configuration.forTesting + config.reflowMultilineStringLiterals = .always + assertPrettyPrintEqual(input: input, expected: expected, linelength: 20, configuration: config) + } + + func testRawMultilineStringIsNotFormatted() { + let input = + ##""" + #""" + this is a long line that is not broken. + """# + """## + let expected = + ##""" + #""" + this is a long line that is not broken. + """# + + """## + + assertPrettyPrintEqual(input: input, expected: expected, linelength: 10) + } + + func testMultilineStringIsNotFormattedWithNeverReflowBehavior() { + let input = + #""" + """ + this is a long line that is not broken. + """ + """# + let expected = + #""" + """ + this is a long line that is not broken. + """ + + """# + + var config = Configuration.forTesting + config.reflowMultilineStringLiterals = .never + assertPrettyPrintEqual(input: input, expected: expected, linelength: 10, configuration: config) } func testMultilineStringInParenthesizedExpression() { @@ -437,7 +667,7 @@ final class StringTests: PrettyPrintTestCase { let x = """ blah blah - """.data(using: .utf8) { + """.data(using: .utf8) else { print(x) } """# @@ -449,7 +679,7 @@ final class StringTests: PrettyPrintTestCase { blah blah """.data(using: .utf8) - { + else { print(x) }