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
This commit is contained in:
Matt Bierner 2020-07-16 12:32:19 -07:00
parent 16c6b81b3e
commit 0857489caf
14 changed files with 99 additions and 35 deletions

View file

@ -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 => {

View file

@ -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

View file

@ -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));

View file

@ -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);
});
}

View file

@ -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),

View file

@ -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<Proto.NavTreeResponse>,
) {
return conditionalRegistration([
requireConfiguration(modeId, 'referencesCodeLens.enabled')
requireConfiguration(modeId, 'referencesCodeLens.enabled'),
requireCapability(client, ClientCapability.Semantic),
], () => {
return vscode.languages.registerCodeLensProvider(selector,
new TypeScriptReferencesCodeLensProvider(client, cachedResponse, modeId));

View file

@ -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));
});
}

View file

@ -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(

View file

@ -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<ITypeScriptServiceClient>,
) {
return vscode.tasks.registerTaskProvider('typescript', new TscTaskProvider(lazyClient));
}

View file

@ -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);
});

View file

@ -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<ClientCapability>,
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<ClientCapability>,
configuration: TypeScriptServiceConfiguration,
): CompositeServerType {
if (!capabilities.has(ClientCapability.Semantic)) {
return CompositeServerType.SyntaxOnly;
}
switch (configuration.separateSyntaxServer) {
case SeparateSyntaxServerConfiguration.Disabled:
return CompositeServerType.Single;

View file

@ -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<Proto.EndInstallTypesEventBody>;
readonly onTypesInstallerInitializationFailed: vscode.Event<Proto.TypesInstallerInitializationFailedEventBody>;
readonly capabilities: Set<ClientCapability>;
readonly onDidChangeCapabilities: vscode.Event<Set<ClientCapability>>;
onReady(f: () => void): Promise<void>;
showVersionPicker(): void;

View file

@ -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>([
ClientCapability.Semantic,
ClientCapability.Syntax,
]);
}
private readonly _onDidChangeCapabilities = this._register(new vscode.EventEmitter<Set<ClientCapability>>());
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);

View file

@ -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
);
}