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

Conversation

woolsweater
Copy link
Contributor

@woolsweater woolsweater commented Nov 17, 2024

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.

///
/// 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 {
Copy link
Contributor Author

@woolsweater woolsweater Nov 17, 2024

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.

Copy link
Member

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)
Copy link
Contributor Author

@woolsweater woolsweater Nov 17, 2024

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.

Copy link
Member

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?

Copy link
Contributor Author

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.

Copy link
Contributor Author

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.

Sources/SwiftSyntax/SyntaxProtocol.swift Show resolved Hide resolved
@@ -18,6 +18,12 @@ import SwiftSyntaxBuilder
import XCTest
import _SwiftSyntaxTestSupport

fileprivate extension DefaultStringInterpolation {
mutating func appendInterpolation(placeholder string: String) {
self.appendLiteral(wrapInPlaceholder(string))
Copy link
Contributor Author

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.

Copy link
Contributor Author

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?

Copy link
Member

Choose a reason for hiding this comment

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

Yes, correct.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done!

Copy link
Member

@ahoppen ahoppen left a 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 {
Copy link
Member

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.

Sources/SwiftBasicFormat/ClosureLiteralFormat.swift Outdated Show resolved Hide resolved
/// 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) }),
Copy link
Member

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?

Copy link
Contributor Author

@woolsweater woolsweater Nov 25, 2024

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#> }#>
Copy link
Member

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.

Copy link
Contributor Author

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)
Copy link
Member

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(
Copy link
Member

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.

Comment on lines 138 to 141
/// closure2: { ... },
/// closure3: { <#someInt#> in <#T##String#> },
/// closure4: { <#someInt#> in <#T##String#> }
/// )
Copy link
Member

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.

Copy link
Contributor Author

@woolsweater woolsweater left a 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))
Copy link
Contributor Author

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)
Copy link
Contributor Author

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.

@woolsweater woolsweater changed the title Refine expansion of function-typed placeholders Allow customization of expansion for function-typed placeholders Nov 25, 2024
Copy link
Member

@ahoppen ahoppen left a 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
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!

Comment on lines 198 to 248
public struct ExpandEditorPlaceholdersToTrailingClosures: SyntaxRefactoringProvider {
public struct ExpandEditorPlaceholdersToLiteralClosures: SyntaxRefactoringProvider {
Copy link
Member

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?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done!

Copy link
Member

@ahoppen ahoppen left a 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.

Copy link
Member

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*

Copy link
Contributor Author

Choose a reason for hiding this comment

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

No problem; done!

Comment on lines 89 to 97
/// ## With nested placeholders allowed
/// ### Before
/// ```swift
/// <#T##(Int) -> String##(Int) -> String##(_ someInt: Int) -> String#>
/// ```
///
/// ### After
/// ```swift
/// <#{ someInt in <#String#> }#>
/// ```
Copy link
Member

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

Suggested change
/// ## 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#> }#>
/// ```

Copy link
Contributor Author

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!

@ahoppen
Copy link
Member

ahoppen commented Dec 4, 2024

swiftlang/sourcekit-lsp#1831

@swift-ci Please test

1 similar comment
@ahoppen
Copy link
Member

ahoppen commented Dec 4, 2024

swiftlang/sourcekit-lsp#1831

@swift-ci Please test

@ahoppen
Copy link
Member

ahoppen commented Dec 4, 2024

Looks like CI is failing for two reasons:

  1. Could you format your changes using swift-format
  2. Compilation of SourceKit-LSP is failing with (+ maybe other similar issues, I didn’t scan the entire log)
/home/build-user/sourcekit-lsp/Tests/SourceKitLSPTests/ClosureCompletionFormatTests.swift:120:62: error: initializer 'init(integerLiteral:)' is not available due to missing import of defining module 'SwiftSyntaxBuilder'
 17 | @_spi(Testing) import SourceKitLSP
 18 | import XCTest
 19 | 
    | `- note: add import of module 'SwiftSyntaxBuilder'
 20 | fileprivate func assertFormatted<T: SyntaxProtocol>(
 21 |   tree: T,
    :
118 |       tree: ClosureExprSyntax(
119 |         statements: CodeBlockItemListSyntax([
120 |           CodeBlockItemSyntax(item: CodeBlockItemSyntax.Item(IntegerLiteralExprSyntax(integerLiteral: 2)))
    |                                                              `- error: initializer 'init(integerLiteral:)' is not available due to missing import of defining module 'SwiftSyntaxBuilder'
121 |         ])
122 |       ),

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.
@woolsweater
Copy link
Contributor Author

  1. Could you format your changes using swift-format

Oops, had a stray empty line, sorry! Fixed.

  1. Compilation of SourceKit-LSP is failing with (+ maybe other similar issues, I didn’t scan the entire log)
/home/build-user/sourcekit-lsp/Tests/SourceKitLSPTests/ClosureCompletionFormatTests.swift:120:62: error: initializer 'init(integerLiteral:)' is not available due to missing import of defining module 'SwiftSyntaxBuilder'

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.

@ahoppen
Copy link
Member

ahoppen commented Dec 5, 2024

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 MemberImportVisibility upcoming feature and your local compiler probably doesn’t have it (I can’t remember whether it was introduced by 6.0 or 6.1).

@ahoppen
Copy link
Member

ahoppen commented Dec 5, 2024

swiftlang/sourcekit-lsp#1831

@swift-ci Please test

@woolsweater
Copy link
Contributor Author

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 MemberImportVisibility upcoming feature and your local compiler probably doesn’t have it (I can’t remember whether it was introduced by 6.0 or 6.1).

Ah, I see, thanks. I am an Xcode point release or two behind the latest 🙂

@ahoppen
Copy link
Member

ahoppen commented Dec 5, 2024

swiftlang/sourcekit-lsp#1831

@swift-ci Please test Windows

1 similar comment
@ahoppen
Copy link
Member

ahoppen commented Dec 5, 2024

swiftlang/sourcekit-lsp#1831

@swift-ci Please test Windows

@ahoppen ahoppen merged commit 507f323 into swiftlang:main Dec 6, 2024
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants