Skip to content

Commit 36450e4

Browse files
committed
Add the textEdit parameter to code completion responses
1 parent 42f15b8 commit 36450e4

File tree

5 files changed

+85
-16
lines changed

5 files changed

+85
-16
lines changed

Sources/SourceKit/sourcekitd/SwiftLanguageServer.swift

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -373,7 +373,7 @@ extension SwiftLanguageServer {
373373
return true // continue
374374
}
375375

376-
let filterName: String? = value[self.keys.name]
376+
var filterName: String? = value[self.keys.name]
377377
let insertText: String? = value[self.keys.sourcetext]
378378
let typeName: String? = value[self.keys.typename]
379379

@@ -384,13 +384,35 @@ extension SwiftLanguageServer {
384384
}
385385
let isInsertTextSnippet = clientSupportsSnippets && text != insertText
386386

387+
let textEdit: TextEdit?
388+
if let text = text {
389+
let bytesToErase: Int = value[self.keys.num_bytes_to_erase] ?? 0
390+
assert(bytesToErase <= completionPos.utf16index, "Deleting more bytes than are in the current line")
391+
let textEditRangeStart = Position(line: completionPos.line, utf16index: completionPos.utf16index - bytesToErase)
392+
textEdit = TextEdit(range: textEditRangeStart..<req.params.position, newText: text)
393+
394+
if bytesToErase != 0 && filterName != nil {
395+
// To support the case where the client is doing prefix matching on the TextEdit range,
396+
// we need to prepend the deleted text to filterText.
397+
// This also works around a behaviour in VS Code that causes completions to not show up
398+
// if a '.' is being replaced for Optional completion.
399+
let line = snapshot.lineTable[completionPos.line]
400+
let startIndex = line.utf16.index(line.startIndex, offsetBy: textEditRangeStart.utf16index)
401+
let endIndex = line.utf16.index(startIndex, offsetBy: bytesToErase)
402+
let filterPrefix = line[startIndex..<endIndex]
403+
filterName = filterPrefix + filterName!
404+
}
405+
} else {
406+
textEdit = nil
407+
}
408+
387409
let kind: sourcekitd_uid_t? = value[self.keys.kind]
388410
result.items.append(CompletionItem(
389411
label: name,
390412
detail: typeName,
391413
sortText: nil,
392414
filterText: filterName,
393-
textEdit: nil,
415+
textEdit: textEdit,
394416
insertText: text,
395417
insertTextFormat: isInsertTextSnippet ? .snippet : .plain,
396418
kind: kind?.asCompletionItemKind(self.values) ?? .value,

Sources/SourceKit/sourcekitd/SwiftSourceKitFramework.swift

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,14 @@ struct sourcekitd_keys {
241241
let edits: sourcekitd_uid_t
242242
let text: sourcekitd_uid_t
243243

244+
// Code Completion related keys.
245+
let context: sourcekitd_uid_t
246+
let doc: sourcekitd_uid_t
247+
let not_recommended: sourcekitd_uid_t
248+
let num_bytes_to_erase: sourcekitd_uid_t
249+
let associated_usrs: sourcekitd_uid_t
250+
251+
244252
init(api: sourcekitd_functions_t) {
245253
request = api.uid_get_from_cstr("key.request")!
246254
compilerargs = api.uid_get_from_cstr("key.compilerargs")!
@@ -280,6 +288,13 @@ struct sourcekitd_keys {
280288
categorizededits = api.uid_get_from_cstr("key.categorizededits")!
281289
edits = api.uid_get_from_cstr("key.edits")!
282290
text = api.uid_get_from_cstr("key.text")!
291+
292+
// Code Completion related keys.
293+
context = api.uid_get_from_cstr("key.context")!
294+
doc = api.uid_get_from_cstr("key.doc.brief")!
295+
not_recommended = api.uid_get_from_cstr("key.not_recommended")!
296+
num_bytes_to_erase = api.uid_get_from_cstr("key.num_bytes_to_erase")!
297+
associated_usrs = api.uid_get_from_cstr("key.associated_usrs")!
283298
}
284299
}
285300

Tests/SourceKitTests/SourceKitTests.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,7 @@ final class SKTests: XCTestCase {
156156
detail: "Void",
157157
sortText: nil,
158158
filterText: "method(a:)",
159-
textEdit: nil,
159+
textEdit: TextEdit(range: Position(line: 1, utf16index: 14)..<Position(line: 1, utf16index: 14), newText: "method(a: )"),
160160
insertText: "method(a: )",
161161
insertTextFormat: .plain,
162162
kind: .method,
@@ -166,7 +166,7 @@ final class SKTests: XCTestCase {
166166
detail: "A",
167167
sortText: nil,
168168
filterText: "self",
169-
textEdit: nil,
169+
textEdit: TextEdit(range: Position(line: 1, utf16index: 14)..<Position(line: 1, utf16index: 14), newText: "self"),
170170
insertText: "self",
171171
insertTextFormat: .plain,
172172
kind: .keyword,

Tests/SourceKitTests/SwiftCompletionTests.swift

Lines changed: 42 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -101,8 +101,7 @@ final class SwiftCompletionTests: XCTestCase {
101101
XCTAssertEqual(abc.kind, .property)
102102
XCTAssertEqual(abc.detail, "Int")
103103
XCTAssertEqual(abc.filterText, "abc")
104-
// FIXME:
105-
XCTAssertNil(abc.textEdit)
104+
XCTAssertEqual(abc.textEdit, TextEdit(range: Position(line: 4, utf16index: 9)..<Position(line: 4, utf16index: 9), newText: "abc"))
106105
XCTAssertEqual(abc.insertText, "abc")
107106
XCTAssertEqual(abc.insertTextFormat, .plain)
108107
}
@@ -111,8 +110,18 @@ final class SwiftCompletionTests: XCTestCase {
111110
let inIdent = try! sk.sendSync(CompletionRequest(
112111
textDocument: TextDocumentIdentifier(url),
113112
position: Position(line: 4, utf16index: col)))
113+
guard let abc = inIdent.items.first(where: { $0.label == "abc" }) else {
114+
XCTFail("No completion item with label 'abc'")
115+
return
116+
}
117+
114118
// If we switch to server-side filtering this will change.
115-
XCTAssertEqual(inIdent, selfDot)
119+
XCTAssertEqual(abc.kind, .property)
120+
XCTAssertEqual(abc.detail, "Int")
121+
XCTAssertEqual(abc.filterText, "abc")
122+
XCTAssertEqual(abc.textEdit, TextEdit(range: Position(line: 4, utf16index: 9)..<Position(line: 4, utf16index: col), newText: "abc"))
123+
XCTAssertEqual(abc.insertText, "abc")
124+
XCTAssertEqual(abc.insertTextFormat, .plain)
116125
}
117126

118127
let after = try! sk.sendSync(CompletionRequest(
@@ -150,8 +159,7 @@ final class SwiftCompletionTests: XCTestCase {
150159
XCTAssertEqual(test.kind, .method)
151160
XCTAssertEqual(test.detail, "Void")
152161
XCTAssertEqual(test.filterText, "test(a:)")
153-
// FIXME:
154-
XCTAssertNil(test.textEdit)
162+
XCTAssertEqual(test.textEdit, TextEdit(range: Position(line: 4, utf16index: 9)..<Position(line: 4, utf16index: 9), newText: "test(a: ${1:Int})"))
155163
XCTAssertEqual(test.insertText, "test(a: ${1:Int})")
156164
XCTAssertEqual(test.insertTextFormat, .snippet)
157165
}
@@ -162,8 +170,7 @@ final class SwiftCompletionTests: XCTestCase {
162170
XCTAssertEqual(test.kind, .method)
163171
XCTAssertEqual(test.detail, "Void")
164172
XCTAssertEqual(test.filterText, "test(:)")
165-
// FIXME:
166-
XCTAssertNil(test.textEdit)
173+
XCTAssertEqual(test.textEdit, TextEdit(range: Position(line: 8, utf16index: 9)..<Position(line: 8, utf16index: 9), newText: "test(${1:b: Int})"))
167174
XCTAssertEqual(test.insertText, "test(${1:b: Int})")
168175
XCTAssertEqual(test.insertTextFormat, .snippet)
169176
}
@@ -179,8 +186,7 @@ final class SwiftCompletionTests: XCTestCase {
179186
XCTAssertEqual(test.kind, .method)
180187
XCTAssertEqual(test.detail, "Void")
181188
XCTAssertEqual(test.filterText, "test(a:)")
182-
// FIXME:
183-
XCTAssertNil(test.textEdit)
189+
XCTAssertEqual(test.textEdit, TextEdit(range: Position(line: 4, utf16index: 9)..<Position(line: 4, utf16index: 9), newText: "test(a: )"))
184190
XCTAssertEqual(test.insertText, "test(a: )")
185191
XCTAssertEqual(test.insertTextFormat, .plain)
186192
}
@@ -192,7 +198,7 @@ final class SwiftCompletionTests: XCTestCase {
192198
XCTAssertEqual(test.detail, "Void")
193199
XCTAssertEqual(test.filterText, "test(:)")
194200
// FIXME:
195-
XCTAssertNil(test.textEdit)
201+
XCTAssertEqual(test.textEdit, TextEdit(range: Position(line: 8, utf16index: 9)..<Position(line: 8, utf16index: 9), newText: "test()"))
196202
XCTAssertEqual(test.insertText, "test()")
197203
XCTAssertEqual(test.insertTextFormat, .plain)
198204
}
@@ -221,4 +227,30 @@ final class SwiftCompletionTests: XCTestCase {
221227
position: Position(line: 1, utf16index: 0)))
222228
XCTAssertTrue(outOfRange2.isIncomplete)
223229
}
230+
231+
func testCompletionOptional() {
232+
initializeServer()
233+
let url = URL(fileURLWithPath: "/a.swift")
234+
let text = """
235+
struct Foo {
236+
let bar: Int
237+
}
238+
let a: Foo? = Foo(bar: 1)
239+
a.ba
240+
"""
241+
openDocument(text: text, url: url)
242+
243+
for col in 2...4 {
244+
let response = try! sk.sendSync(CompletionRequest(
245+
textDocument: TextDocumentIdentifier(url),
246+
position: Position(line: 4, utf16index: col)))
247+
XCTAssertFalse(response.isIncomplete)
248+
guard let item = response.items.first(where: { $0.label == "bar" }) else {
249+
XCTFail("No completion item with label 'bar'")
250+
return
251+
}
252+
XCTAssertEqual(item.filterText, ".bar")
253+
XCTAssertEqual(item.textEdit, TextEdit(range: Position(line: 4, utf16index: 1)..<Position(line: 4, utf16index: col), newText: "?.bar"))
254+
}
255+
}
224256
}

Tests/SourceKitTests/SwiftPMIntegration.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ final class SwiftPMIntegrationTests: XCTestCase {
3737
detail: "Void",
3838
sortText: nil,
3939
filterText: "foo()",
40-
textEdit: nil,
40+
textEdit: TextEdit(range: Position(line: 2, utf16index: 24)..<Position(line: 2, utf16index: 24), newText: "foo()"),
4141
insertText: "foo()",
4242
insertTextFormat: .plain,
4343
kind: .method,
@@ -47,7 +47,7 @@ final class SwiftPMIntegrationTests: XCTestCase {
4747
detail: "Lib",
4848
sortText: nil,
4949
filterText: "self",
50-
textEdit: nil,
50+
textEdit: TextEdit(range: Position(line: 2, utf16index: 24)..<Position(line: 2, utf16index: 24), newText: "self"),
5151
insertText: "self",
5252
insertTextFormat: .plain,
5353
kind: .keyword,

0 commit comments

Comments
 (0)