Skip to content

Commit ac85eb7

Browse files
committed
Created OnAutoInsertFeature to support dynamic capability registration
1 parent 091ed53 commit ac85eb7

File tree

4 files changed

+181
-25
lines changed

4 files changed

+181
-25
lines changed

src/lsptoolshost/onAutoInsert.ts

Lines changed: 20 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,10 @@ import { UriConverter } from './uriConverter';
99
import { FormattingOptions, TextDocumentIdentifier } from 'vscode-languageclient/node';
1010
import * as RoslynProtocol from './roslynProtocol';
1111
import { RoslynLanguageServer } from './roslynLanguageServer';
12-
import { languageServerOptions } from '../shared/options';
1312

1413
export function registerOnAutoInsert(languageServer: RoslynLanguageServer) {
1514
let source = new vscode.CancellationTokenSource();
1615
vscode.workspace.onDidChangeTextDocument(async (e) => {
17-
if (!languageServerOptions.documentSelector.includes(e.document.languageId)) {
18-
return;
19-
}
20-
2116
if (e.contentChanges.length > 1 || e.contentChanges.length === 0) {
2217
return;
2318
}
@@ -28,27 +23,31 @@ export function registerOnAutoInsert(languageServer: RoslynLanguageServer) {
2823
return;
2924
}
3025

31-
const capabilities = await languageServer.getServerCapabilities();
26+
const onAutoInsertFeature = languageServer.getOnAutoInsertFeature();
27+
const onAutoInsertOptions = onAutoInsertFeature?.getOptions(e.document);
28+
const vsTriggerCharacters = onAutoInsertOptions?._vs_triggerCharacters;
3229

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

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

41-
source.cancel();
42-
source = new vscode.CancellationTokenSource();
43-
try {
44-
await applyAutoInsertEdit(e, changeTrimmed, languageServer, source.token);
45-
} catch (e) {
46-
if (e instanceof vscode.CancellationError) {
47-
return;
48-
}
37+
if (!vsTriggerCharacters.includes(changeTrimmed)) {
38+
return;
39+
}
4940

50-
throw e;
41+
source.cancel();
42+
source = new vscode.CancellationTokenSource();
43+
try {
44+
await applyAutoInsertEdit(e, changeTrimmed, languageServer, source.token);
45+
} catch (e) {
46+
if (e instanceof vscode.CancellationError) {
47+
return;
5148
}
49+
50+
throw e;
5251
}
5352
});
5453
}
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import {
7+
languages as Languages,
8+
workspace as Workspace,
9+
DocumentSelector as VDocumentSelector,
10+
TextDocument,
11+
} from 'vscode';
12+
13+
import { DynamicFeature, FeatureState, LanguageClient, RegistrationData, ensure } from 'vscode-languageclient/node';
14+
15+
import {
16+
ClientCapabilities,
17+
DocumentSelector,
18+
InitializeParams,
19+
ProtocolRequestType,
20+
RegistrationType,
21+
ServerCapabilities,
22+
} from 'vscode-languageserver-protocol';
23+
24+
import * as RoslynProtocol from './roslynProtocol';
25+
import * as Is from 'vscode-languageclient/lib/common/utils/is';
26+
import * as UUID from 'vscode-languageclient/lib/common/utils/uuid';
27+
28+
export class OnAutoInsertFeature implements DynamicFeature<RoslynProtocol.OnAutoInsertRegistrationOptions> {
29+
private readonly _client: LanguageClient;
30+
private readonly _registrations: Map<string, RegistrationData<RoslynProtocol.OnAutoInsertRegistrationOptions>>;
31+
32+
constructor(client: LanguageClient) {
33+
this._client = client;
34+
this._registrations = new Map();
35+
this.registrationType = new ProtocolRequestType<
36+
RoslynProtocol.OnAutoInsertParams,
37+
RoslynProtocol.OnAutoInsertResponseItem | null,
38+
never,
39+
void,
40+
RoslynProtocol.OnAutoInsertRegistrationOptions
41+
>(RoslynProtocol.OnAutoInsertRequest.method);
42+
}
43+
fillInitializeParams?: ((params: InitializeParams) => void) | undefined;
44+
preInitialize?:
45+
| ((capabilities: ServerCapabilities<any>, documentSelector: DocumentSelector | undefined) => void)
46+
| undefined;
47+
registrationType: RegistrationType<RoslynProtocol.OnAutoInsertRegistrationOptions>;
48+
register(data: RegistrationData<RoslynProtocol.OnAutoInsertRegistrationOptions>): void {
49+
if (!data.registerOptions.documentSelector) {
50+
return;
51+
}
52+
this._registrations.set(data.id, data);
53+
}
54+
unregister(id: string): void {
55+
const registration = this._registrations.get(id);
56+
if (registration !== undefined) {
57+
this._registrations.delete(id);
58+
}
59+
}
60+
dispose(): void {
61+
this._registrations.clear();
62+
}
63+
64+
public getState(): FeatureState {
65+
const selectors = this.getDocumentSelectors();
66+
67+
let count = 0;
68+
for (const selector of selectors) {
69+
count++;
70+
for (const document of Workspace.textDocuments) {
71+
if (Languages.match(selector, document) > 0) {
72+
return { kind: 'document', id: this.registrationType.method, registrations: true, matches: true };
73+
}
74+
}
75+
}
76+
const registrations = count > 0;
77+
return { kind: 'document', id: this.registrationType.method, registrations, matches: false };
78+
}
79+
80+
public fillClientCapabilities(capabilities: ClientCapabilities): void {
81+
const textDocumentCapabilities: any = ensure(capabilities, 'textDocument')!;
82+
if (textDocumentCapabilities['_vs_onAutoInsert'] === undefined) {
83+
textDocumentCapabilities['_vs_onAutoInsert'] = {} as any;
84+
}
85+
const onAutoInsertCapability = textDocumentCapabilities['_vs_onAutoInsert'];
86+
onAutoInsertCapability.dynamicRegistration = true;
87+
}
88+
89+
public initialize(_capabilities: ServerCapabilities, documentSelector: DocumentSelector): void {
90+
const capabilities: any = _capabilities;
91+
const options = this.getRegistrationOptions(documentSelector, capabilities._vs_onAutoInsertProvider);
92+
if (!options) {
93+
return;
94+
}
95+
this.register({
96+
id: UUID.generateUuid(),
97+
registerOptions: options,
98+
});
99+
}
100+
101+
public getOptions(textDocument: TextDocument): RoslynProtocol.OnAutoInsertOptions | undefined {
102+
for (const registration of this._registrations.values()) {
103+
const selector = registration.registerOptions.documentSelector;
104+
if (
105+
selector !== null &&
106+
Languages.match(this._client.protocol2CodeConverter.asDocumentSelector(selector), textDocument) > 0
107+
) {
108+
return registration.registerOptions;
109+
}
110+
}
111+
return undefined;
112+
}
113+
114+
private *getDocumentSelectors(): IterableIterator<VDocumentSelector> {
115+
for (const registration of this._registrations.values()) {
116+
const selector = registration.registerOptions.documentSelector;
117+
if (selector === null) {
118+
continue;
119+
}
120+
yield this._client.protocol2CodeConverter.asDocumentSelector(selector);
121+
}
122+
}
123+
124+
private getRegistrationOptions(
125+
documentSelector: DocumentSelector | undefined,
126+
capability: undefined | boolean | RoslynProtocol.OnAutoInsertOptions
127+
): (RoslynProtocol.OnAutoInsertRegistrationOptions & { documentSelector: DocumentSelector }) | undefined {
128+
if (!documentSelector || !capability) {
129+
return undefined;
130+
}
131+
return (
132+
Is.boolean(capability) && capability === true
133+
? { documentSelector }
134+
: Object.assign({}, capability, { documentSelector })
135+
) as RoslynProtocol.OnAutoInsertRegistrationOptions & { documentSelector: DocumentSelector };
136+
}
137+
}

src/lsptoolshost/roslynLanguageServer.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ import { IDisposable } from '../disposable';
6262
import { registerNestedCodeActionCommands } from './nestedCodeAction';
6363
import { registerRestoreCommands } from './restore';
6464
import { BuildDiagnosticsService } from './buildDiagnosticsService';
65+
import { OnAutoInsertFeature } from './onAutoInsertFeature';
6566

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

104+
private _onAutoInsertFeature: OnAutoInsertFeature | undefined;
105+
103106
public _buildDiagnosticService: BuildDiagnosticsService;
104107

105108
constructor(
@@ -239,6 +242,9 @@ export class RoslynLanguageServer {
239242

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

245+
server._onAutoInsertFeature = new OnAutoInsertFeature(client);
246+
client.registerFeature(server._onAutoInsertFeature);
247+
242248
// Start the client. This will also launch the server process.
243249
await client.start();
244250
return server;
@@ -476,9 +482,8 @@ export class RoslynLanguageServer {
476482
}
477483
}
478484

479-
public getServerCapabilities(): any {
480-
const capabilities: any = this._languageClient.initializeResult?.capabilities;
481-
return capabilities;
485+
public getOnAutoInsertFeature(): OnAutoInsertFeature | undefined {
486+
return this._onAutoInsertFeature;
482487
}
483488

484489
private static async startServer(

src/lsptoolshost/roslynProtocol.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
import { Command } from 'vscode';
77
import * as lsp from 'vscode-languageserver-protocol';
8-
import { CodeAction } from 'vscode-languageserver-protocol';
8+
import { CodeAction, TextDocumentRegistrationOptions } from 'vscode-languageserver-protocol';
99
import { ProjectConfigurationMessage } from '../shared/projectConfiguration';
1010

1111
export interface WorkspaceDebugConfigurationParams {
@@ -61,6 +61,21 @@ export interface OnAutoInsertResponseItem {
6161
_vs_textEdit: lsp.TextEdit;
6262
}
6363

64+
/**
65+
* OnAutoInsert options.
66+
*/
67+
export interface OnAutoInsertOptions {
68+
/**
69+
* List of characters triggering an {@link OnAutoInsertRequest}.
70+
*/
71+
_vs_triggerCharacters?: string[];
72+
}
73+
74+
/**
75+
* Registration options for an {@link OnAutoInsertRequest}.
76+
*/
77+
export interface OnAutoInsertRegistrationOptions extends TextDocumentRegistrationOptions, OnAutoInsertOptions {}
78+
6479
export interface RegisterSolutionSnapshotResponseItem {
6580
/**
6681
* Represents a solution snapshot.

0 commit comments

Comments
 (0)