Skip to content

feat: support specifying language IDs in plugins #834

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Dec 11, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 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

Specifies additional options related to the internal `tsserver` process, like tracing and logging:
Expand Down Expand Up @@ -218,4 +224,4 @@ implicitProjectConfiguration.strictNullChecks: boolean;
* @default 'ES2020'
*/
implicitProjectConfiguration.target: string;
```
```
7 changes: 5 additions & 2 deletions src/document.ts
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,7 @@ export class LspDocuments {
private _validateJavaScript = true;
private _validateTypeScript = true;

private readonly modeIds: Set<string>;
private modeIds: Set<string> = new Set();
private readonly _files: string[] = [];
private readonly documents = new Map<string, LspDocument>();
private readonly pendingDiagnostics: PendingDiagnostics;
Expand All @@ -242,13 +242,16 @@ export class LspDocuments {
) {
this.client = client;
this.lspClient = lspClient;
this.modeIds = new Set<string>(languageModeIds.jsTsLanguageModes);

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

public initialize(allModeIds: readonly string[]): void {
this.modeIds = new Set<string>(allModeIds);
}

/**
* Sorted by last access.
*/
Expand Down
26 changes: 3 additions & 23 deletions src/lsp-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -158,8 +148,7 @@ export class LspServer {
maxTsServerMemory,
npmLocation,
locale,
globalPlugins,
pluginProbeLocations,
plugins: plugins || [],
onEvent: this.onTsEvent.bind(this),
onExit: (exitCode, signal) => {
this.shutdown();
Expand Down Expand Up @@ -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();
Expand Down
1 change: 1 addition & 0 deletions src/ts-client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ describe('ts server client', () => {
{
logDirectoryProvider: noopLogDirectoryProvider,
logVerbosity: TsServerLogLevel.Off,
plugins: [],
trace: Trace.Off,
typescriptVersion: bundled!,
useSyntaxServer: SyntaxServerConfiguration.Never,
Expand Down
28 changes: 20 additions & 8 deletions src/ts-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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;
Expand Down Expand Up @@ -131,10 +134,6 @@ class ServerInitializingIndicator {
}
}

type WorkspaceFolder = {
uri: URI;
};

export interface TsClientOptions {
trace: Trace;
typescriptVersion: TypeScriptVersion;
Expand All @@ -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;
Expand All @@ -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;
Expand All @@ -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]');
Expand Down Expand Up @@ -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,
Expand All @@ -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);
Expand Down
3 changes: 2 additions & 1 deletion src/ts-protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -337,6 +337,7 @@ export interface SupportedFeatures {

export interface TypeScriptPlugin {
name: string;
languages?: ReadonlyArray<string>;
location: string;
}

Expand All @@ -347,7 +348,7 @@ export interface TypeScriptInitializationOptions {
locale?: string;
maxTsServerMemory?: number;
npmLocation?: string;
plugins: TypeScriptPlugin[];
plugins?: TypeScriptPlugin[];
preferences?: ts.server.protocol.UserPreferences;
tsserver?: TsserverOptions;
}
Expand Down
52 changes: 52 additions & 0 deletions src/tsServer/plugins.ts
Original file line number Diff line number Diff line change
@@ -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<string>;
}

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<TypeScriptServerPlugin>;

public setPlugins(plugins: TypeScriptPlugin[]): void {
this._plugins = this.readPlugins(plugins);
}

public get plugins(): ReadonlyArray<TypeScriptServerPlugin> {
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;
}
}
43 changes: 20 additions & 23 deletions src/tsServer/spawner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -48,6 +49,7 @@ export class TypeScriptServerSpawner {
version: TypeScriptVersion,
capabilities: ClientCapabilities,
configuration: TsClientOptions,
pluginManager: PluginManager,
delegate: TsServerDelegate,
): ITypeScriptServer {
let primaryServer: ITypeScriptServer;
Expand All @@ -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;
}
}
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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[] = [];
Expand All @@ -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');
Expand Down Expand Up @@ -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) {
Expand Down
3 changes: 2 additions & 1 deletion src/typescriptService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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;
Expand Down
Loading