Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support async completion #4116

Merged
merged 5 commits into from May 17, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
6 changes: 5 additions & 1 deletion package.json
Expand Up @@ -54,7 +54,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"
},
Expand Down Expand Up @@ -821,6 +820,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",
Expand Down
49 changes: 46 additions & 3 deletions src/features/completionProvider.ts
Expand Up @@ -3,25 +3,33 @@
* 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, WorkspaceEdit, workspace } 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<CompletionItem, protocol.OmnisharpCompletionItem>;

constructor(server: OmniSharpServer, languageMiddlewareFeature: LanguageMiddlewareFeature) {
super(server, languageMiddlewareFeature);
}

public async provideCompletionItems(document: TextDocument, position: Position, token: CancellationToken, context: CompletionContext): Promise<CompletionList> {
let request = createRequest<protocol.CompletionRequest>(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);
const mappedItems = response.Items.map(arg => this._convertToVscodeCompletionItem(arg));

let lastCompletions = new Map();

Expand Down Expand Up @@ -59,6 +67,40 @@ 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;
}

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;
}

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;

Expand Down Expand Up @@ -94,7 +136,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
};
}
}
1 change: 1 addition & 0 deletions src/observers/OptionChangeObserver.ts
Expand Up @@ -22,6 +22,7 @@ const omniSharpOptions: ReadonlyArray<OptionsKey> = [
"enableDecompilationSupport",
"enableImportCompletion",
"organizeImportsOnFormat",
"enableAsyncCompletion",
];

function OmniSharpOptionChangeObservable(optionObservable: Observable<Options>): Observable<Options> {
Expand Down
6 changes: 4 additions & 2 deletions src/omnisharp/extension.ts
Expand Up @@ -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';
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
3 changes: 3 additions & 0 deletions src/omnisharp/options.ts
Expand Up @@ -32,6 +32,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,
Expand Down Expand Up @@ -75,6 +76,7 @@ export class Options {
const enableEditorConfigSupport = omnisharpConfig.get<boolean>('enableEditorConfigSupport', false);
const enableDecompilationSupport = omnisharpConfig.get<boolean>('enableDecompilationSupport', false);
const enableImportCompletion = omnisharpConfig.get<boolean>('enableImportCompletion', false);
const enableAsyncCompletion = omnisharpConfig.get<boolean>('enableAsyncCompletion', false);

const useFormatting = csharpConfig.get<boolean>('format.enable', true);
const organizeImportsOnFormat = omnisharpConfig.get<boolean>('organizeImportsOnFormat', false);
Expand Down Expand Up @@ -130,6 +132,7 @@ export class Options {
enableEditorConfigSupport,
enableDecompilationSupport,
enableImportCompletion,
enableAsyncCompletion,
useSemanticHighlighting,
razorPluginPath,
defaultLaunchSolution,
Expand Down
12 changes: 12 additions & 0 deletions src/omnisharp/protocol.ts
Expand Up @@ -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 {
Expand Down Expand Up @@ -524,6 +525,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;
Expand All @@ -539,6 +550,7 @@ export interface OmnisharpCompletionItem {
CommitCharacters?: string[];
AdditionalTextEdits?: LinePositionSpanTextChange[];
Data: any;
HasAfterInsertStep: boolean;
}

export namespace V2 {
Expand Down
4 changes: 4 additions & 0 deletions src/omnisharp/server.ts
Expand Up @@ -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);
Expand Down
4 changes: 4 additions & 0 deletions src/omnisharp/utils.ts
Expand Up @@ -179,6 +179,10 @@ export async function getCompletionResolve(server: OmniSharpServer, request: pro
return server.makeRequest<protocol.CompletionResolveResponse>(protocol.Requests.CompletionResolve, request, context);
}

export async function getCompletionAfterInsert(server: OmniSharpServer, request: protocol.CompletionAfterInsertionRequest) {
return server.makeRequest<protocol.CompletionAfterInsertResponse>(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;
}
Expand Down
2 changes: 1 addition & 1 deletion test/unitTests/Fakes/FakeOptions.ts
Expand Up @@ -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, "", "");
}
1 change: 1 addition & 0 deletions test/unitTests/optionStream.test.ts
Expand Up @@ -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;
});

Expand Down