Skip to content

Commit 507f323

Browse files
authored
Merge pull request #2897 from woolsweater/the-trail-is-closed
Allow customization of expansion for function-typed placeholders
2 parents d600d5c + 2c96735 commit 507f323

File tree

4 files changed

+310
-56
lines changed

4 files changed

+310
-56
lines changed

Release Notes/602.md

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# Swift Syntax 602 Release Notes
2+
3+
## New APIs
4+
5+
## API Behavior Changes
6+
7+
## Deprecations
8+
9+
## API-Incompatible Changes
10+
11+
- `ExpandEditorPlaceholdersToTrailingClosures` has changed to `ExpandEditorPlaceholdersToLiteralClosures`
12+
- Description: Whether function-typed placeholders are expanded to trailing closures is now configurable using a `format` argument to this rewriter. Additionally clients that support nested placeholders may request that the entire expanded closure be wrapped in an outer placeholder, e.g. `<#{ <#foo#> in <#Bar#> }#>`.
13+
- Pull Request: https://github.com/swiftlang/swift-syntax/pull/2897
14+
- Migration steps: Replace uses of `ExpandEditorPlaceholdersToTrailingClosures` with `ExpandEditorPlaceholdersToLiteralClosures`. The initializer does not need to change: `.init(indentationWidth:)` on the new type provides the same behavior as the old type.
15+
- Notes: This improves code completion in a SourceKitLSP session where the trailing closure form may be undesirable. The nested placeholders offer more flexibility to end users, in editors that support it.
16+
17+
## Template
18+
19+
- *Affected API or two word description*
20+
- Description: *A 1-2 sentence description of the new/modified API*
21+
- Issue: *If an issue exists for this change, a link to the issue*
22+
- Pull Request: *Link to the pull request(s) that introduces this change*
23+
- Migration steps: Steps that adopters of swift-syntax should take to move to the new API (required for deprecations and API-incompatible changes).
24+
- Notes: *In case of deprecations or API-incompatible changes, the reason why this change was made and the suggested alternative*
25+
26+
*Insert entries in chronological order, with newer entries at the bottom*

Sources/SwiftRefactor/ExpandEditorPlaceholder.swift

+123-40
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,9 @@ import SwiftSyntaxBuilder
4040
/// `type-for-expansion-string`), is parsed into a syntax node. If that node is
4141
/// a `FunctionTypeSyntax` then the placeholder is expanded into a
4242
/// `ClosureExprSyntax`. Otherwise it is expanded as is, which is also the case
43-
/// for when only a display string is provided.
43+
/// for when only a display string is provided. You may customize the formatting
44+
/// of a closure expansion via ``Context/closureLiteralFormat``, for example to
45+
/// change whether it is split onto multiple lines.
4446
///
4547
/// ## Function Typed Placeholder
4648
/// ### Before
@@ -78,12 +80,29 @@ import SwiftSyntaxBuilder
7880
/// ```
7981
struct ExpandSingleEditorPlaceholder: EditRefactoringProvider {
8082
struct Context {
81-
let indentationWidth: Trivia?
82-
let initialIndentation: Trivia
83-
84-
init(indentationWidth: Trivia? = nil, initialIndentation: Trivia = []) {
85-
self.indentationWidth = indentationWidth
86-
self.initialIndentation = initialIndentation
83+
/// The formatter to use when expanding a function-typed placeholder.
84+
let closureLiteralFormat: BasicFormat
85+
/// When true, the expansion will wrap a function-typed placeholder's entire
86+
/// expansion in placeholder delimiters, in addition to any placeholders
87+
/// inside the expanded closure literal.
88+
///
89+
/// With `allowNestedPlaceholders = false`
90+
/// ```swift
91+
/// { someInt in <#String#> }
92+
/// ```
93+
///
94+
/// With `allowNestedPlaceholders = true`
95+
/// ```swift
96+
/// <#{ someInt in <#String#> }#>
97+
/// ```
98+
let allowNestedPlaceholders: Bool
99+
100+
init(
101+
closureLiteralFormat: BasicFormat = BasicFormat(),
102+
allowNestedPlaceholders: Bool = false
103+
) {
104+
self.closureLiteralFormat = closureLiteralFormat
105+
self.allowNestedPlaceholders = allowNestedPlaceholders
87106
}
88107
}
89108

@@ -94,16 +113,17 @@ struct ExpandSingleEditorPlaceholder: EditRefactoringProvider {
94113

95114
let expanded: String
96115
if let functionType = placeholder.typeForExpansion?.as(FunctionTypeSyntax.self) {
97-
let basicFormat = BasicFormat(
98-
indentationWidth: context.indentationWidth,
99-
initialIndentation: context.initialIndentation
100-
)
101-
var formattedExpansion = functionType.closureExpansion.formatted(using: basicFormat).description
116+
let format = context.closureLiteralFormat
117+
let initialIndentation = format.currentIndentationLevel
118+
var formattedExpansion = functionType.closureExpansion.formatted(using: format).description
102119
// Strip the initial indentation from the placeholder itself. We only introduced the initial indentation to
103120
// format consecutive lines. We don't want it at the front of the initial line because it replaces an expression
104121
// that might be in the middle of a line.
105-
if formattedExpansion.hasPrefix(context.initialIndentation.description) {
106-
formattedExpansion = String(formattedExpansion.dropFirst(context.initialIndentation.description.count))
122+
if formattedExpansion.hasPrefix(initialIndentation.description) {
123+
formattedExpansion = String(formattedExpansion.dropFirst(initialIndentation.description.count))
124+
}
125+
if context.allowNestedPlaceholders {
126+
formattedExpansion = wrapInPlaceholder(formattedExpansion)
107127
}
108128
expanded = formattedExpansion
109129
} else {
@@ -161,20 +181,24 @@ public struct ExpandEditorPlaceholder: EditRefactoringProvider {
161181
let arg = placeholder.parent?.as(LabeledExprSyntax.self),
162182
let argList = arg.parent?.as(LabeledExprListSyntax.self),
163183
let call = argList.parent?.as(FunctionCallExprSyntax.self),
164-
let expandedTrailingClosures = ExpandEditorPlaceholdersToTrailingClosures.expandTrailingClosurePlaceholders(
184+
let expandedClosures = ExpandEditorPlaceholdersToLiteralClosures.expandClosurePlaceholders(
165185
in: call,
166186
ifIncluded: arg,
167-
indentationWidth: context.indentationWidth
187+
context: ExpandEditorPlaceholdersToLiteralClosures.Context(
188+
format: .trailing(indentationWidth: context.indentationWidth)
189+
)
168190
)
169191
else {
170192
return ExpandSingleEditorPlaceholder.textRefactor(syntax: token)
171193
}
172194

173-
return [SourceEdit.replace(call, with: expandedTrailingClosures.description)]
195+
return [SourceEdit.replace(call, with: expandedClosures.description)]
174196
}
175197
}
176198

177-
/// Expand all the editor placeholders in the function call that can be converted to trailing closures.
199+
/// Expand all the editor placeholders in the function call to literal closures.
200+
/// By default they will be expanded to trailing form; if you provide your own
201+
/// formatter via ``Context/format`` they will be expanded inline.
178202
///
179203
/// ## Before
180204
/// ```swift
@@ -185,7 +209,7 @@ public struct ExpandEditorPlaceholder: EditRefactoringProvider {
185209
/// )
186210
/// ```
187211
///
188-
/// ## Expansion of `foo`
212+
/// ## Expansion of `foo`, default behavior
189213
/// ```swift
190214
/// foo(
191215
/// arg: <#T##Int#>,
@@ -195,45 +219,98 @@ public struct ExpandEditorPlaceholder: EditRefactoringProvider {
195219
/// <#T##String#>
196220
/// }
197221
/// ```
198-
public struct ExpandEditorPlaceholdersToTrailingClosures: SyntaxRefactoringProvider {
222+
///
223+
/// ## Expansion of `foo` with a basic custom formatter
224+
/// ```swift
225+
/// foo(
226+
/// arg: <#T##Int#>,
227+
/// firstClosure: { someInt in
228+
/// <#T##String#>
229+
/// },
230+
/// secondClosure: { someInt in
231+
/// <#T##String#>
232+
/// }
233+
/// )
234+
/// ```
235+
///
236+
/// ## Expansion of `foo`, custom formatter with `allowNestedPlaceholders: true`
237+
/// ```swift
238+
/// foo(
239+
/// arg: <#T##Int#>,
240+
/// firstClosure: <#{ someInt in
241+
/// <#T##String#>
242+
/// }#>,
243+
/// secondClosure: <#{ someInt in
244+
/// <#T##String#>
245+
/// }#>
246+
/// )
247+
/// ```
248+
public struct ExpandEditorPlaceholdersToLiteralClosures: SyntaxRefactoringProvider {
199249
public struct Context {
200-
public let indentationWidth: Trivia?
250+
public enum Format {
251+
/// Default formatting behavior: expand to trailing closures.
252+
case trailing(indentationWidth: Trivia?)
253+
/// Use the given formatter and expand the placeholder inline, without
254+
/// moving it to trailing position. If `allowNestedPlaceholders` is true,
255+
/// the entire closure will also be wrapped as a placeholder.
256+
case custom(BasicFormat, allowNestedPlaceholders: Bool)
257+
}
258+
public let format: Format
259+
260+
public init(format: Format) {
261+
self.format = format
262+
}
201263

202264
public init(indentationWidth: Trivia? = nil) {
203-
self.indentationWidth = indentationWidth
265+
self.init(format: .trailing(indentationWidth: indentationWidth))
204266
}
205267
}
206268

207269
public static func refactor(
208270
syntax call: FunctionCallExprSyntax,
209271
in context: Context = Context()
210272
) -> FunctionCallExprSyntax? {
211-
return Self.expandTrailingClosurePlaceholders(in: call, ifIncluded: nil, indentationWidth: context.indentationWidth)
273+
return Self.expandClosurePlaceholders(
274+
in: call,
275+
ifIncluded: nil,
276+
context: context
277+
)
212278
}
213279

214280
/// If the given argument is `nil` or one of the last arguments that are all
215281
/// function-typed placeholders and this call doesn't have a trailing
216282
/// closure, then return a replacement of this call with one that uses
217283
/// closures based on the function types provided by each editor placeholder.
218284
/// Otherwise return nil.
219-
fileprivate static func expandTrailingClosurePlaceholders(
285+
fileprivate static func expandClosurePlaceholders(
220286
in call: FunctionCallExprSyntax,
221287
ifIncluded arg: LabeledExprSyntax?,
222-
indentationWidth: Trivia?
288+
context: Context
223289
) -> FunctionCallExprSyntax? {
224-
guard let expanded = call.expandTrailingClosurePlaceholders(ifIncluded: arg, indentationWidth: indentationWidth)
225-
else {
226-
return nil
227-
}
290+
switch context.format {
291+
case let .custom(formatter, allowNestedPlaceholders: allowNesting):
292+
let expanded = call.expandClosurePlaceholders(
293+
ifIncluded: arg,
294+
customFormat: formatter,
295+
allowNestedPlaceholders: allowNesting
296+
)
297+
return expanded?.expr
228298

229-
let callToTrailingContext = CallToTrailingClosures.Context(
230-
startAtArgument: call.arguments.count - expanded.numClosures
231-
)
232-
guard let trailing = CallToTrailingClosures.refactor(syntax: expanded.expr, in: callToTrailingContext) else {
233-
return nil
234-
}
299+
case let .trailing(indentationWidth):
300+
guard let expanded = call.expandClosurePlaceholders(ifIncluded: arg, indentationWidth: indentationWidth)
301+
else {
302+
return nil
303+
}
235304

236-
return trailing
305+
let callToTrailingContext = CallToTrailingClosures.Context(
306+
startAtArgument: call.arguments.count - expanded.numClosures
307+
)
308+
guard let trailing = CallToTrailingClosures.refactor(syntax: expanded.expr, in: callToTrailingContext) else {
309+
return nil
310+
}
311+
312+
return trailing
313+
}
237314
}
238315
}
239316

@@ -311,9 +388,11 @@ extension FunctionCallExprSyntax {
311388
/// closure, then return a replacement of this call with one that uses
312389
/// closures based on the function types provided by each editor placeholder.
313390
/// Otherwise return nil.
314-
fileprivate func expandTrailingClosurePlaceholders(
391+
fileprivate func expandClosurePlaceholders(
315392
ifIncluded: LabeledExprSyntax?,
316-
indentationWidth: Trivia?
393+
indentationWidth: Trivia? = nil,
394+
customFormat: BasicFormat? = nil,
395+
allowNestedPlaceholders: Bool = false
317396
) -> (expr: FunctionCallExprSyntax, numClosures: Int)? {
318397
var includedArg = false
319398
var argsToExpand = 0
@@ -343,8 +422,12 @@ extension FunctionCallExprSyntax {
343422
let edits = ExpandSingleEditorPlaceholder.textRefactor(
344423
syntax: arg.expression.cast(DeclReferenceExprSyntax.self).baseName,
345424
in: ExpandSingleEditorPlaceholder.Context(
346-
indentationWidth: indentationWidth,
347-
initialIndentation: lineIndentation
425+
closureLiteralFormat: customFormat
426+
?? BasicFormat(
427+
indentationWidth: indentationWidth,
428+
initialIndentation: lineIndentation
429+
),
430+
allowNestedPlaceholders: allowNestedPlaceholders
348431
)
349432
)
350433
guard edits.count == 1, let edit = edits.first, !edit.replacement.isEmpty else {

Sources/SwiftSyntax/SyntaxProtocol.swift

+4-1
Original file line numberDiff line numberDiff line change
@@ -235,7 +235,10 @@ extension SyntaxProtocol {
235235
return self.previousToken(viewMode: .sourceAccurate)
236236
}
237237

238-
/// Returns this node or the first ancestor that satisfies `condition`.
238+
/// Applies `map` to this node and each of its ancestors until a non-`nil`
239+
/// value is produced, then returns that value.
240+
///
241+
/// If no node has a non-`nil` mapping, returns `nil`.
239242
public func ancestorOrSelf<T>(mapping map: (Syntax) -> T?) -> T? {
240243
return self.withUnownedSyntax {
241244
var node = $0

0 commit comments

Comments
 (0)