From 6f47507a5a2da6ed5189b61bb86c13e9b7f988a4 Mon Sep 17 00:00:00 2001 From: Fredric Silberberg Date: Fri, 16 Oct 2020 00:25:37 -0700 Subject: [PATCH 1/3] Support async completion vscode side of https://github.com/OmniSharp/omnisharp-roslyn/pull/1986. --- package.json | 8 +++-- src/features/completionProvider.ts | 45 +++++++++++++++++++++++++-- src/observers/OptionChangeObserver.ts | 1 + src/omnisharp/extension.ts | 6 ++-- src/omnisharp/options.ts | 3 ++ src/omnisharp/protocol.ts | 13 ++++++++ src/omnisharp/server.ts | 4 +++ src/omnisharp/utils.ts | 4 +++ test/unitTests/Fakes/FakeOptions.ts | 2 +- test/unitTests/optionStream.test.ts | 1 + 10 files changed, 79 insertions(+), 8 deletions(-) diff --git a/package.json b/package.json index a926f3b65..0ab78ccd4 100644 --- a/package.json +++ b/package.json @@ -48,7 +48,6 @@ "test:integration:slnFilterWithCsproj": "tsc -p ./ && gulp test:integration:slnFilterWithCsproj", "test:release": "mocha --config ./.mocharc.jsonc test/releaseTests/**/*.test.ts", "test:artifacts": "mocha --config ./.mocharc.jsonc test/artifactTests/**/*.test.ts", - "unpackage:vsix": "gulp vsix:release:unpackage", "gulp": "gulp" }, @@ -807,6 +806,11 @@ "default": false, "description": "Specifies whether 'using' directives should be grouped and sorted during document formatting." }, + "omnisharp.enableAsyncCompletion": { + "type": "boolean", + "default": false, + "description": "(EXPERIMENTAL) Enables support for resolving completion edits asynchronously. This can speed up time to show the completion list, particularly override and partial method completion lists, at the cost of slight delays after inserting a completion item. Most completion items will have no noticeable impact with this feature, but typing immediately after inserting an override or partial method completion, before the insert is completed, can have unpredictable results." + }, "razor.plugin.path": { "type": [ "string", @@ -3688,4 +3692,4 @@ ] } } -} \ No newline at end of file +} diff --git a/src/features/completionProvider.ts b/src/features/completionProvider.ts index 8e04a145f..749bda988 100644 --- a/src/features/completionProvider.ts +++ b/src/features/completionProvider.ts @@ -3,17 +3,25 @@ * 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 { CompletionItemProvider, TextDocument, Position, CompletionContext, CompletionList, CompletionItem, MarkdownString, TextEdit, Range, SnippetString, window, Selection } 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"; +import { LanguageMiddlewareFeature } from "../omnisharp/LanguageMiddlewareFeature"; +import { OmniSharpServer } from "../omnisharp/server"; + +export const CompletionAfterInsertCommand = "csharp.completion.afterInsert"; export default class OmnisharpCompletionProvider extends AbstractProvider implements CompletionItemProvider { #lastCompletions?: Map; + constructor(server: OmniSharpServer, languageMiddlewareFeature: LanguageMiddlewareFeature) { + super(server, languageMiddlewareFeature); + } + public async provideCompletionItems(document: TextDocument, position: Position, token: CancellationToken, context: CompletionContext): Promise { let request = createRequest(document, position); request.CompletionTrigger = (context.triggerKind + 1) as LspCompletionTriggerKind; @@ -21,7 +29,7 @@ export default class OmnisharpCompletionProvider extends AbstractProvider implem try { const response = await serverUtils.getCompletion(this._server, request, token); - const mappedItems = response.Items.map(this._convertToVscodeCompletionItem); + const mappedItems = response.Items.map(arg => this._convertToVscodeCompletionItem(arg)); let lastCompletions = new Map(); @@ -59,6 +67,36 @@ export default class OmnisharpCompletionProvider extends AbstractProvider implem } } + public async afterInsert(item: protocol.OmnisharpCompletionItem) { + try { + const response = await serverUtils.getCompletionAfterInsert(this._server, { Item: item }); + + if (!response.Changes || !response.Column || !response.Line) { + return; + } + + const applied = await window.activeTextEditor.edit(editBuilder => { + for (const change of response.Changes) { + const replaceRange = new Range(change.StartLine, change.StartColumn, change.EndLine, change.EndColumn); + editBuilder.replace(replaceRange, change.NewText); + } + }); + + if (!applied) { + return; + } + + const responseLine = response.Line; + const responseColumn = response.Column; + + const finalPosition = new Position(responseLine, responseColumn); + window.activeTextEditor.selections = [new Selection(finalPosition, finalPosition)]; + } + catch (error) { + return; + } + } + private _convertToVscodeCompletionItem(omnisharpCompletion: protocol.OmnisharpCompletionItem): CompletionItem { const docs: MarkdownString | undefined = omnisharpCompletion.Documentation ? new MarkdownString(omnisharpCompletion.Documentation, false) : undefined; @@ -94,7 +132,8 @@ export default class OmnisharpCompletionProvider extends AbstractProvider implem tags: omnisharpCompletion.Tags, sortText: omnisharpCompletion.SortText, additionalTextEdits: additionalTextEdits, - keepWhitespace: true + keepWhitespace: true, + command: omnisharpCompletion.HasAfterInsertStep ? { command: CompletionAfterInsertCommand, title: "", arguments: [omnisharpCompletion] } : undefined }; } } diff --git a/src/observers/OptionChangeObserver.ts b/src/observers/OptionChangeObserver.ts index 2021ab609..7d80288c2 100644 --- a/src/observers/OptionChangeObserver.ts +++ b/src/observers/OptionChangeObserver.ts @@ -22,6 +22,7 @@ const omniSharpOptions: ReadonlyArray = [ "enableDecompilationSupport", "enableImportCompletion", "organizeImportsOnFormat", + "enableAsyncCompletion", ]; function OmniSharpOptionChangeObservable(optionObservable: Observable): Observable { diff --git a/src/omnisharp/extension.ts b/src/omnisharp/extension.ts index f6cd2d450..95de09137 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 CompletionProvider from '../features/completionProvider'; +import CompletionProvider, { CompletionAfterInsertCommand } from '../features/completionProvider'; import DefinitionMetadataDocumentProvider from '../features/definitionMetadataDocumentProvider'; import DefinitionProvider from '../features/definitionProvider'; import DocumentHighlightProvider from '../features/documentHighlightProvider'; @@ -69,6 +69,7 @@ export async function activate(context: vscode.ExtensionContext, packageJSON: an disposables.add(languageMiddlewareFeature); let localDisposables: CompositeDisposable; const testManager = new TestManager(server, eventStream, languageMiddlewareFeature); + const completionProvider = new CompletionProvider(server, languageMiddlewareFeature); disposables.add(server.onServerStart(() => { // register language feature provider on start @@ -90,7 +91,8 @@ 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), '}', '/', '\n', ';')); } - localDisposables.add(vscode.languages.registerCompletionItemProvider(documentSelector, new CompletionProvider(server, languageMiddlewareFeature), '.', ' ')); + localDisposables.add(vscode.languages.registerCompletionItemProvider(documentSelector, completionProvider, '.', ' ')); + localDisposables.add(vscode.commands.registerCommand(CompletionAfterInsertCommand, async (item) => completionProvider.afterInsert(item))); 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 diff --git a/src/omnisharp/options.ts b/src/omnisharp/options.ts index 0d5d9fc50..ad6538567 100644 --- a/src/omnisharp/options.ts +++ b/src/omnisharp/options.ts @@ -31,6 +31,7 @@ export class Options { public enableEditorConfigSupport: boolean, public enableDecompilationSupport: boolean, public enableImportCompletion: boolean, + public enableAsyncCompletion: boolean, public useSemanticHighlighting: boolean, public razorPluginPath?: string, public defaultLaunchSolution?: string, @@ -74,6 +75,7 @@ export class Options { const enableEditorConfigSupport = omnisharpConfig.get('enableEditorConfigSupport', false); const enableDecompilationSupport = omnisharpConfig.get('enableDecompilationSupport', false); const enableImportCompletion = omnisharpConfig.get('enableImportCompletion', false); + const enableAsyncCompletion = omnisharpConfig.get('enableAsyncCompletion', false); const useFormatting = csharpConfig.get('format.enable', true); const organizeImportsOnFormat = omnisharpConfig.get('organizeImportsOnFormat', false); @@ -127,6 +129,7 @@ export class Options { enableEditorConfigSupport, enableDecompilationSupport, enableImportCompletion, + enableAsyncCompletion, useSemanticHighlighting, razorPluginPath, defaultLaunchSolution, diff --git a/src/omnisharp/protocol.ts b/src/omnisharp/protocol.ts index 7fe3c74ec..d80795ccc 100644 --- a/src/omnisharp/protocol.ts +++ b/src/omnisharp/protocol.ts @@ -34,6 +34,7 @@ export module Requests { export const QuickInfo = '/quickinfo'; export const Completion = '/completion'; export const CompletionResolve = '/completion/resolve'; + export const CompletionAfterInsert = '/completion/afterInsert'; } export namespace WireProtocol { @@ -509,6 +510,7 @@ export interface QuickInfoResponse { export interface CompletionRequest extends Request { CompletionTrigger: CompletionTriggerKind; TriggerCharacter?: string; + UseAsyncCompletion: boolean; } export interface CompletionResponse { @@ -524,6 +526,16 @@ export interface CompletionResolveResponse { Item: OmnisharpCompletionItem; } +export interface CompletionAfterInsertionRequest { + Item: OmnisharpCompletionItem; +} + +export interface CompletionAfterInsertResponse { + Changes?: LinePositionSpanTextChange[]; + Line?: number; + Column?: number; +} + export interface OmnisharpCompletionItem { Label: string; Kind: CompletionItemKind; @@ -539,6 +551,7 @@ export interface OmnisharpCompletionItem { CommitCharacters?: string[]; AdditionalTextEdits?: LinePositionSpanTextChange[]; Data: any; + HasAfterInsertStep: boolean; } export namespace V2 { diff --git a/src/omnisharp/server.ts b/src/omnisharp/server.ts index 3fb242822..98676a128 100644 --- a/src/omnisharp/server.ts +++ b/src/omnisharp/server.ts @@ -372,6 +372,10 @@ export class OmniSharpServer { args.push('RoslynExtensionsOptions:EnableImportCompletion=true'); } + if (options.enableAsyncCompletion === true) { + args.push('RoslynExtensionsOptions:EnableAsyncCompletion=true'); + } + let launchInfo: LaunchInfo; try { launchInfo = await this._omnisharpManager.GetOmniSharpLaunchInfo(this.packageJSON.defaults.omniSharp, options.path, serverUrl, latestVersionFileServerPath, installPath, this.extensionPath); diff --git a/src/omnisharp/utils.ts b/src/omnisharp/utils.ts index c7a392731..136b33499 100644 --- a/src/omnisharp/utils.ts +++ b/src/omnisharp/utils.ts @@ -179,6 +179,10 @@ export async function getCompletionResolve(server: OmniSharpServer, request: pro return server.makeRequest(protocol.Requests.CompletionResolve, request, context); } +export async function getCompletionAfterInsert(server: OmniSharpServer, request: protocol.CompletionAfterInsertionRequest) { + return server.makeRequest(protocol.Requests.CompletionAfterInsert, request); +} + 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/unitTests/Fakes/FakeOptions.ts b/test/unitTests/Fakes/FakeOptions.ts index 59fe2c817..a8ec9342e 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, false, false, 0, 0, false, false, false, false, false, false, false, undefined, "", ""); + return new Options("", "", false, "", false, 0, 0, false, false, false, false, false, false, false, false, 0, 0, false, false, false, false, false, false, false, false, undefined, "", ""); } diff --git a/test/unitTests/optionStream.test.ts b/test/unitTests/optionStream.test.ts index 21559e107..df53949c5 100644 --- a/test/unitTests/optionStream.test.ts +++ b/test/unitTests/optionStream.test.ts @@ -55,6 +55,7 @@ suite('OptionStream', () => { options.enableEditorConfigSupport.should.equal(false); options.enableDecompilationSupport.should.equal(false); options.enableImportCompletion.should.equal(false); + options.enableAsyncCompletion.should.equal(false); expect(options.defaultLaunchSolution).to.be.undefined; }); From 4a9bcf5c5cc740aab69fe0a9cb63079f3371894c Mon Sep 17 00:00:00 2001 From: Fredric Silberberg Date: Sat, 17 Oct 2020 22:57:53 -0700 Subject: [PATCH 2/3] Use the editor remap API to make razor interop work. --- src/features/completionProvider.ts | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/src/features/completionProvider.ts b/src/features/completionProvider.ts index 749bda988..01af931b6 100644 --- a/src/features/completionProvider.ts +++ b/src/features/completionProvider.ts @@ -3,7 +3,7 @@ * 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, window, Selection } from "vscode"; +import { CompletionItemProvider, TextDocument, Position, CompletionContext, CompletionList, CompletionItem, MarkdownString, TextEdit, Range, SnippetString, window, Selection, WorkspaceEdit, workspace } from "vscode"; import AbstractProvider from "./abstractProvider"; import * as protocol from "../omnisharp/protocol"; import * as serverUtils from '../omnisharp/utils'; @@ -69,19 +69,23 @@ export default class OmnisharpCompletionProvider extends AbstractProvider implem public async afterInsert(item: protocol.OmnisharpCompletionItem) { try { + const uri = window.activeTextEditor.document.uri; const response = await serverUtils.getCompletionAfterInsert(this._server, { Item: item }); if (!response.Changes || !response.Column || !response.Line) { return; } - const applied = await window.activeTextEditor.edit(editBuilder => { - for (const change of response.Changes) { - const replaceRange = new Range(change.StartLine, change.StartColumn, change.EndLine, change.EndColumn); - editBuilder.replace(replaceRange, change.NewText); - } - }); + let edit = new WorkspaceEdit(); + edit.set(uri, response.Changes.map(change => ({ + newText: change.NewText, + range: new Range(new Position(change.StartLine, change.StartColumn), + new Position(change.EndLine, change.EndColumn)) + }))); + edit = await this._languageMiddlewareFeature.remap("remapWorkspaceEdit", edit, CancellationToken.None); + + const applied = await workspace.applyEdit(edit); if (!applied) { return; } From 0e83f1aae45993868b5299ae17ad297033683758 Mon Sep 17 00:00:00 2001 From: Fred Silberberg Date: Fri, 7 May 2021 15:00:26 -0700 Subject: [PATCH 3/3] Update src/omnisharp/protocol.ts --- src/omnisharp/protocol.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/omnisharp/protocol.ts b/src/omnisharp/protocol.ts index d80795ccc..9aea9ea35 100644 --- a/src/omnisharp/protocol.ts +++ b/src/omnisharp/protocol.ts @@ -510,7 +510,6 @@ export interface QuickInfoResponse { export interface CompletionRequest extends Request { CompletionTrigger: CompletionTriggerKind; TriggerCharacter?: string; - UseAsyncCompletion: boolean; } export interface CompletionResponse {