support plugins

This commit is contained in:
chemzqm 2019-03-05 20:33:05 +08:00
parent a7278f7f84
commit e68adfbe0b
12 changed files with 194 additions and 29 deletions

View file

@ -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": {

View file

@ -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<void> {
interface API {
configurePlugin(pluginId: string, configuration: {}): void
}
export async function activate(context: ExtensionContext): Promise<API> {
let { subscriptions } = context
const config = workspace.getConfiguration().get<any>('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()
}
function registCommand(cmd: Command): void {
let { id, execute } = cmd
@ -27,6 +31,7 @@ export async function activate(context: ExtensionContext): Promise<void> {
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<void> {
})
}
}))
return {
configurePlugin: (pluginId: string, configuration: {}): void => {
pluginManager.setConfiguration(pluginId, configuration)
}
}
}

View file

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

View file

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

View file

@ -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<any> {
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<boolean>('enableJavascript')
this.enable = config.get<boolean>('enable')
@ -42,8 +43,9 @@ export default class TsserverService implements IServiceProvider {
}
public start(): Promise<void> {
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 => {

View file

@ -50,6 +50,11 @@ export interface ITypeScriptServiceClient {
args: Proto.ConfigureRequestArguments,
token?: CancellationToken
): Promise<Proto.ConfigureResponse>
execute(
command: 'configurePlugin',
args: Proto.ConfigurePluginRequestArguments,
token?: CancellationToken
): Promise<Proto.ConfigureResponse>
execute(
command: 'open',
args: Proto.OpenRequestArgs,

View file

@ -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<TsDiagnostics>()
@ -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 {

View file

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

View file

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

View file

@ -64,10 +64,6 @@ export class TypeScriptServiceConfiguration {
return this._configuration.get<string>('typingsCacheLocation', '')
}
public get tsServerPluginNames(): string[] {
return this._configuration.get<string[]>('pluginNames', [])
}
public get tsServerPluginRoot(): string | null {
return this._configuration.get<string | null>('tsServerPluginRoot', null)
}

21
src/utils/arrays.ts Normal file
View file

@ -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<T>(one: ReadonlyArray<T>, other: ReadonlyArray<T>, 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<T>(arr: ReadonlyArray<T>[]): T[] {
return ([] as T[]).concat.apply([], arr)
}

99
src/utils/plugins.ts Normal file
View file

@ -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<string>
}
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<string, {}>()
private _disposables = []
private _plugins: Map<string, ReadonlyArray<TypeScriptServerPlugin>> | 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<TypeScriptServerPlugin> {
if (!this._plugins) {
this._plugins = this.readPlugins()
}
return arrays.flatten(Array.from(this._plugins.values()))
}
public _register<T extends Disposable>(value: T): T {
this._disposables.push(value)
return value
}
private readonly _onDidUpdatePlugins = this._register(new Emitter<this>())
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<string, ReadonlyArray<TypeScriptServerPlugin>>()
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
}
}