diff --git a/package.json b/package.json index e589627..7d1e059 100644 --- a/package.json +++ b/package.json @@ -131,17 +131,12 @@ ], "description": "Trace level of tsserver" }, - "tserver.pluginNames": { - "type": "array", + "tsserver.pluginRoot": { + "type": "string", "default": [], "items": { "type": "string" }, - "description": "Module names of tsserver plugins" - }, - "tsserver.pluginRoot": { - "type": "string", - "default": "", "description": "Folder contains tsserver plugins" }, "tsserver.debugPort": { diff --git a/src/index.ts b/src/index.ts index dcacd41..9b4e937 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,21 +1,25 @@ import { commands, ExtensionContext, services, workspace } from 'coc.nvim' import TsserverService from './server' -import { Command, OpenTsServerLogCommand, AutoFixCommand, ReloadProjectsCommand, TypeScriptGoToProjectConfigCommand } from './server/commands' +import { AutoFixCommand, Command, ConfigurePluginCommand, OpenTsServerLogCommand, ReloadProjectsCommand, TypeScriptGoToProjectConfigCommand } from './server/commands' import OrganizeImportsCommand from './server/organizeImports' +import { PluginManager } from './utils/plugins' -export async function activate(context: ExtensionContext): Promise { +interface API { + configurePlugin(pluginId: string, configuration: {}): void +} + +export async function activate(context: ExtensionContext): Promise { let { subscriptions } = context const config = workspace.getConfiguration().get('tsserver', {}) if (!config.enable) return - const service = new TsserverService() + const pluginManager = new PluginManager() + const service = new TsserverService(pluginManager) subscriptions.push( (services as any).regist(service) ) - if (!service.clientHost) { - await service.start() - } + await service.start() function registCommand(cmd: Command): void { let { id, execute } = cmd @@ -27,6 +31,7 @@ export async function activate(context: ExtensionContext): Promise { registCommand(new OpenTsServerLogCommand(service.clientHost)) registCommand(new TypeScriptGoToProjectConfigCommand(service.clientHost)) registCommand(new OrganizeImportsCommand(service.clientHost)) + registCommand(new ConfigurePluginCommand(pluginManager)) registCommand(commands.register({ id: 'tsserver.restart', execute: (): void => { @@ -38,4 +43,9 @@ export async function activate(context: ExtensionContext): Promise { }) } })) + return { + configurePlugin: (pluginId: string, configuration: {}): void => { + pluginManager.setConfiguration(pluginId, configuration) + } + } } diff --git a/src/server/commands.ts b/src/server/commands.ts index 2313759..826b3dc 100644 --- a/src/server/commands.ts +++ b/src/server/commands.ts @@ -7,6 +7,7 @@ import * as typeConverters from './utils/typeConverters' import { TextEdit, Range } from 'vscode-languageserver-types' import { installModules } from './utils/modules' import { nodeModules } from './utils/helper' +import { PluginManager } from '../utils/plugins' export interface Command { readonly id: string | string[] @@ -164,3 +165,15 @@ export class AutoFixCommand implements Command { if (command) await commands.executeCommand(command) } } + +export class ConfigurePluginCommand implements Command { + public readonly id = '_typescript.configurePlugin' + + public constructor( + private readonly pluginManager: PluginManager, + ) { } + + public execute(pluginId: string, configuration: any) { + this.pluginManager.setConfiguration(pluginId, configuration) + } +} diff --git a/src/server/features/completionItemProvider.ts b/src/server/features/completionItemProvider.ts index f9f7085..c0684b7 100644 --- a/src/server/features/completionItemProvider.ts +++ b/src/server/features/completionItemProvider.ts @@ -50,7 +50,7 @@ class ApplyCompletionCodeActionCommand implements CommandItem { export default class TypeScriptCompletionItemProvider implements CompletionItemProvider { - public static readonly triggerCharacters = ['.', '"', '\'', '/', '@', '<'] + public static readonly triggerCharacters = ['.', '"', '\'', '/', '@'] private completeOption: SuggestOptions private noSemicolons = false diff --git a/src/server/index.ts b/src/server/index.ts index fe24153..fb5a634 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -3,6 +3,7 @@ import { Disposable, DocumentSelector, Emitter, Event } from 'vscode-languageser import URI from 'vscode-uri' import TypeScriptServiceClientHost from './typescriptServiceClientHost' import { LanguageDescription, standardLanguageDescriptions } from './utils/languageDescription' +import { PluginManager } from '../utils/plugins' function wait(ms: number): Promise { return new Promise(resolve => { @@ -25,7 +26,7 @@ export default class TsserverService implements IServiceProvider { private readonly disposables: Disposable[] = [] private descriptions: LanguageDescription[] = [] - constructor() { + constructor(private pluginManager: PluginManager) { const config = workspace.getConfiguration('tsserver') const enableJavascript = !!config.get('enableJavascript') this.enable = config.get('enable') @@ -42,8 +43,9 @@ export default class TsserverService implements IServiceProvider { } public start(): Promise { + if (this.clientHost) return this.state = ServiceStat.Starting - this.clientHost = new TypeScriptServiceClientHost(this.descriptions) + this.clientHost = new TypeScriptServiceClientHost(this.descriptions, this.pluginManager) this.disposables.push(this.clientHost) let client = this.clientHost.serviceClient return new Promise(resolve => { diff --git a/src/server/typescriptService.ts b/src/server/typescriptService.ts index ca78276..9253e73 100644 --- a/src/server/typescriptService.ts +++ b/src/server/typescriptService.ts @@ -50,6 +50,11 @@ export interface ITypeScriptServiceClient { args: Proto.ConfigureRequestArguments, token?: CancellationToken ): Promise + execute( + command: 'configurePlugin', + args: Proto.ConfigurePluginRequestArguments, + token?: CancellationToken + ): Promise execute( command: 'open', args: Proto.OpenRequestArgs, diff --git a/src/server/typescriptServiceClient.ts b/src/server/typescriptServiceClient.ts index 7d7faf0..f7afd79 100644 --- a/src/server/typescriptServiceClient.ts +++ b/src/server/typescriptServiceClient.ts @@ -23,6 +23,7 @@ import Tracer from './utils/tracer' import { inferredProjectConfig } from './utils/tsconfig' import { TypeScriptVersion, TypeScriptVersionProvider } from './utils/versionProvider' import VersionStatus from './utils/versionStatus' +import { PluginManager } from '../utils/plugins' import { ICallback, Reader } from './utils/wireProtocol' interface CallbackItem { @@ -166,7 +167,7 @@ export default class TypeScriptServiceClient implements ITypeScriptServiceClient private _apiVersion: API private readonly disposables: Disposable[] = [] - constructor() { + constructor(private pluginManager: PluginManager) { this.pathSeparator = path.sep this.lastStart = Date.now() this.servicePromise = null @@ -180,6 +181,13 @@ export default class TypeScriptServiceClient implements ITypeScriptServiceClient this._apiVersion = API.defaultVersion this.tracer = new Tracer(this.logger) this.versionStatus = new VersionStatus(this.normalizePath.bind(this)) + pluginManager.onDidUpdateConfig(update => { + this.configurePlugin(update.pluginId, update.config) + }, null, this.disposables) + + pluginManager.onDidChangePlugins(() => { + this.restartTsServer() + }, null, this.disposables) } private _onDiagnosticsReceived = new Emitter() @@ -781,14 +789,20 @@ export default class TypeScriptServiceClient implements ITypeScriptServiceClient } if (this.apiVersion.gte(API.v230)) { - const plugins = this._configuration.tsServerPluginNames + const pluginNames = this.pluginManager.plugins.map(x => x.name) const pluginRoot = this._configuration.tsServerPluginRoot - if (plugins.length) { - args.push('--globalPlugins', plugins.join(',')) - if (pluginRoot) { - args.push('--pluginProbeLocations', pluginRoot) + const pluginPaths = pluginRoot ? [pluginRoot] : [] + + if (pluginNames.length) { + args.push('--globalPlugins', pluginNames.join(',')) + for (const plugin of this.pluginManager.plugins) { + pluginPaths.push(plugin.path) } } + + if (pluginPaths.length) { + args.push('--pluginProbeLocations', pluginPaths.join(',')) + } } if (this._configuration.typingsCacheLocation) { @@ -808,7 +822,6 @@ export default class TypeScriptServiceClient implements ITypeScriptServiceClient if (this.apiVersion.gte(API.v291)) { args.push('--noGetErrOnBackgroundUpdate') } - return args } @@ -819,6 +832,15 @@ export default class TypeScriptServiceClient implements ITypeScriptServiceClient let res = findUp.sync(['tsconfig.json', 'jsconfig.json'], { cwd: path.dirname(u.fsPath) }) return res ? path.dirname(res) : workspace.cwd } + + public configurePlugin(pluginName: string, configuration: {}): any { + if (this.apiVersion.gte(API.v314)) { + if (!this.servicePromise) return + this.servicePromise.then(() => { + this.execute('configurePlugin', { pluginName, configuration }, false) + }) + } + } } function getDiagnosticsKind(event: Proto.Event): DiagnosticKind { diff --git a/src/server/typescriptServiceClientHost.ts b/src/server/typescriptServiceClientHost.ts index 0b2a9af..958799f 100644 --- a/src/server/typescriptServiceClientHost.ts +++ b/src/server/typescriptServiceClientHost.ts @@ -12,6 +12,7 @@ import TypeScriptServiceClient from './typescriptServiceClient' import { LanguageDescription } from './utils/languageDescription' import * as typeConverters from './utils/typeConverters' import TypingsStatus, { AtaProgressReporter } from './utils/typingsStatus' +import { PluginManager } from '../utils/plugins' // Style check diagnostics that can be reported as warnings const styleCheckDiagnostics = [ @@ -32,7 +33,7 @@ export default class TypeScriptServiceClientHost implements Disposable { private readonly disposables: Disposable[] = [] private reportStyleCheckAsWarnings = true - constructor(descriptions: LanguageDescription[]) { + constructor(descriptions: LanguageDescription[], pluginManager: PluginManager) { let timer: NodeJS.Timer const handleProjectChange = () => { if (timer) clearTimeout(timer) @@ -50,7 +51,7 @@ export default class TypeScriptServiceClientHost implements Disposable { packageFileWatcher.onDidCreate(this.reloadProjects, this, this.disposables) packageFileWatcher.onDidChange(handleProjectChange, this, this.disposables) - this.client = new TypeScriptServiceClient() + this.client = new TypeScriptServiceClient(pluginManager) this.disposables.push(this.client) this.client.onDiagnosticsReceived(({ kind, resource, diagnostics }) => { this.diagnosticsReceived(kind, resource, diagnostics) diff --git a/src/server/utils/api.ts b/src/server/utils/api.ts index 082c391..d9d4b93 100644 --- a/src/server/utils/api.ts +++ b/src/server/utils/api.ts @@ -28,6 +28,7 @@ export default class API { public static readonly v292 = API.fromSimpleString('2.9.2') public static readonly v300 = API.fromSimpleString('3.0.0') public static readonly v310 = API.fromSimpleString('3.1.0') + public static readonly v314 = API.fromSimpleString('3.1.4') public static readonly v320 = API.fromSimpleString('3.2.0') public static fromVersionString(versionString: string): API { diff --git a/src/server/utils/configuration.ts b/src/server/utils/configuration.ts index a26c643..8a0bc8c 100644 --- a/src/server/utils/configuration.ts +++ b/src/server/utils/configuration.ts @@ -64,10 +64,6 @@ export class TypeScriptServiceConfiguration { return this._configuration.get('typingsCacheLocation', '') } - public get tsServerPluginNames(): string[] { - return this._configuration.get('pluginNames', []) - } - public get tsServerPluginRoot(): string | null { return this._configuration.get('tsServerPluginRoot', null) } diff --git a/src/utils/arrays.ts b/src/utils/arrays.ts new file mode 100644 index 0000000..aacd3be --- /dev/null +++ b/src/utils/arrays.ts @@ -0,0 +1,21 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +export function equals(one: ReadonlyArray, other: ReadonlyArray, itemEquals: (a: T, b: T) => boolean = (a, b) => a === b): boolean { + if (one.length !== other.length) { + return false + } + + for (let i = 0, len = one.length; i < len; i++) { + if (!itemEquals(one[i], other[i])) { + return false + } + } + + return true +} + +export function flatten(arr: ReadonlyArray[]): T[] { + return ([] as T[]).concat.apply([], arr) +} diff --git a/src/utils/plugins.ts b/src/utils/plugins.ts new file mode 100644 index 0000000..b09696e --- /dev/null +++ b/src/utils/plugins.ts @@ -0,0 +1,99 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { extensions, disposeAll } from 'coc.nvim' +import * as arrays from './arrays' +import { Disposable, Emitter } from 'vscode-languageserver-protocol' + +export interface TypeScriptServerPlugin { + readonly path: string + readonly name: string + readonly enableForWorkspaceTypeScriptVersions: boolean + readonly languages: ReadonlyArray +} + +namespace TypeScriptServerPlugin { + export function equals(a: TypeScriptServerPlugin, b: TypeScriptServerPlugin): boolean { + return a.path === b.path + && a.name === b.name + && a.enableForWorkspaceTypeScriptVersions === b.enableForWorkspaceTypeScriptVersions + && arrays.equals(a.languages, b.languages) + } +} + +export class PluginManager implements Disposable { + private readonly _pluginConfigurations = new Map() + private _disposables = [] + + private _plugins: Map> | undefined + + constructor() { + let loadPlugins = () => { + if (!this._plugins) { + return + } + const newPlugins = this.readPlugins() + if (!arrays.equals(arrays.flatten(Array.from(this._plugins.values())), arrays.flatten(Array.from(newPlugins.values())), TypeScriptServerPlugin.equals)) { + this._plugins = newPlugins + this._onDidUpdatePlugins.fire(this) + } + } + extensions.onDidActiveExtension(loadPlugins, undefined, this._disposables) + extensions.onDidUnloadExtension(loadPlugins, undefined, this._disposables) + } + + public dispose(): void { + disposeAll(this._disposables) + } + + public get plugins(): ReadonlyArray { + if (!this._plugins) { + this._plugins = this.readPlugins() + } + return arrays.flatten(Array.from(this._plugins.values())) + } + + public _register(value: T): T { + this._disposables.push(value) + return value + } + + private readonly _onDidUpdatePlugins = this._register(new Emitter()) + public readonly onDidChangePlugins = this._onDidUpdatePlugins.event + + private readonly _onDidUpdateConfig = this._register(new Emitter<{ pluginId: string, config: {} }>()) + public readonly onDidUpdateConfig = this._onDidUpdateConfig.event + + public setConfiguration(pluginId: string, config: {}) { + this._pluginConfigurations.set(pluginId, config) + this._onDidUpdateConfig.fire({ pluginId, config }) + } + + public configurations(): IterableIterator<[string, {}]> { + return this._pluginConfigurations.entries() + } + + private readPlugins() { + const pluginMap = new Map>() + for (const extension of extensions.all) { + const pack = extension.packageJSON + if (pack.contributes && Array.isArray(pack.contributes.typescriptServerPlugins)) { + const plugins: TypeScriptServerPlugin[] = [] + for (const plugin of pack.contributes.typescriptServerPlugins) { + plugins.push({ + name: plugin.name, + enableForWorkspaceTypeScriptVersions: !!plugin.enableForWorkspaceTypeScriptVersions, + path: extension.extensionPath, + languages: Array.isArray(plugin.languages) ? plugin.languages : [], + }) + } + if (plugins.length) { + pluginMap.set(extension.id, plugins) + } + } + } + return pluginMap + } +}