@@ -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
@@ -1781,8 +1799,9 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor {
1781
1799
1782
1800
// If the rhs starts with a parenthesized expression, stack indentation around it.
1783
1801
// 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) ) ]
1786
1805
after ( unindentingNode. lastToken, tokens: [ . break( . close( mustBreak: false ) , size: 0 ) ] )
1787
1806
} else {
1788
1807
beforeTokens = [ . break( . continue) ]
@@ -1797,7 +1816,7 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor {
1797
1816
}
1798
1817
1799
1818
after ( binOp. lastToken, tokens: beforeTokens)
1800
- } else if let ( unindentingNode, shouldReset) =
1819
+ } else if let ( unindentingNode, shouldReset, breakKind ) =
1801
1820
stackedIndentationBehavior ( after: binOp, rhs: rhs)
1802
1821
{
1803
1822
// For parenthesized expressions and for unparenthesized usages of `&&` and `||`, we don't
@@ -1807,7 +1826,7 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor {
1807
1826
// use open-continuation/close pairs around such operators and their right-hand sides so
1808
1827
// that the continuation breaks inside those scopes "stack", instead of receiving the
1809
1828
// 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]
1811
1830
if wrapsBeforeOperator {
1812
1831
before ( binOp. firstToken, tokens: openBreakTokens)
1813
1832
} else {
@@ -1928,8 +1947,8 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor {
1928
1947
if let initializer = node. initializer {
1929
1948
let expr = initializer. value
1930
1949
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 ) ) )
1933
1952
after ( unindentingNode. lastToken, tokens: . break( . close( mustBreak: false ) , size: 0 ) )
1934
1953
} else {
1935
1954
after ( initializer. equal, tokens: . break( . continue) )
@@ -2107,32 +2126,48 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor {
2107
2126
2108
2127
override func visit( _ node: StringLiteralExprSyntax ) -> SyntaxVisitorContinueKind {
2109
2128
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 ) ) )
2130
2133
}
2131
2134
return . visitChildren
2132
2135
}
2133
2136
2134
2137
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
2136
2171
}
2137
2172
2138
2173
override func visit( _ node: AssociatedtypeDeclSyntax ) -> SyntaxVisitorContinueKind {
@@ -2350,9 +2385,7 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor {
2350
2385
extractLeadingTrivia ( token)
2351
2386
closeScopeTokens. forEach ( appendToken)
2352
2387
2353
- if let pendingSegmentIndex = pendingMultilineStringSegmentPrefixLengths. index ( forKey: token) {
2354
- appendMultilineStringSegments ( at: pendingSegmentIndex)
2355
- } else if !ignoredTokens. contains ( token) {
2388
+ if !ignoredTokens. contains ( token) {
2356
2389
// Otherwise, it's just a regular token, so add the text as-is.
2357
2390
appendToken ( . syntax( token. presence == . present ? token. text : " " ) )
2358
2391
}
@@ -2364,48 +2397,6 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor {
2364
2397
return . skipChildren
2365
2398
}
2366
2399
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
-
2409
2400
/// Appends the before-tokens of the given syntax token to the token stream.
2410
2401
private func appendBeforeTokens( _ token: TokenSyntax ) {
2411
2402
if let before = beforeMap. removeValue ( forKey: token) {
@@ -3186,6 +3177,26 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor {
3186
3177
}
3187
3178
}
3188
3179
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
+
3189
3200
/// Returns the outermost node enclosing the given node whose closing delimiter(s) must be kept
3190
3201
/// alongside the last token of the given node. Any tokens between `node.lastToken` and the
3191
3202
/// returned node's `lastToken` are delimiter tokens that shouldn't be preceded by a break.
@@ -3215,7 +3226,7 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor {
3215
3226
private func stackedIndentationBehavior(
3216
3227
after operatorExpr: ExprSyntax ? = nil ,
3217
3228
rhs: ExprSyntax
3218
- ) -> ( unindentingNode: Syntax , shouldReset: Bool ) ? {
3229
+ ) -> ( unindentingNode: Syntax , shouldReset: Bool , breakKind : OpenBreakKind ) ? {
3219
3230
// Check for logical operators first, and if it's that kind of operator, stack indentation
3220
3231
// around the entire right-hand-side. We have to do this check before checking the RHS for
3221
3232
// parentheses because if the user writes something like `... && (foo) > bar || ...`, we don't
@@ -3234,9 +3245,10 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor {
3234
3245
// paren into the right hand side by unindenting after the final closing paren. This glues
3235
3246
// the paren to the last token of `rhs`.
3236
3247
if let unindentingParenExpr = outermostEnclosingNode ( from: Syntax ( rhs) ) {
3237
- return ( unindentingNode: unindentingParenExpr, shouldReset: true )
3248
+ return (
3249
+ unindentingNode: unindentingParenExpr, shouldReset: true , breakKind: . continuation)
3238
3250
}
3239
- return ( unindentingNode: Syntax ( rhs) , shouldReset: true )
3251
+ return ( unindentingNode: Syntax ( rhs) , shouldReset: true , breakKind : . continuation )
3240
3252
}
3241
3253
}
3242
3254
@@ -3245,7 +3257,9 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor {
3245
3257
if let ternaryExpr = rhs. as ( TernaryExprSyntax . self) {
3246
3258
// We don't try to absorb any parens in this case, because the condition of a ternary cannot
3247
3259
// 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)
3249
3263
}
3250
3264
3251
3265
// 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 {
3256
3270
// paren into the right hand side by unindenting after the final closing paren. This glues the
3257
3271
// paren to the last token of `rhs`.
3258
3272
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
3260
3282
}
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)
3262
3293
}
3263
3294
3264
3295
// Otherwise, don't stack--use regular continuation breaks instead.
0 commit comments