Skip to content

Commit

Permalink
Rework the Command architecture (2)
Browse files Browse the repository at this point in the history
  • Loading branch information
rockbruno committed Mar 3, 2019
1 parent d9e03ed commit f0b632c
Show file tree
Hide file tree
Showing 9 changed files with 292 additions and 126 deletions.
107 changes: 87 additions & 20 deletions Sources/LanguageServerProtocol/Command.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,34 +12,104 @@

import SKSupport

/// Represents the identifiers of SourceKit-LSP's supported commands.
public enum CommandIdentifier: String, Codable, CaseIterable {
case semanticRefactor = "sourcekit.lsp.semantic.refactoring.command"
}

/// 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 protocol Command {
/// The internal identifier for this command.
static var command: String { get set }
public enum Command: Hashable {
case semanticRefactor(TextDocumentIdentifier, SemanticRefactorCommandArgs)

/// The title of this command.
public var title: String {
switch self {
case let .semanticRefactor(_, args):
return args.title
}
}

/// The internal identifier of this command.
public var identifier: CommandIdentifier {
switch self {
case .semanticRefactor:
return CommandIdentifier.semanticRefactor
}
}

/// The arguments related to this command.
/// This is [Any]? in the LSP, but internally we treat it
/// differently to make it easier to create and (de)serialize commands.
var arguments: CommandArgsType? { get set }
}
/// This is [Any]? in the LSP, but treated differently here
/// to make it easier to create and (de)serialize commands.
public var arguments: CommandArgs? {
switch self {
case let .semanticRefactor(_, args):
return args
}
}

/// A `CommandDataType` represents the arguments required to execute a `Command`.
public protocol CommandArgsType: Codable {
var textDocument: TextDocumentIdentifier { get set }
/// The documented related to this command.
public var textDocument: TextDocumentIdentifier {
switch self {
case let .semanticRefactor(textDocument, _):
return textDocument
}
}
}

public struct SemanticRefactorCommand: Command {
public static let command = "sourcekit.lsp.semantic.refactoring.command"
public protocol CommandArgs: Codable {}
extension TextDocumentIdentifier: CommandArgs {}

public var arguments: SemanticRefactorCommandArgs?
extension Command: Codable {

public init(arguments: SemanticRefactorCommandArgs) {
self.arguments = arguments
public enum CodingKeys: String, CodingKey {
case title
case command
case arguments
}

public enum CodingError: Error {
case unknownCommand
}

public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let identifier = try container.decode(String.self, forKey: .command)
var argumentsContainer = try container.nestedUnkeyedContainer(forKey: .arguments)
// Command arguments are sent to the LSP as a [Any]? [textDocument, arguments?] array.
let textDocument = try argumentsContainer.decode(TextDocumentIdentifier.self)
switch identifier {
case CommandIdentifier.semanticRefactor.rawValue:
let args = try argumentsContainer.decode(SemanticRefactorCommandArgs.self)
self = .semanticRefactor(textDocument, args)
default:
log("Failed to decode Command: Unknown identifier \(identifier)", level: .warning)
throw CodingError.unknownCommand
}
}

public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(title, forKey: .title)
try container.encode(identifier, forKey: .command)
var argumentsContainer = container.nestedUnkeyedContainer(forKey: .arguments)
// Command arguments are sent to the LSP as a [Any]? [textDocument, arguments?] array.
try argumentsContainer.encode(textDocument)
try arguments?.encode(toArgumentsEncoder: &argumentsContainer)
}
}

public struct SemanticRefactorCommandArgs: CommandArgsType {
extension CommandArgs {
func encode(toArgumentsEncoder encoder: inout UnkeyedEncodingContainer) throws {
try encoder.encode(self)
}
}

public struct SemanticRefactorCommandArgs: CommandArgs, Hashable {

/// The name of this refactoring action.
public var title: String

/// The sourcekitd identifier of the refactoring action.
public var actionString: String
Expand All @@ -53,11 +123,8 @@ public struct SemanticRefactorCommandArgs: CommandArgsType {
/// The length of the range to refactor.
public var length: Int

public var textDocument: TextDocumentIdentifier

public init(actionString: String, line: Int, column: Int, length: Int, textDocument: TextDocumentIdentifier) {
public init(title: String, actionString: String, line: Int, column: Int, length: Int) {
self.title = title
self.textDocument = textDocument
self.actionString = actionString
self.line = line
self.column = column
Expand Down
6 changes: 3 additions & 3 deletions Sources/LanguageServerProtocol/ExecuteCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2014 - 2018 Apple Inc. and the Swift project authors
// 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
Expand Down Expand Up @@ -39,7 +39,7 @@ public struct ExecuteCommandRequest: TextDocumentRequest {
}

public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
command = try container.decode(Command.self)
let singleContainer = try decoder.singleValueContainer()
command = try singleContainer.decode(Command.self)
}
}
4 changes: 1 addition & 3 deletions Sources/LanguageServerProtocol/Messages.swift
Original file line number Diff line number Diff line change
Expand Up @@ -61,9 +61,7 @@ public let builtinNotifications: [NotificationType.Type] = [
/// The set of known commands.
///
/// All commands provided by the server should be listed here.
public let builtinCommands: [Command.Type] = [
SemanticRefactorCommand.self
]
public let builtinCommands: [CommandIdentifier] = CommandIdentifier.allCases

// MARK: Miscellaneous Message Types

Expand Down
2 changes: 1 addition & 1 deletion Sources/SourceKit/SourceKitServer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -258,7 +258,7 @@ extension SourceKitServer {
codeActionKinds: [.refactor]
),
executeCommandProvider: ExecuteCommandOptions(
commands: builtinCommands.map { $0.identifier }
commands: builtinCommands.map { $0.rawValue }
)
)))
}
Expand Down
11 changes: 1 addition & 10 deletions Sources/SourceKit/sourcekitd/SemanticRefactoring.swift
Original file line number Diff line number Diff line change
Expand Up @@ -85,9 +85,6 @@ enum SemanticRefactoringError: Error {
/// The given URL is not a known document.
case unknownDocument(URL)

/// The given command is not a known command.
case unknownCommand(Command)

/// The underlying sourcekitd request failed with the given error.
case responseError(ResponseError)
}
Expand All @@ -97,8 +94,6 @@ extension SemanticRefactoringError: CustomStringConvertible {
switch self {
case .unknownDocument(let url):
return "failed to find snapshot for url \(url)"
case .unknownCommand(let command):
return "invalid refactoring command \(command)"
case .responseError(let error):
return "\(error)"
}
Expand All @@ -117,16 +112,12 @@ extension SwiftLanguageServer {
/// - completion: Completion block to asynchronously receive the SemanticRefactoring data, or error.
func semanticRefactoring(
_ url: URL,
_ command: Command,
_ refactorCommand: SemanticRefactorCommandArgs,
_ completion: @escaping (Result<SemanticRefactoring?, SemanticRefactoringError>) -> Void)
{
guard let refactorCommand = command.getDataAs(SemanticRefactorCommandDataType.self) else {
return completion(.failure(.unknownCommand(command)))
}
guard let snapshot = documentManager.latestSnapshot(url) else {
return completion(.failure(.unknownDocument(url)))
}

let url = snapshot.document.url
let skreq = SKRequestDictionary(sourcekitd: sourcekitd)
skreq[keys.request] = requests.semantic_refactoring
Expand Down
23 changes: 13 additions & 10 deletions Sources/SourceKit/sourcekitd/SwiftLanguageServer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@ extension SwiftLanguageServer {
codeActionProvider: CodeActionOptions(
codeActionKinds: [.refactor]),
executeCommandProvider: ExecuteCommandOptions(
commands: builtinCommands.map { $0.identifier })
commands: builtinCommands.map { $0.rawValue })
)))
}

Expand Down Expand Up @@ -736,13 +736,12 @@ extension SwiftLanguageServer {
guard actionName != "source.refactoring.kind.rename.global" else {
return true
}
let data = SemanticRefactorCommandDataType(title: name,
textDocument: params.textDocument,
actionString: actionName,
line: params.range.lowerBound.line,
column: params.range.lowerBound.utf16index,
length: length)
let command = Command(data: data)
let arguments = SemanticRefactorCommandArgs(title: name,
actionString: actionName,
line: params.range.lowerBound.line,
column: params.range.lowerBound.utf16index,
length: length)
let command = Command.semanticRefactor(params.textDocument, arguments)
let codeAction = CodeAction(title: name, kind: .refactor, command: command)
codeActions.append(codeAction)
}
Expand All @@ -756,10 +755,14 @@ extension SwiftLanguageServer {
}

func executeCommand(_ req: Request<ExecuteCommandRequest>) {
//TODO: If there's support for several types of commands, we might need to structure this similarly to the code actions request.
let params = req.params
let url = params.textDocument.url
semanticRefactoring(url, params.command) { result in
//TODO: If there's support for several types of commands, we might need to structure this similarly to the code actions request.
guard case let .semanticRefactor(_, arguments) = params.command else {
log("semantic refactoring: unknown command \(params.command.identifier)", level: .warning)
return req.reply(nil)
}
semanticRefactoring(url, arguments) { result in
guard let refactor: SemanticRefactoring = result.success ?? nil else {
if let error = result.failure {
log("semantic refactoring failed \(url): \(error)", level: .warning)
Expand Down
Loading

0 comments on commit f0b632c

Please sign in to comment.