@@ -25,10 +25,128 @@ import SourceKitD
25
25
///
26
26
/// At the sourcekitd level, this uses `codecomplete.open`, `codecomplete.update` and
27
27
/// `codecomplete.close` requests.
28
- actor CodeCompletionSession {
28
+ class CodeCompletionSession {
29
+ // MARK: - Public static API
30
+
31
+ /// The queue on which all code completion requests are executed.
32
+ ///
33
+ /// This is needed because sourcekitd has a single, global code completion
34
+ /// session and we need to make sure that multiple code completion requests
35
+ /// don't race each other.
36
+ ///
37
+ /// Technically, we would only need one queue for each sourcekitd and different
38
+ /// sourcekitd could serve code completion requests simultaneously.
39
+ ///
40
+ /// But it's rare to open multiple sourcekitd instances simultaneously and
41
+ /// even rarer to interact with them at the same time, so we have a global
42
+ /// queue for now to simplify the implementation.
43
+ private static let completionQueue = AsyncQueue ( . serial)
44
+
45
+ /// The code completion session for each sourcekitd instance.
46
+ ///
47
+ /// `sourcekitd` has a global code completion session, that's why we need to
48
+ /// have a global mapping from `sourcekitd` to its currently active code
49
+ /// completion session.
50
+ ///
51
+ /// Modification of code completion sessions should only happen on
52
+ /// `completionQueue`.
53
+ private static var completionSessions : [ ObjectIdentifier : CodeCompletionSession ] = [ : ]
54
+
55
+ /// Gets the code completion results for the given parameters.
56
+ ///
57
+ /// If a code completion session that is compatible with the parameters
58
+ /// already exists, this just performs an update to the filtering. If it does
59
+ /// not, this opens a new code completion session with `sourcekitd` and gets
60
+ /// the results.
61
+ ///
62
+ /// - Parameters:
63
+ /// - sourcekitd: The `sourcekitd` instance from which to get code
64
+ /// completion results
65
+ /// - snapshot: The document in which to perform completion.
66
+ /// - completionPosition: The position at which to perform completion.
67
+ /// This is the position at which the code completion token logically
68
+ /// starts. For example when completing `foo.ba|`, then the completion
69
+ /// position should be after the `.`.
70
+ /// - completionUtf8Offset: Same as `completionPosition` but as a UTF-8
71
+ /// offset within the buffer.
72
+ /// - cursorPosition: The position at which the cursor is positioned. E.g.
73
+ /// when completing `foo.ba|`, this is after the `a` (see
74
+ /// `completionPosition` for comparison)
75
+ /// - compileCommand: The compiler arguments to use.
76
+ /// - options: Further options that can be sent from the editor to control
77
+ /// completion.
78
+ /// - clientSupportsSnippets: Whether the editor supports LSP snippets.
79
+ /// - filterText: The text by which to filter code completion results.
80
+ /// - mustReuse: If `true` and there is an active session in this
81
+ /// `sourcekitd` instance, cancel the request instead of opening a new
82
+ /// session.
83
+ /// This is set to `true` when triggering a filter from incomplete results
84
+ /// so that clients can rely on results being delivered quickly when
85
+ /// getting updated results after updating the filter text.
86
+ /// - Returns: The code completion results for those parameters.
87
+ static func completionList(
88
+ sourcekitd: any SourceKitD ,
89
+ snapshot: DocumentSnapshot ,
90
+ completionPosition: Position ,
91
+ completionUtf8Offset: Int ,
92
+ cursorPosition: Position ,
93
+ compileCommand: SwiftCompileCommand ? ,
94
+ options: SKCompletionOptions ,
95
+ clientSupportsSnippets: Bool ,
96
+ filterText: String ,
97
+ mustReuse: Bool
98
+ ) async throws -> CompletionList {
99
+ let task = completionQueue. asyncThrowing {
100
+ if let session = completionSessions [ ObjectIdentifier ( sourcekitd) ] , session. state == . open {
101
+ let isCompatible = session. snapshot. uri == snapshot. uri &&
102
+ session. utf8StartOffset == completionUtf8Offset &&
103
+ session. position == completionPosition &&
104
+ session. compileCommand == compileCommand &&
105
+ session. clientSupportsSnippets == clientSupportsSnippets
106
+
107
+ if isCompatible {
108
+ return try await session. update ( filterText: filterText, position: cursorPosition, in: snapshot, options: options)
109
+ }
110
+
111
+ if mustReuse {
112
+ logger. error (
113
+ """
114
+ triggerFromIncompleteCompletions with incompatible completion session; expected \
115
+ \( session. uri. forLogging) @ \( session. utf8StartOffset) , \
116
+ but got \( snapshot. uri. forLogging) @ \( completionUtf8Offset)
117
+ """
118
+ )
119
+ throw ResponseError . serverCancelled
120
+ }
121
+ // The sessions aren't compatible. Close the existing session and open
122
+ // a new one below.
123
+ session. close ( )
124
+ }
125
+ if mustReuse {
126
+ logger. error ( " triggerFromIncompleteCompletions with no existing completion session " )
127
+ throw ResponseError . serverCancelled
128
+ }
129
+ let session = CodeCompletionSession (
130
+ sourcekitd: sourcekitd,
131
+ snapshot: snapshot,
132
+ utf8Offset: completionUtf8Offset,
133
+ position: completionPosition,
134
+ compileCommand: compileCommand,
135
+ clientSupportsSnippets: clientSupportsSnippets
136
+ )
137
+ completionSessions [ ObjectIdentifier ( sourcekitd) ] = session
138
+ return try await session. open ( filterText: filterText, position: cursorPosition, in: snapshot, options: options)
139
+ }
140
+
141
+ // FIXME: (async) Use valuePropagatingCancellation once we support cancellation
142
+ return try await task. value
143
+ }
144
+
145
+ // MARK: - Implementation
146
+
29
147
private let sourcekitd : any SourceKitD
30
148
private let snapshot : DocumentSnapshot
31
- let utf8StartOffset : Int
149
+ private let utf8StartOffset : Int
32
150
private let position : Position
33
151
private let compileCommand : SwiftCompileCommand ?
34
152
private let clientSupportsSnippets : Bool
@@ -39,10 +157,10 @@ actor CodeCompletionSession {
39
157
case open
40
158
}
41
159
42
- nonisolated var uri : DocumentURI { snapshot. uri }
43
- nonisolated var keys : sourcekitd_keys { return sourcekitd. keys }
160
+ private nonisolated var uri : DocumentURI { snapshot. uri }
161
+ private nonisolated var keys : sourcekitd_keys { return sourcekitd. keys }
44
162
45
- init (
163
+ private init (
46
164
sourcekitd: any SourceKitD ,
47
165
snapshot: DocumentSnapshot ,
48
166
utf8Offset: Int ,
@@ -58,30 +176,6 @@ actor CodeCompletionSession {
58
176
self . clientSupportsSnippets = clientSupportsSnippets
59
177
}
60
178
61
- /// Retrieve completions for the given `filterText`, opening or updating the session.
62
- ///
63
- /// - parameters:
64
- /// - filterText: The text to use for fuzzy matching the results.
65
- /// - position: The position at the end of the existing text (typically right after the end of
66
- /// `filterText`), which determines the end of the `TextEdit` replacement range
67
- /// in the resulting completions.
68
- /// - snapshot: The current snapshot that the `TextEdit` replacement in results will be in.
69
- /// - options: The completion options, such as the maximum number of results.
70
- func update(
71
- filterText: String ,
72
- position: Position ,
73
- in snapshot: DocumentSnapshot ,
74
- options: SKCompletionOptions
75
- ) async throws -> CompletionList {
76
- switch self . state {
77
- case . closed:
78
- self . state = . open
79
- return try await self . open ( filterText: filterText, position: position, in: snapshot, options: options)
80
- case . open:
81
- return try await self . updateImpl ( filterText: filterText, position: position, in: snapshot, options: options)
82
- }
83
- }
84
-
85
179
private func open(
86
180
filterText: String ,
87
181
position: Position ,
@@ -105,6 +199,7 @@ actor CodeCompletionSession {
105
199
}
106
200
107
201
let dict = try await sourcekitd. send ( req)
202
+ self . state = . open
108
203
109
204
guard let completions: SKDResponseArray = dict [ keys. results] else {
110
205
return CompletionList ( isIncomplete: false , items: [ ] )
@@ -121,7 +216,7 @@ actor CodeCompletionSession {
121
216
)
122
217
}
123
218
124
- private func updateImpl (
219
+ private func update (
125
220
filterText: String ,
126
221
position: Position ,
127
222
in snapshot: DocumentSnapshot ,
@@ -170,22 +265,18 @@ actor CodeCompletionSession {
170
265
return dict
171
266
}
172
267
173
- private func sendClose( ) {
174
- let req = SKDRequestDictionary ( sourcekitd: sourcekitd)
175
- req [ keys. request] = sourcekitd. requests. codecomplete_close
176
- req [ keys. offset] = self . utf8StartOffset
177
- req [ keys. name] = self . snapshot. uri. pseudoPath
178
- logger. info ( " Closing code completion session: \( self , privacy: . private) " )
179
- _ = try ? sourcekitd. sendSync ( req)
180
- }
181
-
182
- func close( ) async {
268
+ private func close( ) {
183
269
switch self . state {
184
270
case . closed:
185
271
// Already closed, nothing to do.
186
272
break
187
273
case . open:
188
- self . sendClose ( )
274
+ let req = SKDRequestDictionary ( sourcekitd: sourcekitd)
275
+ req [ keys. request] = sourcekitd. requests. codecomplete_close
276
+ req [ keys. offset] = self . utf8StartOffset
277
+ req [ keys. name] = self . snapshot. uri. pseudoPath
278
+ logger. info ( " Closing code completion session: \( self , privacy: . private) " )
279
+ _ = try ? sourcekitd. sendSync ( req)
189
280
self . state = . closed
190
281
}
191
282
}
0 commit comments