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 ExecuteCommand/ApplyEdit #114

Merged
merged 6 commits into from
Sep 25, 2019
Merged

Conversation

rockbruno
Copy link
Contributor

@rockbruno rockbruno commented Jun 28, 2019

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 a TextDocumentRequest 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 of SwiftCommand that can be detected by the use of a swift.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.

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

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.

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

Copy link
Contributor

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

Choose a reason for hiding this comment

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

///
/// - Parameters:
/// - type: The `SwiftCommand` metatype to convert to.
public func swiftCommand<T: SwiftCommand>(ofType type: T.Type) -> T? {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@@ -888,6 +891,34 @@ extension SwiftLanguageServer {
}
}
}

func executeCommand(_ req: Request<ExecuteCommandRequest>) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

let haveClangd = ToolchainRegistry.shared.toolchains.contains { $0.clangd != nil }
if !haveClangd {
XCTFail("Cannot find clangd in toolchain")
}
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 actually wasn't able to get this or anything in LocalClangTests to pass, I haven't got the right toolchains yet.

Choose a reason for hiding this comment

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

Thanks

Choose a reason for hiding this comment

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

Thanks


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.
Copy link

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?

Copy link
Contributor Author

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

Copy link

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?

Copy link
Contributor Author

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.

Copy link
Contributor Author

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.

@rockbruno
Copy link
Contributor Author

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

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.

Copy link
Contributor Author

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.

Copy link
Contributor

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

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

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

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?
Copy link
Contributor

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

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

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?

Copy link
Contributor Author

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

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.

@rockbruno
Copy link
Contributor Author

Rebased from master

Copy link
Contributor

@benlangmuir benlangmuir left a 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]]?
Copy link
Contributor

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

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

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]? {
Copy link
Contributor

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,
Copy link
Contributor

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

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

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

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?

@rockbruno
Copy link
Contributor Author

No worries! I applied the changes here - I just wasn't sure about the encode(toLSPAny:) bit, I named it encodeToLSPAny() directly because we can't really append/construct the LSPAny type from an external reference in that context, but if you have an idea feel free to share it.

@benlangmuir
Copy link
Contributor

@swift-ci please test

1 similar comment
@benlangmuir
Copy link
Contributor

@swift-ci please test

@benlangmuir
Copy link
Contributor

@swift-ci please test macOS

1 similar comment
@benlangmuir
Copy link
Contributor

@swift-ci please test macOS

@benlangmuir
Copy link
Contributor

@testable import SourceKit

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 *Public for testing* in their documentation comment.

@rockbruno
Copy link
Contributor Author

Ah that actually wasn't needed for the tests, I think I had that added out of habit for iOS.

@benlangmuir
Copy link
Contributor

@swift-ci please test

1 similar comment
@benlangmuir
Copy link
Contributor

@swift-ci please test

@benlangmuir
Copy link
Contributor

@swift-ci please test macOS

1 similar comment
@benlangmuir
Copy link
Contributor

@swift-ci please test macOS

@benlangmuir benlangmuir merged commit 942aaa4 into swiftlang:master Sep 25, 2019
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.

4 participants