Skip to content

Commit b2a4a2c

Browse files
committed
Fix multiline string indentation
1 parent 8372760 commit b2a4a2c

File tree

4 files changed

+71
-27
lines changed

4 files changed

+71
-27
lines changed

CodeGeneration/Sources/generate-swiftsyntax/templates/basicformat/BasicFormatFile.swift

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -92,11 +92,14 @@ let basicFormatFile = SourceFileSyntax(leadingTrivia: generateCopyrightHeader(fo
9292
if requiresTrailingSpace(node) && trailingTrivia.isEmpty {
9393
trailingTrivia += .space
9494
}
95-
if let keyPath = getKeyPath(Syntax(node)), requiresLeadingNewline(keyPath), !(leadingTrivia.first?.isNewline ?? false) {
96-
leadingTrivia = .newline + leadingTrivia
95+
if let keyPath = getKeyPath(Syntax(node)), requiresLeadingNewline(keyPath), !(leadingTrivia.first?.isNewline ?? false), !isInStringLiteralInterpolationSegment(node) {
96+
leadingTrivia = .newline + leadingTrivia
9797
}
98-
leadingTrivia = leadingTrivia.indented(indentation: indentation)
99-
trailingTrivia = trailingTrivia.indented(indentation: indentation)
98+
var isOnNewline: Bool = (lastRewrittenToken?.trailingTrivia.pieces.last?.isNewline == true)
99+
if case .stringSegment(let text) = lastRewrittenToken?.tokenKind {
100+
isOnNewline = isOnNewline || (text.last?.isNewline == true)
101+
}
102+
leadingTrivia = leadingTrivia.indented(indentation: indentation, isOnNewline: isOnNewline)
100103
let rewritten = TokenSyntax(
101104
node.tokenKind,
102105
leadingTrivia: leadingTrivia,
@@ -110,6 +113,22 @@ let basicFormatFile = SourceFileSyntax(leadingTrivia: generateCopyrightHeader(fo
110113
"""
111114
)
112115

116+
DeclSyntax(
117+
"""
118+
open func isInStringLiteralInterpolationSegment(_ node: TokenSyntax) -> Bool {
119+
var ancestor: Syntax = Syntax(node)
120+
while let parent = ancestor.parent {
121+
ancestor = parent
122+
if ancestor.is(ExpressionSegmentSyntax.self) {
123+
return true
124+
}
125+
}
126+
127+
return false
128+
}
129+
"""
130+
)
131+
113132
try FunctionDeclSyntax("open func shouldIndent(_ keyPath: AnyKeyPath) -> Bool") {
114133
try SwitchExprSyntax("switch keyPath") {
115134
for node in SYNTAX_NODES where !node.isBase {

Sources/SwiftBasicFormat/Trivia+Indented.swift

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,25 +13,35 @@
1313
import SwiftSyntax
1414

1515
extension Trivia {
16-
func indented(indentation: TriviaPiece) -> Trivia {
16+
/// Makes sure each newline of this trivia is followed by `indentation`. If this is not the case, the existing indentation is extended to `indentation`.
17+
/// `isOnNewline` determines whether the trivia starts on a new line. If this is the case, the function makes sure that the returned trivia starts with `indentation`.
18+
func indented(indentation: TriviaPiece, isOnNewline: Bool = false) -> Trivia {
1719
var indentedPieces: [TriviaPiece] = []
1820
for (index, piece) in self.enumerated() {
19-
let nextPiece = index < pieces.count - 1 ? pieces[index + 1] : nil
20-
indentedPieces.append(piece)
21-
if piece.isNewline {
22-
switch (nextPiece, indentation) {
23-
case (.spaces(let nextPieceSpaces)?, .spaces(let indentationSpaces)):
21+
let previousPieceIsNewline: Bool
22+
if index == 0 {
23+
previousPieceIsNewline = isOnNewline
24+
} else {
25+
previousPieceIsNewline = pieces[index - 1].isNewline
26+
}
27+
if previousPieceIsNewline {
28+
switch (piece, indentation) {
29+
case (.spaces(let nextPieceSpaces), .spaces(let indentationSpaces)):
2430
if nextPieceSpaces < indentationSpaces {
2531
indentedPieces.append(.spaces(indentationSpaces - nextPieceSpaces))
2632
}
27-
case (.tabs(let nextPieceTabs)?, .tabs(let indentationTabs)):
33+
case (.tabs(let nextPieceTabs), .tabs(let indentationTabs)):
2834
if nextPieceTabs < indentationTabs {
2935
indentedPieces.append(.tabs(indentationTabs - nextPieceTabs))
3036
}
3137
default:
3238
indentedPieces.append(indentation)
3339
}
3440
}
41+
indentedPieces.append(piece)
42+
}
43+
if self.pieces.last?.isNewline == true {
44+
indentedPieces.append(indentation)
3545
}
3646
return Trivia(pieces: indentedPieces)
3747
}

Sources/SwiftBasicFormat/generated/BasicFormat.swift

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,11 +54,14 @@ open class BasicFormat: SyntaxRewriter {
5454
if requiresTrailingSpace(node) && trailingTrivia.isEmpty {
5555
trailingTrivia += .space
5656
}
57-
if let keyPath = getKeyPath(Syntax(node)), requiresLeadingNewline(keyPath), !(leadingTrivia.first?.isNewline ?? false) {
57+
if let keyPath = getKeyPath(Syntax(node)), requiresLeadingNewline(keyPath), !(leadingTrivia.first?.isNewline ?? false), !isInStringLiteralInterpolationSegment(node) {
5858
leadingTrivia = .newline + leadingTrivia
5959
}
60-
leadingTrivia = leadingTrivia.indented(indentation: indentation)
61-
trailingTrivia = trailingTrivia.indented(indentation: indentation)
60+
var isOnNewline: Bool = (lastRewrittenToken?.trailingTrivia.pieces.last?.isNewline == true)
61+
if case .stringSegment(let text) = lastRewrittenToken?.tokenKind {
62+
isOnNewline = isOnNewline || (text.last?.isNewline == true)
63+
}
64+
leadingTrivia = leadingTrivia.indented(indentation: indentation, isOnNewline: isOnNewline)
6265
let rewritten = TokenSyntax(
6366
node.tokenKind,
6467
leadingTrivia: leadingTrivia,
@@ -71,6 +74,18 @@ open class BasicFormat: SyntaxRewriter {
7174
return rewritten
7275
}
7376

77+
open func isInStringLiteralInterpolationSegment(_ node: TokenSyntax) -> Bool {
78+
var ancestor: Syntax = Syntax(node)
79+
while let parent = ancestor.parent {
80+
ancestor = parent
81+
if ancestor.is(ExpressionSegmentSyntax.self) {
82+
return true
83+
}
84+
}
85+
86+
return false
87+
}
88+
7489
open func shouldIndent(_ keyPath: AnyKeyPath) -> Bool {
7590
switch keyPath {
7691
case \AccessorBlockSyntax.accessors:

Tests/SwiftSyntaxBuilderTest/StringLiteralTests.swift

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -286,11 +286,11 @@ final class StringLiteralTests: XCTestCase {
286286
buildable,
287287
#"""
288288
assertionFailure("""
289-
Error validating child at index \(index) of \(nodeKind):
290-
Node did not satisfy any node choice requirement.
291-
Validation failures:
292-
\(nonNilErrors.map({ "- \($0.description)" }).joined(separator: "\n"))
293-
""", file: file, line: line)
289+
Error validating child at index \(index) of \(nodeKind):
290+
Node did not satisfy any node choice requirement.
291+
Validation failures:
292+
\(nonNilErrors.map({ "- \($0.description)" }).joined(separator: "\n"))
293+
""", file: file, line: line)
294294
"""#
295295
)
296296
}
@@ -313,10 +313,10 @@ final class StringLiteralTests: XCTestCase {
313313
#"""
314314
if true {
315315
assertionFailure("""
316-
Error validating child at index
317-
Node did not satisfy any node choice requirement.
318-
Validation failures:
319-
""")
316+
Error validating child at index
317+
Node did not satisfy any node choice requirement.
318+
Validation failures:
319+
""")
320320
}
321321
"""#
322322
)
@@ -343,10 +343,10 @@ final class StringLiteralTests: XCTestCase {
343343
if true {
344344
assertionFailure(
345345
"""
346-
Error validating child at index
347-
Node did not satisfy any node choice requirement.
348-
Validation failures:
349-
"""
346+
Error validating child at index
347+
Node did not satisfy any node choice requirement.
348+
Validation failures:
349+
"""
350350
)
351351
}
352352
"""#

0 commit comments

Comments
 (0)