Skip to content

Commit e9c0b11

Browse files
authored
feat: support specifying language IDs in plugins (typescript-language-server#834)
1 parent 04abbbf commit e9c0b11

10 files changed

+118
-60
lines changed

docs/configuration.md

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,16 @@ The language server accepts various settings through the `initializationOptions`
2020
| 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` |
2121
| npmLocation | string | Specifies the path to the NPM executable used for Automatic Type Acquisition. |
2222
| locale | string | The locale to use to show error messages. |
23-
| plugins | object[] | An array of `{ name: string, location: string }` objects for registering a Typescript plugins. **Default**: [] |
23+
| plugins | object[] | An array of `{ name: string, location: string, languages?: string[] }` objects for registering a Typescript plugins. **Default**: [] |
2424
| preferences | object | Preferences passed to the Typescript (`tsserver`) process. See below for more |
2525
| tsserver | object | Options related to the `tsserver` process. See below for more |
2626

27+
### `plugins` option
28+
29+
Accepts a list of `tsserver` (typescript) plugins.
30+
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.
31+
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.
32+
2733
### `tsserver` options
2834

2935
Specifies additional options related to the internal `tsserver` process, like tracing and logging:
@@ -218,4 +224,4 @@ implicitProjectConfiguration.strictNullChecks: boolean;
218224
* @default 'ES2020'
219225
*/
220226
implicitProjectConfiguration.target: string;
221-
```
227+
```

src/document.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -228,7 +228,7 @@ export class LspDocuments {
228228
private _validateJavaScript = true;
229229
private _validateTypeScript = true;
230230

231-
private readonly modeIds: Set<string>;
231+
private modeIds: Set<string> = new Set();
232232
private readonly _files: string[] = [];
233233
private readonly documents = new Map<string, LspDocument>();
234234
private readonly pendingDiagnostics: PendingDiagnostics;
@@ -242,13 +242,16 @@ export class LspDocuments {
242242
) {
243243
this.client = client;
244244
this.lspClient = lspClient;
245-
this.modeIds = new Set<string>(languageModeIds.jsTsLanguageModes);
246245

247246
const pathNormalizer = (path: URI) => this.client.toTsFilePath(path.toString());
248247
this.pendingDiagnostics = new PendingDiagnostics(pathNormalizer, { onCaseInsensitiveFileSystem });
249248
this.diagnosticDelayer = new Delayer<any>(300);
250249
}
251250

251+
public initialize(allModeIds: readonly string[]): void {
252+
this.modeIds = new Set<string>(allModeIds);
253+
}
254+
252255
/**
253256
* Sorted by last access.
254257
*/

src/lsp-server.ts

Lines changed: 3 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -95,17 +95,7 @@ export class LspServer {
9595
this.workspaceRoot = this.initializeParams.rootUri ? URI.parse(this.initializeParams.rootUri).fsPath : this.initializeParams.rootPath || undefined;
9696

9797
const userInitializationOptions: TypeScriptInitializationOptions = this.initializeParams.initializationOptions || {};
98-
const { disableAutomaticTypingAcquisition, hostInfo, maxTsServerMemory, npmLocation, locale, tsserver } = userInitializationOptions;
99-
const { plugins }: TypeScriptInitializationOptions = {
100-
plugins: userInitializationOptions.plugins || [],
101-
};
102-
103-
const globalPlugins: string[] = [];
104-
const pluginProbeLocations: string[] = [];
105-
for (const plugin of plugins) {
106-
globalPlugins.push(plugin.name);
107-
pluginProbeLocations.push(plugin.location);
108-
}
98+
const { disableAutomaticTypingAcquisition, hostInfo, maxTsServerMemory, npmLocation, locale, plugins, tsserver } = userInitializationOptions;
10999

110100
const typescriptVersion = this.findTypescriptVersion(tsserver?.path, tsserver?.fallbackPath);
111101
if (typescriptVersion) {
@@ -158,8 +148,7 @@ export class LspServer {
158148
maxTsServerMemory,
159149
npmLocation,
160150
locale,
161-
globalPlugins,
162-
pluginProbeLocations,
151+
plugins: plugins || [],
163152
onEvent: this.onTsEvent.bind(this),
164153
onExit: (exitCode, signal) => {
165154
this.shutdown();
@@ -886,16 +875,7 @@ export class LspServer {
886875
}
887876
} else if (params.command === Commands.CONFIGURE_PLUGIN && params.arguments) {
888877
const [pluginName, configuration] = params.arguments as [string, unknown];
889-
890-
if (this.tsClient.apiVersion.gte(API.v314)) {
891-
this.tsClient.executeWithoutWaitingForResponse(
892-
CommandTypes.ConfigurePlugin,
893-
{
894-
configuration,
895-
pluginName,
896-
},
897-
);
898-
}
878+
this.tsClient.configurePlugin(pluginName, configuration);
899879
} else if (params.command === Commands.ORGANIZE_IMPORTS && params.arguments) {
900880
const file = params.arguments[0] as string;
901881
const uri = this.tsClient.toResource(file).toString();

src/ts-client.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ describe('ts server client', () => {
3535
{
3636
logDirectoryProvider: noopLogDirectoryProvider,
3737
logVerbosity: TsServerLogLevel.Off,
38+
plugins: [],
3839
trace: Trace.Off,
3940
typescriptVersion: bundled!,
4041
useSyntaxServer: SyntaxServerConfiguration.Never,

src/ts-client.ts

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,12 @@ import { type DocumentUri } from 'vscode-languageserver-textdocument';
1717
import { type CancellationToken, CancellationTokenSource } from 'vscode-jsonrpc';
1818
import { type LspDocument, LspDocuments } from './document.js';
1919
import * as fileSchemes from './configuration/fileSchemes.js';
20+
import * as languageModeIds from './configuration/languageIds.js';
2021
import { CommandTypes, EventName } from './ts-protocol.js';
21-
import type { ts } from './ts-protocol.js';
22+
import type { TypeScriptPlugin, ts } from './ts-protocol.js';
2223
import type { ILogDirectoryProvider } from './tsServer/logDirectoryProvider.js';
2324
import { AsyncTsServerRequests, ClientCapabilities, ClientCapability, ExecConfig, NoResponseTsServerRequests, ITypeScriptServiceClient, ServerResponse, StandardTsServerRequests, TypeScriptRequestTypes } from './typescriptService.js';
25+
import { PluginManager } from './tsServer/plugins.js';
2426
import type { ITypeScriptServer, TypeScriptServerExitEvent } from './tsServer/server.js';
2527
import { TypeScriptServerError } from './tsServer/serverError.js';
2628
import { TypeScriptServerSpawner } from './tsServer/spawner.js';
@@ -30,6 +32,7 @@ import type { LspClient } from './lsp-client.js';
3032
import API from './utils/api.js';
3133
import { SyntaxServerConfiguration, TsServerLogLevel } from './utils/configuration.js';
3234
import { Logger, PrefixingLogger } from './utils/logger.js';
35+
import type { WorkspaceFolder } from './utils/types.js';
3336

3437
interface ToCancelOnResourceChanged {
3538
readonly resource: string;
@@ -131,10 +134,6 @@ class ServerInitializingIndicator {
131134
}
132135
}
133136

134-
type WorkspaceFolder = {
135-
uri: URI;
136-
};
137-
138137
export interface TsClientOptions {
139138
trace: Trace;
140139
typescriptVersion: TypeScriptVersion;
@@ -144,8 +143,7 @@ export interface TsClientOptions {
144143
maxTsServerMemory?: number;
145144
npmLocation?: string;
146145
locale?: string;
147-
globalPlugins?: string[];
148-
pluginProbeLocations?: string[];
146+
plugins: TypeScriptPlugin[];
149147
onEvent?: (event: ts.server.protocol.Event) => void;
150148
onExit?: (exitCode: number | null, signal: NodeJS.Signals | null) => void;
151149
useSyntaxServer: SyntaxServerConfiguration;
@@ -154,6 +152,7 @@ export interface TsClientOptions {
154152
export class TsClient implements ITypeScriptServiceClient {
155153
public apiVersion: API = API.defaultVersion;
156154
public typescriptVersionSource: TypeScriptVersionSource = TypeScriptVersionSource.Bundled;
155+
public readonly pluginManager: PluginManager;
157156
private serverState: ServerState.State = ServerState.None;
158157
private readonly lspClient: LspClient;
159158
private readonly logger: Logger;
@@ -171,6 +170,7 @@ export class TsClient implements ITypeScriptServiceClient {
171170
logger: Logger,
172171
lspClient: LspClient,
173172
) {
173+
this.pluginManager = new PluginManager();
174174
this.documents = new LspDocuments(this, lspClient, onCaseInsensitiveFileSystem);
175175
this.logger = new PrefixingLogger(logger, '[tsclient]');
176176
this.tsserverLogger = new PrefixingLogger(this.logger, '[tsserver]');
@@ -312,6 +312,12 @@ export class TsClient implements ITypeScriptServiceClient {
312312
}
313313
}
314314

315+
public configurePlugin(pluginName: string, configuration: unknown): void {
316+
if (this.apiVersion.gte(API.v314)) {
317+
this.executeWithoutWaitingForResponse(CommandTypes.ConfigurePlugin, { pluginName, configuration });
318+
}
319+
}
320+
315321
start(
316322
workspaceRoot: string | undefined,
317323
options: TsClientOptions,
@@ -323,9 +329,15 @@ export class TsClient implements ITypeScriptServiceClient {
323329
this.useSyntaxServer = options.useSyntaxServer;
324330
this.onEvent = options.onEvent;
325331
this.onExit = options.onExit;
332+
this.pluginManager.setPlugins(options.plugins);
333+
const modeIds: string[] = [
334+
...languageModeIds.jsTsLanguageModes,
335+
...this.pluginManager.plugins.flatMap(x => x.languages),
336+
];
337+
this.documents.initialize(modeIds);
326338

327339
const tsServerSpawner = new TypeScriptServerSpawner(this.apiVersion, options.logDirectoryProvider, this.logger, this.tracer);
328-
const tsServer = tsServerSpawner.spawn(options.typescriptVersion, this.capabilities, options, {
340+
const tsServer = tsServerSpawner.spawn(options.typescriptVersion, this.capabilities, options, this.pluginManager, {
329341
onFatalError: (command, err) => this.fatalError(command, err),
330342
});
331343
this.serverState = new ServerState.Running(tsServer, this.apiVersion, undefined, true);

src/ts-protocol.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -337,6 +337,7 @@ export interface SupportedFeatures {
337337

338338
export interface TypeScriptPlugin {
339339
name: string;
340+
languages?: ReadonlyArray<string>;
340341
location: string;
341342
}
342343

@@ -347,7 +348,7 @@ export interface TypeScriptInitializationOptions {
347348
locale?: string;
348349
maxTsServerMemory?: number;
349350
npmLocation?: string;
350-
plugins: TypeScriptPlugin[];
351+
plugins?: TypeScriptPlugin[];
351352
preferences?: ts.server.protocol.UserPreferences;
352353
tsserver?: TsserverOptions;
353354
}

src/tsServer/plugins.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
/*
6+
* Copyright (C) 2023 TypeFox and others.
7+
*
8+
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License.
9+
* You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
10+
*/
11+
12+
import { URI } from 'vscode-uri';
13+
import * as arrays from '../utils/arrays.js';
14+
import { TypeScriptPlugin } from '../ts-protocol.js';
15+
16+
export interface TypeScriptServerPlugin {
17+
readonly uri: URI;
18+
readonly name: string;
19+
readonly languages: ReadonlyArray<string>;
20+
}
21+
22+
namespace TypeScriptServerPlugin {
23+
export function equals(a: TypeScriptServerPlugin, b: TypeScriptServerPlugin): boolean {
24+
return a.uri.toString() === b.uri.toString()
25+
&& a.name === b.name
26+
&& arrays.equals(a.languages, b.languages);
27+
}
28+
}
29+
30+
export class PluginManager {
31+
private _plugins?: ReadonlyArray<TypeScriptServerPlugin>;
32+
33+
public setPlugins(plugins: TypeScriptPlugin[]): void {
34+
this._plugins = this.readPlugins(plugins);
35+
}
36+
37+
public get plugins(): ReadonlyArray<TypeScriptServerPlugin> {
38+
return Array.from(this._plugins || []);
39+
}
40+
41+
private readPlugins(plugins: TypeScriptPlugin[]) {
42+
const newPlugins: TypeScriptServerPlugin[] = [];
43+
for (const plugin of plugins) {
44+
newPlugins.push({
45+
name: plugin.name,
46+
uri: URI.file(plugin.location),
47+
languages: Array.isArray(plugin.languages) ? plugin.languages : [],
48+
});
49+
}
50+
return newPlugins;
51+
}
52+
}

src/tsServer/spawner.ts

Lines changed: 20 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { Logger, LogLevel } from '../utils/logger.js';
1616
import type { TsClientOptions } from '../ts-client.js';
1717
import { nodeRequestCancellerFactory } from './cancellation.js';
1818
import type { ILogDirectoryProvider } from './logDirectoryProvider.js';
19+
import type { PluginManager } from './plugins.js';
1920
import { ITypeScriptServer, SingleTsServer, SyntaxRoutingTsServer, TsServerDelegate, TsServerProcessKind } from './server.js';
2021
import { NodeTsServerProcessFactory } from './serverProcess.js';
2122
import type Tracer from './tracer.js';
@@ -48,6 +49,7 @@ export class TypeScriptServerSpawner {
4849
version: TypeScriptVersion,
4950
capabilities: ClientCapabilities,
5051
configuration: TsClientOptions,
52+
pluginManager: PluginManager,
5153
delegate: TsServerDelegate,
5254
): ITypeScriptServer {
5355
let primaryServer: ITypeScriptServer;
@@ -59,19 +61,19 @@ export class TypeScriptServerSpawner {
5961
{
6062
const enableDynamicRouting = serverType === CompositeServerType.DynamicSeparateSyntax;
6163
primaryServer = new SyntaxRoutingTsServer({
62-
syntax: this.spawnTsServer(TsServerProcessKind.Syntax, version, configuration),
63-
semantic: this.spawnTsServer(TsServerProcessKind.Semantic, version, configuration),
64+
syntax: this.spawnTsServer(TsServerProcessKind.Syntax, version, configuration, pluginManager),
65+
semantic: this.spawnTsServer(TsServerProcessKind.Semantic, version, configuration, pluginManager),
6466
}, delegate, enableDynamicRouting);
6567
break;
6668
}
6769
case CompositeServerType.Single:
6870
{
69-
primaryServer = this.spawnTsServer(TsServerProcessKind.Main, version, configuration);
71+
primaryServer = this.spawnTsServer(TsServerProcessKind.Main, version, configuration, pluginManager);
7072
break;
7173
}
7274
case CompositeServerType.SyntaxOnly:
7375
{
74-
primaryServer = this.spawnTsServer(TsServerProcessKind.Syntax, version, configuration);
76+
primaryServer = this.spawnTsServer(TsServerProcessKind.Syntax, version, configuration, pluginManager);
7577
break;
7678
}
7779
}
@@ -109,10 +111,11 @@ export class TypeScriptServerSpawner {
109111
kind: TsServerProcessKind,
110112
version: TypeScriptVersion,
111113
configuration: TsClientOptions,
114+
pluginManager: PluginManager,
112115
): ITypeScriptServer {
113116
const processFactory = new NodeTsServerProcessFactory();
114117
const canceller = nodeRequestCancellerFactory.create(kind, this._tracer);
115-
const { args, tsServerLogFile } = this.getTsServerArgs(kind, configuration, this._apiVersion, canceller.cancellationPipeName);
118+
const { args, tsServerLogFile } = this.getTsServerArgs(kind, configuration, this._apiVersion, pluginManager, canceller.cancellationPipeName);
116119

117120
if (this.isLoggingEnabled(configuration)) {
118121
if (tsServerLogFile) {
@@ -152,6 +155,7 @@ export class TypeScriptServerSpawner {
152155
configuration: TsClientOptions,
153156
// currentVersion: TypeScriptVersion,
154157
apiVersion: API,
158+
pluginManager: PluginManager,
155159
cancellationPipeName: string | undefined,
156160
): { args: string[]; tsServerLogFile: string | undefined; tsServerTraceDirectory: string | undefined; } {
157161
const args: string[] = [];
@@ -168,7 +172,7 @@ export class TypeScriptServerSpawner {
168172

169173
args.push('--useInferredProjectPerProjectRoot');
170174

171-
const { disableAutomaticTypingAcquisition, globalPlugins, locale, npmLocation, pluginProbeLocations } = configuration;
175+
const { disableAutomaticTypingAcquisition, locale, npmLocation } = configuration;
172176

173177
if (disableAutomaticTypingAcquisition || kind === TsServerProcessKind.Syntax || kind === TsServerProcessKind.Diagnostics) {
174178
args.push('--disableAutomaticTypingAcquisition');
@@ -197,26 +201,19 @@ export class TypeScriptServerSpawner {
197201
// args.push('--traceDirectory', tsServerTraceDirectory);
198202
// }
199203
// }
200-
// const pluginPaths = this._pluginPathsProvider.getPluginPaths();
201-
// if (pluginManager.plugins.length) {
202-
// args.push('--globalPlugins', pluginManager.plugins.map(x => x.name).join(','));
203-
// const isUsingBundledTypeScriptVersion = currentVersion.path === this._versionProvider.defaultVersion.path;
204-
// for (const plugin of pluginManager.plugins) {
205-
// if (isUsingBundledTypeScriptVersion || plugin.enableForWorkspaceTypeScriptVersions) {
206-
// pluginPaths.push(isWeb() ? plugin.uri.toString() : plugin.uri.fsPath);
207-
// }
208-
// }
209-
// }
210-
// if (pluginPaths.length !== 0) {
211-
// args.push('--pluginProbeLocations', pluginPaths.join(','));
212-
// }
213204

214-
if (globalPlugins?.length) {
215-
args.push('--globalPlugins', globalPlugins.join(','));
205+
const pluginPaths: string[] = [];
206+
207+
if (pluginManager.plugins.length) {
208+
args.push('--globalPlugins', pluginManager.plugins.map(x => x.name).join(','));
209+
210+
for (const plugin of pluginManager.plugins) {
211+
pluginPaths.push(plugin.uri.fsPath);
212+
}
216213
}
217214

218-
if (pluginProbeLocations?.length) {
219-
args.push('--pluginProbeLocations', pluginProbeLocations.join(','));
215+
if (pluginPaths.length !== 0) {
216+
args.push('--pluginProbeLocations', pluginPaths.join(','));
220217
}
221218

222219
if (npmLocation) {

src/typescriptService.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { type DocumentUri } from 'vscode-languageserver-textdocument';
1515
import type { LspDocument } from './document.js';
1616
import { CommandTypes } from './ts-protocol.js';
1717
import type { ts } from './ts-protocol.js';
18+
import { PluginManager } from './tsServer/plugins.js';
1819
import { ExecutionTarget } from './tsServer/server.js';
1920
import API from './utils/api.js';
2021

@@ -111,7 +112,7 @@ export interface ITypeScriptServiceClient {
111112

112113
readonly apiVersion: API;
113114

114-
// readonly pluginManager: PluginManager;
115+
readonly pluginManager: PluginManager;
115116
// readonly configuration: TypeScriptServiceConfiguration;
116117
// readonly bufferSyncSupport: BufferSyncSupport;
117118
// readonly telemetryReporter: TelemetryReporter;

0 commit comments

Comments
 (0)