Skip to content

Commit 3e2dd31

Browse files
committed
Fixing provisional completion and minor cleanup
- We currently have to invoke provisional completion in Roslyn via vscode command vs Roslyn command - Minor cleanup of function names, TODOs - Fixing item kind translation from vscode to LSP (it's off by one)
1 parent afdf9a6 commit 3e2dd31

File tree

1 file changed

+105
-93
lines changed

1 file changed

+105
-93
lines changed

src/razor/src/completion/completionHandler.ts

Lines changed: 105 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,9 @@ export class CompletionHandler {
4141
CompletionItem,
4242
any
4343
> = new RequestType(CompletionHandler.completionResolveEndpoint);
44-
// TODO: do we always need empty result members defined? Can we declare type on handler function and return null?
45-
// Also, none of the empty repsonses are declared as static readonly in other handles - should they be?
46-
private static readonly emptyCompletionList: CompletionList = <CompletionList>{};
44+
private static readonly emptyCompletionList: CompletionList = <CompletionList>{
45+
items: new Array(0),
46+
};
4747
private static readonly emptyCompletionItem: CompletionItem = <CompletionItem>{};
4848

4949
constructor(
@@ -94,11 +94,14 @@ export class CompletionHandler {
9494
} else if (delegatedCompletionParams.projectedKind === LanguageKind.CSharp) {
9595
virtualDocument = razorDocument.csharpDocument;
9696
} else {
97-
// TODO: equivalent of Debug.Fail?
97+
this.logger.logWarning(`Unknown language kind value ${delegatedCompletionParams.projectedKind}`);
9898
return CompletionHandler.emptyCompletionList;
9999
}
100100

101-
// TODO: Should we check for null or undefined virtual document like we do in C# code?
101+
if (!virtualDocument) {
102+
this.logger.logWarning(`Null or undefined virtual document: ${virtualDocument}`);
103+
return CompletionHandler.emptyCompletionList;
104+
}
102105

103106
const synchronized = await this.documentSynchronizer.trySynchronizeProjectedDocument(
104107
textDocument,
@@ -114,8 +117,6 @@ export class CompletionHandler {
114117

115118
delegatedCompletionParams.identifier.textDocumentIdentifier.uri = virtualDocumentUri;
116119

117-
// TODO: support for provisional edits
118-
119120
// "@" is not a valid trigger character for C# / HTML and therefore we need to translate
120121
// it into a non-trigger invocation.
121122
const modifiedTriggerCharacter =
@@ -139,51 +140,12 @@ export class CompletionHandler {
139140
);
140141
}
141142

142-
// HTML completion
143-
const completions = await vscode.commands.executeCommand<vscode.CompletionList | vscode.CompletionItem[]>(
144-
'vscode.executeCompletionItemProvider',
145-
UriConverter.deserialize(virtualDocumentUri),
143+
// HTML completion - provided via vscode command
144+
return this.provideVscodeCompletions(
145+
virtualDocument.uri,
146146
delegatedCompletionParams.projectedPosition,
147147
modifiedTriggerCharacter
148148
);
149-
150-
const completionItems =
151-
completions instanceof Array
152-
? completions // was vscode.CompletionItem[]
153-
: completions
154-
? completions.items // was vscode.CompletionList
155-
: [];
156-
157-
const convertedCompletionItems: CompletionItem[] = new Array(completionItems.length);
158-
for (let i = 0; i < completionItems.length; i++) {
159-
const completionItem = completionItems[i];
160-
const convertedCompletionItem = <CompletionItem>{
161-
command: completionItem.command, // no conversion needed as fields match
162-
commitCharacters: completionItem.commitCharacters,
163-
detail: completionItem.detail,
164-
documentation: CompletionHandler.ToMarkupContent(completionItem.documentation),
165-
filterText: completionItem.filterText,
166-
insertText: CompletionHandler.ToLspInsertText(completionItem.insertText),
167-
insertTextMode: CompletionHandler.ToInsertTextMode(completionItem.keepWhitespace),
168-
kind: completionItem.kind,
169-
label: CompletionHandler.ToLspCompletionItemLabel(completionItem.label),
170-
preselect: completionItem.preselect,
171-
sortText: completionItem.sortText,
172-
textEdit: CompletionHandler.ToLspTextEdit(
173-
CompletionHandler.ToLspInsertText(completionItem.insertText),
174-
completionItem.range
175-
),
176-
};
177-
convertedCompletionItems[i] = convertedCompletionItem;
178-
}
179-
180-
const isIncomplete = completions instanceof Array ? false : completions ? completions.isIncomplete : false;
181-
const completionList = <CompletionList>{
182-
isIncomplete: isIncomplete,
183-
items: convertedCompletionItems,
184-
};
185-
186-
return completionList;
187149
} catch (error) {
188150
this.logger.logWarning(`${CompletionHandler.completionEndpoint} failed with ${error}`);
189151
}
@@ -215,33 +177,6 @@ export class CompletionHandler {
215177
return CompletionHandler.emptyCompletionItem;
216178
}
217179

218-
private static AdjustCSharpCompletionList(completionList: CompletionList, triggerCharacter: string | undefined) {
219-
const data = completionList.itemDefaults?.data;
220-
for (const completionItem of completionList.items) {
221-
// textEdit is deprecated in favor of .range. Clear out its value to avoid any unexpected behavior.
222-
completionItem.textEdit = undefined;
223-
224-
if (triggerCharacter === '@' && completionItem.commitCharacters) {
225-
// We remove `{`, '(', and '*' from the commit characters to prevent auto-completing the first
226-
// completion item with a curly brace when a user intended to type `@{}` or `@()`.
227-
completionItem.commitCharacters = completionItem.commitCharacters.filter(
228-
(commitChar) => commitChar !== '{' && commitChar !== '(' && commitChar !== '*'
229-
);
230-
}
231-
232-
// for intellicode items, manually set the insertText to avoid including stars in the commit
233-
if (completionItem.label.toString().includes('\u2605')) {
234-
if (completionItem.textEditText) {
235-
completionItem.insertText = completionItem.textEditText;
236-
}
237-
}
238-
239-
if (!completionItem.data) {
240-
completionItem.data = data;
241-
}
242-
}
243-
}
244-
245180
private async provideCSharpCompletions(
246181
triggerKind: CompletionTriggerKind,
247182
triggerCharacter: string | undefined,
@@ -274,10 +209,7 @@ export class CompletionHandler {
274209
const newDocument = await vscode.workspace.openTextDocument(virtualDocument.uri);
275210
await newDocument.save();
276211

277-
params.position = <Position>{
278-
line: projectedPosition.line,
279-
character: projectedPosition.character + 1,
280-
};
212+
return this.provideVscodeCompletions(virtualDocument.uri, projectedPosition, triggerCharacter);
281213
}
282214
const csharpCompletions = await vscode.commands.executeCommand<CompletionList>(
283215
provideCompletionsCommand,
@@ -287,7 +219,36 @@ export class CompletionHandler {
287219
return csharpCompletions;
288220
}
289221

290-
private static getIndexOfPosition(document: IProjectedDocument, position: Position) {
222+
private static AdjustCSharpCompletionList(completionList: CompletionList, triggerCharacter: string | undefined) {
223+
const data = completionList.itemDefaults?.data;
224+
for (const completionItem of completionList.items) {
225+
// textEdit is deprecated in favor of .range. Clear out its value to avoid any unexpected behavior.
226+
completionItem.textEdit = undefined;
227+
228+
if (triggerCharacter === '@' && completionItem.commitCharacters) {
229+
// We remove `{`, '(', and '*' from the commit characters to prevent auto-completing the first
230+
// completion item with a curly brace when a user intended to type `@{}` or `@()`.
231+
completionItem.commitCharacters = completionItem.commitCharacters.filter(
232+
(commitChar) => commitChar !== '{' && commitChar !== '(' && commitChar !== '*'
233+
);
234+
}
235+
236+
// for intellicode items, manually set the insertText to avoid including stars in the commit
237+
if (completionItem.label.toString().includes('\u2605')) {
238+
if (completionItem.textEditText) {
239+
completionItem.insertText = completionItem.textEditText;
240+
}
241+
}
242+
243+
// copy default item data to each item or else completion item resolve will fail later
244+
if (!completionItem.data) {
245+
completionItem.data = data;
246+
}
247+
}
248+
}
249+
250+
// convert (line, character) Position to absolute index number
251+
private static getIndexOfPosition(document: IProjectedDocument, position: Position): number {
291252
const content: string = document.getContent();
292253
let lineNumber = 0;
293254
let index = 0;
@@ -314,8 +275,59 @@ export class CompletionHandler {
314275
return positionIndex;
315276
}
316277

278+
private async provideVscodeCompletions(
279+
virtualDocumentUri: vscode.Uri,
280+
projectedPosition: Position,
281+
triggerCharacter: string | undefined
282+
) {
283+
const completions = await vscode.commands.executeCommand<vscode.CompletionList | vscode.CompletionItem[]>(
284+
'vscode.executeCompletionItemProvider',
285+
virtualDocumentUri,
286+
projectedPosition,
287+
triggerCharacter
288+
);
289+
290+
const completionItems =
291+
completions instanceof Array
292+
? completions // was vscode.CompletionItem[]
293+
: completions
294+
? completions.items // was vscode.CompletionList
295+
: [];
296+
297+
const convertedCompletionItems: CompletionItem[] = new Array(completionItems.length);
298+
for (let i = 0; i < completionItems.length; i++) {
299+
const completionItem = completionItems[i];
300+
const convertedCompletionItem = <CompletionItem>{
301+
command: completionItem.command, // no conversion needed as fields match
302+
commitCharacters: completionItem.commitCharacters,
303+
detail: completionItem.detail,
304+
documentation: CompletionHandler.toMarkupContent(completionItem.documentation),
305+
filterText: completionItem.filterText,
306+
insertText: CompletionHandler.toLspInsertText(completionItem.insertText),
307+
insertTextMode: CompletionHandler.toInsertTextMode(completionItem.keepWhitespace),
308+
kind: completionItem.kind ? completionItem.kind + 1 : completionItem.kind, // VSCode and LSP are off by one
309+
label: CompletionHandler.toLspCompletionItemLabel(completionItem.label),
310+
preselect: completionItem.preselect,
311+
sortText: completionItem.sortText,
312+
textEdit: CompletionHandler.toLspTextEdit(
313+
CompletionHandler.toLspInsertText(completionItem.insertText),
314+
completionItem.range
315+
),
316+
};
317+
convertedCompletionItems[i] = convertedCompletionItem;
318+
}
319+
320+
const isIncomplete = completions instanceof Array ? false : completions ? completions.isIncomplete : false;
321+
const completionList = <CompletionList>{
322+
isIncomplete: isIncomplete,
323+
items: convertedCompletionItems,
324+
};
325+
326+
return completionList;
327+
}
328+
317329
// converts completion item documentation from vscode format to LSP format
318-
private static ToMarkupContent(documentation?: string | vscode.MarkdownString): string | MarkupContent | undefined {
330+
private static toMarkupContent(documentation?: string | vscode.MarkdownString): string | MarkupContent | undefined {
319331
const markdownString = documentation as vscode.MarkdownString;
320332
if (!markdownString?.value) {
321333
return <string | undefined>documentation;
@@ -328,17 +340,17 @@ export class CompletionHandler {
328340
return markupContent;
329341
}
330342

331-
private static ToLspCompletionItemLabel(label: string | vscode.CompletionItemLabel): string {
343+
private static toLspCompletionItemLabel(label: string | vscode.CompletionItemLabel): string {
332344
const completionItemLabel = label as vscode.CompletionItemLabel;
333345
return completionItemLabel?.label ?? <string>label;
334346
}
335347

336-
private static ToLspInsertText(insertText?: string | vscode.SnippetString): string | undefined {
348+
private static toLspInsertText(insertText?: string | vscode.SnippetString): string | undefined {
337349
const snippetString = insertText as vscode.SnippetString;
338350
return snippetString?.value ?? <string | undefined>insertText;
339351
}
340352

341-
private static ToLspTextEdit(
353+
private static toLspTextEdit(
342354
newText?: string,
343355
range?: vscode.Range | { inserting: vscode.Range; replacing: vscode.Range }
344356
): TextEdit | InsertReplaceEdit | undefined {
@@ -360,7 +372,7 @@ export class CompletionHandler {
360372
if (!(insertingRange || replacingRange)) {
361373
const textEdit: TextEdit = {
362374
newText: newText,
363-
range: this.ToLspRange(<vscode.Range>range),
375+
range: this.toLspRange(<vscode.Range>range),
364376
};
365377

366378
return textEdit;
@@ -372,23 +384,23 @@ export class CompletionHandler {
372384
}
373385
const insertReplaceEdit: InsertReplaceEdit = {
374386
newText: newText,
375-
insert: CompletionHandler.ToLspRange(insertingRange),
376-
replace: CompletionHandler.ToLspRange(replacingRange),
387+
insert: CompletionHandler.toLspRange(insertingRange),
388+
replace: CompletionHandler.toLspRange(replacingRange),
377389
};
378390

379391
return insertReplaceEdit;
380392
}
381393

382-
private static ToLspRange(range: vscode.Range): Range {
394+
private static toLspRange(range: vscode.Range): Range {
383395
const lspRange: Range = {
384-
start: CompletionHandler.ToLspPosition(range.start),
385-
end: CompletionHandler.ToLspPosition(range.end),
396+
start: CompletionHandler.toLspPosition(range.start),
397+
end: CompletionHandler.toLspPosition(range.end),
386398
};
387399

388400
return lspRange;
389401
}
390402

391-
private static ToLspPosition(position: vscode.Position): Position {
403+
private static toLspPosition(position: vscode.Position): Position {
392404
const lspPosition: Position = {
393405
line: position.line,
394406
character: position.character,
@@ -397,7 +409,7 @@ export class CompletionHandler {
397409
return lspPosition;
398410
}
399411

400-
private static ToInsertTextMode(keepWhitespace?: boolean): InsertTextMode | undefined {
412+
private static toInsertTextMode(keepWhitespace?: boolean): InsertTextMode | undefined {
401413
if (keepWhitespace === undefined) {
402414
return undefined;
403415
}

0 commit comments

Comments
 (0)