@@ -23,6 +23,31 @@ import SwiftParser
23
23
@_spi ( SourceKitLSP) import SwiftRefactor
24
24
import SwiftSyntax
25
25
26
+ /// Data that is attached to a `CompletionItem`.
27
+ private struct CompletionItemData : LSPAnyCodable {
28
+ let id : Int ?
29
+
30
+ init ( id: Int ? ) {
31
+ self . id = id
32
+ }
33
+
34
+ init ? ( fromLSPDictionary dictionary: [ String : LSPAny ] ) {
35
+ if case . int( let id) = dictionary [ " id " ] {
36
+ self . id = id
37
+ } else {
38
+ self . id = nil
39
+ }
40
+ }
41
+
42
+ func encodeToLSPAny( ) -> LSPAny {
43
+ var dict : [ String : LSPAny ] = [ : ]
44
+ if let id {
45
+ dict [ " id " ] = . int( id)
46
+ }
47
+ return . dictionary( dict)
48
+ }
49
+ }
50
+
26
51
/// Represents a code-completion session for a given source location that can be efficiently
27
52
/// re-filtered by calling `update()`.
28
53
///
@@ -98,7 +123,6 @@ class CodeCompletionSession {
98
123
options: SourceKitLSPOptions ,
99
124
indentationWidth: Trivia ? ,
100
125
completionPosition: Position ,
101
- completionUtf8Offset: Int ,
102
126
cursorPosition: Position ,
103
127
compileCommand: SwiftCompileCommand ? ,
104
128
clientSupportsSnippets: Bool ,
@@ -107,8 +131,9 @@ class CodeCompletionSession {
107
131
let task = completionQueue. asyncThrowing {
108
132
if let session = completionSessions [ ObjectIdentifier ( sourcekitd) ] , session. state == . open {
109
133
let isCompatible =
110
- session. snapshot. uri == snapshot. uri && session. utf8StartOffset == completionUtf8Offset
111
- && session. position == completionPosition && session. compileCommand == compileCommand
134
+ session. snapshot. uri == snapshot. uri
135
+ && session. position == completionPosition
136
+ && session. compileCommand == compileCommand
112
137
&& session. clientSupportsSnippets == clientSupportsSnippets
113
138
114
139
if isCompatible {
@@ -128,7 +153,6 @@ class CodeCompletionSession {
128
153
snapshot: snapshot,
129
154
options: options,
130
155
indentationWidth: indentationWidth,
131
- utf8Offset: completionUtf8Offset,
132
156
position: completionPosition,
133
157
compileCommand: compileCommand,
134
158
clientSupportsSnippets: clientSupportsSnippets
@@ -161,7 +185,6 @@ class CodeCompletionSession {
161
185
private let options : SourceKitLSPOptions
162
186
/// The inferred indentation width of the source file the completion is being performed in
163
187
private let indentationWidth : Trivia ?
164
- private let utf8StartOffset : Int
165
188
private let position : Position
166
189
private let compileCommand : SwiftCompileCommand ?
167
190
private let clientSupportsSnippets : Bool
@@ -180,7 +203,6 @@ class CodeCompletionSession {
180
203
snapshot: DocumentSnapshot ,
181
204
options: SourceKitLSPOptions ,
182
205
indentationWidth: Trivia ? ,
183
- utf8Offset: Int ,
184
206
position: Position ,
185
207
compileCommand: SwiftCompileCommand ? ,
186
208
clientSupportsSnippets: Bool
@@ -189,30 +211,30 @@ class CodeCompletionSession {
189
211
self . options = options
190
212
self . indentationWidth = indentationWidth
191
213
self . snapshot = snapshot
192
- self . utf8StartOffset = utf8Offset
193
214
self . position = position
194
215
self . compileCommand = compileCommand
195
216
self . clientSupportsSnippets = clientSupportsSnippets
196
217
}
197
218
198
219
private func open(
199
220
filterText: String ,
200
- position: Position ,
221
+ position cursorPosition : Position ,
201
222
in snapshot: DocumentSnapshot
202
223
) async throws -> CompletionList {
203
224
logger. info ( " Opening code completion session: \( self . description) filter= \( filterText) " )
204
225
guard snapshot. version == self . snapshot. version else {
205
226
throw ResponseError ( code: . invalidRequest, message: " open must use the original snapshot " )
206
227
}
207
228
229
+ let sourcekitdPosition = snapshot. sourcekitdPosition ( of: self . position)
208
230
let req = sourcekitd. dictionary ( [
209
231
keys. request: sourcekitd. requests. codeCompleteOpen,
210
- keys. offset: utf8StartOffset,
232
+ keys. line: sourcekitdPosition. line,
233
+ keys. column: sourcekitdPosition. utf8Column,
211
234
keys. name: uri. pseudoPath,
212
235
keys. sourceFile: uri. pseudoPath,
213
236
keys. sourceText: snapshot. text,
214
237
keys. codeCompleteOptions: optionsDictionary ( filterText: filterText) ,
215
- keys. compilerArgs: compileCommand? . compilerArgs as [ SKDRequestValue ] ? ,
216
238
] )
217
239
218
240
let dict = try await sourcekitd. send (
@@ -228,11 +250,11 @@ class CodeCompletionSession {
228
250
229
251
try Task . checkCancellation ( )
230
252
231
- return self . completionsFromSKDResponse (
253
+ return await self . completionsFromSKDResponse (
232
254
completions,
233
255
in: snapshot,
234
256
completionPos: self . position,
235
- requestPosition: position ,
257
+ requestPosition: cursorPosition ,
236
258
isIncomplete: true
237
259
)
238
260
}
@@ -243,10 +265,13 @@ class CodeCompletionSession {
243
265
in snapshot: DocumentSnapshot
244
266
) async throws -> CompletionList {
245
267
logger. info ( " Updating code completion session: \( self . description) filter= \( filterText) " )
268
+ let sourcekitdPosition = snapshot. sourcekitdPosition ( of: self . position)
246
269
let req = sourcekitd. dictionary ( [
247
270
keys. request: sourcekitd. requests. codeCompleteUpdate,
248
- keys. offset: utf8StartOffset,
271
+ keys. line: sourcekitdPosition. line,
272
+ keys. column: sourcekitdPosition. utf8Column,
249
273
keys. name: uri. pseudoPath,
274
+ keys. sourceFile: uri. pseudoPath,
250
275
keys. codeCompleteOptions: optionsDictionary ( filterText: filterText) ,
251
276
] )
252
277
@@ -259,7 +284,7 @@ class CodeCompletionSession {
259
284
return CompletionList ( isIncomplete: false , items: [ ] )
260
285
}
261
286
262
- return self . completionsFromSKDResponse (
287
+ return await self . completionsFromSKDResponse (
263
288
completions,
264
289
in: snapshot,
265
290
completionPos: self . position,
@@ -281,6 +306,7 @@ class CodeCompletionSession {
281
306
// Filtering options.
282
307
keys. filterText: filterText,
283
308
keys. requestLimit: 200 ,
309
+ keys. useNewAPI: 1 ,
284
310
] )
285
311
return dict
286
312
}
@@ -291,9 +317,12 @@ class CodeCompletionSession {
291
317
// Already closed, nothing to do.
292
318
break
293
319
case . open:
320
+ let sourcekitdPosition = snapshot. sourcekitdPosition ( of: self . position)
294
321
let req = sourcekitd. dictionary ( [
295
322
keys. request: sourcekitd. requests. codeCompleteClose,
296
- keys. offset: utf8StartOffset,
323
+ keys. line: sourcekitdPosition. line,
324
+ keys. column: sourcekitdPosition. utf8Column,
325
+ keys. sourceFile: snapshot. uri. pseudoPath,
297
326
keys. name: snapshot. uri. pseudoPath,
298
327
] )
299
328
logger. info ( " Closing code completion session: \( self . description) " )
@@ -356,7 +385,10 @@ class CodeCompletionSession {
356
385
completionPos: Position ,
357
386
requestPosition: Position ,
358
387
isIncomplete: Bool
359
- ) -> CompletionList {
388
+ ) async -> CompletionList {
389
+ let sourcekitd = self . sourcekitd
390
+ let keys = sourcekitd. keys
391
+
360
392
let completionItems = completions. compactMap { ( value: SKDResponseDictionary ) -> CompletionItem ? in
361
393
guard let name: String = value [ keys. description] ,
362
394
var insertText: String = value [ keys. sourceText]
@@ -366,7 +398,6 @@ class CodeCompletionSession {
366
398
367
399
var filterName : String ? = value [ keys. name]
368
400
let typeName : String ? = value [ sourcekitd. keys. typeName]
369
- let docBrief : String ? = value [ sourcekitd. keys. docBrief]
370
401
let utf8CodeUnitsToErase : Int = value [ sourcekitd. keys. numBytesToErase] ?? 0
371
402
372
403
if let closureExpanded = expandClosurePlaceholders ( insertText: insertText) {
@@ -398,22 +429,64 @@ class CodeCompletionSession {
398
429
// Map SourceKit's not_recommended field to LSP's deprecated
399
430
let notRecommended = ( value [ sourcekitd. keys. notRecommended] ?? 0 ) != 0
400
431
432
+ let sortText : String ?
433
+ if let semanticScore: Double = value [ sourcekitd. keys. semanticScore] {
434
+ // sourcekitd returns numeric completion item scores with a higher score being better. LSP's sort text is
435
+ // lexicographical. Map the numeric score to a lexicographically sortable score by subtracting it from 5_000.
436
+ // This gives us a valid range of semantic scores from -5_000 to 5_000 that can be sorted correctly
437
+ // lexicographically. This should be sufficient as semantic scores are typically single-digit.
438
+ var lexicallySortableScore = 5_000 - semanticScore
439
+ if lexicallySortableScore < 0 {
440
+ logger. fault ( " Semantic score out-of-bounds: \( semanticScore, privacy: . public) " )
441
+ lexicallySortableScore = 0
442
+ }
443
+ if lexicallySortableScore >= 10_000 {
444
+ logger. fault ( " Semantic score out-of-bounds: \( semanticScore, privacy: . public) " )
445
+ lexicallySortableScore = 9_999.99999999
446
+ }
447
+ sortText = String ( format: " %013.8f " , lexicallySortableScore) + " - \( name) "
448
+ } else {
449
+ sortText = nil
450
+ }
451
+
452
+ let data = CompletionItemData ( id: value [ keys. identifier] as Int ? )
453
+
401
454
let kind : sourcekitd_api_uid_t ? = value [ sourcekitd. keys. kind]
402
455
return CompletionItem (
403
456
label: name,
404
457
kind: kind? . asCompletionItemKind ( sourcekitd. values) ?? . value,
405
458
detail: typeName,
406
- documentation: docBrief != nil ? . markupContent ( MarkupContent ( kind : . markdown , value : docBrief! ) ) : nil ,
459
+ documentation: nil ,
407
460
deprecated: notRecommended,
408
- sortText: nil ,
461
+ sortText: sortText ,
409
462
filterText: filterName,
410
463
insertText: text,
411
464
insertTextFormat: isInsertTextSnippet ? . snippet : . plain,
412
- textEdit: textEdit. map ( CompletionItemEdit . textEdit)
465
+ textEdit: textEdit. map ( CompletionItemEdit . textEdit) ,
466
+ data: data. encodeToLSPAny ( )
413
467
)
414
468
}
415
469
416
- return CompletionList ( isIncomplete: isIncomplete, items: completionItems)
470
+ // TODO: Only compute documentation if the client doesn't support `completionItem/resolve`
471
+ // (https://github.com/swiftlang/sourcekit-lsp/issues/1935)
472
+ let withDocumentation = await completionItems. asyncMap { item in
473
+ var item = item
474
+
475
+ if let itemId = CompletionItemData ( fromLSPAny: item. data) ? . id {
476
+ let req = sourcekitd. dictionary ( [
477
+ keys. request: sourcekitd. requests. codeCompleteDocumentation,
478
+ keys. identifier: itemId,
479
+ ] )
480
+ let documentationResponse = try ? await sourcekitd. send ( req, timeout: . seconds( 1 ) , fileContents: snapshot. text)
481
+ if let docString: String = documentationResponse ? [ keys. docBrief] {
482
+ item. documentation = . markupContent( MarkupContent ( kind: . markdown, value: docString) )
483
+ }
484
+ }
485
+
486
+ return item
487
+ }
488
+
489
+ return CompletionList ( isIncomplete: isIncomplete, items: withDocumentation)
417
490
}
418
491
419
492
private func computeCompletionTextEdit(
0 commit comments