-
Notifications
You must be signed in to change notification settings - Fork 426
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
Conversation
/// | ||
/// A closure that seems to be a simple predicate or transform — based on the | ||
/// number of enclosed statements — will not be reformatted to multiple lines. | ||
open class ClosureLiteralFormat: BasicFormat { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ℹ️ I wasn't confident that tweaking BasicFormat
directly wouldn't break something else, so I opted to introduce a new formatter. It's easy enough to roll this into BasicFormat
if preferred.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this is exactly the right choice. That’s why BasicFormat
is an open class.
// 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)) | ||
} | ||
expanded = formattedExpansion | ||
expanded = wrapInPlaceholder(formattedExpansion) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ℹ️ @ahoppen This is an extension of your original suggestion. I think wrapping the whole closure in a placeholder, with inner placeholders for the args and body, provides an optimal UX: you can immediately delete/replace the entire thing or you can tab to the constituent parts. This does necessitate a change in sourcekit-lsp's placeholder translation routine to handle the nesting; this is done in swiftlang/sourcekit-lsp#1831, which also has a screen recording of the new behavior.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unfortunately, we can’t do that. The Swift compiler and Xcode don’t support nested editor placeholders using the <# … #>
syntax. But I do agree that wrapping the closure in a placeholder again makes sense in the context of automatic SourceKit-LSP expansions. Do you think we could do the re-wrapping on the SourceKit-LSP side?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Darn, I was afraid that might be the case. Okay, following this and your other comments, I'll figure out how to move the bulk of the behavior change into sourcekit-lsp.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't see a (good) way to move the nested wrapping into SourceKit-LSP; we'd essentially have to re-do most of this work: re-parsing the expansion and finding the closure body to wrap. I have added a flag to the Context
to control this. It is off by default and CodeCompletionSession
will set it for its own expansion requests.
@@ -18,6 +18,12 @@ import SwiftSyntaxBuilder | |||
import XCTest | |||
import _SwiftSyntaxTestSupport | |||
|
|||
fileprivate extension DefaultStringInterpolation { | |||
mutating func appendInterpolation(placeholder string: String) { | |||
self.appendLiteral(wrapInPlaceholder(string)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ℹ️ Helper to keep literal <#
and #>
delimiters out of the test assertions.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@ahoppen I assume you would prefer I remove this, per your comment on the same method in the sourcekit-lsp PR?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, correct.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the changes @woolsweater. I don’t want to change the general behavior of the refactorings because there are applications where the current behavior makes sense, so we’d need a way to only get the modified behavior for SourceKit-LSP.
/// | ||
/// A closure that seems to be a simple predicate or transform — based on the | ||
/// number of enclosed statements — will not be reformatted to multiple lines. | ||
open class ClosureLiteralFormat: BasicFormat { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this is exactly the right choice. That’s why BasicFormat
is an open class.
/// Returns `true` if `token` is the last token in the signature of a closure, | ||
/// and that closure has no more than one statement in its body. | ||
private func isEndOfSmallClosureSignature(_ token: TokenSyntax) -> Bool { | ||
guard let signature = token.ancestorOrSelf(mapping: { $0.as(ClosureSignatureSyntax.self) }), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we need to generically look for the last token in the closure signature here or could we just look for the in
keyword that terminates the signature?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That seems fine; done! (This code has moved to sourcekit-lsp.)
/// { someInt in | ||
/// <#T##String#> | ||
/// } | ||
/// <#{ <#someInt#> in <#T##String#> }#> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we expand the editor placeholder, we shouldn’t end up with an editor placeholder again.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oops, reverted!
// 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)) | ||
} | ||
expanded = formattedExpansion | ||
expanded = wrapInPlaceholder(formattedExpansion) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unfortunately, we can’t do that. The Swift compiler and Xcode don’t support nested editor placeholders using the <# … #>
syntax. But I do agree that wrapping the closure in a placeholder again makes sense in the context of automatic SourceKit-LSP expansions. Do you think we could do the re-wrapping on the SourceKit-LSP side?
@@ -94,18 +92,18 @@ struct ExpandSingleEditorPlaceholder: EditRefactoringProvider { | |||
|
|||
let expanded: String | |||
if let functionType = placeholder.typeForExpansion?.as(FunctionTypeSyntax.self) { | |||
let basicFormat = BasicFormat( | |||
let format = ClosureLiteralFormat( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could we make the format that’s used to expand the editor placeholder an option that still defaults to BasicFormat
? For example, when you explicitly run the refactoring to expand a placeholder to a trailing closure, I think it makes sense to have the multi-line behavior. Then we could just override it with ClosureLiteralFormat
for SourceKit-LSP code completion expansion. That way ClosureLiteralFormat
could also be specified in SourceKit-LSP and wouldn’t become public API of swift-syntax.
/// closure2: { ... }, | ||
/// closure3: { <#someInt#> in <#T##String#> }, | ||
/// closure4: { <#someInt#> in <#T##String#> } | ||
/// ) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Similar here, I think that the general ExpandSingleEditorPlaceholder
refactoring should produce trailing closures, we just want to tweak the behavior from SourceKit-LSP. Maybe we need an option for that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you for the prompt reviews here and in sourcekit-lsp, @ahoppen! I will work on the revisions you've proposed.
@@ -18,6 +18,12 @@ import SwiftSyntaxBuilder | |||
import XCTest | |||
import _SwiftSyntaxTestSupport | |||
|
|||
fileprivate extension DefaultStringInterpolation { | |||
mutating func appendInterpolation(placeholder string: String) { | |||
self.appendLiteral(wrapInPlaceholder(string)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@ahoppen I assume you would prefer I remove this, per your comment on the same method in the sourcekit-lsp PR?
// 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)) | ||
} | ||
expanded = formattedExpansion | ||
expanded = wrapInPlaceholder(formattedExpansion) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Darn, I was afraid that might be the case. Okay, following this and your other comments, I'll figure out how to move the bulk of the behavior change into sourcekit-lsp.
d079e11
to
130a224
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice. Thank you, @woolsweater. Just a couple minor comments.
self.indentationWidth = indentationWidth | ||
self.initialIndentation = initialIndentation | ||
let closureLiteralFormat: BasicFormat | ||
let allowNestedPlaceholders: Bool |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Absolutely; done!
public struct ExpandEditorPlaceholdersToTrailingClosures: SyntaxRefactoringProvider { | ||
public struct ExpandEditorPlaceholdersToLiteralClosures: SyntaxRefactoringProvider { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Because this is a change to a public API, we should have a release note entry for it. Could you create a Release Notes/602.md
file and add the change to it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done!
130a224
to
8c947dc
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for addressing my last comments. Two more quick follow-ups. We’re really close.
Release Notes/602.md
Outdated
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry, I should have maybe been a little more specific. Could you also include the other headings and the template? Ie. this file should look as follows before adding your entry:
# Swift Syntax 601 Release Notes
## New APIs
## API Behavior Changes
## Deprecations
## 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*
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No problem; done!
/// ## With nested placeholders allowed | ||
/// ### Before | ||
/// ```swift | ||
/// <#T##(Int) -> String##(Int) -> String##(_ someInt: Int) -> String#> | ||
/// ``` | ||
/// | ||
/// ### After | ||
/// ```swift | ||
/// <#{ someInt in <#String#> }#> | ||
/// ``` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I’m not sure Before
and After
are the correct headings here. I would suggest
/// ## With nested placeholders allowed | |
/// ### Before | |
/// ```swift | |
/// <#T##(Int) -> String##(Int) -> String##(_ someInt: Int) -> String#> | |
/// ``` | |
/// | |
/// ### After | |
/// ```swift | |
/// <#{ someInt in <#String#> }#> | |
/// ``` | |
/// With `allowNestedPlaceholders = false` | |
/// ```swift | |
/// <#T##(Int) -> String##(Int) -> String##(_ someInt: Int) -> String#> | |
/// ``` | |
/// | |
/// With `allowNestedPlaceholders = true` | |
/// ```swift | |
/// <#{ someInt in <#String#> }#> | |
/// ``` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure, was following the wording from the doc comment above this; changed!
8c947dc
to
184115f
Compare
@swift-ci Please test |
1 similar comment
@swift-ci Please test |
Looks like CI is failing for two reasons:
|
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.
184115f
to
2c96735
Compare
Oops, had a stray empty line, sorry! Fixed.
Huh, is import behavior different on Linux? I only have a Mac to test on. Well, I have added that import and that's the only error I see in the logs. |
I think it’s because the diagnostic is created by the |
@swift-ci Please test |
Ah, I see, thanks. I am an Xcode point release or two behind the latest 🙂 |
@swift-ci Please test Windows |
1 similar comment
@swift-ci Please test Windows |
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.