Skip to content

Commit 8c947dc

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 0f622c8 commit 8c947dc

File tree

4 files changed

+293
-56
lines changed

4 files changed

+293
-56
lines changed

Release Notes/602.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# Swift Syntax 602 Release Notes
2+
3+
## API-Incompatible Changes
4+
5+
- `ExpandEditorPlaceholdersToTrailingClosures` has changed to `ExpandEditorPlaceholdersToLiteralClosures`
6+
- Description: Whether function-typed placeholders are expanded to trailing closures is now configurable using a `format` argument to this rewriter. This improves code completion in a SourceKitLSP session where the trailing form may be undesirable. Additionally clients that support nested placeholders may request that the entire expanded closure be wrapped in an outer placeholder, e.g. `<#{ <#foo#> in <#Bar#> }#>`).
7+
- Pull Request: https://github.com/swiftlang/swift-syntax/pull/2897

Sources/SwiftRefactor/ExpandEditorPlaceholder.swift

Lines changed: 124 additions & 40 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,30 @@ 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 nested placeholders allowed
90+
/// ### Before
91+
/// ```swift
92+
/// <#T##(Int) -> String##(Int) -> String##(_ someInt: Int) -> String#>
93+
/// ```
94+
///
95+
/// ### After
96+
/// ```swift
97+
/// <#{ someInt in <#String#> }#>
98+
/// ```
99+
let allowNestedPlaceholders: Bool
100+
101+
init(
102+
closureLiteralFormat: BasicFormat = BasicFormat(),
103+
allowNestedPlaceholders: Bool = false
104+
) {
105+
self.closureLiteralFormat = closureLiteralFormat
106+
self.allowNestedPlaceholders = allowNestedPlaceholders
87107
}
88108
}
89109

@@ -94,16 +114,17 @@ struct ExpandSingleEditorPlaceholder: EditRefactoringProvider {
94114

95115
let expanded: String
96116
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
117+
let format = context.closureLiteralFormat
118+
let initialIndentation = format.currentIndentationLevel
119+
var formattedExpansion = functionType.closureExpansion.formatted(using: format).description
102120
// Strip the initial indentation from the placeholder itself. We only introduced the initial indentation to
103121
// format consecutive lines. We don't want it at the front of the initial line because it replaces an expression
104122
// 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))
123+
if formattedExpansion.hasPrefix(initialIndentation.description) {
124+
formattedExpansion = String(formattedExpansion.dropFirst(initialIndentation.description.count))
125+
}
126+
if context.allowNestedPlaceholders {
127+
formattedExpansion = wrapInPlaceholder(formattedExpansion)
107128
}
108129
expanded = formattedExpansion
109130
} else {
@@ -161,20 +182,24 @@ public struct ExpandEditorPlaceholder: EditRefactoringProvider {
161182
let arg = placeholder.parent?.as(LabeledExprSyntax.self),
162183
let argList = arg.parent?.as(LabeledExprListSyntax.self),
163184
let call = argList.parent?.as(FunctionCallExprSyntax.self),
164-
let expandedTrailingClosures = ExpandEditorPlaceholdersToTrailingClosures.expandTrailingClosurePlaceholders(
185+
let expandedClosures = ExpandEditorPlaceholdersToLiteralClosures.expandClosurePlaceholders(
165186
in: call,
166187
ifIncluded: arg,
167-
indentationWidth: context.indentationWidth
188+
context: ExpandEditorPlaceholdersToLiteralClosures.Context(
189+
format: .trailing(indentationWidth: context.indentationWidth)
190+
)
168191
)
169192
else {
170193
return ExpandSingleEditorPlaceholder.textRefactor(syntax: token)
171194
}
172195

173-
return [SourceEdit.replace(call, with: expandedTrailingClosures.description)]
196+
return [SourceEdit.replace(call, with: expandedClosures.description)]
174197
}
175198
}
176199

177-
/// Expand all the editor placeholders in the function call that can be converted to trailing closures.
200+
/// Expand all the editor placeholders in the function call to literal closures.
201+
/// By default they will be expanded to trailing form; if you provide your own
202+
/// formatter via ``Context/format`` they will be expanded inline.
178203
///
179204
/// ## Before
180205
/// ```swift
@@ -185,7 +210,7 @@ public struct ExpandEditorPlaceholder: EditRefactoringProvider {
185210
/// )
186211
/// ```
187212
///
188-
/// ## Expansion of `foo`
213+
/// ## Expansion of `foo`, default behavior
189214
/// ```swift
190215
/// foo(
191216
/// arg: <#T##Int#>,
@@ -195,45 +220,98 @@ public struct ExpandEditorPlaceholder: EditRefactoringProvider {
195220
/// <#T##String#>
196221
/// }
197222
/// ```
198-
public struct ExpandEditorPlaceholdersToTrailingClosures: SyntaxRefactoringProvider {
223+
///
224+
/// ## Expansion of `foo` with a basic custom formatter
225+
/// ```swift
226+
/// foo(
227+
/// arg: <#T##Int#>,
228+
/// firstClosure: { someInt in
229+
/// <#T##String#>
230+
/// },
231+
/// secondClosure: { someInt in
232+
/// <#T##String#>
233+
/// }
234+
/// )
235+
/// ```
236+
///
237+
/// ## Expansion of `foo`, custom formatter with `allowNestedPlaceholders: true`
238+
/// ```swift
239+
/// foo(
240+
/// arg: <#T##Int#>,
241+
/// firstClosure: <#{ someInt in
242+
/// <#T##String#>
243+
/// }#>,
244+
/// secondClosure: <#{ someInt in
245+
/// <#T##String#>
246+
/// }#>
247+
/// )
248+
/// ```
249+
public struct ExpandEditorPlaceholdersToLiteralClosures: SyntaxRefactoringProvider {
199250
public struct Context {
200-
public let indentationWidth: Trivia?
251+
public enum Format {
252+
/// Default formatting behavior: expand to trailing closures.
253+
case trailing(indentationWidth: Trivia?)
254+
/// Use the given formatter and expand the placeholder inline, without
255+
/// moving it to trailing position. If `allowNestedPlaceholders` is true,
256+
/// the entire closure will also be wrapped as a placeholder.
257+
case custom(BasicFormat, allowNestedPlaceholders: Bool)
258+
}
259+
public let format: Format
260+
261+
public init(format: Format) {
262+
self.format = format
263+
}
201264

202265
public init(indentationWidth: Trivia? = nil) {
203-
self.indentationWidth = indentationWidth
266+
self.init(format: .trailing(indentationWidth: indentationWidth))
204267
}
205268
}
206269

207270
public static func refactor(
208271
syntax call: FunctionCallExprSyntax,
209272
in context: Context = Context()
210273
) -> FunctionCallExprSyntax? {
211-
return Self.expandTrailingClosurePlaceholders(in: call, ifIncluded: nil, indentationWidth: context.indentationWidth)
274+
return Self.expandClosurePlaceholders(
275+
in: call,
276+
ifIncluded: nil,
277+
context: context
278+
)
212279
}
213280

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

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-
}
300+
case let .trailing(indentationWidth):
301+
guard let expanded = call.expandClosurePlaceholders(ifIncluded: arg, indentationWidth: indentationWidth)
302+
else {
303+
return nil
304+
}
235305

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

@@ -311,9 +389,11 @@ extension FunctionCallExprSyntax {
311389
/// closure, then return a replacement of this call with one that uses
312390
/// closures based on the function types provided by each editor placeholder.
313391
/// Otherwise return nil.
314-
fileprivate func expandTrailingClosurePlaceholders(
392+
fileprivate func expandClosurePlaceholders(
315393
ifIncluded: LabeledExprSyntax?,
316-
indentationWidth: Trivia?
394+
indentationWidth: Trivia? = nil,
395+
customFormat: BasicFormat? = nil,
396+
allowNestedPlaceholders: Bool = false
317397
) -> (expr: FunctionCallExprSyntax, numClosures: Int)? {
318398
var includedArg = false
319399
var argsToExpand = 0
@@ -343,8 +423,12 @@ extension FunctionCallExprSyntax {
343423
let edits = ExpandSingleEditorPlaceholder.textRefactor(
344424
syntax: arg.expression.cast(DeclReferenceExprSyntax.self).baseName,
345425
in: ExpandSingleEditorPlaceholder.Context(
346-
indentationWidth: indentationWidth,
347-
initialIndentation: lineIndentation
426+
closureLiteralFormat: customFormat
427+
?? BasicFormat(
428+
indentationWidth: indentationWidth,
429+
initialIndentation: lineIndentation
430+
),
431+
allowNestedPlaceholders: allowNestedPlaceholders
348432
)
349433
)
350434
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)