Skip to content

Commit 480c98f

Browse files
committed
Add ProjectLoadingRoutingSyntaxTsServer
For #99643 Add a new server option for TypeScript that routes request based on if a project is loading or not The is enabled by the undocumented `"typescript.tsserver.useSeparateSyntaxServer": "dynamic"` setting
1 parent 0184d2f commit 480c98f

4 files changed

Lines changed: 188 additions & 39 deletions

File tree

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

Lines changed: 122 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { Reader } from '../utils/wireProtocol';
1616
import { CallbackMap } from './callbackMap';
1717
import { RequestItem, RequestQueue, RequestQueueingType } from './requestQueue';
1818
import { TypeScriptServerError } from './serverError';
19+
import { EventName } from '../protocol.const';
1920

2021
export interface OngoingRequestCanceller {
2122
tryCancelOngoingRequest(seq: number): boolean;
@@ -309,7 +310,7 @@ class RequestRouter {
309310
]);
310311

311312
constructor(
312-
private readonly servers: ReadonlyArray<{ readonly server: ITypeScriptServer, readonly preferredCommands?: ReadonlySet<keyof TypeScriptRequests> }>,
313+
private readonly servers: ReadonlyArray<{ readonly server: ITypeScriptServer, canRun?(command: keyof TypeScriptRequests): void }>,
313314
private readonly delegate: TsServerDelegate,
314315
) { }
315316

@@ -368,8 +369,8 @@ class RequestRouter {
368369
return firstRequest;
369370
}
370371

371-
for (const { preferredCommands, server } of this.servers) {
372-
if (!preferredCommands || preferredCommands.has(command)) {
372+
for (const { canRun, server } of this.servers) {
373+
if (!canRun || canRun(command)) {
373374
return server.executeImpl(command, args, executeInfo);
374375
}
375376
}
@@ -379,17 +380,17 @@ class RequestRouter {
379380
}
380381

381382

382-
export class SyntaxRoutingTsServer extends Disposable implements ITypeScriptServer {
383+
const syntaxCommands: ReadonlySet<keyof TypeScriptRequests> = new Set<keyof TypeScriptRequests>([
384+
'navtree',
385+
'getOutliningSpans',
386+
'jsxClosingTag',
387+
'selectionRange',
388+
'format',
389+
'formatonkey',
390+
'docCommentTemplate',
391+
]);
383392

384-
private static readonly syntaxCommands = new Set<keyof TypeScriptRequests>([
385-
'navtree',
386-
'getOutliningSpans',
387-
'jsxClosingTag',
388-
'selectionRange',
389-
'format',
390-
'formatonkey',
391-
'docCommentTemplate',
392-
]);
393+
export class SyntaxRoutingTsServer extends Disposable implements ITypeScriptServer {
393394

394395
private readonly syntaxServer: ITypeScriptServer;
395396
private readonly semanticServer: ITypeScriptServer;
@@ -406,8 +407,8 @@ export class SyntaxRoutingTsServer extends Disposable implements ITypeScriptServ
406407

407408
this.router = new RequestRouter(
408409
[
409-
{ server: this.syntaxServer, preferredCommands: SyntaxRoutingTsServer.syntaxCommands },
410-
{ server: this.semanticServer, preferredCommands: undefined /* gets all other commands */ }
410+
{ server: this.syntaxServer, canRun: (command) => syntaxCommands.has(command) },
411+
{ server: this.semanticServer, canRun: undefined /* gets all other commands */ }
411412
],
412413
delegate);
413414

@@ -449,11 +450,11 @@ export class SyntaxRoutingTsServer extends Disposable implements ITypeScriptServ
449450

450451
export class GetErrRoutingTsServer extends Disposable implements ITypeScriptServer {
451452

452-
private static readonly diagnosticEvents = new Set([
453-
'configFileDiag',
454-
'syntaxDiag',
455-
'semanticDiag',
456-
'suggestionDiag'
453+
private static readonly diagnosticEvents = new Set<string>([
454+
EventName.configFileDiag,
455+
EventName.syntaxDiag,
456+
EventName.semanticDiag,
457+
EventName.suggestionDiag
457458
]);
458459

459460
private readonly getErrServer: ITypeScriptServer;
@@ -471,8 +472,8 @@ export class GetErrRoutingTsServer extends Disposable implements ITypeScriptServ
471472

472473
this.router = new RequestRouter(
473474
[
474-
{ server: this.getErrServer, preferredCommands: new Set<keyof TypeScriptRequests>(['geterr', 'geterrForProject']) },
475-
{ server: this.mainServer, preferredCommands: undefined /* gets all other commands */ }
475+
{ server: this.getErrServer, canRun: (command) => ['geterr', 'geterrForProject'].includes(command) },
476+
{ server: this.mainServer, canRun: undefined /* gets all other commands */ }
476477
],
477478
delegate);
478479

@@ -524,6 +525,105 @@ export class GetErrRoutingTsServer extends Disposable implements ITypeScriptServ
524525
}
525526

526527

528+
export class ProjectLoadingRoutingSyntaxTsServer extends Disposable implements ITypeScriptServer {
529+
530+
private static readonly semanticCommands = new Set<keyof TypeScriptRequests>([
531+
'geterr',
532+
'geterrForProject'
533+
]);
534+
535+
private readonly syntaxServer: ITypeScriptServer;
536+
private readonly semanticServer: ITypeScriptServer;
537+
private readonly router: RequestRouter;
538+
539+
private _projectLoading = true;
540+
541+
public constructor(
542+
servers: { syntax: ITypeScriptServer, semantic: ITypeScriptServer },
543+
delegate: TsServerDelegate,
544+
) {
545+
super();
546+
547+
this.syntaxServer = servers.syntax;
548+
this.semanticServer = servers.semantic;
549+
550+
this.router = new RequestRouter(
551+
[
552+
{
553+
server: this.syntaxServer,
554+
canRun: (command) => {
555+
if (syntaxCommands.has(command)) {
556+
return true;
557+
}
558+
if (ProjectLoadingRoutingSyntaxTsServer.semanticCommands.has(command)) {
559+
return false;
560+
}
561+
if (this._projectLoading) {
562+
return true;
563+
}
564+
return false;
565+
}
566+
}, {
567+
server: this.semanticServer,
568+
canRun: undefined /* gets all other commands */
569+
}
570+
],
571+
delegate);
572+
573+
this._register(this.syntaxServer.onEvent(e => {
574+
return this._onEvent.fire(e);
575+
}));
576+
577+
this._register(this.semanticServer.onEvent(e => {
578+
switch (e.event) {
579+
case EventName.projectLoadingStart:
580+
this._projectLoading = true;
581+
break;
582+
583+
case EventName.projectLoadingFinish:
584+
case EventName.semanticDiag:
585+
case EventName.syntaxDiag:
586+
case EventName.suggestionDiag:
587+
case EventName.configFileDiag:
588+
this._projectLoading = false;
589+
break;
590+
}
591+
return this._onEvent.fire(e);
592+
}));
593+
594+
this._register(this.semanticServer.onExit(e => {
595+
this._onExit.fire(e);
596+
this.syntaxServer.kill();
597+
}));
598+
599+
this._register(this.semanticServer.onError(e => this._onError.fire(e)));
600+
}
601+
602+
private readonly _onEvent = this._register(new vscode.EventEmitter<Proto.Event>());
603+
public readonly onEvent = this._onEvent.event;
604+
605+
private readonly _onExit = this._register(new vscode.EventEmitter<any>());
606+
public readonly onExit = this._onExit.event;
607+
608+
private readonly _onError = this._register(new vscode.EventEmitter<any>());
609+
public readonly onError = this._onError.event;
610+
611+
public get onReaderError() { return this.semanticServer.onReaderError; }
612+
613+
public get tsServerLogFile() { return this.semanticServer.tsServerLogFile; }
614+
615+
public kill(): void {
616+
this.syntaxServer.kill();
617+
this.semanticServer.kill();
618+
}
619+
620+
public executeImpl(command: keyof TypeScriptRequests, args: any, executeInfo: { isAsync: boolean, token?: vscode.CancellationToken, expectsResult: false, lowPriority?: boolean }): undefined;
621+
public executeImpl(command: keyof TypeScriptRequests, args: any, executeInfo: { isAsync: boolean, token?: vscode.CancellationToken, expectsResult: boolean, lowPriority?: boolean }): Promise<ServerResponse.Response<Proto.Response>>;
622+
public executeImpl(command: keyof TypeScriptRequests, args: any, executeInfo: { isAsync: boolean, token?: vscode.CancellationToken, expectsResult: boolean, lowPriority?: boolean }): Promise<ServerResponse.Response<Proto.Response>> | undefined {
623+
return this.router.execute(command, args, executeInfo);
624+
}
625+
}
626+
527627
namespace RequestState {
528628
export const enum Type { Unresolved, Resolved, Errored }
529629

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

Lines changed: 47 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import * as stream from 'stream';
99
import * as vscode from 'vscode';
1010
import type * as Proto from '../protocol';
1111
import API from '../utils/api';
12-
import { TsServerLogLevel, TypeScriptServiceConfiguration } from '../utils/configuration';
12+
import { TsServerLogLevel, TypeScriptServiceConfiguration, SeparateSyntaxServerConfigration } from '../utils/configuration';
1313
import * as electron from '../utils/electron';
1414
import LogDirectoryProvider from '../utils/logDirectoryProvider';
1515
import Logger from '../utils/logger';
@@ -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, PipeRequestCanceller, ProcessBasedTsServer, SyntaxRoutingTsServer, TsServerProcess, TsServerDelegate, GetErrRoutingTsServer } from './server';
21+
import { ITypeScriptServer, PipeRequestCanceller, ProcessBasedTsServer, SyntaxRoutingTsServer, TsServerProcess, TsServerDelegate, GetErrRoutingTsServer, ProjectLoadingRoutingSyntaxTsServer } from './server';
2222

2323
const enum ServerKind {
2424
Main = 'main',
@@ -27,6 +27,17 @@ const enum ServerKind {
2727
Diagnostics = 'diagnostics'
2828
}
2929

30+
const enum CompositeServerType {
31+
/** Run a single server that handles all commands */
32+
Single,
33+
34+
/** Run a separate server for syntax commands */
35+
SeparateSyntax,
36+
37+
/** Use a separate suntax server while the project is loading */
38+
DynamicSeparateSyntax,
39+
}
40+
3041
export class TypeScriptServerSpawner {
3142
public constructor(
3243
private readonly _versionProvider: TypeScriptVersionProvider,
@@ -44,13 +55,28 @@ export class TypeScriptServerSpawner {
4455
delegate: TsServerDelegate,
4556
): ITypeScriptServer {
4657
let primaryServer: ITypeScriptServer;
47-
if (this.shouldUseSeparateSyntaxServer(version, configuration)) {
48-
primaryServer = new SyntaxRoutingTsServer({
49-
syntax: this.spawnTsServer(ServerKind.Syntax, version, configuration, pluginManager),
50-
semantic: this.spawnTsServer(ServerKind.Semantic, version, configuration, pluginManager)
51-
}, delegate);
52-
} else {
53-
primaryServer = this.spawnTsServer(ServerKind.Main, version, configuration, pluginManager);
58+
switch (this.getCompositeServerType(version, configuration)) {
59+
case CompositeServerType.SeparateSyntax:
60+
{
61+
primaryServer = new SyntaxRoutingTsServer({
62+
syntax: this.spawnTsServer(ServerKind.Syntax, version, configuration, pluginManager),
63+
semantic: this.spawnTsServer(ServerKind.Semantic, version, configuration, pluginManager)
64+
}, delegate);
65+
break;
66+
}
67+
case CompositeServerType.DynamicSeparateSyntax:
68+
{
69+
primaryServer = new ProjectLoadingRoutingSyntaxTsServer({
70+
syntax: this.spawnTsServer(ServerKind.Syntax, version, configuration, pluginManager),
71+
semantic: this.spawnTsServer(ServerKind.Semantic, version, configuration, pluginManager)
72+
}, delegate);
73+
break;
74+
}
75+
case CompositeServerType.Single:
76+
{
77+
primaryServer = this.spawnTsServer(ServerKind.Main, version, configuration, pluginManager);
78+
break;
79+
}
5480
}
5581

5682
if (this.shouldUseSeparateDiagnosticsServer(configuration)) {
@@ -63,11 +89,20 @@ export class TypeScriptServerSpawner {
6389
return primaryServer;
6490
}
6591

66-
private shouldUseSeparateSyntaxServer(
92+
private getCompositeServerType(
6793
version: TypeScriptVersion,
6894
configuration: TypeScriptServiceConfiguration,
69-
): boolean {
70-
return configuration.useSeparateSyntaxServer && !!version.apiVersion && version.apiVersion.gte(API.v340);
95+
): CompositeServerType {
96+
switch (configuration.separateSyntaxServer) {
97+
case SeparateSyntaxServerConfigration.Disabled:
98+
return CompositeServerType.Single;
99+
100+
case SeparateSyntaxServerConfigration.Enabled:
101+
return version.apiVersion?.gte(API.v340) ? CompositeServerType.SeparateSyntax : CompositeServerType.Single;
102+
103+
case SeparateSyntaxServerConfigration.Dynamic:
104+
return version.apiVersion?.gte(API.v400) ? CompositeServerType.DynamicSeparateSyntax : CompositeServerType.Single;
105+
}
71106
}
72107

73108
private shouldUseSeparateDiagnosticsServer(

extensions/typescript-language-features/src/utils/api.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ export default class API {
3434
public static readonly v380 = API.fromSimpleString('3.8.0');
3535
public static readonly v381 = API.fromSimpleString('3.8.1');
3636
public static readonly v390 = API.fromSimpleString('3.9.0');
37+
public static readonly v400 = API.fromSimpleString('4.0.0');
3738

3839
public static fromVersionString(versionString: string): API {
3940
let version = semver.valid(versionString);

extensions/typescript-language-features/src/utils/configuration.ts

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,12 @@ export namespace TsServerLogLevel {
4646
}
4747
}
4848

49+
export const enum SeparateSyntaxServerConfigration {
50+
Disabled,
51+
Enabled,
52+
Dynamic,
53+
}
54+
4955
export class TypeScriptServiceConfiguration {
5056
public readonly locale: string | null;
5157
public readonly globalTsdk: string | null;
@@ -56,7 +62,7 @@ export class TypeScriptServiceConfiguration {
5662
public readonly checkJs: boolean;
5763
public readonly experimentalDecorators: boolean;
5864
public readonly disableAutomaticTypeAcquisition: boolean;
59-
public readonly useSeparateSyntaxServer: boolean;
65+
public readonly separateSyntaxServer: SeparateSyntaxServerConfigration;
6066
public readonly enableProjectDiagnostics: boolean;
6167
public readonly maxTsServerMemory: number;
6268
public readonly enablePromptUseWorkspaceTsdk: boolean;
@@ -78,7 +84,7 @@ export class TypeScriptServiceConfiguration {
7884
this.checkJs = TypeScriptServiceConfiguration.readCheckJs(configuration);
7985
this.experimentalDecorators = TypeScriptServiceConfiguration.readExperimentalDecorators(configuration);
8086
this.disableAutomaticTypeAcquisition = TypeScriptServiceConfiguration.readDisableAutomaticTypeAcquisition(configuration);
81-
this.useSeparateSyntaxServer = TypeScriptServiceConfiguration.readUseSeparateSyntaxServer(configuration);
87+
this.separateSyntaxServer = TypeScriptServiceConfiguration.readUseSeparateSyntaxServer(configuration);
8288
this.enableProjectDiagnostics = TypeScriptServiceConfiguration.readEnableProjectDiagnostics(configuration);
8389
this.maxTsServerMemory = TypeScriptServiceConfiguration.readMaxTsServerMemory(configuration);
8490
this.enablePromptUseWorkspaceTsdk = TypeScriptServiceConfiguration.readEnablePromptUseWorkspaceTsdk(configuration);
@@ -95,7 +101,7 @@ export class TypeScriptServiceConfiguration {
95101
&& this.experimentalDecorators === other.experimentalDecorators
96102
&& this.disableAutomaticTypeAcquisition === other.disableAutomaticTypeAcquisition
97103
&& arrays.equals(this.tsServerPluginPaths, other.tsServerPluginPaths)
98-
&& this.useSeparateSyntaxServer === other.useSeparateSyntaxServer
104+
&& this.separateSyntaxServer === other.separateSyntaxServer
99105
&& this.enableProjectDiagnostics === other.enableProjectDiagnostics
100106
&& this.maxTsServerMemory === other.maxTsServerMemory
101107
&& objects.equals(this.watchOptions, other.watchOptions)
@@ -157,8 +163,15 @@ export class TypeScriptServiceConfiguration {
157163
return configuration.get<string | null>('typescript.locale', null);
158164
}
159165

160-
private static readUseSeparateSyntaxServer(configuration: vscode.WorkspaceConfiguration): boolean {
161-
return configuration.get<boolean>('typescript.tsserver.useSeparateSyntaxServer', true);
166+
private static readUseSeparateSyntaxServer(configuration: vscode.WorkspaceConfiguration): SeparateSyntaxServerConfigration {
167+
const value = configuration.get('typescript.tsserver.useSeparateSyntaxServer', true);
168+
if (value === true) {
169+
return SeparateSyntaxServerConfigration.Enabled;
170+
}
171+
if (value === 'dynamic') {
172+
return SeparateSyntaxServerConfigration.Dynamic;
173+
}
174+
return SeparateSyntaxServerConfigration.Disabled;
162175
}
163176

164177
private static readEnableProjectDiagnostics(configuration: vscode.WorkspaceConfiguration): boolean {

0 commit comments

Comments
 (0)