Skip to content

Commit a8a9c24

Browse files
author
Jordi Ramos
authored
Include tooltip to Razor provisional completion (#7440)
* provide provisional completion for razor * temporarily adding provision dot for both completion and resolve completion requests * used vscode.workspace.onDidChangeTextDocument to ensure virtual doc changes
1 parent f62272a commit a8a9c24

File tree

2 files changed

+147
-48
lines changed

2 files changed

+147
-48
lines changed

src/razor/src/completion/completionHandler.ts

Lines changed: 90 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -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,

src/razor/src/csharp/csharpProjectedDocument.ts

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,18 @@
66
import { IProjectedDocument } from '../projection/IProjectedDocument';
77
import { ServerTextChange } from '../rpc/serverTextChange';
88
import { getUriPath } from '../uriPaths';
9+
import { Position } from 'vscode-languageclient';
910
import * as vscode from '../vscodeAdapter';
1011

1112
export class CSharpProjectedDocument implements IProjectedDocument {
1213
public readonly path: string;
1314

1415
private content = '';
1516
private preProvisionalContent: string | undefined;
17+
private preResolveProvisionalContent: string | undefined;
1618
private provisionalEditAt: number | undefined;
19+
private resolveProvisionalEditAt: number | undefined;
20+
private ProvisionalDotPosition: Position | undefined;
1721
private hostDocumentVersion: number | null = null;
1822
private projectedDocumentVersion = 0;
1923

@@ -67,8 +71,10 @@ export class CSharpProjectedDocument implements IProjectedDocument {
6771
// Edits already applied.
6872
return;
6973
}
70-
74+
//reset the state for provisional completion and resolve completion
7175
this.removeProvisionalDot();
76+
this.resolveProvisionalEditAt = undefined;
77+
this.ProvisionalDotPosition = undefined;
7278

7379
const newContent = this.getEditedContent('.', index, index, this.content);
7480
this.preProvisionalContent = this.content;
@@ -80,6 +86,7 @@ export class CSharpProjectedDocument implements IProjectedDocument {
8086
if (this.provisionalEditAt && this.preProvisionalContent) {
8187
// Undo provisional edit if one was applied.
8288
this.setContent(this.preProvisionalContent);
89+
this.resolveProvisionalEditAt = this.provisionalEditAt;
8390
this.provisionalEditAt = undefined;
8491
this.preProvisionalContent = undefined;
8592
return true;
@@ -88,6 +95,55 @@ export class CSharpProjectedDocument implements IProjectedDocument {
8895
return false;
8996
}
9097

98+
// add resolve provisional dot if a provisional completion request was made
99+
// A resolve provisional dot is the same as a provisional dot, but it remembers the
100+
// last provisional dot inserted location and is used for the roslyn.resolveCompletion API
101+
public ensureResolveProvisionalDot() {
102+
//remove the last resolve provisional dot it it exists
103+
this.removeResolveProvisionalDot();
104+
105+
if (this.resolveProvisionalEditAt) {
106+
const newContent = this.getEditedContent(
107+
'.',
108+
this.resolveProvisionalEditAt,
109+
this.resolveProvisionalEditAt,
110+
this.content
111+
);
112+
this.preResolveProvisionalContent = this.content;
113+
this.setContent(newContent);
114+
return true;
115+
}
116+
return false;
117+
}
118+
119+
public removeResolveProvisionalDot() {
120+
if (this.resolveProvisionalEditAt && this.preResolveProvisionalContent) {
121+
// Undo provisional edit if one was applied.
122+
this.setContent(this.preResolveProvisionalContent);
123+
this.provisionalEditAt = undefined;
124+
this.preResolveProvisionalContent = undefined;
125+
return true;
126+
}
127+
128+
return false;
129+
}
130+
131+
public setProvisionalDotPosition(position: Position) {
132+
this.ProvisionalDotPosition = position;
133+
}
134+
135+
public getProvisionalDotPosition() {
136+
return this.ProvisionalDotPosition;
137+
}
138+
139+
// since multiple roslyn.resolveCompletion requests can be made for each completion,
140+
// we need to clear the resolveProvisionalEditIndex (currently when a new completion request is made,
141+
// this works if resolve requests are always preceded by a completion request)
142+
public clearResolveCompletionRequestVariables() {
143+
this.resolveProvisionalEditAt = undefined;
144+
this.ProvisionalDotPosition = undefined;
145+
}
146+
91147
private getEditedContent(newText: string, start: number, end: number, content: string) {
92148
const before = content.substr(0, start);
93149
const after = content.substr(end);

0 commit comments

Comments
 (0)