From cef91670f583c2e1fa996c335d63785347c2c90a Mon Sep 17 00:00:00 2001 From: Bruno Rocha Date: Wed, 19 Jun 2019 16:28:44 -0300 Subject: [PATCH 1/6] Add ExecuteCommand/ApplyEdit --- .../LanguageServerProtocol/ApplyEdit.swift | 47 ++++++++ .../LanguageServerProtocol/CodeAction.swift | 6 +- .../LanguageServerProtocol/Connection.swift | 2 +- .../ExecuteCommand.swift | 40 +++++++ Sources/LanguageServerProtocol/Messages.swift | 1 + .../ServerCapabilities.swift | 20 +++- .../WorkspaceEdit.swift | 26 +++++ Sources/SourceKit/SourceKitServer.swift | 31 ++++- .../SourceKit/sourcekitd/SwiftCommand.swift | 110 ++++++++++++++++++ .../sourcekitd/SwiftLanguageServer.swift | 33 +++++- .../SourceKitTests/ExecuteCommandTests.swift | 100 ++++++++++++++++ Tests/SourceKitTests/XCTestManifests.swift | 10 ++ 12 files changed, 420 insertions(+), 6 deletions(-) create mode 100644 Sources/LanguageServerProtocol/ApplyEdit.swift create mode 100644 Sources/LanguageServerProtocol/ExecuteCommand.swift create mode 100644 Sources/LanguageServerProtocol/WorkspaceEdit.swift create mode 100644 Sources/SourceKit/sourcekitd/SwiftCommand.swift create mode 100644 Tests/SourceKitTests/ExecuteCommandTests.swift diff --git a/Sources/LanguageServerProtocol/ApplyEdit.swift b/Sources/LanguageServerProtocol/ApplyEdit.swift new file mode 100644 index 000000000..df37360bb --- /dev/null +++ b/Sources/LanguageServerProtocol/ApplyEdit.swift @@ -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 + } +} diff --git a/Sources/LanguageServerProtocol/CodeAction.swift b/Sources/LanguageServerProtocol/CodeAction.swift index bf7e15e05..fc7f08170 100644 --- a/Sources/LanguageServerProtocol/CodeAction.swift +++ b/Sources/LanguageServerProtocol/CodeAction.swift @@ -123,15 +123,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 } } diff --git a/Sources/LanguageServerProtocol/Connection.swift b/Sources/LanguageServerProtocol/Connection.swift index cb084e922..3e4b99e7f 100644 --- a/Sources/LanguageServerProtocol/Connection.swift +++ b/Sources/LanguageServerProtocol/Connection.swift @@ -78,7 +78,7 @@ public final class LocalConnection { var state: State = .ready - var handler: MessageHandler? = nil + public internal(set) var handler: MessageHandler? = nil public init() {} diff --git a/Sources/LanguageServerProtocol/ExecuteCommand.swift b/Sources/LanguageServerProtocol/ExecuteCommand.swift new file mode 100644 index 000000000..2e3bc752b --- /dev/null +++ b/Sources/LanguageServerProtocol/ExecuteCommand.swift @@ -0,0 +1,40 @@ +//===----------------------------------------------------------------------===// +// +// 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 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 = CommandArgumentType? + + /// The command to be executed. + public var command: String + + /// Arguments that the command should be invoked with. + public var arguments: [CommandArgumentType]? + + public init(command: String, arguments: [CommandArgumentType]?) { + self.command = command + self.arguments = arguments + } +} diff --git a/Sources/LanguageServerProtocol/Messages.swift b/Sources/LanguageServerProtocol/Messages.swift index 1f87e191b..67dc7d136 100644 --- a/Sources/LanguageServerProtocol/Messages.swift +++ b/Sources/LanguageServerProtocol/Messages.swift @@ -36,6 +36,7 @@ public let builtinRequests: [_RequestType.Type] = [ DocumentColorRequest.self, ColorPresentationRequest.self, CodeActionRequest.self, + ExecuteCommandRequest.self, // MARK: LSP Extension Requests diff --git a/Sources/LanguageServerProtocol/ServerCapabilities.swift b/Sources/LanguageServerProtocol/ServerCapabilities.swift index 0695e6a58..1a4fe96ce 100644 --- a/Sources/LanguageServerProtocol/ServerCapabilities.swift +++ b/Sources/LanguageServerProtocol/ServerCapabilities.swift @@ -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( @@ -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 @@ -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 { @@ -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 @@ -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 + } +} diff --git a/Sources/LanguageServerProtocol/WorkspaceEdit.swift b/Sources/LanguageServerProtocol/WorkspaceEdit.swift new file mode 100644 index 000000000..e23e137ad --- /dev/null +++ b/Sources/LanguageServerProtocol/WorkspaceEdit.swift @@ -0,0 +1,26 @@ +//===----------------------------------------------------------------------===// +// +// 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: [String: [TextEdit]]? + + public init(changes: [URL: [TextEdit]]?) { + guard let changes = changes else { + return + } + let changesArray = changes.map { ($0.key.absoluteString, $0.value) } + self.changes = Dictionary(uniqueKeysWithValues: changesArray) + } +} diff --git a/Sources/SourceKit/SourceKitServer.swift b/Sources/SourceKit/SourceKitServer.swift index c3554aff0..3bfa9a5de 100644 --- a/Sources/SourceKit/SourceKitServer.swift +++ b/Sources/SourceKit/SourceKitServer.swift @@ -83,6 +83,7 @@ public final class SourceKitServer: LanguageServer { registerWorkspaceRequest(SourceKitServer.colorPresentation) registerWorkspaceRequest(SourceKitServer.codeAction) registerWorkspaceRequest(SourceKitServer.pollIndex) + registerWorkspaceRequest(SourceKitServer.executeCommand) } func registerWorkspaceRequest( @@ -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: builtinSwiftCommands // FIXME: Clangd commands? + ) ))) } @@ -462,6 +466,31 @@ extension SourceKitServer { toolchainTextDocumentRequest(req, workspace: workspace, fallback: nil) } + func executeCommand(_ req: Request, workspace: Workspace) { + guard let service = serviceFor(executeCommandRequest: req.params, workspace: workspace) else { + req.reply(nil) + return + } + let id = service.send(req.params, queue: DispatchQueue.global()) { result in + req.reply(result) + } + req.cancellationToken.addCancellationHandler { [weak service] in + service?.send(CancelRequest(id: id)) + } + } + + 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. + let connections = workspace.documentService.values.compactMap { $0 as? LocalConnection } + let isSwiftCommand = Command.isCommandIdentifierFromSwiftLSP(req.command) + if isSwiftCommand { + return connections.first { $0.handler is SwiftLanguageServer } + } else { + return connections.first { $0.handler is ClangLanguageServerShim } + } + } + func definition(_ req: Request, workspace: Workspace) { // FIXME: sending yourself a request isn't very convenient diff --git a/Sources/SourceKit/sourcekitd/SwiftCommand.swift b/Sources/SourceKit/sourcekitd/SwiftCommand.swift new file mode 100644 index 000000000..4acbd0519 --- /dev/null +++ b/Sources/SourceKit/sourcekitd/SwiftCommand.swift @@ -0,0 +1,110 @@ +//===----------------------------------------------------------------------===// +// +// 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 SKSupport +import LanguageServerProtocol +import Foundation + +/// The set of known Swift commands. +/// +/// All commands from the Swift LSP should be listed here. +public let builtinSwiftCommands: [String] = [] + +/// A `Command` that should be executed by Swift's language server. +public protocol SwiftCommand: Codable, Hashable { + var title: String { get set } + static var swiftCommandIdentifier: String { get } +} + +extension SwiftCommand { + public static var identifier: String { + return Command.swiftCommandIdentifierPrefix + Self.swiftCommandIdentifier + } + + /// 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) + return Command(title: title, command: Self.identifier, arguments: [argument]) + } +} + +extension Command { + /// The prefix applied to the identifier of Swift-specific LSP `Command`s. + fileprivate static var swiftCommandIdentifierPrefix: String { + return "swift.lsp." + } + + /// Returns true if the provided command identifier should be handled by Swift's language server. + /// + /// - Parameters: + /// - command: The command identifier. + public static func isCommandIdentifierFromSwiftLSP(_ command: String) -> Bool { + return command.hasPrefix(Command.swiftCommandIdentifierPrefix) + } +} + +extension ExecuteCommandRequest { + /// Attempts to convert the underlying `Command` metadata from this request + /// to a specific Swift language server `SwiftCommand`. + /// + /// - Parameters: + /// - type: The `SwiftCommand` metatype to convert to. + public func swiftCommand(ofType type: T.Type) -> T? { + guard type.identifier == command else { + return nil + } + guard let argument = arguments?.first else { + return nil + } + guard case let .dictionary(dictionary) = argument else { + return nil + } + guard let data = try? JSONEncoder().encode(dictionary) else { + return nil + } + return try? JSONDecoder().decode(type, from: data) + } +} + +public struct SemanticRefactorCommand: SwiftCommand { + public static var swiftCommandIdentifier: String { + return "semantic.refactor.command" + } + + /// The name of this refactoring action. + public var title: String + + /// The sourcekitd identifier of the refactoring action. + public var actionString: String + + /// The starting line of the range to refactor. + public var line: Int + + /// The starting column of the range to refactor. + public var column: Int + + /// The length of the range to refactor. + public var length: Int + + /// The text document related to the refactoring action. + public var textDocument: TextDocumentIdentifier + + public init(title: String, actionString: String, line: Int, column: Int, length: Int, textDocument: TextDocumentIdentifier) { + self.title = title + self.actionString = actionString + self.line = line + self.column = column + self.length = length + self.textDocument = textDocument + } +} diff --git a/Sources/SourceKit/sourcekitd/SwiftLanguageServer.swift b/Sources/SourceKit/sourcekitd/SwiftLanguageServer.swift index 2eee02cfe..d8086372c 100644 --- a/Sources/SourceKit/sourcekitd/SwiftLanguageServer.swift +++ b/Sources/SourceKit/sourcekitd/SwiftLanguageServer.swift @@ -73,6 +73,7 @@ public final class SwiftLanguageServer: LanguageServer { _register(SwiftLanguageServer.documentColor) _register(SwiftLanguageServer.colorPresentation) _register(SwiftLanguageServer.codeAction) + _register(SwiftLanguageServer.executeCommand) } func publishDiagnostics( @@ -167,7 +168,9 @@ extension SwiftLanguageServer { codeActionProvider: CodeActionServerCapabilities( clientCapabilities: request.params.capabilities.textDocument?.codeAction, codeActionOptions: CodeActionOptions(codeActionKinds: nil), - supportsCodeActions: false) // TODO: Turn it on after a provider is implemented. + supportsCodeActions: false), // TODO: Turn it on after a provider is implemented. + executeCommandProvider: ExecuteCommandOptions( + commands: builtinSwiftCommands) ))) } @@ -921,6 +924,34 @@ extension SwiftLanguageServer { } } } + + func executeCommand(_ req: Request) { + //TODO: Implement commands. + return req.reply(nil) + } + + func applyEdit(label: String, edit: WorkspaceEdit) { + let req = ApplyEditRequest(label: label, edit: edit) + let handle = client.send(req, queue: queue) { reply in + switch reply { + case .success(let response): + if response?.applied == false { + let reason: String + if let failureReason = response?.failureReason { + reason = " reason: \(failureReason)" + } else { + reason = "" + } + log("client refused to apply edit for \(label)!\(reason)", level: .warning) + } + case .failure(let error): + log("applyEdit failed: \(error)", level: .warning) + } + } + + // FIXME: cancellation + _ = handle + } } extension DocumentSnapshot { diff --git a/Tests/SourceKitTests/ExecuteCommandTests.swift b/Tests/SourceKitTests/ExecuteCommandTests.swift new file mode 100644 index 000000000..ce72401e8 --- /dev/null +++ b/Tests/SourceKitTests/ExecuteCommandTests.swift @@ -0,0 +1,100 @@ +//===----------------------------------------------------------------------===// +// +// 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 LanguageServerProtocol +import SKCore +import SKSupport +import SKTestSupport +import XCTest + +@testable import SourceKit + +final class ExecuteCommandTests: XCTestCase { + + /// Connection and lifetime management for the service. + var connection: TestSourceKitServer! = nil + + /// The primary interface to make requests to the SourceKitServer. + var sk: TestClient! = nil + + /// The server's workspace data. Accessing this is unsafe if the server does so concurrently. + var workspace: Workspace! = nil + + override func tearDown() { + workspace = nil + sk = nil + connection = nil + } + + override func setUp() { + connection = TestSourceKitServer() + sk = connection.client + _ = try! sk.sendSync(InitializeRequest( + processId: nil, + rootPath: nil, + rootURL: nil, + initializationOptions: nil, + capabilities: ClientCapabilities(workspace: nil, textDocument: nil), + trace: .off, + workspaceFolders: nil)) + + workspace = connection.server!.workspace! + } + + func testCommandIsRoutedToTheCorrectServer() { + +// FIXME: See comment on sendNoteSync. +#if os(macOS) + + let haveClangd = ToolchainRegistry.shared.toolchains.contains { $0.clangd != nil } + if !haveClangd { + XCTFail("Cannot find clangd in toolchain") + } + + var req = ExecuteCommandRequest(command: "swift.lsp.command", arguments: nil) + var service = connection.server?.serviceFor(executeCommandRequest: req, workspace: workspace) + XCTAssertNil(service) + + sk.allowUnexpectedNotification = true + let swiftUrl = URL(fileURLWithPath: "/a.swift") + sk.sendNoteSync(DidOpenTextDocument(textDocument: TextDocumentItem( + url: swiftUrl, + language: .swift, + version: 12, + text: """ + var foo = "" + """)), { (note: Notification) in + XCTAssertEqual(note.params.diagnostics.count, 0) + XCTAssertEqual("var foo = \"\"", self.workspace.documentManager.latestSnapshot(swiftUrl)!.text) + }) + let cUrl = URL(fileURLWithPath: "/b.cpp") + sk.sendNoteSync(DidOpenTextDocument(textDocument: TextDocumentItem( + url: cUrl, + language: .cpp, + version: 1, + text: """ + int foo = 1; + """)), { (note: Notification) in + XCTAssertEqual(note.params.diagnostics.count, 0) + XCTAssertEqual("int foo = 1;", self.workspace.documentManager.latestSnapshot(cUrl)!.text) + }) + + XCTAssertEqual(workspace.documentService.count, 2) + service = connection.server?.serviceFor(executeCommandRequest: req, workspace: workspace) + XCTAssertTrue((service as? LocalConnection)?.handler is SwiftLanguageServer) + req = ExecuteCommandRequest(command: "generic.lsp.command", arguments: nil) + service = connection.server?.serviceFor(executeCommandRequest: req, workspace: workspace) + XCTAssertTrue((service as? LocalConnection)?.handler is ClangLanguageServerShim) + +#endif + } +} diff --git a/Tests/SourceKitTests/XCTestManifests.swift b/Tests/SourceKitTests/XCTestManifests.swift index 610d3d107..101c69b00 100644 --- a/Tests/SourceKitTests/XCTestManifests.swift +++ b/Tests/SourceKitTests/XCTestManifests.swift @@ -48,6 +48,15 @@ extension DocumentSymbolTest { ] } +extension ExecuteCommandTests { + // DO NOT MODIFY: This is autogenerated, use: + // `swift test --generate-linuxmain` + // to regenerate. + static let __allTests__ExecuteCommandTests = [ + ("testCommandIsRoutedToTheCorrectServer", testCommandIsRoutedToTheCorrectServer), + ] +} + extension FoldingRangeTests { // DO NOT MODIFY: This is autogenerated, use: // `swift test --generate-linuxmain` @@ -134,6 +143,7 @@ public func __allTests() -> [XCTestCaseEntry] { testCase(CodeActionTests.__allTests__CodeActionTests), testCase(DocumentColorTests.__allTests__DocumentColorTests), testCase(DocumentSymbolTest.__allTests__DocumentSymbolTest), + testCase(ExecuteCommandTests.__allTests__ExecuteCommandTests), testCase(FoldingRangeTests.__allTests__FoldingRangeTests), testCase(ImplementationTests.__allTests__ImplementationTests), testCase(LocalClangTests.__allTests__LocalClangTests), From e71965fc7d67d0f8beae24c77fbd295194cf5342 Mon Sep 17 00:00:00 2001 From: Bruno Rocha Date: Mon, 8 Jul 2019 07:52:18 -0300 Subject: [PATCH 2/6] Inject metadata at commands --- .../LanguageServerProtocol/CodeAction.swift | 25 +++++ .../LanguageServerProtocol/Connection.swift | 2 +- .../ExecuteCommand.swift | 29 +++++ Sources/SourceKit/SourceKitServer.swift | 46 ++++---- .../SourceKit/sourcekitd/SwiftCommand.swift | 23 +--- Tests/SourceKitTests/CodeActionTests.swift | 36 +++++++ .../SourceKitTests/ExecuteCommandTests.swift | 100 ------------------ Tests/SourceKitTests/XCTestManifests.swift | 11 +- 8 files changed, 118 insertions(+), 154 deletions(-) delete mode 100644 Tests/SourceKitTests/ExecuteCommandTests.swift diff --git a/Sources/LanguageServerProtocol/CodeAction.swift b/Sources/LanguageServerProtocol/CodeAction.swift index fc7f08170..28b222dc8 100644 --- a/Sources/LanguageServerProtocol/CodeAction.swift +++ b/Sources/LanguageServerProtocol/CodeAction.swift @@ -11,6 +11,7 @@ //===----------------------------------------------------------------------===// import Foundation +import SKSupport public typealias CodeActionProviderCompletion = (([CodeAction]) -> Void) public typealias CodeActionProvider = ((CodeActionRequest, @escaping CodeActionProviderCompletion) -> Void) @@ -47,6 +48,30 @@ public struct CodeActionRequest: TextDocumentRequest, Hashable { self.context = context self.textDocument = textDocument } + + 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 + { + log("failed to inject metadata in codeAction response", level: .error) + return nil + } + switch response { + case .codeActions(var codeActions)?: + for i in 0.., workspace: Workspace) { - toolchainTextDocumentRequest(req, workspace: workspace, fallback: nil) + toolchainTextDocumentRequest(req, workspace: workspace, resultHandler: { result in + switch result { + case .success(let reply): + return .success(req.params.injectMetadata(atResponse: reply)) + default: + return result + } + }, fallback: nil) } func executeCommand(_ req: Request, workspace: Workspace) { - guard let service = serviceFor(executeCommandRequest: req.params, workspace: workspace) else { + guard let url = req.params.textDocument?.url else { + log("attempted to perform executeCommand request without an url!", level: .error) req.reply(nil) return } - let id = service.send(req.params, queue: DispatchQueue.global()) { result in - req.reply(result) - } - req.cancellationToken.addCancellationHandler { [weak service] in - service?.send(CancelRequest(id: id)) - } - } - - 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. - let connections = workspace.documentService.values.compactMap { $0 as? LocalConnection } - let isSwiftCommand = Command.isCommandIdentifierFromSwiftLSP(req.command) - if isSwiftCommand { - return connections.first { $0.handler is SwiftLanguageServer } - } else { - return connections.first { $0.handler is ClangLanguageServerShim } - } + sendRequest(req, url: url, workspace: workspace, fallback: nil) } func definition(_ req: Request, workspace: Workspace) { @@ -657,16 +648,27 @@ extension SourceKitServer { func toolchainTextDocumentRequest( _ req: Request, workspace: Workspace, + resultHandler: ((LSPResult) -> LSPResult)? = nil, fallback: @autoclosure () -> PositionRequest.Response) where PositionRequest: TextDocumentRequest { - guard let service = workspace.documentService[req.params.textDocument.url] else { + sendRequest(req, url: req.params.textDocument.url, workspace: workspace, resultHandler: resultHandler, fallback: fallback) + } + + func sendRequest( + _ req: Request, + url: URL, + workspace: Workspace, + resultHandler: ((LSPResult) -> LSPResult)? = 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) + req.reply(resultHandler?(result) ?? result) } req.cancellationToken.addCancellationHandler { [weak service] in service?.send(CancelRequest(id: id)) diff --git a/Sources/SourceKit/sourcekitd/SwiftCommand.swift b/Sources/SourceKit/sourcekitd/SwiftCommand.swift index 4acbd0519..cb130ce6b 100644 --- a/Sources/SourceKit/sourcekitd/SwiftCommand.swift +++ b/Sources/SourceKit/sourcekitd/SwiftCommand.swift @@ -21,15 +21,11 @@ public let builtinSwiftCommands: [String] = [] /// A `Command` that should be executed by Swift's language server. public protocol SwiftCommand: Codable, Hashable { + static var identifier: String { get } var title: String { get set } - static var swiftCommandIdentifier: String { get } } extension SwiftCommand { - public static var identifier: String { - return Command.swiftCommandIdentifierPrefix + Self.swiftCommandIdentifier - } - /// Converts this `SwiftCommand` to a generic LSP `Command` object. public func asCommand() throws -> Command { let data = try JSONEncoder().encode(self) @@ -38,21 +34,6 @@ extension SwiftCommand { } } -extension Command { - /// The prefix applied to the identifier of Swift-specific LSP `Command`s. - fileprivate static var swiftCommandIdentifierPrefix: String { - return "swift.lsp." - } - - /// Returns true if the provided command identifier should be handled by Swift's language server. - /// - /// - Parameters: - /// - command: The command identifier. - public static func isCommandIdentifierFromSwiftLSP(_ command: String) -> Bool { - return command.hasPrefix(Command.swiftCommandIdentifierPrefix) - } -} - extension ExecuteCommandRequest { /// Attempts to convert the underlying `Command` metadata from this request /// to a specific Swift language server `SwiftCommand`. @@ -77,7 +58,7 @@ extension ExecuteCommandRequest { } public struct SemanticRefactorCommand: SwiftCommand { - public static var swiftCommandIdentifier: String { + public static var identifier: String { return "semantic.refactor.command" } diff --git a/Tests/SourceKitTests/CodeActionTests.swift b/Tests/SourceKitTests/CodeActionTests.swift index e29c66bce..83e3ce060 100644 --- a/Tests/SourceKitTests/CodeActionTests.swift +++ b/Tests/SourceKitTests/CodeActionTests.swift @@ -105,6 +105,42 @@ final class CodeActionTests: XCTestCase { XCTAssertEqual(response, .codeActions([unspecifiedAction])) } + func testCodeActionResponseCommandMetadataInjection() { + let url = URL(fileURLWithPath: "/a.swift") + let textDocument = TextDocumentIdentifier(url) + let expectedMetadata: CommandArgumentType = { + let metadata = SourceKitLSPCommandMetadata(textDocument: textDocument) + let data = try! JSONEncoder().encode(metadata) + return try! JSONDecoder().decode(CommandArgumentType.self, from: data) + }() + XCTAssertEqual(expectedMetadata, .dictionary(["textDocument": ["uri": "file:///a.swift"]])) + let command = Command(title: "Title", command: "Command", arguments: [1, "text", 2.2, nil]) + let codeAction = CodeAction(title: "1") + let codeAction2 = CodeAction(title: "2", command: command) + let request = CodeActionRequest(range: Position(line: 0, utf16index: 0)..) in - XCTAssertEqual(note.params.diagnostics.count, 0) - XCTAssertEqual("var foo = \"\"", self.workspace.documentManager.latestSnapshot(swiftUrl)!.text) - }) - let cUrl = URL(fileURLWithPath: "/b.cpp") - sk.sendNoteSync(DidOpenTextDocument(textDocument: TextDocumentItem( - url: cUrl, - language: .cpp, - version: 1, - text: """ - int foo = 1; - """)), { (note: Notification) in - XCTAssertEqual(note.params.diagnostics.count, 0) - XCTAssertEqual("int foo = 1;", self.workspace.documentManager.latestSnapshot(cUrl)!.text) - }) - - XCTAssertEqual(workspace.documentService.count, 2) - service = connection.server?.serviceFor(executeCommandRequest: req, workspace: workspace) - XCTAssertTrue((service as? LocalConnection)?.handler is SwiftLanguageServer) - req = ExecuteCommandRequest(command: "generic.lsp.command", arguments: nil) - service = connection.server?.serviceFor(executeCommandRequest: req, workspace: workspace) - XCTAssertTrue((service as? LocalConnection)?.handler is ClangLanguageServerShim) - -#endif - } -} diff --git a/Tests/SourceKitTests/XCTestManifests.swift b/Tests/SourceKitTests/XCTestManifests.swift index 101c69b00..8284ab560 100644 --- a/Tests/SourceKitTests/XCTestManifests.swift +++ b/Tests/SourceKitTests/XCTestManifests.swift @@ -17,6 +17,7 @@ extension CodeActionTests { // `swift test --generate-linuxmain` // to regenerate. static let __allTests__CodeActionTests = [ + ("testCodeActionResponseCommandMetadataInjection", testCodeActionResponseCommandMetadataInjection), ("testCodeActionResponseLegacySupport", testCodeActionResponseLegacySupport), ("testCodeActionResponseRespectsSupportedKinds", testCodeActionResponseRespectsSupportedKinds), ("testCommandEncoding", testCommandEncoding), @@ -48,15 +49,6 @@ extension DocumentSymbolTest { ] } -extension ExecuteCommandTests { - // DO NOT MODIFY: This is autogenerated, use: - // `swift test --generate-linuxmain` - // to regenerate. - static let __allTests__ExecuteCommandTests = [ - ("testCommandIsRoutedToTheCorrectServer", testCommandIsRoutedToTheCorrectServer), - ] -} - extension FoldingRangeTests { // DO NOT MODIFY: This is autogenerated, use: // `swift test --generate-linuxmain` @@ -143,7 +135,6 @@ public func __allTests() -> [XCTestCaseEntry] { testCase(CodeActionTests.__allTests__CodeActionTests), testCase(DocumentColorTests.__allTests__DocumentColorTests), testCase(DocumentSymbolTest.__allTests__DocumentSymbolTest), - testCase(ExecuteCommandTests.__allTests__ExecuteCommandTests), testCase(FoldingRangeTests.__allTests__FoldingRangeTests), testCase(ImplementationTests.__allTests__ImplementationTests), testCase(LocalClangTests.__allTests__LocalClangTests), From 7180d66e607c9c702a93e662eeddcf3f8622a401 Mon Sep 17 00:00:00 2001 From: Bruno Rocha Date: Fri, 12 Jul 2019 14:26:38 -0300 Subject: [PATCH 3/6] Avoid using decoders for retrieving dictionary data --- .../LanguageServerProtocol/CodeAction.swift | 24 ----- .../ExecuteCommand.swift | 27 ------ .../TextDocumentIdentifier.swift | 2 +- .../SourceKitLSPCommandMetadata.swift | 93 +++++++++++++++++++ Sources/SourceKit/SourceKitServer.swift | 9 +- .../SourceKit/sourcekitd/SwiftCommand.swift | 43 +++++++-- Tests/SourceKitTests/CodeActionTests.swift | 2 +- .../SourceKitTests/ExecuteCommandTests.swift | 54 +++++++++++ Tests/SourceKitTests/XCTestManifests.swift | 11 +++ 9 files changed, 203 insertions(+), 62 deletions(-) create mode 100644 Sources/SourceKit/SourceKitLSPCommandMetadata.swift create mode 100644 Tests/SourceKitTests/ExecuteCommandTests.swift diff --git a/Sources/LanguageServerProtocol/CodeAction.swift b/Sources/LanguageServerProtocol/CodeAction.swift index 28b222dc8..93d34209e 100644 --- a/Sources/LanguageServerProtocol/CodeAction.swift +++ b/Sources/LanguageServerProtocol/CodeAction.swift @@ -48,30 +48,6 @@ public struct CodeActionRequest: TextDocumentRequest, Hashable { self.context = context self.textDocument = textDocument } - - 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 - { - log("failed to inject metadata in codeAction response", level: .error) - return nil - } - switch response { - case .codeActions(var codeActions)?: - for i in 0.. SourceKitLSPCommandMetadata? { + guard case .dictionary(let textDocumentDict)? = dictionary[CodingKeys.sourcekitlsp_textDocument.stringValue], + case .string(let urlString)? = textDocumentDict[TextDocumentIdentifier.CodingKeys.url.stringValue], + let url = URL(string: urlString) else + { + return nil + } + let textDocument = TextDocumentIdentifier(url) + return SourceKitLSPCommandMetadata(textDocument: textDocument) + } + + public var sourcekitlsp_textDocument: TextDocumentIdentifier + + public init(textDocument: TextDocumentIdentifier) { + self.sourcekitlsp_textDocument = textDocument + } + + func asCommandArgument() -> CommandArgumentType { + let textDocumentArgument = CommandArgumentType.dictionary( + [TextDocumentIdentifier.CodingKeys.url.stringValue: .string(sourcekitlsp_textDocument.url.absoluteString)] + ) + return .dictionary([CodingKeys.sourcekitlsp_textDocument.stringValue: textDocumentArgument]) + } +} + +extension CodeActionRequest { + public func injectMetadata(atResponse response: CodeActionRequestResponse?) -> CodeActionRequestResponse? { + let metadata = SourceKitLSPCommandMetadata(textDocument: textDocument) + let metadataArgument = metadata.asCommandArgument() + switch response { + case .codeActions(var codeActions)?: + for i in 0.., workspace: Workspace) { @@ -652,11 +654,12 @@ extension SourceKitServer { fallback: @autoclosure () -> PositionRequest.Response) where PositionRequest: TextDocumentRequest { - sendRequest(req, url: req.params.textDocument.url, workspace: workspace, resultHandler: resultHandler, fallback: fallback) + sendRequest(req, params: req.params, url: req.params.textDocument.url, workspace: workspace, resultHandler: resultHandler, fallback: fallback) } func sendRequest( _ req: Request, + params: PositionRequest, url: URL, workspace: Workspace, resultHandler: ((LSPResult) -> LSPResult)? = nil, @@ -667,7 +670,7 @@ extension SourceKitServer { return } - let id = service.send(req.params, queue: DispatchQueue.global()) { result in + let id = service.send(params, queue: DispatchQueue.global()) { result in req.reply(resultHandler?(result) ?? result) } req.cancellationToken.addCancellationHandler { [weak service] in diff --git a/Sources/SourceKit/sourcekitd/SwiftCommand.swift b/Sources/SourceKit/sourcekitd/SwiftCommand.swift index cb130ce6b..6820b0212 100644 --- a/Sources/SourceKit/sourcekitd/SwiftCommand.swift +++ b/Sources/SourceKit/sourcekitd/SwiftCommand.swift @@ -22,14 +22,15 @@ public let builtinSwiftCommands: [String] = [] /// A `Command` that should be executed by Swift's language server. public protocol SwiftCommand: Codable, Hashable { static var identifier: String { get } + static func decode(fromDictionary dictionary: [String: CommandArgumentType]) -> Self? var title: String { get set } + func asCommandArgument() -> CommandArgumentType } extension SwiftCommand { /// 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) + let argument = asCommandArgument() return Command(title: title, command: Self.identifier, arguments: [argument]) } } @@ -50,14 +51,12 @@ extension ExecuteCommandRequest { guard case let .dictionary(dictionary) = argument else { return nil } - guard let data = try? JSONEncoder().encode(dictionary) else { - return nil - } - return try? JSONDecoder().decode(type, from: data) + return type.decode(fromDictionary: dictionary) } } public struct SemanticRefactorCommand: SwiftCommand { + public static var identifier: String { return "semantic.refactor.command" } @@ -80,6 +79,26 @@ public struct SemanticRefactorCommand: SwiftCommand { /// The text document related to the refactoring action. public var textDocument: TextDocumentIdentifier + public static func decode(fromDictionary dictionary: [String: CommandArgumentType]) -> SemanticRefactorCommand? { + guard case .dictionary(let dict)? = dictionary[CodingKeys.textDocument.stringValue], + case .string(let title)? = dictionary[CodingKeys.title.stringValue], + case .string(let actionString)? = dictionary[CodingKeys.actionString.stringValue], + case .int(let line)? = dictionary[CodingKeys.line.stringValue], + case .int(let column)? = dictionary[CodingKeys.column.stringValue], + case .int(let length)? = dictionary[CodingKeys.length.stringValue], + case .string(let urlString)? = dict[TextDocumentIdentifier.CodingKeys.url.stringValue], + let url = URL(string: urlString) else + { + return nil + } + return SemanticRefactorCommand(title: title, + actionString: actionString, + line: line, + column: column, + length: length, + textDocument: TextDocumentIdentifier(url)) + } + public init(title: String, actionString: String, line: Int, column: Int, length: Int, textDocument: TextDocumentIdentifier) { self.title = title self.actionString = actionString @@ -88,4 +107,16 @@ public struct SemanticRefactorCommand: SwiftCommand { self.length = length self.textDocument = textDocument } + + public func asCommandArgument() -> CommandArgumentType { + let textDocumentArgument = CommandArgumentType.dictionary( + [TextDocumentIdentifier.CodingKeys.url.stringValue: .string(textDocument.url.absoluteString)] + ) + return .dictionary([CodingKeys.title.stringValue: .string(title), + CodingKeys.actionString.stringValue: .string(actionString), + CodingKeys.line.stringValue: .int(line), + CodingKeys.column.stringValue: .int(column), + CodingKeys.length.stringValue: .int(length), + CodingKeys.textDocument.stringValue: textDocumentArgument]) + } } diff --git a/Tests/SourceKitTests/CodeActionTests.swift b/Tests/SourceKitTests/CodeActionTests.swift index 83e3ce060..9560758ce 100644 --- a/Tests/SourceKitTests/CodeActionTests.swift +++ b/Tests/SourceKitTests/CodeActionTests.swift @@ -113,7 +113,7 @@ final class CodeActionTests: XCTestCase { let data = try! JSONEncoder().encode(metadata) return try! JSONDecoder().decode(CommandArgumentType.self, from: data) }() - XCTAssertEqual(expectedMetadata, .dictionary(["textDocument": ["uri": "file:///a.swift"]])) + XCTAssertEqual(expectedMetadata, .dictionary(["sourcekitlsp_textDocument": ["uri": "file:///a.swift"]])) let command = Command(title: "Title", command: "Command", arguments: [1, "text", 2.2, nil]) let codeAction = CodeAction(title: "1") let codeAction2 = CodeAction(title: "2", command: command) diff --git a/Tests/SourceKitTests/ExecuteCommandTests.swift b/Tests/SourceKitTests/ExecuteCommandTests.swift new file mode 100644 index 000000000..819d76810 --- /dev/null +++ b/Tests/SourceKitTests/ExecuteCommandTests.swift @@ -0,0 +1,54 @@ +//===----------------------------------------------------------------------===// +// +// 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 LanguageServerProtocol +import SKSupport +import SKTestSupport +import XCTest + +@testable import SourceKit + +final class ExecuteCommandTests: XCTestCase { + func testLSPCommandMetadataRetrieval() { + var req = ExecuteCommandRequest(command: "", arguments: nil) + XCTAssertNil(req.metadata) + req.arguments = [1, 2, ""] + XCTAssertNil(req.metadata) + let url = URL(fileURLWithPath: "/a.swift") + let textDocument = TextDocumentIdentifier(url) + let metadata = SourceKitLSPCommandMetadata(textDocument: textDocument) + req.arguments = [metadata.asCommandArgument(), 1, 2, ""] + XCTAssertNil(req.metadata) + req.arguments = [1, 2, "", [metadata.asCommandArgument()]] + XCTAssertNil(req.metadata) + req.arguments = [1, 2, "", metadata.asCommandArgument()] + XCTAssertEqual(req.metadata, metadata) + req.arguments = [metadata.asCommandArgument()] + XCTAssertEqual(req.metadata, metadata) + } + + func testLSPCommandMetadataRemoval() { + var req = ExecuteCommandRequest(command: "", arguments: nil) + XCTAssertNil(req.argumentsWithoutLSPMetadata) + req.arguments = [1, 2, ""] + XCTAssertEqual(req.arguments, req.argumentsWithoutLSPMetadata) + let url = URL(fileURLWithPath: "/a.swift") + let textDocument = TextDocumentIdentifier(url) + let metadata = SourceKitLSPCommandMetadata(textDocument: textDocument) + req.arguments = [metadata.asCommandArgument(), 1, 2, ""] + XCTAssertEqual(req.arguments, req.argumentsWithoutLSPMetadata) + req.arguments = [1, 2, "", [metadata.asCommandArgument()]] + XCTAssertEqual(req.arguments, req.argumentsWithoutLSPMetadata) + req.arguments = [1, 2, "", metadata.asCommandArgument()] + XCTAssertEqual([1, 2, ""], req.argumentsWithoutLSPMetadata) + } +} diff --git a/Tests/SourceKitTests/XCTestManifests.swift b/Tests/SourceKitTests/XCTestManifests.swift index 8284ab560..2da35fbbd 100644 --- a/Tests/SourceKitTests/XCTestManifests.swift +++ b/Tests/SourceKitTests/XCTestManifests.swift @@ -49,6 +49,16 @@ extension DocumentSymbolTest { ] } +extension ExecuteCommandTests { + // DO NOT MODIFY: This is autogenerated, use: + // `swift test --generate-linuxmain` + // to regenerate. + static let __allTests__ExecuteCommandTests = [ + ("testLSPCommandMetadataRemoval", testLSPCommandMetadataRemoval), + ("testLSPCommandMetadataRetrieval", testLSPCommandMetadataRetrieval), + ] +} + extension FoldingRangeTests { // DO NOT MODIFY: This is autogenerated, use: // `swift test --generate-linuxmain` @@ -135,6 +145,7 @@ public func __allTests() -> [XCTestCaseEntry] { testCase(CodeActionTests.__allTests__CodeActionTests), testCase(DocumentColorTests.__allTests__DocumentColorTests), testCase(DocumentSymbolTest.__allTests__DocumentSymbolTest), + testCase(ExecuteCommandTests.__allTests__ExecuteCommandTests), testCase(FoldingRangeTests.__allTests__FoldingRangeTests), testCase(ImplementationTests.__allTests__ImplementationTests), testCase(LocalClangTests.__allTests__LocalClangTests), From c20fe44c42255e81adf86d5a40036af364acc800 Mon Sep 17 00:00:00 2001 From: Bruno Rocha Date: Thu, 12 Sep 2019 09:58:42 -0300 Subject: [PATCH 4/6] Convert CommandArgumentType -> LSPAny references --- Sources/LanguageServerProtocol/ExecuteCommand.swift | 6 +++--- Sources/LanguageServerProtocol/LSPAny.swift | 2 ++ Sources/SourceKit/SourceKitLSPCommandMetadata.swift | 8 ++++---- Sources/SourceKit/SourceKitServer.swift | 2 +- Sources/SourceKit/sourcekitd/SwiftCommand.swift | 10 +++++----- Tests/SourceKitTests/CodeActionTests.swift | 4 ++-- 6 files changed, 17 insertions(+), 15 deletions(-) diff --git a/Sources/LanguageServerProtocol/ExecuteCommand.swift b/Sources/LanguageServerProtocol/ExecuteCommand.swift index e36f5041b..53f8e6dd1 100644 --- a/Sources/LanguageServerProtocol/ExecuteCommand.swift +++ b/Sources/LanguageServerProtocol/ExecuteCommand.swift @@ -27,15 +27,15 @@ public struct ExecuteCommandRequest: RequestType { public static let method: String = "workspace/executeCommand" // Note: The LSP type for this response is `Any?`. - public typealias Response = CommandArgumentType? + public typealias Response = LSPAny? /// The command to be executed. public var command: String /// Arguments that the command should be invoked with. - public var arguments: [CommandArgumentType]? + public var arguments: [LSPAny]? - public init(command: String, arguments: [CommandArgumentType]?) { + public init(command: String, arguments: [LSPAny]?) { self.command = command self.arguments = arguments } diff --git a/Sources/LanguageServerProtocol/LSPAny.swift b/Sources/LanguageServerProtocol/LSPAny.swift index 4951a08ab..a70b5e8e7 100644 --- a/Sources/LanguageServerProtocol/LSPAny.swift +++ b/Sources/LanguageServerProtocol/LSPAny.swift @@ -68,6 +68,8 @@ extension LSPAny: Encodable { } } +extension LSPAny: ResponseType {} + extension LSPAny: ExpressibleByNilLiteral { public init(nilLiteral _: ()) { self = .null diff --git a/Sources/SourceKit/SourceKitLSPCommandMetadata.swift b/Sources/SourceKit/SourceKitLSPCommandMetadata.swift index dde36b614..fe93032bf 100644 --- a/Sources/SourceKit/SourceKitLSPCommandMetadata.swift +++ b/Sources/SourceKit/SourceKitLSPCommandMetadata.swift @@ -19,7 +19,7 @@ import SKSupport /// to determine where a command should be executed. public struct SourceKitLSPCommandMetadata: Codable, Hashable { - public static func decode(fromDictionary dictionary: [String: CommandArgumentType]) -> SourceKitLSPCommandMetadata? { + public static func decode(fromDictionary dictionary: [String: LSPAny]) -> SourceKitLSPCommandMetadata? { guard case .dictionary(let textDocumentDict)? = dictionary[CodingKeys.sourcekitlsp_textDocument.stringValue], case .string(let urlString)? = textDocumentDict[TextDocumentIdentifier.CodingKeys.url.stringValue], let url = URL(string: urlString) else @@ -36,8 +36,8 @@ public struct SourceKitLSPCommandMetadata: Codable, Hashable { self.sourcekitlsp_textDocument = textDocument } - func asCommandArgument() -> CommandArgumentType { - let textDocumentArgument = CommandArgumentType.dictionary( + func asCommandArgument() -> LSPAny { + let textDocumentArgument = LSPAny.dictionary( [TextDocumentIdentifier.CodingKeys.url.stringValue: .string(sourcekitlsp_textDocument.url.absoluteString)] ) return .dictionary([CodingKeys.sourcekitlsp_textDocument.stringValue: textDocumentArgument]) @@ -84,7 +84,7 @@ extension ExecuteCommandRequest { } /// Returns this Command's arguments without SourceKit-LSP's injected metadata, if it exists. - public var argumentsWithoutLSPMetadata: [CommandArgumentType]? { + public var argumentsWithoutLSPMetadata: [LSPAny]? { guard metadata != nil else { return arguments } diff --git a/Sources/SourceKit/SourceKitServer.swift b/Sources/SourceKit/SourceKitServer.swift index 7a4dd1e0b..dde867733 100644 --- a/Sources/SourceKit/SourceKitServer.swift +++ b/Sources/SourceKit/SourceKitServer.swift @@ -654,7 +654,7 @@ extension SourceKitServer { fallback: @autoclosure () -> PositionRequest.Response) where PositionRequest: TextDocumentRequest { - sendRequest(req, params: req.params, url: req.params.textDocument.url, workspace: workspace, resultHandler: resultHandler, fallback: fallback) + sendRequest(req, params: req.params, url: req.params.textDocument.url, workspace: workspace, resultHandler: resultHandler, fallback: fallback()) } func sendRequest( diff --git a/Sources/SourceKit/sourcekitd/SwiftCommand.swift b/Sources/SourceKit/sourcekitd/SwiftCommand.swift index 6820b0212..af3a0f06e 100644 --- a/Sources/SourceKit/sourcekitd/SwiftCommand.swift +++ b/Sources/SourceKit/sourcekitd/SwiftCommand.swift @@ -22,9 +22,9 @@ public let builtinSwiftCommands: [String] = [] /// A `Command` that should be executed by Swift's language server. public protocol SwiftCommand: Codable, Hashable { static var identifier: String { get } - static func decode(fromDictionary dictionary: [String: CommandArgumentType]) -> Self? + static func decode(fromDictionary dictionary: [String: LSPAny]) -> Self? var title: String { get set } - func asCommandArgument() -> CommandArgumentType + func asCommandArgument() -> LSPAny } extension SwiftCommand { @@ -79,7 +79,7 @@ public struct SemanticRefactorCommand: SwiftCommand { /// The text document related to the refactoring action. public var textDocument: TextDocumentIdentifier - public static func decode(fromDictionary dictionary: [String: CommandArgumentType]) -> SemanticRefactorCommand? { + public static func decode(fromDictionary dictionary: [String: LSPAny]) -> SemanticRefactorCommand? { guard case .dictionary(let dict)? = dictionary[CodingKeys.textDocument.stringValue], case .string(let title)? = dictionary[CodingKeys.title.stringValue], case .string(let actionString)? = dictionary[CodingKeys.actionString.stringValue], @@ -108,8 +108,8 @@ public struct SemanticRefactorCommand: SwiftCommand { self.textDocument = textDocument } - public func asCommandArgument() -> CommandArgumentType { - let textDocumentArgument = CommandArgumentType.dictionary( + public func asCommandArgument() -> LSPAny { + let textDocumentArgument = LSPAny.dictionary( [TextDocumentIdentifier.CodingKeys.url.stringValue: .string(textDocument.url.absoluteString)] ) return .dictionary([CodingKeys.title.stringValue: .string(title), diff --git a/Tests/SourceKitTests/CodeActionTests.swift b/Tests/SourceKitTests/CodeActionTests.swift index 9560758ce..3f45720dc 100644 --- a/Tests/SourceKitTests/CodeActionTests.swift +++ b/Tests/SourceKitTests/CodeActionTests.swift @@ -108,10 +108,10 @@ final class CodeActionTests: XCTestCase { func testCodeActionResponseCommandMetadataInjection() { let url = URL(fileURLWithPath: "/a.swift") let textDocument = TextDocumentIdentifier(url) - let expectedMetadata: CommandArgumentType = { + let expectedMetadata: LSPAny = { let metadata = SourceKitLSPCommandMetadata(textDocument: textDocument) let data = try! JSONEncoder().encode(metadata) - return try! JSONDecoder().decode(CommandArgumentType.self, from: data) + return try! JSONDecoder().decode(LSPAny.self, from: data) }() XCTAssertEqual(expectedMetadata, .dictionary(["sourcekitlsp_textDocument": ["uri": "file:///a.swift"]])) let command = Command(title: "Title", command: "Command", arguments: [1, "text", 2.2, nil]) From 20c026c8e25b4c2b053413af41d50714cc9937a8 Mon Sep 17 00:00:00 2001 From: Bruno Rocha Date: Tue, 24 Sep 2019 17:59:38 -0300 Subject: [PATCH 5/6] Cleaning Code --- .../WorkspaceEdit.swift | 8 +- .../SourceKitLSPCommandMetadata.swift | 24 ++-- Sources/SourceKit/SourceKitServer.swift | 16 +-- .../SourceKit/sourcekitd/SwiftCommand.swift | 122 ------------------ .../sourcekitd/SwiftLanguageServer.swift | 2 +- Tests/SourceKitTests/CodeActionTests.swift | 6 +- .../SourceKitTests/ExecuteCommandTests.swift | 24 ++-- 7 files changed, 39 insertions(+), 163 deletions(-) delete mode 100644 Sources/SourceKit/sourcekitd/SwiftCommand.swift diff --git a/Sources/LanguageServerProtocol/WorkspaceEdit.swift b/Sources/LanguageServerProtocol/WorkspaceEdit.swift index e23e137ad..1a49f1cfc 100644 --- a/Sources/LanguageServerProtocol/WorkspaceEdit.swift +++ b/Sources/LanguageServerProtocol/WorkspaceEdit.swift @@ -14,13 +14,9 @@ public struct WorkspaceEdit: Codable, Hashable, ResponseType { /// The edits to be applied to existing resources. - public var changes: [String: [TextEdit]]? + public var changes: [URL: [TextEdit]]? public init(changes: [URL: [TextEdit]]?) { - guard let changes = changes else { - return - } - let changesArray = changes.map { ($0.key.absoluteString, $0.value) } - self.changes = Dictionary(uniqueKeysWithValues: changesArray) + self.changes = changes } } diff --git a/Sources/SourceKit/SourceKitLSPCommandMetadata.swift b/Sources/SourceKit/SourceKitLSPCommandMetadata.swift index fe93032bf..2401506de 100644 --- a/Sources/SourceKit/SourceKitLSPCommandMetadata.swift +++ b/Sources/SourceKit/SourceKitLSPCommandMetadata.swift @@ -19,24 +19,26 @@ import SKSupport /// to determine where a command should be executed. public struct SourceKitLSPCommandMetadata: Codable, Hashable { - public static func decode(fromDictionary dictionary: [String: LSPAny]) -> SourceKitLSPCommandMetadata? { - guard case .dictionary(let textDocumentDict)? = dictionary[CodingKeys.sourcekitlsp_textDocument.stringValue], - case .string(let urlString)? = textDocumentDict[TextDocumentIdentifier.CodingKeys.url.stringValue], + 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) - return SourceKitLSPCommandMetadata(textDocument: textDocument) + self.init(textDocument: textDocument) } - public var sourcekitlsp_textDocument: TextDocumentIdentifier - public init(textDocument: TextDocumentIdentifier) { self.sourcekitlsp_textDocument = textDocument } - func asCommandArgument() -> LSPAny { + public func encodeToLSPAny() -> LSPAny { let textDocumentArgument = LSPAny.dictionary( [TextDocumentIdentifier.CodingKeys.url.stringValue: .string(sourcekitlsp_textDocument.url.absoluteString)] ) @@ -45,9 +47,9 @@ public struct SourceKitLSPCommandMetadata: Codable, Hashable { } extension CodeActionRequest { - public func injectMetadata(atResponse response: CodeActionRequestResponse?) -> CodeActionRequestResponse? { + public func injectMetadata(toResponse response: CodeActionRequestResponse?) -> CodeActionRequestResponse? { let metadata = SourceKitLSPCommandMetadata(textDocument: textDocument) - let metadataArgument = metadata.asCommandArgument() + let metadataArgument = metadata.encodeToLSPAny() switch response { case .codeActions(var codeActions)?: for i in 0.., workspace: Workspace) { - toolchainTextDocumentRequest(req, workspace: workspace, resultHandler: { result in + toolchainTextDocumentRequest(req, workspace: workspace, resultTransformer: { result in switch result { case .success(let reply): - return .success(req.params.injectMetadata(atResponse: reply)) + return .success(req.params.injectMetadata(toResponse: reply)) default: return result } @@ -480,7 +480,7 @@ extension SourceKitServer { return } var params = req.params - params.arguments = params.argumentsWithoutLSPMetadata + params.arguments = params.argumentsWithoutSourceKitMetadata sendRequest(req, params: params, url: url, workspace: workspace, fallback: nil) } @@ -650,11 +650,11 @@ extension SourceKitServer { func toolchainTextDocumentRequest( _ req: Request, workspace: Workspace, - resultHandler: ((LSPResult) -> LSPResult)? = nil, + resultTransformer: ((LSPResult) -> LSPResult)? = nil, fallback: @autoclosure () -> PositionRequest.Response) where PositionRequest: TextDocumentRequest { - sendRequest(req, params: req.params, url: req.params.textDocument.url, workspace: workspace, resultHandler: resultHandler, fallback: fallback()) + sendRequest(req, params: req.params, url: req.params.textDocument.url, workspace: workspace, resultTransformer: resultTransformer, fallback: fallback()) } func sendRequest( @@ -662,7 +662,7 @@ extension SourceKitServer { params: PositionRequest, url: URL, workspace: Workspace, - resultHandler: ((LSPResult) -> LSPResult)? = nil, + resultTransformer: ((LSPResult) -> LSPResult)? = nil, fallback: @autoclosure () -> PositionRequest.Response) { guard let service = workspace.documentService[url] else { @@ -671,7 +671,7 @@ extension SourceKitServer { } let id = service.send(params, queue: DispatchQueue.global()) { result in - req.reply(resultHandler?(result) ?? result) + req.reply(resultTransformer?(result) ?? result) } req.cancellationToken.addCancellationHandler { [weak service] in service?.send(CancelRequest(id: id)) diff --git a/Sources/SourceKit/sourcekitd/SwiftCommand.swift b/Sources/SourceKit/sourcekitd/SwiftCommand.swift deleted file mode 100644 index af3a0f06e..000000000 --- a/Sources/SourceKit/sourcekitd/SwiftCommand.swift +++ /dev/null @@ -1,122 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// 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 SKSupport -import LanguageServerProtocol -import Foundation - -/// The set of known Swift commands. -/// -/// All commands from the Swift LSP should be listed here. -public let builtinSwiftCommands: [String] = [] - -/// A `Command` that should be executed by Swift's language server. -public protocol SwiftCommand: Codable, Hashable { - static var identifier: String { get } - static func decode(fromDictionary dictionary: [String: LSPAny]) -> Self? - var title: String { get set } - func asCommandArgument() -> LSPAny -} - -extension SwiftCommand { - /// Converts this `SwiftCommand` to a generic LSP `Command` object. - public func asCommand() throws -> Command { - let argument = asCommandArgument() - return Command(title: title, command: Self.identifier, arguments: [argument]) - } -} - -extension ExecuteCommandRequest { - /// Attempts to convert the underlying `Command` metadata from this request - /// to a specific Swift language server `SwiftCommand`. - /// - /// - Parameters: - /// - type: The `SwiftCommand` metatype to convert to. - public func swiftCommand(ofType type: T.Type) -> T? { - guard type.identifier == command else { - return nil - } - guard let argument = arguments?.first else { - return nil - } - guard case let .dictionary(dictionary) = argument else { - return nil - } - return type.decode(fromDictionary: dictionary) - } -} - -public struct SemanticRefactorCommand: SwiftCommand { - - public static var identifier: String { - return "semantic.refactor.command" - } - - /// The name of this refactoring action. - public var title: String - - /// The sourcekitd identifier of the refactoring action. - public var actionString: String - - /// The starting line of the range to refactor. - public var line: Int - - /// The starting column of the range to refactor. - public var column: Int - - /// The length of the range to refactor. - public var length: Int - - /// The text document related to the refactoring action. - public var textDocument: TextDocumentIdentifier - - public static func decode(fromDictionary dictionary: [String: LSPAny]) -> SemanticRefactorCommand? { - guard case .dictionary(let dict)? = dictionary[CodingKeys.textDocument.stringValue], - case .string(let title)? = dictionary[CodingKeys.title.stringValue], - case .string(let actionString)? = dictionary[CodingKeys.actionString.stringValue], - case .int(let line)? = dictionary[CodingKeys.line.stringValue], - case .int(let column)? = dictionary[CodingKeys.column.stringValue], - case .int(let length)? = dictionary[CodingKeys.length.stringValue], - case .string(let urlString)? = dict[TextDocumentIdentifier.CodingKeys.url.stringValue], - let url = URL(string: urlString) else - { - return nil - } - return SemanticRefactorCommand(title: title, - actionString: actionString, - line: line, - column: column, - length: length, - textDocument: TextDocumentIdentifier(url)) - } - - public init(title: String, actionString: String, line: Int, column: Int, length: Int, textDocument: TextDocumentIdentifier) { - self.title = title - self.actionString = actionString - self.line = line - self.column = column - self.length = length - self.textDocument = textDocument - } - - public func asCommandArgument() -> LSPAny { - let textDocumentArgument = LSPAny.dictionary( - [TextDocumentIdentifier.CodingKeys.url.stringValue: .string(textDocument.url.absoluteString)] - ) - return .dictionary([CodingKeys.title.stringValue: .string(title), - CodingKeys.actionString.stringValue: .string(actionString), - CodingKeys.line.stringValue: .int(line), - CodingKeys.column.stringValue: .int(column), - CodingKeys.length.stringValue: .int(length), - CodingKeys.textDocument.stringValue: textDocumentArgument]) - } -} diff --git a/Sources/SourceKit/sourcekitd/SwiftLanguageServer.swift b/Sources/SourceKit/sourcekitd/SwiftLanguageServer.swift index d8086372c..f6d8eeec3 100644 --- a/Sources/SourceKit/sourcekitd/SwiftLanguageServer.swift +++ b/Sources/SourceKit/sourcekitd/SwiftLanguageServer.swift @@ -170,7 +170,7 @@ extension SwiftLanguageServer { codeActionOptions: CodeActionOptions(codeActionKinds: nil), supportsCodeActions: false), // TODO: Turn it on after a provider is implemented. executeCommandProvider: ExecuteCommandOptions( - commands: builtinSwiftCommands) + commands: []) ))) } diff --git a/Tests/SourceKitTests/CodeActionTests.swift b/Tests/SourceKitTests/CodeActionTests.swift index 3f45720dc..92cb2b3ed 100644 --- a/Tests/SourceKitTests/CodeActionTests.swift +++ b/Tests/SourceKitTests/CodeActionTests.swift @@ -120,7 +120,7 @@ final class CodeActionTests: XCTestCase { let request = CodeActionRequest(range: Position(line: 0, utf16index: 0).. Date: Tue, 24 Sep 2019 19:56:05 -0300 Subject: [PATCH 6/6] Remove @testable from test class --- Tests/SourceKitTests/ExecuteCommandTests.swift | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Tests/SourceKitTests/ExecuteCommandTests.swift b/Tests/SourceKitTests/ExecuteCommandTests.swift index a10f771e6..69a5f2876 100644 --- a/Tests/SourceKitTests/ExecuteCommandTests.swift +++ b/Tests/SourceKitTests/ExecuteCommandTests.swift @@ -14,8 +14,7 @@ import LanguageServerProtocol import SKSupport import SKTestSupport import XCTest - -@testable import SourceKit +import SourceKit final class ExecuteCommandTests: XCTestCase { func testLSPCommandMetadataRetrieval() {