diff --git a/README.md b/README.md index f8e895408..27fa80f41 100644 --- a/README.md +++ b/README.md @@ -116,7 +116,8 @@ SourceKit-LSP is still in early development, so you may run into rough edges wit | Find References | ✅ | | | Background Indexing | ❌ | Build project to update the index using [Indexing While Building](#indexing-while-building) | | Workspace Symbols | ❌ | | -| Refactoring | ❌ | | +| Global Rename | ❌ | | +| Local Refactoring | ✅ | | | Formatting | ❌ | | | Folding | ✅ | | | Syntax Highlighting | ❌ | Not currently part of LSP. | diff --git a/Sources/LanguageServerProtocol/ApplyEdit.swift b/Sources/LanguageServerProtocol/ApplyEdit.swift new file mode 100644 index 000000000..d2126e792 --- /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?, 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/ClientCapabilities.swift b/Sources/LanguageServerProtocol/ClientCapabilities.swift index f6f28effc..006671b01 100644 --- a/Sources/LanguageServerProtocol/ClientCapabilities.swift +++ b/Sources/LanguageServerProtocol/ClientCapabilities.swift @@ -221,12 +221,23 @@ public struct TextDocumentClientCapabilities: Hashable, Codable { /// /// If specified, the client *also* guarantees that it will handle unknown kinds gracefully. public var valueSet: [LanguageServerProtocol.CodeActionKind] + + public init(valueSet: [LanguageServerProtocol.CodeActionKind]) { + self.valueSet = valueSet + } } public var codeActionKind: CodeActionKind + + public init(codeActionKind: CodeActionKind) { + self.codeActionKind = codeActionKind + } } public var codeActionLiteralSupport: CodeActionLiteralSupport? = nil + + public init() { + } } /// Capabilities specific to `textDocument/publishDiagnostics`. 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..4ed99c1c0 --- /dev/null +++ b/Sources/LanguageServerProtocol/ExecuteCommand.swift @@ -0,0 +1,41 @@ +//===----------------------------------------------------------------------===// +// +// 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" + + // The LSP has no response for this request (Any?), but we return + // the resulting edit for testing purposes. + public typealias Response = WorkspaceEdit? + + /// 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 a8fd735af..a7ec4b503 100644 --- a/Sources/LanguageServerProtocol/Messages.swift +++ b/Sources/LanguageServerProtocol/Messages.swift @@ -34,6 +34,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 5ebaed1ee..ea872a16f 100644 --- a/Sources/LanguageServerProtocol/ServerCapabilities.swift +++ b/Sources/LanguageServerProtocol/ServerCapabilities.swift @@ -51,6 +51,9 @@ public struct ServerCapabilities: Codable, Hashable { /// Whether the server provides "textDocument/codeAction". public var codeActionProvider: CodeActionServerCapabilities? + /// Whether the server provides "workspace/executeCommand". + public var executeCommandProvider: ExecuteCommandOptions? + // TODO: fill-in the rest. public init( @@ -66,7 +69,8 @@ public struct ServerCapabilities: Codable, Hashable { foldingRangeProvider: Bool? = nil, documentSymbolProvider: Bool? = nil, colorProvider: Bool? = nil, - codeActionProvider: CodeActionServerCapabilities? = nil + codeActionProvider: CodeActionServerCapabilities? = nil, + executeCommandProvider: ExecuteCommandOptions? = nil ) { self.textDocumentSync = textDocumentSync @@ -82,6 +86,7 @@ public struct ServerCapabilities: Codable, Hashable { self.documentSymbolProvider = documentSymbolProvider self.colorProvider = colorProvider self.codeActionProvider = codeActionProvider + self.executeCommandProvider = executeCommandProvider } public init(from decoder: Decoder) throws { @@ -93,6 +98,7 @@ public struct ServerCapabilities: Codable, Hashable { self.documentSymbolProvider = try container.decodeIfPresent(Bool.self, forKey: .documentSymbolProvider) self.colorProvider = try container.decodeIfPresent(Bool.self, forKey: .colorProvider) self.codeActionProvider = try container.decodeIfPresent(CodeActionServerCapabilities.self, forKey: .codeActionProvider) + self.executeCommandProvider = try container.decodeIfPresent(ExecuteCommandOptions.self, forKey: .executeCommandProvider) if let textDocumentSync = try? container.decode(TextDocumentSyncOptions.self, forKey: .textDocumentSync) { self.textDocumentSync = textDocumentSync @@ -234,3 +240,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/SKTestSupport/TestJSONRPCConnection.swift b/Sources/SKTestSupport/TestJSONRPCConnection.swift index 3584af858..508b00b39 100644 --- a/Sources/SKTestSupport/TestJSONRPCConnection.swift +++ b/Sources/SKTestSupport/TestJSONRPCConnection.swift @@ -96,6 +96,7 @@ public final class TestClient: LanguageServerEndpoint { var oneShotNotificationHandlers: [((Any) -> Void)] = [] public var allowUnexpectedNotification: Bool = false + public var allowUnexpectedRequest: Bool = false public func appendOneShotNotificationHandler(_ handler: @escaping (Notification) -> Void) { oneShotNotificationHandlers.append({ anyNote in @@ -125,7 +126,10 @@ public final class TestClient: LanguageServerEndpoint { } override public func _handleUnknown(_ request: Request) where R : RequestType { - fatalError() + guard allowUnexpectedRequest else { + fatalError("unexpected request \(request)") + } + request.reply(.failure(.cancelled)) } } diff --git a/Sources/SourceKit/SourceKitServer.swift b/Sources/SourceKit/SourceKitServer.swift index 2c420894d..f81cf0afe 100644 --- a/Sources/SourceKit/SourceKitServer.swift +++ b/Sources/SourceKit/SourceKitServer.swift @@ -80,6 +80,7 @@ public final class SourceKitServer: LanguageServer { registerWorkspaceRequest(SourceKitServer.documentColor) registerWorkspaceRequest(SourceKitServer.colorPresentation) registerWorkspaceRequest(SourceKitServer.codeAction) + registerWorkspaceRequest(SourceKitServer.executeCommand) } func registerWorkspaceRequest( @@ -268,7 +269,10 @@ extension SourceKitServer { codeActionProvider: CodeActionServerCapabilities( clientCapabilities: req.params.capabilities.textDocument?.codeAction, codeActionOptions: CodeActionOptions(codeActionKinds: nil), - supportsCodeActions: false // TODO: Turn it on after a provider is implemented. + supportsCodeActions: true + ), + executeCommandProvider: ExecuteCommandOptions( + commands: builtinSwiftCommands // FIXME: Clangd commands? ) ))) } @@ -367,6 +371,29 @@ extension SourceKitServer { toolchainTextDocumentRequest(req, workspace: workspace, fallback: nil) } + func executeCommand(_ req: Request, workspace: Workspace) { + // FIXME: This request has 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 isSwiftCommand = Command.isCommandIdentifierFromSwiftLSP(req.params.command) + let connections = workspace.documentService.values.compactMap { $0 as? LocalConnection } + let service: Connection? + if isSwiftCommand { + service = connections.first { $0.handler is SwiftLanguageServer } + } else { + service = connections.first { $0.handler is ClangLanguageServerShim } + } + guard let serviceToExecute = service else { + req.reply(nil) + return + } + let id = serviceToExecute.send(req.params, queue: DispatchQueue.global()) { result in + req.reply(result) + } + req.cancellationToken.addCancellationHandler { [weak serviceToExecute] in + serviceToExecute?.send(CancelRequest(id: id)) + } + } + func definition(_ req: Request, workspace: Workspace) { // FIXME: sending yourself a request isn't very convenient diff --git a/Sources/SourceKit/sourcekitd/SemanticRefactoring.swift b/Sources/SourceKit/sourcekitd/SemanticRefactoring.swift new file mode 100644 index 000000000..32eefdb99 --- /dev/null +++ b/Sources/SourceKit/sourcekitd/SemanticRefactoring.swift @@ -0,0 +1,152 @@ +//===----------------------------------------------------------------------===// +// +// 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 Basic +import sourcekitd + +/// Detailed information about the result of a specific refactoring operation. +/// +/// Wraps the information returned by sourcekitd's `semantic_refactoring` request, such as the necessary edits and placeholder locations. +struct SemanticRefactoring { + + /// The title of the refactoring action. + var title: String + + /// The resulting `WorkspaceEdit` of a `semantic_refactoring` request. + var edit: WorkspaceEdit + + init(_ title: String, _ edit: WorkspaceEdit) { + self.title = title + self.edit = edit + } +} + +extension SemanticRefactoring { + + /// Create a `SemanticRefactoring` from a sourcekitd response dictionary, if possible. + /// + /// - Parameters: + /// - title: The title of the refactoring action. + /// - dict: Response dictionary to extract information from. + /// - url: The client URL that triggered the `semantic_refactoring` request. + /// - keys: The sourcekitd key set to use for looking up into `dict`. + init?(_ title: String, _ dict: SKResponseDictionary, _ url: URL, _ keys: sourcekitd_keys) { + guard let categorizedEdits: SKResponseArray = dict[keys.categorizededits] else { + // Nothing to report. + return nil + } + + var textEdits = [TextEdit]() + + categorizedEdits.forEach { _, value in + guard let edits: SKResponseArray = value[keys.edits] else { + return false + } + edits.forEach { _, value in + if let startLine: Int = value[keys.line], + let startColumn: Int = value[keys.column], + let endLine: Int = value[keys.endline], + let endColumn: Int = value[keys.endcolumn], + let text: String = value[keys.text] + { + // The LSP is zero based, but semantic_refactoring is one based. + let startPosition = Position(line: startLine - 1, utf16index: startColumn - 1) + let endPosition = Position(line: endLine - 1, utf16index: endColumn - 1) + let edit = TextEdit(range: startPosition..) -> Void) + { + let url = refactorCommand.textDocument.url + guard let snapshot = documentManager.latestSnapshot(url) else { + return completion(.failure(.unknownDocument(url))) + } + let skreq = SKRequestDictionary(sourcekitd: sourcekitd) + skreq[keys.request] = requests.semantic_refactoring + skreq[keys.name] = "" + skreq[keys.sourcefile] = url.path + skreq[keys.line] = refactorCommand.line + 1 + skreq[keys.column] = refactorCommand.column + 1 // LSP is zero based, but this request is 1 based. + skreq[keys.length] = refactorCommand.length + skreq[keys.actionuid] = sourcekitd.api.uid_get_from_cstr(refactorCommand.actionString)! + if let settings = buildSystem.settings(for: url, snapshot.document.language) { + skreq[keys.compilerargs] = settings.compilerArguments + } + + let handle = sourcekitd.send(skreq) { [weak self] result in + guard let self = self else { return } + guard let dict = result.success else { + return completion(.failure(.responseError(result.failure!))) + } + guard let refactor = SemanticRefactoring(refactorCommand.title, dict, url, self.keys) else { + return completion(.failure(.noEditsNeeded(url))) + } + completion(.success(refactor)) + } + + // FIXME: cancellation + _ = handle + } +} diff --git a/Sources/SourceKit/sourcekitd/SwiftCommand.swift b/Sources/SourceKit/sourcekitd/SwiftCommand.swift new file mode 100644 index 000000000..ca788f13f --- /dev/null +++ b/Sources/SourceKit/sourcekitd/SwiftCommand.swift @@ -0,0 +1,112 @@ +//===----------------------------------------------------------------------===// +// +// 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] = [ + SemanticRefactorCommand.self +].map { $0.identifier } + +/// 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 deb6c3e36..80f28f2b5 100644 --- a/Sources/SourceKit/sourcekitd/SwiftLanguageServer.swift +++ b/Sources/SourceKit/sourcekitd/SwiftLanguageServer.swift @@ -68,6 +68,7 @@ public final class SwiftLanguageServer: LanguageServer { _register(SwiftLanguageServer.documentColor) _register(SwiftLanguageServer.colorPresentation) _register(SwiftLanguageServer.codeAction) + _register(SwiftLanguageServer.executeCommand) } func getDiagnostic(_ diag: SKResponseDictionary, for snapshot: DocumentSnapshot) -> Diagnostic? { @@ -202,7 +203,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: true), + executeCommandProvider: ExecuteCommandOptions( + commands: builtinSwiftCommands) ))) } @@ -746,7 +749,6 @@ extension SwiftLanguageServer { req.reply(.failure(result.failure!)) return } - guard let syntaxMap: SKResponseArray = dict[self.keys.syntaxmap], let substructure: SKResponseArray = dict[self.keys.substructure] else { return req.reply([]) @@ -854,8 +856,8 @@ extension SwiftLanguageServer { func codeAction(_ req: Request) { let providersAndKinds: [(provider: CodeActionProvider, kind: CodeActionKind)] = [ + (retrieveRefactorCodeActions, .refactor) //TODO: Implement the providers. - //(retrieveRefactorCodeActions, .refactor), //(retrieveQuickFixCodeActions, .quickFix) ] let wantedActionKinds = req.params.context.only @@ -888,6 +890,107 @@ extension SwiftLanguageServer { } } } + + func retrieveRefactorCodeActions(_ params: CodeActionRequest, completion: @escaping CodeActionProviderCompletion) { + guard let snapshot = documentManager.latestSnapshot(params.textDocument.url) else { + log("failed to find snapshot for url \(params.textDocument.url)") + completion([]) + return + } + guard let startOffset = snapshot.utf8Offset(of: params.range.lowerBound), + let endOffset = snapshot.utf8Offset(of: params.range.upperBound) else + { + completion([]) + return + } + let skreq = SKRequestDictionary(sourcekitd: sourcekitd) + skreq[keys.request] = requests.cursorinfo + skreq[keys.sourcefile] = snapshot.document.url.path + skreq[keys.offset] = startOffset + let length = endOffset - startOffset + skreq[keys.length] = length + skreq[keys.retrieve_refactor_actions] = 1 + if let settings = buildSystem.settings(for: snapshot.document.url, snapshot.document.language) { + skreq[keys.compilerargs] = settings.compilerArguments + } + + let handle = sourcekitd.send(skreq) { [weak self] result in + guard let self = self else { + completion([]) + return + } + guard let dict = result.success else { + log("failed to find refactor actions: \(result.failure!)") + completion([]) + return + } + guard let results: SKResponseArray = dict[self.keys.refactor_actions] else { + completion([]) + return + } + var codeActions = [CodeAction]() + results.forEach { _, value in + if let name: String = value[self.keys.actionname], + let actionuid: sourcekitd_uid_t = value[self.keys.actionuid], + let ptr = self.sourcekitd.api.uid_get_string_ptr(actionuid) + { + let actionName = String(cString: ptr) + //TODO: Global refactoring. + guard actionName != "source.refactoring.kind.rename.global" else { + return true + } + let swiftCommand = SemanticRefactorCommand(title: name, + actionString: actionName, + line: params.range.lowerBound.line, + column: params.range.lowerBound.utf16index, + length: length, + textDocument: params.textDocument) + do { + let command = try swiftCommand.asCommand() + let codeAction = CodeAction(title: name, kind: .refactor, command: command) + codeActions.append(codeAction) + } catch { + log("Failed to convert SwiftCommand to Command type: \(error)") + } + } + return true + } + + completion(codeActions) + } + + // FIXME: cancellation + _ = handle + } + + func executeCommand(_ req: Request) { + let params = req.params + //TODO: If there's support for several types of commands, we might need to structure this similarly to the code actions request. + guard let swiftCommand = params.swiftCommand(ofType: SemanticRefactorCommand.self) else { + log("semantic refactoring: unknown command \(params.command)", level: .warning) + return req.reply(nil) + } + let url = swiftCommand.textDocument.url + semanticRefactoring(swiftCommand) { result in + guard case let .success(refactor) = result else { + if case let .failure(error) = result { + log("semantic refactoring failed \(url): \(error)", level: .warning) + } + return req.reply(nil) + } + let edit = refactor.edit + let editReq = ApplyEditRequest(label: refactor.title, edit: edit) + do { + let response = try self.client.sendSync(editReq) + if response?.applied == false { + log("client refused to apply edit for \(refactor.title)!", level: .warning) + } + } catch { + log("applyEdit failed: \(error)", level: .warning) + } + req.reply(edit) + } + } } extension DocumentSnapshot { diff --git a/Sources/SourceKit/sourcekitd/SwiftSourceKitFramework.swift b/Sources/SourceKit/sourcekitd/SwiftSourceKitFramework.swift index c13dde6c2..f83015bb7 100644 --- a/Sources/SourceKit/sourcekitd/SwiftSourceKitFramework.swift +++ b/Sources/SourceKit/sourcekitd/SwiftSourceKitFramework.swift @@ -207,6 +207,8 @@ struct sourcekitd_keys { let severity: sourcekitd_uid_t let line: sourcekitd_uid_t let column: sourcekitd_uid_t + let endline: sourcekitd_uid_t + let endcolumn: sourcekitd_uid_t let filepath: sourcekitd_uid_t let ranges: sourcekitd_uid_t let usr: sourcekitd_uid_t @@ -220,6 +222,13 @@ struct sourcekitd_keys { let syntaxmap: sourcekitd_uid_t let namelength: sourcekitd_uid_t let nameoffset: sourcekitd_uid_t + let retrieve_refactor_actions: sourcekitd_uid_t + let refactor_actions: sourcekitd_uid_t + let actionname: sourcekitd_uid_t + let actionuid: sourcekitd_uid_t + let categorizededits: sourcekitd_uid_t + let edits: sourcekitd_uid_t + let text: sourcekitd_uid_t init(api: sourcekitd_functions_t) { request = api.uid_get_from_cstr("key.request")! @@ -237,6 +246,8 @@ struct sourcekitd_keys { severity = api.uid_get_from_cstr("key.severity")! line = api.uid_get_from_cstr("key.line")! column = api.uid_get_from_cstr("key.column")! + endline = api.uid_get_from_cstr("key.endline")! + endcolumn = api.uid_get_from_cstr("key.endcolumn")! filepath = api.uid_get_from_cstr("key.filepath")! ranges = api.uid_get_from_cstr("key.ranges")! usr = api.uid_get_from_cstr("key.usr")! @@ -250,6 +261,13 @@ struct sourcekitd_keys { syntaxmap = api.uid_get_from_cstr("key.syntaxmap")! namelength = api.uid_get_from_cstr("key.namelength")! nameoffset = api.uid_get_from_cstr("key.nameoffset")! + retrieve_refactor_actions = api.uid_get_from_cstr("key.retrieve_refactor_actions")! + refactor_actions = api.uid_get_from_cstr("key.refactor_actions")! + actionname = api.uid_get_from_cstr("key.actionname")! + actionuid = api.uid_get_from_cstr("key.actionuid")! + categorizededits = api.uid_get_from_cstr("key.categorizededits")! + edits = api.uid_get_from_cstr("key.edits")! + text = api.uid_get_from_cstr("key.text")! } } @@ -261,6 +279,7 @@ struct sourcekitd_requests { let codecomplete: sourcekitd_uid_t let cursorinfo: sourcekitd_uid_t let relatedidents: sourcekitd_uid_t + let semantic_refactoring: sourcekitd_uid_t init(api: sourcekitd_functions_t) { editor_open = api.uid_get_from_cstr("source.request.editor.open")! @@ -269,6 +288,7 @@ struct sourcekitd_requests { codecomplete = api.uid_get_from_cstr("source.request.codecomplete")! cursorinfo = api.uid_get_from_cstr("source.request.cursorinfo")! relatedidents = api.uid_get_from_cstr("source.request.relatedidents")! + semantic_refactoring = api.uid_get_from_cstr("source.request.semantic.refactoring")! } } diff --git a/Tests/SourceKitTests/CodeActionTests.swift b/Tests/SourceKitTests/CodeActionTests.swift index f2db24b50..55ecfb925 100644 --- a/Tests/SourceKitTests/CodeActionTests.swift +++ b/Tests/SourceKitTests/CodeActionTests.swift @@ -18,6 +18,47 @@ import XCTest @testable import SourceKit final class CodeActionTests: XCTestCase { + + typealias CodeActionCapabilities = TextDocumentClientCapabilities.CodeAction + typealias CodeActionLiteralSupport = CodeActionCapabilities.CodeActionLiteralSupport + typealias CodeActionKindCapabilities = CodeActionLiteralSupport.CodeActionKind + + /// 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 + var documentCapabilities = TextDocumentClientCapabilities() + var codeActionCapabilities = CodeActionCapabilities() + let codeActionKinds = CodeActionKindCapabilities(valueSet: [.refactor, .quickFix]) + let codeActionLiteralSupport = CodeActionLiteralSupport(codeActionKind: codeActionKinds) + codeActionCapabilities.codeActionLiteralSupport = codeActionLiteralSupport + documentCapabilities.codeAction = codeActionCapabilities + _ = try! sk.sendSync(InitializeRequest( + processId: nil, + rootPath: nil, + rootURL: nil, + initializationOptions: nil, + capabilities: ClientCapabilities(workspace: nil, textDocument: documentCapabilities), + trace: .off, + workspaceFolders: nil)) + + workspace = connection.server!.workspace! + } + func testCodeActionResponseLegacySupport() { let command = Command(title: "Title", command: "Command", arguments: [1, "text", 2.2, nil]) let codeAction = CodeAction(title: "1") @@ -114,4 +155,57 @@ final class CodeActionTests: XCTestCase { let decoded = try! JSONDecoder().decode(Command.self, from: JSONEncoder().encode(command)) XCTAssertEqual(decoded, command) } + + func testEmptyCodeActionResult() { + let url = URL(fileURLWithPath: "/a.swift") + sk.allowUnexpectedNotification = true + + sk.send(DidOpenTextDocument(textDocument: TextDocumentItem( + url: url, + language: .swift, + version: 12, + text: """ + func foo() -> String { + var a = "abc" + return a + } + """))) + + let textDocument = TextDocumentIdentifier(url) + let start = Position(line: 2, utf16index: 0) + let request = CodeActionRequest(range: start.. String { + var a = "abc" + return a + } + """))) + + let textDocument = TextDocumentIdentifier(url) + let start = Position(line: 1, utf16index: 11) + let request = CodeActionRequest(range: start.. String { + var a = "abc" + return a + } + """))) + + let textDocument = TextDocumentIdentifier(url) + + let args = SemanticRefactorCommand(title: "Localize String", + actionString: "source.refactoring.kind.localize.string", + line: 1, + column: 10, + length: 5, + textDocument: textDocument) + + let command = try! args.asCommand() + let request = ExecuteCommandRequest(command: command.command, arguments: command.arguments) + + let result = try! sk.sendSync(request) + XCTAssertEqual(result, WorkspaceEdit(changes: [ + url: [TextEdit(range: Position(line: 1, utf16index: 10).. [XCTestCaseEntry] { testCase(CodeActionTests.__allTests__CodeActionTests), testCase(DocumentColorTests.__allTests__DocumentColorTests), testCase(DocumentSymbolTest.__allTests__DocumentSymbolTest), + testCase(ExecuteCommandTests.__allTests__ExecuteCommandTests), testCase(FoldingRangeTests.__allTests__FoldingRangeTests), testCase(LocalClangTests.__allTests__LocalClangTests), testCase(LocalSwiftTests.__allTests__LocalSwiftTests),