Skip to content

Commit

Permalink
Add Local Refactor
Browse files Browse the repository at this point in the history
Tests

READMË
  • Loading branch information
rockbruno committed Jul 8, 2019
1 parent 414de87 commit 5f912cf
Show file tree
Hide file tree
Showing 13 changed files with 481 additions and 11 deletions.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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. |
Expand Down
2 changes: 1 addition & 1 deletion Sources/LanguageServerProtocol/ApplyEdit.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ public struct ApplyEditRequest: RequestType {
/// The edits to apply.
public var edit: WorkspaceEdit

public init(label: String? = nil, edit: WorkspaceEdit) {
public init(label: String?, edit: WorkspaceEdit) {
self.label = label
self.edit = edit
}
Expand Down
11 changes: 11 additions & 0 deletions Sources/LanguageServerProtocol/ClientCapabilities.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down
2 changes: 1 addition & 1 deletion Sources/LanguageServerProtocol/ExecuteCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ public struct ExecuteCommandRequest: RequestType {
guard case .dictionary(let dictionary)? = arguments?.last else {
return nil
}
guard let data = try? JSONSerialization.data(withJSONObject: dictionary, options: []) else {
guard let data = try? JSONEncoder().encode(dictionary) else {
return nil
}
return try? JSONDecoder().decode(SourceKitLSPCommandMetadata.self, from: data)
Expand Down
6 changes: 5 additions & 1 deletion Sources/SKTestSupport/TestJSONRPCConnection.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<N>(_ handler: @escaping (Notification<N>) -> Void) {
oneShotNotificationHandlers.append({ anyNote in
Expand Down Expand Up @@ -125,7 +126,10 @@ public final class TestClient: LanguageServerEndpoint {
}

override public func _handleUnknown<R>(_ request: Request<R>) where R : RequestType {
fatalError()
guard allowUnexpectedRequest else {
fatalError("unexpected request \(request)")
}
request.reply(.failure(.cancelled))
}
}

Expand Down
2 changes: 1 addition & 1 deletion Sources/SourceKit/SourceKitServer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -269,7 +269,7 @@ 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?
Expand Down
152 changes: 152 additions & 0 deletions Sources/SourceKit/sourcekitd/SemanticRefactoring.swift
Original file line number Diff line number Diff line change
@@ -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..<endPosition, newText: text)
textEdits.append(edit)
}
return true
}
return true
}

guard textEdits.isEmpty == false else {
return nil
}

self.title = title
self.edit = WorkspaceEdit(changes: [url: textEdits])
}
}

/// An error from a cursor info request.
enum SemanticRefactoringError: Error {

/// The given URL is not a known document.
case unknownDocument(URL)

/// The underlying sourcekitd request failed with the given error.
case responseError(ResponseError)

/// The underlying sourcekitd reported no edits for this action.
case noEditsNeeded(URL)
}

extension SemanticRefactoringError: CustomStringConvertible {
var description: String {
switch self {
case .unknownDocument(let url):
return "failed to find snapshot for url \(url)"
case .responseError(let error):
return "\(error)"
case .noEditsNeeded(let url):
return "no edits reported for semantic refactoring action for url \(url)"
}
}
}

extension SwiftLanguageServer {

/// Provides 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.
///
/// - Parameters:
/// - url: Document URL in which to perform the request. Must be an open document.
/// - command: The semantic refactor `Command` that triggered this request.
/// - completion: Completion block to asynchronously receive the SemanticRefactoring data, or error.
func semanticRefactoring(
_ refactorCommand: SemanticRefactorCommand,
_ completion: @escaping (Result<SemanticRefactoring, SemanticRefactoringError>) -> 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
}
}
4 changes: 3 additions & 1 deletion Sources/SourceKit/sourcekitd/SwiftCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ import Foundation
/// The set of known Swift commands.
///
/// All commands from the Swift LSP should be listed here.
public let builtinSwiftCommands: [String] = []
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 {
Expand Down
104 changes: 100 additions & 4 deletions Sources/SourceKit/sourcekitd/SwiftLanguageServer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,7 @@ 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)
)))
Expand Down Expand Up @@ -857,8 +857,8 @@ extension SwiftLanguageServer {

func codeAction(_ req: Request<CodeActionRequest>) {
let providersAndKinds: [(provider: CodeActionProvider, kind: CodeActionKind)] = [
(retrieveRefactorCodeActions, .refactor)
//TODO: Implement the providers.
//(retrieveRefactorCodeActions, .refactor),
//(retrieveQuickFixCodeActions, .quickFix)
]
let wantedActionKinds = req.params.context.only
Expand Down Expand Up @@ -892,9 +892,105 @@ 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<ExecuteCommandRequest>) {
//TODO: Implement commands.
return req.reply(nil)
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(nil)
}
}

func applyEdit(label: String, edit: WorkspaceEdit) {
Expand Down
Loading

0 comments on commit 5f912cf

Please sign in to comment.