Skip to content

Commit 5fc67bb

Browse files
author
Andrew Hall (METAL)
committed
Merged PR 474124: Fix complex edit completion in Razor
In Razor, we use the vscode command `vscode.executeCompletionItemProvider` to retrieve completions. This works for items that don't need another call for resolution. Unfortunately, Roslyn uses resolution to lazily compute text edits instead of providing them for the completion list. This presents two problems: 1. The data field is where Roslyn puts it's `ResultId` in order to retrieve information on the resolution call. This doesn't exist in the vscode CompletionItem type and was being removed on calling the command. 2. resolveCompletionItem was never being called. This hooks everything up to call Roslyn for both `provideCompletionItems` and `resolveCompletionItem` when applicable. Related work items: #1824087
1 parent b9ee917 commit 5fc67bb

File tree

5 files changed

+158
-34
lines changed

5 files changed

+158
-34
lines changed

src/lsptoolshost/commands.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,10 @@ async function restartServer(languageServer: RoslynLanguageServer): Promise<void
5151
/**
5252
* Callback after a completion item with complex edit is committed. The change needs to be made outside completion resolve
5353
* handling
54+
*
55+
* IMPORTANT: @see RazorCompletionItemProvider.resolveCompletionItem matches the arguments for this commands
56+
* so it can remap correctly in razor files. Any updates to this function signature requires updates there as well.
57+
*
5458
* @param uriStr The uri containing the location of the document where the completion item was committed in.
5559
* @param textEdits The additional complex edit for the committed completion item.
5660
* @param isSnippetString Indicates if the TextEdit contains a snippet string.

src/lsptoolshost/roslynLanguageServer.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,10 @@ import {
3333
CodeActionParams,
3434
CodeActionRequest,
3535
CodeActionResolveRequest,
36+
CompletionParams,
37+
CompletionRequest,
38+
CompletionResolveRequest,
39+
CompletionItem,
3640
} from 'vscode-languageclient/node';
3741
import { PlatformInformation } from '../shared/platform';
3842
import { acquireDotNetProcessDependencies } from './dotnetRuntime';
@@ -67,6 +71,8 @@ export class RoslynLanguageServer {
6771
public static readonly roslynPullDiagnosticCommand: string = 'roslyn.pullDiagnosticRazorCSharp';
6872
public static readonly provideCodeActionsCommand: string = 'roslyn.provideCodeActions';
6973
public static readonly resolveCodeActionCommand: string = 'roslyn.resolveCodeAction';
74+
public static readonly provideCompletionsCommand: string = 'roslyn.provideCompletions';
75+
public static readonly resolveCompletionsCommand: string = 'roslyn.resolveCompletion';
7076
public static readonly razorInitializeCommand: string = 'razor.initialize';
7177

7278
// These are notifications we will get from the LSP server and will forward to the Razor extension.
@@ -476,6 +482,13 @@ export class RoslynLanguageServer {
476482
return await this.sendRequest(CodeActionResolveRequest.type, request, CancellationToken.None);
477483
});
478484

485+
vscode.commands.registerCommand(RoslynLanguageServer.provideCompletionsCommand, async (request: CompletionParams) => {
486+
return await this.sendRequest(CompletionRequest.type, request, CancellationToken.None);
487+
});
488+
vscode.commands.registerCommand(RoslynLanguageServer.resolveCompletionsCommand, async (request: CompletionItem) => {
489+
return await this.sendRequest(CompletionResolveRequest.type, request, CancellationToken.None);
490+
});
491+
479492
// Roslyn is responsible for producing a json file containing information for Razor, that comes from the compilation for
480493
// a project. We want to defer this work until necessary, so this command is called by the Razor document manager to tell
481494
// us when they need us to initialize the Razor things.

src/razor/src/Completion/ProvisionalCompletionOrchestrator.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,8 @@ export class ProvisionalCompletionOrchestrator {
129129
projectedDocument.uri,
130130
htmlPosition,
131131
provisionalPosition,
132-
completionContext.triggerCharacter);
132+
completionContext,
133+
projection.languageKind);
133134

134135
// We track when we add provisional dots to avoid doing unnecessary work on commonly invoked events.
135136
this.provisionalDotsMayBeActive = true;

src/razor/src/Completion/RazorCompletionItemProvider.ts

Lines changed: 100 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -11,27 +11,60 @@ import { RazorLanguageServiceClient } from '../RazorLanguageServiceClient';
1111
import { RazorLogger } from '../RazorLogger';
1212
import { getUriPath } from '../UriPaths';
1313
import { ProvisionalCompletionOrchestrator } from './ProvisionalCompletionOrchestrator';
14+
import { LanguageKind } from '../RPC/LanguageKind';
15+
import { RoslynLanguageServer } from '../../../lsptoolshost/roslynLanguageServer';
16+
import { CompletionItem, CompletionParams, CompletionTriggerKind } from 'vscode-languageclient';
17+
import { UriConverter } from '../../../lsptoolshost/uriConverter';
18+
import * as RazorConventions from '../RazorConventions';
19+
import { MappingHelpers } from '../Mapping/MappingHelpers';
1420

1521
export class RazorCompletionItemProvider
1622
extends RazorLanguageFeatureBase
1723
implements vscode.CompletionItemProvider {
1824

1925
public static async getCompletions(
2026
projectedUri: vscode.Uri, hostDocumentPosition: vscode.Position,
21-
projectedPosition: vscode.Position, triggerCharacter: string | undefined) {
27+
projectedPosition: vscode.Position, context: vscode.CompletionContext,
28+
language: LanguageKind) {
2229

2330
if (projectedUri) {
2431
// "@" is not a valid trigger character for C# / HTML and therefore we need to translate
2532
// it into a non-trigger invocation.
26-
const modifiedTriggerCharacter = triggerCharacter === '@' ? undefined : triggerCharacter;
27-
28-
const completions = await vscode
29-
.commands
30-
.executeCommand<vscode.CompletionList | vscode.CompletionItem[]>(
31-
'vscode.executeCompletionItemProvider',
32-
projectedUri,
33-
projectedPosition,
34-
modifiedTriggerCharacter);
33+
const modifiedTriggerCharacter = context.triggerCharacter === '@' ? undefined : context.triggerCharacter;
34+
35+
let completions: vscode.CompletionList | vscode.CompletionItem[];
36+
37+
// For CSharp, completions need to keep the "data" field
38+
// on the completion item for lazily resolving the edits in
39+
// the resolveCompletionItem step. Using the vs code command
40+
// drops that field because it doesn't exist in the declared vs code
41+
// CompletionItem type.
42+
if (language === LanguageKind.CSharp) {
43+
const params: CompletionParams = {
44+
context: {
45+
triggerKind: getTriggerKind(context.triggerKind),
46+
triggerCharacter: modifiedTriggerCharacter
47+
},
48+
textDocument: {
49+
uri: UriConverter.serialize(projectedUri),
50+
},
51+
position: projectedPosition
52+
};
53+
54+
completions = await vscode
55+
.commands
56+
.executeCommand<vscode.CompletionList | vscode.CompletionItem[]>(
57+
RoslynLanguageServer.provideCompletionsCommand,
58+
params);
59+
} else {
60+
completions = await vscode
61+
.commands
62+
.executeCommand<vscode.CompletionList | vscode.CompletionItem[]>(
63+
'vscode.executeCompletionItemProvider',
64+
projectedUri,
65+
projectedPosition,
66+
modifiedTriggerCharacter);
67+
}
3568

3669
const completionItems =
3770
completions instanceof Array ? completions // was vscode.CompletionItem[]
@@ -68,7 +101,7 @@ export class RazorCompletionItemProvider
68101
(range as any).replacing = new vscode.Range(replacingRangeStart, replacingRangeEnd);
69102
}
70103

71-
if (range instanceof vscode.Range && range.start && range.end) {
104+
if (range instanceof vscode.Range && range.start && range.end) {
72105
const rangeStart = this.offsetColumn(completionCharacterOffset, hostDocumentPosition.line, range.start);
73106
const rangeEnd = this.offsetColumn(completionCharacterOffset, hostDocumentPosition.line, range.end);
74107
completionItem.range = new vscode.Range(rangeStart, rangeEnd);
@@ -78,7 +111,7 @@ export class RazorCompletionItemProvider
78111
// textEdit is deprecated in favor of .range. Clear out its value to avoid any unexpected behavior.
79112
completionItem.textEdit = undefined;
80113

81-
if (triggerCharacter === '@' &&
114+
if (context.triggerCharacter === '@' &&
82115
completionItem.commitCharacters) {
83116
// We remove `{`, '(', and '*' from the commit characters to prevent auto-completing the first
84117
// completion item with a curly brace when a user intended to type `@{}` or `@()`.
@@ -138,7 +171,61 @@ export class RazorCompletionItemProvider
138171
projection.uri,
139172
position,
140173
projection.position,
141-
context.triggerCharacter);
174+
context,
175+
projection.languageKind);
176+
142177
return completionList;
143178
}
179+
180+
public async resolveCompletionItem(item: vscode.CompletionItem, token: vscode.CancellationToken): Promise<vscode.CompletionItem> {
181+
// We assume that only the RoslynLanguageServer provides data, which
182+
// if it does we use LSP calls directly to Roslyn since there's no
183+
// equivalent vscode command to generically do that.
184+
if ((<CompletionItem>item).data) {
185+
let newItem = await vscode
186+
.commands
187+
.executeCommand<vscode.CompletionItem>(
188+
RoslynLanguageServer.resolveCompletionsCommand,
189+
item);
190+
191+
if (!newItem) {
192+
return item;
193+
}
194+
195+
item = newItem;
196+
197+
if (item.command && item.command.arguments?.length === 4) {
198+
let uri = vscode.Uri.parse(item.command.arguments[0]);
199+
200+
if (uri && RazorConventions.isRazorCSharpFile(uri)) {
201+
let razorUri = RazorConventions.getRazorDocumentUri(uri);
202+
let textEdit = item.command.arguments[1] as vscode.TextEdit;
203+
204+
let remappedEdit = await MappingHelpers.remapGeneratedFileTextEdit(razorUri, textEdit, this.serviceClient, this.logger, token);
205+
206+
if (remappedEdit) {
207+
item.command.arguments[0] = razorUri;
208+
item.command.arguments[1] = remappedEdit;
209+
}
210+
}
211+
}
212+
}
213+
214+
return item;
215+
}
216+
}
217+
218+
function getTriggerKind(triggerKind: vscode.CompletionTriggerKind): CompletionTriggerKind {
219+
switch (triggerKind) {
220+
case vscode.CompletionTriggerKind.Invoke:
221+
return CompletionTriggerKind.Invoked;
222+
case vscode.CompletionTriggerKind.TriggerCharacter:
223+
return CompletionTriggerKind.TriggerCharacter;
224+
case vscode.CompletionTriggerKind.TriggerForIncompleteCompletions:
225+
return CompletionTriggerKind.TriggerForIncompleteCompletions;
226+
default:
227+
throw new Error(`Unexpected completion trigger kind: ${triggerKind}`);
228+
229+
}
144230
}
231+

src/razor/src/Mapping/MappingHelpers.ts

Lines changed: 39 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -35,36 +35,55 @@ export class MappingHelpers {
3535

3636
// Re-map each edit to its position in the original Razor document.
3737
for (const edit of edits) {
38-
const remappedResponse = await serviceClient.mapToDocumentRanges(
39-
LanguageKind.CSharp,
40-
[edit.range],
41-
documentUri);
42-
43-
if (!remappedResponse ||
44-
!remappedResponse.ranges ||
45-
!remappedResponse.ranges[0]) {
46-
// This is kind of wrong. Workspace edits commonly consist of a bunch of different edits which
47-
// don't make sense individually. If we try to introspect on them individually there won't be
48-
// enough context to do anything intelligent. But we also need to know if the edit can just
49-
// be handled by mapToDocumentRange (something went wrong here), so we ignore the edit.
50-
logger.logWarning(`Unable to remap file ${uri.path} at ${edit.range}.`);
51-
continue;
52-
} else {
53-
const remappedEdit = new vscode.TextEdit(remappedResponse.ranges[0], edit.newText);
54-
55-
logger.logVerbose(
56-
`Re-mapping text ${edit.newText} at ${edit.range} in ${uri.path} to ${remappedResponse.ranges[0]} in ${documentUri.path}`);
38+
let remappedEdit = await MappingHelpers.remapGeneratedFileTextEdit(documentUri, edit, serviceClient, logger, token);
5739

40+
if (remappedEdit) {
5841
this.addElementToDictionary(map, documentUri, remappedEdit);
5942
}
60-
6143
}
6244
}
6345

6446
const result = this.mapToTextEdit(map);
6547
return result;
6648
}
6749

50+
/**
51+
* Try to map a textedit from a generated document onto the razor file
52+
* @param uri The document uri, expected to be the generated document
53+
* @param textEdit The edit to map from a generated document
54+
* @returns A mapped edit, or undefined if the edit couldn't be mapped (or is in a razor file)
55+
*/
56+
public static async remapGeneratedFileTextEdit(
57+
uri: vscode.Uri,
58+
textEdit: vscode.TextEdit,
59+
serviceClient: RazorLanguageServiceClient,
60+
logger: RazorLogger,
61+
token: vscode.CancellationToken): Promise<vscode.TextEdit | undefined> {
62+
63+
const remappedResponse = await serviceClient.mapToDocumentRanges(
64+
LanguageKind.CSharp,
65+
[textEdit.range],
66+
uri);
67+
68+
if (!remappedResponse ||
69+
!remappedResponse.ranges ||
70+
!remappedResponse.ranges[0]) {
71+
// This is kind of wrong. Workspace edits commonly consist of a bunch of different edits which
72+
// don't make sense individually. If we try to introspect on them individually there won't be
73+
// enough context to do anything intelligent. But we also need to know if the edit can just
74+
// be handled by mapToDocumentRange (something went wrong here), so we ignore the edit.
75+
logger.logWarning(`Unable to remap file ${uri.path} at ${textEdit.range}.`);
76+
return undefined;
77+
} else {
78+
const remappedEdit = new vscode.TextEdit(remappedResponse.ranges[0], textEdit.newText);
79+
80+
logger.logVerbose(
81+
`Re-mapping text ${textEdit.newText} at ${textEdit.range} in ${uri.path} to ${remappedResponse.ranges[0]} in ${uri.path}`);
82+
83+
return remappedEdit;
84+
}
85+
}
86+
6887
public static async remapGeneratedFileLocation(
6988
location: vscode.Location,
7089
serviceClient: RazorLanguageServiceClient,

0 commit comments

Comments
 (0)