Skip to content

Commit 87b8402

Browse files
committed
Add experimental dual TS server
Fixes microsoft#75866
1 parent 8ec2559 commit 87b8402

8 files changed

Lines changed: 152 additions & 51 deletions

File tree

extensions/typescript-language-features/package.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -591,6 +591,12 @@
591591
"default": true,
592592
"description": "%configuration.surveys.enabled%",
593593
"scope": "window"
594+
},
595+
"typescript.experimental.useSeparateSyntaxServer": {
596+
"type": "boolean",
597+
"default": false,
598+
"description": "%configuration.experimental.useSeparateSyntaxServer%",
599+
"scope": "window"
594600
}
595601
}
596602
},

extensions/typescript-language-features/package.nls.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
"typescript.problemMatchers.tsc.label": "TypeScript problems",
5050
"typescript.problemMatchers.tscWatch.label": "TypeScript problems (watch mode)",
5151
"configuration.suggest.paths": "Enable/disable suggestions for paths in import statements and require calls.",
52+
"configuration.experimental.useSeparateSyntaxServer": "Enable/disable spawning a separate TypeScript server that can more quickly respond to syntax related operations, such as calculating folding or computing document symbols. Note that you must restart the TypeScript server after changing this setting. Requires using TypeScript 3.4.0 or newer in the workspace.",
5253
"typescript.locale": "Sets the locale used to report JavaScript and TypeScript errors. Requires using TypeScript 2.6.0 or newer in the workspace. Default of `null` uses VS Code's locale.",
5354
"javascript.implicitProjectConfig.experimentalDecorators": "Enable/disable `experimentalDecorators` for JavaScript files that are not part of a project. Existing jsconfig.json or tsconfig.json files override this setting. Requires using TypeScript 2.3.1 or newer in the workspace.",
5455
"configuration.suggest.autoImports": "Enable/disable auto import suggestions. Requires using TypeScript 2.6.1 or newer in the workspace.",

extensions/typescript-language-features/src/test/server.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ suite('Server', () => {
6262

6363
test('should send requests with increasing sequence numbers', async () => {
6464
const process = new FakeServerProcess();
65-
const server = new ProcessBasedTsServer(process, undefined, new PipeRequestCanceller(undefined, tracer), undefined!, NoopTelemetryReporter, tracer);
65+
const server = new ProcessBasedTsServer('semantic', process, undefined, new PipeRequestCanceller('semantic', undefined, tracer), undefined!, NoopTelemetryReporter, tracer);
6666

6767
const onWrite1 = process.onWrite();
6868
server.executeImpl('geterr', {}, { isAsync: false, token: nulToken, expectsResult: true });

extensions/typescript-language-features/src/tsServer/server.ts

Lines changed: 70 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import * as fs from 'fs';
77
import * as stream from 'stream';
88
import * as vscode from 'vscode';
99
import * as Proto from '../protocol';
10-
import { ServerResponse } from '../typescriptService';
10+
import { ServerResponse, TypeScriptRequests } from '../typescriptService';
1111
import { Disposable } from '../utils/dispose';
1212
import TelemetryReporter from '../utils/telemetry';
1313
import Tracer from '../utils/tracer';
@@ -23,6 +23,7 @@ export interface OngoingRequestCanceller {
2323

2424
export class PipeRequestCanceller implements OngoingRequestCanceller {
2525
public constructor(
26+
private readonly _serverId: string,
2627
private readonly _cancellationPipeName: string | undefined,
2728
private readonly _tracer: Tracer,
2829
) { }
@@ -31,7 +32,7 @@ export class PipeRequestCanceller implements OngoingRequestCanceller {
3132
if (!this._cancellationPipeName) {
3233
return false;
3334
}
34-
this._tracer.logTrace(`TypeScript Server: trying to cancel ongoing request with sequence number ${seq}`);
35+
this._tracer.logTrace(this._serverId, `TypeScript Server: trying to cancel ongoing request with sequence number ${seq}`);
3536
try {
3637
fs.writeFileSync(this._cancellationPipeName + seq, '');
3738
} catch {
@@ -51,9 +52,9 @@ export interface ITypeScriptServer {
5152

5253
kill(): void;
5354

54-
executeImpl(command: string, args: any, executeInfo: { isAsync: boolean, token?: vscode.CancellationToken, expectsResult: false, lowPriority?: boolean }): undefined;
55-
executeImpl(command: string, args: any, executeInfo: { isAsync: boolean, token?: vscode.CancellationToken, expectsResult: boolean, lowPriority?: boolean }): Promise<ServerResponse.Response<Proto.Response>>;
56-
executeImpl(command: string, args: any, executeInfo: { isAsync: boolean, token?: vscode.CancellationToken, expectsResult: boolean, lowPriority?: boolean }): Promise<ServerResponse.Response<Proto.Response>> | undefined;
55+
executeImpl(command: keyof TypeScriptRequests, args: any, executeInfo: { isAsync: boolean, token?: vscode.CancellationToken, expectsResult: false, lowPriority?: boolean }): undefined;
56+
executeImpl(command: keyof TypeScriptRequests, args: any, executeInfo: { isAsync: boolean, token?: vscode.CancellationToken, expectsResult: boolean, lowPriority?: boolean }): Promise<ServerResponse.Response<Proto.Response>>;
57+
executeImpl(command: keyof TypeScriptRequests, args: any, executeInfo: { isAsync: boolean, token?: vscode.CancellationToken, expectsResult: boolean, lowPriority?: boolean }): Promise<ServerResponse.Response<Proto.Response>> | undefined;
5758

5859
dispose(): void;
5960
}
@@ -75,6 +76,7 @@ export class ProcessBasedTsServer extends Disposable implements ITypeScriptServe
7576
private readonly _pendingResponses = new Set<number>();
7677

7778
constructor(
79+
private readonly _serverId: string,
7880
private readonly _process: TsServerProcess,
7981
private readonly _tsServerLogFile: string | undefined,
8082
private readonly _requestCanceller: OngoingRequestCanceller,
@@ -136,11 +138,11 @@ export class ProcessBasedTsServer extends Disposable implements ITypeScriptServe
136138
const seq = (event as Proto.RequestCompletedEvent).body.request_seq;
137139
const p = this._callbacks.fetch(seq);
138140
if (p) {
139-
this._tracer.traceRequestCompleted('requestCompleted', seq, p.startTime);
141+
this._tracer.traceRequestCompleted(this._serverId, 'requestCompleted', seq, p.startTime);
140142
p.onSuccess(undefined);
141143
}
142144
} else {
143-
this._tracer.traceEvent(event);
145+
this._tracer.traceEvent(this._serverId, event);
144146
this._onEvent.fire(event);
145147
}
146148
break;
@@ -156,15 +158,15 @@ export class ProcessBasedTsServer extends Disposable implements ITypeScriptServe
156158
private tryCancelRequest(seq: number, command: string): boolean {
157159
try {
158160
if (this._requestQueue.tryDeletePendingRequest(seq)) {
159-
this._tracer.logTrace(`TypeScript Server: canceled request with sequence number ${seq}`);
161+
this.logTrace(`Canceled request with sequence number ${seq}`);
160162
return true;
161163
}
162164

163165
if (this._requestCanceller.tryCancelOngoingRequest(seq)) {
164166
return true;
165167
}
166168

167-
this._tracer.logTrace(`TypeScript Server: tried to cancel request with sequence number ${seq}. But request got already delivered.`);
169+
this.logTrace(`Tried to cancel request with sequence number ${seq}. But request got already delivered.`);
168170
return false;
169171
} finally {
170172
const callback = this.fetchCallback(seq);
@@ -180,7 +182,7 @@ export class ProcessBasedTsServer extends Disposable implements ITypeScriptServe
180182
return;
181183
}
182184

183-
this._tracer.traceResponse(response, callback.startTime);
185+
this._tracer.traceResponse(this._serverId, response, callback.startTime);
184186
if (response.success) {
185187
callback.onSuccess(response);
186188
} else if (response.message === 'No content available.') {
@@ -191,9 +193,9 @@ export class ProcessBasedTsServer extends Disposable implements ITypeScriptServe
191193
}
192194
}
193195

194-
public executeImpl(command: string, args: any, executeInfo: { isAsync: boolean, token?: vscode.CancellationToken, expectsResult: false, lowPriority?: boolean }): undefined;
195-
public executeImpl(command: string, args: any, executeInfo: { isAsync: boolean, token?: vscode.CancellationToken, expectsResult: boolean, lowPriority?: boolean }): Promise<ServerResponse.Response<Proto.Response>>;
196-
public executeImpl(command: string, args: any, executeInfo: { isAsync: boolean, token?: vscode.CancellationToken, expectsResult: boolean, lowPriority?: boolean }): Promise<ServerResponse.Response<Proto.Response>> | undefined {
196+
public executeImpl(command: keyof TypeScriptRequests, args: any, executeInfo: { isAsync: boolean, token?: vscode.CancellationToken, expectsResult: false, lowPriority?: boolean }): undefined;
197+
public executeImpl(command: keyof TypeScriptRequests, args: any, executeInfo: { isAsync: boolean, token?: vscode.CancellationToken, expectsResult: boolean, lowPriority?: boolean }): Promise<ServerResponse.Response<Proto.Response>>;
198+
public executeImpl(command: keyof TypeScriptRequests, args: any, executeInfo: { isAsync: boolean, token?: vscode.CancellationToken, expectsResult: boolean, lowPriority?: boolean }): Promise<ServerResponse.Response<Proto.Response>> | undefined {
197199
const request = this._requestQueue.createRequest(command, args);
198200
const requestInfo: RequestItem = {
199201
request,
@@ -255,7 +257,7 @@ export class ProcessBasedTsServer extends Disposable implements ITypeScriptServe
255257

256258
private sendRequest(requestItem: RequestItem): void {
257259
const serverRequest = requestItem.request;
258-
this._tracer.traceRequest(serverRequest, requestItem.expectsResponse, this._requestQueue.length);
260+
this._tracer.traceRequest(this._serverId, serverRequest, requestItem.expectsResponse, this._requestQueue.length);
259261

260262
if (requestItem.expectsResponse && !requestItem.isAsync) {
261263
this._pendingResponses.add(requestItem.request.seq);
@@ -281,6 +283,10 @@ export class ProcessBasedTsServer extends Disposable implements ITypeScriptServe
281283
return callback;
282284
}
283285

286+
private logTrace(message: string) {
287+
this._tracer.logTrace(this._serverId, message);
288+
}
289+
284290
private static readonly fenceCommands = new Set(['change', 'close', 'open', 'updateOpen']);
285291

286292
private static getQueueingType(
@@ -294,3 +300,53 @@ export class ProcessBasedTsServer extends Disposable implements ITypeScriptServe
294300
}
295301
}
296302

303+
304+
export class SyntaxRoutingTsServer extends Disposable implements ITypeScriptServer {
305+
public constructor(
306+
private readonly syntaxServer: ITypeScriptServer,
307+
private readonly semanticServer: ITypeScriptServer,
308+
) {
309+
super();
310+
311+
this._register(syntaxServer.onEvent(e => this._onEvent.fire(e)));
312+
this._register(semanticServer.onEvent(e => this._onEvent.fire(e)));
313+
314+
this._register(semanticServer.onExit(e => this._onExit.fire(e)));
315+
this._register(semanticServer.onError(e => this._onError.fire(e)));
316+
}
317+
318+
private readonly _onEvent = this._register(new vscode.EventEmitter<Proto.Event>());
319+
public readonly onEvent = this._onEvent.event;
320+
321+
private readonly _onExit = this._register(new vscode.EventEmitter<any>());
322+
public readonly onExit = this._onExit.event;
323+
324+
private readonly _onError = this._register(new vscode.EventEmitter<any>());
325+
public readonly onError = this._onError.event;
326+
327+
public get onReaderError() { return this.semanticServer.onReaderError; }
328+
329+
public get tsServerLogFile() { return this.semanticServer.tsServerLogFile; }
330+
331+
public kill(): void {
332+
this.syntaxServer.kill();
333+
this.semanticServer.kill();
334+
}
335+
336+
private static readonly syntaxCommands = new Set<keyof TypeScriptRequests>(['navtree', 'getOutliningSpans', 'jsxClosingTag', 'selectionRange']);
337+
private static readonly sharedCommands = new Set<keyof TypeScriptRequests>(['change', 'close', 'open', 'updateOpen', 'configure', 'configurePlugin']);
338+
339+
public executeImpl(command: keyof TypeScriptRequests, args: any, executeInfo: { isAsync: boolean, token?: vscode.CancellationToken, expectsResult: false, lowPriority?: boolean }): undefined;
340+
public executeImpl(command: keyof TypeScriptRequests, args: any, executeInfo: { isAsync: boolean, token?: vscode.CancellationToken, expectsResult: boolean, lowPriority?: boolean }): Promise<ServerResponse.Response<Proto.Response>>;
341+
public executeImpl(command: keyof TypeScriptRequests, args: any, executeInfo: { isAsync: boolean, token?: vscode.CancellationToken, expectsResult: boolean, lowPriority?: boolean }): Promise<ServerResponse.Response<Proto.Response>> | undefined {
342+
343+
if (SyntaxRoutingTsServer.syntaxCommands.has(command)) {
344+
return this.syntaxServer.executeImpl(command, args, executeInfo);
345+
} else if (SyntaxRoutingTsServer.sharedCommands.has(command)) {
346+
this.syntaxServer.executeImpl(command, args, executeInfo);
347+
return this.semanticServer.executeImpl(command, args, executeInfo);
348+
} else {
349+
return this.semanticServer.executeImpl(command, args, executeInfo);
350+
}
351+
}
352+
}

extensions/typescript-language-features/src/tsServer/spanwer.ts

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import { PluginManager } from '../utils/plugins';
1818
import TelemetryReporter from '../utils/telemetry';
1919
import Tracer from '../utils/tracer';
2020
import { TypeScriptVersion, TypeScriptVersionProvider } from '../utils/versionProvider';
21-
import { ITypeScriptServer, TsServerProcess, ProcessBasedTsServer, PipeRequestCanceller } from './server';
21+
import { ITypeScriptServer, PipeRequestCanceller, ProcessBasedTsServer, SyntaxRoutingTsServer, TsServerProcess } from './server';
2222

2323
export class TypeScriptServerSpawner {
2424
public constructor(
@@ -34,27 +34,52 @@ export class TypeScriptServerSpawner {
3434
version: TypeScriptVersion,
3535
configuration: TypeScriptServiceConfiguration,
3636
pluginManager: PluginManager
37+
): ITypeScriptServer {
38+
if (this.shouldUserSeparateSyntaxServer(version)) {
39+
const syntaxServer = this.spawnProcessBasedTsServer('syntax', version, configuration, pluginManager, ['--syntaxOnly', '--disableAutomaticTypingAcquisition']);
40+
const semanticServer = this.spawnProcessBasedTsServer('semantic', version, configuration, pluginManager, []);
41+
return new SyntaxRoutingTsServer(syntaxServer, semanticServer);
42+
}
43+
44+
return this.spawnProcessBasedTsServer('main', version, configuration, pluginManager, []);
45+
}
46+
47+
private shouldUserSeparateSyntaxServer(version: TypeScriptVersion): boolean {
48+
if (!version.version || version.version.lt(API.v340)) {
49+
return false;
50+
}
51+
return vscode.workspace.getConfiguration('typescript')
52+
.get<boolean>('experimental.useSeparateSyntaxServer', false);
53+
}
54+
55+
private spawnProcessBasedTsServer(
56+
serverId: string,
57+
version: TypeScriptVersion,
58+
configuration: TypeScriptServiceConfiguration,
59+
pluginManager: PluginManager,
60+
extraForkArgs: readonly string[],
3761
): ITypeScriptServer {
3862
const apiVersion = version.version || API.defaultVersion;
3963

4064
const { args, cancellationPipeName, tsServerLogFile } = this.getTsServerArgs(configuration, version, apiVersion, pluginManager);
4165

4266
if (TypeScriptServerSpawner.isLoggingEnabled(apiVersion, configuration)) {
4367
if (tsServerLogFile) {
44-
this._logger.info(`TSServer log file: ${tsServerLogFile}`);
68+
this._logger.info(`<${serverId}> Log file: ${tsServerLogFile}`);
4569
} else {
46-
this._logger.error('Could not create TSServer log directory');
70+
this._logger.error(`<${serverId}> Could not create log directory`);
4771
}
4872
}
4973

50-
this._logger.info('Forking TSServer');
51-
const childProcess = electron.fork(version.tsServerPath, args, this.getForkOptions());
52-
this._logger.info('Started TSServer');
74+
this._logger.info(`<${serverId}> Forking...`);
75+
const childProcess = electron.fork(version.tsServerPath, [...args, ...extraForkArgs], this.getForkOptions());
76+
this._logger.info(`<${serverId}> Starting...`);
5377

5478
return new ProcessBasedTsServer(
79+
serverId,
5580
new ChildServerProcess(childProcess),
5681
tsServerLogFile,
57-
new PipeRequestCanceller(cancellationPipeName, this._tracer),
82+
new PipeRequestCanceller(serverId, cancellationPipeName, this._tracer),
5883
version,
5984
this._telemetryReporter,
6085
this._tracer);

extensions/typescript-language-features/src/typescriptService.ts

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ export namespace ServerResponse {
2626
export type Response<T extends Proto.Response> = T | Cancelled | typeof NoContent;
2727
}
2828

29-
export interface TypeScriptRequestTypes {
29+
interface StandardTsServerRequests {
3030
'applyCodeActionCommand': [Proto.ApplyCodeActionCommandRequestArgs, Proto.ApplyCodeActionCommandResponse];
3131
'completionEntryDetails': [Proto.CompletionDetailsRequestArgs, Proto.CompletionDetailsResponse];
3232
'completionInfo': [Proto.CompletionsRequestArgs, Proto.CompletionInfoResponse];
@@ -59,6 +59,22 @@ export interface TypeScriptRequestTypes {
5959
'typeDefinition': [Proto.FileLocationRequestArgs, Proto.TypeDefinitionResponse];
6060
}
6161

62+
interface NoResponseTsServerRequests {
63+
'open': [Proto.OpenRequestArgs, null];
64+
'close': [Proto.FileRequestArgs];
65+
'change': [Proto.ChangeRequestArgs, null];
66+
'updateOpen': [Proto.UpdateOpenRequestArgs, null];
67+
'compilerOptionsForInferredProjects': [Proto.SetCompilerOptionsForInferredProjectsArgs, null];
68+
'reloadProjects': [null, null];
69+
'configurePlugin': [Proto.ConfigurePluginRequest, Proto.ConfigurePluginResponse];
70+
}
71+
72+
interface AsyncTsServerRequests {
73+
'geterr': [Proto.GeterrRequestArgs, Proto.Response];
74+
}
75+
76+
export type TypeScriptRequests = StandardTsServerRequests & NoResponseTsServerRequests & AsyncTsServerRequests;
77+
6278
export interface ITypeScriptServiceClient {
6379
/**
6480
* Convert a resource (VS Code) to a normalized path (TypeScript).
@@ -100,19 +116,17 @@ export interface ITypeScriptServiceClient {
100116
readonly logger: Logger;
101117
readonly bufferSyncSupport: BufferSyncSupport;
102118

103-
execute<K extends keyof TypeScriptRequestTypes>(
119+
execute<K extends keyof StandardTsServerRequests>(
104120
command: K,
105-
args: TypeScriptRequestTypes[K][0],
121+
args: StandardTsServerRequests[K][0],
106122
token: vscode.CancellationToken,
107123
lowPriority?: boolean
108-
): Promise<ServerResponse.Response<TypeScriptRequestTypes[K][1]>>;
109-
110-
executeWithoutWaitingForResponse(command: 'open', args: Proto.OpenRequestArgs): void;
111-
executeWithoutWaitingForResponse(command: 'close', args: Proto.FileRequestArgs): void;
112-
executeWithoutWaitingForResponse(command: 'change', args: Proto.ChangeRequestArgs): void;
113-
executeWithoutWaitingForResponse(command: 'updateOpen', args: Proto.UpdateOpenRequestArgs): void;
114-
executeWithoutWaitingForResponse(command: 'compilerOptionsForInferredProjects', args: Proto.SetCompilerOptionsForInferredProjectsArgs): void;
115-
executeWithoutWaitingForResponse(command: 'reloadProjects', args: null): void;
124+
): Promise<ServerResponse.Response<StandardTsServerRequests[K][1]>>;
125+
126+
executeWithoutWaitingForResponse<K extends keyof NoResponseTsServerRequests>(
127+
command: K,
128+
args: NoResponseTsServerRequests[K][0]
129+
): void;
116130

117131
executeAsync(command: 'geterr', args: Proto.GeterrRequestArgs, token: vscode.CancellationToken): Promise<ServerResponse.Response<Proto.Response>>;
118132

0 commit comments

Comments
 (0)