Skip to content

Commit f9fbb37

Browse files
committed
Update WorkspaceEdit to match version 3.14 of the LSP spec
1 parent 717a53f commit f9fbb37

File tree

2 files changed

+263
-13
lines changed

2 files changed

+263
-13
lines changed

Sources/LanguageServerProtocol/WorkspaceEdit.swift

Lines changed: 215 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -16,38 +16,240 @@ public struct WorkspaceEdit: Hashable, ResponseType {
1616
/// The edits to be applied to existing resources.
1717
public var changes: [DocumentURI: [TextEdit]]?
1818

19-
public init(changes: [DocumentURI: [TextEdit]]?) {
19+
public var documentChanges: [WorkspaceEditDocumentChange]?
20+
21+
public init(changes: [DocumentURI: [TextEdit]]? = nil, documentChanges: [WorkspaceEditDocumentChange]? = nil) {
2022
self.changes = changes
23+
self.documentChanges = documentChanges
2124
}
2225
}
2326

2427
// Workaround for Codable not correctly encoding dictionaries whose keys aren't strings.
2528
extension WorkspaceEdit: Codable {
2629
private enum CodingKeys: String, CodingKey {
2730
case changes
31+
case documentChanges
2832
}
2933

3034
public init(from decoder: Decoder) throws {
3135
let container = try decoder.container(keyedBy: CodingKeys.self)
32-
let changesDict = try container.decode([String: [TextEdit]].self, forKey: .changes)
33-
var changes = [DocumentURI: [TextEdit]]()
34-
for change in changesDict {
35-
let uri = DocumentURI(string: change.key)
36-
changes[uri] = change.value
36+
if let changesDict = try container.decodeIfPresent([String: [TextEdit]].self, forKey: .changes) {
37+
var changes = [DocumentURI: [TextEdit]]()
38+
for change in changesDict {
39+
let uri = DocumentURI(string: change.key)
40+
changes[uri] = change.value
41+
}
42+
self.changes = changes
43+
} else {
44+
self.changes = nil
3745
}
38-
self.changes = changes
46+
self.documentChanges = try container.decodeIfPresent([WorkspaceEditDocumentChange].self, forKey: .documentChanges)
3947
}
4048

4149
public func encode(to encoder: Encoder) throws {
42-
guard let changes = changes else {
43-
return
50+
var container = encoder.container(keyedBy: CodingKeys.self)
51+
if let changes = changes {
52+
var stringDictionary = [String: [TextEdit]]()
53+
for (key, value) in changes {
54+
stringDictionary[key.stringValue] = value
55+
}
56+
try container.encodeIfPresent(stringDictionary, forKey: .changes)
57+
}
58+
try container.encodeIfPresent(documentChanges, forKey: .documentChanges)
59+
}
60+
}
61+
62+
public enum WorkspaceEditDocumentChange: Codable, Hashable {
63+
case textDocumentEdit(TextDocumentEdit)
64+
case createFile(CreateFile)
65+
case renameFile(RenameFile)
66+
case deleteFile(DeleteFile)
67+
68+
public init(from decoder: Decoder) throws {
69+
if let edit = try? TextDocumentEdit(from: decoder) {
70+
self = .textDocumentEdit(edit)
71+
} else if let createFile = try? CreateFile(from: decoder) {
72+
self = .createFile(createFile)
73+
} else if let renameFile = try? RenameFile(from: decoder) {
74+
self = .renameFile(renameFile)
75+
} else if let deleteFile = try? DeleteFile(from: decoder) {
76+
self = .deleteFile(deleteFile)
77+
} else {
78+
let context = DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Could neither decode WorkspaceEditDocumentChange as TextDocumentEdit, nor CreateFile, nor RenameFile, nor DeleteFile")
79+
throw DecodingError.dataCorrupted(context)
80+
}
81+
}
82+
83+
public func encode(to encoder: Encoder) throws {
84+
switch self {
85+
case .textDocumentEdit(let textDocumentEdit):
86+
try textDocumentEdit.encode(to: encoder)
87+
case .createFile(let createFile):
88+
try createFile.encode(to: encoder)
89+
case .renameFile(let renameFile):
90+
try renameFile.encode(to: encoder)
91+
case .deleteFile(let deleteFile):
92+
try deleteFile.encode(to: encoder)
93+
}
94+
}
95+
}
96+
97+
/// Options to create a file.
98+
public struct CreateFileOptions: Codable, Hashable {
99+
/// Overwrite existing file. Overwrite wins over `ignoreIfExists`
100+
public var overwrite: Bool?
101+
/// Ignore if exists.
102+
public var ignoreIfExists: Bool?
103+
104+
public init(overwrite: Bool? = nil, ignoreIfExists: Bool? = nil) {
105+
self.overwrite = overwrite
106+
self.ignoreIfExists = ignoreIfExists
107+
}
108+
}
109+
110+
/// Create file operation
111+
public struct CreateFile: Codable, Hashable {
112+
/// The resource to create.
113+
public var uri: DocumentURI
114+
/// Additional options
115+
public var options: CreateFileOptions?
116+
117+
public init(uri: DocumentURI, options: CreateFileOptions? = nil) {
118+
self.uri = uri
119+
self.options = options
120+
}
121+
122+
// MARK: Codable conformance
123+
124+
public enum CodingKeys: String, CodingKey {
125+
case kind
126+
case uri
127+
case options
128+
}
129+
130+
public init(from decoder: Decoder) throws {
131+
let container = try decoder.container(keyedBy: CodingKeys.self)
132+
let kind = try container.decode(String.self, forKey: .kind)
133+
guard kind == "create" else {
134+
throw DecodingError.dataCorruptedError(forKey: .kind, in: container, debugDescription: "Kind of CreateFile is not 'create'")
135+
}
136+
self.uri = try container.decode(DocumentURI.self, forKey: .uri)
137+
self.options = try container.decodeIfPresent(CreateFileOptions.self, forKey: .options)
138+
}
139+
140+
public func encode(to encoder: Encoder) throws {
141+
var container = encoder.container(keyedBy: CodingKeys.self)
142+
try container.encode("create", forKey: .kind)
143+
try container.encode(self.uri, forKey: .uri)
144+
try container.encodeIfPresent(self.options, forKey: .options)
145+
}
146+
}
147+
148+
/// Rename file options
149+
public struct RenameFileOptions: Codable, Hashable {
150+
/// Overwrite target if existing. Overwrite wins over `ignoreIfExists`
151+
public var overwrite: Bool?
152+
/// Ignores if target exists.
153+
public var ignoreIfExists: Bool?
154+
155+
public init(overwrite: Bool? = nil, ignoreIfExists: Bool? = nil) {
156+
self.overwrite = overwrite
157+
self.ignoreIfExists = ignoreIfExists
158+
}
159+
}
160+
161+
/// Rename file operation
162+
public struct RenameFile: Codable, Hashable {
163+
/// The old (existing) location.
164+
public var oldUri: DocumentURI
165+
/// The new location.
166+
public var newUri: DocumentURI
167+
/// Rename options.
168+
public var options: RenameFileOptions?
169+
170+
public init(oldUri: DocumentURI, newUri: DocumentURI, options: RenameFileOptions? = nil) {
171+
self.oldUri = oldUri
172+
self.newUri = newUri
173+
self.options = options
174+
}
175+
176+
// MARK: Codable conformance
177+
178+
public enum CodingKeys: String, CodingKey {
179+
case kind
180+
case oldUri
181+
case newUri
182+
case options
183+
}
184+
185+
public init(from decoder: Decoder) throws {
186+
let container = try decoder.container(keyedBy: CodingKeys.self)
187+
let kind = try container.decode(String.self, forKey: .kind)
188+
guard kind == "rename" else {
189+
throw DecodingError.dataCorruptedError(forKey: .kind, in: container, debugDescription: "Kind of RenameFile is not 'rename'")
44190
}
45-
var stringDictionary = [String: [TextEdit]]()
46-
for (key, value) in changes {
47-
stringDictionary[key.stringValue] = value
191+
self.oldUri = try container.decode(DocumentURI.self, forKey: .oldUri)
192+
self.newUri = try container.decode(DocumentURI.self, forKey: .newUri)
193+
self.options = try container.decodeIfPresent(RenameFileOptions.self, forKey: .options)
194+
}
195+
196+
public func encode(to encoder: Encoder) throws {
197+
var container = encoder.container(keyedBy: CodingKeys.self)
198+
try container.encode("rename", forKey: .kind)
199+
try container.encode(self.oldUri, forKey: .oldUri)
200+
try container.encode(self.newUri, forKey: .newUri)
201+
try container.encodeIfPresent(self.options, forKey: .options)
202+
}
203+
}
204+
205+
/// Delete file options
206+
public struct DeleteFileOptions: Codable, Hashable {
207+
/// Delete the content recursively if a folder is denoted.
208+
public var recursive: Bool?
209+
/// Ignore the operation if the file doesn't exist.
210+
public var ignoreIfNotExists: Bool?
211+
212+
public init(recursive: Bool? = nil, ignoreIfNotExists: Bool? = nil) {
213+
self.recursive = recursive
214+
self.ignoreIfNotExists = ignoreIfNotExists
215+
}
216+
}
217+
218+
/// Delete file operation
219+
public struct DeleteFile: Codable, Hashable {
220+
/// The file to delete.
221+
public var uri: DocumentURI
222+
/// Delete options.
223+
public var options: DeleteFileOptions?
224+
225+
public init(uri: DocumentURI, options: DeleteFileOptions? = nil) {
226+
self.uri = uri
227+
self.options = options
228+
}
229+
230+
// MARK: Codable conformance
231+
232+
public enum CodingKeys: String, CodingKey {
233+
case kind
234+
case uri
235+
case options
236+
}
237+
238+
public init(from decoder: Decoder) throws {
239+
let container = try decoder.container(keyedBy: CodingKeys.self)
240+
let kind = try container.decode(String.self, forKey: .kind)
241+
guard kind == "delete" else {
242+
throw DecodingError.dataCorruptedError(forKey: .kind, in: container, debugDescription: "Kind of DeleteFile is not 'delete'")
48243
}
244+
self.uri = try container.decode(DocumentURI.self, forKey: .uri)
245+
self.options = try container.decodeIfPresent(DeleteFileOptions.self, forKey: .options)
246+
}
247+
248+
public func encode(to encoder: Encoder) throws {
49249
var container = encoder.container(keyedBy: CodingKeys.self)
50-
try container.encode(stringDictionary, forKey: .changes)
250+
try container.encode("delete", forKey: .kind)
251+
try container.encode(self.uri, forKey: .uri)
252+
try container.encodeIfPresent(self.options, forKey: .options)
51253
}
52254
}
53255

Tests/LanguageServerProtocolTests/CodingTests.swift

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,54 @@ final class CodingTests: XCTestCase {
265265
}
266266
}
267267
""")
268+
269+
checkCoding(WorkspaceEdit(documentChanges: [.textDocumentEdit(TextDocumentEdit(textDocument: VersionedTextDocumentIdentifier(uri, version: 2), edits: []))]), json: """
270+
{
271+
"documentChanges" : [
272+
{
273+
"edits" : [
274+
275+
],
276+
"textDocument" : {
277+
"uri" : "\(urljson)",
278+
"version" : 2
279+
}
280+
}
281+
]
282+
}
283+
""")
284+
checkCoding(WorkspaceEdit(documentChanges: [.createFile(CreateFile(uri: uri))]), json: """
285+
{
286+
"documentChanges" : [
287+
{
288+
"kind" : "create",
289+
"uri" : "\(urljson)"
290+
}
291+
]
292+
}
293+
""")
294+
checkCoding(WorkspaceEdit(documentChanges: [.renameFile(RenameFile(oldUri: uri, newUri: uri))]), json: """
295+
{
296+
"documentChanges" : [
297+
{
298+
"kind" : "rename",
299+
"newUri" : "\(urljson)",
300+
"oldUri" : "\(urljson)"
301+
}
302+
]
303+
}
304+
""")
305+
checkCoding(WorkspaceEdit(documentChanges: [.deleteFile(DeleteFile(uri: uri))]), json: """
306+
{
307+
"documentChanges" : [
308+
{
309+
"kind" : "delete",
310+
"uri" : "\(urljson)"
311+
}
312+
]
313+
}
314+
""")
315+
268316
}
269317

270318
func testPositionRange() {

0 commit comments

Comments
 (0)