949 lines
32 KiB
TypeScript
949 lines
32 KiB
TypeScript
/*---------------------------------------------------------------------------------------------
|
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
* Licensed under the MIT License. See License.txt in the project root for license information.
|
|
*--------------------------------------------------------------------------------------------*/
|
|
import { Document, ServiceStat, Uri, window, workspace } from 'coc.nvim'
|
|
import fs from 'fs'
|
|
import os from 'os'
|
|
import path from 'path'
|
|
import { CancellationToken, CancellationTokenSource, Disposable, Emitter, Event } from 'vscode-languageserver-protocol'
|
|
import * as fileSchemes from '../utils/fileSchemes'
|
|
import { PluginManager } from '../utils/plugins'
|
|
import { CallbackMap } from './callbackMap'
|
|
import BufferSyncSupport from './features/bufferSyncSupport'
|
|
import { DiagnosticKind, DiagnosticsManager } from './features/diagnostics'
|
|
import FileConfigurationManager from './features/fileConfigurationManager'
|
|
import * as Proto from './protocol'
|
|
import { RequestItem, RequestQueue, RequestQueueingType } from './requestQueue'
|
|
import { ExecConfig, ITypeScriptServiceClient, ServerResponse } from './typescriptService'
|
|
import API from './utils/api'
|
|
import { TsServerLogLevel, TypeScriptServiceConfiguration } from './utils/configuration'
|
|
import Logger from './utils/logger'
|
|
import { fork, getTempDirectory, createTempDirectory, getTempFile, IForkOptions, makeRandomHexString } from './utils/process'
|
|
import Tracer from './utils/tracer'
|
|
import { inferredProjectConfig } from './utils/tsconfig'
|
|
import { TypeScriptVersion, TypeScriptVersionProvider } from './utils/versionProvider'
|
|
import VersionStatus from './utils/versionStatus'
|
|
import ForkedTsServerProcess, { ToCancelOnResourceChanged } from './tsServerProcess'
|
|
|
|
export interface TsDiagnostics {
|
|
readonly kind: DiagnosticKind
|
|
readonly resource: Uri
|
|
readonly diagnostics: Proto.Diagnostic[]
|
|
}
|
|
|
|
export default class TypeScriptServiceClient implements ITypeScriptServiceClient {
|
|
private token: number = 0
|
|
public state = ServiceStat.Initial
|
|
public readonly logger: Logger = new Logger()
|
|
public readonly bufferSyncSupport: BufferSyncSupport
|
|
public readonly diagnosticsManager: DiagnosticsManager
|
|
|
|
private fileConfigurationManager: FileConfigurationManager
|
|
private pathSeparator: string
|
|
private tracer: Tracer
|
|
private _configuration: TypeScriptServiceConfiguration
|
|
private versionProvider: TypeScriptVersionProvider
|
|
private tsServerLogFile: string | null = null
|
|
private tsServerProcess: ForkedTsServerProcess | undefined
|
|
private lastStart: number
|
|
private numberRestarts: number
|
|
private cancellationPipeName: string | null = null
|
|
private _callbacks = new CallbackMap<Proto.Response>()
|
|
private _requestQueue = new RequestQueue()
|
|
private _pendingResponses = new Set<number>()
|
|
private _onReady?: { promise: Promise<void>; resolve: () => void; reject: () => void }
|
|
|
|
private versionStatus: VersionStatus
|
|
private readonly _onTsServerStarted = new Emitter<API>()
|
|
private readonly _onProjectLanguageServiceStateChanged = new Emitter<Proto.ProjectLanguageServiceStateEventBody>()
|
|
private readonly _onDidBeginInstallTypings = new Emitter<Proto.BeginInstallTypesEventBody>()
|
|
private readonly _onDidEndInstallTypings = new Emitter<Proto.EndInstallTypesEventBody>()
|
|
private readonly _onTypesInstallerInitializationFailed = new Emitter<
|
|
Proto.TypesInstallerInitializationFailedEventBody
|
|
>()
|
|
private _apiVersion: API
|
|
private _tscPath: string
|
|
private readonly disposables: Disposable[] = []
|
|
private isRestarting = false
|
|
|
|
constructor(
|
|
public readonly pluginManager: PluginManager,
|
|
public readonly modeIds: string[]
|
|
) {
|
|
this.pathSeparator = path.sep
|
|
this.lastStart = Date.now()
|
|
this.numberRestarts = 0
|
|
let resolve: () => void
|
|
let reject: () => void
|
|
const p = new Promise<void>((res, rej) => {
|
|
resolve = res
|
|
reject = rej
|
|
})
|
|
this._onReady = { promise: p, resolve: resolve!, reject: reject! }
|
|
|
|
this.fileConfigurationManager = new FileConfigurationManager(this)
|
|
this._configuration = TypeScriptServiceConfiguration.loadFromWorkspace()
|
|
this.versionProvider = new TypeScriptVersionProvider(this._configuration)
|
|
this._apiVersion = API.defaultVersion
|
|
this.tracer = new Tracer(this.logger)
|
|
this.versionStatus = new VersionStatus(this.normalizePath.bind(this), this.fileConfigurationManager.enableJavascript())
|
|
pluginManager.onDidUpdateConfig(update => {
|
|
this.configurePlugin(update.pluginId, update.config)
|
|
}, null, this.disposables)
|
|
|
|
pluginManager.onDidChangePlugins(() => {
|
|
this.restartTsServer()
|
|
}, null, this.disposables)
|
|
|
|
this.bufferSyncSupport = new BufferSyncSupport(this, modeIds)
|
|
this.onReady(() => {
|
|
this.bufferSyncSupport.listen()
|
|
})
|
|
|
|
this.diagnosticsManager = new DiagnosticsManager()
|
|
this.bufferSyncSupport.onDelete(resource => {
|
|
this.cancelInflightRequestsForResource(resource)
|
|
this.diagnosticsManager.delete(resource)
|
|
}, null, this.disposables)
|
|
this.bufferSyncSupport.onWillChange(resource => {
|
|
this.cancelInflightRequestsForResource(resource)
|
|
})
|
|
}
|
|
|
|
private _onDiagnosticsReceived = new Emitter<TsDiagnostics>()
|
|
public get onDiagnosticsReceived(): Event<TsDiagnostics> {
|
|
return this._onDiagnosticsReceived.event
|
|
}
|
|
|
|
private _onConfigDiagnosticsReceived = new Emitter<Proto.ConfigFileDiagnosticEvent>()
|
|
public get onConfigDiagnosticsReceived(): Event<Proto.ConfigFileDiagnosticEvent> {
|
|
return this._onConfigDiagnosticsReceived.event
|
|
}
|
|
|
|
private _onResendModelsRequested = new Emitter<void>()
|
|
public get onResendModelsRequested(): Event<void> {
|
|
return this._onResendModelsRequested.event
|
|
}
|
|
|
|
public get configuration(): TypeScriptServiceConfiguration {
|
|
return this._configuration
|
|
}
|
|
|
|
public onReady(f: () => void): Promise<void> {
|
|
return this._onReady!.promise.then(f)
|
|
}
|
|
|
|
public dispose(): void {
|
|
this.tsServerProcess.kill()
|
|
this.bufferSyncSupport.dispose()
|
|
this.logger.dispose()
|
|
this._onTsServerStarted.dispose()
|
|
this._onResendModelsRequested.dispose()
|
|
this.versionStatus.dispose()
|
|
}
|
|
|
|
private info(message: string, data?: any): void {
|
|
this.logger.info(message, data)
|
|
}
|
|
|
|
private error(message: string, data?: any): void {
|
|
this.logger.error(message, data)
|
|
}
|
|
|
|
public restartTsServer(): void {
|
|
if (this.tsServerProcess) {
|
|
this.state = ServiceStat.Stopping
|
|
this.info('Killing TS Server')
|
|
this.isRestarting = true
|
|
this.tsServerProcess.kill()
|
|
}
|
|
this.startService(true)
|
|
}
|
|
|
|
public stop(): Promise<void> {
|
|
return new Promise(resolve => {
|
|
let { tsServerProcess } = this
|
|
if (tsServerProcess && this.state == ServiceStat.Running) {
|
|
this.info('Killing TS Server')
|
|
tsServerProcess.onExit(() => {
|
|
resolve()
|
|
})
|
|
tsServerProcess.kill()
|
|
} else {
|
|
resolve()
|
|
}
|
|
})
|
|
}
|
|
|
|
public get onTsServerStarted(): Event<API> {
|
|
return this._onTsServerStarted.event
|
|
}
|
|
|
|
public get onProjectLanguageServiceStateChanged(): Event<
|
|
Proto.ProjectLanguageServiceStateEventBody
|
|
> {
|
|
return this._onProjectLanguageServiceStateChanged.event
|
|
}
|
|
|
|
public get onDidBeginInstallTypings(): Event<Proto.BeginInstallTypesEventBody> {
|
|
return this._onDidBeginInstallTypings.event
|
|
}
|
|
|
|
public get onDidEndInstallTypings(): Event<Proto.EndInstallTypesEventBody> {
|
|
return this._onDidEndInstallTypings.event
|
|
}
|
|
|
|
public get onTypesInstallerInitializationFailed(): Event<Proto.TypesInstallerInitializationFailedEventBody> {
|
|
return this._onTypesInstallerInitializationFailed.event
|
|
}
|
|
|
|
public get apiVersion(): API {
|
|
return this._apiVersion
|
|
}
|
|
|
|
public get tscPath(): string {
|
|
return this._tscPath
|
|
}
|
|
|
|
public ensureServiceStarted(): void {
|
|
if (!this.tsServerProcess) {
|
|
this.startService()
|
|
}
|
|
}
|
|
|
|
private startService(resendModels = false): ForkedTsServerProcess | undefined {
|
|
const { ignoreLocalTsserver } = this.configuration
|
|
let currentVersion: TypeScriptVersion
|
|
if (!ignoreLocalTsserver) currentVersion = this.versionProvider.getLocalVersion()
|
|
if (!currentVersion || !fs.existsSync(currentVersion.tsServerPath)) {
|
|
this.info('Local tsserver not found, using bundled tsserver with coc-tsserver.')
|
|
currentVersion = this.versionProvider.getDefaultVersion()
|
|
}
|
|
if (!currentVersion || !currentVersion.isValid) {
|
|
if (this.configuration.globalTsdk) {
|
|
window.showErrorMessage(`Can not find typescript module, in 'tsserver.tsdk': ${this.configuration.globalTsdk}`)
|
|
} else {
|
|
window.showErrorMessage(`Can not find typescript module, run ':CocInstall coc-tsserver' to fix it!`)
|
|
}
|
|
return
|
|
}
|
|
this._apiVersion = currentVersion.version
|
|
this._tscPath = currentVersion.tscPath
|
|
this.versionStatus.onDidChangeTypeScriptVersion(currentVersion)
|
|
const tsServerForkArgs = this.getTsServerArgs(currentVersion)
|
|
const options = { execArgv: this.getExecArgv() }
|
|
return this.startProcess(currentVersion, tsServerForkArgs, options, resendModels)
|
|
}
|
|
|
|
private getExecArgv(): string[] {
|
|
const args: string[] = []
|
|
const debugPort = getDebugPort()
|
|
if (debugPort) {
|
|
const isBreak = process.env[process.env.remoteName ? 'TSS_REMOTE_DEBUG_BRK' : 'TSS_DEBUG_BRK'] !== undefined
|
|
const inspectFlag = isBreak ? '--inspect-brk' : '--inspect'
|
|
args.push(`${inspectFlag}=${debugPort}`)
|
|
}
|
|
const maxTsServerMemory = this._configuration.maxTsServerMemory
|
|
if (maxTsServerMemory) {
|
|
args.push(`--max-old-space-size=${maxTsServerMemory}`)
|
|
}
|
|
return args
|
|
}
|
|
|
|
private startProcess(currentVersion: TypeScriptVersion, args: string[], options: IForkOptions, resendModels: boolean): ForkedTsServerProcess {
|
|
const myToken = ++this.token
|
|
this.state = ServiceStat.Starting
|
|
try {
|
|
let childProcess = fork(currentVersion.tsServerPath, args, options, this.logger)
|
|
this.state = ServiceStat.Running
|
|
this.info('Starting TSServer', JSON.stringify(currentVersion, null, 2))
|
|
const handle = new ForkedTsServerProcess(childProcess)
|
|
this.tsServerProcess = handle
|
|
this.lastStart = Date.now()
|
|
handle.onError((err: Error) => {
|
|
if (this.token != myToken) return
|
|
window.showErrorMessage(`TypeScript language server exited with error. Error message is: ${err.message}`)
|
|
this.error('TSServer errored with error.', err)
|
|
this.error(`TSServer log file: ${this.tsServerLogFile || ''}`)
|
|
window.showMessage(`TSServer errored with error. ${err.message}`, 'error')
|
|
this.serviceExited(false)
|
|
})
|
|
handle.onExit((code: any, signal: string) => {
|
|
handle.dispose()
|
|
if (this.token != myToken) return
|
|
if (code == null) {
|
|
this.info(`TSServer exited. Signal: ${signal}`)
|
|
} else {
|
|
this.error(`TSServer exited with code: ${code}. Signal: ${signal}`)
|
|
}
|
|
this.info(`TSServer log file: ${this.tsServerLogFile || ''}`)
|
|
this.serviceExited(!this.isRestarting)
|
|
this.isRestarting = false
|
|
})
|
|
handle.onData(msg => {
|
|
this.dispatchMessage(msg)
|
|
})
|
|
this.serviceStarted(resendModels)
|
|
this._onReady!.resolve()
|
|
this._onTsServerStarted.fire(currentVersion.version)
|
|
return handle
|
|
} catch (err) {
|
|
this.state = ServiceStat.StartFailed
|
|
this.error('Starting TSServer failed with error.', err.stack)
|
|
return undefined
|
|
}
|
|
}
|
|
|
|
public async openTsServerLogFile(): Promise<boolean> {
|
|
const isRoot = process.getuid && process.getuid() == 0
|
|
let echoErr = (msg: string) => {
|
|
window.showErrorMessage(msg)
|
|
}
|
|
if (isRoot) {
|
|
echoErr('Log disabled for root user.')
|
|
return false
|
|
}
|
|
if (!this.apiVersion.gte(API.v222)) {
|
|
echoErr('TS Server logging requires TS 2.2.2+')
|
|
return false
|
|
}
|
|
if (this._configuration.tsServerLogLevel === TsServerLogLevel.Off) {
|
|
echoErr(`TS Server logging is off. Change 'tsserver.log' in 'coc-settings.json' to enable`)
|
|
return false
|
|
}
|
|
if (!this.tsServerLogFile) {
|
|
echoErr('TS Server has not started logging.')
|
|
return false
|
|
}
|
|
try {
|
|
await workspace.nvim.command(`edit ${this.tsServerLogFile}`)
|
|
return true
|
|
} catch {
|
|
echoErr('Could not open TS Server log file')
|
|
return false
|
|
}
|
|
}
|
|
|
|
private serviceStarted(resendModels: boolean): void {
|
|
this.bufferSyncSupport.reset()
|
|
const watchOptions = this.apiVersion.gte(API.v380)
|
|
? this.configuration.watchOptions
|
|
: undefined
|
|
const configureOptions: Proto.ConfigureRequestArguments = {
|
|
hostInfo: 'coc-nvim',
|
|
preferences: {
|
|
providePrefixAndSuffixTextForRename: true,
|
|
allowRenameOfImportPath: true,
|
|
includePackageJsonAutoImports: this._configuration.includePackageJsonAutoImports
|
|
},
|
|
watchOptions
|
|
}
|
|
this.executeWithoutWaitingForResponse('configure', configureOptions) // tslint:disable-line
|
|
this.setCompilerOptionsForInferredProjects(this._configuration)
|
|
if (resendModels) {
|
|
this._onResendModelsRequested.fire(void 0)
|
|
this.fileConfigurationManager.reset()
|
|
this.diagnosticsManager.reInitialize()
|
|
this.bufferSyncSupport.reinitialize()
|
|
}
|
|
// Reconfigure any plugins
|
|
for (const [config, pluginName] of this.pluginManager.configurations()) {
|
|
this.configurePlugin(config, pluginName)
|
|
}
|
|
}
|
|
|
|
private setCompilerOptionsForInferredProjects(
|
|
configuration: TypeScriptServiceConfiguration
|
|
): void {
|
|
if (!this.apiVersion.gte(API.v206)) return
|
|
const args: Proto.SetCompilerOptionsForInferredProjectsArgs = {
|
|
options: this.getCompilerOptionsForInferredProjects(configuration)
|
|
}
|
|
this.executeWithoutWaitingForResponse('compilerOptionsForInferredProjects', args) // tslint:disable-line
|
|
}
|
|
|
|
private getCompilerOptionsForInferredProjects(
|
|
configuration: TypeScriptServiceConfiguration
|
|
): Proto.ExternalProjectCompilerOptions {
|
|
return {
|
|
...inferredProjectConfig(configuration),
|
|
allowJs: true,
|
|
allowSyntheticDefaultImports: true,
|
|
allowNonTsExtensions: true
|
|
}
|
|
}
|
|
|
|
private serviceExited(restart: boolean): void {
|
|
this.state = ServiceStat.Stopped
|
|
this.tsServerLogFile = null
|
|
this._callbacks.destroy('Service died.')
|
|
this._callbacks = new CallbackMap<Proto.Response>()
|
|
this._requestQueue = new RequestQueue()
|
|
this._pendingResponses = new Set<number>()
|
|
if (restart) {
|
|
const diff = Date.now() - this.lastStart
|
|
this.numberRestarts++
|
|
let startService = true
|
|
if (this.numberRestarts > 5) {
|
|
this.numberRestarts = 0
|
|
if (diff < 10 * 1000 /* 10 seconds */) {
|
|
this.lastStart = Date.now()
|
|
startService = false
|
|
window.showMessage('The TypeScript language service died 5 times right after it got started.', 'error') // tslint:disable-line
|
|
} else if (diff < 60 * 1000 /* 1 Minutes */) {
|
|
this.lastStart = Date.now()
|
|
window.showMessage('The TypeScript language service died unexpectedly 5 times in the last 5 Minutes.', 'error') // tslint:disable-line
|
|
}
|
|
}
|
|
if (startService) {
|
|
this.startService(true)
|
|
}
|
|
}
|
|
}
|
|
|
|
public toPath(uri: string): string {
|
|
return this.normalizePath(Uri.parse(uri))
|
|
}
|
|
|
|
public toOpenedFilePath(uri: string, options: { suppressAlertOnFailure?: boolean } = {}): string | undefined {
|
|
if (!this.bufferSyncSupport.ensureHasBuffer(uri)) {
|
|
if (!options.suppressAlertOnFailure) {
|
|
this.error(`Unexpected resource ${uri}`)
|
|
}
|
|
return undefined
|
|
}
|
|
return this.toPath(uri)
|
|
}
|
|
|
|
public toResource(filepath: string): string {
|
|
if (this._apiVersion.gte(API.v213)) {
|
|
if (filepath.startsWith(this.inMemoryResourcePrefix + 'untitled:')) {
|
|
let resource = Uri.parse(filepath)
|
|
if (this.inMemoryResourcePrefix) {
|
|
const dirName = path.dirname(resource.path)
|
|
const fileName = path.basename(resource.path)
|
|
if (fileName.startsWith(this.inMemoryResourcePrefix)) {
|
|
resource = resource.with({ path: path.posix.join(dirName, fileName.slice(this.inMemoryResourcePrefix.length)) })
|
|
}
|
|
}
|
|
return resource.toString()
|
|
}
|
|
}
|
|
return Uri.file(filepath).toString()
|
|
}
|
|
|
|
public normalizePath(resource: Uri): string | undefined {
|
|
if (fileSchemes.disabledSchemes.has(resource.scheme)) {
|
|
return undefined
|
|
}
|
|
switch (resource.scheme) {
|
|
case fileSchemes.file: {
|
|
let result = resource.fsPath
|
|
if (!result) return undefined
|
|
result = path.normalize(result)
|
|
// Both \ and / must be escaped in regular expressions
|
|
return result.replace(new RegExp('\\' + this.pathSeparator, 'g'), '/')
|
|
}
|
|
default: {
|
|
return this.inMemoryResourcePrefix + resource.toString(true)
|
|
}
|
|
}
|
|
}
|
|
|
|
public getDocument(resource: string): Document | undefined {
|
|
if (resource.startsWith('untitled:')) {
|
|
let bufnr = parseInt(resource.split(':', 2)[1], 10)
|
|
return workspace.getDocument(bufnr)
|
|
}
|
|
return workspace.getDocument(resource)
|
|
}
|
|
|
|
private get inMemoryResourcePrefix(): string {
|
|
return this._apiVersion.gte(API.v270) ? '^' : ''
|
|
}
|
|
|
|
public asUrl(filepath: string): Uri {
|
|
if (this._apiVersion.gte(API.v213)) {
|
|
if (filepath.startsWith(this.inMemoryResourcePrefix + 'untitled:')) {
|
|
let resource = Uri.parse(filepath.slice(this.inMemoryResourcePrefix.length))
|
|
if (this.inMemoryResourcePrefix) {
|
|
const dirName = path.dirname(resource.path)
|
|
const fileName = path.basename(resource.path)
|
|
if (fileName.startsWith(this.inMemoryResourcePrefix)) {
|
|
resource = resource.with({
|
|
path: path.posix.join(
|
|
dirName,
|
|
fileName.slice(this.inMemoryResourcePrefix.length)
|
|
)
|
|
})
|
|
}
|
|
}
|
|
return resource
|
|
}
|
|
}
|
|
return Uri.file(filepath)
|
|
}
|
|
|
|
public execute(
|
|
command: string, args: any,
|
|
token: CancellationToken,
|
|
config?: ExecConfig
|
|
): Promise<ServerResponse.Response<Proto.Response>> {
|
|
let execution: Promise<ServerResponse.Response<Proto.Response>>
|
|
|
|
if (config?.cancelOnResourceChange) {
|
|
const source = new CancellationTokenSource()
|
|
token.onCancellationRequested(() => source.cancel())
|
|
const inFlight: ToCancelOnResourceChanged = {
|
|
resource: config.cancelOnResourceChange,
|
|
cancel: () => source.cancel(),
|
|
}
|
|
this.tsServerProcess?.toCancelOnResourceChange.add(inFlight)
|
|
|
|
execution = this.executeImpl(command, args, {
|
|
isAsync: false,
|
|
token: source.token,
|
|
expectsResult: true,
|
|
...config,
|
|
}).finally(() => {
|
|
this.tsServerProcess?.toCancelOnResourceChange.delete(inFlight)
|
|
source.dispose()
|
|
})
|
|
} else {
|
|
execution = this.executeImpl(command, args, {
|
|
isAsync: false,
|
|
token,
|
|
expectsResult: true,
|
|
...config,
|
|
})
|
|
}
|
|
|
|
if (config?.nonRecoverable) {
|
|
execution.catch(err => this.fatalError(command, err))
|
|
}
|
|
return execution
|
|
}
|
|
|
|
private fatalError(command: string, error: any): void {
|
|
this.error(`A non-recoverable error occured while executing tsserver command: ${command}`)
|
|
if (this.state === ServiceStat.Running) {
|
|
this.info('Killing TS Server by fatal error:', error)
|
|
let { tsServerProcess } = this
|
|
if (tsServerProcess) {
|
|
this.tsServerProcess = undefined
|
|
tsServerProcess.kill()
|
|
}
|
|
}
|
|
}
|
|
|
|
public executeAsync(
|
|
command: string, args: Proto.GeterrRequestArgs,
|
|
token: CancellationToken): Promise<ServerResponse.Response<Proto.Response>> {
|
|
return this.executeImpl(command, args, {
|
|
isAsync: true,
|
|
token,
|
|
expectsResult: true
|
|
})
|
|
}
|
|
|
|
public executeWithoutWaitingForResponse(command: string, args: any): void {
|
|
this.executeImpl(command, args, {
|
|
isAsync: false,
|
|
token: undefined,
|
|
expectsResult: false
|
|
})
|
|
}
|
|
|
|
private executeImpl(command: string, args: any, executeInfo: { isAsync: boolean, token?: CancellationToken, expectsResult: false, lowPriority?: boolean }): undefined
|
|
private executeImpl(command: string, args: any, executeInfo: { isAsync: boolean, token?: CancellationToken, expectsResult: boolean, lowPriority?: boolean }): Promise<ServerResponse.Response<Proto.Response>>
|
|
private executeImpl(command: string, args: any, executeInfo: { isAsync: boolean, token?: CancellationToken, expectsResult: boolean, lowPriority?: boolean }): Promise<ServerResponse.Response<Proto.Response>> | undefined {
|
|
if (!this.tsServerProcess) {
|
|
return Promise.resolve(undefined)
|
|
}
|
|
this.bufferSyncSupport.beforeCommand(command)
|
|
|
|
const request = this._requestQueue.createRequest(command, args)
|
|
const requestInfo: RequestItem = {
|
|
request,
|
|
expectsResponse: executeInfo.expectsResult,
|
|
isAsync: executeInfo.isAsync,
|
|
queueingType: getQueueingType(command, executeInfo.lowPriority)
|
|
}
|
|
let result: Promise<ServerResponse.Response<Proto.Response>> | undefined
|
|
if (executeInfo.expectsResult) {
|
|
result = new Promise<ServerResponse.Response<Proto.Response>>((resolve, reject) => {
|
|
this._callbacks.add(request.seq, { onSuccess: resolve, onError: reject, startTime: Date.now(), isAsync: executeInfo.isAsync }, executeInfo.isAsync)
|
|
|
|
if (executeInfo.token) {
|
|
executeInfo.token.onCancellationRequested(() => {
|
|
this.tryCancelRequest(request.seq, command)
|
|
})
|
|
}
|
|
}).catch((err: Error) => {
|
|
throw err
|
|
})
|
|
}
|
|
|
|
this._requestQueue.enqueue(requestInfo)
|
|
this.sendNextRequests()
|
|
return result
|
|
}
|
|
|
|
private sendNextRequests(): void {
|
|
while (this._pendingResponses.size === 0 && this._requestQueue.length > 0) {
|
|
const item = this._requestQueue.dequeue()
|
|
if (item) {
|
|
this.sendRequest(item)
|
|
}
|
|
}
|
|
}
|
|
|
|
private sendRequest(requestItem: RequestItem): void {
|
|
const serverRequest = requestItem.request
|
|
this.tracer.traceRequest(serverRequest, requestItem.expectsResponse, this._requestQueue.length)
|
|
|
|
if (requestItem.expectsResponse && !requestItem.isAsync) {
|
|
this._pendingResponses.add(requestItem.request.seq)
|
|
}
|
|
if (!this.tsServerProcess) return
|
|
try {
|
|
this.tsServerProcess.write(serverRequest)
|
|
} catch (err) {
|
|
const callback = this.fetchCallback(serverRequest.seq)
|
|
if (callback) {
|
|
callback.onError(err)
|
|
}
|
|
}
|
|
}
|
|
|
|
private tryCancelRequest(seq: number, command: string): boolean {
|
|
try {
|
|
if (this._requestQueue.tryDeletePendingRequest(seq)) {
|
|
this.tracer.logTrace(`TypeScript Server: canceled request with sequence number ${seq}`)
|
|
return true
|
|
}
|
|
|
|
if (this.cancellationPipeName) {
|
|
this.tracer.logTrace(`TypeScript Server: trying to cancel ongoing request with sequence number ${seq}`)
|
|
try {
|
|
fs.writeFileSync(this.cancellationPipeName + seq, '')
|
|
} catch {
|
|
// noop
|
|
}
|
|
return true
|
|
}
|
|
|
|
this.tracer.logTrace(`TypeScript Server: tried to cancel request with sequence number ${seq}. But request got already delivered.`)
|
|
return false
|
|
} finally {
|
|
const callback = this.fetchCallback(seq)
|
|
if (callback) {
|
|
callback.onSuccess(new ServerResponse.Cancelled(`Cancelled request ${seq} - ${command}`))
|
|
}
|
|
}
|
|
}
|
|
|
|
private fetchCallback(seq: number): any {
|
|
const callback = this._callbacks.fetch(seq)
|
|
if (!callback) {
|
|
return undefined
|
|
}
|
|
this._pendingResponses.delete(seq)
|
|
return callback
|
|
}
|
|
|
|
private dispatchMessage(message: Proto.Message): void {
|
|
try {
|
|
switch (message.type) {
|
|
case 'response':
|
|
this.dispatchResponse(message as Proto.Response)
|
|
break
|
|
|
|
case 'event':
|
|
const event = message as Proto.Event
|
|
if (event.event === 'requestCompleted') {
|
|
const seq = (event as Proto.RequestCompletedEvent).body.request_seq
|
|
const p = this._callbacks.fetch(seq)
|
|
if (p) {
|
|
this.tracer.traceRequestCompleted('requestCompleted', seq, p.startTime)
|
|
p.onSuccess(undefined)
|
|
}
|
|
} else {
|
|
this.tracer.traceEvent(event)
|
|
this.dispatchEvent(event)
|
|
}
|
|
break
|
|
|
|
default:
|
|
throw new Error(`Unknown message type ${message.type} received`)
|
|
}
|
|
} finally {
|
|
this.sendNextRequests()
|
|
}
|
|
}
|
|
|
|
private dispatchResponse(response: Proto.Response): void {
|
|
const callback = this.fetchCallback(response.request_seq)
|
|
if (!callback) {
|
|
return
|
|
}
|
|
|
|
this.tracer.traceResponse(response, callback.startTime)
|
|
if (response.success) {
|
|
callback.onSuccess(response)
|
|
} else if (response.message === 'No content available.') {
|
|
// Special case where response itself is successful but there is not any data to return.
|
|
callback.onSuccess(ServerResponse.NoContent)
|
|
} else {
|
|
callback.onError(new Error(response.message))
|
|
}
|
|
}
|
|
|
|
private dispatchEvent(event: Proto.Event): void {
|
|
switch (event.event) {
|
|
case 'syntaxDiag':
|
|
case 'semanticDiag':
|
|
case 'suggestionDiag':
|
|
const diagnosticEvent = event as Proto.DiagnosticEvent
|
|
if (diagnosticEvent.body && diagnosticEvent.body.diagnostics) {
|
|
this._onDiagnosticsReceived.fire({
|
|
kind: getDiagnosticsKind(event),
|
|
resource: this.asUrl(diagnosticEvent.body.file),
|
|
diagnostics: diagnosticEvent.body.diagnostics
|
|
})
|
|
}
|
|
break
|
|
|
|
case 'configFileDiag':
|
|
this._onConfigDiagnosticsReceived.fire(
|
|
event as Proto.ConfigFileDiagnosticEvent
|
|
)
|
|
break
|
|
|
|
case 'projectLanguageServiceState':
|
|
if (event.body) {
|
|
this._onProjectLanguageServiceStateChanged.fire(
|
|
(event as Proto.ProjectLanguageServiceStateEvent).body
|
|
)
|
|
}
|
|
break
|
|
|
|
case 'beginInstallTypes':
|
|
if (event.body) {
|
|
this._onDidBeginInstallTypings.fire(
|
|
(event as Proto.BeginInstallTypesEvent).body
|
|
)
|
|
}
|
|
break
|
|
|
|
case 'endInstallTypes':
|
|
if (event.body) {
|
|
this._onDidEndInstallTypings.fire(
|
|
(event as Proto.EndInstallTypesEvent).body
|
|
)
|
|
}
|
|
break
|
|
case 'projectsUpdatedInBackground':
|
|
const body = (event as Proto.ProjectsUpdatedInBackgroundEvent).body
|
|
const resources = body.openFiles.map(Uri.file)
|
|
this.bufferSyncSupport.getErr(resources)
|
|
break
|
|
case 'typesInstallerInitializationFailed':
|
|
if (event.body) {
|
|
this._onTypesInstallerInitializationFailed.fire(
|
|
(event as Proto.TypesInstallerInitializationFailedEvent).body
|
|
)
|
|
}
|
|
break
|
|
case 'projectLoadingStart':
|
|
this.versionStatus.loading = true
|
|
break
|
|
|
|
case 'projectLoadingFinish':
|
|
this.versionStatus.loading = false
|
|
break
|
|
}
|
|
}
|
|
|
|
private getTsServerArgs(currentVersion: TypeScriptVersion): string[] {
|
|
const args: string[] = []
|
|
|
|
args.push('--allowLocalPluginLoads')
|
|
|
|
if (this.apiVersion.gte(API.v250)) {
|
|
args.push('--useInferredProjectPerProjectRoot')
|
|
} else {
|
|
args.push('--useSingleInferredProject')
|
|
}
|
|
|
|
if (this.apiVersion.gte(API.v206) && this._configuration.disableAutomaticTypeAcquisition) {
|
|
args.push('--disableAutomaticTypingAcquisition')
|
|
}
|
|
|
|
if (this.apiVersion.gte(API.v222)) {
|
|
this.cancellationPipeName = getTempFile(`tscancellation-${makeRandomHexString(20)}`)
|
|
args.push('--cancellationPipeName', this.cancellationPipeName + '*')
|
|
}
|
|
|
|
const logDir = getTempDirectory()
|
|
if (this.apiVersion.gte(API.v222)) {
|
|
const isRoot = process.getuid && process.getuid() == 0
|
|
if (this._configuration.tsServerLogLevel !== TsServerLogLevel.Off && !isRoot) {
|
|
if (logDir) {
|
|
this.tsServerLogFile = path.join(logDir, `tsserver.log`)
|
|
this.info('TSServer log file :', this.tsServerLogFile)
|
|
} else {
|
|
this.tsServerLogFile = null
|
|
this.error('Could not create TSServer log directory')
|
|
}
|
|
|
|
if (this.tsServerLogFile) {
|
|
args.push(
|
|
'--logVerbosity',
|
|
TsServerLogLevel.toString(this._configuration.tsServerLogLevel)
|
|
)
|
|
args.push('--logFile', this.tsServerLogFile)
|
|
}
|
|
}
|
|
}
|
|
|
|
if (this._configuration.enableTsServerTracing) {
|
|
let tsServerTraceDirectory = createTempDirectory(`tsserver-trace-${makeRandomHexString(5)}`)
|
|
if (tsServerTraceDirectory) {
|
|
args.push('--traceDirectory', tsServerTraceDirectory)
|
|
this.info('TSServer trace directory :', tsServerTraceDirectory)
|
|
} else {
|
|
this.error('Could not create TSServer trace directory')
|
|
}
|
|
}
|
|
|
|
if (this.apiVersion.gte(API.v230)) {
|
|
const pluginNames = this.pluginManager.plugins.map(x => x.name)
|
|
let pluginPaths = this._configuration.tsServerPluginPaths
|
|
pluginPaths = pluginPaths.reduce((p, c) => {
|
|
if (path.isAbsolute(c)) {
|
|
p.push(c)
|
|
} else {
|
|
let roots = workspace.workspaceFolders.map(o => Uri.parse(o.uri).fsPath)
|
|
p.push(...roots.map(r => path.join(r, c)))
|
|
}
|
|
return p
|
|
}, [])
|
|
|
|
if (pluginNames.length) {
|
|
const isUsingBundledTypeScriptVersion = currentVersion.path == this.versionProvider.bundledVersion.path
|
|
args.push('--globalPlugins', pluginNames.join(','))
|
|
for (const plugin of this.pluginManager.plugins) {
|
|
if (isUsingBundledTypeScriptVersion || plugin.enableForWorkspaceTypeScriptVersions) {
|
|
pluginPaths.push(plugin.path)
|
|
}
|
|
}
|
|
}
|
|
|
|
if (pluginPaths.length) {
|
|
args.push('--pluginProbeLocations', pluginPaths.join(','))
|
|
}
|
|
}
|
|
|
|
if (this._configuration.locale) {
|
|
args.push('--locale', this._configuration.locale)
|
|
}
|
|
|
|
if (this._configuration.typingsCacheLocation) {
|
|
args.push('--globalTypingsCacheLocation', `"${this._configuration.typingsCacheLocation}"`)
|
|
}
|
|
|
|
if (this.apiVersion.gte(API.v234)) {
|
|
let { npmLocation } = this._configuration
|
|
if (npmLocation) {
|
|
this.logger.info(`using npm from ${npmLocation}`)
|
|
args.push('--npmLocation', `"${npmLocation}"`)
|
|
}
|
|
}
|
|
|
|
if (this.apiVersion.gte(API.v291)) {
|
|
args.push('--noGetErrOnBackgroundUpdate')
|
|
}
|
|
|
|
if (this.apiVersion.gte(API.v345)) {
|
|
args.push('--validateDefaultNpmLocation')
|
|
}
|
|
return args
|
|
}
|
|
|
|
public getProjectRootPath(uri: string): string | undefined {
|
|
let root = workspace.cwd
|
|
let u = Uri.parse(uri)
|
|
if (u.scheme !== 'file') return undefined
|
|
let folder = workspace.getWorkspaceFolder(uri)
|
|
if (folder) {
|
|
root = Uri.parse(folder.uri).fsPath
|
|
} else {
|
|
let filepath = Uri.parse(uri).fsPath
|
|
if (!filepath.startsWith(root)) {
|
|
root = path.dirname(filepath)
|
|
}
|
|
}
|
|
if (root == os.homedir()) return undefined
|
|
return root
|
|
}
|
|
|
|
public configurePlugin(pluginName: string, configuration: {}): any {
|
|
if (this.apiVersion.gte(API.v314)) {
|
|
if (!this.tsServerProcess) return
|
|
this.executeWithoutWaitingForResponse('configurePlugin', { pluginName, configuration })
|
|
}
|
|
}
|
|
|
|
public interruptGetErr<R>(f: () => R): R {
|
|
return this.bufferSyncSupport.interuptGetErr(f)
|
|
}
|
|
|
|
private cancelInflightRequestsForResource(resource: string): void {
|
|
if (this.state !== ServiceStat.Running || !this.tsServerProcess) {
|
|
return
|
|
}
|
|
for (const request of this.tsServerProcess.toCancelOnResourceChange) {
|
|
if (request.resource.toString() === resource.toString()) {
|
|
request.cancel()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function getDiagnosticsKind(event: Proto.Event): DiagnosticKind {
|
|
switch (event.event) {
|
|
case 'syntaxDiag':
|
|
return DiagnosticKind.Syntax
|
|
case 'semanticDiag':
|
|
return DiagnosticKind.Semantic
|
|
case 'suggestionDiag':
|
|
return DiagnosticKind.Suggestion
|
|
}
|
|
throw new Error('Unknown dignostics kind')
|
|
}
|
|
|
|
const fenceCommands = new Set(['change', 'close', 'open'])
|
|
|
|
function getQueueingType(
|
|
command: string,
|
|
lowPriority?: boolean
|
|
): RequestQueueingType {
|
|
if (fenceCommands.has(command)) {
|
|
return RequestQueueingType.Fence
|
|
}
|
|
return lowPriority ? RequestQueueingType.LowPriority : RequestQueueingType.Normal
|
|
}
|
|
|
|
function getDebugPort(): number | undefined {
|
|
let debugBrk = process.env[process.env.remoteName ? 'TSS_REMOTE_DEBUG_BRK' : 'TSS_DEBUG_BRK']
|
|
let value = debugBrk || process.env[process.env.remoteName ? 'TSS_REMOTE_DEBUG_BRK' : 'TSS_DEBUG_BRK']
|
|
if (value) {
|
|
const port = parseInt(value)
|
|
if (!isNaN(port)) {
|
|
return port
|
|
}
|
|
}
|
|
return undefined
|
|
}
|