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
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions Sources/LanguageServerProtocol/ApplyEdit.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2014 - 2019 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See https://swift.org/LICENSE.txt for license information
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//===----------------------------------------------------------------------===//

/// Request from the server to the client to modify resources on the client side.
///
/// - Parameters:
/// - label: An optional label of the workspace edit.
/// - edit: The edits to apply.
public struct ApplyEditRequest: RequestType {
public static let method: String = "workspace/applyEdit"
public typealias Response = ApplyEditResponse?

/// An optional label of the workspace edit.
/// Used by the client's user interface for things such as
/// the stack to undo the workspace edit.
public var label: String?

/// The edits to apply.
public var edit: WorkspaceEdit

public init(label: String? = nil, edit: WorkspaceEdit) {
self.label = label
self.edit = edit
}
}

public struct ApplyEditResponse: Codable, Hashable, ResponseType {
/// Indicates whether the edit was applied or not.
public var applied: Bool

/// An optional textual description for why the edit was not applied.
public var failureReason: String?

public init(applied: Bool, failureReason: String?) {
self.applied = applied
self.failureReason = failureReason
}
}
7 changes: 6 additions & 1 deletion Sources/LanguageServerProtocol/CodeAction.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
//===----------------------------------------------------------------------===//

import Foundation
import SKSupport

public typealias CodeActionProviderCompletion = (([CodeAction]) -> Void)
public typealias CodeActionProvider = ((CodeActionRequest, @escaping CodeActionProviderCompletion) -> Void)
Expand Down Expand Up @@ -123,15 +124,19 @@ public struct CodeAction: Codable, Equatable, ResponseType {
/// The diagnostics that this code action resolves, if applicable.
public var diagnostics: [Diagnostic]?

/// The workspace edit this code action performs.
public var edit: WorkspaceEdit?

/// A command this code action executes.
/// If a code action provides an edit and a command,
/// first the edit is executed and then the command.
public var command: Command?

public init(title: String, kind: CodeActionKind? = nil, diagnostics: [Diagnostic]? = nil, command: Command? = nil) {
public init(title: String, kind: CodeActionKind? = nil, diagnostics: [Diagnostic]? = nil, edit: WorkspaceEdit? = nil, command: Command? = nil) {
self.title = title
self.kind = kind
self.diagnostics = diagnostics
self.edit = edit
self.command = command
}
}
42 changes: 42 additions & 0 deletions Sources/LanguageServerProtocol/ExecuteCommand.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2014 - 2019 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See https://swift.org/LICENSE.txt for license information
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//===----------------------------------------------------------------------===//

import Foundation

/// Request sent from the client to to trigger command execution on the server.
///
/// The execution of this request can be the result of a request that returns a command,
/// such as CodeActionsRequest and CodeLensRequest. In most cases, the server creates a WorkspaceEdit
/// structure and applies the changes to the workspace using the ApplyEditRequest.
///
/// Servers that provide command execution should set the `executeCommand` server capability.
///
/// - Parameters:
/// - command: The command to be executed.
/// - arguments: The arguments to use when executing the command.
public struct ExecuteCommandRequest: RequestType {
public static let method: String = "workspace/executeCommand"

// Note: The LSP type for this response is `Any?`.
public typealias Response = LSPAny?

/// The command to be executed.
public var command: String

/// Arguments that the command should be invoked with.
public var arguments: [LSPAny]?

public init(command: String, arguments: [LSPAny]?) {
self.command = command
self.arguments = arguments
}
}
2 changes: 2 additions & 0 deletions Sources/LanguageServerProtocol/LSPAny.swift
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@ extension LSPAny: Encodable {
}
}

extension LSPAny: ResponseType {}

extension LSPAny: ExpressibleByNilLiteral {
public init(nilLiteral _: ()) {
self = .null
Expand Down
1 change: 1 addition & 0 deletions Sources/LanguageServerProtocol/Messages.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ public let builtinRequests: [_RequestType.Type] = [
DocumentColorRequest.self,
ColorPresentationRequest.self,
CodeActionRequest.self,
ExecuteCommandRequest.self,

// MARK: LSP Extension Requests

Expand Down
20 changes: 18 additions & 2 deletions Sources/LanguageServerProtocol/ServerCapabilities.swift
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,10 @@ public struct ServerCapabilities: Codable, Hashable {

/// The server provides workspace symbol support.
public var workspaceSymbolProvider: Bool?


/// Whether the server provides "workspace/executeCommand".
public var executeCommandProvider: ExecuteCommandOptions?

// TODO: fill-in the rest.

public init(
Expand All @@ -74,7 +77,8 @@ public struct ServerCapabilities: Codable, Hashable {
documentSymbolProvider: Bool? = nil,
colorProvider: Bool? = nil,
codeActionProvider: CodeActionServerCapabilities? = nil,
workspaceSymbolProvider: Bool? = nil
workspaceSymbolProvider: Bool? = nil,
executeCommandProvider: ExecuteCommandOptions? = nil
)
{
self.textDocumentSync = textDocumentSync
Expand All @@ -92,6 +96,7 @@ public struct ServerCapabilities: Codable, Hashable {
self.colorProvider = colorProvider
self.codeActionProvider = codeActionProvider
self.workspaceSymbolProvider = workspaceSymbolProvider
self.executeCommandProvider = executeCommandProvider
}

public init(from decoder: Decoder) throws {
Expand All @@ -105,6 +110,7 @@ public struct ServerCapabilities: Codable, Hashable {
self.colorProvider = try container.decodeIfPresent(Bool.self, forKey: .colorProvider)
self.codeActionProvider = try container.decodeIfPresent(CodeActionServerCapabilities.self, forKey: .codeActionProvider)
self.workspaceSymbolProvider = try container.decodeIfPresent(Bool.self, forKey: .workspaceSymbolProvider)
self.executeCommandProvider = try container.decodeIfPresent(ExecuteCommandOptions.self, forKey: .executeCommandProvider)

if let textDocumentSync = try? container.decode(TextDocumentSyncOptions.self, forKey: .textDocumentSync) {
self.textDocumentSync = textDocumentSync
Expand Down Expand Up @@ -246,3 +252,13 @@ public struct CodeActionOptions: Codable, Hashable {
self.codeActionKinds = codeActionKinds
}
}

public struct ExecuteCommandOptions: Codable, Hashable {

/// The commands to be executed on this server.
public var commands: [String]

public init(commands: [String]) {
self.commands = commands
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ public struct TextDocumentIdentifier: Hashable {

// Encode using the key "uri" to match LSP.
extension TextDocumentIdentifier: Codable {
private enum CodingKeys: String, CodingKey {
public enum CodingKeys: String, CodingKey {
case url = "uri"
}
}
22 changes: 22 additions & 0 deletions Sources/LanguageServerProtocol/WorkspaceEdit.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2014 - 2019 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See https://swift.org/LICENSE.txt for license information
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//===----------------------------------------------------------------------===//

/// A workspace edit represents changes to many resources managed in the workspace.
public struct WorkspaceEdit: Codable, Hashable, ResponseType {

/// The edits to be applied to existing resources.
public var changes: [URL: [TextEdit]]?

public init(changes: [URL: [TextEdit]]?) {
self.changes = changes
}
}
95 changes: 95 additions & 0 deletions Sources/SourceKit/SourceKitLSPCommandMetadata.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2014 - 2018 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See https://swift.org/LICENSE.txt for license information
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//===----------------------------------------------------------------------===//

import LanguageServerProtocol
import Foundation
import SKSupport

/// 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 {

public var sourcekitlsp_textDocument: TextDocumentIdentifier

public init?(fromLSPDictionary dictionary: [String: LSPAny]) {
let textDocumentKey = CodingKeys.sourcekitlsp_textDocument.stringValue
let urlKey = TextDocumentIdentifier.CodingKeys.url.stringValue
guard case .dictionary(let textDocumentDict)? = dictionary[textDocumentKey],
case .string(let urlString)? = textDocumentDict[urlKey],
let url = URL(string: urlString) else
{
return nil
}
let textDocument = TextDocumentIdentifier(url)
self.init(textDocument: textDocument)
}

public init(textDocument: TextDocumentIdentifier) {
self.sourcekitlsp_textDocument = textDocument
}

public func encodeToLSPAny() -> LSPAny {
let textDocumentArgument = LSPAny.dictionary(
[TextDocumentIdentifier.CodingKeys.url.stringValue: .string(sourcekitlsp_textDocument.url.absoluteString)]
)
return .dictionary([CodingKeys.sourcekitlsp_textDocument.stringValue: textDocumentArgument])
}
}

extension CodeActionRequest {
public func injectMetadata(toResponse response: CodeActionRequestResponse?) -> CodeActionRequestResponse? {
let metadata = SourceKitLSPCommandMetadata(textDocument: textDocument)
let metadataArgument = metadata.encodeToLSPAny()
switch response {
case .codeActions(var codeActions)?:
for i in 0..<codeActions.count {
codeActions[i].command?.arguments?.append(metadataArgument)
}
return .codeActions(codeActions)
case .commands(var commands)?:
for i in 0..<commands.count {
commands[i].arguments?.append(metadataArgument)
}
return .commands(commands)
case nil:
return nil
}
}
}

extension ExecuteCommandRequest {
/// The document in which the command was invoked.
public var textDocument: TextDocumentIdentifier? {
return metadata?.sourcekitlsp_textDocument
}

/// Optional metadata containing SourceKit-LSP information about this command.
public var metadata: SourceKitLSPCommandMetadata? {
guard case .dictionary(let dictionary)? = arguments?.last else {
return nil
}
guard let metadata = SourceKitLSPCommandMetadata(fromLSPDictionary: dictionary) else {
log("failed to decode lsp metadata in executeCommand request", level: .error)
return nil
}
return metadata
}

/// Returns this Command's arguments without SourceKit-LSP's injected metadata, if it exists.
public var argumentsWithoutSourceKitMetadata: [LSPAny]? {
guard metadata != nil else {
return arguments
}
return arguments?.dropLast()
}
}
44 changes: 39 additions & 5 deletions Sources/SourceKit/SourceKitServer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ public final class SourceKitServer: LanguageServer {
registerWorkspaceRequest(SourceKitServer.colorPresentation)
registerWorkspaceRequest(SourceKitServer.codeAction)
registerWorkspaceRequest(SourceKitServer.pollIndex)
registerWorkspaceRequest(SourceKitServer.executeCommand)
}

func registerWorkspaceRequest<R>(
Expand Down Expand Up @@ -313,7 +314,10 @@ extension SourceKitServer {
codeActionOptions: CodeActionOptions(codeActionKinds: nil),
supportsCodeActions: false // TODO: Turn it on after a provider is implemented.
),
workspaceSymbolProvider: true
workspaceSymbolProvider: true,
executeCommandProvider: ExecuteCommandOptions(
commands: [] // FIXME: Clangd commands?
)
)))
}

Expand Down Expand Up @@ -459,7 +463,25 @@ extension SourceKitServer {
}

func codeAction(_ req: Request<CodeActionRequest>, workspace: Workspace) {
toolchainTextDocumentRequest(req, workspace: workspace, fallback: nil)
toolchainTextDocumentRequest(req, workspace: workspace, resultTransformer: { result in
switch result {
case .success(let reply):
return .success(req.params.injectMetadata(toResponse: reply))
default:
return result
}
}, fallback: nil)
}

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 👍

log("attempted to perform executeCommand request without an url!", level: .error)
req.reply(nil)
return
}
var params = req.params
params.arguments = params.argumentsWithoutSourceKitMetadata
sendRequest(req, params: params, url: url, workspace: workspace, fallback: nil)
}

func definition(_ req: Request<DefinitionRequest>, workspace: Workspace) {
Expand Down Expand Up @@ -628,16 +650,28 @@ extension SourceKitServer {
func toolchainTextDocumentRequest<PositionRequest>(
_ req: Request<PositionRequest>,
workspace: Workspace,
resultTransformer: ((LSPResult<PositionRequest.Response>) -> LSPResult<PositionRequest.Response>)? = nil,
fallback: @autoclosure () -> PositionRequest.Response)
where PositionRequest: TextDocumentRequest
{
guard let service = workspace.documentService[req.params.textDocument.url] else {
sendRequest(req, params: req.params, url: req.params.textDocument.url, workspace: workspace, resultTransformer: resultTransformer, fallback: fallback())
}

func sendRequest<PositionRequest>(
_ req: Request<PositionRequest>,
params: PositionRequest,
url: URL,
workspace: Workspace,
resultTransformer: ((LSPResult<PositionRequest.Response>) -> LSPResult<PositionRequest.Response>)? = nil,
fallback: @autoclosure () -> PositionRequest.Response)
{
guard let service = workspace.documentService[url] else {
req.reply(fallback())
return
}

let id = service.send(req.params, queue: DispatchQueue.global()) { result in
req.reply(result)
let id = service.send(params, queue: DispatchQueue.global()) { result in
req.reply(resultTransformer?(result) ?? result)
}
req.cancellationToken.addCancellationHandler { [weak service] in
service?.send(CancelRequest(id: id))
Expand Down
Loading