Skip to content

Commit e726f27

Browse files
authored
Merge pull request #7033 from mgoertz-msft/dev/mgoertz/autoinsert
Created OnAutoInsertFeature to support dynamic capability registration
2 parents 7166bf7 + 70f1b0d commit e726f27

File tree

4 files changed

+179
-25
lines changed

4 files changed

+179
-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: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
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 UUID from 'vscode-languageclient/lib/common/utils/uuid';
26+
27+
export class OnAutoInsertFeature implements DynamicFeature<RoslynProtocol.OnAutoInsertRegistrationOptions> {
28+
private readonly _client: LanguageClient;
29+
private readonly _registrations: Map<string, RegistrationData<RoslynProtocol.OnAutoInsertRegistrationOptions>>;
30+
31+
constructor(client: LanguageClient) {
32+
this._client = client;
33+
this._registrations = new Map();
34+
this.registrationType = new ProtocolRequestType<
35+
RoslynProtocol.OnAutoInsertParams,
36+
RoslynProtocol.OnAutoInsertResponseItem | null,
37+
never,
38+
void,
39+
RoslynProtocol.OnAutoInsertRegistrationOptions
40+
>(RoslynProtocol.OnAutoInsertRequest.method);
41+
}
42+
fillInitializeParams?: ((params: InitializeParams) => void) | undefined;
43+
preInitialize?:
44+
| ((capabilities: ServerCapabilities<any>, documentSelector: DocumentSelector | undefined) => void)
45+
| undefined;
46+
registrationType: RegistrationType<RoslynProtocol.OnAutoInsertRegistrationOptions>;
47+
register(data: RegistrationData<RoslynProtocol.OnAutoInsertRegistrationOptions>): void {
48+
if (!data.registerOptions.documentSelector) {
49+
return;
50+
}
51+
this._registrations.set(data.id, data);
52+
}
53+
unregister(id: string): void {
54+
const registration = this._registrations.get(id);
55+
if (registration !== undefined) {
56+
this._registrations.delete(id);
57+
}
58+
}
59+
dispose(): void {
60+
this._registrations.clear();
61+
}
62+
63+
public getState(): FeatureState {
64+
const selectors = this.getDocumentSelectors();
65+
66+
let count = 0;
67+
for (const selector of selectors) {
68+
count++;
69+
for (const document of Workspace.textDocuments) {
70+
if (Languages.match(selector, document) > 0) {
71+
return { kind: 'document', id: this.registrationType.method, registrations: true, matches: true };
72+
}
73+
}
74+
}
75+
const registrations = count > 0;
76+
return { kind: 'document', id: this.registrationType.method, registrations, matches: false };
77+
}
78+
79+
public fillClientCapabilities(capabilities: ClientCapabilities): void {
80+
const textDocumentCapabilities: any = ensure(capabilities, 'textDocument')!;
81+
if (textDocumentCapabilities['_vs_onAutoInsert'] === undefined) {
82+
textDocumentCapabilities['_vs_onAutoInsert'] = {} as any;
83+
}
84+
const onAutoInsertCapability = textDocumentCapabilities['_vs_onAutoInsert'];
85+
onAutoInsertCapability.dynamicRegistration = true;
86+
}
87+
88+
public initialize(_capabilities: ServerCapabilities, documentSelector: DocumentSelector): void {
89+
const capabilities: any = _capabilities;
90+
const options = this.getRegistrationOptions(documentSelector, capabilities._vs_onAutoInsertProvider);
91+
if (!options) {
92+
return;
93+
}
94+
this.register({
95+
id: UUID.generateUuid(),
96+
registerOptions: options,
97+
});
98+
}
99+
100+
public getOptions(textDocument: TextDocument): RoslynProtocol.OnAutoInsertOptions | undefined {
101+
for (const registration of this._registrations.values()) {
102+
const selector = registration.registerOptions.documentSelector;
103+
if (
104+
selector !== null &&
105+
Languages.match(this._client.protocol2CodeConverter.asDocumentSelector(selector), textDocument) > 0
106+
) {
107+
return registration.registerOptions;
108+
}
109+
}
110+
return undefined;
111+
}
112+
113+
private *getDocumentSelectors(): IterableIterator<VDocumentSelector> {
114+
for (const registration of this._registrations.values()) {
115+
const selector = registration.registerOptions.documentSelector;
116+
if (selector === null) {
117+
continue;
118+
}
119+
yield this._client.protocol2CodeConverter.asDocumentSelector(selector);
120+
}
121+
}
122+
123+
private getRegistrationOptions(
124+
documentSelector: DocumentSelector | undefined,
125+
capability: undefined | RoslynProtocol.OnAutoInsertOptions
126+
): (RoslynProtocol.OnAutoInsertRegistrationOptions & { documentSelector: DocumentSelector }) | undefined {
127+
if (!documentSelector || !capability) {
128+
return undefined;
129+
}
130+
return Object.assign({}, capability, { documentSelector }) as RoslynProtocol.OnAutoInsertRegistrationOptions & {
131+
documentSelector: DocumentSelector;
132+
};
133+
}
134+
}

src/lsptoolshost/roslynLanguageServer.ts

Lines changed: 9 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+
public readonly _onAutoInsertFeature: OnAutoInsertFeature;
105+
103106
public _buildDiagnosticService: BuildDiagnosticsService;
104107

105108
constructor(
@@ -126,6 +129,8 @@ export class RoslynLanguageServer {
126129
this.registerDebuggerAttach();
127130

128131
registerShowToastNotification(this._languageClient);
132+
133+
this._onAutoInsertFeature = new OnAutoInsertFeature(this._languageClient);
129134
}
130135

131136
private registerSetTrace() {
@@ -239,6 +244,8 @@ export class RoslynLanguageServer {
239244

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

247+
client.registerFeature(server._onAutoInsertFeature);
248+
242249
// Start the client. This will also launch the server process.
243250
await client.start();
244251
return server;
@@ -476,9 +483,8 @@ export class RoslynLanguageServer {
476483
}
477484
}
478485

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

484490
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)