@@ -154,14 +154,30 @@ export class CompletionHandler {
154154 _cancellationToken : vscode . CancellationToken
155155 ) {
156156 // 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 ( ) ;
158164 try {
159165 if (
160166 delegatedCompletionItemResolveParams . originatingKind != LanguageKind . CSharp ||
161167 delegatedCompletionItemResolveParams . completionItem . data . TextDocument == null
162168 ) {
163169 return delegatedCompletionItemResolveParams . completionItem ;
164170 } 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+ }
165181 const newItem = await vscode . commands . executeCommand < CompletionItem > (
166182 resolveCompletionsCommand ,
167183 delegatedCompletionItemResolveParams . completionItem
@@ -175,6 +191,18 @@ export class CompletionHandler {
175191 }
176192 } catch ( error ) {
177193 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+ }
178206 }
179207
180208 return CompletionHandler . emptyCompletionItem ;
@@ -187,13 +215,28 @@ export class CompletionHandler {
187215 projectedPosition : Position ,
188216 provisionalTextEdit ?: SerializableTextEdit
189217 ) {
218+ // Convert projected position to absolute index for provisional dot
219+ const absoluteIndex = CompletionHandler . getIndexOfPosition ( virtualDocument , projectedPosition ) ;
190220 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 ( ) ;
191226 if ( provisionalTextEdit ) {
192227 // 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 ) ;
194238 }
195239
196- // non-provisional C# completion
197240 const virtualDocumentUri = UriConverter . serialize ( virtualDocument . uri ) ;
198241 const params : CompletionParams = {
199242 context : {
@@ -217,55 +260,57 @@ export class CompletionHandler {
217260 return csharpCompletions ;
218261 } catch ( error ) {
219262 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+ }
220272 }
221273
222274 return CompletionHandler . emptyCompletionList ;
223275 }
224276
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
240281 ) {
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
263283 this . projectedCSharpProvider . ensureDocumentContent ( virtualDocumentUri ) ;
284+ await this . waitForDocumentChange ( virtualDocumentUri , projectedPosition , removeDot ) ;
285+ }
264286
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+ } ) ;
269314 }
270315
271316 // Adjust Roslyn completion command results to make it more palatable to VSCode
@@ -326,9 +371,7 @@ export class CompletionHandler {
326371 }
327372
328373 // 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
332375 private async provideVscodeCompletions (
333376 virtualDocumentUri : vscode . Uri ,
334377 projectedPosition : Position ,
0 commit comments