@@ -33,10 +33,10 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor {
3333 /// appended since that break.
3434 private var canMergeNewlinesIntoLastBreak = false
3535
36- /// Keeps track of the prefix length of multiline string segments when they are visited so that
37- /// the prefix can be stripped at the beginning of lines before the text is added to the token
38- /// stream .
39- private var pendingMultilineStringSegmentPrefixLengths = [ TokenSyntax : Int ] ( )
36+ /// Keeps track of the kind of break that should be used inside a multiline string. This differs
37+ /// depending on surrounding context due to some tricky special cases, so this lets us pass that
38+ /// information down to the strings that need it .
39+ private var pendingMultilineStringBreakKinds = [ StringLiteralExprSyntax : BreakKind ] ( )
4040
4141 /// Lists tokens that shouldn't be appended to the token stream as `syntax` tokens. They will be
4242 /// printed conditionally using a different type of token.
@@ -659,7 +659,14 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor {
659659 }
660660
661661 override func visit( _ node: ReturnStmtSyntax ) -> SyntaxVisitorContinueKind {
662- before ( node. expression? . firstToken, tokens: . break)
662+ if let expression = node. expression {
663+ if leftmostMultilineStringLiteral ( of: expression) != nil {
664+ before ( expression. firstToken, tokens: . break( . open) )
665+ after ( expression. lastToken, tokens: . break( . close( mustBreak: false ) ) )
666+ } else {
667+ before ( expression. firstToken, tokens: . break)
668+ }
669+ }
663670 return . visitChildren
664671 }
665672
@@ -1035,21 +1042,32 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor {
10351042 before ( node. firstToken, tokens: . open)
10361043 }
10371044
1038- // If we have an open delimiter following the colon, use a space instead of a continuation
1039- // break so that we don't awkwardly shift the delimiter down and indent it further if it
1040- // wraps.
1041- let tokenAfterColon : Token = startsWithOpenDelimiter ( Syntax ( node. expression) ) ? . space : . break
1045+ var additionalEndTokens = [ Token] ( )
1046+ if let colon = node. colon {
1047+ // If we have an open delimiter following the colon, use a space instead of a continuation
1048+ // break so that we don't awkwardly shift the delimiter down and indent it further if it
1049+ // wraps.
1050+ var tokensAfterColon : [ Token ] = [
1051+ startsWithOpenDelimiter ( Syntax ( node. expression) ) ? . space : . break
1052+ ]
10421053
1043- after ( node. colon, tokens: tokenAfterColon)
1054+ if leftmostMultilineStringLiteral ( of: node. expression) != nil {
1055+ tokensAfterColon. append ( . break( . open( kind: . block) , size: 0 ) )
1056+ additionalEndTokens = [ . break( . close( mustBreak: false ) , size: 0 ) ]
1057+ }
1058+
1059+ after ( colon, tokens: tokensAfterColon)
1060+ }
10441061
10451062 if let trailingComma = node. trailingComma {
1063+ before ( trailingComma, tokens: additionalEndTokens)
10461064 var afterTrailingComma : [ Token ] = [ . break( . same) ]
10471065 if shouldGroup {
10481066 afterTrailingComma. insert ( . close, at: 0 )
10491067 }
10501068 after ( trailingComma, tokens: afterTrailingComma)
10511069 } else if shouldGroup {
1052- after ( node. lastToken, tokens: . close)
1070+ after ( node. lastToken, tokens: additionalEndTokens + [ . close] )
10531071 }
10541072 }
10551073
@@ -1781,8 +1799,9 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor {
17811799
17821800 // If the rhs starts with a parenthesized expression, stack indentation around it.
17831801 // Otherwise, use regular continuation breaks.
1784- if let ( unindentingNode, _) = stackedIndentationBehavior ( after: binOp, rhs: rhs) {
1785- beforeTokens = [ . break( . open( kind: . continuation) ) ]
1802+ if let ( unindentingNode, _, breakKind) = stackedIndentationBehavior ( after: binOp, rhs: rhs)
1803+ {
1804+ beforeTokens = [ . break( . open( kind: breakKind) ) ]
17861805 after ( unindentingNode. lastToken, tokens: [ . break( . close( mustBreak: false ) , size: 0 ) ] )
17871806 } else {
17881807 beforeTokens = [ . break( . continue) ]
@@ -1797,7 +1816,7 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor {
17971816 }
17981817
17991818 after ( binOp. lastToken, tokens: beforeTokens)
1800- } else if let ( unindentingNode, shouldReset) =
1819+ } else if let ( unindentingNode, shouldReset, breakKind ) =
18011820 stackedIndentationBehavior ( after: binOp, rhs: rhs)
18021821 {
18031822 // For parenthesized expressions and for unparenthesized usages of `&&` and `||`, we don't
@@ -1807,7 +1826,7 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor {
18071826 // use open-continuation/close pairs around such operators and their right-hand sides so
18081827 // that the continuation breaks inside those scopes "stack", instead of receiving the
18091828 // usual single-level "continuation line or not" behavior.
1810- let openBreakTokens : [ Token ] = [ . break( . open( kind: . continuation ) ) , . open]
1829+ let openBreakTokens : [ Token ] = [ . break( . open( kind: breakKind ) ) , . open]
18111830 if wrapsBeforeOperator {
18121831 before ( binOp. firstToken, tokens: openBreakTokens)
18131832 } else {
@@ -1928,8 +1947,8 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor {
19281947 if let initializer = node. initializer {
19291948 let expr = initializer. value
19301949
1931- if let ( unindentingNode, _) = stackedIndentationBehavior ( rhs: expr) {
1932- after ( initializer. equal, tokens: . break( . open( kind: . continuation ) ) )
1950+ if let ( unindentingNode, _, breakKind ) = stackedIndentationBehavior ( rhs: expr) {
1951+ after ( initializer. equal, tokens: . break( . open( kind: breakKind ) ) )
19331952 after ( unindentingNode. lastToken, tokens: . break( . close( mustBreak: false ) , size: 0 ) )
19341953 } else {
19351954 after ( initializer. equal, tokens: . break( . continue) )
@@ -2107,32 +2126,48 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor {
21072126
21082127 override func visit( _ node: StringLiteralExprSyntax ) -> SyntaxVisitorContinueKind {
21092128 if node. openQuote. tokenKind == . multilineStringQuote {
2110- // If it's a multiline string, the last segment of the literal will end with a newline and
2111- // zero or more whitespace that indicates the amount of whitespace stripped from each line of
2112- // the string literal.
2113- if let lastSegment = node. segments. last? . as ( StringSegmentSyntax . self) ,
2114- let lastLine
2115- = lastSegment. content. text. split ( separator: " \n " , omittingEmptySubsequences: false ) . last
2116- {
2117- let prefixCount = lastLine. count
2118-
2119- // Segments may be `StringSegmentSyntax` or `ExpressionSegmentSyntax`; for the purposes of
2120- // newline handling and whitespace stripping, we only need to handle the former.
2121- for segmentSyntax in node. segments {
2122- guard let segment = segmentSyntax. as ( StringSegmentSyntax . self) else {
2123- continue
2124- }
2125- // Register the content tokens of the segments and the amount of leading whitespace to
2126- // strip; this will be retrieved when we visit the token.
2127- pendingMultilineStringSegmentPrefixLengths [ segment. content] = prefixCount
2128- }
2129- }
2129+ // Looks up the correct break kind based on prior context.
2130+ let breakKind = pendingMultilineStringBreakKinds [ node, default: . same]
2131+ after ( node. openQuote, tokens: . break( breakKind, size: 0 , newlines: . hard( count: 1 ) ) )
2132+ before ( node. closeQuote, tokens: . break( breakKind, newlines: . hard( count: 1 ) ) )
21302133 }
21312134 return . visitChildren
21322135 }
21332136
21342137 override func visit( _ node: StringSegmentSyntax ) -> SyntaxVisitorContinueKind {
2135- return . visitChildren
2138+ // Looks up the correct break kind based on prior context.
2139+ func breakKind( ) -> BreakKind {
2140+ if let stringLiteralSegments = node. parent? . as ( StringLiteralSegmentsSyntax . self) ,
2141+ let stringLiteralExpr = stringLiteralSegments. parent? . as ( StringLiteralExprSyntax . self)
2142+ {
2143+ return pendingMultilineStringBreakKinds [ stringLiteralExpr, default: . same]
2144+ } else {
2145+ return . same
2146+ }
2147+ }
2148+
2149+ let segmentText = node. content. text
2150+ if segmentText. hasSuffix ( " \n " ) {
2151+ // If this is a multiline string segment, it will end in a newline. Remove the newline and
2152+ // append the rest of the string, followed by a break if it's not the last line before the
2153+ // closing quotes. (The `StringLiteralExpr` above does the closing break.)
2154+ let remainder = node. content. text. dropLast ( )
2155+ if !remainder. isEmpty {
2156+ appendToken ( . syntax( String ( remainder) ) )
2157+ }
2158+ appendToken ( . break( breakKind ( ) , newlines: . hard( count: 1 ) ) )
2159+ } else {
2160+ appendToken ( . syntax( segmentText) )
2161+ }
2162+
2163+ if node. trailingTrivia? . containsBackslashes == true {
2164+ // Segments with trailing backslashes won't end with a literal newline; the backslash is
2165+ // considered trivia. To preserve the original text and wrapping, we need to manually render
2166+ // the backslash and a break into the token stream.
2167+ appendToken ( . syntax( " \\ " ) )
2168+ appendToken ( . break( breakKind ( ) , newlines: . hard( count: 1 ) ) )
2169+ }
2170+ return . skipChildren
21362171 }
21372172
21382173 override func visit( _ node: AssociatedtypeDeclSyntax ) -> SyntaxVisitorContinueKind {
@@ -2350,9 +2385,7 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor {
23502385 extractLeadingTrivia ( token)
23512386 closeScopeTokens. forEach ( appendToken)
23522387
2353- if let pendingSegmentIndex = pendingMultilineStringSegmentPrefixLengths. index ( forKey: token) {
2354- appendMultilineStringSegments ( at: pendingSegmentIndex)
2355- } else if !ignoredTokens. contains ( token) {
2388+ if !ignoredTokens. contains ( token) {
23562389 // Otherwise, it's just a regular token, so add the text as-is.
23572390 appendToken ( . syntax( token. presence == . present ? token. text : " " ) )
23582391 }
@@ -2364,48 +2397,6 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor {
23642397 return . skipChildren
23652398 }
23662399
2367- /// Appends the contents of the pending multiline string segment at the given index in the
2368- /// registration dictionary (removing it from that dictionary) to the token stream, splitting it
2369- /// into lines along with required line breaks and stripping the leading whitespace.
2370- private func appendMultilineStringSegments( at index: Dictionary < TokenSyntax , Int > . Index ) {
2371- let ( token, prefixCount) = pendingMultilineStringSegmentPrefixLengths [ index]
2372- pendingMultilineStringSegmentPrefixLengths. remove ( at: index)
2373-
2374- let lines = token. text. split ( separator: " \n " , omittingEmptySubsequences: false )
2375-
2376- // The first "line" is a special case. If it is non-empty, then it is a piece of text that
2377- // immediately followed an interpolation segment on the same line of the string, like the
2378- // " baz" in "foo bar \(x + y) baz". If that is the case, we need to insert that text before
2379- // anything else.
2380- let firstLine = lines. first!
2381- if !firstLine. isEmpty {
2382- appendToken ( . syntax( String ( firstLine) ) )
2383- }
2384-
2385- // Add the remaining lines of the segment, preceding each with a newline and stripping the
2386- // leading whitespace so that the pretty-printer can re-indent the string according to the
2387- // standard rules that it would apply.
2388- for line in lines. dropFirst ( ) as ArraySlice {
2389- appendNewlines ( . hard)
2390-
2391- // Verify that the characters to be stripped are all spaces. If they are not, the string
2392- // is not valid (no line should contain less leading whitespace than the line with the
2393- // closing quotes), but the parser still allows this and it's flagged as an error later during
2394- // compilation, so we don't want to destroy the user's text in that case.
2395- let stringToAppend : Substring
2396- if ( line. prefix ( prefixCount) . allSatisfy { $0 == " " } ) {
2397- stringToAppend = line. dropFirst ( prefixCount)
2398- } else {
2399- // Only strip as many spaces as we have. This will force the misaligned line to line up with
2400- // the others; let's assume that's what the user wanted anyway.
2401- stringToAppend = line. drop { $0 == " " }
2402- }
2403- if !stringToAppend. isEmpty {
2404- appendToken ( . syntax( String ( stringToAppend) ) )
2405- }
2406- }
2407- }
2408-
24092400 /// Appends the before-tokens of the given syntax token to the token stream.
24102401 private func appendBeforeTokens( _ token: TokenSyntax ) {
24112402 if let before = beforeMap. removeValue ( forKey: token) {
@@ -3186,6 +3177,26 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor {
31863177 }
31873178 }
31883179
3180+ /// Walks the expression and returns the leftmost multiline string literal (which might be the
3181+ /// expression itself) if the leftmost child is a multiline string literal.
3182+ ///
3183+ /// - Parameter expr: The expression whose leftmost multiline string literal should be returned.
3184+ /// - Returns: The leftmost multiline string literal, or nil if the leftmost subexpression was
3185+ /// not a multiline string literal.
3186+ private func leftmostMultilineStringLiteral( of expr: ExprSyntax ) -> StringLiteralExprSyntax ? {
3187+ switch Syntax ( expr) . as ( SyntaxEnum . self) {
3188+ case . stringLiteralExpr( let stringLiteralExpr)
3189+ where stringLiteralExpr. openQuote. tokenKind == . multilineStringQuote:
3190+ return stringLiteralExpr
3191+ case . infixOperatorExpr( let infixOperatorExpr) :
3192+ return leftmostMultilineStringLiteral ( of: infixOperatorExpr. leftOperand)
3193+ case . ternaryExpr( let ternaryExpr) :
3194+ return leftmostMultilineStringLiteral ( of: ternaryExpr. conditionExpression)
3195+ default :
3196+ return nil
3197+ }
3198+ }
3199+
31893200 /// Returns the outermost node enclosing the given node whose closing delimiter(s) must be kept
31903201 /// alongside the last token of the given node. Any tokens between `node.lastToken` and the
31913202 /// returned node's `lastToken` are delimiter tokens that shouldn't be preceded by a break.
@@ -3215,7 +3226,7 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor {
32153226 private func stackedIndentationBehavior(
32163227 after operatorExpr: ExprSyntax ? = nil ,
32173228 rhs: ExprSyntax
3218- ) -> ( unindentingNode: Syntax , shouldReset: Bool ) ? {
3229+ ) -> ( unindentingNode: Syntax , shouldReset: Bool , breakKind : OpenBreakKind ) ? {
32193230 // Check for logical operators first, and if it's that kind of operator, stack indentation
32203231 // around the entire right-hand-side. We have to do this check before checking the RHS for
32213232 // parentheses because if the user writes something like `... && (foo) > bar || ...`, we don't
@@ -3234,9 +3245,10 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor {
32343245 // paren into the right hand side by unindenting after the final closing paren. This glues
32353246 // the paren to the last token of `rhs`.
32363247 if let unindentingParenExpr = outermostEnclosingNode ( from: Syntax ( rhs) ) {
3237- return ( unindentingNode: unindentingParenExpr, shouldReset: true )
3248+ return (
3249+ unindentingNode: unindentingParenExpr, shouldReset: true , breakKind: . continuation)
32383250 }
3239- return ( unindentingNode: Syntax ( rhs) , shouldReset: true )
3251+ return ( unindentingNode: Syntax ( rhs) , shouldReset: true , breakKind : . continuation )
32403252 }
32413253 }
32423254
@@ -3245,7 +3257,9 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor {
32453257 if let ternaryExpr = rhs. as ( TernaryExprSyntax . self) {
32463258 // We don't try to absorb any parens in this case, because the condition of a ternary cannot
32473259 // be grouped with any exprs outside of the condition.
3248- return ( unindentingNode: Syntax ( ternaryExpr. conditionExpression) , shouldReset: false )
3260+ return (
3261+ unindentingNode: Syntax ( ternaryExpr. conditionExpression) , shouldReset: false ,
3262+ breakKind: . continuation)
32493263 }
32503264
32513265 // If the right-hand-side of the operator is or starts with a parenthesized expression, stack
@@ -3256,9 +3270,26 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor {
32563270 // paren into the right hand side by unindenting after the final closing paren. This glues the
32573271 // paren to the last token of `rhs`.
32583272 if let unindentingParenExpr = outermostEnclosingNode ( from: Syntax ( rhs) ) {
3259- return ( unindentingNode: unindentingParenExpr, shouldReset: true )
3273+ return ( unindentingNode: unindentingParenExpr, shouldReset: true , breakKind: . continuation)
3274+ }
3275+
3276+ if let innerExpr = parenthesizedExpr. elementList. first? . expression,
3277+ let stringLiteralExpr = innerExpr. as ( StringLiteralExprSyntax . self) ,
3278+ stringLiteralExpr. openQuote. tokenKind == . multilineStringQuote
3279+ {
3280+ pendingMultilineStringBreakKinds [ stringLiteralExpr] = . continue
3281+ return nil
32603282 }
3261- return ( unindentingNode: Syntax ( parenthesizedExpr) , shouldReset: false )
3283+
3284+ return (
3285+ unindentingNode: Syntax ( parenthesizedExpr) , shouldReset: false , breakKind: . continuation)
3286+ }
3287+
3288+ // If the expression is a multiline string that is unparenthesized, create a block-based
3289+ // indentation scope and have the segments aligned inside it.
3290+ if let stringLiteralExpr = leftmostMultilineStringLiteral ( of: rhs) {
3291+ pendingMultilineStringBreakKinds [ stringLiteralExpr] = . same
3292+ return ( unindentingNode: Syntax ( stringLiteralExpr) , shouldReset: false , breakKind: . block)
32623293 }
32633294
32643295 // Otherwise, don't stack--use regular continuation breaks instead.
0 commit comments