diff --git a/extensions/typescript-language-features/src/tsServer/server.ts b/extensions/typescript-language-features/src/tsServer/server.ts index 9eef0fc7b62..7cbb83bd439 100644 --- a/extensions/typescript-language-features/src/tsServer/server.ts +++ b/extensions/typescript-language-features/src/tsServer/server.ts @@ -16,6 +16,7 @@ import { Reader } from '../utils/wireProtocol'; import { CallbackMap } from './callbackMap'; import { RequestItem, RequestQueue, RequestQueueingType } from './requestQueue'; import { TypeScriptServerError } from './serverError'; +import { EventName } from '../protocol.const'; export interface OngoingRequestCanceller { tryCancelOngoingRequest(seq: number): boolean; @@ -309,7 +310,7 @@ class RequestRouter { ]); constructor( - private readonly servers: ReadonlyArray<{ readonly server: ITypeScriptServer, readonly preferredCommands?: ReadonlySet }>, + private readonly servers: ReadonlyArray<{ readonly server: ITypeScriptServer, canRun?(command: keyof TypeScriptRequests): void }>, private readonly delegate: TsServerDelegate, ) { } @@ -368,8 +369,8 @@ class RequestRouter { return firstRequest; } - for (const { preferredCommands, server } of this.servers) { - if (!preferredCommands || preferredCommands.has(command)) { + for (const { canRun, server } of this.servers) { + if (!canRun || canRun(command)) { return server.executeImpl(command, args, executeInfo); } } @@ -379,17 +380,17 @@ class RequestRouter { } -export class SyntaxRoutingTsServer extends Disposable implements ITypeScriptServer { +const syntaxCommands: ReadonlySet = new Set([ + 'navtree', + 'getOutliningSpans', + 'jsxClosingTag', + 'selectionRange', + 'format', + 'formatonkey', + 'docCommentTemplate', +]); - private static readonly syntaxCommands = new Set([ - 'navtree', - 'getOutliningSpans', - 'jsxClosingTag', - 'selectionRange', - 'format', - 'formatonkey', - 'docCommentTemplate', - ]); +export class SyntaxRoutingTsServer extends Disposable implements ITypeScriptServer { private readonly syntaxServer: ITypeScriptServer; private readonly semanticServer: ITypeScriptServer; @@ -406,8 +407,8 @@ export class SyntaxRoutingTsServer extends Disposable implements ITypeScriptServ this.router = new RequestRouter( [ - { server: this.syntaxServer, preferredCommands: SyntaxRoutingTsServer.syntaxCommands }, - { server: this.semanticServer, preferredCommands: undefined /* gets all other commands */ } + { server: this.syntaxServer, canRun: (command) => syntaxCommands.has(command) }, + { server: this.semanticServer, canRun: undefined /* gets all other commands */ } ], delegate); @@ -449,11 +450,11 @@ export class SyntaxRoutingTsServer extends Disposable implements ITypeScriptServ export class GetErrRoutingTsServer extends Disposable implements ITypeScriptServer { - private static readonly diagnosticEvents = new Set([ - 'configFileDiag', - 'syntaxDiag', - 'semanticDiag', - 'suggestionDiag' + private static readonly diagnosticEvents = new Set([ + EventName.configFileDiag, + EventName.syntaxDiag, + EventName.semanticDiag, + EventName.suggestionDiag ]); private readonly getErrServer: ITypeScriptServer; @@ -471,8 +472,8 @@ export class GetErrRoutingTsServer extends Disposable implements ITypeScriptServ this.router = new RequestRouter( [ - { server: this.getErrServer, preferredCommands: new Set(['geterr', 'geterrForProject']) }, - { server: this.mainServer, preferredCommands: undefined /* gets all other commands */ } + { server: this.getErrServer, canRun: (command) => ['geterr', 'geterrForProject'].includes(command) }, + { server: this.mainServer, canRun: undefined /* gets all other commands */ } ], 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([ + '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()); + public readonly onEvent = this._onEvent.event; + + private readonly _onExit = this._register(new vscode.EventEmitter()); + public readonly onExit = this._onExit.event; + + private readonly _onError = this._register(new vscode.EventEmitter()); + 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>; + public executeImpl(command: keyof TypeScriptRequests, args: any, executeInfo: { isAsync: boolean, token?: vscode.CancellationToken, expectsResult: boolean, lowPriority?: boolean }): Promise> | undefined { + return this.router.execute(command, args, executeInfo); + } +} + namespace RequestState { export const enum Type { Unresolved, Resolved, Errored } diff --git a/extensions/typescript-language-features/src/tsServer/spawner.ts b/extensions/typescript-language-features/src/tsServer/spawner.ts index 99318d9940d..292e3d42f4b 100644 --- a/extensions/typescript-language-features/src/tsServer/spawner.ts +++ b/extensions/typescript-language-features/src/tsServer/spawner.ts @@ -9,7 +9,7 @@ import * as stream from 'stream'; import * as vscode from 'vscode'; import type * as Proto from '../protocol'; 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 LogDirectoryProvider from '../utils/logDirectoryProvider'; import Logger from '../utils/logger'; @@ -18,7 +18,7 @@ import { PluginManager } from '../utils/plugins'; import { TelemetryReporter } from '../utils/telemetry'; import Tracer from '../utils/tracer'; 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 { Main = 'main', @@ -27,6 +27,17 @@ const enum ServerKind { 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 { public constructor( private readonly _versionProvider: TypeScriptVersionProvider, @@ -44,13 +55,28 @@ export class TypeScriptServerSpawner { delegate: TsServerDelegate, ): ITypeScriptServer { let primaryServer: ITypeScriptServer; - if (this.shouldUseSeparateSyntaxServer(version, configuration)) { - primaryServer = new SyntaxRoutingTsServer({ - syntax: this.spawnTsServer(ServerKind.Syntax, version, configuration, pluginManager), - semantic: this.spawnTsServer(ServerKind.Semantic, version, configuration, pluginManager) - }, delegate); - } else { - primaryServer = this.spawnTsServer(ServerKind.Main, version, configuration, pluginManager); + switch (this.getCompositeServerType(version, configuration)) { + case CompositeServerType.SeparateSyntax: + { + primaryServer = new SyntaxRoutingTsServer({ + syntax: this.spawnTsServer(ServerKind.Syntax, version, configuration, pluginManager), + semantic: this.spawnTsServer(ServerKind.Semantic, 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)) { @@ -63,11 +89,20 @@ export class TypeScriptServerSpawner { return primaryServer; } - private shouldUseSeparateSyntaxServer( + private getCompositeServerType( version: TypeScriptVersion, configuration: TypeScriptServiceConfiguration, - ): boolean { - return configuration.useSeparateSyntaxServer && !!version.apiVersion && version.apiVersion.gte(API.v340); + ): CompositeServerType { + 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( diff --git a/extensions/typescript-language-features/src/utils/api.ts b/extensions/typescript-language-features/src/utils/api.ts index 1845285caa4..2a72b19004c 100644 --- a/extensions/typescript-language-features/src/utils/api.ts +++ b/extensions/typescript-language-features/src/utils/api.ts @@ -34,6 +34,7 @@ export default class API { public static readonly v380 = API.fromSimpleString('3.8.0'); public static readonly v381 = API.fromSimpleString('3.8.1'); public static readonly v390 = API.fromSimpleString('3.9.0'); + public static readonly v400 = API.fromSimpleString('4.0.0'); public static fromVersionString(versionString: string): API { let version = semver.valid(versionString); diff --git a/extensions/typescript-language-features/src/utils/configuration.ts b/extensions/typescript-language-features/src/utils/configuration.ts index f306503b83d..caa38efe842 100644 --- a/extensions/typescript-language-features/src/utils/configuration.ts +++ b/extensions/typescript-language-features/src/utils/configuration.ts @@ -46,6 +46,12 @@ export namespace TsServerLogLevel { } } +export const enum SeparateSyntaxServerConfigration { + Disabled, + Enabled, + Dynamic, +} + export class TypeScriptServiceConfiguration { public readonly locale: string | null; public readonly globalTsdk: string | null; @@ -56,7 +62,7 @@ export class TypeScriptServiceConfiguration { public readonly checkJs: boolean; public readonly experimentalDecorators: boolean; public readonly disableAutomaticTypeAcquisition: boolean; - public readonly useSeparateSyntaxServer: boolean; + public readonly separateSyntaxServer: SeparateSyntaxServerConfigration; public readonly enableProjectDiagnostics: boolean; public readonly maxTsServerMemory: number; public readonly enablePromptUseWorkspaceTsdk: boolean; @@ -78,7 +84,7 @@ export class TypeScriptServiceConfiguration { this.checkJs = TypeScriptServiceConfiguration.readCheckJs(configuration); this.experimentalDecorators = TypeScriptServiceConfiguration.readExperimentalDecorators(configuration); this.disableAutomaticTypeAcquisition = TypeScriptServiceConfiguration.readDisableAutomaticTypeAcquisition(configuration); - this.useSeparateSyntaxServer = TypeScriptServiceConfiguration.readUseSeparateSyntaxServer(configuration); + this.separateSyntaxServer = TypeScriptServiceConfiguration.readUseSeparateSyntaxServer(configuration); this.enableProjectDiagnostics = TypeScriptServiceConfiguration.readEnableProjectDiagnostics(configuration); this.maxTsServerMemory = TypeScriptServiceConfiguration.readMaxTsServerMemory(configuration); this.enablePromptUseWorkspaceTsdk = TypeScriptServiceConfiguration.readEnablePromptUseWorkspaceTsdk(configuration); @@ -95,7 +101,7 @@ export class TypeScriptServiceConfiguration { && this.experimentalDecorators === other.experimentalDecorators && this.disableAutomaticTypeAcquisition === other.disableAutomaticTypeAcquisition && arrays.equals(this.tsServerPluginPaths, other.tsServerPluginPaths) - && this.useSeparateSyntaxServer === other.useSeparateSyntaxServer + && this.separateSyntaxServer === other.separateSyntaxServer && this.enableProjectDiagnostics === other.enableProjectDiagnostics && this.maxTsServerMemory === other.maxTsServerMemory && objects.equals(this.watchOptions, other.watchOptions) @@ -157,8 +163,15 @@ export class TypeScriptServiceConfiguration { return configuration.get('typescript.locale', null); } - private static readUseSeparateSyntaxServer(configuration: vscode.WorkspaceConfiguration): boolean { - return configuration.get('typescript.tsserver.useSeparateSyntaxServer', true); + private static readUseSeparateSyntaxServer(configuration: vscode.WorkspaceConfiguration): SeparateSyntaxServerConfigration { + 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 {