-
Notifications
You must be signed in to change notification settings - Fork 274
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 ExecuteCommand/ApplyEdit #114
Conversation
@@ -269,6 +270,9 @@ extension SourceKitServer { | |||
clientCapabilities: req.params.capabilities.textDocument?.codeAction, | |||
codeActionOptions: CodeActionOptions(codeActionKinds: nil), | |||
supportsCodeActions: false // TODO: Turn it on after a provider is implemented. | |||
), | |||
executeCommandProvider: ExecuteCommandOptions( | |||
commands: builtinSwiftCommands // FIXME: Clangd commands? |
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.
VSCode doesn't call the request if a command isn't defined by the server - should we worry about clangd here? I noticed that it has an initialize request of its own, but I was unsure if what we define here affects 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.
You can make any steps to get closer to opening it still learning
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.
Hmm, that's unfortunate. We load clangd dynamically through the toolchain, so there's no good way for us to get the response from clang this early. Maybe someday this would be a good place for the specification to evolve to add dynamic capabilities for the server (there are already dynamic capabilities for the client). As a short term hack, we could maintain a list of every command clangd supports, but that doesn't need to be done in this patch.
For now, this is fine.
} | ||
|
||
/// Converts this `SwiftCommand` to a generic LSP `Command` object. | ||
public func asCommand() throws -> Command { |
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.
You can see how this gets used here:
https://github.com/apple/sourcekit-lsp/pull/70/files#diff-6175f3a32db6757aaeb5b49a98bab346R949
/// | ||
/// - Parameters: | ||
/// - type: The `SwiftCommand` metatype to convert to. | ||
public func swiftCommand<T: SwiftCommand>(ofType type: T.Type) -> T? { |
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.
You can see how this gets used here:
https://github.com/apple/sourcekit-lsp/pull/70/files#diff-6175f3a32db6757aaeb5b49a98bab346R969
@@ -888,6 +891,34 @@ extension SwiftLanguageServer { | |||
} | |||
} | |||
} | |||
|
|||
func executeCommand(_ req: Request<ExecuteCommandRequest>) { |
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.
It's outdated, but the actual request is here:
https://github.com/apple/sourcekit-lsp/pull/70/files#diff-6175f3a32db6757aaeb5b49a98bab346R966
let haveClangd = ToolchainRegistry.shared.toolchains.contains { $0.clangd != nil } | ||
if !haveClangd { | ||
XCTFail("Cannot find clangd in toolchain") | ||
} |
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 actually wasn't able to get this or anything in LocalClangTests to pass, I haven't got the right toolchains yet.
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
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
927e46c
to
75e4df6
Compare
|
||
func serviceFor(executeCommandRequest req: ExecuteCommandRequest, workspace: Workspace) -> Connection? { | ||
// FIXME: ExecuteCommand requests have no URL associated to it, but we need to determine the server | ||
// that it gets sent to. There should be a better way to do this. |
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.
can we pass url back in the argument
field of Commands
returned by CodeActionResponse
?
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.
We can for Swift, but AFAIK we have no control over what clangd's commands will look like
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.
please correct me if i'm missing something, I assume parameters passed in the CodeActionResponse
will be sent back to server when user picks the command. If that's the case, even for Clangd, server should still be the best place to generate those arguments, no?
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.
True, I believe we can intercept the response from clang and inject the url that called the code action itself into the arguments. It should be enough to avoid this logic, so we just need to confirm that doing so won't be a problem for clangd.
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.
True, I believe we can intercept the response from clang and inject the url that called the code action itself into the arguments. It should be enough to avoid this logic, so we just need to confirm that doing so won't be a problem for clangd.
71026b0
to
393b327
Compare
Done, I added a result handler to the CodeActions that intercepts the original result and adds a metadata object to the commands. Worked great! |
public func injectMetadata(atResponse response: CodeActionRequestResponse?) -> CodeActionRequestResponse? { | ||
let metadata = SourceKitLSPCommandMetadata(textDocument: textDocument) | ||
guard let data = try? JSONEncoder().encode(metadata), | ||
let metadataArgument = try? JSONDecoder().decode(CommandArgumentType.self, from: data) else |
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.
We should just construct the dictionary instead of round-tripping through json. The json (de) serialization code has a relatively high up front cost even for small payloads.
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.
Ah, I was trying to make the compiler synthetize the usage of CommandArguments as much as possible to make it easy to add and edit commands... I changed these guys to encode and decode the dictionaries manually, but let me know if there's something that can be done to reduce the boilerplate.
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 aware of anything good here. In principle you could write a Decoder implementation that works directly from the dictionary - basically perform the object mapping part of JSONDecoder without doing any of the parsing. But that seems unnecessarily general to implement in sourcekit-lsp, so unless someone adds such a thing to the stdlib or Foundation I don't see us using it.
@@ -47,6 +48,30 @@ public struct CodeActionRequest: TextDocumentRequest, Hashable { | |||
self.context = context | |||
self.textDocument = textDocument | |||
} | |||
|
|||
public func injectMetadata(atResponse response: CodeActionRequestResponse?) -> CodeActionRequestResponse? { |
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.
This implementation is not really a general feature of CodeAction, so it should probably move into an extension the SourceKit module.
} | ||
|
||
/// Optional metadata containing SourceKit-LSP infomration about this command. | ||
public var metadata: SourceKitLSPCommandMetadata? { |
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.
Same here, I'd prefer this moved to extensions in the SourceKit module.
guard let data = try? JSONEncoder().encode(dictionary) else { | ||
return nil | ||
} | ||
return try? JSONDecoder().decode(SourceKitLSPCommandMetadata.self, from: data) |
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 to above, we already have a dictionary, so we should map it to our metadata directly without going back through json.
@@ -269,6 +270,9 @@ extension SourceKitServer { | |||
clientCapabilities: req.params.capabilities.textDocument?.codeAction, | |||
codeActionOptions: CodeActionOptions(codeActionKinds: nil), | |||
supportsCodeActions: false // TODO: Turn it on after a provider is implemented. | |||
), | |||
executeCommandProvider: ExecuteCommandOptions( | |||
commands: builtinSwiftCommands // FIXME: Clangd commands? |
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.
Hmm, that's unfortunate. We load clangd dynamically through the toolchain, so there's no good way for us to get the response from clang this early. Maybe someday this would be a good place for the specification to evolve to add dynamic capabilities for the server (there are already dynamic capabilities for the client). As a short term hack, we could maintain a list of every command clangd supports, but that doesn't need to be done in this patch.
For now, this is fine.
/// Represents metadata that SourceKit-LSP injects at every command returned by code actions. | ||
/// The ExecuteCommand is not a TextDocumentRequest, so metadata is injected to allow SourceKit-LSP | ||
/// to determine where a command should be executed. | ||
public struct SourceKitLSPCommandMetadata: Codable, Hashable { |
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're going to inject this into clang responses, I think we need to mangle all the dictionary keys so it can't collide with anything (e.g. sourcekitlsp_textDocument).
} | ||
|
||
func executeCommand(_ req: Request<ExecuteCommandRequest>, workspace: Workspace) { | ||
guard let url = req.params.textDocument?.url else { |
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 wonder if we should strip the metadata off before sending it to clang. What do you think?
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.
Yeah, since at this point the correct server can be determined we can strip it for Swift as well. Should make things more predictable 👍
/// Converts this `SwiftCommand` to a generic LSP `Command` object. | ||
public func asCommand() throws -> Command { | ||
let data = try JSONEncoder().encode(self) | ||
let argument = try JSONDecoder().decode(CommandArgumentType.self, from: data) |
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.
More round-tripping through JSON that we should avoid.
447cb87
to
ce7b695
Compare
ce7b695
to
61f429c
Compare
Rebased from master |
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.
Hey @rockbruno! I saw that you updated this recently, but I didn't realize it that even before that it had been waiting on me. I must have missed the notification when you last changed. Sorry for the long delay! Please feel free to ping me in the future if I am not responding in a timely fashion.
I have a few minor requests, but otherwise I think this is ready to go. It's not ideal that we can't test the changes to SourceKitServer yet, but not the end of the world. On the other hand, I think we should defer the specific SwiftCommand
bits until we can use them.
Thanks again for working on this!
public struct WorkspaceEdit: Codable, Hashable, ResponseType { | ||
|
||
/// The edits to be applied to existing resources. | ||
public var changes: [String: [TextEdit]]? |
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.
Why not use URL
directly here? Everywhere else that uses DocumentUri
we are using URL
I think.
/// to determine where a command should be executed. | ||
public struct SourceKitLSPCommandMetadata: Codable, Hashable { | ||
|
||
public static func decode(fromDictionary dictionary: [String: LSPAny]) -> SourceKitLSPCommandMetadata? { |
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.
Let's rearrange this so that the instance fields and init are at the top, and the decoding and encoding are together.
Also, how about changing the signature to init?(fromLSPDictionary:)
? Seems like it belongs in an init
.
} | ||
|
||
extension CodeActionRequest { | ||
public func injectMetadata(atResponse response: CodeActionRequestResponse?) -> CodeActionRequestResponse? { |
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.
Nitpick: toResponse
seems more grammatical to me
} | ||
|
||
/// Returns this Command's arguments without SourceKit-LSP's injected metadata, if it exists. | ||
public var argumentsWithoutLSPMetadata: [LSPAny]? { |
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.
WithoutSourceKitMetadata
seems more correct, since it's not general to LSP, but specific to SK.
@@ -628,16 +650,28 @@ extension SourceKitServer { | |||
func toolchainTextDocumentRequest<PositionRequest>( | |||
_ req: Request<PositionRequest>, | |||
workspace: Workspace, | |||
resultHandler: ((LSPResult<PositionRequest.Response>) -> LSPResult<PositionRequest.Response>)? = nil, |
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.
How about resultTransformer
? To me "handler" implies something else.
public var actionString: String | ||
|
||
/// The starting line of the range to refactor. | ||
public var line: Int |
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.
Why not use a PositionRange
?
public let builtinSwiftCommands: [String] = [] | ||
|
||
/// A `Command` that should be executed by Swift's language server. | ||
public protocol SwiftCommand: Codable, Hashable { |
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.
It looks like none of this is used yet. The basic design looks okay, but I'd like to leave this out until we have code that uses it (basically defer it to the next PR). WDYT?
self.sourcekitlsp_textDocument = textDocument | ||
} | ||
|
||
func asCommandArgument() -> LSPAny { |
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.
How about encode(toLSPAny:)
to be closer to Codable?
61f429c
to
20c026c
Compare
No worries! I applied the changes here - I just wasn't sure about the |
@swift-ci please test |
1 similar comment
@swift-ci please test |
@swift-ci please test macOS |
1 similar comment
@swift-ci please test macOS |
I disabled testable support so that we can test in a configuration that matches what we want to distribute. This will need to be changed to a normal import. If there are specific internals you want to use, you can make them public, add a leading underscore to the name and put |
Ah that actually wasn't needed for the tests, I think I had that added out of habit for iOS. |
@swift-ci please test |
1 similar comment
@swift-ci please test |
@swift-ci please test macOS |
1 similar comment
@swift-ci please test macOS |
Part 2 to #70: This adds the skeleton for command execution and the resulting edits that are sent back to client.
The thing to note here is that
ExecuteCommand
isn't aTextDocumentRequest
in the LSP, but we still need to determine which server we send the request to. My solution for this involved creating a special type ofSwiftCommand
that can be detected by the use of aswift.lsp
prefix in the command identifier, but I would love to know if there are cleaner ways to handle this.EDIT: It now simply intercepts the CodeAction result and appends a metadata object containing the URL.