@@ -33,10 +33,10 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor {
33
33
/// appended since that break.
34
34
private var canMergeNewlinesIntoLastBreak = false
35
35
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 ] ( )
40
40
41
41
/// Lists tokens that shouldn't be appended to the token stream as `syntax` tokens. They will be
42
42
/// printed conditionally using a different type of token.
@@ -659,7 +659,14 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor {
659
659
}
660
660
661
661
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
+ }
663
670
return . visitChildren
664
671
}
665
672
@@ -1035,21 +1042,32 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor {
1035
1042
before ( node. firstToken, tokens: . open)
1036
1043
}
1037
1044
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
+ ]
1042
1053
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
+ }
1044
1061
1045
1062
if let trailingComma = node. trailingComma {
1063
+ before ( trailingComma, tokens: additionalEndTokens)
1046
1064
var afterTrailingComma : [ Token ] = [ . break( . same) ]
1047
1065
if shouldGroup {
1048
1066
afterTrailingComma. insert ( . close, at: 0 )
1049
1067
}
1050
1068
after ( trailingComma, tokens: afterTrailingComma)
1051
1069
} else if shouldGroup {
1052
- after ( node. lastToken, tokens: . close)
1070
+ after ( node. lastToken, tokens: additionalEndTokens + [ . close] )
1053
1071
}
1054
1072
}
1055
1073
@@ -1774,8 +1792,9 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor {
1774
1792
1775
1793
// If the rhs starts with a parenthesized expression, stack indentation around it.
1776
1794
// 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) ) ]
1779
1798
after ( unindentingNode. lastToken, tokens: [ . break( . close( mustBreak: false ) , size: 0 ) ] )
1780
1799
} else {
1781
1800
beforeTokens = [ . break( . continue) ]
@@ -1790,7 +1809,7 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor {
1790
1809
}
1791
1810
1792
1811
after ( binOp. lastToken, tokens: beforeTokens)
1793
- } else if let ( unindentingNode, shouldReset) =
1812
+ } else if let ( unindentingNode, shouldReset, breakKind ) =
1794
1813
stackedIndentationBehavior ( after: binOp, rhs: rhs)
1795
1814
{
1796
1815
// For parenthesized expressions and for unparenthesized usages of `&&` and `||`, we don't
@@ -1800,7 +1819,7 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor {
1800
1819
// use open-continuation/close pairs around such operators and their right-hand sides so
1801
1820
// that the continuation breaks inside those scopes "stack", instead of receiving the
1802
1821
// 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]
1804
1823
if wrapsBeforeOperator {
1805
1824
before ( binOp. firstToken, tokens: openBreakTokens)
1806
1825
} else {
@@ -1921,8 +1940,8 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor {
1921
1940
if let initializer = node. initializer {
1922
1941
let expr = initializer. value
1923
1942
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 ) ) )
1926
1945
after ( unindentingNode. lastToken, tokens: . break( . close( mustBreak: false ) , size: 0 ) )
1927
1946
} else {
1928
1947
after ( initializer. equal, tokens: . break( . continue) )
@@ -2100,32 +2119,48 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor {
2100
2119
2101
2120
override func visit( _ node: StringLiteralExprSyntax ) -> SyntaxVisitorContinueKind {
2102
2121
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 ) ) )
2123
2126
}
2124
2127
return . visitChildren
2125
2128
}
2126
2129
2127
2130
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
2129
2164
}
2130
2165
2131
2166
override func visit( _ node: AssociatedtypeDeclSyntax ) -> SyntaxVisitorContinueKind {
@@ -2343,9 +2378,7 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor {
2343
2378
extractLeadingTrivia ( token)
2344
2379
closeScopeTokens. forEach ( appendToken)
2345
2380
2346
- if let pendingSegmentIndex = pendingMultilineStringSegmentPrefixLengths. index ( forKey: token) {
2347
- appendMultilineStringSegments ( at: pendingSegmentIndex)
2348
- } else if !ignoredTokens. contains ( token) {
2381
+ if !ignoredTokens. contains ( token) {
2349
2382
// Otherwise, it's just a regular token, so add the text as-is.
2350
2383
appendToken ( . syntax( token. presence == . present ? token. text : " " ) )
2351
2384
}
@@ -2357,48 +2390,6 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor {
2357
2390
return . skipChildren
2358
2391
}
2359
2392
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
-
2402
2393
/// Appends the before-tokens of the given syntax token to the token stream.
2403
2394
private func appendBeforeTokens( _ token: TokenSyntax ) {
2404
2395
if let before = beforeMap. removeValue ( forKey: token) {
@@ -3179,6 +3170,26 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor {
3179
3170
}
3180
3171
}
3181
3172
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
+
3182
3193
/// Returns the outermost node enclosing the given node whose closing delimiter(s) must be kept
3183
3194
/// alongside the last token of the given node. Any tokens between `node.lastToken` and the
3184
3195
/// returned node's `lastToken` are delimiter tokens that shouldn't be preceded by a break.
@@ -3208,7 +3219,7 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor {
3208
3219
private func stackedIndentationBehavior(
3209
3220
after operatorExpr: ExprSyntax ? = nil ,
3210
3221
rhs: ExprSyntax
3211
- ) -> ( unindentingNode: Syntax , shouldReset: Bool ) ? {
3222
+ ) -> ( unindentingNode: Syntax , shouldReset: Bool , breakKind : OpenBreakKind ) ? {
3212
3223
// Check for logical operators first, and if it's that kind of operator, stack indentation
3213
3224
// around the entire right-hand-side. We have to do this check before checking the RHS for
3214
3225
// parentheses because if the user writes something like `... && (foo) > bar || ...`, we don't
@@ -3227,9 +3238,10 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor {
3227
3238
// paren into the right hand side by unindenting after the final closing paren. This glues
3228
3239
// the paren to the last token of `rhs`.
3229
3240
if let unindentingParenExpr = outermostEnclosingNode ( from: Syntax ( rhs) ) {
3230
- return ( unindentingNode: unindentingParenExpr, shouldReset: true )
3241
+ return (
3242
+ unindentingNode: unindentingParenExpr, shouldReset: true , breakKind: . continuation)
3231
3243
}
3232
- return ( unindentingNode: Syntax ( rhs) , shouldReset: true )
3244
+ return ( unindentingNode: Syntax ( rhs) , shouldReset: true , breakKind : . continuation )
3233
3245
}
3234
3246
}
3235
3247
@@ -3238,7 +3250,9 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor {
3238
3250
if let ternaryExpr = rhs. as ( TernaryExprSyntax . self) {
3239
3251
// We don't try to absorb any parens in this case, because the condition of a ternary cannot
3240
3252
// 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)
3242
3256
}
3243
3257
3244
3258
// 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 {
3249
3263
// paren into the right hand side by unindenting after the final closing paren. This glues the
3250
3264
// paren to the last token of `rhs`.
3251
3265
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
3253
3275
}
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)
3255
3286
}
3256
3287
3257
3288
// Otherwise, don't stack--use regular continuation breaks instead.
0 commit comments