@@ -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
@@ -1774,8 +1792,9 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor {
17741792
17751793 // If the rhs starts with a parenthesized expression, stack indentation around it.
17761794 // Otherwise, use regular continuation breaks.
1777- if let ( unindentingNode, _) = stackedIndentationBehavior ( after: binOp, rhs: rhs) {
1778- beforeTokens = [ . break( . open( kind: . continuation) ) ]
1795+ if let ( unindentingNode, _, breakKind) = stackedIndentationBehavior ( after: binOp, rhs: rhs)
1796+ {
1797+ beforeTokens = [ . break( . open( kind: breakKind) ) ]
17791798 after ( unindentingNode. lastToken, tokens: [ . break( . close( mustBreak: false ) , size: 0 ) ] )
17801799 } else {
17811800 beforeTokens = [ . break( . continue) ]
@@ -1790,7 +1809,7 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor {
17901809 }
17911810
17921811 after ( binOp. lastToken, tokens: beforeTokens)
1793- } else if let ( unindentingNode, shouldReset) =
1812+ } else if let ( unindentingNode, shouldReset, breakKind ) =
17941813 stackedIndentationBehavior ( after: binOp, rhs: rhs)
17951814 {
17961815 // For parenthesized expressions and for unparenthesized usages of `&&` and `||`, we don't
@@ -1800,7 +1819,7 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor {
18001819 // use open-continuation/close pairs around such operators and their right-hand sides so
18011820 // that the continuation breaks inside those scopes "stack", instead of receiving the
18021821 // usual single-level "continuation line or not" behavior.
1803- let openBreakTokens : [ Token ] = [ . break( . open( kind: . continuation ) ) , . open]
1822+ let openBreakTokens : [ Token ] = [ . break( . open( kind: breakKind ) ) , . open]
18041823 if wrapsBeforeOperator {
18051824 before ( binOp. firstToken, tokens: openBreakTokens)
18061825 } else {
@@ -1921,8 +1940,8 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor {
19211940 if let initializer = node. initializer {
19221941 let expr = initializer. value
19231942
1924- if let ( unindentingNode, _) = stackedIndentationBehavior ( rhs: expr) {
1925- after ( initializer. equal, tokens: . break( . open( kind: . continuation ) ) )
1943+ if let ( unindentingNode, _, breakKind ) = stackedIndentationBehavior ( rhs: expr) {
1944+ after ( initializer. equal, tokens: . break( . open( kind: breakKind ) ) )
19261945 after ( unindentingNode. lastToken, tokens: . break( . close( mustBreak: false ) , size: 0 ) )
19271946 } else {
19281947 after ( initializer. equal, tokens: . break( . continue) )
@@ -2100,32 +2119,48 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor {
21002119
21012120 override func visit( _ node: StringLiteralExprSyntax ) -> SyntaxVisitorContinueKind {
21022121 if node. openQuote. tokenKind == . multilineStringQuote {
2103- // If it's a multiline string, the last segment of the literal will end with a newline and
2104- // zero or more whitespace that indicates the amount of whitespace stripped from each line of
2105- // the string literal.
2106- if let lastSegment = node. segments. last? . as ( StringSegmentSyntax . self) ,
2107- let lastLine
2108- = lastSegment. content. text. split ( separator: " \n " , omittingEmptySubsequences: false ) . last
2109- {
2110- let prefixCount = lastLine. count
2111-
2112- // Segments may be `StringSegmentSyntax` or `ExpressionSegmentSyntax`; for the purposes of
2113- // newline handling and whitespace stripping, we only need to handle the former.
2114- for segmentSyntax in node. segments {
2115- guard let segment = segmentSyntax. as ( StringSegmentSyntax . self) else {
2116- continue
2117- }
2118- // Register the content tokens of the segments and the amount of leading whitespace to
2119- // strip; this will be retrieved when we visit the token.
2120- pendingMultilineStringSegmentPrefixLengths [ segment. content] = prefixCount
2121- }
2122- }
2122+ // Looks up the correct break kind based on prior context.
2123+ let breakKind = pendingMultilineStringBreakKinds [ node, default: . same]
2124+ after ( node. openQuote, tokens: . break( breakKind, size: 0 , newlines: . hard( count: 1 ) ) )
2125+ before ( node. closeQuote, tokens: . break( breakKind, newlines: . hard( count: 1 ) ) )
21232126 }
21242127 return . visitChildren
21252128 }
21262129
21272130 override func visit( _ node: StringSegmentSyntax ) -> SyntaxVisitorContinueKind {
2128- return . visitChildren
2131+ // Looks up the correct break kind based on prior context.
2132+ func breakKind( ) -> BreakKind {
2133+ if let stringLiteralSegments = node. parent? . as ( StringLiteralSegmentsSyntax . self) ,
2134+ let stringLiteralExpr = stringLiteralSegments. parent? . as ( StringLiteralExprSyntax . self)
2135+ {
2136+ return pendingMultilineStringBreakKinds [ stringLiteralExpr, default: . same]
2137+ } else {
2138+ return . same
2139+ }
2140+ }
2141+
2142+ let segmentText = node. content. text
2143+ if segmentText. hasSuffix ( " \n " ) {
2144+ // If this is a multiline string segment, it will end in a newline. Remove the newline and
2145+ // append the rest of the string, followed by a break if it's not the last line before the
2146+ // closing quotes. (The `StringLiteralExpr` above does the closing break.)
2147+ let remainder = node. content. text. dropLast ( )
2148+ if !remainder. isEmpty {
2149+ appendToken ( . syntax( String ( remainder) ) )
2150+ }
2151+ appendToken ( . break( breakKind ( ) , newlines: . hard( count: 1 ) ) )
2152+ } else {
2153+ appendToken ( . syntax( segmentText) )
2154+ }
2155+
2156+ if node. trailingTrivia? . containsBackslashes == true {
2157+ // Segments with trailing backslashes won't end with a literal newline; the backslash is
2158+ // considered trivia. To preserve the original text and wrapping, we need to manually render
2159+ // the backslash and a break into the token stream.
2160+ appendToken ( . syntax( " \\ " ) )
2161+ appendToken ( . break( breakKind ( ) , newlines: . hard( count: 1 ) ) )
2162+ }
2163+ return . skipChildren
21292164 }
21302165
21312166 override func visit( _ node: AssociatedtypeDeclSyntax ) -> SyntaxVisitorContinueKind {
@@ -2343,9 +2378,7 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor {
23432378 extractLeadingTrivia ( token)
23442379 closeScopeTokens. forEach ( appendToken)
23452380
2346- if let pendingSegmentIndex = pendingMultilineStringSegmentPrefixLengths. index ( forKey: token) {
2347- appendMultilineStringSegments ( at: pendingSegmentIndex)
2348- } else if !ignoredTokens. contains ( token) {
2381+ if !ignoredTokens. contains ( token) {
23492382 // Otherwise, it's just a regular token, so add the text as-is.
23502383 appendToken ( . syntax( token. presence == . present ? token. text : " " ) )
23512384 }
@@ -2357,48 +2390,6 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor {
23572390 return . skipChildren
23582391 }
23592392
2360- /// Appends the contents of the pending multiline string segment at the given index in the
2361- /// registration dictionary (removing it from that dictionary) to the token stream, splitting it
2362- /// into lines along with required line breaks and stripping the leading whitespace.
2363- private func appendMultilineStringSegments( at index: Dictionary < TokenSyntax , Int > . Index ) {
2364- let ( token, prefixCount) = pendingMultilineStringSegmentPrefixLengths [ index]
2365- pendingMultilineStringSegmentPrefixLengths. remove ( at: index)
2366-
2367- let lines = token. text. split ( separator: " \n " , omittingEmptySubsequences: false )
2368-
2369- // The first "line" is a special case. If it is non-empty, then it is a piece of text that
2370- // immediately followed an interpolation segment on the same line of the string, like the
2371- // " baz" in "foo bar \(x + y) baz". If that is the case, we need to insert that text before
2372- // anything else.
2373- let firstLine = lines. first!
2374- if !firstLine. isEmpty {
2375- appendToken ( . syntax( String ( firstLine) ) )
2376- }
2377-
2378- // Add the remaining lines of the segment, preceding each with a newline and stripping the
2379- // leading whitespace so that the pretty-printer can re-indent the string according to the
2380- // standard rules that it would apply.
2381- for line in lines. dropFirst ( ) as ArraySlice {
2382- appendNewlines ( . hard)
2383-
2384- // Verify that the characters to be stripped are all spaces. If they are not, the string
2385- // is not valid (no line should contain less leading whitespace than the line with the
2386- // closing quotes), but the parser still allows this and it's flagged as an error later during
2387- // compilation, so we don't want to destroy the user's text in that case.
2388- let stringToAppend : Substring
2389- if ( line. prefix ( prefixCount) . allSatisfy { $0 == " " } ) {
2390- stringToAppend = line. dropFirst ( prefixCount)
2391- } else {
2392- // Only strip as many spaces as we have. This will force the misaligned line to line up with
2393- // the others; let's assume that's what the user wanted anyway.
2394- stringToAppend = line. drop { $0 == " " }
2395- }
2396- if !stringToAppend. isEmpty {
2397- appendToken ( . syntax( String ( stringToAppend) ) )
2398- }
2399- }
2400- }
2401-
24022393 /// Appends the before-tokens of the given syntax token to the token stream.
24032394 private func appendBeforeTokens( _ token: TokenSyntax ) {
24042395 if let before = beforeMap. removeValue ( forKey: token) {
@@ -3179,6 +3170,26 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor {
31793170 }
31803171 }
31813172
3173+ /// Walks the expression and returns the leftmost multiline string literal (which might be the
3174+ /// expression itself) if the leftmost child is a multiline string literal.
3175+ ///
3176+ /// - Parameter expr: The expression whose leftmost multiline string literal should be returned.
3177+ /// - Returns: The leftmost multiline string literal, or nil if the leftmost subexpression was
3178+ /// not a multiline string literal.
3179+ private func leftmostMultilineStringLiteral( of expr: ExprSyntax ) -> StringLiteralExprSyntax ? {
3180+ switch Syntax ( expr) . as ( SyntaxEnum . self) {
3181+ case . stringLiteralExpr( let stringLiteralExpr)
3182+ where stringLiteralExpr. openQuote. tokenKind == . multilineStringQuote:
3183+ return stringLiteralExpr
3184+ case . infixOperatorExpr( let infixOperatorExpr) :
3185+ return leftmostMultilineStringLiteral ( of: infixOperatorExpr. leftOperand)
3186+ case . ternaryExpr( let ternaryExpr) :
3187+ return leftmostMultilineStringLiteral ( of: ternaryExpr. conditionExpression)
3188+ default :
3189+ return nil
3190+ }
3191+ }
3192+
31823193 /// Returns the outermost node enclosing the given node whose closing delimiter(s) must be kept
31833194 /// alongside the last token of the given node. Any tokens between `node.lastToken` and the
31843195 /// returned node's `lastToken` are delimiter tokens that shouldn't be preceded by a break.
@@ -3208,7 +3219,7 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor {
32083219 private func stackedIndentationBehavior(
32093220 after operatorExpr: ExprSyntax ? = nil ,
32103221 rhs: ExprSyntax
3211- ) -> ( unindentingNode: Syntax , shouldReset: Bool ) ? {
3222+ ) -> ( unindentingNode: Syntax , shouldReset: Bool , breakKind : OpenBreakKind ) ? {
32123223 // Check for logical operators first, and if it's that kind of operator, stack indentation
32133224 // around the entire right-hand-side. We have to do this check before checking the RHS for
32143225 // parentheses because if the user writes something like `... && (foo) > bar || ...`, we don't
@@ -3227,9 +3238,10 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor {
32273238 // paren into the right hand side by unindenting after the final closing paren. This glues
32283239 // the paren to the last token of `rhs`.
32293240 if let unindentingParenExpr = outermostEnclosingNode ( from: Syntax ( rhs) ) {
3230- return ( unindentingNode: unindentingParenExpr, shouldReset: true )
3241+ return (
3242+ unindentingNode: unindentingParenExpr, shouldReset: true , breakKind: . continuation)
32313243 }
3232- return ( unindentingNode: Syntax ( rhs) , shouldReset: true )
3244+ return ( unindentingNode: Syntax ( rhs) , shouldReset: true , breakKind : . continuation )
32333245 }
32343246 }
32353247
@@ -3238,7 +3250,9 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor {
32383250 if let ternaryExpr = rhs. as ( TernaryExprSyntax . self) {
32393251 // We don't try to absorb any parens in this case, because the condition of a ternary cannot
32403252 // be grouped with any exprs outside of the condition.
3241- return ( unindentingNode: Syntax ( ternaryExpr. conditionExpression) , shouldReset: false )
3253+ return (
3254+ unindentingNode: Syntax ( ternaryExpr. conditionExpression) , shouldReset: false ,
3255+ breakKind: . continuation)
32423256 }
32433257
32443258 // If the right-hand-side of the operator is or starts with a parenthesized expression, stack
@@ -3249,9 +3263,26 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor {
32493263 // paren into the right hand side by unindenting after the final closing paren. This glues the
32503264 // paren to the last token of `rhs`.
32513265 if let unindentingParenExpr = outermostEnclosingNode ( from: Syntax ( rhs) ) {
3252- return ( unindentingNode: unindentingParenExpr, shouldReset: true )
3266+ return ( unindentingNode: unindentingParenExpr, shouldReset: true , breakKind: . continuation)
3267+ }
3268+
3269+ if let innerExpr = parenthesizedExpr. elementList. first? . expression,
3270+ let stringLiteralExpr = innerExpr. as ( StringLiteralExprSyntax . self) ,
3271+ stringLiteralExpr. openQuote. tokenKind == . multilineStringQuote
3272+ {
3273+ pendingMultilineStringBreakKinds [ stringLiteralExpr] = . continue
3274+ return nil
32533275 }
3254- return ( unindentingNode: Syntax ( parenthesizedExpr) , shouldReset: false )
3276+
3277+ return (
3278+ unindentingNode: Syntax ( parenthesizedExpr) , shouldReset: false , breakKind: . continuation)
3279+ }
3280+
3281+ // If the expression is a multiline string that is unparenthesized, create a block-based
3282+ // indentation scope and have the segments aligned inside it.
3283+ if let stringLiteralExpr = leftmostMultilineStringLiteral ( of: rhs) {
3284+ pendingMultilineStringBreakKinds [ stringLiteralExpr] = . same
3285+ return ( unindentingNode: Syntax ( stringLiteralExpr) , shouldReset: false , breakKind: . block)
32553286 }
32563287
32573288 // Otherwise, don't stack--use regular continuation breaks instead.
0 commit comments