Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 20 additions & 21 deletions src/lsptoolshost/onAutoInsert.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,10 @@ import { UriConverter } from './uriConverter';
import { FormattingOptions, TextDocumentIdentifier } from 'vscode-languageclient/node';
import * as RoslynProtocol from './roslynProtocol';
import { RoslynLanguageServer } from './roslynLanguageServer';
import { languageServerOptions } from '../shared/options';

export function registerOnAutoInsert(languageServer: RoslynLanguageServer) {
let source = new vscode.CancellationTokenSource();
vscode.workspace.onDidChangeTextDocument(async (e) => {
if (!languageServerOptions.documentSelector.includes(e.document.languageId)) {
return;
}

if (e.contentChanges.length > 1 || e.contentChanges.length === 0) {
return;
}
Expand All @@ -28,27 +23,31 @@ export function registerOnAutoInsert(languageServer: RoslynLanguageServer) {
return;
}

const capabilities = await languageServer.getServerCapabilities();
const onAutoInsertFeature = languageServer.getOnAutoInsertFeature();
const onAutoInsertOptions = onAutoInsertFeature?.getOptions(e.document);
const vsTriggerCharacters = onAutoInsertOptions?._vs_triggerCharacters;

if (capabilities._vs_onAutoInsertProvider) {
// Regular expression to match all whitespace characters except the newline character
const changeTrimmed = change.text.replace(/[^\S\n]+/g, '');
if (vsTriggerCharacters === undefined) {
return;
}

if (!capabilities._vs_onAutoInsertProvider._vs_triggerCharacters.includes(changeTrimmed)) {
return;
}
// Regular expression to match all whitespace characters except the newline character
const changeTrimmed = change.text.replace(/[^\S\n]+/g, '');

source.cancel();
source = new vscode.CancellationTokenSource();
try {
await applyAutoInsertEdit(e, changeTrimmed, languageServer, source.token);
} catch (e) {
if (e instanceof vscode.CancellationError) {
return;
}
if (!vsTriggerCharacters.includes(changeTrimmed)) {
return;
}

throw e;
source.cancel();
source = new vscode.CancellationTokenSource();
try {
await applyAutoInsertEdit(e, changeTrimmed, languageServer, source.token);
} catch (e) {
if (e instanceof vscode.CancellationError) {
return;
}

throw e;
}
});
}
Expand Down
134 changes: 134 additions & 0 deletions src/lsptoolshost/onAutoInsertFeature.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import {
languages as Languages,
workspace as Workspace,
DocumentSelector as VDocumentSelector,
TextDocument,
} from 'vscode';

import { DynamicFeature, FeatureState, LanguageClient, RegistrationData, ensure } from 'vscode-languageclient/node';

import {
ClientCapabilities,
DocumentSelector,
InitializeParams,
ProtocolRequestType,
RegistrationType,
ServerCapabilities,
} from 'vscode-languageserver-protocol';

import * as RoslynProtocol from './roslynProtocol';
import * as UUID from 'vscode-languageclient/lib/common/utils/uuid';

export class OnAutoInsertFeature implements DynamicFeature<RoslynProtocol.OnAutoInsertRegistrationOptions> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did not know this could be extended in this way. This is super useful!

private readonly _client: LanguageClient;
private readonly _registrations: Map<string, RegistrationData<RoslynProtocol.OnAutoInsertRegistrationOptions>>;

constructor(client: LanguageClient) {
this._client = client;
this._registrations = new Map();
this.registrationType = new ProtocolRequestType<
RoslynProtocol.OnAutoInsertParams,
RoslynProtocol.OnAutoInsertResponseItem | null,
never,
void,
RoslynProtocol.OnAutoInsertRegistrationOptions
>(RoslynProtocol.OnAutoInsertRequest.method);
}
fillInitializeParams?: ((params: InitializeParams) => void) | undefined;
preInitialize?:
| ((capabilities: ServerCapabilities<any>, documentSelector: DocumentSelector | undefined) => void)
| undefined;
registrationType: RegistrationType<RoslynProtocol.OnAutoInsertRegistrationOptions>;
register(data: RegistrationData<RoslynProtocol.OnAutoInsertRegistrationOptions>): void {
if (!data.registerOptions.documentSelector) {
return;
}
this._registrations.set(data.id, data);
}
unregister(id: string): void {
const registration = this._registrations.get(id);
if (registration !== undefined) {
this._registrations.delete(id);
}
}
dispose(): void {
this._registrations.clear();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assume VSCode calls this when the instance shuts down? We don't need to manually do anything right?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Documentation says this:

    /**
     * Called when the client is stopped to dispose this feature. Usually a feature
     * un-registers listeners registered hooked up with the VS Code extension host.
     */

That said, it didnt' actually get called on shutdown. Perhaps there's a different way to trigger this.

}

public getState(): FeatureState {
const selectors = this.getDocumentSelectors();

let count = 0;
for (const selector of selectors) {
count++;
for (const document of Workspace.textDocuments) {
if (Languages.match(selector, document) > 0) {
return { kind: 'document', id: this.registrationType.method, registrations: true, matches: true };
}
}
}
const registrations = count > 0;
return { kind: 'document', id: this.registrationType.method, registrations, matches: false };
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wondering - could this implement DynamicDocumentFeature instead? Looks like it might already have this state management code
https://github.com/microsoft/vscode-languageserver-node/blob/54dd4abae40251a18fbe263285e6e97f27457e62/client/src/common/features.ts#L177

This is also probably outside ofthe scope of this PR, but it seems like most vscode features implement TextDocumentLanguageFeature, and then also have the implementation of the feature inside that type.
Wondering if that is something we can do here (instead of having a separate place where the implementation vs the capabilities happen).
e.g. https://github.com/microsoft/vscode-languageserver-node/blob/54dd4abae40251a18fbe263285e6e97f27457e62/client/src/common/hover.ts#L28

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did go down that road initially. However, I wasn't able to get all the way there because it started making hardcoded assumptions about the list of language features. For example, it requires a client that implements FeatureClient<Middleware, LanguageClientOptions> but MiddleWare is hardcoded to this: https://github.com/microsoft/vscode-languageserver-node/blob/54dd4abae40251a18fbe263285e6e97f27457e62/client/src/common/client.ts#L338

}

public fillClientCapabilities(capabilities: ClientCapabilities): void {
const textDocumentCapabilities: any = ensure(capabilities, 'textDocument')!;
if (textDocumentCapabilities['_vs_onAutoInsert'] === undefined) {
textDocumentCapabilities['_vs_onAutoInsert'] = {} as any;
}
const onAutoInsertCapability = textDocumentCapabilities['_vs_onAutoInsert'];
onAutoInsertCapability.dynamicRegistration = true;
}

public initialize(_capabilities: ServerCapabilities, documentSelector: DocumentSelector): void {
const capabilities: any = _capabilities;
const options = this.getRegistrationOptions(documentSelector, capabilities._vs_onAutoInsertProvider);
if (!options) {
return;
}
this.register({
id: UUID.generateUuid(),
registerOptions: options,
});
}

public getOptions(textDocument: TextDocument): RoslynProtocol.OnAutoInsertOptions | undefined {
for (const registration of this._registrations.values()) {
const selector = registration.registerOptions.documentSelector;
if (
selector !== null &&
Languages.match(this._client.protocol2CodeConverter.asDocumentSelector(selector), textDocument) > 0
) {
return registration.registerOptions;
}
}
return undefined;
}

private *getDocumentSelectors(): IterableIterator<VDocumentSelector> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm learning so many new things in this PR...

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Me too. I haven't really written any TypeScript before. :)

for (const registration of this._registrations.values()) {
const selector = registration.registerOptions.documentSelector;
if (selector === null) {
continue;
}
yield this._client.protocol2CodeConverter.asDocumentSelector(selector);
}
}

private getRegistrationOptions(
documentSelector: DocumentSelector | undefined,
capability: undefined | RoslynProtocol.OnAutoInsertOptions
): (RoslynProtocol.OnAutoInsertRegistrationOptions & { documentSelector: DocumentSelector }) | undefined {
if (!documentSelector || !capability) {
return undefined;
}
return Object.assign({}, capability, { documentSelector }) as RoslynProtocol.OnAutoInsertRegistrationOptions & {
documentSelector: DocumentSelector;
};
}
}
12 changes: 9 additions & 3 deletions src/lsptoolshost/roslynLanguageServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ import { IDisposable } from '../disposable';
import { registerNestedCodeActionCommands } from './nestedCodeAction';
import { registerRestoreCommands } from './restore';
import { BuildDiagnosticsService } from './buildDiagnosticsService';
import { OnAutoInsertFeature } from './onAutoInsertFeature';

let _channel: vscode.OutputChannel;
let _traceChannel: vscode.OutputChannel;
Expand Down Expand Up @@ -100,6 +101,8 @@ export class RoslynLanguageServer {
/** The project files previously opened; we hold onto this for the same reason as _solutionFile. */
private _projectFiles: vscode.Uri[] = new Array<vscode.Uri>();

public readonly _onAutoInsertFeature: OnAutoInsertFeature;

public _buildDiagnosticService: BuildDiagnosticsService;

constructor(
Expand All @@ -126,6 +129,8 @@ export class RoslynLanguageServer {
this.registerDebuggerAttach();

registerShowToastNotification(this._languageClient);

this._onAutoInsertFeature = new OnAutoInsertFeature(this._languageClient);
}

private registerSetTrace() {
Expand Down Expand Up @@ -239,6 +244,8 @@ export class RoslynLanguageServer {

const server = new RoslynLanguageServer(client, platformInfo, context, telemetryReporter, languageServerEvents);

client.registerFeature(server._onAutoInsertFeature);

// Start the client. This will also launch the server process.
await client.start();
return server;
Expand Down Expand Up @@ -476,9 +483,8 @@ export class RoslynLanguageServer {
}
}

public getServerCapabilities(): any {
const capabilities: any = this._languageClient.initializeResult?.capabilities;
return capabilities;
public getOnAutoInsertFeature(): OnAutoInsertFeature | undefined {
return this._onAutoInsertFeature;
}

private static async startServer(
Expand Down
17 changes: 16 additions & 1 deletion src/lsptoolshost/roslynProtocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

import { Command } from 'vscode';
import * as lsp from 'vscode-languageserver-protocol';
import { CodeAction } from 'vscode-languageserver-protocol';
import { CodeAction, TextDocumentRegistrationOptions } from 'vscode-languageserver-protocol';
import { ProjectConfigurationMessage } from '../shared/projectConfiguration';

export interface WorkspaceDebugConfigurationParams {
Expand Down Expand Up @@ -61,6 +61,21 @@ export interface OnAutoInsertResponseItem {
_vs_textEdit: lsp.TextEdit;
}

/**
* OnAutoInsert options.
*/
export interface OnAutoInsertOptions {
/**
* List of characters triggering an {@link OnAutoInsertRequest}.
*/
_vs_triggerCharacters?: string[];
}

/**
* Registration options for an {@link OnAutoInsertRequest}.
*/
export interface OnAutoInsertRegistrationOptions extends TextDocumentRegistrationOptions, OnAutoInsertOptions {}

export interface RegisterSolutionSnapshotResponseItem {
/**
* Represents a solution snapshot.
Expand Down