Skip to content

Commit 799c88b

Browse files
authored
Merge pull request #534 from allevato/more-multiline-string-fixes
Further improve multiline string formatting.
2 parents e2d247c + 02f4db7 commit 799c88b

File tree

2 files changed

+85
-27
lines changed

2 files changed

+85
-27
lines changed

Sources/SwiftFormatPrettyPrint/TokenStreamCreator.swift

Lines changed: 55 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1883,24 +1883,27 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor {
18831883

18841884
// If the rhs starts with a parenthesized expression, stack indentation around it.
18851885
// Otherwise, use regular continuation breaks.
1886-
if let (unindentingNode, _, breakKind) = stackedIndentationBehavior(after: binOp, rhs: rhs)
1886+
if let (unindentingNode, _, breakKind, _) =
1887+
stackedIndentationBehavior(after: binOp, rhs: rhs)
18871888
{
18881889
beforeTokens = [.break(.open(kind: breakKind))]
1889-
after(unindentingNode.lastToken(viewMode: .sourceAccurate), tokens: [.break(.close(mustBreak: false), size: 0)])
1890+
after(
1891+
unindentingNode.lastToken(viewMode: .sourceAccurate),
1892+
tokens: [.break(.close(mustBreak: false), size: 0)])
18901893
} else {
18911894
beforeTokens = [.break(.continue)]
18921895
}
18931896

18941897
// When the RHS is a simple expression, even if is requires multiple lines, we don't add a
18951898
// group so that as much of the expression as possible can stay on the same line as the
18961899
// operator token.
1897-
if isCompoundExpression(rhs) {
1900+
if isCompoundExpression(rhs) && leftmostMultilineStringLiteral(of: rhs) == nil {
18981901
beforeTokens.append(.open)
18991902
after(rhs.lastToken(viewMode: .sourceAccurate), tokens: .close)
19001903
}
19011904

19021905
after(binOp.lastToken(viewMode: .sourceAccurate), tokens: beforeTokens)
1903-
} else if let (unindentingNode, shouldReset, breakKind) =
1906+
} else if let (unindentingNode, shouldReset, breakKind, shouldGroup) =
19041907
stackedIndentationBehavior(after: binOp, rhs: rhs)
19051908
{
19061909
// For parenthesized expressions and for unparenthesized usages of `&&` and `||`, we don't
@@ -1910,16 +1913,22 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor {
19101913
// use open-continuation/close pairs around such operators and their right-hand sides so
19111914
// that the continuation breaks inside those scopes "stack", instead of receiving the
19121915
// usual single-level "continuation line or not" behavior.
1913-
let openBreakTokens: [Token] = [.break(.open(kind: breakKind)), .open]
1916+
var openBreakTokens: [Token] = [.break(.open(kind: breakKind))]
1917+
if shouldGroup {
1918+
openBreakTokens.append(.open)
1919+
}
19141920
if wrapsBeforeOperator {
19151921
before(binOp.firstToken(viewMode: .sourceAccurate), tokens: openBreakTokens)
19161922
} else {
19171923
after(binOp.lastToken(viewMode: .sourceAccurate), tokens: openBreakTokens)
19181924
}
19191925

1920-
let closeBreakTokens: [Token] =
1926+
var closeBreakTokens: [Token] =
19211927
(shouldReset ? [.break(.reset, size: 0)] : [])
1922-
+ [.break(.close(mustBreak: false), size: 0), .close]
1928+
+ [.break(.close(mustBreak: false), size: 0)]
1929+
if shouldGroup {
1930+
closeBreakTokens.append(.close)
1931+
}
19231932
after(unindentingNode.lastToken(viewMode: .sourceAccurate), tokens: closeBreakTokens)
19241933
} else {
19251934
if wrapsBeforeOperator {
@@ -2031,7 +2040,7 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor {
20312040
if let initializer = node.initializer {
20322041
let expr = initializer.value
20332042

2034-
if let (unindentingNode, _, breakKind) = stackedIndentationBehavior(rhs: expr) {
2043+
if let (unindentingNode, _, breakKind, _) = stackedIndentationBehavior(rhs: expr) {
20352044
after(initializer.equal, tokens: .break(.open(kind: breakKind)))
20362045
after(unindentingNode.lastToken(viewMode: .sourceAccurate), tokens: .break(.close(mustBreak: false), size: 0))
20372046
} else {
@@ -2042,7 +2051,7 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor {
20422051
// When the RHS is a simple expression, even if is requires multiple lines, we don't add a
20432052
// group so that as much of the expression as possible can stay on the same line as the
20442053
// operator token.
2045-
if isCompoundExpression(expr) {
2054+
if isCompoundExpression(expr) && leftmostMultilineStringLiteral(of: expr) == nil {
20462055
before(expr.firstToken(viewMode: .sourceAccurate), tokens: .open)
20472056
after(expr.lastToken(viewMode: .sourceAccurate), tokens: .close)
20482057
}
@@ -3357,8 +3366,9 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor {
33573366
}
33583367

33593368
/// Determines if indentation should be stacked around a subexpression to the right of the given
3360-
/// operator, and, if so, returns the node after which indentation stacking should be closed and
3361-
/// whether or not the continuation state should be reset as well.
3369+
/// operator, and, if so, returns the node after which indentation stacking should be closed,
3370+
/// whether or not the continuation state should be reset as well, and whether or not a group
3371+
/// should be placed around the operator and the expression.
33623372
///
33633373
/// Stacking is applied around parenthesized expressions, but also for low-precedence operators
33643374
/// that frequently occur in long chains, such as logical AND (`&&`) and OR (`||`) in conditional
@@ -3367,7 +3377,7 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor {
33673377
private func stackedIndentationBehavior(
33683378
after operatorExpr: ExprSyntax? = nil,
33693379
rhs: ExprSyntax
3370-
) -> (unindentingNode: Syntax, shouldReset: Bool, breakKind: OpenBreakKind)? {
3380+
) -> (unindentingNode: Syntax, shouldReset: Bool, breakKind: OpenBreakKind, shouldGroup: Bool)? {
33713381
// Check for logical operators first, and if it's that kind of operator, stack indentation
33723382
// around the entire right-hand-side. We have to do this check before checking the RHS for
33733383
// parentheses because if the user writes something like `... && (foo) > bar || ...`, we don't
@@ -3387,9 +3397,18 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor {
33873397
// the paren to the last token of `rhs`.
33883398
if let unindentingParenExpr = outermostEnclosingNode(from: Syntax(rhs)) {
33893399
return (
3390-
unindentingNode: unindentingParenExpr, shouldReset: true, breakKind: .continuation)
3400+
unindentingNode: unindentingParenExpr,
3401+
shouldReset: true,
3402+
breakKind: .continuation,
3403+
shouldGroup: true
3404+
)
33913405
}
3392-
return (unindentingNode: Syntax(rhs), shouldReset: true, breakKind: .continuation)
3406+
return (
3407+
unindentingNode: Syntax(rhs),
3408+
shouldReset: true,
3409+
breakKind: .continuation,
3410+
shouldGroup: true
3411+
)
33933412
}
33943413
}
33953414

@@ -3399,8 +3418,11 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor {
33993418
// We don't try to absorb any parens in this case, because the condition of a ternary cannot
34003419
// be grouped with any exprs outside of the condition.
34013420
return (
3402-
unindentingNode: Syntax(ternaryExpr.conditionExpression), shouldReset: false,
3403-
breakKind: .continuation)
3421+
unindentingNode: Syntax(ternaryExpr.conditionExpression),
3422+
shouldReset: false,
3423+
breakKind: .continuation,
3424+
shouldGroup: true
3425+
)
34043426
}
34053427

34063428
// If the right-hand-side of the operator is or starts with a parenthesized expression, stack
@@ -3411,7 +3433,12 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor {
34113433
// paren into the right hand side by unindenting after the final closing paren. This glues the
34123434
// paren to the last token of `rhs`.
34133435
if let unindentingParenExpr = outermostEnclosingNode(from: Syntax(rhs)) {
3414-
return (unindentingNode: unindentingParenExpr, shouldReset: true, breakKind: .continuation)
3436+
return (
3437+
unindentingNode: unindentingParenExpr,
3438+
shouldReset: true,
3439+
breakKind: .continuation,
3440+
shouldGroup: true
3441+
)
34153442
}
34163443

34173444
if let innerExpr = parenthesizedExpr.elementList.first?.expression,
@@ -3423,14 +3450,23 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor {
34233450
}
34243451

34253452
return (
3426-
unindentingNode: Syntax(parenthesizedExpr), shouldReset: false, breakKind: .continuation)
3453+
unindentingNode: Syntax(parenthesizedExpr),
3454+
shouldReset: false,
3455+
breakKind: .continuation,
3456+
shouldGroup: true
3457+
)
34273458
}
34283459

34293460
// If the expression is a multiline string that is unparenthesized, create a block-based
34303461
// indentation scope and have the segments aligned inside it.
34313462
if let stringLiteralExpr = leftmostMultilineStringLiteral(of: rhs) {
34323463
pendingMultilineStringBreakKinds[stringLiteralExpr] = .same
3433-
return (unindentingNode: Syntax(stringLiteralExpr), shouldReset: false, breakKind: .block)
3464+
return (
3465+
unindentingNode: Syntax(stringLiteralExpr),
3466+
shouldReset: false,
3467+
breakKind: .block,
3468+
shouldGroup: false
3469+
)
34343470
}
34353471

34363472
// Otherwise, don't stack--use regular continuation breaks instead.

Tests/SwiftFormatPrettyPrintTests/StringTests.swift

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -296,10 +296,35 @@ final class StringTests: PrettyPrintTestCase {
296296
assertPrettyPrintEqual(input: input, expected: expected, linelength: 20)
297297
}
298298

299+
func testMultilineStringsInExpressionWithNarrowMargins() {
300+
let input =
301+
#"""
302+
x = """
303+
abcdefg
304+
hijklmn
305+
""" + """
306+
abcde
307+
hijkl
308+
"""
309+
"""#
310+
311+
let expected =
312+
#"""
313+
x = """
314+
abcdefg
315+
hijklmn
316+
"""
317+
+ """
318+
abcde
319+
hijkl
320+
"""
321+
322+
"""#
323+
324+
assertPrettyPrintEqual(input: input, expected: expected, linelength: 9)
325+
}
326+
299327
func testMultilineStringsInExpression() {
300-
// This output could probably be improved, but it's also a fairly unlikely occurrence. The
301-
// important part of this test is that the first string in the expression is indented relative
302-
// to the `let`.
303328
let input =
304329
#"""
305330
let x = """
@@ -313,12 +338,10 @@ final class StringTests: PrettyPrintTestCase {
313338

314339
let expected =
315340
#"""
316-
let x =
317-
"""
341+
let x = """
318342
this is a
319343
multiline string
320-
"""
321-
+ """
344+
""" + """
322345
this is more
323346
multiline string
324347
"""
@@ -327,7 +350,6 @@ final class StringTests: PrettyPrintTestCase {
327350

328351
assertPrettyPrintEqual(input: input, expected: expected, linelength: 20)
329352
}
330-
331353
func testLeadingMultilineStringsInOtherExpressions() {
332354
// The stacked indentation behavior needs to drill down into different node types to find the
333355
// leftmost multiline string literal. This makes sure that we cover various cases.

0 commit comments

Comments
 (0)