Skip to content

Commit 4e10b2b

Browse files
committed
Wrap closure params in open/close breaks to indent inside parens.
Closures have an `input` clause which can either be parenthesized or not. When the clause is parenthesized, it's a different type of syntax. We want the formatter to create extra indentation inside of the parens in that case, just like it would for function call argument lists. To do this, I have added an open and a close break inside of the parens to create a block scope around the closure parameters.
1 parent e0a13b1 commit 4e10b2b

File tree

2 files changed

+126
-9
lines changed

2 files changed

+126
-9
lines changed

Sources/SwiftFormatPrettyPrint/TokenStreamCreator.swift

Lines changed: 30 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -889,22 +889,43 @@ private final class TokenStreamCreator: SyntaxVisitor {
889889
}
890890

891891
override func visit(_ node: ClosureSignatureSyntax) -> SyntaxVisitorContinueKind {
892-
let consistency: GroupBreakStyle
893-
if let input = node.input, input.is(ClosureParamListSyntax.self) {
894-
consistency = argumentListConsistency()
895-
} else {
896-
consistency = .inconsistent
897-
}
898-
899892
before(node.firstToken, tokens: .open)
900893

901894
if let input = node.input {
902895
// We unconditionally put a break before the `in` keyword below, so we should only put a break
903896
// after the capture list's right bracket if there are arguments following it or we'll end up
904897
// with an extra space if the line doesn't wrap.
905898
after(node.capture?.rightSquare, tokens: .break(.same))
906-
before(input.firstToken, tokens: .open(consistency))
907-
after(input.lastToken, tokens: .close)
899+
900+
// When it's parenthesized, the input is a `ParameterClauseSyntax`. Otherwise, it's a
901+
// `ClosureParamListSyntax`. The parenthesized version is wrapped in open/close breaks so that
902+
// the parens create an extra level of indentation.
903+
if let parameterClause = input.as(ParameterClauseSyntax.self) {
904+
// Whether we should prioritize keeping ") throws -> <return_type>" together. We can only do
905+
// this if the closure has arguments.
906+
let keepOutputTogether =
907+
!parameterClause.parameterList.isEmpty && config.prioritizeKeepingFunctionOutputTogether
908+
909+
// Keep the output together by grouping from the right paren to the end of the output.
910+
if keepOutputTogether {
911+
// Due to visitation order, the matching .open break is added in ParameterClauseSyntax.
912+
// Since the output clause is optional but the in-token is required, placing the .close
913+
// before `inTok` ensures the close gets into the token stream.
914+
before(node.inTok, tokens: .close)
915+
} else {
916+
// Group outside of the parens, so that the argument list together, preferring to break
917+
// between the argument list and the output.
918+
before(input.firstToken, tokens: .open)
919+
after(input.lastToken, tokens: .close)
920+
}
921+
922+
arrangeParameterClause(parameterClause, forcesBreakBeforeRightParen: true)
923+
} else {
924+
// Group around the arguments, but don't use open/close breaks because there are no parens
925+
// to create a new scope.
926+
before(input.firstToken, tokens: .open(argumentListConsistency()))
927+
after(input.lastToken, tokens: .close)
928+
}
908929
}
909930

910931
before(node.throwsTok, tokens: .break)

Tests/SwiftFormatPrettyPrintTests/ClosureExprTests.swift

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ public class ClosureExprTests: PrettyPrintTestCase {
1414
funcCall(closure: { s1, s2, s3, s4, s5, s6, s7, s8, s9, s10 in return s1 })
1515
funcCall(param1: 123, closure: { s1, s2, s3 in return s1 })
1616
funcCall(closure: { (s1: String, s2: String) -> Bool in return s1 > s2 })
17+
funcCall(closure: { (s1: String, s2: String, s3: String, s4: String, s5: String) -> Bool in return s1 > s2 })
1718
"""
1819

1920
let expected =
@@ -53,6 +54,15 @@ public class ClosureExprTests: PrettyPrintTestCase {
5354
(s1: String, s2: String) -> Bool in
5455
return s1 > s2
5556
})
57+
funcCall(closure: {
58+
(
59+
s1: String,
60+
s2: String,
61+
s3: String,
62+
s4: String,
63+
s5: String
64+
) -> Bool in return s1 > s2
65+
})
5666
5767
"""
5868

@@ -73,6 +83,7 @@ public class ClosureExprTests: PrettyPrintTestCase {
7383
funcCall(closure: { s1, s2, s3, s4, s5, s6, s7, s8, s9, s10 in return s1 })
7484
funcCall(param1: 123, closure: { s1, s2, s3 in return s1 })
7585
funcCall(closure: { (s1: String, s2: String) -> Bool in return s1 > s2 })
86+
funcCall(closure: { (s1: String, s2: String, s3: String, s4: String, s5: String) -> Bool in return s1 > s2 })
7687
"""
7788

7889
let expected =
@@ -98,6 +109,12 @@ public class ClosureExprTests: PrettyPrintTestCase {
98109
(s1: String, s2: String) -> Bool in
99110
return s1 > s2
100111
})
112+
funcCall(closure: {
113+
(
114+
s1: String, s2: String, s3: String,
115+
s4: String, s5: String
116+
) -> Bool in return s1 > s2
117+
})
101118
102119
"""
103120

@@ -113,6 +130,7 @@ public class ClosureExprTests: PrettyPrintTestCase {
113130
funcCall(param1: 2) { $1 < $2 }
114131
funcCall(param1: 2) { s1, s2, s3 in return s1}
115132
funcCall(param1: 2) { s1, s2, s3, s4, s5 in return s1}
133+
funcCall(param1: 2) { (s1: String, s2: String, s3: String, s4: String, s5: String) -> Bool in return s1 > s2 }
116134
"""
117135

118136
let expected =
@@ -125,6 +143,12 @@ public class ClosureExprTests: PrettyPrintTestCase {
125143
funcCall(param1: 2) {
126144
s1, s2, s3, s4, s5 in return s1
127145
}
146+
funcCall(param1: 2) {
147+
(
148+
s1: String, s2: String, s3: String,
149+
s4: String, s5: String
150+
) -> Bool in return s1 > s2
151+
}
128152
129153
"""
130154

@@ -345,4 +369,76 @@ public class ClosureExprTests: PrettyPrintTestCase {
345369

346370
assertPrettyPrintEqual(input: input, expected: expected, linelength: 60)
347371
}
372+
373+
public func testClosureOutputGrouping() {
374+
let input =
375+
"""
376+
funcCall(closure: {
377+
(s1: String, s2: String, s3: String) throws -> Bool in return s1 > s2
378+
})
379+
funcCall(closure: {
380+
(s1: String, s2: String, s3: String) throws -> AVeryLongReturnTypeThatOverflowsFiftyColumns in return s1 > s2
381+
})
382+
funcCall(closure: {
383+
() throws -> Bool in return s1 > s2
384+
})
385+
funcCall(closure: {
386+
() throws -> AVeryLongReturnTypeThatOverflowsFiftyColumns in return s1 > s2
387+
})
388+
"""
389+
390+
let expectedNotGroupingOutput =
391+
"""
392+
funcCall(closure: {
393+
(s1: String, s2: String, s3: String) throws
394+
-> Bool in return s1 > s2
395+
})
396+
funcCall(closure: {
397+
(s1: String, s2: String, s3: String) throws
398+
-> AVeryLongReturnTypeThatOverflowsFiftyColumns
399+
in return s1 > s2
400+
})
401+
funcCall(closure: {
402+
() throws -> Bool in return s1 > s2
403+
})
404+
funcCall(closure: {
405+
() throws
406+
-> AVeryLongReturnTypeThatOverflowsFiftyColumns
407+
in return s1 > s2
408+
})
409+
410+
"""
411+
412+
assertPrettyPrintEqual(input: input, expected: expectedNotGroupingOutput, linelength: 50)
413+
414+
let expectedKeepingOutputTogether =
415+
"""
416+
funcCall(closure: {
417+
(
418+
s1: String, s2: String, s3: String
419+
) throws -> Bool in return s1 > s2
420+
})
421+
funcCall(closure: {
422+
(
423+
s1: String, s2: String, s3: String
424+
) throws
425+
-> AVeryLongReturnTypeThatOverflowsFiftyColumns
426+
in return s1 > s2
427+
})
428+
funcCall(closure: {
429+
() throws -> Bool in return s1 > s2
430+
})
431+
funcCall(closure: {
432+
() throws
433+
-> AVeryLongReturnTypeThatOverflowsFiftyColumns
434+
in return s1 > s2
435+
})
436+
437+
"""
438+
439+
var config = Configuration()
440+
config.prioritizeKeepingFunctionOutputTogether = true
441+
assertPrettyPrintEqual(
442+
input: input, expected: expectedKeepingOutputTogether, linelength: 50, configuration: config)
443+
}
348444
}

0 commit comments

Comments
 (0)