Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow customization of expansion for function-typed placeholders #2897

Merged
merged 1 commit into from
Dec 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions Release Notes/602.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Swift Syntax 602 Release Notes

## New APIs

## API Behavior Changes

## Deprecations

## API-Incompatible Changes

- `ExpandEditorPlaceholdersToTrailingClosures` has changed to `ExpandEditorPlaceholdersToLiteralClosures`
- 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#> }#>`.
- Pull Request: https://github.com/swiftlang/swift-syntax/pull/2897
- 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.
- 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.

## Template

- *Affected API or two word description*
- Description: *A 1-2 sentence description of the new/modified API*
- Issue: *If an issue exists for this change, a link to the issue*
- Pull Request: *Link to the pull request(s) that introduces this change*
- Migration steps: Steps that adopters of swift-syntax should take to move to the new API (required for deprecations and API-incompatible changes).
- Notes: *In case of deprecations or API-incompatible changes, the reason why this change was made and the suggested alternative*

*Insert entries in chronological order, with newer entries at the bottom*
163 changes: 123 additions & 40 deletions Sources/SwiftRefactor/ExpandEditorPlaceholder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,9 @@ import SwiftSyntaxBuilder
/// `type-for-expansion-string`), is parsed into a syntax node. If that node is
/// a `FunctionTypeSyntax` then the placeholder is expanded into a
/// `ClosureExprSyntax`. Otherwise it is expanded as is, which is also the case
/// for when only a display string is provided.
/// for when only a display string is provided. You may customize the formatting
/// of a closure expansion via ``Context/closureLiteralFormat``, for example to
/// change whether it is split onto multiple lines.
///
/// ## Function Typed Placeholder
/// ### Before
Expand Down Expand Up @@ -78,12 +80,29 @@ import SwiftSyntaxBuilder
/// ```
struct ExpandSingleEditorPlaceholder: EditRefactoringProvider {
struct Context {
let indentationWidth: Trivia?
let initialIndentation: Trivia

init(indentationWidth: Trivia? = nil, initialIndentation: Trivia = []) {
self.indentationWidth = indentationWidth
self.initialIndentation = initialIndentation
/// The formatter to use when expanding a function-typed placeholder.
let closureLiteralFormat: BasicFormat
/// When true, the expansion will wrap a function-typed placeholder's entire
/// expansion in placeholder delimiters, in addition to any placeholders
/// inside the expanded closure literal.
///
/// With `allowNestedPlaceholders = false`
/// ```swift
/// { someInt in <#String#> }
/// ```
///
/// With `allowNestedPlaceholders = true`
/// ```swift
/// <#{ someInt in <#String#> }#>
/// ```
let allowNestedPlaceholders: Bool
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you add a short doc comment what allowNestedPlaceholders does, ideally with a short example. I think that would be useful in the future.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Absolutely; done!


init(
closureLiteralFormat: BasicFormat = BasicFormat(),
allowNestedPlaceholders: Bool = false
) {
self.closureLiteralFormat = closureLiteralFormat
self.allowNestedPlaceholders = allowNestedPlaceholders
}
}

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

let expanded: String
if let functionType = placeholder.typeForExpansion?.as(FunctionTypeSyntax.self) {
let basicFormat = BasicFormat(
indentationWidth: context.indentationWidth,
initialIndentation: context.initialIndentation
)
var formattedExpansion = functionType.closureExpansion.formatted(using: basicFormat).description
let format = context.closureLiteralFormat
let initialIndentation = format.currentIndentationLevel
var formattedExpansion = functionType.closureExpansion.formatted(using: format).description
// Strip the initial indentation from the placeholder itself. We only introduced the initial indentation to
// format consecutive lines. We don't want it at the front of the initial line because it replaces an expression
// that might be in the middle of a line.
if formattedExpansion.hasPrefix(context.initialIndentation.description) {
formattedExpansion = String(formattedExpansion.dropFirst(context.initialIndentation.description.count))
if formattedExpansion.hasPrefix(initialIndentation.description) {
formattedExpansion = String(formattedExpansion.dropFirst(initialIndentation.description.count))
}
if context.allowNestedPlaceholders {
formattedExpansion = wrapInPlaceholder(formattedExpansion)
}
expanded = formattedExpansion
} else {
Expand Down Expand Up @@ -161,20 +181,24 @@ public struct ExpandEditorPlaceholder: EditRefactoringProvider {
let arg = placeholder.parent?.as(LabeledExprSyntax.self),
let argList = arg.parent?.as(LabeledExprListSyntax.self),
let call = argList.parent?.as(FunctionCallExprSyntax.self),
let expandedTrailingClosures = ExpandEditorPlaceholdersToTrailingClosures.expandTrailingClosurePlaceholders(
let expandedClosures = ExpandEditorPlaceholdersToLiteralClosures.expandClosurePlaceholders(
in: call,
ifIncluded: arg,
indentationWidth: context.indentationWidth
context: ExpandEditorPlaceholdersToLiteralClosures.Context(
format: .trailing(indentationWidth: context.indentationWidth)
)
)
else {
return ExpandSingleEditorPlaceholder.textRefactor(syntax: token)
}

return [SourceEdit.replace(call, with: expandedTrailingClosures.description)]
return [SourceEdit.replace(call, with: expandedClosures.description)]
}
}

/// Expand all the editor placeholders in the function call that can be converted to trailing closures.
/// Expand all the editor placeholders in the function call to literal closures.
/// By default they will be expanded to trailing form; if you provide your own
/// formatter via ``Context/format`` they will be expanded inline.
///
/// ## Before
/// ```swift
Expand All @@ -185,7 +209,7 @@ public struct ExpandEditorPlaceholder: EditRefactoringProvider {
/// )
/// ```
///
/// ## Expansion of `foo`
/// ## Expansion of `foo`, default behavior
/// ```swift
/// foo(
/// arg: <#T##Int#>,
Expand All @@ -195,45 +219,98 @@ public struct ExpandEditorPlaceholder: EditRefactoringProvider {
/// <#T##String#>
/// }
/// ```
public struct ExpandEditorPlaceholdersToTrailingClosures: SyntaxRefactoringProvider {
///
/// ## Expansion of `foo` with a basic custom formatter
/// ```swift
/// foo(
/// arg: <#T##Int#>,
/// firstClosure: { someInt in
/// <#T##String#>
/// },
/// secondClosure: { someInt in
/// <#T##String#>
/// }
/// )
/// ```
///
/// ## Expansion of `foo`, custom formatter with `allowNestedPlaceholders: true`
/// ```swift
/// foo(
/// arg: <#T##Int#>,
/// firstClosure: <#{ someInt in
/// <#T##String#>
/// }#>,
/// secondClosure: <#{ someInt in
/// <#T##String#>
/// }#>
/// )
/// ```
public struct ExpandEditorPlaceholdersToLiteralClosures: SyntaxRefactoringProvider {
public struct Context {
public let indentationWidth: Trivia?
public enum Format {
/// Default formatting behavior: expand to trailing closures.
case trailing(indentationWidth: Trivia?)
/// Use the given formatter and expand the placeholder inline, without
/// moving it to trailing position. If `allowNestedPlaceholders` is true,
/// the entire closure will also be wrapped as a placeholder.
case custom(BasicFormat, allowNestedPlaceholders: Bool)
}
public let format: Format

public init(format: Format) {
self.format = format
}

public init(indentationWidth: Trivia? = nil) {
self.indentationWidth = indentationWidth
self.init(format: .trailing(indentationWidth: indentationWidth))
}
}

public static func refactor(
syntax call: FunctionCallExprSyntax,
in context: Context = Context()
) -> FunctionCallExprSyntax? {
return Self.expandTrailingClosurePlaceholders(in: call, ifIncluded: nil, indentationWidth: context.indentationWidth)
return Self.expandClosurePlaceholders(
in: call,
ifIncluded: nil,
context: context
)
}

/// If the given argument is `nil` or one of the last arguments that are all
/// function-typed placeholders and this call doesn't have a trailing
/// closure, then return a replacement of this call with one that uses
/// closures based on the function types provided by each editor placeholder.
/// Otherwise return nil.
fileprivate static func expandTrailingClosurePlaceholders(
fileprivate static func expandClosurePlaceholders(
in call: FunctionCallExprSyntax,
ifIncluded arg: LabeledExprSyntax?,
indentationWidth: Trivia?
context: Context
) -> FunctionCallExprSyntax? {
guard let expanded = call.expandTrailingClosurePlaceholders(ifIncluded: arg, indentationWidth: indentationWidth)
else {
return nil
}
switch context.format {
case let .custom(formatter, allowNestedPlaceholders: allowNesting):
let expanded = call.expandClosurePlaceholders(
ifIncluded: arg,
customFormat: formatter,
allowNestedPlaceholders: allowNesting
)
return expanded?.expr

let callToTrailingContext = CallToTrailingClosures.Context(
startAtArgument: call.arguments.count - expanded.numClosures
)
guard let trailing = CallToTrailingClosures.refactor(syntax: expanded.expr, in: callToTrailingContext) else {
return nil
}
case let .trailing(indentationWidth):
guard let expanded = call.expandClosurePlaceholders(ifIncluded: arg, indentationWidth: indentationWidth)
else {
return nil
}

return trailing
let callToTrailingContext = CallToTrailingClosures.Context(
startAtArgument: call.arguments.count - expanded.numClosures
)
guard let trailing = CallToTrailingClosures.refactor(syntax: expanded.expr, in: callToTrailingContext) else {
return nil
}

return trailing
}
}
}

Expand Down Expand Up @@ -311,9 +388,11 @@ extension FunctionCallExprSyntax {
/// closure, then return a replacement of this call with one that uses
/// closures based on the function types provided by each editor placeholder.
/// Otherwise return nil.
fileprivate func expandTrailingClosurePlaceholders(
fileprivate func expandClosurePlaceholders(
ifIncluded: LabeledExprSyntax?,
indentationWidth: Trivia?
indentationWidth: Trivia? = nil,
customFormat: BasicFormat? = nil,
allowNestedPlaceholders: Bool = false
) -> (expr: FunctionCallExprSyntax, numClosures: Int)? {
var includedArg = false
var argsToExpand = 0
Expand Down Expand Up @@ -343,8 +422,12 @@ extension FunctionCallExprSyntax {
let edits = ExpandSingleEditorPlaceholder.textRefactor(
syntax: arg.expression.cast(DeclReferenceExprSyntax.self).baseName,
in: ExpandSingleEditorPlaceholder.Context(
indentationWidth: indentationWidth,
initialIndentation: lineIndentation
closureLiteralFormat: customFormat
?? BasicFormat(
indentationWidth: indentationWidth,
initialIndentation: lineIndentation
),
allowNestedPlaceholders: allowNestedPlaceholders
)
)
guard edits.count == 1, let edit = edits.first, !edit.replacement.isEmpty else {
Expand Down
5 changes: 4 additions & 1 deletion Sources/SwiftSyntax/SyntaxProtocol.swift
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,10 @@ extension SyntaxProtocol {
return self.previousToken(viewMode: .sourceAccurate)
}

/// Returns this node or the first ancestor that satisfies `condition`.
/// Applies `map` to this node and each of its ancestors until a non-`nil`
ahoppen marked this conversation as resolved.
Show resolved Hide resolved
/// value is produced, then returns that value.
///
/// If no node has a non-`nil` mapping, returns `nil`.
public func ancestorOrSelf<T>(mapping map: (Syntax) -> T?) -> T? {
return self.withUnownedSyntax {
var node = $0
Expand Down
Loading