Skip to content

Commit

Permalink
Base CodeAction tests
Browse files Browse the repository at this point in the history
  • Loading branch information
rockbruno committed Jan 20, 2019
1 parent 5499db5 commit 7011c0a
Show file tree
Hide file tree
Showing 8 changed files with 130 additions and 16 deletions.
7 changes: 6 additions & 1 deletion Sources/LanguageServerProtocol/CodeAction.swift
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,14 @@ public struct CodeActionContext: Codable, Hashable {
/// If provided, actions of these kinds are filtered out by the client before being shown,
/// so servers can omit computing them.
public var only: [CodeActionKind]?

public init(diagnostics: [Diagnostic] = [], only: [CodeActionKind]?) {
self.diagnostics = diagnostics
self.only = only
}
}

public struct CodeAction: Codable, ResponseType {
public struct CodeAction: Codable, Equatable, ResponseType {

/// A short, human-readable, title for this code action.
public var title: String
Expand Down
16 changes: 14 additions & 2 deletions Sources/LanguageServerProtocol/Command.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,15 @@
//===----------------------------------------------------------------------===//

/// A `CommandData` represents the underlying data of a custom `Command`.
public protocol CommandData: Codable {
public protocol CommandData: Codable, Hashable {
static var identifier: String { get }
var textDocument: TextDocumentIdentifier { get set }
}

/// Represents a reference to a command identified by a string. Used as the result of
/// requests that returns actions to the user, later used as the parameter of
/// workspace/executeCommand if the user wishes to execute said command.
public struct Command<Data: CommandData> {
public struct Command<Data: CommandData>: Codable, Hashable {

/// Title of the command, like `save`.
public var title: String
Expand Down Expand Up @@ -85,6 +85,18 @@ extension AnyCommand: Codable {
}
}

extension AnyCommand: Equatable {
public static func == (lhs: AnyCommand, rhs: AnyCommand) -> Bool {
if let lhs = lhs.getAs(SemanticRefactorCommandData.self),
let rhs = rhs.getAs(SemanticRefactorCommandData.self)
{
return lhs == rhs
} else {
return false
}
}
}

public struct SemanticRefactorCommandData: CommandData {
public static let identifier = "sourcekit.lsp.semantic.refactoring.command"

Expand Down
12 changes: 7 additions & 5 deletions Sources/LanguageServerProtocol/ExecuteCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,9 @@
/// - arguments: The arguments to use when executing the command.
public struct ExecuteCommandRequest: TextDocumentRequest {
public static let method: String = "workspace/executeCommand"
// FIXME: A response isn't expected for this request. There's probably a better way
// to handle this.
public typealias Response = ExecuteCommandResponse?
// FIXME: The LSP defines the response of this request as Any?, but we return
// the edit for testing purposes.
public typealias Response = WorkspaceEdit?

/// The command to be executed.
public var command: AnyCommand
Expand All @@ -34,10 +34,12 @@ public struct ExecuteCommandRequest: TextDocumentRequest {
return command.textDocument
}

public init(command: AnyCommand) {
self.command = command
}

public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
command = try container.decode(AnyCommand.self)
}
}

public struct ExecuteCommandResponse: Codable, Hashable, ResponseType {}
2 changes: 1 addition & 1 deletion Sources/LanguageServerProtocol/WorkspaceEdit.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
//===----------------------------------------------------------------------===//

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

/// The edits to be applied to existing resources.
public var changes: [String: [TextEdit]]?
Expand Down
7 changes: 6 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,11 @@ public final class TestClient: LanguageServerEndpoint {
}

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

Expand Down
5 changes: 5 additions & 0 deletions Sources/SourceKit/sourcekitd/SemanticRefactoring.swift
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,11 @@ 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(
_ url: URL,
_ command: AnyCommand,
Expand Down
16 changes: 10 additions & 6 deletions Sources/SourceKit/sourcekitd/SwiftLanguageServer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -665,16 +665,18 @@ extension SwiftLanguageServer {
(retrieveRefactorCodeActions, .refactor)
//TODO: Quick fix and other action providers
]
let wantedActionKinds = req.params.context.only ?? []
let providers = providersAndKinds.filter {
wantedActionKinds.isEmpty || wantedActionKinds.contains($0.1)
}
let wantedActionKinds = req.params.context.only
let providers = providersAndKinds.filter { wantedActionKinds?.contains($0.1) != false }
retrieveCodeActions(req, providers: providers.map { $0.0 }) { codeActions in
req.reply(codeActions)
}
}

func retrieveCodeActions(_ req: Request<CodeActionRequest>, providers: [CodeActionProvider], completion: CodeActionProviderCompletion?) {
guard providers.isEmpty == false else {
completion?([])
return
}
var providersLeftToProcess = Set<Int>(0..<providers.count)
var codeActions = [CodeAction]()
for i in 0..<providers.count {
Expand All @@ -698,6 +700,7 @@ extension SwiftLanguageServer {
}
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)
Expand Down Expand Up @@ -762,15 +765,16 @@ extension SwiftLanguageServer {
}
return req.reply(nil)
}
let edit = refactor.edit
// TODO: Maybe we can route the command's title here so we can show something
// more meaningful to the user.
let editReq = ApplyEditRequest(label: "Command", edit: refactor.edit)
let editReq = ApplyEditRequest(label: "Command", edit: edit)
do {
_ = try self.client.sendSync(editReq)
} catch {
log("failed to applyEdit: \(error)", level: .warning)
}
req.reply(nil)
req.reply(edit)
}
}
}
Expand Down
81 changes: 81 additions & 0 deletions Tests/SourceKitTests/LocalSwiftTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -705,4 +705,85 @@ final class LocalSwiftTests: XCTestCase {
}
}
}

func testCodeActionContexts() {
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 range = Position(line: 1, utf16index: 10)..<Position(line: 1, utf16index: 15)
let emptyContext = CodeActionContext(only: [])
let emptyRequest = CodeActionRequest(range: range,
context: emptyContext,
textDocument: textDocument)
let emptyResult = try! sk.sendSync(emptyRequest)

XCTAssertEqual(emptyResult?.isEmpty, true)

let refactorContext = CodeActionContext(only: [.refactor])
let refactorRequest = CodeActionRequest(range: range,
context: refactorContext,
textDocument: textDocument)
let refactorResult = try! sk.sendSync(refactorRequest)
XCTAssertEqual(refactorResult, [
CodeAction(title: "Localize String",
kind: .refactor,
command: AnyCommand(
Command(title: "Localize String",
data: SemanticRefactorCommandData(
textDocument: textDocument,
actionString: "source.refactoring.kind.localize.string",
line: 1,
column: 10,
length: 5))))
])
}

func testSemanticRefactoring() {
let url = URL(fileURLWithPath: "/a.swift")
sk.allowUnexpectedNotification = true
sk.allowUnexpectedRequest = 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 command = Command(title: "Localize String",
data: SemanticRefactorCommandData(
textDocument: textDocument,
actionString: "source.refactoring.kind.localize.string",
line: 1,
column: 10,
length: 5))

let request = ExecuteCommandRequest(command: AnyCommand(command))
let result = try! sk.sendSync(request)
XCTAssertEqual(result, WorkspaceEdit(changes: [
url: [TextEdit(range: Position(line: 1, utf16index: 10)..<Position(line: 1, utf16index: 10),
newText: "NSLocalizedString("),
TextEdit(range: Position(line: 1, utf16index: 15)..<Position(line: 1, utf16index: 15),
newText: ", comment: \"\")")]
]))
}
}

0 comments on commit 7011c0a

Please sign in to comment.