From 8af9096c2bf4fee1ad4e02442dfc9e23f77bcecd Mon Sep 17 00:00:00 2001 From: Fredric Silberberg Date: Sat, 15 Aug 2020 17:25:43 -0700 Subject: [PATCH 1/6] Use the new completion and completion/resolve services being added to the roslyn backend for a better overall completion experience. --- src/features/completionItemProvider.ts | 137 ------------------ src/features/completionProvider.ts | 90 ++++++++++++ src/omnisharp/extension.ts | 6 +- src/omnisharp/protocol.ts | 63 ++++---- src/omnisharp/utils.ts | 12 +- ...=> completionProvider.integration.test.ts} | 6 +- 6 files changed, 142 insertions(+), 172 deletions(-) delete mode 100644 src/features/completionItemProvider.ts create mode 100644 src/features/completionProvider.ts rename test/integrationTests/{completionItemProvider.integration.test.ts => completionProvider.integration.test.ts} (91%) diff --git a/src/features/completionItemProvider.ts b/src/features/completionItemProvider.ts deleted file mode 100644 index bea3722bc..000000000 --- a/src/features/completionItemProvider.ts +++ /dev/null @@ -1,137 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { extractSummaryText } from './documentation'; -import AbstractSupport from './abstractProvider'; -import * as protocol from '../omnisharp/protocol'; -import * as serverUtils from '../omnisharp/utils'; -import { createRequest } from '../omnisharp/typeConversion'; -import { CompletionItemProvider, CompletionItem, CompletionItemKind, CompletionContext, CompletionTriggerKind, CancellationToken, TextDocument, Range, Position, CompletionList } from 'vscode'; - -export default class OmniSharpCompletionItemProvider extends AbstractSupport implements CompletionItemProvider { - - // copied from Roslyn here: https://github.com/dotnet/roslyn/blob/6e8f6d600b6c4bc0b92bc3d782a9e0b07e1c9f8e/src/Features/Core/Portable/Completion/CompletionRules.cs#L166-L169 - private static AllCommitCharacters = [ - ' ', '{', '}', '[', ']', '(', ')', '.', ',', ':', - ';', '+', '-', '*', '/', '%', '&', '|', '^', '!', - '~', '=', '<', '>', '?', '@', '#', '\'', '\"', '\\']; - - private static CommitCharactersWithoutSpace = [ - '{', '}', '[', ']', '(', ')', '.', ',', ':', - ';', '+', '-', '*', '/', '%', '&', '|', '^', '!', - '~', '=', '<', '>', '?', '@', '#', '\'', '\"', '\\']; - - public async provideCompletionItems(document: TextDocument, position: Position, token: CancellationToken, context: CompletionContext): Promise { - - let wordToComplete = ''; - let range = document.getWordRangeAtPosition(position); - if (range) { - wordToComplete = document.getText(new Range(range.start, position)); - } - - let req = createRequest(document, position); - req.WordToComplete = wordToComplete; - req.WantDocumentationForEveryCompletionResult = true; - req.WantKind = true; - req.WantReturnType = true; - if (context.triggerKind == CompletionTriggerKind.TriggerCharacter) { - req.TriggerCharacter = context.triggerCharacter; - } - - try { - let responses = await serverUtils.autoComplete(this._server, req, token); - - if (!responses) { - return; - } - - let result: CompletionItem[] = []; - let completions: { [c: string]: { items: CompletionItem[], preselect: boolean } } = Object.create(null); - - // transform AutoCompleteResponse to CompletionItem and - // group by code snippet - for (let response of responses) { - let completion = new CompletionItem(response.CompletionText); - - completion.detail = response.ReturnType - ? `${response.ReturnType} ${response.DisplayText}` - : response.DisplayText; - - completion.documentation = extractSummaryText(response.Description); - completion.kind = _kinds[response.Kind] || CompletionItemKind.Property; - completion.insertText = response.CompletionText.replace(/<>/g, ''); - - completion.commitCharacters = response.IsSuggestionMode - ? OmniSharpCompletionItemProvider.CommitCharactersWithoutSpace - : OmniSharpCompletionItemProvider.AllCommitCharacters; - - completion.preselect = response.Preselect; - - let completionSet = completions[completion.label]; - if (!completionSet) { - completions[completion.label] = { items: [completion], preselect: completion.preselect }; - } - else { - completionSet.preselect = completionSet.preselect || completion.preselect; - completionSet.items.push(completion); - } - } - - // per suggestion group, select on and indicate overloads - for (let key in completions) { - - let suggestion = completions[key].items[0], - overloadCount = completions[key].items.length - 1; - - if (overloadCount === 0) { - // remove non overloaded items - delete completions[key]; - - } - else { - // indicate that there is more - suggestion.detail = `${suggestion.detail} (+ ${overloadCount} overload(s))`; - suggestion.preselect = completions[key].preselect; - } - - result.push(suggestion); - } - - // for short completions (up to 1 character), treat the list as incomplete - // because the server has likely witheld some matches due to performance constraints - return new CompletionList(result, wordToComplete.length > 1 ? false : true); - } - catch (error) { - return; - } - } -} - -const _kinds: { [kind: string]: CompletionItemKind; } = Object.create(null); - -// types -_kinds['Class'] = CompletionItemKind.Class; -_kinds['Delegate'] = CompletionItemKind.Class; // need a better option for this. -_kinds['Enum'] = CompletionItemKind.Enum; -_kinds['Interface'] = CompletionItemKind.Interface; -_kinds['Struct'] = CompletionItemKind.Struct; - -// variables -_kinds['Local'] = CompletionItemKind.Variable; -_kinds['Parameter'] = CompletionItemKind.Variable; -_kinds['RangeVariable'] = CompletionItemKind.Variable; - -// members -_kinds['Const'] = CompletionItemKind.Constant; -_kinds['EnumMember'] = CompletionItemKind.EnumMember; -_kinds['Event'] = CompletionItemKind.Event; -_kinds['Field'] = CompletionItemKind.Field; -_kinds['Method'] = CompletionItemKind.Method; -_kinds['Property'] = CompletionItemKind.Property; - -// other stuff -_kinds['Label'] = CompletionItemKind.Unit; // need a better option for this. -_kinds['Keyword'] = CompletionItemKind.Keyword; -_kinds['Namespace'] = CompletionItemKind.Module; diff --git a/src/features/completionProvider.ts b/src/features/completionProvider.ts new file mode 100644 index 000000000..6ad10c737 --- /dev/null +++ b/src/features/completionProvider.ts @@ -0,0 +1,90 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { CompletionItemProvider, TextDocument, Position, CompletionContext, CompletionList, CompletionItem, MarkdownString, TextEdit, Range, SnippetString } from "vscode"; +import AbstractProvider from "./abstractProvider"; +import * as protocol from "../omnisharp/protocol"; +import * as serverUtils from '../omnisharp/utils'; +import { CancellationToken, CompletionTriggerKind as LspCompletionTriggerKind, InsertTextFormat } from "vscode-languageserver-protocol"; +import { createRequest } from "../omnisharp/typeConversion"; + +export default class OmnisharpCompletionProvider extends AbstractProvider implements CompletionItemProvider { + + #lastCompletions?: Map; + + public async provideCompletionItems(document: TextDocument, position: Position, token: CancellationToken, context: CompletionContext): Promise { + let request = createRequest(document, position); + request.CompletionTrigger = (context.triggerKind + 1) as LspCompletionTriggerKind; + request.TriggerCharacter = context.triggerCharacter; + + try { + const response = await serverUtils.getCompletion(this._server, request, token); + const mappedItems = response.Items.map(this._convertToVscodeCompletionItem); + + this.#lastCompletions = new Map(); + + for (let i = 0; i < mappedItems.length; i++) { + this.#lastCompletions.set(mappedItems[i], response.Items[i]); + } + + return { items: mappedItems }; + } + catch (error) { + return; + } + } + + public async resolveCompletionItem(item: CompletionItem, token: CancellationToken): Promise { + if (!this.#lastCompletions) { + return item; + } + + const lspItem = this.#lastCompletions.get(item); + if (!lspItem) { + return item; + } + + const request: protocol.CompletionResolveRequest = { Item: lspItem }; + try { + const response = await serverUtils.getCompletionResolve(this._server, request, token); + return this._convertToVscodeCompletionItem(response.Item); + } + catch (error) { + return; + } + } + + private _convertToVscodeCompletionItem(omnisharpCompletion: protocol.OmnisharpCompletionItem): CompletionItem { + let docs: MarkdownString | undefined = omnisharpCompletion.Documentation ? new MarkdownString(omnisharpCompletion.Documentation, false) : undefined; + + const mapTextEdit = function (edit: protocol.LinePositionSpanTextChange): TextEdit { + const newStart = new Position(edit.StartLine, edit.StartColumn); + const newEnd = new Position(edit.EndLine, edit.EndColumn); + const newRange = new Range(newStart, newEnd); + return new TextEdit(newRange, edit.NewText); + }; + + const additionalTextEdits = omnisharpCompletion.AdditionalTextEdits?.map(mapTextEdit); + + const insertText = omnisharpCompletion.InsertTextFormat === InsertTextFormat.Snippet + ? new SnippetString(omnisharpCompletion.InsertText) + : omnisharpCompletion.InsertText; + + return { + label: omnisharpCompletion.Label, + kind: omnisharpCompletion.Kind - 1, + detail: omnisharpCompletion.Detail, + documentation: docs, + commitCharacters: omnisharpCompletion.CommitCharacters, + preselect: omnisharpCompletion.Preselect, + filterText: omnisharpCompletion.FilterText, + insertText: insertText, + tags: omnisharpCompletion.Tags, + sortText: omnisharpCompletion.SortText, + additionalTextEdits: additionalTextEdits, + keepWhitespace: true + }; + } +} diff --git a/src/omnisharp/extension.ts b/src/omnisharp/extension.ts index a71e5559b..2b2731897 100644 --- a/src/omnisharp/extension.ts +++ b/src/omnisharp/extension.ts @@ -11,7 +11,7 @@ import { safeLength, sum } from '../common'; import { CSharpConfigurationProvider } from '../configurationProvider'; import CodeActionProvider from '../features/codeActionProvider'; import CodeLensProvider from '../features/codeLensProvider'; -import CompletionItemProvider from '../features/completionItemProvider'; +import CompletionProvider from '../features/completionProvider'; import DefinitionMetadataDocumentProvider from '../features/definitionMetadataDocumentProvider'; import DefinitionProvider from '../features/definitionProvider'; import DocumentHighlightProvider from '../features/documentHighlightProvider'; @@ -90,7 +90,7 @@ export async function activate(context: vscode.ExtensionContext, packageJSON: an localDisposables.add(vscode.languages.registerDocumentRangeFormattingEditProvider(documentSelector, new FormatProvider(server, languageMiddlewareFeature))); localDisposables.add(vscode.languages.registerOnTypeFormattingEditProvider(documentSelector, new FormatProvider(server, languageMiddlewareFeature), '}', ';')); } - localDisposables.add(vscode.languages.registerCompletionItemProvider(documentSelector, new CompletionItemProvider(server, languageMiddlewareFeature), '.', ' ')); + localDisposables.add(vscode.languages.registerCompletionItemProvider(documentSelector, new CompletionProvider(server, languageMiddlewareFeature), '.', ' ')); localDisposables.add(vscode.languages.registerWorkspaceSymbolProvider(new WorkspaceSymbolProvider(server, optionProvider, languageMiddlewareFeature))); localDisposables.add(vscode.languages.registerSignatureHelpProvider(documentSelector, new SignatureHelpProvider(server, languageMiddlewareFeature), '(', ',')); // Since the CodeActionProvider registers its own commands, we must instantiate it and add it to the localDisposables @@ -210,4 +210,4 @@ export async function activate(context: vscode.ExtensionContext, packageJSON: an return new Promise(resolve => server.onServerStart(e => resolve({ server, advisor, testManager }))); -} \ No newline at end of file +} diff --git a/src/omnisharp/protocol.ts b/src/omnisharp/protocol.ts index 6a26f4ebe..7de7f9925 100644 --- a/src/omnisharp/protocol.ts +++ b/src/omnisharp/protocol.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as path from 'path'; +import { CompletionTriggerKind, CompletionItemKind, CompletionItemTag, InsertTextFormat } from 'vscode-languageserver-protocol'; export module Requests { export const AddToProject = '/addtoproject'; @@ -32,6 +33,8 @@ export module Requests { export const GetFixAll = '/getfixall'; export const ReAnalyze = '/reanalyze'; export const QuickInfo = '/quickinfo'; + export const Completion = '/completion'; + export const CompletionResolve = '/completion/resolve'; } export namespace WireProtocol { @@ -275,30 +278,6 @@ export interface SyntaxFeature { Data: string; } -export interface AutoCompleteRequest extends Request { - WordToComplete: string; - WantDocumentationForEveryCompletionResult?: boolean; - WantImportableTypes?: boolean; - WantMethodHeader?: boolean; - WantSnippet?: boolean; - WantReturnType?: boolean; - WantKind?: boolean; - TriggerCharacter?: string; -} - -export interface AutoCompleteResponse { - CompletionText: string; - Description: string; - DisplayText: string; - RequiredNamespaceImport: string; - MethodHeader: string; - ReturnType: string; - Snippet: string; - Kind: string; - IsSuggestionMode: boolean; - Preselect: boolean; -} - export interface ProjectInformationResponse { MsBuildProject: MSBuildProject; } @@ -507,7 +486,7 @@ export interface RunFixAllRequest extends FileBasedRequest { WantsTextChanges: boolean; WantsAllCodeActionOperations: boolean; } - + export interface QuickInfoRequest extends Request { } @@ -515,6 +494,40 @@ export interface QuickInfoResponse { Markdown?: string; } +export interface CompletionRequest extends Request { + CompletionTrigger: CompletionTriggerKind; + TriggerCharacter?: string; +} + +export interface CompletionResponse { + IsIncomplete: boolean; + Items: OmnisharpCompletionItem[]; +} + +export interface CompletionResolveRequest { + Item: OmnisharpCompletionItem; +} + +export interface CompletionResolveResponse { + Item: OmnisharpCompletionItem; +} + +export interface OmnisharpCompletionItem { + Label: string; + Kind: CompletionItemKind; + Tags?: CompletionItemTag[]; + Detail?: string; + Documentation?: string; + Preselect: boolean; + SortText?: string; + FilterText?: string; + InsertText?: string; + InsertTextFormat?: InsertTextFormat; + CommitCharacters?: string[]; + AdditionalTextEdits?: LinePositionSpanTextChange[]; + Data: any; +} + export namespace V2 { export module Requests { diff --git a/src/omnisharp/utils.ts b/src/omnisharp/utils.ts index 9109a0a27..873d7e198 100644 --- a/src/omnisharp/utils.ts +++ b/src/omnisharp/utils.ts @@ -11,10 +11,6 @@ import * as vscode from 'vscode'; import { MSBuildProject } from './protocol'; import { CancellationToken } from 'vscode-languageserver-protocol'; -export async function autoComplete(server: OmniSharpServer, request: protocol.AutoCompleteRequest, token: vscode.CancellationToken) { - return server.makeRequest(protocol.Requests.AutoComplete, request, token); -} - export async function codeCheck(server: OmniSharpServer, request: protocol.Request, token: vscode.CancellationToken) { return server.makeRequest(protocol.Requests.CodeCheck, request, token); } @@ -181,6 +177,14 @@ export async function getQuickInfo(server: OmniSharpServer, request: protocol.Qu return server.makeRequest(protocol.Requests.QuickInfo, request, token); } +export async function getCompletion(server: OmniSharpServer, request: protocol.CompletionRequest, context: vscode.CancellationToken) { + return server.makeRequest(protocol.Requests.Completion, request, context); +} + +export async function getCompletionResolve(server: OmniSharpServer, request: protocol.CompletionResolveRequest, context: vscode.CancellationToken) { + return server.makeRequest(protocol.Requests.CompletionResolve, request, context); +} + export async function isNetCoreProject(project: protocol.MSBuildProject) { return project.TargetFrameworks.find(tf => tf.ShortName.startsWith('netcoreapp') || tf.ShortName.startsWith('netstandard')) !== undefined; } diff --git a/test/integrationTests/completionItemProvider.integration.test.ts b/test/integrationTests/completionProvider.integration.test.ts similarity index 91% rename from test/integrationTests/completionItemProvider.integration.test.ts rename to test/integrationTests/completionProvider.integration.test.ts index 8615319fd..964775203 100644 --- a/test/integrationTests/completionItemProvider.integration.test.ts +++ b/test/integrationTests/completionProvider.integration.test.ts @@ -3,14 +3,14 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import OmniSharpCompletionItemProvider from "../../src/features/completionItemProvider"; +import OmniSharpCompletionProvider from "../../src/features/completionProvider"; import * as vscode from 'vscode'; import testAssetWorkspace from "./testAssets/testAssetWorkspace"; import * as path from "path"; import { expect } from "chai"; import { activateCSharpExtension, isRazorWorkspace } from "./integrationHelpers"; -suite(`${OmniSharpCompletionItemProvider.name}: Returns the completion items`, () => { +suite(`${OmniSharpCompletionProvider.name}: Returns the completion items`, () => { let fileUri: vscode.Uri; suiteSetup(async function () { @@ -42,4 +42,4 @@ suite(`${OmniSharpCompletionItemProvider.name}: Returns the completion items`, ( let preselectList = completionList.items.filter(item => item.preselect === true); expect(preselectList).to.not.be.empty; }); -}); \ No newline at end of file +}); From 32a001a29a5561606714af56cbb85d83b824956c Mon Sep 17 00:00:00 2001 From: Fredric Silberberg Date: Sat, 15 Aug 2020 17:32:37 -0700 Subject: [PATCH 2/6] Be more threadsafe. --- src/features/completionProvider.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/features/completionProvider.ts b/src/features/completionProvider.ts index 6ad10c737..e4ba54546 100644 --- a/src/features/completionProvider.ts +++ b/src/features/completionProvider.ts @@ -23,12 +23,14 @@ export default class OmnisharpCompletionProvider extends AbstractProvider implem const response = await serverUtils.getCompletion(this._server, request, token); const mappedItems = response.Items.map(this._convertToVscodeCompletionItem); - this.#lastCompletions = new Map(); + let lastCompletions = new Map(); for (let i = 0; i < mappedItems.length; i++) { - this.#lastCompletions.set(mappedItems[i], response.Items[i]); + lastCompletions.set(mappedItems[i], response.Items[i]); } + this.#lastCompletions = lastCompletions; + return { items: mappedItems }; } catch (error) { @@ -37,11 +39,12 @@ export default class OmnisharpCompletionProvider extends AbstractProvider implem } public async resolveCompletionItem(item: CompletionItem, token: CancellationToken): Promise { - if (!this.#lastCompletions) { + const lastCompletions = this.#lastCompletions; + if (!lastCompletions) { return item; } - const lspItem = this.#lastCompletions.get(item); + const lspItem = lastCompletions.get(item); if (!lspItem) { return item; } From 37640c8742cc4f950951ee135ec5017a8af9e52d Mon Sep 17 00:00:00 2001 From: Fredric Silberberg Date: Sun, 16 Aug 2020 23:02:50 -0700 Subject: [PATCH 3/6] Add additional tests, including an override completion test. --- .../completionProvider.integration.test.ts | 24 ++++++++++++++++++- .../testAssets/singleCsproj/completion.cs | 2 ++ .../slnWithCsproj/src/app/completion.cs | 4 +++- 3 files changed, 28 insertions(+), 2 deletions(-) diff --git a/test/integrationTests/completionProvider.integration.test.ts b/test/integrationTests/completionProvider.integration.test.ts index 964775203..a6f2defbf 100644 --- a/test/integrationTests/completionProvider.integration.test.ts +++ b/test/integrationTests/completionProvider.integration.test.ts @@ -26,6 +26,11 @@ suite(`${OmniSharpCompletionProvider.name}: Returns the completion items`, () => let dir = testAssetWorkspace.projects[0].projectDirectoryPath; fileUri = vscode.Uri.file(path.join(dir, fileName)); await vscode.commands.executeCommand("vscode.open", fileUri); + + // The override bit is commented out to allow later debugging to work correctly. + let overrideUncomment = new vscode.WorkspaceEdit(); + overrideUncomment.delete(fileUri, new vscode.Range(new vscode.Position(11, 8), new vscode.Position(11, 11))); + await vscode.workspace.applyEdit(overrideUncomment); }); suiteTeardown(async () => { @@ -37,9 +42,26 @@ suite(`${OmniSharpCompletionProvider.name}: Returns the completion items`, () => expect(completionList.items).to.not.be.empty; }); - test("Preselect is enabled for atleast one completionItem when there is a new", async () => { + test("Preselect is enabled for at least one completionItem when there is a new", async () => { let completionList = (await vscode.commands.executeCommand("vscode.executeCompletionItemProvider", fileUri, new vscode.Position(8, 31), " ")); let preselectList = completionList.items.filter(item => item.preselect === true); expect(preselectList).to.not.be.empty; }); + + test("Resolve adds documentation", async () => { + let completionList = (await vscode.commands.executeCommand("vscode.executeCompletionItemProvider", fileUri, new vscode.Position(8, 31), " ", 10)); + // At least some of the first 10 fully-resolved elements should have documentation attached. If this ever ends up not being + // true, adjust the cutoff appropriately. + const documentation = completionList.items.slice(0, 9).filter(item => item.documentation); + expect(documentation).to.not.be.empty; + }); + + test("Override completion has additional edits", async () => { + let completionList = (await vscode.commands.executeCommand("vscode.executeCompletionItemProvider", fileUri, new vscode.Position(11, 17), " ")); + const nonSnippets = completionList.items.filter(c => c.kind != vscode.CompletionItemKind.Snippet); + for (const i of nonSnippets) { + expect((i.insertText).value).contains("$0"); + expect(i.additionalTextEdits).is.not.null; + } + }); }); diff --git a/test/integrationTests/testAssets/singleCsproj/completion.cs b/test/integrationTests/testAssets/singleCsproj/completion.cs index e844bc954..9059e4e92 100644 --- a/test/integrationTests/testAssets/singleCsproj/completion.cs +++ b/test/integrationTests/testAssets/singleCsproj/completion.cs @@ -8,5 +8,7 @@ static void shouldHaveCompletions(string[] args) { Completion a = new Completion(); } + + // override // Trailing space is intentional } } diff --git a/test/integrationTests/testAssets/slnWithCsproj/src/app/completion.cs b/test/integrationTests/testAssets/slnWithCsproj/src/app/completion.cs index 715d21ebc..9059e4e92 100644 --- a/test/integrationTests/testAssets/slnWithCsproj/src/app/completion.cs +++ b/test/integrationTests/testAssets/slnWithCsproj/src/app/completion.cs @@ -6,7 +6,9 @@ class Completion { static void shouldHaveCompletions(string[] args) { - Completion a = new Completion(); + Completion a = new Completion(); } + + // override // Trailing space is intentional } } From d3d5360a116b14730f0f425956755f9199c73f95 Mon Sep 17 00:00:00 2001 From: Fredric Silberberg Date: Tue, 18 Aug 2020 00:01:34 -0700 Subject: [PATCH 4/6] Correct line/column handling. --- src/features/completionProvider.ts | 4 ++-- test/integrationTests/completionProvider.integration.test.ts | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/features/completionProvider.ts b/src/features/completionProvider.ts index e4ba54546..33069812b 100644 --- a/src/features/completionProvider.ts +++ b/src/features/completionProvider.ts @@ -63,8 +63,8 @@ export default class OmnisharpCompletionProvider extends AbstractProvider implem let docs: MarkdownString | undefined = omnisharpCompletion.Documentation ? new MarkdownString(omnisharpCompletion.Documentation, false) : undefined; const mapTextEdit = function (edit: protocol.LinePositionSpanTextChange): TextEdit { - const newStart = new Position(edit.StartLine, edit.StartColumn); - const newEnd = new Position(edit.EndLine, edit.EndColumn); + const newStart = new Position(edit.StartLine - 1, edit.StartColumn - 1); + const newEnd = new Position(edit.EndLine - 1, edit.EndColumn - 1); const newRange = new Range(newStart, newEnd); return new TextEdit(newRange, edit.NewText); }; diff --git a/test/integrationTests/completionProvider.integration.test.ts b/test/integrationTests/completionProvider.integration.test.ts index a6f2defbf..99d769c62 100644 --- a/test/integrationTests/completionProvider.integration.test.ts +++ b/test/integrationTests/completionProvider.integration.test.ts @@ -62,6 +62,10 @@ suite(`${OmniSharpCompletionProvider.name}: Returns the completion items`, () => for (const i of nonSnippets) { expect((i.insertText).value).contains("$0"); expect(i.additionalTextEdits).is.not.null; + expect(i.additionalTextEdits[0].range.start.line).equals(11); + expect(i.additionalTextEdits[0].range.start.character).equals(8); + expect(i.additionalTextEdits[0].range.end.line).equals(11); + expect(i.additionalTextEdits[0].range.end.character).equals(16); } }); }); From f33296235990f02fc7d922cdf2b41aa42f0ceecb Mon Sep 17 00:00:00 2001 From: Fredric Silberberg Date: Wed, 19 Aug 2020 20:15:11 -0700 Subject: [PATCH 5/6] Add an option to support the forthcoming unimported type completion from Omnisharp. --- package.json | 7 ++++++- src/observers/OptionChangeObserver.ts | 3 ++- src/omnisharp/options.ts | 5 ++++- src/omnisharp/server.ts | 4 ++++ test/unitTests/Fakes/FakeOptions.ts | 2 +- test/unitTests/optionStream.test.ts | 3 ++- test/unitTests/options.test.ts | 1 + 7 files changed, 20 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index bad855334..8d2b6055e 100644 --- a/package.json +++ b/package.json @@ -777,6 +777,11 @@ "scope": "machine", "description": "Enables support for decompiling external references instead of viewing metadata." }, + "omnisharp.enableImportCompletion": { + "type": "boolean", + "default": false, + "description": "Enables support for showing unimported types and unimported extension methods in completion lists. When committed, the appropriate using statement will be added at the top of the current file. This can have a negative impact on initial completion responsiveness, particularly for the first few completion sessions after opening a solution." + }, "razor.plugin.path": { "type": [ "string", @@ -3476,4 +3481,4 @@ ] } } -} \ No newline at end of file +} diff --git a/src/observers/OptionChangeObserver.ts b/src/observers/OptionChangeObserver.ts index 877b10065..e3c445c98 100644 --- a/src/observers/OptionChangeObserver.ts +++ b/src/observers/OptionChangeObserver.ts @@ -19,7 +19,8 @@ const omniSharpOptions: ReadonlyArray = [ "waitForDebugger", "loggingLevel", "enableEditorConfigSupport", - "enableDecompilationSupport" + "enableDecompilationSupport", + "enableImportCompletion" ]; function OmniSharpOptionChangeObservable(optionObservable: Observable): Observable { diff --git a/src/omnisharp/options.ts b/src/omnisharp/options.ts index 049e1ac23..2d856ed5c 100644 --- a/src/omnisharp/options.ts +++ b/src/omnisharp/options.ts @@ -28,6 +28,7 @@ export class Options { public enableRoslynAnalyzers: boolean, public enableEditorConfigSupport: boolean, public enableDecompilationSupport: boolean, + public enableImportCompletion: boolean, public useSemanticHighlighting: boolean, public razorPluginPath?: string, public defaultLaunchSolution?: string, @@ -70,6 +71,7 @@ export class Options { const enableRoslynAnalyzers = omnisharpConfig.get('enableRoslynAnalyzers', false); const enableEditorConfigSupport = omnisharpConfig.get('enableEditorConfigSupport', false); const enableDecompilationSupport = omnisharpConfig.get('enableDecompilationSupport', false); + const enableImportCompletion = omnisharpConfig.get('enableImportCompletion', false); const useFormatting = csharpConfig.get('format.enable', true); @@ -117,6 +119,7 @@ export class Options { enableRoslynAnalyzers, enableEditorConfigSupport, enableDecompilationSupport, + enableImportCompletion, useSemanticHighlighting, razorPluginPath, defaultLaunchSolution, @@ -190,4 +193,4 @@ export class Options { return "auto"; } } -} \ No newline at end of file +} diff --git a/src/omnisharp/server.ts b/src/omnisharp/server.ts index 9ce62b5d9..4384e0f76 100644 --- a/src/omnisharp/server.ts +++ b/src/omnisharp/server.ts @@ -353,6 +353,10 @@ export class OmniSharpServer { args.push('RoslynExtensionsOptions:EnableDecompilationSupport=true'); } + if (options.enableImportCompletion === true) { + args.push('RoslynExtensionsOptions:EnableImportCompletion=true'); + } + let launchInfo: LaunchInfo; try { launchInfo = await this._omnisharpManager.GetOmniSharpLaunchInfo(this.packageJSON.defaults.omniSharp, options.path, serverUrl, latestVersionFileServerPath, installPath, this.extensionPath); diff --git a/test/unitTests/Fakes/FakeOptions.ts b/test/unitTests/Fakes/FakeOptions.ts index 638d3214b..07885d295 100644 --- a/test/unitTests/Fakes/FakeOptions.ts +++ b/test/unitTests/Fakes/FakeOptions.ts @@ -6,5 +6,5 @@ import { Options } from "../../../src/omnisharp/options"; export function getEmptyOptions(): Options { - return new Options("", "", false, "", false, 0, 0, false, false, false, false, false, false, 0, 0, false, false, false, false, false, false, undefined, "", ""); + return new Options("", "", false, "", false, 0, 0, false, false, false, false, false, false, 0, 0, false, false, false, false, false, false, false, undefined, "", ""); } diff --git a/test/unitTests/optionStream.test.ts b/test/unitTests/optionStream.test.ts index 2fe2a6b49..21559e107 100644 --- a/test/unitTests/optionStream.test.ts +++ b/test/unitTests/optionStream.test.ts @@ -54,6 +54,7 @@ suite('OptionStream', () => { options.enableRoslynAnalyzers.should.equal(false); options.enableEditorConfigSupport.should.equal(false); options.enableDecompilationSupport.should.equal(false); + options.enableImportCompletion.should.equal(false); expect(options.defaultLaunchSolution).to.be.undefined; }); @@ -103,4 +104,4 @@ suite('OptionStream', () => { return vscode; } -}); \ No newline at end of file +}); diff --git a/test/unitTests/options.test.ts b/test/unitTests/options.test.ts index 88ac39b84..a314a1dbc 100644 --- a/test/unitTests/options.test.ts +++ b/test/unitTests/options.test.ts @@ -33,6 +33,7 @@ suite("Options tests", () => { options.enableRoslynAnalyzers.should.equal(false); options.enableEditorConfigSupport.should.equal(false); options.enableDecompilationSupport.should.equal(false); + options.enableImportCompletion.should.equal(false); expect(options.defaultLaunchSolution).to.be.undefined; }); From 8c47b9ad8cf637548feb485d7bbc1dcdfe1bf65c Mon Sep 17 00:00:00 2001 From: Fred Silberberg Date: Thu, 20 Aug 2020 10:16:13 -0700 Subject: [PATCH 6/6] Adjust wording of the description. Co-authored-by: Joey Robichaud --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 8d2b6055e..b5d75dd02 100644 --- a/package.json +++ b/package.json @@ -780,7 +780,7 @@ "omnisharp.enableImportCompletion": { "type": "boolean", "default": false, - "description": "Enables support for showing unimported types and unimported extension methods in completion lists. When committed, the appropriate using statement will be added at the top of the current file. This can have a negative impact on initial completion responsiveness, particularly for the first few completion sessions after opening a solution." + "description": "Enables support for showing unimported types and unimported extension methods in completion lists. When committed, the appropriate using directive will be added at the top of the current file. This option can have a negative impact on initial completion responsiveness, particularly for the first few completion sessions after opening a solution." }, "razor.plugin.path": { "type": [