1
0
Fork 0
mirror of synced 2024-06-16 05:51:11 -04:00
ultimate-vim/sources_non_forked/coc.nvim/src/services.ts
2022-07-20 13:20:15 +08:00

570 lines
18 KiB
TypeScript

'use strict'
import { SpawnOptions } from 'child_process'
import { EventEmitter } from 'events'
import fs from 'fs'
import net from 'net'
import { CancellationToken, CancellationTokenSource, Disposable, DocumentSelector, Emitter, Event } from 'vscode-languageserver-protocol'
import { TextDocument } from 'vscode-languageserver-textdocument'
import { Executable, ForkOptions, LanguageClient, LanguageClientOptions, RevealOutputChannelOn, ServerOptions, State, Transport, TransportKind } from './language-client'
import { ServiceStat } from './types'
import { disposeAll, wait } from './util'
import window from './window'
import workspace from './workspace'
const logger = require('./util/logger')('services')
interface ServiceInfo {
id: string
state: string
languageIds: string[]
}
export interface LanguageServerConfig {
module?: string
command?: string
transport?: string
transportPort?: number
disableSnippetCompletion?: boolean
disableDynamicRegister?: boolean
disabledFeatures?: string[]
formatterPriority?: number
filetypes: string[]
additionalSchemes?: string[]
enable?: boolean
args?: string[]
cwd?: string
env?: any
// socket port
port?: number
host?: string
detached?: boolean
shell?: boolean
execArgv?: string[]
rootPatterns?: string[]
ignoredRootPaths?: string[]
initializationOptions?: any
progressOnInitialization?: boolean
revealOutputChannelOn?: string
configSection?: string
stdioEncoding?: string
runtime?: string
}
export function getStateName(state: ServiceStat): string {
switch (state) {
case ServiceStat.Initial:
return 'init'
case ServiceStat.Running:
return 'running'
case ServiceStat.Starting:
return 'starting'
case ServiceStat.StartFailed:
return 'startFailed'
case ServiceStat.Stopping:
return 'stopping'
case ServiceStat.Stopped:
return 'stopped'
default:
return 'unknown'
}
}
export interface IServiceProvider {
// unique service id
id: string
name: string
client?: LanguageClient
selector: DocumentSelector
// current state
state: ServiceStat
start(): Promise<void>
dispose(): void
stop(): Promise<void> | void
restart(): Promise<void> | void
onServiceReady: Event<void>
}
export class ServiceManager extends EventEmitter implements Disposable {
private readonly registered: Map<string, IServiceProvider> = new Map()
private disposables: Disposable[] = []
public init(): void {
workspace.onDidOpenTextDocument(document => {
this.start(document)
}, null, this.disposables)
workspace.onDidChangeConfiguration(e => {
if (e.affectsConfiguration('languageserver')) {
this.createCustomServices()
}
}, null, this.disposables)
this.createCustomServices()
}
public dispose(): void {
this.removeAllListeners()
disposeAll(this.disposables)
for (let service of this.registered.values()) {
service.dispose()
}
}
public regist(service: IServiceProvider): Disposable {
let { id } = service
if (!id) logger.error('invalid service configuration. ', service.name)
if (this.registered.get(id)) return
this.registered.set(id, service)
logger.info(`registered service "${id}"`)
if (this.shouldStart(service)) {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
service.start()
}
if (service.state == ServiceStat.Running) {
this.emit('ready', id)
}
service.onServiceReady(() => {
logger.info(`service ${id} started`)
this.emit('ready', id)
}, null, this.disposables)
return Disposable.create(() => {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
service.stop()
service.dispose()
this.registered.delete(id)
})
}
public getService(id: string): IServiceProvider {
let service = this.registered.get(id)
if (!service) service = this.registered.get(`languageserver.${id}`)
return service
}
private shouldStart(service: IServiceProvider): boolean {
if (service.state != ServiceStat.Initial) {
return false
}
let selector = service.selector
for (let doc of workspace.documents) {
if (workspace.match(selector, doc.textDocument)) {
return true
}
}
return false
}
private start(document: TextDocument): void {
let services = this.getServices(document)
for (let service of services) {
if (service.state == ServiceStat.Initial) {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
service.start()
}
}
}
public getServices(document: TextDocument): IServiceProvider[] {
let res: IServiceProvider[] = []
for (let service of this.registered.values()) {
if (workspace.match(service.selector, document) > 0) {
res.push(service)
}
}
return res
}
public stop(id: string): Promise<void> {
let service = this.registered.get(id)
if (!service) {
window.showMessage(`Service ${id} not found`, 'error')
return
}
return Promise.resolve(service.stop())
}
public stopAll(): void {
for (let service of this.registered.values()) {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
service.stop()
}
}
public async toggle(id: string): Promise<void> {
let service = this.registered.get(id)
if (!service) {
window.showMessage(`Service ${id} not found`, 'error')
return
}
let { state } = service
try {
if (state == ServiceStat.Running) {
await Promise.resolve(service.stop())
} else if (state == ServiceStat.Initial) {
await service.start()
} else if (state == ServiceStat.Stopped) {
await service.restart()
}
} catch (e) {
window.showMessage(`Service error: ${e}`, 'error')
}
}
public getServiceStats(): ServiceInfo[] {
let res: ServiceInfo[] = []
for (let [id, service] of this.registered) {
res.push({
id,
languageIds: documentSelectorToLanguageIds(service.selector),
state: getStateName(service.state)
})
}
return res
}
private createCustomServices(): void {
let lspConfig = workspace.getConfiguration().get<{ key: LanguageServerConfig }>('languageserver', {} as any)
for (let key of Object.keys(lspConfig)) {
let config: LanguageServerConfig = lspConfig[key]
if (!this.validServerConfig(key, config)) {
continue
}
this.registLanguageClient(key, config)
}
}
private validServerConfig(key: string, config: LanguageServerConfig): boolean {
let errors: string[] = []
if (config.module != null && typeof config.module !== 'string') {
errors.push(`"module" field of languageserver ${key} should be string`)
}
if (config.command != null && typeof config.command !== 'string') {
errors.push(`"command" field of languageserver ${key} should be string`)
}
if (config.transport != null && typeof config.transport !== 'string') {
errors.push(`"transport" field of languageserver ${key} should be string`)
}
if (config.transportPort != null && typeof config.transportPort !== 'number') {
errors.push(`"transportPort" field of languageserver ${key} should be string`)
}
if (!Array.isArray(config.filetypes) || !config.filetypes.every(s => typeof s === 'string')) {
errors.push(`"filetypes" field of languageserver ${key} should be array of string`)
}
if (config.additionalSchemes && (!Array.isArray(config.additionalSchemes) || config.additionalSchemes.some(s => typeof s !== 'string'))) {
errors.push(`"additionalSchemes" field of languageserver ${key} should be array of string`)
}
if (errors.length) {
window.showMessage(errors.join('\n'), 'error')
return false
}
return true
}
private waitClient(id: string): Promise<void> {
let service = this.getService(id)
if (service && service.state == ServiceStat.Running) return Promise.resolve()
if (service) return new Promise(resolve => {
service.onServiceReady(() => {
resolve()
})
})
return new Promise(resolve => {
let listener = clientId => {
if (clientId == id || clientId == `languageserver.${id}`) {
this.off('ready', listener)
resolve()
}
}
this.on('ready', listener)
})
}
public async registNotification(id: string, method: string): Promise<void> {
await this.waitClient(id)
let service = this.getService(id)
if (!service.client) {
window.showMessage(`Not a language client: ${id}`, 'error')
return
}
let client = service.client
client.onNotification(method, async result => {
workspace.nvim.call('coc#do_notify', [id, method, result], true)
})
}
public async sendNotification(id: string, method: string, params?: any): Promise<void> {
if (!method) throw new Error(`method required for ontification`)
let service = this.getService(id)
// wait for extension activate
if (!service || !service.client) throw new Error(`Language server ${id} not found`)
if (service.state == ServiceStat.Starting) {
await service.client.onReady()
}
if (service.state != ServiceStat.Running) {
throw new Error(`Language server ${id} not running`)
}
await Promise.resolve(service.client.sendNotification(method, params))
}
public async sendRequest(id: string, method: string, params?: any, token?: CancellationToken): Promise<any> {
if (!method) throw new Error(`method required for sendRequest`)
let service = this.getService(id)
// wait for extension activate
if (!service) await wait(100)
service = this.getService(id)
if (!service || !service.client) {
throw new Error(`Language server ${id} not found`)
}
if (service.state == ServiceStat.Starting) {
await service.client.onReady()
}
if (service.state != ServiceStat.Running) {
throw new Error(`Language server ${id} not running`)
}
if (!token) {
let tokenSource = new CancellationTokenSource()
token = tokenSource.token
}
return await Promise.resolve(service.client.sendRequest(method, params, token))
}
public registLanguageClient(client: LanguageClient): Disposable
public registLanguageClient(name: string, config: LanguageServerConfig): Disposable
public registLanguageClient(name: string | LanguageClient, config?: LanguageServerConfig): Disposable {
let id = typeof name === 'string' ? `languageserver.${name}` : name.id
let disposables: Disposable[] = []
let onDidServiceReady = new Emitter<void>()
let client: LanguageClient | null = typeof name === 'string' ? null : name
if (this.registered.has(id)) return
let created = false
let service: IServiceProvider = {
id,
client,
name: typeof name === 'string' ? name : name.name,
selector: typeof name === 'string' ? getDocumentSelector(config.filetypes, config.additionalSchemes) : name.clientOptions.documentSelector,
state: ServiceStat.Initial,
onServiceReady: onDidServiceReady.event,
start: (): Promise<void> => {
if (service.state == ServiceStat.Starting || service.state == ServiceStat.Running) {
return
}
if (client && !client.needsStart()) {
return
}
if (created && client) {
client.restart()
return Promise.resolve()
}
if (!created) {
if (typeof name == 'string' && !client) {
let config: LanguageServerConfig = workspace.getConfiguration().get<{ key: LanguageServerConfig }>('languageserver', {} as any)[name]
if (!config || config.enable === false) return
let opts = getLanguageServerOptions(id, name, config)
if (!opts) return
client = new LanguageClient(id, name, opts[1], opts[0])
service.selector = opts[0].documentSelector
service.client = client
}
client.onDidChangeState(changeEvent => {
let { oldState, newState } = changeEvent
if (newState == State.Starting) {
service.state = ServiceStat.Starting
} else if (newState == State.Running) {
service.state = ServiceStat.Running
} else if (newState == State.Stopped) {
service.state = ServiceStat.Stopped
}
let oldStr = stateString(oldState)
let newStr = stateString(newState)
logger.info(`${client.name} state change: ${oldStr} => ${newStr}`)
}, null, disposables)
created = true
}
service.state = ServiceStat.Starting
logger.debug(`starting service: ${id}`)
let disposable = client.start()
disposables.push(disposable)
return new Promise(resolve => {
client.onReady().then(() => {
onDidServiceReady.fire(void 0)
resolve()
}, e => {
window.showMessage(`Server ${id} failed to start: ${e}`, 'error')
logger.error(`Server ${id} failed to start:`, e)
service.state = ServiceStat.StartFailed
resolve()
})
})
},
dispose: async () => {
onDidServiceReady.dispose()
disposeAll(disposables)
},
stop: async (): Promise<void> => {
if (!client || !client.needsStop()) return
await Promise.resolve(client.stop())
},
restart: async (): Promise<void> => {
if (client) {
service.state = ServiceStat.Starting
client.restart()
} else {
await service.start()
}
},
}
return this.regist(service)
}
}
export function documentSelectorToLanguageIds(documentSelector: DocumentSelector): string[] {
let res = documentSelector.map(filter => {
if (typeof filter == 'string') {
return filter
}
return filter.language
})
res = res.filter(s => typeof s == 'string')
return Array.from(new Set(res))
}
// convert config to options
export function getLanguageServerOptions(id: string, name: string, config: Readonly<LanguageServerConfig>): [LanguageClientOptions, ServerOptions] {
let { command, module, port, args, filetypes } = config
args = args || []
if (!filetypes) {
window.showMessage(`Wrong configuration of LS "${name}", filetypes not found`, 'error')
return null
}
if (!command && !module && !port) {
window.showMessage(`Wrong configuration of LS "${name}", no command or module specified.`, 'error')
return null
}
let serverOptions: ServerOptions
if (module) {
module = workspace.expand(module)
if (!fs.existsSync(module)) {
window.showMessage(`Module file "${module}" not found for LS "${name}"`, 'error')
return null
}
serverOptions = {
module,
runtime: config.runtime || process.execPath,
args,
transport: getTransportKind(config),
options: getForkOptions(config)
}
} else if (command) {
serverOptions = {
command,
args,
options: getSpawnOptions(config)
} as Executable
} else if (port) {
serverOptions = () => new Promise((resolve, reject) => {
let client = new net.Socket()
let host = config.host || '127.0.0.1'
logger.info(`languageserver "${id}" connecting to ${host}:${port}`)
client.connect(port, host, () => {
resolve({
reader: client,
writer: client
})
})
client.on('error', e => {
reject(new Error(`Connection error for ${id}: ${e.message}`))
})
})
}
// compatible
let disabledFeatures: string[] = Array.from(config.disabledFeatures || [])
for (let key of ['disableWorkspaceFolders', 'disableCompletion', 'disableDiagnostics']) {
if (config[key] === true) {
let s = key.slice(7)
disabledFeatures.push(s[0].toLowerCase() + s.slice(1))
}
}
let disableSnippetCompletion = !!config.disableSnippetCompletion
let ignoredRootPaths = config.ignoredRootPaths || []
let clientOptions: LanguageClientOptions = {
ignoredRootPaths: ignoredRootPaths.map(s => workspace.expand(s)),
disableSnippetCompletion,
disableDynamicRegister: !!config.disableDynamicRegister,
disabledFeatures,
formatterPriority: config.formatterPriority || 0,
documentSelector: getDocumentSelector(config.filetypes, config.additionalSchemes),
revealOutputChannelOn: getRevealOutputChannelOn(config.revealOutputChannelOn),
synchronize: {
configurationSection: `${id}.settings`
},
diagnosticCollectionName: name,
outputChannelName: id,
stdioEncoding: config.stdioEncoding || 'utf8',
progressOnInitialization: config.progressOnInitialization === true,
initializationOptions: config.initializationOptions || {}
}
return [clientOptions, serverOptions]
}
export function getRevealOutputChannelOn(revealOn: string | undefined): RevealOutputChannelOn {
switch (revealOn) {
case 'info':
return RevealOutputChannelOn.Info
case 'warn':
return RevealOutputChannelOn.Warn
case 'error':
return RevealOutputChannelOn.Error
case 'never':
return RevealOutputChannelOn.Never
default:
return RevealOutputChannelOn.Never
}
}
export function getDocumentSelector(filetypes: string[] | undefined, additionalSchemes?: string[]): DocumentSelector {
let documentSelector: DocumentSelector = []
let schemes = ['file', 'untitled'].concat(additionalSchemes || [])
if (!filetypes) return schemes.map(s => ({ scheme: s }))
filetypes.forEach(filetype => {
documentSelector.push(...schemes.map(scheme => ({ language: filetype, scheme })))
})
return documentSelector
}
export function getTransportKind(config: LanguageServerConfig): Transport {
let { transport, transportPort } = config
if (!transport || transport == 'ipc') return TransportKind.ipc
if (transport == 'stdio') return TransportKind.stdio
if (transport == 'pipe') return TransportKind.pipe
return { kind: TransportKind.socket, port: transportPort }
}
function getForkOptions(config: LanguageServerConfig): ForkOptions {
return {
cwd: config.cwd,
execArgv: config.execArgv || [],
env: config.env || undefined
}
}
function getSpawnOptions(config: LanguageServerConfig): SpawnOptions {
return {
cwd: config.cwd,
detached: !!config.detached,
shell: !!config.shell,
env: config.env || undefined
}
}
function stateString(state: State): string {
switch (state) {
case State.Running:
return 'running'
case State.Starting:
return 'starting'
case State.Stopped:
return 'stopped'
default:
return 'unknown'
}
}
export default new ServiceManager()