Skip to content

Commit 130a224

Browse files
committed
Allow custom format in closure placeholder expansion
This addresses <swiftlang/sourcekit-lsp#1788>, refining code completion for closure placeholders. By default function-typed placeholders will continue to expand to multi-line trailing form. A caller, such as sourcekit-lsp, may now customize the behavior by passing its own formatter. It may additionally request that the closure itself be marked as a placeholder, with the argument and return type placeholders nested inside.
1 parent b721fa8 commit 130a224

File tree

3 files changed

+244
-55
lines changed

3 files changed

+244
-55
lines changed

Sources/SwiftRefactor/ExpandEditorPlaceholder.swift

Lines changed: 82 additions & 39 deletions
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,15 @@ 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+
let closureLiteralFormat: BasicFormat
84+
let allowNestedPlaceholders: Bool
85+
86+
init(
87+
closureLiteralFormat: BasicFormat = BasicFormat(),
88+
allowNestedPlaceholders: Bool = false
89+
) {
90+
self.closureLiteralFormat = closureLiteralFormat
91+
self.allowNestedPlaceholders = allowNestedPlaceholders
8792
}
8893
}
8994

@@ -94,16 +99,17 @@ struct ExpandSingleEditorPlaceholder: EditRefactoringProvider {
9499

95100
let expanded: String
96101
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
102+
let format = context.closureLiteralFormat
103+
let initialIndentation = format.currentIndentationLevel
104+
var formattedExpansion = functionType.closureExpansion.formatted(using: format).description
102105
// Strip the initial indentation from the placeholder itself. We only introduced the initial indentation to
103106
// format consecutive lines. We don't want it at the front of the initial line because it replaces an expression
104107
// 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))
108+
if formattedExpansion.hasPrefix(initialIndentation.description) {
109+
formattedExpansion = String(formattedExpansion.dropFirst(initialIndentation.description.count))
110+
}
111+
if context.allowNestedPlaceholders {
112+
formattedExpansion = wrapInPlaceholder(formattedExpansion)
107113
}
108114
expanded = formattedExpansion
109115
} else {
@@ -161,20 +167,24 @@ public struct ExpandEditorPlaceholder: EditRefactoringProvider {
161167
let arg = placeholder.parent?.as(LabeledExprSyntax.self),
162168
let argList = arg.parent?.as(LabeledExprListSyntax.self),
163169
let call = argList.parent?.as(FunctionCallExprSyntax.self),
164-
let expandedTrailingClosures = ExpandEditorPlaceholdersToTrailingClosures.expandTrailingClosurePlaceholders(
170+
let expandedClosures = ExpandEditorPlaceholdersToLiteralClosures.expandClosurePlaceholders(
165171
in: call,
166172
ifIncluded: arg,
167-
indentationWidth: context.indentationWidth
173+
context: ExpandEditorPlaceholdersToLiteralClosures.Context(
174+
format: .trailing(indentationWidth: context.indentationWidth)
175+
)
168176
)
169177
else {
170178
return ExpandSingleEditorPlaceholder.textRefactor(syntax: token)
171179
}
172180

173-
return [SourceEdit.replace(call, with: expandedTrailingClosures.description)]
181+
return [SourceEdit.replace(call, with: expandedClosures.description)]
174182
}
175183
}
176184

177-
/// Expand all the editor placeholders in the function call that can be converted to trailing closures.
185+
/// Expand all the editor placeholders in the function call to literal closures.
186+
/// By default they will be expanded to trailing form; if you provide your own
187+
/// formatter via ``Context/format`` they will be expanded inline.
178188
///
179189
/// ## Before
180190
/// ```swift
@@ -195,45 +205,72 @@ public struct ExpandEditorPlaceholder: EditRefactoringProvider {
195205
/// <#T##String#>
196206
/// }
197207
/// ```
198-
public struct ExpandEditorPlaceholdersToTrailingClosures: SyntaxRefactoringProvider {
208+
public struct ExpandEditorPlaceholdersToLiteralClosures: SyntaxRefactoringProvider {
199209
public struct Context {
200-
public let indentationWidth: Trivia?
210+
public enum Format {
211+
/// Default formatting behavior: expand to trailing closures.
212+
case trailing(indentationWidth: Trivia?)
213+
/// Use the given formatter and expand the placeholder inline, without
214+
/// moving it to trailing position. If `allowNestedPlaceholders` is true,
215+
/// the entire closure will also be wrapped as a placeholder.
216+
case custom(BasicFormat, allowNestedPlaceholders: Bool)
217+
}
218+
public let format: Format
219+
220+
public init(format: Format) {
221+
self.format = format
222+
}
201223

202224
public init(indentationWidth: Trivia? = nil) {
203-
self.indentationWidth = indentationWidth
225+
self.init(format: .trailing(indentationWidth: indentationWidth))
204226
}
205227
}
206228

207229
public static func refactor(
208230
syntax call: FunctionCallExprSyntax,
209231
in context: Context = Context()
210232
) -> FunctionCallExprSyntax? {
211-
return Self.expandTrailingClosurePlaceholders(in: call, ifIncluded: nil, indentationWidth: context.indentationWidth)
233+
return Self.expandClosurePlaceholders(
234+
in: call,
235+
ifIncluded: nil,
236+
context: context
237+
)
212238
}
213239

214240
/// If the given argument is `nil` or one of the last arguments that are all
215241
/// function-typed placeholders and this call doesn't have a trailing
216242
/// closure, then return a replacement of this call with one that uses
217243
/// closures based on the function types provided by each editor placeholder.
218244
/// Otherwise return nil.
219-
fileprivate static func expandTrailingClosurePlaceholders(
245+
fileprivate static func expandClosurePlaceholders(
220246
in call: FunctionCallExprSyntax,
221247
ifIncluded arg: LabeledExprSyntax?,
222-
indentationWidth: Trivia?
248+
context: Context
223249
) -> FunctionCallExprSyntax? {
224-
guard let expanded = call.expandTrailingClosurePlaceholders(ifIncluded: arg, indentationWidth: indentationWidth)
225-
else {
226-
return nil
227-
}
250+
switch context.format {
251+
case let .custom(formatter, allowNestedPlaceholders: allowNesting):
252+
let expanded = call.expandClosurePlaceholders(
253+
ifIncluded: arg,
254+
customFormat: formatter,
255+
allowNestedPlaceholders: allowNesting
256+
)
257+
return expanded?.expr
228258

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-
}
259+
case let .trailing(indentationWidth):
260+
guard let expanded = call.expandClosurePlaceholders(ifIncluded: arg, indentationWidth: indentationWidth)
261+
else {
262+
return nil
263+
}
264+
265+
let callToTrailingContext = CallToTrailingClosures.Context(
266+
startAtArgument: call.arguments.count - expanded.numClosures
267+
)
268+
guard let trailing = CallToTrailingClosures.refactor(syntax: expanded.expr, in: callToTrailingContext) else {
269+
return nil
270+
}
235271

236-
return trailing
272+
return trailing
273+
}
237274
}
238275
}
239276

@@ -311,9 +348,11 @@ extension FunctionCallExprSyntax {
311348
/// closure, then return a replacement of this call with one that uses
312349
/// closures based on the function types provided by each editor placeholder.
313350
/// Otherwise return nil.
314-
fileprivate func expandTrailingClosurePlaceholders(
351+
fileprivate func expandClosurePlaceholders(
315352
ifIncluded: LabeledExprSyntax?,
316-
indentationWidth: Trivia?
353+
indentationWidth: Trivia? = nil,
354+
customFormat: BasicFormat? = nil,
355+
allowNestedPlaceholders: Bool = false
317356
) -> (expr: FunctionCallExprSyntax, numClosures: Int)? {
318357
var includedArg = false
319358
var argsToExpand = 0
@@ -343,8 +382,12 @@ extension FunctionCallExprSyntax {
343382
let edits = ExpandSingleEditorPlaceholder.textRefactor(
344383
syntax: arg.expression.cast(DeclReferenceExprSyntax.self).baseName,
345384
in: ExpandSingleEditorPlaceholder.Context(
346-
indentationWidth: indentationWidth,
347-
initialIndentation: lineIndentation
385+
closureLiteralFormat: customFormat
386+
?? BasicFormat(
387+
indentationWidth: indentationWidth,
388+
initialIndentation: lineIndentation
389+
),
390+
allowNestedPlaceholders: allowNestedPlaceholders
348391
)
349392
)
350393
guard edits.count == 1, let edit = edits.first, !edit.replacement.isEmpty else {

Sources/SwiftSyntax/SyntaxProtocol.swift

Lines changed: 4 additions & 1 deletion
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)