@@ -23,6 +23,31 @@ import SwiftParser
2323@_spi ( SourceKitLSP) import SwiftRefactor
2424import SwiftSyntax
2525
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+
2651/// Represents a code-completion session for a given source location that can be efficiently
2752/// re-filtered by calling `update()`.
2853///
@@ -98,7 +123,6 @@ class CodeCompletionSession {
98123 options: SourceKitLSPOptions ,
99124 indentationWidth: Trivia ? ,
100125 completionPosition: Position ,
101- completionUtf8Offset: Int ,
102126 cursorPosition: Position ,
103127 compileCommand: SwiftCompileCommand ? ,
104128 clientSupportsSnippets: Bool ,
@@ -107,8 +131,9 @@ class CodeCompletionSession {
107131 let task = completionQueue. asyncThrowing {
108132 if let session = completionSessions [ ObjectIdentifier ( sourcekitd) ] , session. state == . open {
109133 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
112137 && session. clientSupportsSnippets == clientSupportsSnippets
113138
114139 if isCompatible {
@@ -128,7 +153,6 @@ class CodeCompletionSession {
128153 snapshot: snapshot,
129154 options: options,
130155 indentationWidth: indentationWidth,
131- utf8Offset: completionUtf8Offset,
132156 position: completionPosition,
133157 compileCommand: compileCommand,
134158 clientSupportsSnippets: clientSupportsSnippets
@@ -161,7 +185,6 @@ class CodeCompletionSession {
161185 private let options : SourceKitLSPOptions
162186 /// The inferred indentation width of the source file the completion is being performed in
163187 private let indentationWidth : Trivia ?
164- private let utf8StartOffset : Int
165188 private let position : Position
166189 private let compileCommand : SwiftCompileCommand ?
167190 private let clientSupportsSnippets : Bool
@@ -180,7 +203,6 @@ class CodeCompletionSession {
180203 snapshot: DocumentSnapshot ,
181204 options: SourceKitLSPOptions ,
182205 indentationWidth: Trivia ? ,
183- utf8Offset: Int ,
184206 position: Position ,
185207 compileCommand: SwiftCompileCommand ? ,
186208 clientSupportsSnippets: Bool
@@ -189,30 +211,30 @@ class CodeCompletionSession {
189211 self . options = options
190212 self . indentationWidth = indentationWidth
191213 self . snapshot = snapshot
192- self . utf8StartOffset = utf8Offset
193214 self . position = position
194215 self . compileCommand = compileCommand
195216 self . clientSupportsSnippets = clientSupportsSnippets
196217 }
197218
198219 private func open(
199220 filterText: String ,
200- position: Position ,
221+ position cursorPosition : Position ,
201222 in snapshot: DocumentSnapshot
202223 ) async throws -> CompletionList {
203224 logger. info ( " Opening code completion session: \( self . description) filter= \( filterText) " )
204225 guard snapshot. version == self . snapshot. version else {
205226 throw ResponseError ( code: . invalidRequest, message: " open must use the original snapshot " )
206227 }
207228
229+ let sourcekitdPosition = snapshot. sourcekitdPosition ( of: self . position)
208230 let req = sourcekitd. dictionary ( [
209231 keys. request: sourcekitd. requests. codeCompleteOpen,
210- keys. offset: utf8StartOffset,
232+ keys. line: sourcekitdPosition. line,
233+ keys. column: sourcekitdPosition. utf8Column,
211234 keys. name: uri. pseudoPath,
212235 keys. sourceFile: uri. pseudoPath,
213236 keys. sourceText: snapshot. text,
214237 keys. codeCompleteOptions: optionsDictionary ( filterText: filterText) ,
215- keys. compilerArgs: compileCommand? . compilerArgs as [ SKDRequestValue ] ? ,
216238 ] )
217239
218240 let dict = try await sourcekitd. send (
@@ -228,11 +250,11 @@ class CodeCompletionSession {
228250
229251 try Task . checkCancellation ( )
230252
231- return self . completionsFromSKDResponse (
253+ return await self . completionsFromSKDResponse (
232254 completions,
233255 in: snapshot,
234256 completionPos: self . position,
235- requestPosition: position ,
257+ requestPosition: cursorPosition ,
236258 isIncomplete: true
237259 )
238260 }
@@ -243,10 +265,13 @@ class CodeCompletionSession {
243265 in snapshot: DocumentSnapshot
244266 ) async throws -> CompletionList {
245267 logger. info ( " Updating code completion session: \( self . description) filter= \( filterText) " )
268+ let sourcekitdPosition = snapshot. sourcekitdPosition ( of: self . position)
246269 let req = sourcekitd. dictionary ( [
247270 keys. request: sourcekitd. requests. codeCompleteUpdate,
248- keys. offset: utf8StartOffset,
271+ keys. line: sourcekitdPosition. line,
272+ keys. column: sourcekitdPosition. utf8Column,
249273 keys. name: uri. pseudoPath,
274+ keys. sourceFile: uri. pseudoPath,
250275 keys. codeCompleteOptions: optionsDictionary ( filterText: filterText) ,
251276 ] )
252277
@@ -259,7 +284,7 @@ class CodeCompletionSession {
259284 return CompletionList ( isIncomplete: false , items: [ ] )
260285 }
261286
262- return self . completionsFromSKDResponse (
287+ return await self . completionsFromSKDResponse (
263288 completions,
264289 in: snapshot,
265290 completionPos: self . position,
@@ -281,6 +306,7 @@ class CodeCompletionSession {
281306 // Filtering options.
282307 keys. filterText: filterText,
283308 keys. requestLimit: 200 ,
309+ keys. useNewAPI: 1 ,
284310 ] )
285311 return dict
286312 }
@@ -291,9 +317,12 @@ class CodeCompletionSession {
291317 // Already closed, nothing to do.
292318 break
293319 case . open:
320+ let sourcekitdPosition = snapshot. sourcekitdPosition ( of: self . position)
294321 let req = sourcekitd. dictionary ( [
295322 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,
297326 keys. name: snapshot. uri. pseudoPath,
298327 ] )
299328 logger. info ( " Closing code completion session: \( self . description) " )
@@ -356,7 +385,10 @@ class CodeCompletionSession {
356385 completionPos: Position ,
357386 requestPosition: Position ,
358387 isIncomplete: Bool
359- ) -> CompletionList {
388+ ) async -> CompletionList {
389+ let sourcekitd = self . sourcekitd
390+ let keys = sourcekitd. keys
391+
360392 let completionItems = completions. compactMap { ( value: SKDResponseDictionary ) -> CompletionItem ? in
361393 guard let name: String = value [ keys. description] ,
362394 var insertText: String = value [ keys. sourceText]
@@ -366,7 +398,6 @@ class CodeCompletionSession {
366398
367399 var filterName : String ? = value [ keys. name]
368400 let typeName : String ? = value [ sourcekitd. keys. typeName]
369- let docBrief : String ? = value [ sourcekitd. keys. docBrief]
370401 let utf8CodeUnitsToErase : Int = value [ sourcekitd. keys. numBytesToErase] ?? 0
371402
372403 if let closureExpanded = expandClosurePlaceholders ( insertText: insertText) {
@@ -398,22 +429,64 @@ class CodeCompletionSession {
398429 // Map SourceKit's not_recommended field to LSP's deprecated
399430 let notRecommended = ( value [ sourcekitd. keys. notRecommended] ?? 0 ) != 0
400431
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+
401454 let kind : sourcekitd_api_uid_t ? = value [ sourcekitd. keys. kind]
402455 return CompletionItem (
403456 label: name,
404457 kind: kind? . asCompletionItemKind ( sourcekitd. values) ?? . value,
405458 detail: typeName,
406- documentation: docBrief != nil ? . markupContent ( MarkupContent ( kind : . markdown , value : docBrief! ) ) : nil ,
459+ documentation: nil ,
407460 deprecated: notRecommended,
408- sortText: nil ,
461+ sortText: sortText ,
409462 filterText: filterName,
410463 insertText: text,
411464 insertTextFormat: isInsertTextSnippet ? . snippet : . plain,
412- textEdit: textEdit. map ( CompletionItemEdit . textEdit)
465+ textEdit: textEdit. map ( CompletionItemEdit . textEdit) ,
466+ data: data. encodeToLSPAny ( )
413467 )
414468 }
415469
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)
417490 }
418491
419492 private func computeCompletionTextEdit(
0 commit comments