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

Add config option for trailing closure rewriting #1789

Closed
wants to merge 1 commit into from

Conversation

woolsweater
Copy link

@woolsweater woolsweater commented Oct 27, 2024

This would resolve #1787

The default remains the automatic expansion introduced in #1072, but the new option allows the user to disable it via a configuration file.

This is expressed as an enumeration rather than a boolean flag because there is room for other levels of rewriting. E.g. for trailing closures, a "basic" level might be implemented as rewriting to a pair of brackets on the same line, with a single placeholder, rather than the "full" multi-line behavior.

A configuration file like the following will disable the expansion feature:

{
  "codeCompletion" : {
    "rewriteTrailingClosures" : "never"
  }
}

Default to the automatic expansion introduced in
90c124c but allow the user to disable
it via a configuration file.

This is expressed as an enumeration rather than a boolean flag because
there is room for other levels of rewriting. E.g. for trailing closures,
a "basic" level might be implemented as rewriting to a pair of brackets
on the same line, with a single placeholder, rather than the "full"
multi-line behavior.
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 PR, @woolsweater.  I agree that we are currently using trailing closure syntax for code completion fairly aggressively and that there is a problem to be solved here, but I don’t think that a global configuration option is the right solution here.

I think that whether you want to use a trailing closure depends less on personal preference and more on the context. For example, I think that in almost all cases a user is expecting Task.init to be followed by a trailing closure. For methods like filter it usually depends on whether the condition will be complex and span multiple lines or, as you stated in your issue, if you are intending to use a simple condition or key path.

Thus, I think the user should be given the choice of whether to use a trailing closure completion on every completion. The options I see right now are:

  1. Offer two different completion options for the trailing closure and the non-trailing closure case. But this might inflate the number of code completion items and make it harder to find the one you are looking for.
  2. Revert code completions to suggest snippets (like SourceKit-LSP did in 5.10 and earlier) and then offer a code action to expand snippets to trailing closure, similar to how Xcode expands placeholders to trailing closures when you press enter while they are selected. This may be tricky to implement because editors don’t represent snippets in a file’s source text, so SourceKit-LSP doesn’t know if the user is currently at a snippet that was inserted to represent a closure, unless SourceKit-LSP keeps track of that information itself, which I guess it could.
  3. Make code completion expand snippets to single-line closures but not trailing closures (e.g expand to filter(where: { ${1:Int} in ${2:Bool} }) instead of filter { ${1:Int} in \n${2:Bool}\n }). This would be a compromise from where you can go into either direction: (a) Deleting the closure to replace it with a function reference isn’t too much effort since it only spas a single line, (b) similarly using anonymous closure argument names ($0 etc) is fairly straightforward, (c) to go to trailing closure syntax, we already have a code action for that, we would just need to make that refactoring more fault tolerant and be applicable in case there is a closure with snippets.
  4. Continue expanding to trailing closure syntax like we do now and offer a code action to convert trailing closure syntax back to non-trailing closure syntax. The tricky part here is inferring the label for the function call, eg. the where to convert firstIndex { … } to firstIndex(where: { … })

While writing this, I think that I’m preferring option (2) or (3). What do you think, @woolsweater?

@woolsweater
Copy link
Author

depends […] more on the context. For example, I think that in almost all cases a user is expecting Task.init to be followed by a trailing closure.

Thanks @ahoppen! Yes, absolutely; there are cases where it would be very unusual not to use a trailing closure. But I think there are more cases where the language server can't read the user's mind and can't know which it should choose. I agree that (2) and (3) are the best paths forward 🙌 It seems to me that they could mostly coexist, actually.

Having a code action to expand a call to trailing closures would be a great thing to have regardless of how the snippet expansion works. I would think the server could get around the "was this a snippet?" problem by simply offering it on any call expression where there is a closure literal present? That would make it useful even when revisiting the code long after the current LSP session is closed. But perhaps that is unworkable for some reason?

I saw this PR as a stepping stone to a state like your (3). I do genuinely think there's an element of user preference here: for example, I assume the author of #854 likes the current implementation, and I didn't want to break their experience. So they and anyone else who prefers it can use the full setting and get that behavior. The (new default) medium or basic level would be your (3).¹ And the curmudgeons can turn it off altogether with never. I think it's going to be impossible for the language server to please everyone in this regard. So having a good default with a little adjustability seems ideal.

I see that reworking the expansion requires changes to swift-syntax, and I have not gotten a chance to dig into that, but I was planning to do so this coming weekend. I would be happy to try to implement your item (3) as a resolution of #1788 unless you would prefer to do it.


¹ And perhaps there's even a high setting between that and full...but then someone's go to figure out what that means and then implement it, so maybe not 😅

@ahoppen
Copy link
Member

ahoppen commented Oct 29, 2024

If you would be interested in implementing (3) to resolve #1788, that would be great. I’m happy to help with any concrete questions you might have.

Regarding an option: I would prefer to try and avoid options as long as we don’t have two opinions that fundamentally oppose each other. Every option needs to be tested and with a growing number of configurations, the chance of missing some broken configuration rises. For now, I think that (2) or (3) offer a good middle ground and I would like to explore if they can please the majority before jumping to a configuration parameter.

@woolsweater
Copy link
Author

Every option needs to be tested and with a growing number of configurations…

Yes, that's fair and understandable. Okay, I will put this idea aside and work on the swift-syntax changes. Is simply changing the behavior of ExpandEditorPlaceholdersToTrailingClosures the right approach? Are there any other clients of it besides sourcekit-lsp?

@woolsweater woolsweater marked this pull request as draft October 29, 2024 17:13
@ahoppen
Copy link
Member

ahoppen commented Oct 30, 2024

Yes, that's fair and understandable. Okay, I will put this idea aside and work on the swift-syntax changes.
Thank you!

Is simply changing the behavior of ExpandEditorPlaceholdersToTrailingClosures the right approach? Are there any other clients of it besides sourcekit-lsp?

SourceKit-LSP is the main client of ExpandEditorPlaceholdersToTrailingClosures and I assume that any changes to it will also benefit any other clients that might exist.

@woolsweater
Copy link
Author

Sorry for the delay, @ahoppen ; I did not get to this as soon as I'd hoped. I have the changes just about ready to go now. I will have to make a PR to swift-syntax, to change the expansion behavior, and then one to sourcekit-lsp to adopt the swift-syntax update.

Is there a good way for me to make a draft PR to sourcekit-lsp, while the swift-syntax PR is pending, that includes my swift-syntax changes? Should I just change the pin in sourcekit-lsp's Package.resolved to point to my branch, and then update it after the swift-syntax merge? Or do you have another preferred way to handle this?

@ahoppen
Copy link
Member

ahoppen commented Nov 12, 2024

Is there a good way for me to make a draft PR to sourcekit-lsp, while the swift-syntax PR is pending, that includes my swift-syntax changes? Should I just change the pin in sourcekit-lsp's Package.resolved to point to my branch, and then update it after the swift-syntax merge? Or do you have another preferred way to handle this?

Just open a PR to swift-syntax and reference it from this PR. We can do cross-PR testing in CI.

woolsweater added a commit to woolsweater/swift-syntax that referenced this pull request Nov 14, 2024
Per <swiftlang/sourcekit-lsp#1789>, these
placeholders no longer expand to multi-line trailing closures. Instead
they become inline closures, with nested placeholders for the signature
and body.

This introduces a new formatter, `ClosureLiteralFormat`, so that other
uses of `BasicFormat` are not impacted. `ClosureLiteralFormat` does not
introduce newlines to a closure when there is only a single statement in
the body.
woolsweater added a commit to woolsweater/swift-syntax that referenced this pull request Nov 16, 2024
Per <swiftlang/sourcekit-lsp#1789>, these
placeholders no longer expand to multi-line trailing closures. Instead
they become inline closures, with nested placeholders for the signature
and body.

This introduces a new formatter, `ClosureLiteralFormat`, so that other
uses of `BasicFormat` are not impacted. `ClosureLiteralFormat` does not
insert newlines into a closure when there is only a single statement in
the body.
woolsweater added a commit to woolsweater/swift-syntax that referenced this pull request Nov 17, 2024
This addresses <swiftlang/sourcekit-lsp#1788>.

Per <swiftlang/sourcekit-lsp#1789>, these
placeholders no longer expand to multi-line trailing closures. Instead
they become inline closures, with nested placeholders for the signature
and body.

This introduces a new formatter, `ClosureLiteralFormat`, so that other
uses of `BasicFormat` are not impacted. `ClosureLiteralFormat` does not
insert newlines into a closure when there is only a single statement in
the body.
woolsweater added a commit to woolsweater/swift-syntax that referenced this pull request Nov 17, 2024
This addresses <swiftlang/sourcekit-lsp#1788>.

Per <swiftlang/sourcekit-lsp#1789>, these
placeholders no longer expand to multi-line trailing closures. Instead
they become inline closures, with nested placeholders for the signature
and body.

This introduces a new formatter, `ClosureLiteralFormat`, so that other
uses of `BasicFormat` are not impacted. `ClosureLiteralFormat` does not
insert newlines into a closure when there is only a single statement in
the body.
@woolsweater
Copy link
Author

Okay, I've opened swiftlang/swift-syntax#2897 and #1831 for the change discussed here.

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.

Configuration option for auto-rewrite of trailing closures
2 participants