From 0857489caf7378f1063376a02344a87772092e43 Mon Sep 17 00:00:00 2001 From: Matt Bierner Date: Thu, 16 Jul 2020 12:32:19 -0700 Subject: [PATCH] Add the concept of client capabilities for TypeScript For serverless, we will only be able to run the TypeScript syntax server which does not support all features. This change makes this possible by adding the concept of client capabilities. Providers such as rename will only be registered when the client has semantic capabilities --- .../src/extension.ts | 10 +++++----- .../src/features/bufferSyncSupport.ts | 6 +++--- .../src/features/implementationsCodeLens.ts | 7 ++++--- .../src/features/quickFix.ts | 13 +++++++++---- .../src/features/refactor.ts | 7 ++++--- .../src/features/referencesCodeLens.ts | 7 ++++--- .../src/features/rename.ts | 11 ++++++++--- .../src/features/semanticTokens.ts | 5 +++-- .../src/features/task.ts | 8 +++++++- .../src/features/updatePathsOnRename.ts | 5 +++-- .../src/tsServer/spawner.ts | 17 ++++++++++++++++- .../src/typescriptService.ts | 8 ++++++++ .../src/typescriptServiceClient.ts | 18 ++++++++++++++---- .../src/utils/dependentRegistration.ts | 12 +++++++++++- 14 files changed, 99 insertions(+), 35 deletions(-) diff --git a/extensions/typescript-language-features/src/extension.ts b/extensions/typescript-language-features/src/extension.ts index 36783cc5e6a..657755e964a 100644 --- a/extensions/typescript-language-features/src/extension.ts +++ b/extensions/typescript-language-features/src/extension.ts @@ -3,23 +3,23 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import * as rimraf from 'rimraf'; import * as vscode from 'vscode'; import { Api, getExtensionApi } from './api'; import { registerCommands } from './commands/index'; import { LanguageConfigurationManager } from './features/languageConfiguration'; +import * as task from './features/task'; import TypeScriptServiceClientHost from './typeScriptServiceClientHost'; import { flatten } from './utils/arrays'; -import * as electron from './utils/electron'; -import * as rimraf from 'rimraf'; import { CommandManager } from './utils/commandManager'; +import * as electron from './utils/electron'; import * as fileSchemes from './utils/fileSchemes'; import { standardLanguageDescriptions } from './utils/languageDescription'; +import * as ProjectStatus from './utils/largeProjectStatus'; import { lazy, Lazy } from './utils/lazy'; import LogDirectoryProvider from './utils/logDirectoryProvider'; import ManagedFileContextManager from './utils/managedFileContext'; import { PluginManager } from './utils/plugins'; -import * as ProjectStatus from './utils/largeProjectStatus'; -import TscTaskProvider from './features/task'; export function activate( context: vscode.ExtensionContext @@ -38,7 +38,7 @@ export function activate( }); registerCommands(commandManager, lazyClientHost, pluginManager); - context.subscriptions.push(vscode.tasks.registerTaskProvider('typescript', new TscTaskProvider(lazyClientHost.map(x => x.serviceClient)))); + context.subscriptions.push(task.register(lazyClientHost.map(x => x.serviceClient))); context.subscriptions.push(new LanguageConfigurationManager()); import('./features/tsconfig').then(module => { diff --git a/extensions/typescript-language-features/src/features/bufferSyncSupport.ts b/extensions/typescript-language-features/src/features/bufferSyncSupport.ts index 647db7a1187..7fb00fbc894 100644 --- a/extensions/typescript-language-features/src/features/bufferSyncSupport.ts +++ b/extensions/typescript-language-features/src/features/bufferSyncSupport.ts @@ -5,7 +5,7 @@ import * as vscode from 'vscode'; import type * as Proto from '../protocol'; -import { ITypeScriptServiceClient } from '../typescriptService'; +import { ITypeScriptServiceClient, ClientCapability } from '../typescriptService'; import API from '../utils/api'; import { coalesce } from '../utils/arrays'; import { Delayer } from '../utils/async'; @@ -302,9 +302,9 @@ class GetErrRequest { onDone: () => void ) { const allFiles = coalesce(Array.from(files.entries).map(entry => client.normalizedPath(entry.resource))); - if (!allFiles.length) { + if (!allFiles.length || !client.capabilities.has(ClientCapability.Semantic)) { this._done = true; - onDone(); + setImmediate(onDone); } else { const request = client.configuration.enableProjectDiagnostics // Note that geterrForProject is almost certainly not the api we want here as it ends up computing far diff --git a/extensions/typescript-language-features/src/features/implementationsCodeLens.ts b/extensions/typescript-language-features/src/features/implementationsCodeLens.ts index 1e8b5bdeec4..4e56d6499e7 100644 --- a/extensions/typescript-language-features/src/features/implementationsCodeLens.ts +++ b/extensions/typescript-language-features/src/features/implementationsCodeLens.ts @@ -7,11 +7,11 @@ import * as vscode from 'vscode'; import * as nls from 'vscode-nls'; import type * as Proto from '../protocol'; import * as PConst from '../protocol.const'; -import { ITypeScriptServiceClient } from '../typescriptService'; -import { conditionalRegistration, requireConfiguration } from '../utils/dependentRegistration'; -import { TypeScriptBaseCodeLensProvider, ReferencesCodeLens, getSymbolRange } from './baseCodeLensProvider'; import { CachedResponse } from '../tsServer/cachedResponse'; +import { ClientCapability, ITypeScriptServiceClient } from '../typescriptService'; +import { conditionalRegistration, requireCapability, requireConfiguration } from '../utils/dependentRegistration'; import * as typeConverters from '../utils/typeConverters'; +import { getSymbolRange, ReferencesCodeLens, TypeScriptBaseCodeLensProvider } from './baseCodeLensProvider'; const localize = nls.loadMessageBundle(); @@ -96,6 +96,7 @@ export function register( ) { return conditionalRegistration([ requireConfiguration(modeId, 'implementationsCodeLens.enabled'), + requireCapability(client, ClientCapability.Semantic), ], () => { return vscode.languages.registerCodeLensProvider(selector, new TypeScriptImplementationsCodeLensProvider(client, cachedResponse)); diff --git a/extensions/typescript-language-features/src/features/quickFix.ts b/extensions/typescript-language-features/src/features/quickFix.ts index 57b6a2f0f46..1d6fd6aa349 100644 --- a/extensions/typescript-language-features/src/features/quickFix.ts +++ b/extensions/typescript-language-features/src/features/quickFix.ts @@ -6,7 +6,7 @@ import * as vscode from 'vscode'; import * as nls from 'vscode-nls'; import type * as Proto from '../protocol'; -import { ITypeScriptServiceClient } from '../typescriptService'; +import { ITypeScriptServiceClient, ClientCapability } from '../typescriptService'; import API from '../utils/api'; import { nulToken } from '../utils/cancellation'; import { applyCodeActionCommands, getEditForCodeAction } from '../utils/codeAction'; @@ -18,6 +18,7 @@ import * as typeConverters from '../utils/typeConverters'; import { DiagnosticsManager } from './diagnostics'; import FileConfigurationManager from './fileConfigurationManager'; import { equals } from '../utils/objects'; +import { conditionalRegistration, requireCapability } from '../utils/dependentRegistration'; const localize = nls.loadMessageBundle(); @@ -409,7 +410,11 @@ export function register( diagnosticsManager: DiagnosticsManager, telemetryReporter: TelemetryReporter ) { - return vscode.languages.registerCodeActionsProvider(selector, - new TypeScriptQuickFixProvider(client, fileConfigurationManager, commandManager, diagnosticsManager, telemetryReporter), - TypeScriptQuickFixProvider.metadata); + return conditionalRegistration([ + requireCapability(client, ClientCapability.Semantic), + ], () => { + return vscode.languages.registerCodeActionsProvider(selector, + new TypeScriptQuickFixProvider(client, fileConfigurationManager, commandManager, diagnosticsManager, telemetryReporter), + TypeScriptQuickFixProvider.metadata); + }); } diff --git a/extensions/typescript-language-features/src/features/refactor.ts b/extensions/typescript-language-features/src/features/refactor.ts index 9c6ac23977c..a56267acb42 100644 --- a/extensions/typescript-language-features/src/features/refactor.ts +++ b/extensions/typescript-language-features/src/features/refactor.ts @@ -7,11 +7,11 @@ import * as vscode from 'vscode'; import * as nls from 'vscode-nls'; import { LearnMoreAboutRefactoringsCommand } from '../commands/learnMoreAboutRefactorings'; import type * as Proto from '../protocol'; -import { ITypeScriptServiceClient } from '../typescriptService'; +import { ClientCapability, ITypeScriptServiceClient } from '../typescriptService'; import API from '../utils/api'; import { nulToken } from '../utils/cancellation'; import { Command, CommandManager } from '../utils/commandManager'; -import { conditionalRegistration, requireMinVersion } from '../utils/dependentRegistration'; +import { conditionalRegistration, requireCapability, requireMinVersion } from '../utils/dependentRegistration'; import * as fileSchemes from '../utils/fileSchemes'; import { TelemetryReporter } from '../utils/telemetry'; import * as typeConverters from '../utils/typeConverters'; @@ -403,7 +403,8 @@ export function register( telemetryReporter: TelemetryReporter, ) { return conditionalRegistration([ - requireMinVersion(client, TypeScriptRefactorProvider.minVersion) + requireMinVersion(client, TypeScriptRefactorProvider.minVersion), + requireCapability(client, ClientCapability.Semantic), ], () => { return vscode.languages.registerCodeActionsProvider(selector, new TypeScriptRefactorProvider(client, formattingOptionsManager, commandManager, telemetryReporter), diff --git a/extensions/typescript-language-features/src/features/referencesCodeLens.ts b/extensions/typescript-language-features/src/features/referencesCodeLens.ts index 1bf3c404ad7..bfda4f07230 100644 --- a/extensions/typescript-language-features/src/features/referencesCodeLens.ts +++ b/extensions/typescript-language-features/src/features/referencesCodeLens.ts @@ -8,8 +8,8 @@ import * as nls from 'vscode-nls'; import type * as Proto from '../protocol'; import * as PConst from '../protocol.const'; import { CachedResponse } from '../tsServer/cachedResponse'; -import { ITypeScriptServiceClient } from '../typescriptService'; -import { conditionalRegistration, requireConfiguration } from '../utils/dependentRegistration'; +import { ITypeScriptServiceClient, ClientCapability } from '../typescriptService'; +import { conditionalRegistration, requireConfiguration, requireCapability } from '../utils/dependentRegistration'; import * as typeConverters from '../utils/typeConverters'; import { getSymbolRange, ReferencesCodeLens, TypeScriptBaseCodeLensProvider } from './baseCodeLensProvider'; @@ -128,7 +128,8 @@ export function register( cachedResponse: CachedResponse, ) { return conditionalRegistration([ - requireConfiguration(modeId, 'referencesCodeLens.enabled') + requireConfiguration(modeId, 'referencesCodeLens.enabled'), + requireCapability(client, ClientCapability.Semantic), ], () => { return vscode.languages.registerCodeLensProvider(selector, new TypeScriptReferencesCodeLensProvider(client, cachedResponse, modeId)); diff --git a/extensions/typescript-language-features/src/features/rename.ts b/extensions/typescript-language-features/src/features/rename.ts index 9074faa4372..da784760d66 100644 --- a/extensions/typescript-language-features/src/features/rename.ts +++ b/extensions/typescript-language-features/src/features/rename.ts @@ -7,8 +7,9 @@ import * as path from 'path'; import * as vscode from 'vscode'; import * as nls from 'vscode-nls'; import type * as Proto from '../protocol'; -import { ITypeScriptServiceClient, ServerResponse } from '../typescriptService'; +import { ClientCapability, ITypeScriptServiceClient, ServerResponse } from '../typescriptService'; import API from '../utils/api'; +import { conditionalRegistration, requireCapability } from '../utils/dependentRegistration'; import * as typeConverters from '../utils/typeConverters'; import FileConfigurationManager from './fileConfigurationManager'; @@ -141,6 +142,10 @@ export function register( client: ITypeScriptServiceClient, fileConfigurationManager: FileConfigurationManager, ) { - return vscode.languages.registerRenameProvider(selector, - new TypeScriptRenameProvider(client, fileConfigurationManager)); + return conditionalRegistration([ + requireCapability(client, ClientCapability.Semantic), + ], () => { + return vscode.languages.registerRenameProvider(selector, + new TypeScriptRenameProvider(client, fileConfigurationManager)); + }); } diff --git a/extensions/typescript-language-features/src/features/semanticTokens.ts b/extensions/typescript-language-features/src/features/semanticTokens.ts index 4546a1e6bc8..6174b51c8e5 100644 --- a/extensions/typescript-language-features/src/features/semanticTokens.ts +++ b/extensions/typescript-language-features/src/features/semanticTokens.ts @@ -7,9 +7,9 @@ import { TokenEncodingConsts, TokenModifier, TokenType, VersionRequirement } from 'typescript-vscode-sh-plugin/lib/constants'; import * as vscode from 'vscode'; import * as Proto from '../protocol'; -import { ExecConfig, ITypeScriptServiceClient, ServerResponse } from '../typescriptService'; +import { ClientCapability, ExecConfig, ITypeScriptServiceClient, ServerResponse } from '../typescriptService'; import API from '../utils/api'; -import { conditionalRegistration, requireMinVersion } from '../utils/dependentRegistration'; +import { conditionalRegistration, requireCapability, requireMinVersion } from '../utils/dependentRegistration'; const minTypeScriptVersion = API.fromVersionString(`${VersionRequirement.major}.${VersionRequirement.minor}`); @@ -20,6 +20,7 @@ const CONTENT_LENGTH_LIMIT = 100000; export function register(selector: vscode.DocumentSelector, client: ITypeScriptServiceClient) { return conditionalRegistration([ requireMinVersion(client, minTypeScriptVersion), + requireCapability(client, ClientCapability.Semantic), ], () => { const provider = new DocumentSemanticTokensProvider(client); return vscode.Disposable.from( diff --git a/extensions/typescript-language-features/src/features/task.ts b/extensions/typescript-language-features/src/features/task.ts index b63ff25019f..4fa8bba2bdb 100644 --- a/extensions/typescript-language-features/src/features/task.ts +++ b/extensions/typescript-language-features/src/features/task.ts @@ -35,7 +35,7 @@ interface TypeScriptTaskDefinition extends vscode.TaskDefinition { /** * Provides tasks for building `tsconfig.json` files in a project. */ -export default class TscTaskProvider implements vscode.TaskProvider { +class TscTaskProvider implements vscode.TaskProvider { private readonly projectInfoRequestTimeout = 2000; private autoDetect: AutoDetect = 'on'; @@ -291,3 +291,9 @@ export default class TscTaskProvider implements vscode.TaskProvider { this.autoDetect = typeof type === 'undefined' ? 'on' : type; } } + +export function register( + lazyClient: Lazy, +) { + return vscode.tasks.registerTaskProvider('typescript', new TscTaskProvider(lazyClient)); +} diff --git a/extensions/typescript-language-features/src/features/updatePathsOnRename.ts b/extensions/typescript-language-features/src/features/updatePathsOnRename.ts index 408d5100017..84171e18fb7 100644 --- a/extensions/typescript-language-features/src/features/updatePathsOnRename.ts +++ b/extensions/typescript-language-features/src/features/updatePathsOnRename.ts @@ -7,11 +7,11 @@ import * as path from 'path'; import * as vscode from 'vscode'; import * as nls from 'vscode-nls'; import type * as Proto from '../protocol'; -import { ITypeScriptServiceClient } from '../typescriptService'; +import { ClientCapability, ITypeScriptServiceClient } from '../typescriptService'; import API from '../utils/api'; import { Delayer } from '../utils/async'; import { nulToken } from '../utils/cancellation'; -import { conditionalRegistration, requireMinVersion } from '../utils/dependentRegistration'; +import { conditionalRegistration, requireCapability, requireMinVersion } from '../utils/dependentRegistration'; import { Disposable } from '../utils/dispose'; import * as fileSchemes from '../utils/fileSchemes'; import { doesResourceLookLikeATypeScriptFile } from '../utils/languageDescription'; @@ -296,6 +296,7 @@ export function register( ) { return conditionalRegistration([ requireMinVersion(client, UpdateImportsOnFileRenameHandler.minVersion), + requireCapability(client, ClientCapability.Semantic), ], () => { return new UpdateImportsOnFileRenameHandler(client, fileConfigurationManager, handles); }); diff --git a/extensions/typescript-language-features/src/tsServer/spawner.ts b/extensions/typescript-language-features/src/tsServer/spawner.ts index eefd944d70e..c84ef656484 100644 --- a/extensions/typescript-language-features/src/tsServer/spawner.ts +++ b/extensions/typescript-language-features/src/tsServer/spawner.ts @@ -8,6 +8,7 @@ import * as path from 'path'; import * as stream from 'stream'; import * as vscode from 'vscode'; import type * as Proto from '../protocol'; +import { ClientCapability } from '../typescriptService'; import API from '../utils/api'; import { SeparateSyntaxServerConfiguration, TsServerLogLevel, TypeScriptServiceConfiguration } from '../utils/configuration'; import * as electron from '../utils/electron'; @@ -36,6 +37,9 @@ const enum CompositeServerType { /** Use a separate syntax server while the project is loading */ DynamicSeparateSyntax, + + /** Only enable the syntax server */ + SyntaxOnly } export class TypeScriptServerSpawner { @@ -50,12 +54,13 @@ export class TypeScriptServerSpawner { public spawn( version: TypeScriptVersion, + capabilities: Set, configuration: TypeScriptServiceConfiguration, pluginManager: PluginManager, delegate: TsServerDelegate, ): ITypeScriptServer { let primaryServer: ITypeScriptServer; - const serverType = this.getCompositeServerType(version, configuration); + const serverType = this.getCompositeServerType(version, capabilities, configuration); switch (serverType) { case CompositeServerType.SeparateSyntax: case CompositeServerType.DynamicSeparateSyntax: @@ -72,6 +77,11 @@ export class TypeScriptServerSpawner { primaryServer = this.spawnTsServer(ServerKind.Main, version, configuration, pluginManager); break; } + case CompositeServerType.SyntaxOnly: + { + primaryServer = this.spawnTsServer(ServerKind.Syntax, version, configuration, pluginManager); + break; + } } if (this.shouldUseSeparateDiagnosticsServer(configuration)) { @@ -86,8 +96,13 @@ export class TypeScriptServerSpawner { private getCompositeServerType( version: TypeScriptVersion, + capabilities: Set, configuration: TypeScriptServiceConfiguration, ): CompositeServerType { + if (!capabilities.has(ClientCapability.Semantic)) { + return CompositeServerType.SyntaxOnly; + } + switch (configuration.separateSyntaxServer) { case SeparateSyntaxServerConfiguration.Disabled: return CompositeServerType.Single; diff --git a/extensions/typescript-language-features/src/typescriptService.ts b/extensions/typescript-language-features/src/typescriptService.ts index e3d89eb2bc7..bd7437936e2 100644 --- a/extensions/typescript-language-features/src/typescriptService.ts +++ b/extensions/typescript-language-features/src/typescriptService.ts @@ -85,6 +85,11 @@ export type ExecConfig = { readonly cancelOnResourceChange?: vscode.Uri }; +export enum ClientCapability { + Syntax, + Semantic, +} + export interface ITypeScriptServiceClient { /** * Convert a resource (VS Code) to a normalized path (TypeScript). @@ -120,6 +125,9 @@ export interface ITypeScriptServiceClient { readonly onDidEndInstallTypings: vscode.Event; readonly onTypesInstallerInitializationFailed: vscode.Event; + readonly capabilities: Set; + readonly onDidChangeCapabilities: vscode.Event>; + onReady(f: () => void): Promise; showVersionPicker(): void; diff --git a/extensions/typescript-language-features/src/typescriptServiceClient.ts b/extensions/typescript-language-features/src/typescriptServiceClient.ts index 61c176a2ad3..e1c750725d2 100644 --- a/extensions/typescript-language-features/src/typescriptServiceClient.ts +++ b/extensions/typescript-language-features/src/typescriptServiceClient.ts @@ -10,10 +10,11 @@ import * as nls from 'vscode-nls'; import BufferSyncSupport from './features/bufferSyncSupport'; import { DiagnosticKind, DiagnosticsManager } from './features/diagnostics'; import * as Proto from './protocol'; +import { EventName } from './protocol.const'; import { ITypeScriptServer } from './tsServer/server'; import { TypeScriptServerError } from './tsServer/serverError'; import { TypeScriptServerSpawner } from './tsServer/spawner'; -import { ExecConfig, ITypeScriptServiceClient, ServerResponse, TypeScriptRequests } from './typescriptService'; +import { ClientCapability, ExecConfig, ITypeScriptServiceClient, ServerResponse, TypeScriptRequests } from './typescriptService'; import API from './utils/api'; import { TsServerLogLevel, TypeScriptServiceConfiguration } from './utils/configuration'; import { Disposable } from './utils/dispose'; @@ -22,12 +23,11 @@ import LogDirectoryProvider from './utils/logDirectoryProvider'; import Logger from './utils/logger'; import { TypeScriptPluginPathsProvider } from './utils/pluginPathsProvider'; import { PluginManager } from './utils/plugins'; -import { TelemetryReporter, VSCodeTelemetryReporter, TelemetryProperties } from './utils/telemetry'; +import { TelemetryProperties, TelemetryReporter, VSCodeTelemetryReporter } from './utils/telemetry'; import Tracer from './utils/tracer'; import { inferredProjectCompilerOptions, ProjectType } from './utils/tsconfig'; import { TypeScriptVersionManager } from './utils/versionManager'; import { TypeScriptVersion, TypeScriptVersionProvider } from './utils/versionProvider'; -import { EventName } from './protocol.const'; const localize = nls.loadMessageBundle(); @@ -203,6 +203,16 @@ export default class TypeScriptServiceClient extends Disposable implements IType })); } + public get capabilities() { + return new Set([ + ClientCapability.Semantic, + ClientCapability.Syntax, + ]); + } + + private readonly _onDidChangeCapabilities = this._register(new vscode.EventEmitter>()); + readonly onDidChangeCapabilities = this._onDidChangeCapabilities.event; + private cancelInflightRequestsForResource(resource: vscode.Uri): void { if (this.serverState.type !== ServerState.Type.Running) { return; @@ -337,7 +347,7 @@ export default class TypeScriptServiceClient extends Disposable implements IType const apiVersion = version.apiVersion || API.defaultVersion; let mytoken = ++this.token; - const handle = this.typescriptServerSpawner.spawn(version, this.configuration, this.pluginManager, { + const handle = this.typescriptServerSpawner.spawn(version, this.capabilities, this.configuration, this.pluginManager, { onFatalError: (command, err) => this.fatalError(command, err), }); this.serverState = new ServerState.Running(handle, apiVersion, undefined, true); diff --git a/extensions/typescript-language-features/src/utils/dependentRegistration.ts b/extensions/typescript-language-features/src/utils/dependentRegistration.ts index 6a87f286e6d..a10a33ba80c 100644 --- a/extensions/typescript-language-features/src/utils/dependentRegistration.ts +++ b/extensions/typescript-language-features/src/utils/dependentRegistration.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; -import { ITypeScriptServiceClient } from '../typescriptService'; +import { ITypeScriptServiceClient, ClientCapability } from '../typescriptService'; import API from './api'; import { Disposable } from './dispose'; @@ -95,3 +95,13 @@ export function requireConfiguration( vscode.workspace.onDidChangeConfiguration ); } + +export function requireCapability( + client: ITypeScriptServiceClient, + requiredCapability: ClientCapability, +) { + return new Condition( + () => client.capabilities.has(requiredCapability), + client.onDidChangeCapabilities + ); +}