@@ -154,14 +154,30 @@ export class CompletionHandler {
154
154
_cancellationToken : vscode . CancellationToken
155
155
) {
156
156
// TODO: Snippet support
157
-
157
+ const razorDocumentUri = vscode . Uri . parse (
158
+ delegatedCompletionItemResolveParams . identifier . textDocumentIdentifier . uri ,
159
+ true
160
+ ) ;
161
+ const razorDocument = await this . documentManager . getDocument ( razorDocumentUri ) ;
162
+ const virtualCsharpDocument = razorDocument . csharpDocument as CSharpProjectedDocument ;
163
+ const provisionalDotPosition = virtualCsharpDocument . getProvisionalDotPosition ( ) ;
158
164
try {
159
165
if (
160
166
delegatedCompletionItemResolveParams . originatingKind != LanguageKind . CSharp ||
161
167
delegatedCompletionItemResolveParams . completionItem . data . TextDocument == null
162
168
) {
163
169
return delegatedCompletionItemResolveParams . completionItem ;
164
170
} else {
171
+ // will add a provisional dot to the C# document if a C# provisional completion triggered
172
+ // this resolve completion request
173
+ if ( virtualCsharpDocument . ensureResolveProvisionalDot ( ) ) {
174
+ if ( provisionalDotPosition !== undefined ) {
175
+ await this . ensureProvisionalDotUpdatedInCSharpDocument (
176
+ virtualCsharpDocument . uri ,
177
+ provisionalDotPosition
178
+ ) ;
179
+ }
180
+ }
165
181
const newItem = await vscode . commands . executeCommand < CompletionItem > (
166
182
resolveCompletionsCommand ,
167
183
delegatedCompletionItemResolveParams . completionItem
@@ -175,6 +191,18 @@ export class CompletionHandler {
175
191
}
176
192
} catch ( error ) {
177
193
this . logger . logWarning ( `${ CompletionHandler . completionResolveEndpoint } failed with ${ error } ` ) ;
194
+ } finally {
195
+ // remove the provisional dot after the resolve has completed and if it was added
196
+ if ( virtualCsharpDocument . removeResolveProvisionalDot ( ) ) {
197
+ const removeDot = true ;
198
+ if ( provisionalDotPosition !== undefined ) {
199
+ await this . ensureProvisionalDotUpdatedInCSharpDocument (
200
+ virtualCsharpDocument . uri ,
201
+ provisionalDotPosition ,
202
+ removeDot
203
+ ) ;
204
+ }
205
+ }
178
206
}
179
207
180
208
return CompletionHandler . emptyCompletionItem ;
@@ -187,13 +215,28 @@ export class CompletionHandler {
187
215
projectedPosition : Position ,
188
216
provisionalTextEdit ?: SerializableTextEdit
189
217
) {
218
+ // Convert projected position to absolute index for provisional dot
219
+ const absoluteIndex = CompletionHandler . getIndexOfPosition ( virtualDocument , projectedPosition ) ;
190
220
try {
221
+ // currently, we are temporarily adding a '.' to the C# document to ensure correct completions are provided
222
+ // for each roslyn.resolveCompletion request and we remember the location from the last provisional completion request.
223
+ // Therefore we need to remove the resolve provisional dot position
224
+ // at the start of every completion request in case a '.' gets added when it shouldn't be.
225
+ virtualDocument . clearResolveCompletionRequestVariables ( ) ;
191
226
if ( provisionalTextEdit ) {
192
227
// provisional C# completion
193
- return this . provideCSharpProvisionalCompletions ( triggerCharacter , virtualDocument , projectedPosition ) ;
228
+ // add '.' to projected C# document to ensure correct completions are provided
229
+ // This is because when a user types '.' after an object, it is initially in
230
+ // html document and not generated C# document.
231
+ if ( absoluteIndex === - 1 ) {
232
+ return CompletionHandler . emptyCompletionList ;
233
+ }
234
+ virtualDocument . addProvisionalDotAt ( absoluteIndex ) ;
235
+ // projected Position is passed in to the virtual doc so that it can be used during the resolve request
236
+ virtualDocument . setProvisionalDotPosition ( projectedPosition ) ;
237
+ await this . ensureProvisionalDotUpdatedInCSharpDocument ( virtualDocument . uri , projectedPosition ) ;
194
238
}
195
239
196
- // non-provisional C# completion
197
240
const virtualDocumentUri = UriConverter . serialize ( virtualDocument . uri ) ;
198
241
const params : CompletionParams = {
199
242
context : {
@@ -217,55 +260,57 @@ export class CompletionHandler {
217
260
return csharpCompletions ;
218
261
} catch ( error ) {
219
262
this . logger . logWarning ( `${ CompletionHandler . completionEndpoint } failed with ${ error } ` ) ;
263
+ } finally {
264
+ if ( provisionalTextEdit && virtualDocument . removeProvisionalDot ( ) ) {
265
+ const removeDot = true ;
266
+ await this . ensureProvisionalDotUpdatedInCSharpDocument (
267
+ virtualDocument . uri ,
268
+ projectedPosition ,
269
+ removeDot
270
+ ) ;
271
+ }
220
272
}
221
273
222
274
return CompletionHandler . emptyCompletionList ;
223
275
}
224
276
225
- // Provides 'provisional' C# completions.
226
- // This happens when a user types '.' after an object. In that case '.' is initially in
227
- // html document and not generated C# document. To get correct completions as soon as the user
228
- // types '.' we need to
229
- // 1. Temporarily add '.' to projected C# document at the correct position (projected position)
230
- // 2. Make sure projected document is updated on the Roslyn server so Roslyn provides correct completions
231
- // 3. Invoke Roslyn/C# completion and return that to the Razor LSP server.
232
- // NOTE: currently there is an issue (see comments in code below) causing us to invoke vscode command
233
- // rather then the Roslyn command
234
- // 4. Remove temporarily (provisionally) added '.' from the projected C# buffer.
235
- // 5. Make sure the projected C# document is updated since the user will likely continue interacting with this document.
236
- private async provideCSharpProvisionalCompletions (
237
- triggerCharacter : string | undefined ,
238
- virtualDocument : CSharpProjectedDocument ,
239
- projectedPosition : Position
277
+ private async ensureProvisionalDotUpdatedInCSharpDocument (
278
+ virtualDocumentUri : vscode . Uri ,
279
+ projectedPosition : Position ,
280
+ removeDot = false // if true then we ensure the provisional dot is removed instead of being added
240
281
) {
241
- const absoluteIndex = CompletionHandler . getIndexOfPosition ( virtualDocument , projectedPosition ) ;
242
- if ( absoluteIndex === - 1 ) {
243
- return CompletionHandler . emptyCompletionList ;
244
- }
245
-
246
- try {
247
- // temporarily add '.' to projected C# document to ensure correct completions are provided
248
- virtualDocument . addProvisionalDotAt ( absoluteIndex ) ;
249
- await this . ensureProjectedCSharpDocumentUpdated ( virtualDocument . uri ) ;
250
-
251
- // Current code has to execute vscode command vscode.executeCompletionItemProvider for provisional completion
252
- // Calling roslyn command vscode.executeCompletionItemProvider returns null
253
- // Tracked by https://github.com/dotnet/vscode-csharp/issues/7250
254
- return this . provideVscodeCompletions ( virtualDocument . uri , projectedPosition , triggerCharacter ) ;
255
- } finally {
256
- if ( virtualDocument . removeProvisionalDot ( ) ) {
257
- await this . ensureProjectedCSharpDocumentUpdated ( virtualDocument . uri ) ;
258
- }
259
- }
260
- }
261
-
262
- private async ensureProjectedCSharpDocumentUpdated ( virtualDocumentUri : vscode . Uri ) {
282
+ // notifies the C# document content provider that the document content has changed
263
283
this . projectedCSharpProvider . ensureDocumentContent ( virtualDocumentUri ) ;
284
+ await this . waitForDocumentChange ( virtualDocumentUri , projectedPosition , removeDot ) ;
285
+ }
264
286
265
- // We open and then re-save because we're adding content to the text document within an event.
266
- // We need to allow the system to propogate this text document change.
267
- const newDocument = await vscode . workspace . openTextDocument ( virtualDocumentUri ) ;
268
- await newDocument . save ( ) ;
287
+ // make sure the provisional dot is added or deleted in the virtual document for provisional completion
288
+ private async waitForDocumentChange (
289
+ uri : vscode . Uri ,
290
+ projectedPosition : Position ,
291
+ removeDot : boolean
292
+ ) : Promise < void > {
293
+ return new Promise ( ( resolve ) => {
294
+ const disposable = vscode . workspace . onDidChangeTextDocument ( ( event ) => {
295
+ const matchingText = removeDot ? '' : '.' ;
296
+ if ( event . document . uri . toString ( ) === uri . toString ( ) ) {
297
+ // Check if the change is at the expected index
298
+ const changeAtIndex = event . contentChanges . some (
299
+ ( change ) =>
300
+ change . range . start . character <= projectedPosition . character &&
301
+ change . range . start . line === projectedPosition . line &&
302
+ change . range . end . character + 1 >= projectedPosition . character &&
303
+ change . range . end . line === projectedPosition . line &&
304
+ change . text === matchingText
305
+ ) ;
306
+ if ( changeAtIndex ) {
307
+ // Resolve the promise and dispose of the event listener
308
+ resolve ( ) ;
309
+ disposable . dispose ( ) ;
310
+ }
311
+ }
312
+ } ) ;
313
+ } ) ;
269
314
}
270
315
271
316
// Adjust Roslyn completion command results to make it more palatable to VSCode
@@ -326,9 +371,7 @@ export class CompletionHandler {
326
371
}
327
372
328
373
// Provide completions using standard vscode executeCompletionItemProvider command
329
- // Used in HTML context and (temporarily) C# provisional completion context (calling Roslyn
330
- // directly during provisional completion session returns null, root cause TBD, tracked by
331
- // https://github.com/dotnet/vscode-csharp/issues/7250)
374
+ // Used in HTML context
332
375
private async provideVscodeCompletions (
333
376
virtualDocumentUri : vscode . Uri ,
334
377
projectedPosition : Position ,
0 commit comments