Skip to content

Commit 4962175

Browse files
committed
Update WorkspaceEdit to match version 3.14 of the LSP spec
1 parent 918e921 commit 4962175

File tree

2 files changed

+265
-13
lines changed

2 files changed

+265
-13
lines changed

Sources/LanguageServerProtocol/WorkspaceEdit.swift

Lines changed: 216 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -16,38 +16,241 @@ 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,
22+
documentChanges: [WorkspaceEditDocumentChange]? = nil) {
2023
self.changes = changes
24+
self.documentChanges = documentChanges
2125
}
2226
}
2327

2428
// Workaround for Codable not correctly encoding dictionaries whose keys aren't strings.
2529
extension WorkspaceEdit: Codable {
2630
private enum CodingKeys: String, CodingKey {
2731
case changes
32+
case documentChanges
2833
}
2934

3035
public init(from decoder: Decoder) throws {
3136
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
37+
if let changesDict = try container.decodeIfPresent([String: [TextEdit]].self, forKey: .changes) {
38+
var changes = [DocumentURI: [TextEdit]]()
39+
for change in changesDict {
40+
let uri = DocumentURI(string: change.key)
41+
changes[uri] = change.value
42+
}
43+
self.changes = changes
44+
} else {
45+
self.changes = nil
3746
}
38-
self.changes = changes
47+
self.documentChanges = try container.decodeIfPresent([WorkspaceEditDocumentChange].self, forKey: .documentChanges)
3948
}
4049

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

Tests/LanguageServerProtocolTests/CodingTests.swift

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -364,6 +364,55 @@ final class CodingTests: XCTestCase {
364364
"willSaveWaitUntil" : false
365365
}
366366
""")
367+
368+
checkCoding(WorkspaceEdit(documentChanges: [.textDocumentEdit(TextDocumentEdit(textDocument: VersionedTextDocumentIdentifier(uri, version: 2), edits: []))]), json: """
369+
{
370+
"documentChanges" : [
371+
{
372+
"edits" : [
373+
374+
],
375+
"textDocument" : {
376+
"uri" : "\(urljson)",
377+
"version" : 2
378+
}
379+
}
380+
]
381+
}
382+
""")
383+
checkCoding(WorkspaceEdit(documentChanges: [.createFile(CreateFile(uri: uri))]), json: """
384+
{
385+
"documentChanges" : [
386+
{
387+
"kind" : "create",
388+
"uri" : "\(urljson)"
389+
}
390+
]
391+
}
392+
""")
393+
checkCoding(WorkspaceEdit(documentChanges: [.renameFile(RenameFile(oldUri: uri, newUri: uri))]), json: """
394+
{
395+
"documentChanges" : [
396+
{
397+
"kind" : "rename",
398+
"newUri" : "\(urljson)",
399+
"oldUri" : "\(urljson)"
400+
}
401+
]
402+
}
403+
""")
404+
checkCoding(WorkspaceEdit(documentChanges: [.deleteFile(DeleteFile(uri: uri))]), json: """
405+
{
406+
"documentChanges" : [
407+
{
408+
"kind" : "delete",
409+
"uri" : "\(urljson)"
410+
}
411+
]
412+
}
413+
""")
414+
415+
367416
}
368417

369418
func testValueOrBool() {

0 commit comments

Comments
 (0)