Skip to content

Commit 082f85a

Browse files
committed
Add a refactoring to expand all editor placeholders that can be converted to trailing closures
This can be used by sourcekit-lsp to expand editor placeholders to trailing closures in code completion items.
1 parent 6ee1a4e commit 082f85a

File tree

3 files changed

+273
-187
lines changed

3 files changed

+273
-187
lines changed

Sources/SwiftRefactor/ExpandEditorPlaceholder.swift

Lines changed: 75 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -69,20 +69,8 @@ import SwiftSyntaxBuilder
6969
/// ```swift
7070
/// anything here
7171
/// ```
72-
public struct ExpandEditorPlaceholder: EditRefactoringProvider {
73-
public static func isPlaceholder(_ str: String) -> Bool {
74-
return str.hasPrefix(placeholderStart) && str.hasSuffix(placeholderEnd)
75-
}
76-
77-
public static func wrapInPlaceholder(_ str: String) -> String {
78-
return placeholderStart + str + placeholderEnd
79-
}
80-
81-
public static func wrapInTypePlaceholder(_ str: String, type: String) -> String {
82-
return Self.wrapInPlaceholder("T##" + str + "##" + type)
83-
}
84-
85-
public static func textRefactor(syntax token: TokenSyntax, in context: Void) -> [SourceEdit] {
72+
struct ExpandSingleEditorPlaceholder: EditRefactoringProvider {
73+
static func textRefactor(syntax token: TokenSyntax, in context: Void) -> [SourceEdit] {
8674
guard let placeholder = EditorPlaceholderData(token: token) else {
8775
return []
8876
}
@@ -132,28 +120,68 @@ public struct ExpandEditorPlaceholder: EditRefactoringProvider {
132120
/// }
133121
/// ```
134122
///
135-
/// Expansion on `closure1` and `normalArg` is the same as `ExpandEditorPlaceholder`.
136-
public struct ExpandEditorPlaceholders: EditRefactoringProvider {
123+
/// Expansion on `closure1` and `normalArg` is the same as `ExpandSingleEditorPlaceholder`.
124+
public struct ExpandEditorPlaceholder: EditRefactoringProvider {
137125
public static func textRefactor(syntax token: TokenSyntax, in context: Void) -> [SourceEdit] {
138126
guard let placeholder = token.parent?.as(DeclReferenceExprSyntax.self),
139127
placeholder.baseName.isEditorPlaceholder,
140128
let arg = placeholder.parent?.as(LabeledExprSyntax.self),
141129
let argList = arg.parent?.as(LabeledExprListSyntax.self),
142-
let call = argList.parent?.as(FunctionCallExprSyntax.self)
130+
let call = argList.parent?.as(FunctionCallExprSyntax.self),
131+
let expandedTrailingClosures = ExpandEditorPlaceholdersToTrailingClosures.expandTrailingClosurePlaceholders(in: call, ifIncluded: arg)
143132
else {
144-
return ExpandEditorPlaceholder.textRefactor(syntax: token)
133+
return ExpandSingleEditorPlaceholder.textRefactor(syntax: token)
145134
}
146135

136+
return [SourceEdit.replace(call, with: expandedTrailingClosures.description)]
137+
}
138+
}
139+
140+
/// Expand all the editor placeholders in the function call that can be converted to trailing closures.
141+
///
142+
/// ## Before
143+
/// ```swift
144+
/// foo(
145+
/// arg: <#T##Int#>,
146+
/// firstClosure: <#T##(Int) -> String##(Int) -> String##(_ someInt: Int) -> String#>,
147+
/// secondClosure: <#T##(Int) -> String##(Int) -> String##(_ someInt: Int) -> String#>
148+
/// )
149+
/// ```
150+
///
151+
/// ## Expansion of `foo`
152+
/// ```swift
153+
/// foo(
154+
/// arg: <#T##Int#>,
155+
/// ) { someInt in
156+
/// <#T##String#>
157+
/// } secondClosure: { someInt in
158+
/// <#T##String#>
159+
/// }
160+
/// ```
161+
public struct ExpandEditorPlaceholdersToTrailingClosures: SyntaxRefactoringProvider {
162+
public static func refactor(syntax call: FunctionCallExprSyntax, in context: Void = ()) -> FunctionCallExprSyntax? {
163+
return Self.expandTrailingClosurePlaceholders(in: call, ifIncluded: nil)
164+
}
165+
166+
/// If the given argument is `nil` or one of the last arguments that are all
167+
/// function-typed placeholders and this call doesn't have a trailing
168+
/// closure, then return a replacement of this call with one that uses
169+
/// closures based on the function types provided by each editor placeholder.
170+
/// Otherwise return nil.
171+
fileprivate static func expandTrailingClosurePlaceholders(
172+
in call: FunctionCallExprSyntax,
173+
ifIncluded arg: LabeledExprSyntax?
174+
) -> FunctionCallExprSyntax? {
147175
guard let expanded = call.expandTrailingClosurePlaceholders(ifIncluded: arg) else {
148-
return ExpandEditorPlaceholder.textRefactor(syntax: token)
176+
return nil
149177
}
150178

151-
let callToTrailingContext = CallToTrailingClosures.Context(startAtArgument: argList.count - expanded.numClosures)
179+
let callToTrailingContext = CallToTrailingClosures.Context(startAtArgument: call.arguments.count - expanded.numClosures)
152180
guard let trailing = CallToTrailingClosures.refactor(syntax: expanded.expr, in: callToTrailingContext) else {
153-
return ExpandEditorPlaceholder.textRefactor(syntax: token)
181+
return nil
154182
}
155183

156-
return [SourceEdit.replace(call, with: trailing.description)]
184+
return trailing
157185
}
158186
}
159187

@@ -186,9 +214,9 @@ extension FunctionTypeSyntax {
186214
let ret = returnClause.type.description
187215
let placeholder: String
188216
if ret == "Void" || ret == "()" {
189-
placeholder = ExpandEditorPlaceholder.wrapInTypePlaceholder("code", type: "Void")
217+
placeholder = wrapInTypePlaceholder("code", type: "Void")
190218
} else {
191-
placeholder = ExpandEditorPlaceholder.wrapInTypePlaceholder(ret, type: ret)
219+
placeholder = wrapInTypePlaceholder(ret, type: ret)
192220
}
193221

194222
let statementPlaceholder = DeclReferenceExprSyntax(
@@ -221,17 +249,19 @@ extension TupleTypeElementSyntax {
221249
return firstName
222250
}
223251

224-
return .identifier(ExpandEditorPlaceholder.wrapInPlaceholder(type.description))
252+
return .identifier(wrapInPlaceholder(type.description))
225253
}
226254
}
227255

228256
extension FunctionCallExprSyntax {
229-
/// If the given argument is one of the last arguments that are all
257+
/// If the given argument is `nil` or one of the last arguments that are all
230258
/// function-typed placeholders and this call doesn't have a trailing
231259
/// closure, then return a replacement of this call with one that uses
232260
/// closures based on the function types provided by each editor placeholder.
233261
/// Otherwise return nil.
234-
fileprivate func expandTrailingClosurePlaceholders(ifIncluded: LabeledExprSyntax) -> (expr: FunctionCallExprSyntax, numClosures: Int)? {
262+
fileprivate func expandTrailingClosurePlaceholders(
263+
ifIncluded: LabeledExprSyntax?
264+
) -> (expr: FunctionCallExprSyntax, numClosures: Int)? {
235265
var includedArg = false
236266
var argsToExpand = 0
237267
for arg in arguments.reversed() {
@@ -249,13 +279,13 @@ extension FunctionCallExprSyntax {
249279
argsToExpand += 1
250280
}
251281

252-
guard includedArg else {
282+
guard includedArg || ifIncluded == nil else {
253283
return nil
254284
}
255285

256286
var expandedArgs = [LabeledExprSyntax]()
257287
for arg in arguments.suffix(argsToExpand) {
258-
let edits = ExpandEditorPlaceholder.textRefactor(syntax: arg.expression.cast(DeclReferenceExprSyntax.self).baseName)
288+
let edits = ExpandSingleEditorPlaceholder.textRefactor(syntax: arg.expression.cast(DeclReferenceExprSyntax.self).baseName)
259289
guard edits.count == 1, let edit = edits.first, !edit.replacement.isEmpty else {
260290
return nil
261291
}
@@ -291,7 +321,7 @@ fileprivate enum EditorPlaceholderData {
291321
case typed(text: Substring, type: TypeSyntax)
292322

293323
init?(token: TokenSyntax) {
294-
guard ExpandEditorPlaceholder.isPlaceholder(token.text) else {
324+
guard isPlaceholder(token.text) else {
295325
return nil
296326
}
297327

@@ -346,6 +376,21 @@ fileprivate enum EditorPlaceholderData {
346376
}
347377
}
348378

379+
@_spi(Testing)
380+
public func isPlaceholder(_ str: String) -> Bool {
381+
return str.hasPrefix(placeholderStart) && str.hasSuffix(placeholderEnd)
382+
}
383+
384+
@_spi(Testing)
385+
public func wrapInPlaceholder(_ str: String) -> String {
386+
return placeholderStart + str + placeholderEnd
387+
}
388+
389+
@_spi(Testing)
390+
public func wrapInTypePlaceholder(_ str: String, type: String) -> String {
391+
return wrapInPlaceholder("T##" + str + "##" + type)
392+
}
393+
349394
/// Split the given string into two components on the first instance of
350395
/// `separatedBy`. The second element is empty if `separatedBy` is missing
351396
/// from the initial string.

0 commit comments

Comments
 (0)