Skip to content

Commit 02f4db7

Browse files
committed
Further improve multiline string formatting.
These changes remove unnecessary grouping around multiline string literals that were forcing subexpressions to wrap in less than ideal ways. Since multiline strings force hard line breaks after the open quotes, we can remove the grouping and produce better results when complex expressions are involved. For example, ```swift let x = """ abc def """ + """ ghi jkl """ ``` Before this change, we were forcing breaks after the `=` and before the `+`. Now, we only do so if the open quotes would overflow the line.
1 parent 146c573 commit 02f4db7

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)