Skip to content

Commit 0387483

Browse files
committed
Fix indentation of nested multi-line string literals
1 parent db662d8 commit 0387483

File tree

2 files changed

+109
-6
lines changed

2 files changed

+109
-6
lines changed

Sources/SwiftBasicFormat/BasicFormat.swift

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,14 @@ open class BasicFormat: SyntaxRewriter {
4848
/// been visited yet.
4949
private var previousToken: TokenSyntax? = nil
5050

51+
/// The number of ancestors that are `StringLiteralExprSyntax`.
52+
private var stringLiteralNestingLevel = 0
53+
54+
/// Whether we are currently visiting the subtree of a `StringLiteralExprSyntax`.
55+
private var isInsideStringLiteral: Bool {
56+
return stringLiteralNestingLevel > 0
57+
}
58+
5159
public init(
5260
indentationWidth: Trivia = .spaces(4),
5361
initialIndentation: Trivia = [],
@@ -83,6 +91,9 @@ open class BasicFormat: SyntaxRewriter {
8391
}
8492

8593
open override func visitPre(_ node: Syntax) {
94+
if node.is(StringLiteralExprSyntax.self) {
95+
stringLiteralNestingLevel += 1
96+
}
8697
if requiresIndent(node) {
8798
if let firstToken = node.firstToken(viewMode: viewMode),
8899
let tokenIndentation = firstToken.leadingTrivia.indentation(isOnNewline: false),
@@ -98,6 +109,9 @@ open class BasicFormat: SyntaxRewriter {
98109
}
99110

100111
open override func visitPost(_ node: Syntax) {
112+
if node.is(StringLiteralExprSyntax.self) {
113+
stringLiteralNestingLevel -= 1
114+
}
101115
if requiresIndent(node) {
102116
decreaseIndentationLevel()
103117
}
@@ -498,10 +512,12 @@ open class BasicFormat: SyntaxRewriter {
498512
}
499513
}
500514

501-
let isEmptyLine = token.leadingTrivia.isEmpty && leadingTriviaIsFollowedByNewline
502-
if leadingTrivia.indentation(isOnNewline: isInitialToken || previousTokenWillEndWithNewline) == [] && !isEmptyLine {
515+
if leadingTrivia.indentation(isOnNewline: isInitialToken || previousTokenWillEndWithNewline) == [] && !token.isStringSegment {
503516
// If the token starts on a new line and does not have indentation, this
504-
// is the last non-indented token. Store its indentation level
517+
// is the last non-indented token. Store its indentation level.
518+
// But never consider string segments as anchor points since you can’t
519+
// indent individual lines of a multi-line string literals without breaking
520+
// their integrity.
505521
anchorPoints[token] = currentIndentationLevel
506522
}
507523

@@ -529,14 +545,17 @@ open class BasicFormat: SyntaxRewriter {
529545
var leadingTriviaIndentation = self.currentIndentationLevel
530546
var trailingTriviaIndentation = self.currentIndentationLevel
531547

532-
// If the trivia contain user-defined indentation, find their anchor point
548+
// If the trivia contains user-defined indentation, find their anchor point
533549
// and indent the token relative to that anchor point.
534-
if leadingTrivia.containsIndentation(isOnNewline: previousTokenWillEndWithNewline),
550+
// Always indent string literals relative to their anchor point because
551+
// their indentation has structural meaning and we just want to maintain
552+
// what the user wrote.
553+
if leadingTrivia.containsIndentation(isOnNewline: previousTokenWillEndWithNewline) || isInsideStringLiteral,
535554
let anchorPointIndentation = self.anchorPointIndentation(for: token)
536555
{
537556
leadingTriviaIndentation = anchorPointIndentation
538557
}
539-
if combinedTrailingTrivia.containsIndentation(isOnNewline: previousTokenWillEndWithNewline),
558+
if combinedTrailingTrivia.containsIndentation(isOnNewline: previousTokenWillEndWithNewline) || isInsideStringLiteral,
540559
let anchorPointIndentation = self.anchorPointIndentation(for: token)
541560
{
542561
trailingTriviaIndentation = anchorPointIndentation
@@ -567,6 +586,14 @@ open class BasicFormat: SyntaxRewriter {
567586
}
568587

569588
fileprivate extension TokenSyntax {
589+
var isStringSegment: Bool {
590+
if case .stringSegment = self.tokenKind {
591+
return true
592+
} else {
593+
return false
594+
}
595+
}
596+
570597
var isStringSegmentWithLastCharacterBeingNewline: Bool {
571598
switch self.tokenKind {
572599
case .stringSegment(let segment):

Tests/SwiftBasicFormatTest/BasicFormatTests.swift

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,82 @@ final class BasicFormatTest: XCTestCase {
285285
assertFormatted(source: source, expected: source)
286286
}
287287

288+
func testIndentNestedMultilineStringLiterals() throws {
289+
let stringLiteral: ExprSyntax = #"""
290+
"""
291+
292+
\("""
293+
First Line
294+
""")
295+
"""
296+
"""#
297+
298+
assertFormatted(tree: stringLiteral, expected: stringLiteral.description)
299+
300+
let tree = try FunctionDeclSyntax("func test()") {
301+
stringLiteral
302+
}
303+
304+
assertFormatted(
305+
tree: tree,
306+
expected: #"""
307+
func test() {
308+
"""
309+
310+
\("""
311+
First Line
312+
""")
313+
"""
314+
}
315+
"""#
316+
)
317+
}
318+
319+
func testIndentNestedIndentedMultilineStringLiterals() throws {
320+
let stringLiteral: ExprSyntax = #"""
321+
_ = """
322+
323+
\("""
324+
First Line
325+
""")
326+
"""
327+
"""#
328+
329+
assertFormatted(tree: stringLiteral, expected: stringLiteral.description)
330+
331+
let tree = try FunctionDeclSyntax("func test()") {
332+
stringLiteral
333+
}
334+
335+
assertFormatted(
336+
tree: tree,
337+
expected: #"""
338+
func test() {
339+
_ = """
340+
341+
\("""
342+
First Line
343+
""")
344+
"""
345+
}
346+
"""#
347+
)
348+
}
349+
350+
func testClosureInStringInterpolation() {
351+
let source = #"""
352+
"""
353+
\(gen { (x) in
354+
return """
355+
case
356+
"""
357+
})
358+
"""
359+
"""#
360+
361+
assertFormatted(source: source, expected: source)
362+
}
363+
288364
func testNestedUserDefinedIndentation() {
289365
assertFormatted(
290366
source: """

0 commit comments

Comments
 (0)