Skip to content

Commit 057a711

Browse files
committed
Fix multiline string indentation
1 parent e1beaa8 commit 057a711

File tree

4 files changed

+77
-27
lines changed

4 files changed

+77
-27
lines changed

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

Lines changed: 25 additions & 3 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) {
95+
if let keyPath = getKeyPath(Syntax(node)), requiresLeadingNewline(keyPath), !(leadingTrivia.first?.isNewline ?? false), !shouldOmitNewline(node) {
9696
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,25 @@ let basicFormatFile = SourceFileSyntax(leadingTrivia: generateCopyrightHeader(fo
110113
"""
111114
)
112115

116+
DeclSyntax(
117+
"""
118+
/// If this returns `true`, ``BasicFormat`` will not wrap `node` to a new line. This can be used to e.g. keep string interpolation segments on a single line.
119+
/// - Parameter node: the node that is being visited
120+
/// - Returns: returns true if newline should be omitted
121+
open func shouldOmitNewline(_ node: TokenSyntax) -> Bool {
122+
var ancestor: Syntax = Syntax(node)
123+
while let parent = ancestor.parent {
124+
ancestor = parent
125+
if ancestor.is(ExpressionSegmentSyntax.self) {
126+
return true
127+
}
128+
}
129+
130+
return false
131+
}
132+
"""
133+
)
134+
113135
try FunctionDeclSyntax("open func shouldIndent(_ keyPath: AnyKeyPath) -> Bool") {
114136
try SwitchExprSyntax("switch keyPath") {
115137
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: 21 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), !shouldOmitNewline(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,21 @@ open class BasicFormat: SyntaxRewriter {
7174
return rewritten
7275
}
7376

77+
/// If this returns `true`, ``BasicFormat`` will not wrap `node` to a new line. This can be used to e.g. keep string interpolation segments on a single line.
78+
/// - Parameter node: the node that is being visited
79+
/// - Returns: returns true if newline should be omitted
80+
open func shouldOmitNewline(_ node: TokenSyntax) -> Bool {
81+
var ancestor: Syntax = Syntax(node)
82+
while let parent = ancestor.parent {
83+
ancestor = parent
84+
if ancestor.is(ExpressionSegmentSyntax.self) {
85+
return true
86+
}
87+
}
88+
89+
return false
90+
}
91+
7492
open func shouldIndent(_ keyPath: AnyKeyPath) -> Bool {
7593
switch keyPath {
7694
case \AccessorBlockSyntax.accessors:

Tests/SwiftSyntaxBuilderTest/StringLiteralTests.swift

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -237,7 +237,7 @@ final class StringLiteralTests: XCTestCase {
237237
Error validating child at index \(index) of \(nodeKind):
238238
Node did not satisfy any node choice requirement.
239239
Validation failures:
240-
\(nonNilErrors.map({ "- \($0.description)" }).joined(separator: "\n"))
240+
\(nonNilErrors.map({ "- \($0.description)" }).joined(separator: "\n")))
241241
"""
242242
"""#
243243
)
@@ -259,11 +259,11 @@ final class StringLiteralTests: XCTestCase {
259259
buildable,
260260
#"""
261261
assertionFailure("""
262-
Error validating child at index \(index) of \(nodeKind):
263-
Node did not satisfy any node choice requirement.
264-
Validation failures:
265-
\(nonNilErrors.map({ "- \($0.description)" }).joined(separator: "\n"))
266-
""", file: file, line: line)
262+
Error validating child at index \(index) of \(nodeKind):
263+
Node did not satisfy any node choice requirement.
264+
Validation failures:
265+
\(nonNilErrors.map({ "- \($0.description)" }).joined(separator: "\n"))
266+
""", file: file, line: line)
267267
"""#
268268
)
269269
}
@@ -286,10 +286,10 @@ final class StringLiteralTests: XCTestCase {
286286
#"""
287287
if true {
288288
assertionFailure("""
289-
Error validating child at index
290-
Node did not satisfy any node choice requirement.
291-
Validation failures:
292-
""")
289+
Error validating child at index
290+
Node did not satisfy any node choice requirement.
291+
Validation failures:
292+
""")
293293
}
294294
"""#
295295
)
@@ -316,10 +316,10 @@ final class StringLiteralTests: XCTestCase {
316316
if true {
317317
assertionFailure(
318318
"""
319-
Error validating child at index
320-
Node did not satisfy any node choice requirement.
321-
Validation failures:
322-
"""
319+
Error validating child at index
320+
Node did not satisfy any node choice requirement.
321+
Validation failures:
322+
"""
323323
)
324324
}
325325
"""#

0 commit comments

Comments
 (0)