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
This commit is contained in:
Matt Bierner 2020-06-08 16:46:47 -07:00
parent 0184d2f90d
commit 480c98f1ab
4 changed files with 188 additions and 39 deletions

View file

@ -16,6 +16,7 @@ import { Reader } from '../utils/wireProtocol';
import { CallbackMap } from './callbackMap'; import { CallbackMap } from './callbackMap';
import { RequestItem, RequestQueue, RequestQueueingType } from './requestQueue'; import { RequestItem, RequestQueue, RequestQueueingType } from './requestQueue';
import { TypeScriptServerError } from './serverError'; import { TypeScriptServerError } from './serverError';
import { EventName } from '../protocol.const';
export interface OngoingRequestCanceller { export interface OngoingRequestCanceller {
tryCancelOngoingRequest(seq: number): boolean; tryCancelOngoingRequest(seq: number): boolean;
@ -309,7 +310,7 @@ class RequestRouter {
]); ]);
constructor( constructor(
private readonly servers: ReadonlyArray<{ readonly server: ITypeScriptServer, readonly preferredCommands?: ReadonlySet<keyof TypeScriptRequests> }>, private readonly servers: ReadonlyArray<{ readonly server: ITypeScriptServer, canRun?(command: keyof TypeScriptRequests): void }>,
private readonly delegate: TsServerDelegate, private readonly delegate: TsServerDelegate,
) { } ) { }
@ -368,8 +369,8 @@ class RequestRouter {
return firstRequest; return firstRequest;
} }
for (const { preferredCommands, server } of this.servers) { for (const { canRun, server } of this.servers) {
if (!preferredCommands || preferredCommands.has(command)) { if (!canRun || canRun(command)) {
return server.executeImpl(command, args, executeInfo); return server.executeImpl(command, args, executeInfo);
} }
} }
@ -379,17 +380,17 @@ class RequestRouter {
} }
export class SyntaxRoutingTsServer extends Disposable implements ITypeScriptServer { const syntaxCommands: ReadonlySet<keyof TypeScriptRequests> = new Set<keyof TypeScriptRequests>([
'navtree',
'getOutliningSpans',
'jsxClosingTag',
'selectionRange',
'format',
'formatonkey',
'docCommentTemplate',
]);
private static readonly syntaxCommands = new Set<keyof TypeScriptRequests>([ export class SyntaxRoutingTsServer extends Disposable implements ITypeScriptServer {
'navtree',
'getOutliningSpans',
'jsxClosingTag',
'selectionRange',
'format',
'formatonkey',
'docCommentTemplate',
]);
private readonly syntaxServer: ITypeScriptServer; private readonly syntaxServer: ITypeScriptServer;
private readonly semanticServer: ITypeScriptServer; private readonly semanticServer: ITypeScriptServer;
@ -406,8 +407,8 @@ export class SyntaxRoutingTsServer extends Disposable implements ITypeScriptServ
this.router = new RequestRouter( this.router = new RequestRouter(
[ [
{ server: this.syntaxServer, preferredCommands: SyntaxRoutingTsServer.syntaxCommands }, { server: this.syntaxServer, canRun: (command) => syntaxCommands.has(command) },
{ server: this.semanticServer, preferredCommands: undefined /* gets all other commands */ } { server: this.semanticServer, canRun: undefined /* gets all other commands */ }
], ],
delegate); delegate);
@ -449,11 +450,11 @@ export class SyntaxRoutingTsServer extends Disposable implements ITypeScriptServ
export class GetErrRoutingTsServer extends Disposable implements ITypeScriptServer { export class GetErrRoutingTsServer extends Disposable implements ITypeScriptServer {
private static readonly diagnosticEvents = new Set([ private static readonly diagnosticEvents = new Set<string>([
'configFileDiag', EventName.configFileDiag,
'syntaxDiag', EventName.syntaxDiag,
'semanticDiag', EventName.semanticDiag,
'suggestionDiag' EventName.suggestionDiag
]); ]);
private readonly getErrServer: ITypeScriptServer; private readonly getErrServer: ITypeScriptServer;
@ -471,8 +472,8 @@ export class GetErrRoutingTsServer extends Disposable implements ITypeScriptServ
this.router = new RequestRouter( this.router = new RequestRouter(
[ [
{ server: this.getErrServer, preferredCommands: new Set<keyof TypeScriptRequests>(['geterr', 'geterrForProject']) }, { server: this.getErrServer, canRun: (command) => ['geterr', 'geterrForProject'].includes(command) },
{ server: this.mainServer, preferredCommands: undefined /* gets all other commands */ } { server: this.mainServer, canRun: undefined /* gets all other commands */ }
], ],
delegate); delegate);
@ -524,6 +525,105 @@ export class GetErrRoutingTsServer extends Disposable implements ITypeScriptServ
} }
export class ProjectLoadingRoutingSyntaxTsServer extends Disposable implements ITypeScriptServer {
private static readonly semanticCommands = new Set<keyof TypeScriptRequests>([
'geterr',
'geterrForProject'
]);
private readonly syntaxServer: ITypeScriptServer;
private readonly semanticServer: ITypeScriptServer;
private readonly router: RequestRouter;
private _projectLoading = true;
public constructor(
servers: { syntax: ITypeScriptServer, semantic: ITypeScriptServer },
delegate: TsServerDelegate,
) {
super();
this.syntaxServer = servers.syntax;
this.semanticServer = servers.semantic;
this.router = new RequestRouter(
[
{
server: this.syntaxServer,
canRun: (command) => {
if (syntaxCommands.has(command)) {
return true;
}
if (ProjectLoadingRoutingSyntaxTsServer.semanticCommands.has(command)) {
return false;
}
if (this._projectLoading) {
return true;
}
return false;
}
}, {
server: this.semanticServer,
canRun: undefined /* gets all other commands */
}
],
delegate);
this._register(this.syntaxServer.onEvent(e => {
return this._onEvent.fire(e);
}));
this._register(this.semanticServer.onEvent(e => {
switch (e.event) {
case EventName.projectLoadingStart:
this._projectLoading = true;
break;
case EventName.projectLoadingFinish:
case EventName.semanticDiag:
case EventName.syntaxDiag:
case EventName.suggestionDiag:
case EventName.configFileDiag:
this._projectLoading = false;
break;
}
return this._onEvent.fire(e);
}));
this._register(this.semanticServer.onExit(e => {
this._onExit.fire(e);
this.syntaxServer.kill();
}));
this._register(this.semanticServer.onError(e => this._onError.fire(e)));
}
private readonly _onEvent = this._register(new vscode.EventEmitter<Proto.Event>());
public readonly onEvent = this._onEvent.event;
private readonly _onExit = this._register(new vscode.EventEmitter<any>());
public readonly onExit = this._onExit.event;
private readonly _onError = this._register(new vscode.EventEmitter<any>());
public readonly onError = this._onError.event;
public get onReaderError() { return this.semanticServer.onReaderError; }
public get tsServerLogFile() { return this.semanticServer.tsServerLogFile; }
public kill(): void {
this.syntaxServer.kill();
this.semanticServer.kill();
}
public executeImpl(command: keyof TypeScriptRequests, args: any, executeInfo: { isAsync: boolean, token?: vscode.CancellationToken, expectsResult: false, lowPriority?: boolean }): undefined;
public executeImpl(command: keyof TypeScriptRequests, args: any, executeInfo: { isAsync: boolean, token?: vscode.CancellationToken, expectsResult: boolean, lowPriority?: boolean }): Promise<ServerResponse.Response<Proto.Response>>;
public executeImpl(command: keyof TypeScriptRequests, args: any, executeInfo: { isAsync: boolean, token?: vscode.CancellationToken, expectsResult: boolean, lowPriority?: boolean }): Promise<ServerResponse.Response<Proto.Response>> | undefined {
return this.router.execute(command, args, executeInfo);
}
}
namespace RequestState { namespace RequestState {
export const enum Type { Unresolved, Resolved, Errored } export const enum Type { Unresolved, Resolved, Errored }

View file

@ -9,7 +9,7 @@ import * as stream from 'stream';
import * as vscode from 'vscode'; import * as vscode from 'vscode';
import type * as Proto from '../protocol'; import type * as Proto from '../protocol';
import API from '../utils/api'; import API from '../utils/api';
import { TsServerLogLevel, TypeScriptServiceConfiguration } from '../utils/configuration'; import { TsServerLogLevel, TypeScriptServiceConfiguration, SeparateSyntaxServerConfigration } from '../utils/configuration';
import * as electron from '../utils/electron'; import * as electron from '../utils/electron';
import LogDirectoryProvider from '../utils/logDirectoryProvider'; import LogDirectoryProvider from '../utils/logDirectoryProvider';
import Logger from '../utils/logger'; import Logger from '../utils/logger';
@ -18,7 +18,7 @@ import { PluginManager } from '../utils/plugins';
import { TelemetryReporter } from '../utils/telemetry'; import { TelemetryReporter } from '../utils/telemetry';
import Tracer from '../utils/tracer'; import Tracer from '../utils/tracer';
import { TypeScriptVersion, TypeScriptVersionProvider } from '../utils/versionProvider'; import { TypeScriptVersion, TypeScriptVersionProvider } from '../utils/versionProvider';
import { ITypeScriptServer, PipeRequestCanceller, ProcessBasedTsServer, SyntaxRoutingTsServer, TsServerProcess, TsServerDelegate, GetErrRoutingTsServer } from './server'; import { ITypeScriptServer, PipeRequestCanceller, ProcessBasedTsServer, SyntaxRoutingTsServer, TsServerProcess, TsServerDelegate, GetErrRoutingTsServer, ProjectLoadingRoutingSyntaxTsServer } from './server';
const enum ServerKind { const enum ServerKind {
Main = 'main', Main = 'main',
@ -27,6 +27,17 @@ const enum ServerKind {
Diagnostics = 'diagnostics' Diagnostics = 'diagnostics'
} }
const enum CompositeServerType {
/** Run a single server that handles all commands */
Single,
/** Run a separate server for syntax commands */
SeparateSyntax,
/** Use a separate suntax server while the project is loading */
DynamicSeparateSyntax,
}
export class TypeScriptServerSpawner { export class TypeScriptServerSpawner {
public constructor( public constructor(
private readonly _versionProvider: TypeScriptVersionProvider, private readonly _versionProvider: TypeScriptVersionProvider,
@ -44,13 +55,28 @@ export class TypeScriptServerSpawner {
delegate: TsServerDelegate, delegate: TsServerDelegate,
): ITypeScriptServer { ): ITypeScriptServer {
let primaryServer: ITypeScriptServer; let primaryServer: ITypeScriptServer;
if (this.shouldUseSeparateSyntaxServer(version, configuration)) { switch (this.getCompositeServerType(version, configuration)) {
primaryServer = new SyntaxRoutingTsServer({ case CompositeServerType.SeparateSyntax:
syntax: this.spawnTsServer(ServerKind.Syntax, version, configuration, pluginManager), {
semantic: this.spawnTsServer(ServerKind.Semantic, version, configuration, pluginManager) primaryServer = new SyntaxRoutingTsServer({
}, delegate); syntax: this.spawnTsServer(ServerKind.Syntax, version, configuration, pluginManager),
} else { semantic: this.spawnTsServer(ServerKind.Semantic, version, configuration, pluginManager)
primaryServer = this.spawnTsServer(ServerKind.Main, version, configuration, pluginManager); }, delegate);
break;
}
case CompositeServerType.DynamicSeparateSyntax:
{
primaryServer = new ProjectLoadingRoutingSyntaxTsServer({
syntax: this.spawnTsServer(ServerKind.Syntax, version, configuration, pluginManager),
semantic: this.spawnTsServer(ServerKind.Semantic, version, configuration, pluginManager)
}, delegate);
break;
}
case CompositeServerType.Single:
{
primaryServer = this.spawnTsServer(ServerKind.Main, version, configuration, pluginManager);
break;
}
} }
if (this.shouldUseSeparateDiagnosticsServer(configuration)) { if (this.shouldUseSeparateDiagnosticsServer(configuration)) {
@ -63,11 +89,20 @@ export class TypeScriptServerSpawner {
return primaryServer; return primaryServer;
} }
private shouldUseSeparateSyntaxServer( private getCompositeServerType(
version: TypeScriptVersion, version: TypeScriptVersion,
configuration: TypeScriptServiceConfiguration, configuration: TypeScriptServiceConfiguration,
): boolean { ): CompositeServerType {
return configuration.useSeparateSyntaxServer && !!version.apiVersion && version.apiVersion.gte(API.v340); switch (configuration.separateSyntaxServer) {
case SeparateSyntaxServerConfigration.Disabled:
return CompositeServerType.Single;
case SeparateSyntaxServerConfigration.Enabled:
return version.apiVersion?.gte(API.v340) ? CompositeServerType.SeparateSyntax : CompositeServerType.Single;
case SeparateSyntaxServerConfigration.Dynamic:
return version.apiVersion?.gte(API.v400) ? CompositeServerType.DynamicSeparateSyntax : CompositeServerType.Single;
}
} }
private shouldUseSeparateDiagnosticsServer( private shouldUseSeparateDiagnosticsServer(

View file

@ -34,6 +34,7 @@ export default class API {
public static readonly v380 = API.fromSimpleString('3.8.0'); public static readonly v380 = API.fromSimpleString('3.8.0');
public static readonly v381 = API.fromSimpleString('3.8.1'); public static readonly v381 = API.fromSimpleString('3.8.1');
public static readonly v390 = API.fromSimpleString('3.9.0'); public static readonly v390 = API.fromSimpleString('3.9.0');
public static readonly v400 = API.fromSimpleString('4.0.0');
public static fromVersionString(versionString: string): API { public static fromVersionString(versionString: string): API {
let version = semver.valid(versionString); let version = semver.valid(versionString);

View file

@ -46,6 +46,12 @@ export namespace TsServerLogLevel {
} }
} }
export const enum SeparateSyntaxServerConfigration {
Disabled,
Enabled,
Dynamic,
}
export class TypeScriptServiceConfiguration { export class TypeScriptServiceConfiguration {
public readonly locale: string | null; public readonly locale: string | null;
public readonly globalTsdk: string | null; public readonly globalTsdk: string | null;
@ -56,7 +62,7 @@ export class TypeScriptServiceConfiguration {
public readonly checkJs: boolean; public readonly checkJs: boolean;
public readonly experimentalDecorators: boolean; public readonly experimentalDecorators: boolean;
public readonly disableAutomaticTypeAcquisition: boolean; public readonly disableAutomaticTypeAcquisition: boolean;
public readonly useSeparateSyntaxServer: boolean; public readonly separateSyntaxServer: SeparateSyntaxServerConfigration;
public readonly enableProjectDiagnostics: boolean; public readonly enableProjectDiagnostics: boolean;
public readonly maxTsServerMemory: number; public readonly maxTsServerMemory: number;
public readonly enablePromptUseWorkspaceTsdk: boolean; public readonly enablePromptUseWorkspaceTsdk: boolean;
@ -78,7 +84,7 @@ export class TypeScriptServiceConfiguration {
this.checkJs = TypeScriptServiceConfiguration.readCheckJs(configuration); this.checkJs = TypeScriptServiceConfiguration.readCheckJs(configuration);
this.experimentalDecorators = TypeScriptServiceConfiguration.readExperimentalDecorators(configuration); this.experimentalDecorators = TypeScriptServiceConfiguration.readExperimentalDecorators(configuration);
this.disableAutomaticTypeAcquisition = TypeScriptServiceConfiguration.readDisableAutomaticTypeAcquisition(configuration); this.disableAutomaticTypeAcquisition = TypeScriptServiceConfiguration.readDisableAutomaticTypeAcquisition(configuration);
this.useSeparateSyntaxServer = TypeScriptServiceConfiguration.readUseSeparateSyntaxServer(configuration); this.separateSyntaxServer = TypeScriptServiceConfiguration.readUseSeparateSyntaxServer(configuration);
this.enableProjectDiagnostics = TypeScriptServiceConfiguration.readEnableProjectDiagnostics(configuration); this.enableProjectDiagnostics = TypeScriptServiceConfiguration.readEnableProjectDiagnostics(configuration);
this.maxTsServerMemory = TypeScriptServiceConfiguration.readMaxTsServerMemory(configuration); this.maxTsServerMemory = TypeScriptServiceConfiguration.readMaxTsServerMemory(configuration);
this.enablePromptUseWorkspaceTsdk = TypeScriptServiceConfiguration.readEnablePromptUseWorkspaceTsdk(configuration); this.enablePromptUseWorkspaceTsdk = TypeScriptServiceConfiguration.readEnablePromptUseWorkspaceTsdk(configuration);
@ -95,7 +101,7 @@ export class TypeScriptServiceConfiguration {
&& this.experimentalDecorators === other.experimentalDecorators && this.experimentalDecorators === other.experimentalDecorators
&& this.disableAutomaticTypeAcquisition === other.disableAutomaticTypeAcquisition && this.disableAutomaticTypeAcquisition === other.disableAutomaticTypeAcquisition
&& arrays.equals(this.tsServerPluginPaths, other.tsServerPluginPaths) && arrays.equals(this.tsServerPluginPaths, other.tsServerPluginPaths)
&& this.useSeparateSyntaxServer === other.useSeparateSyntaxServer && this.separateSyntaxServer === other.separateSyntaxServer
&& this.enableProjectDiagnostics === other.enableProjectDiagnostics && this.enableProjectDiagnostics === other.enableProjectDiagnostics
&& this.maxTsServerMemory === other.maxTsServerMemory && this.maxTsServerMemory === other.maxTsServerMemory
&& objects.equals(this.watchOptions, other.watchOptions) && objects.equals(this.watchOptions, other.watchOptions)
@ -157,8 +163,15 @@ export class TypeScriptServiceConfiguration {
return configuration.get<string | null>('typescript.locale', null); return configuration.get<string | null>('typescript.locale', null);
} }
private static readUseSeparateSyntaxServer(configuration: vscode.WorkspaceConfiguration): boolean { private static readUseSeparateSyntaxServer(configuration: vscode.WorkspaceConfiguration): SeparateSyntaxServerConfigration {
return configuration.get<boolean>('typescript.tsserver.useSeparateSyntaxServer', true); const value = configuration.get('typescript.tsserver.useSeparateSyntaxServer', true);
if (value === true) {
return SeparateSyntaxServerConfigration.Enabled;
}
if (value === 'dynamic') {
return SeparateSyntaxServerConfigration.Dynamic;
}
return SeparateSyntaxServerConfigration.Disabled;
} }
private static readEnableProjectDiagnostics(configuration: vscode.WorkspaceConfiguration): boolean { private static readEnableProjectDiagnostics(configuration: vscode.WorkspaceConfiguration): boolean {