From adaf148cf6e3ef6c7142268f240af419bd327bc8 Mon Sep 17 00:00:00 2001 From: Rafal Chlodnicki Date: Sun, 10 Dec 2023 21:21:38 +0100 Subject: [PATCH 1/2] feat: allow plugins to enable support for new language ID --- docs/configuration.md | 10 ++++++-- src/document.ts | 7 ++++-- src/lsp-server.ts | 26 +++----------------- src/ts-client.spec.ts | 1 + src/ts-client.ts | 28 +++++++++++++++------- src/ts-protocol.ts | 3 ++- src/tsServer/plugins.ts | 52 ++++++++++++++++++++++++++++++++++++++++ src/tsServer/spawner.ts | 43 ++++++++++++++++----------------- src/typescriptService.ts | 3 ++- src/utils/types.ts | 5 ++++ 10 files changed, 118 insertions(+), 60 deletions(-) create mode 100644 src/tsServer/plugins.ts diff --git a/docs/configuration.md b/docs/configuration.md index 04024323..44100950 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -20,10 +20,16 @@ The language server accepts various settings through the `initializationOptions` | maxTsServerMemory | number | The maximum size of the V8's old memory section in megabytes (for example `4096` means 4GB). The default value is dynamically configured by Node so can differ per system. Increase for very big projects that exceed allowed memory usage. **Default**: `undefined` | | npmLocation | string | Specifies the path to the NPM executable used for Automatic Type Acquisition. | | locale | string | The locale to use to show error messages. | -| plugins | object[] | An array of `{ name: string, location: string }` objects for registering a Typescript plugins. **Default**: [] | +| plugins | object[] | An array of `{ name: string, location: string, languages?: string[] }` objects for registering a Typescript plugins. **Default**: [] | | preferences | object | Preferences passed to the Typescript (`tsserver`) process. See below for more | | tsserver | object | Options related to the `tsserver` process. See below for more | +### `plugins` option + +Accepts a list of `tsserver` (typescript) plugins. +The `name` and the `location` are required. The `location` is a path to the package or a directory in which `tsserver` will try to import the plugin `name` using Node's `require` API. +The `languages` property specifying which extra language IDs the language server should accept. This is required if plugin enables support for language IDs that this server does not support by default (so other than `typescript`, `typescriptreact`, `javascript`, `javascriptreact`). It's an optional property and only affects which file types the language server allows to be opened and not the `tsserver` itself. + ### `tsserver` options Specifies additional options related to the internal `tsserver` process, like tracing and logging: @@ -218,4 +224,4 @@ implicitProjectConfiguration.strictNullChecks: boolean; * @default 'ES2020' */ implicitProjectConfiguration.target: string; -``` \ No newline at end of file +``` diff --git a/src/document.ts b/src/document.ts index c1b96046..042317b3 100644 --- a/src/document.ts +++ b/src/document.ts @@ -228,7 +228,7 @@ export class LspDocuments { private _validateJavaScript = true; private _validateTypeScript = true; - private readonly modeIds: Set; + private modeIds: Set = new Set(); private readonly _files: string[] = []; private readonly documents = new Map(); private readonly pendingDiagnostics: PendingDiagnostics; @@ -242,13 +242,16 @@ export class LspDocuments { ) { this.client = client; this.lspClient = lspClient; - this.modeIds = new Set(languageModeIds.jsTsLanguageModes); const pathNormalizer = (path: URI) => this.client.toTsFilePath(path.toString()); this.pendingDiagnostics = new PendingDiagnostics(pathNormalizer, { onCaseInsensitiveFileSystem }); this.diagnosticDelayer = new Delayer(300); } + public initialize(allModeIds: readonly string[]): void { + this.modeIds = new Set(allModeIds); + } + /** * Sorted by last access. */ diff --git a/src/lsp-server.ts b/src/lsp-server.ts index e3ac78b2..5165a3f7 100644 --- a/src/lsp-server.ts +++ b/src/lsp-server.ts @@ -95,17 +95,7 @@ export class LspServer { this.workspaceRoot = this.initializeParams.rootUri ? URI.parse(this.initializeParams.rootUri).fsPath : this.initializeParams.rootPath || undefined; const userInitializationOptions: TypeScriptInitializationOptions = this.initializeParams.initializationOptions || {}; - const { disableAutomaticTypingAcquisition, hostInfo, maxTsServerMemory, npmLocation, locale, tsserver } = userInitializationOptions; - const { plugins }: TypeScriptInitializationOptions = { - plugins: userInitializationOptions.plugins || [], - }; - - const globalPlugins: string[] = []; - const pluginProbeLocations: string[] = []; - for (const plugin of plugins) { - globalPlugins.push(plugin.name); - pluginProbeLocations.push(plugin.location); - } + const { disableAutomaticTypingAcquisition, hostInfo, maxTsServerMemory, npmLocation, locale, plugins, tsserver } = userInitializationOptions; const typescriptVersion = this.findTypescriptVersion(tsserver?.path, tsserver?.fallbackPath); if (typescriptVersion) { @@ -158,8 +148,7 @@ export class LspServer { maxTsServerMemory, npmLocation, locale, - globalPlugins, - pluginProbeLocations, + plugins: plugins || [], onEvent: this.onTsEvent.bind(this), onExit: (exitCode, signal) => { this.shutdown(); @@ -886,16 +875,7 @@ export class LspServer { } } else if (params.command === Commands.CONFIGURE_PLUGIN && params.arguments) { const [pluginName, configuration] = params.arguments as [string, unknown]; - - if (this.tsClient.apiVersion.gte(API.v314)) { - this.tsClient.executeWithoutWaitingForResponse( - CommandTypes.ConfigurePlugin, - { - configuration, - pluginName, - }, - ); - } + this.tsClient.configurePlugin(pluginName, configuration); } else if (params.command === Commands.ORGANIZE_IMPORTS && params.arguments) { const file = params.arguments[0] as string; const uri = this.tsClient.toResource(file).toString(); diff --git a/src/ts-client.spec.ts b/src/ts-client.spec.ts index fa187d6e..3eddb8c5 100644 --- a/src/ts-client.spec.ts +++ b/src/ts-client.spec.ts @@ -40,6 +40,7 @@ describe('ts server client', () => { { logDirectoryProvider: noopLogDirectoryProvider, logVerbosity: TsServerLogLevel.Off, + plugins: [], trace: Trace.Off, typescriptVersion: bundled!, useSyntaxServer: SyntaxServerConfiguration.Never, diff --git a/src/ts-client.ts b/src/ts-client.ts index 2045e72b..a182ede1 100644 --- a/src/ts-client.ts +++ b/src/ts-client.ts @@ -17,10 +17,12 @@ import { type DocumentUri } from 'vscode-languageserver-textdocument'; import { type CancellationToken, CancellationTokenSource } from 'vscode-jsonrpc'; import { type LspDocument, LspDocuments } from './document.js'; import * as fileSchemes from './configuration/fileSchemes.js'; +import * as languageModeIds from './configuration/languageIds.js'; import { CommandTypes, EventName } from './ts-protocol.js'; -import type { ts } from './ts-protocol.js'; +import type { TypeScriptPlugin, ts } from './ts-protocol.js'; import type { ILogDirectoryProvider } from './tsServer/logDirectoryProvider.js'; import { AsyncTsServerRequests, ClientCapabilities, ClientCapability, ExecConfig, NoResponseTsServerRequests, ITypeScriptServiceClient, ServerResponse, StandardTsServerRequests, TypeScriptRequestTypes } from './typescriptService.js'; +import { PluginManager } from './tsServer/plugins.js'; import type { ITypeScriptServer, TypeScriptServerExitEvent } from './tsServer/server.js'; import { TypeScriptServerError } from './tsServer/serverError.js'; import { TypeScriptServerSpawner } from './tsServer/spawner.js'; @@ -30,6 +32,7 @@ import type { LspClient } from './lsp-client.js'; import API from './utils/api.js'; import { SyntaxServerConfiguration, TsServerLogLevel } from './utils/configuration.js'; import { Logger, PrefixingLogger } from './utils/logger.js'; +import type { WorkspaceFolder } from './utils/types.js'; interface ToCancelOnResourceChanged { readonly resource: string; @@ -131,10 +134,6 @@ class ServerInitializingIndicator { } } -type WorkspaceFolder = { - uri: URI; -}; - export interface TsClientOptions { trace: Trace; typescriptVersion: TypeScriptVersion; @@ -144,8 +143,7 @@ export interface TsClientOptions { maxTsServerMemory?: number; npmLocation?: string; locale?: string; - globalPlugins?: string[]; - pluginProbeLocations?: string[]; + plugins: TypeScriptPlugin[]; onEvent?: (event: ts.server.protocol.Event) => void; onExit?: (exitCode: number | null, signal: NodeJS.Signals | null) => void; useSyntaxServer: SyntaxServerConfiguration; @@ -154,6 +152,7 @@ export interface TsClientOptions { export class TsClient implements ITypeScriptServiceClient { public apiVersion: API = API.defaultVersion; public typescriptVersionSource: TypeScriptVersionSource = TypeScriptVersionSource.Bundled; + public readonly pluginManager: PluginManager; private serverState: ServerState.State = ServerState.None; private readonly lspClient: LspClient; private readonly logger: Logger; @@ -171,6 +170,7 @@ export class TsClient implements ITypeScriptServiceClient { logger: Logger, lspClient: LspClient, ) { + this.pluginManager = new PluginManager(); this.documents = new LspDocuments(this, lspClient, onCaseInsensitiveFileSystem); this.logger = new PrefixingLogger(logger, '[tsclient]'); this.tsserverLogger = new PrefixingLogger(this.logger, '[tsserver]'); @@ -312,6 +312,12 @@ export class TsClient implements ITypeScriptServiceClient { } } + public configurePlugin(pluginName: string, configuration: unknown): void { + if (this.apiVersion.gte(API.v314)) { + this.executeWithoutWaitingForResponse(CommandTypes.ConfigurePlugin, { pluginName, configuration }); + } + } + start( workspaceRoot: string | undefined, options: TsClientOptions, @@ -323,9 +329,15 @@ export class TsClient implements ITypeScriptServiceClient { this.useSyntaxServer = options.useSyntaxServer; this.onEvent = options.onEvent; this.onExit = options.onExit; + this.pluginManager.setPlugins(options.plugins); + const modeIds: string[] = [ + ...languageModeIds.jsTsLanguageModes, + ...this.pluginManager.plugins.flatMap(x => x.languages), + ]; + this.documents.initialize(modeIds); const tsServerSpawner = new TypeScriptServerSpawner(this.apiVersion, options.logDirectoryProvider, this.logger, this.tracer); - const tsServer = tsServerSpawner.spawn(options.typescriptVersion, this.capabilities, options, { + const tsServer = tsServerSpawner.spawn(options.typescriptVersion, this.capabilities, options, this.pluginManager, { onFatalError: (command, err) => this.fatalError(command, err), }); this.serverState = new ServerState.Running(tsServer, this.apiVersion, undefined, true); diff --git a/src/ts-protocol.ts b/src/ts-protocol.ts index a2a86e47..7f1ec1cd 100644 --- a/src/ts-protocol.ts +++ b/src/ts-protocol.ts @@ -337,6 +337,7 @@ export interface SupportedFeatures { export interface TypeScriptPlugin { name: string; + languages?: ReadonlyArray; location: string; } @@ -347,7 +348,7 @@ export interface TypeScriptInitializationOptions { locale?: string; maxTsServerMemory?: number; npmLocation?: string; - plugins: TypeScriptPlugin[]; + plugins?: TypeScriptPlugin[]; preferences?: ts.server.protocol.UserPreferences; tsserver?: TsserverOptions; } diff --git a/src/tsServer/plugins.ts b/src/tsServer/plugins.ts new file mode 100644 index 00000000..594d7f66 --- /dev/null +++ b/src/tsServer/plugins.ts @@ -0,0 +1,52 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +/* + * Copyright (C) 2023 TypeFox and others. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + */ + +import { URI } from 'vscode-uri'; +import * as arrays from '../utils/arrays.js'; +import { TypeScriptPlugin } from '../ts-protocol.js'; + +export interface TypeScriptServerPlugin { + readonly uri: URI; + readonly name: string; + readonly languages: ReadonlyArray; +} + +namespace TypeScriptServerPlugin { + export function equals(a: TypeScriptServerPlugin, b: TypeScriptServerPlugin): boolean { + return a.uri.toString() === b.uri.toString() + && a.name === b.name + && arrays.equals(a.languages, b.languages); + } +} + +export class PluginManager { + private _plugins?: ReadonlyArray; + + public setPlugins(plugins: TypeScriptPlugin[]): void { + this._plugins = this.readPlugins(plugins); + } + + public get plugins(): ReadonlyArray { + return Array.from(this._plugins || []); + } + + private readPlugins(plugins: TypeScriptPlugin[]) { + const newPlugins: TypeScriptServerPlugin[] = []; + for (const plugin of plugins) { + newPlugins.push({ + name: plugin.name, + uri: URI.file(plugin.location), + languages: Array.isArray(plugin.languages) ? plugin.languages : [], + }); + } + return newPlugins; + } +} diff --git a/src/tsServer/spawner.ts b/src/tsServer/spawner.ts index c44e2e10..e634c92d 100644 --- a/src/tsServer/spawner.ts +++ b/src/tsServer/spawner.ts @@ -16,6 +16,7 @@ import { Logger, LogLevel } from '../utils/logger.js'; import type { TsClientOptions } from '../ts-client.js'; import { nodeRequestCancellerFactory } from './cancellation.js'; import type { ILogDirectoryProvider } from './logDirectoryProvider.js'; +import type { PluginManager } from './plugins.js'; import { ITypeScriptServer, SingleTsServer, SyntaxRoutingTsServer, TsServerDelegate, TsServerProcessKind } from './server.js'; import { NodeTsServerProcessFactory } from './serverProcess.js'; import type Tracer from './tracer.js'; @@ -48,6 +49,7 @@ export class TypeScriptServerSpawner { version: TypeScriptVersion, capabilities: ClientCapabilities, configuration: TsClientOptions, + pluginManager: PluginManager, delegate: TsServerDelegate, ): ITypeScriptServer { let primaryServer: ITypeScriptServer; @@ -59,19 +61,19 @@ export class TypeScriptServerSpawner { { const enableDynamicRouting = serverType === CompositeServerType.DynamicSeparateSyntax; primaryServer = new SyntaxRoutingTsServer({ - syntax: this.spawnTsServer(TsServerProcessKind.Syntax, version, configuration), - semantic: this.spawnTsServer(TsServerProcessKind.Semantic, version, configuration), + syntax: this.spawnTsServer(TsServerProcessKind.Syntax, version, configuration, pluginManager), + semantic: this.spawnTsServer(TsServerProcessKind.Semantic, version, configuration, pluginManager), }, delegate, enableDynamicRouting); break; } case CompositeServerType.Single: { - primaryServer = this.spawnTsServer(TsServerProcessKind.Main, version, configuration); + primaryServer = this.spawnTsServer(TsServerProcessKind.Main, version, configuration, pluginManager); break; } case CompositeServerType.SyntaxOnly: { - primaryServer = this.spawnTsServer(TsServerProcessKind.Syntax, version, configuration); + primaryServer = this.spawnTsServer(TsServerProcessKind.Syntax, version, configuration, pluginManager); break; } } @@ -109,10 +111,11 @@ export class TypeScriptServerSpawner { kind: TsServerProcessKind, version: TypeScriptVersion, configuration: TsClientOptions, + pluginManager: PluginManager, ): ITypeScriptServer { const processFactory = new NodeTsServerProcessFactory(); const canceller = nodeRequestCancellerFactory.create(kind, this._tracer); - const { args, tsServerLogFile } = this.getTsServerArgs(kind, configuration, this._apiVersion, canceller.cancellationPipeName); + const { args, tsServerLogFile } = this.getTsServerArgs(kind, configuration, this._apiVersion, pluginManager, canceller.cancellationPipeName); if (this.isLoggingEnabled(configuration)) { if (tsServerLogFile) { @@ -152,6 +155,7 @@ export class TypeScriptServerSpawner { configuration: TsClientOptions, // currentVersion: TypeScriptVersion, apiVersion: API, + pluginManager: PluginManager, cancellationPipeName: string | undefined, ): { args: string[]; tsServerLogFile: string | undefined; tsServerTraceDirectory: string | undefined; } { const args: string[] = []; @@ -168,7 +172,7 @@ export class TypeScriptServerSpawner { args.push('--useInferredProjectPerProjectRoot'); - const { disableAutomaticTypingAcquisition, globalPlugins, locale, npmLocation, pluginProbeLocations } = configuration; + const { disableAutomaticTypingAcquisition, locale, npmLocation } = configuration; if (disableAutomaticTypingAcquisition || kind === TsServerProcessKind.Syntax || kind === TsServerProcessKind.Diagnostics) { args.push('--disableAutomaticTypingAcquisition'); @@ -197,26 +201,19 @@ export class TypeScriptServerSpawner { // args.push('--traceDirectory', tsServerTraceDirectory); // } // } - // const pluginPaths = this._pluginPathsProvider.getPluginPaths(); - // if (pluginManager.plugins.length) { - // args.push('--globalPlugins', pluginManager.plugins.map(x => x.name).join(',')); - // const isUsingBundledTypeScriptVersion = currentVersion.path === this._versionProvider.defaultVersion.path; - // for (const plugin of pluginManager.plugins) { - // if (isUsingBundledTypeScriptVersion || plugin.enableForWorkspaceTypeScriptVersions) { - // pluginPaths.push(isWeb() ? plugin.uri.toString() : plugin.uri.fsPath); - // } - // } - // } - // if (pluginPaths.length !== 0) { - // args.push('--pluginProbeLocations', pluginPaths.join(',')); - // } - if (globalPlugins?.length) { - args.push('--globalPlugins', globalPlugins.join(',')); + const pluginPaths: string[] = []; + + if (pluginManager.plugins.length) { + args.push('--globalPlugins', pluginManager.plugins.map(x => x.name).join(',')); + + for (const plugin of pluginManager.plugins) { + pluginPaths.push(plugin.uri.fsPath); + } } - if (pluginProbeLocations?.length) { - args.push('--pluginProbeLocations', pluginProbeLocations.join(',')); + if (pluginPaths.length !== 0) { + args.push('--pluginProbeLocations', pluginPaths.join(',')); } if (npmLocation) { diff --git a/src/typescriptService.ts b/src/typescriptService.ts index 4d5f3d5c..7bfad944 100644 --- a/src/typescriptService.ts +++ b/src/typescriptService.ts @@ -15,6 +15,7 @@ import { type DocumentUri } from 'vscode-languageserver-textdocument'; import type { LspDocument } from './document.js'; import { CommandTypes } from './ts-protocol.js'; import type { ts } from './ts-protocol.js'; +import { PluginManager } from './tsServer/plugins.js'; import { ExecutionTarget } from './tsServer/server.js'; import API from './utils/api.js'; @@ -111,7 +112,7 @@ export interface ITypeScriptServiceClient { readonly apiVersion: API; - // readonly pluginManager: PluginManager; + readonly pluginManager: PluginManager; // readonly configuration: TypeScriptServiceConfiguration; // readonly bufferSyncSupport: BufferSyncSupport; // readonly telemetryReporter: TelemetryReporter; diff --git a/src/utils/types.ts b/src/utils/types.ts index 60acda77..e61af11c 100644 --- a/src/utils/types.ts +++ b/src/utils/types.ts @@ -3,8 +3,13 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import type { URI } from 'vscode-uri'; import * as lsp from 'vscode-languageserver'; +export type WorkspaceFolder = { + uri: URI; +}; + export class CodeActionKind { private static readonly sep = '.'; From dafc59607b031b985c71d0b057fa8813430bc48d Mon Sep 17 00:00:00 2001 From: Rafal Chlodnicki Date: Sun, 10 Dec 2023 23:06:16 +0100 Subject: [PATCH 2/2] readme --- docs/configuration.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/configuration.md b/docs/configuration.md index 44100950..3b816123 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -28,7 +28,7 @@ The language server accepts various settings through the `initializationOptions` Accepts a list of `tsserver` (typescript) plugins. The `name` and the `location` are required. The `location` is a path to the package or a directory in which `tsserver` will try to import the plugin `name` using Node's `require` API. -The `languages` property specifying which extra language IDs the language server should accept. This is required if plugin enables support for language IDs that this server does not support by default (so other than `typescript`, `typescriptreact`, `javascript`, `javascriptreact`). It's an optional property and only affects which file types the language server allows to be opened and not the `tsserver` itself. +The `languages` property specifies which extra language IDs the language server should accept. This is required when plugin enables support for language IDs that this server does not support by default (so other than `typescript`, `typescriptreact`, `javascript`, `javascriptreact`). It's an optional property and only affects which file types the language server allows to be opened and do not concern the `tsserver` itself. ### `tsserver` options