Files
sourcekit-lsp/Sources/LanguageServerProtocol/WorkspaceEdit.swift

310 lines
10 KiB
Swift

//===----------------------------------------------------------------------===//
//
// 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: Hashable, ResponseType {
/// The edits to be applied to existing resources.
public var changes: [DocumentURI: [TextEdit]]?
public var documentChanges: [WorkspaceEditDocumentChange]?
public init(changes: [DocumentURI: [TextEdit]]? = nil,
documentChanges: [WorkspaceEditDocumentChange]? = nil) {
self.changes = changes
self.documentChanges = documentChanges
}
}
// Workaround for Codable not correctly encoding dictionaries whose keys aren't strings.
extension WorkspaceEdit: Codable {
private enum CodingKeys: String, CodingKey {
case changes
case documentChanges
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
if let changesDict = try container.decodeIfPresent([String: [TextEdit]].self, forKey: .changes) {
var changes = [DocumentURI: [TextEdit]]()
for change in changesDict {
let uri = DocumentURI(string: change.key)
changes[uri] = change.value
}
self.changes = changes
} else {
self.changes = nil
}
self.documentChanges = try container.decodeIfPresent([WorkspaceEditDocumentChange].self, forKey: .documentChanges)
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
if let changes = changes {
var stringDictionary = [String: [TextEdit]]()
for (key, value) in changes {
stringDictionary[key.stringValue] = value
}
try container.encodeIfPresent(stringDictionary, forKey: .changes)
}
try container.encodeIfPresent(documentChanges, forKey: .documentChanges)
}
}
public enum WorkspaceEditDocumentChange: Codable, Hashable {
case textDocumentEdit(TextDocumentEdit)
case createFile(CreateFile)
case renameFile(RenameFile)
case deleteFile(DeleteFile)
public init(from decoder: Decoder) throws {
if let edit = try? TextDocumentEdit(from: decoder) {
self = .textDocumentEdit(edit)
} else if let createFile = try? CreateFile(from: decoder) {
self = .createFile(createFile)
} else if let renameFile = try? RenameFile(from: decoder) {
self = .renameFile(renameFile)
} else if let deleteFile = try? DeleteFile(from: decoder) {
self = .deleteFile(deleteFile)
} else {
let context = DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Expected TextDocumentEdit, CreateFile, RenameFile, or DeleteFile")
throw DecodingError.dataCorrupted(context)
}
}
public func encode(to encoder: Encoder) throws {
switch self {
case .textDocumentEdit(let textDocumentEdit):
try textDocumentEdit.encode(to: encoder)
case .createFile(let createFile):
try createFile.encode(to: encoder)
case .renameFile(let renameFile):
try renameFile.encode(to: encoder)
case .deleteFile(let deleteFile):
try deleteFile.encode(to: encoder)
}
}
}
/// Options to create a file.
public struct CreateFileOptions: Codable, Hashable {
/// Overwrite existing file. Overwrite wins over `ignoreIfExists`
public var overwrite: Bool?
/// Ignore if exists.
public var ignoreIfExists: Bool?
public init(overwrite: Bool? = nil, ignoreIfExists: Bool? = nil) {
self.overwrite = overwrite
self.ignoreIfExists = ignoreIfExists
}
}
/// Create file operation
public struct CreateFile: Codable, Hashable {
/// The resource to create.
public var uri: DocumentURI
/// Additional options
public var options: CreateFileOptions?
public init(uri: DocumentURI, options: CreateFileOptions? = nil) {
self.uri = uri
self.options = options
}
// MARK: Codable conformance
public enum CodingKeys: String, CodingKey {
case kind
case uri
case options
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let kind = try container.decode(String.self, forKey: .kind)
guard kind == "create" else {
throw DecodingError.dataCorruptedError(forKey: .kind, in: container, debugDescription: "Kind of CreateFile is not 'create'")
}
self.uri = try container.decode(DocumentURI.self, forKey: .uri)
self.options = try container.decodeIfPresent(CreateFileOptions.self, forKey: .options)
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode("create", forKey: .kind)
try container.encode(self.uri, forKey: .uri)
try container.encodeIfPresent(self.options, forKey: .options)
}
}
/// Rename file options
public struct RenameFileOptions: Codable, Hashable {
/// Overwrite target if existing. Overwrite wins over `ignoreIfExists`
public var overwrite: Bool?
/// Ignores if target exists.
public var ignoreIfExists: Bool?
public init(overwrite: Bool? = nil, ignoreIfExists: Bool? = nil) {
self.overwrite = overwrite
self.ignoreIfExists = ignoreIfExists
}
}
/// Rename file operation
public struct RenameFile: Codable, Hashable {
/// The old (existing) location.
public var oldUri: DocumentURI
/// The new location.
public var newUri: DocumentURI
/// Rename options.
public var options: RenameFileOptions?
public init(oldUri: DocumentURI, newUri: DocumentURI, options: RenameFileOptions? = nil) {
self.oldUri = oldUri
self.newUri = newUri
self.options = options
}
// MARK: Codable conformance
public enum CodingKeys: String, CodingKey {
case kind
case oldUri
case newUri
case options
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let kind = try container.decode(String.self, forKey: .kind)
guard kind == "rename" else {
throw DecodingError.dataCorruptedError(forKey: .kind, in: container, debugDescription: "Kind of RenameFile is not 'rename'")
}
self.oldUri = try container.decode(DocumentURI.self, forKey: .oldUri)
self.newUri = try container.decode(DocumentURI.self, forKey: .newUri)
self.options = try container.decodeIfPresent(RenameFileOptions.self, forKey: .options)
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode("rename", forKey: .kind)
try container.encode(self.oldUri, forKey: .oldUri)
try container.encode(self.newUri, forKey: .newUri)
try container.encodeIfPresent(self.options, forKey: .options)
}
}
/// Delete file options
public struct DeleteFileOptions: Codable, Hashable {
/// Delete the content recursively if a folder is denoted.
public var recursive: Bool?
/// Ignore the operation if the file doesn't exist.
public var ignoreIfNotExists: Bool?
public init(recursive: Bool? = nil, ignoreIfNotExists: Bool? = nil) {
self.recursive = recursive
self.ignoreIfNotExists = ignoreIfNotExists
}
}
/// Delete file operation
public struct DeleteFile: Codable, Hashable {
/// The file to delete.
public var uri: DocumentURI
/// Delete options.
public var options: DeleteFileOptions?
public init(uri: DocumentURI, options: DeleteFileOptions? = nil) {
self.uri = uri
self.options = options
}
// MARK: Codable conformance
public enum CodingKeys: String, CodingKey {
case kind
case uri
case options
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let kind = try container.decode(String.self, forKey: .kind)
guard kind == "delete" else {
throw DecodingError.dataCorruptedError(forKey: .kind, in: container, debugDescription: "Kind of DeleteFile is not 'delete'")
}
self.uri = try container.decode(DocumentURI.self, forKey: .uri)
self.options = try container.decodeIfPresent(DeleteFileOptions.self, forKey: .options)
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode("delete", forKey: .kind)
try container.encode(self.uri, forKey: .uri)
try container.encodeIfPresent(self.options, forKey: .options)
}
}
extension WorkspaceEdit: LSPAnyCodable {
public init?(fromLSPDictionary dictionary: [String : LSPAny]) {
guard case .dictionary(let lspDict) = dictionary[CodingKeys.changes.stringValue] else {
return nil
}
var dictionary = [DocumentURI: [TextEdit]]()
for (key, value) in lspDict {
let uri = DocumentURI(string: key)
guard let edits = [TextEdit](fromLSPArray: value) else {
return nil
}
dictionary[uri] = edits
}
self.changes = dictionary
}
public func encodeToLSPAny() -> LSPAny {
guard let changes = changes else {
return nil
}
let values = changes.map {
($0.key.stringValue, $0.value.encodeToLSPAny())
}
let dictionary = Dictionary(uniqueKeysWithValues: values)
return .dictionary([
CodingKeys.changes.stringValue: .dictionary(dictionary)
])
}
}
extension Array: LSPAnyCodable where Element: LSPAnyCodable {
public init?(fromLSPArray array: LSPAny) {
guard case .array(let array) = array else {
return nil
}
var result = [Element]()
for case .dictionary(let editDict) in array {
guard let element = Element.init(fromLSPDictionary: editDict) else {
return nil
}
result.append(element)
}
self = result
}
public init?(fromLSPDictionary dictionary: [String : LSPAny]) {
return nil
}
public func encodeToLSPAny() -> LSPAny {
return .array(map { $0.encodeToLSPAny() })
}
}