Skip to content

Commit 942aaa4

Browse files
authored
Merge pull request swiftlang#114 from rockbruno/execute-command
Add ExecuteCommand/ApplyEdit
2 parents 888c66d + 3536217 commit 942aaa4

14 files changed

+406
-10
lines changed
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2019 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
/// Request from the server to the client to modify resources on the client side.
14+
///
15+
/// - Parameters:
16+
/// - label: An optional label of the workspace edit.
17+
/// - edit: The edits to apply.
18+
public struct ApplyEditRequest: RequestType {
19+
public static let method: String = "workspace/applyEdit"
20+
public typealias Response = ApplyEditResponse?
21+
22+
/// An optional label of the workspace edit.
23+
/// Used by the client's user interface for things such as
24+
/// the stack to undo the workspace edit.
25+
public var label: String?
26+
27+
/// The edits to apply.
28+
public var edit: WorkspaceEdit
29+
30+
public init(label: String? = nil, edit: WorkspaceEdit) {
31+
self.label = label
32+
self.edit = edit
33+
}
34+
}
35+
36+
public struct ApplyEditResponse: Codable, Hashable, ResponseType {
37+
/// Indicates whether the edit was applied or not.
38+
public var applied: Bool
39+
40+
/// An optional textual description for why the edit was not applied.
41+
public var failureReason: String?
42+
43+
public init(applied: Bool, failureReason: String?) {
44+
self.applied = applied
45+
self.failureReason = failureReason
46+
}
47+
}

Sources/LanguageServerProtocol/CodeAction.swift

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
//===----------------------------------------------------------------------===//
1212

1313
import Foundation
14+
import SKSupport
1415

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

127+
/// The workspace edit this code action performs.
128+
public var edit: WorkspaceEdit?
129+
126130
/// A command this code action executes.
127131
/// If a code action provides an edit and a command,
128132
/// first the edit is executed and then the command.
129133
public var command: Command?
130134

131-
public init(title: String, kind: CodeActionKind? = nil, diagnostics: [Diagnostic]? = nil, command: Command? = nil) {
135+
public init(title: String, kind: CodeActionKind? = nil, diagnostics: [Diagnostic]? = nil, edit: WorkspaceEdit? = nil, command: Command? = nil) {
132136
self.title = title
133137
self.kind = kind
134138
self.diagnostics = diagnostics
139+
self.edit = edit
135140
self.command = command
136141
}
137142
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2019 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
import Foundation
14+
15+
/// Request sent from the client to to trigger command execution on the server.
16+
///
17+
/// The execution of this request can be the result of a request that returns a command,
18+
/// such as CodeActionsRequest and CodeLensRequest. In most cases, the server creates a WorkspaceEdit
19+
/// structure and applies the changes to the workspace using the ApplyEditRequest.
20+
///
21+
/// Servers that provide command execution should set the `executeCommand` server capability.
22+
///
23+
/// - Parameters:
24+
/// - command: The command to be executed.
25+
/// - arguments: The arguments to use when executing the command.
26+
public struct ExecuteCommandRequest: RequestType {
27+
public static let method: String = "workspace/executeCommand"
28+
29+
// Note: The LSP type for this response is `Any?`.
30+
public typealias Response = LSPAny?
31+
32+
/// The command to be executed.
33+
public var command: String
34+
35+
/// Arguments that the command should be invoked with.
36+
public var arguments: [LSPAny]?
37+
38+
public init(command: String, arguments: [LSPAny]?) {
39+
self.command = command
40+
self.arguments = arguments
41+
}
42+
}

Sources/LanguageServerProtocol/LSPAny.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,8 @@ extension LSPAny: Encodable {
6868
}
6969
}
7070

71+
extension LSPAny: ResponseType {}
72+
7173
extension LSPAny: ExpressibleByNilLiteral {
7274
public init(nilLiteral _: ()) {
7375
self = .null

Sources/LanguageServerProtocol/Messages.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ public let builtinRequests: [_RequestType.Type] = [
3636
DocumentColorRequest.self,
3737
ColorPresentationRequest.self,
3838
CodeActionRequest.self,
39+
ExecuteCommandRequest.self,
3940

4041
// MARK: LSP Extension Requests
4142

Sources/LanguageServerProtocol/ServerCapabilities.swift

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,10 @@ public struct ServerCapabilities: Codable, Hashable {
5656

5757
/// The server provides workspace symbol support.
5858
public var workspaceSymbolProvider: Bool?
59-
59+
60+
/// Whether the server provides "workspace/executeCommand".
61+
public var executeCommandProvider: ExecuteCommandOptions?
62+
6063
// TODO: fill-in the rest.
6164

6265
public init(
@@ -74,7 +77,8 @@ public struct ServerCapabilities: Codable, Hashable {
7477
documentSymbolProvider: Bool? = nil,
7578
colorProvider: Bool? = nil,
7679
codeActionProvider: CodeActionServerCapabilities? = nil,
77-
workspaceSymbolProvider: Bool? = nil
80+
workspaceSymbolProvider: Bool? = nil,
81+
executeCommandProvider: ExecuteCommandOptions? = nil
7882
)
7983
{
8084
self.textDocumentSync = textDocumentSync
@@ -92,6 +96,7 @@ public struct ServerCapabilities: Codable, Hashable {
9296
self.colorProvider = colorProvider
9397
self.codeActionProvider = codeActionProvider
9498
self.workspaceSymbolProvider = workspaceSymbolProvider
99+
self.executeCommandProvider = executeCommandProvider
95100
}
96101

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

109115
if let textDocumentSync = try? container.decode(TextDocumentSyncOptions.self, forKey: .textDocumentSync) {
110116
self.textDocumentSync = textDocumentSync
@@ -246,3 +252,13 @@ public struct CodeActionOptions: Codable, Hashable {
246252
self.codeActionKinds = codeActionKinds
247253
}
248254
}
255+
256+
public struct ExecuteCommandOptions: Codable, Hashable {
257+
258+
/// The commands to be executed on this server.
259+
public var commands: [String]
260+
261+
public init(commands: [String]) {
262+
self.commands = commands
263+
}
264+
}

Sources/LanguageServerProtocol/TextDocumentIdentifier.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ public struct TextDocumentIdentifier: Hashable {
2828

2929
// Encode using the key "uri" to match LSP.
3030
extension TextDocumentIdentifier: Codable {
31-
private enum CodingKeys: String, CodingKey {
31+
public enum CodingKeys: String, CodingKey {
3232
case url = "uri"
3333
}
3434
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2019 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
/// A workspace edit represents changes to many resources managed in the workspace.
14+
public struct WorkspaceEdit: Codable, Hashable, ResponseType {
15+
16+
/// The edits to be applied to existing resources.
17+
public var changes: [URL: [TextEdit]]?
18+
19+
public init(changes: [URL: [TextEdit]]?) {
20+
self.changes = changes
21+
}
22+
}
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2018 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
import LanguageServerProtocol
14+
import Foundation
15+
import SKSupport
16+
17+
/// Represents metadata that SourceKit-LSP injects at every command returned by code actions.
18+
/// The ExecuteCommand is not a TextDocumentRequest, so metadata is injected to allow SourceKit-LSP
19+
/// to determine where a command should be executed.
20+
public struct SourceKitLSPCommandMetadata: Codable, Hashable {
21+
22+
public var sourcekitlsp_textDocument: TextDocumentIdentifier
23+
24+
public init?(fromLSPDictionary dictionary: [String: LSPAny]) {
25+
let textDocumentKey = CodingKeys.sourcekitlsp_textDocument.stringValue
26+
let urlKey = TextDocumentIdentifier.CodingKeys.url.stringValue
27+
guard case .dictionary(let textDocumentDict)? = dictionary[textDocumentKey],
28+
case .string(let urlString)? = textDocumentDict[urlKey],
29+
let url = URL(string: urlString) else
30+
{
31+
return nil
32+
}
33+
let textDocument = TextDocumentIdentifier(url)
34+
self.init(textDocument: textDocument)
35+
}
36+
37+
public init(textDocument: TextDocumentIdentifier) {
38+
self.sourcekitlsp_textDocument = textDocument
39+
}
40+
41+
public func encodeToLSPAny() -> LSPAny {
42+
let textDocumentArgument = LSPAny.dictionary(
43+
[TextDocumentIdentifier.CodingKeys.url.stringValue: .string(sourcekitlsp_textDocument.url.absoluteString)]
44+
)
45+
return .dictionary([CodingKeys.sourcekitlsp_textDocument.stringValue: textDocumentArgument])
46+
}
47+
}
48+
49+
extension CodeActionRequest {
50+
public func injectMetadata(toResponse response: CodeActionRequestResponse?) -> CodeActionRequestResponse? {
51+
let metadata = SourceKitLSPCommandMetadata(textDocument: textDocument)
52+
let metadataArgument = metadata.encodeToLSPAny()
53+
switch response {
54+
case .codeActions(var codeActions)?:
55+
for i in 0..<codeActions.count {
56+
codeActions[i].command?.arguments?.append(metadataArgument)
57+
}
58+
return .codeActions(codeActions)
59+
case .commands(var commands)?:
60+
for i in 0..<commands.count {
61+
commands[i].arguments?.append(metadataArgument)
62+
}
63+
return .commands(commands)
64+
case nil:
65+
return nil
66+
}
67+
}
68+
}
69+
70+
extension ExecuteCommandRequest {
71+
/// The document in which the command was invoked.
72+
public var textDocument: TextDocumentIdentifier? {
73+
return metadata?.sourcekitlsp_textDocument
74+
}
75+
76+
/// Optional metadata containing SourceKit-LSP information about this command.
77+
public var metadata: SourceKitLSPCommandMetadata? {
78+
guard case .dictionary(let dictionary)? = arguments?.last else {
79+
return nil
80+
}
81+
guard let metadata = SourceKitLSPCommandMetadata(fromLSPDictionary: dictionary) else {
82+
log("failed to decode lsp metadata in executeCommand request", level: .error)
83+
return nil
84+
}
85+
return metadata
86+
}
87+
88+
/// Returns this Command's arguments without SourceKit-LSP's injected metadata, if it exists.
89+
public var argumentsWithoutSourceKitMetadata: [LSPAny]? {
90+
guard metadata != nil else {
91+
return arguments
92+
}
93+
return arguments?.dropLast()
94+
}
95+
}

Sources/SourceKit/SourceKitServer.swift

Lines changed: 39 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ public final class SourceKitServer: LanguageServer {
8383
registerWorkspaceRequest(SourceKitServer.colorPresentation)
8484
registerWorkspaceRequest(SourceKitServer.codeAction)
8585
registerWorkspaceRequest(SourceKitServer.pollIndex)
86+
registerWorkspaceRequest(SourceKitServer.executeCommand)
8687
}
8788

8889
func registerWorkspaceRequest<R>(
@@ -313,7 +314,10 @@ extension SourceKitServer {
313314
codeActionOptions: CodeActionOptions(codeActionKinds: nil),
314315
supportsCodeActions: false // TODO: Turn it on after a provider is implemented.
315316
),
316-
workspaceSymbolProvider: true
317+
workspaceSymbolProvider: true,
318+
executeCommandProvider: ExecuteCommandOptions(
319+
commands: [] // FIXME: Clangd commands?
320+
)
317321
)))
318322
}
319323

@@ -459,7 +463,25 @@ extension SourceKitServer {
459463
}
460464

461465
func codeAction(_ req: Request<CodeActionRequest>, workspace: Workspace) {
462-
toolchainTextDocumentRequest(req, workspace: workspace, fallback: nil)
466+
toolchainTextDocumentRequest(req, workspace: workspace, resultTransformer: { result in
467+
switch result {
468+
case .success(let reply):
469+
return .success(req.params.injectMetadata(toResponse: reply))
470+
default:
471+
return result
472+
}
473+
}, fallback: nil)
474+
}
475+
476+
func executeCommand(_ req: Request<ExecuteCommandRequest>, workspace: Workspace) {
477+
guard let url = req.params.textDocument?.url else {
478+
log("attempted to perform executeCommand request without an url!", level: .error)
479+
req.reply(nil)
480+
return
481+
}
482+
var params = req.params
483+
params.arguments = params.argumentsWithoutSourceKitMetadata
484+
sendRequest(req, params: params, url: url, workspace: workspace, fallback: nil)
463485
}
464486

465487
func definition(_ req: Request<DefinitionRequest>, workspace: Workspace) {
@@ -628,16 +650,28 @@ extension SourceKitServer {
628650
func toolchainTextDocumentRequest<PositionRequest>(
629651
_ req: Request<PositionRequest>,
630652
workspace: Workspace,
653+
resultTransformer: ((LSPResult<PositionRequest.Response>) -> LSPResult<PositionRequest.Response>)? = nil,
631654
fallback: @autoclosure () -> PositionRequest.Response)
632655
where PositionRequest: TextDocumentRequest
633656
{
634-
guard let service = workspace.documentService[req.params.textDocument.url] else {
657+
sendRequest(req, params: req.params, url: req.params.textDocument.url, workspace: workspace, resultTransformer: resultTransformer, fallback: fallback())
658+
}
659+
660+
func sendRequest<PositionRequest>(
661+
_ req: Request<PositionRequest>,
662+
params: PositionRequest,
663+
url: URL,
664+
workspace: Workspace,
665+
resultTransformer: ((LSPResult<PositionRequest.Response>) -> LSPResult<PositionRequest.Response>)? = nil,
666+
fallback: @autoclosure () -> PositionRequest.Response)
667+
{
668+
guard let service = workspace.documentService[url] else {
635669
req.reply(fallback())
636670
return
637671
}
638672

639-
let id = service.send(req.params, queue: DispatchQueue.global()) { result in
640-
req.reply(result)
673+
let id = service.send(params, queue: DispatchQueue.global()) { result in
674+
req.reply(resultTransformer?(result) ?? result)
641675
}
642676
req.cancellationToken.addCancellationHandler { [weak service] in
643677
service?.send(CancelRequest(id: id))

0 commit comments

Comments
 (0)