rework bufferSyncSupport

This commit is contained in:
Qiming Zhao 2020-12-07 18:57:53 +08:00
parent 6fc53cd80e
commit c4ae2c2fbf
23 changed files with 735 additions and 460 deletions

View file

@ -7,7 +7,6 @@ import { TextEdit, Range } from 'vscode-languageserver-types'
import { installModules } from './utils/modules'
import { nodeModules } from './utils/helper'
import { PluginManager } from '../utils/plugins'
import { languageIds } from './utils/languageModeIds'
export interface Command {
readonly id: string | string[]
@ -48,12 +47,11 @@ export class TypeScriptGoToProjectConfigCommand implements Command {
public async execute(): Promise<void> {
let doc = await workspace.document
let { filetype } = doc
if (languageIds.indexOf(filetype) == -1) {
workspace.showMessage(`Could not determine TypeScript or JavaScript project. Unsupported file type: ${filetype}`, 'warning')
let { languageId } = doc.textDocument
if (this.client.serviceClient.modeIds.indexOf(languageId) == -1) {
workspace.showMessage(`Could not determine TypeScript or JavaScript project. Unsupported file type: ${languageId}`, 'warning')
return
}
// doc.filetype
await goToProjectConfig(this.client, doc.uri)
}
}
@ -100,7 +98,8 @@ export class AutoFixCommand implements Command {
public async execute(): Promise<void> {
let document = await workspace.document
let { uri } = document
if (!this.client.handles(uri)) {
let handles = await this.client.handles(uri)
if (!handles) {
workspace.showMessage(`Document ${uri} is not handled by tsserver.`, 'warning')
return
}

View file

@ -3,33 +3,146 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Uri, disposeAll, workspace } from 'coc.nvim'
import { CancellationTokenSource, Emitter, Event, DidChangeTextDocumentParams, Disposable, TextDocumentContentChangeEvent } from 'vscode-languageserver-protocol'
import { CancellationTokenSource, CancellationToken, Emitter, Event, DidChangeTextDocumentParams, Disposable, TextDocumentContentChangeEvent } from 'vscode-languageserver-protocol'
import { TextDocument } from 'vscode-languageserver-textdocument'
import Proto from '../protocol'
import { ITypeScriptServiceClient } from '../typescriptService'
import API from '../utils/api'
import { Delayer } from '../utils/async'
import * as typeConverters from '../utils/typeConverters'
import * as languageModeIds from '../utils/languageModeIds'
import { mode2ScriptKind } from '../utils/languageModeIds'
import { ResourceMap } from './resourceMap'
function mode2ScriptKind(
mode: string
): 'TS' | 'TSX' | 'JS' | 'JSX' | undefined {
switch (mode) {
case languageModeIds.typescript:
return 'TS'
case languageModeIds.typescripttsx:
return 'TSX'
case languageModeIds.typescriptjsx:
return 'TSX'
case languageModeIds.typescriptreact:
return 'TSX'
case languageModeIds.javascript:
return 'JS'
case languageModeIds.javascriptreact:
return 'JSX'
const enum BufferKind {
TypeScript = 1,
JavaScript = 2,
}
const enum BufferState {
Initial = 1,
Open = 2,
Closed = 2,
}
const enum BufferOperationType { Close, Open, Change }
class CloseOperation {
readonly type = BufferOperationType.Close;
constructor(
public readonly args: string
) { }
}
class OpenOperation {
readonly type = BufferOperationType.Open;
constructor(
public readonly args: Proto.OpenRequestArgs
) { }
}
class ChangeOperation {
readonly type = BufferOperationType.Change;
constructor(
public readonly args: Proto.FileCodeEdits
) { }
}
type BufferOperation = CloseOperation | OpenOperation | ChangeOperation
class SyncedBuffer {
private state = BufferState.Initial;
constructor(
private readonly document: TextDocument,
public readonly filepath: string,
private readonly client: ITypeScriptServiceClient,
private readonly synchronizer: BufferSynchronizer,
) { }
public open(): void {
const args: Proto.OpenRequestArgs = {
file: this.filepath,
fileContent: this.document.getText(),
projectRootPath: this.client.getProjectRootPath(this.document.uri),
}
const scriptKind = mode2ScriptKind(this.document.languageId)
if (scriptKind) {
args.scriptKindName = scriptKind
}
if (this.client.apiVersion.gte(API.v240)) {
// plugin managed.
const tsPluginsForDocument = this.client.pluginManager.plugins
.filter(x => x.languages.indexOf(this.document.languageId) >= 0)
if (tsPluginsForDocument.length) {
(args as any).plugins = tsPluginsForDocument.map(plugin => plugin.name)
}
}
this.synchronizer.open(this.resource, args)
this.state = BufferState.Open
}
public get resource(): string {
return this.document.uri
}
public get lineCount(): number {
return this.document.lineCount
}
public get kind(): BufferKind {
if (this.document.languageId.startsWith('javascript')) {
return BufferKind.JavaScript
}
return BufferKind.TypeScript
}
/**
* @return Was the buffer open?
*/
public close(): boolean {
if (this.state !== BufferState.Open) {
this.state = BufferState.Closed
return false
}
this.state = BufferState.Closed
return this.synchronizer.close(this.resource, this.filepath)
}
public onContentChanged(events: readonly TextDocumentContentChangeEvent[]): void {
if (this.state !== BufferState.Open) {
console.error(`Unexpected buffer state: ${this.state}`)
}
this.synchronizer.change(this.resource, this.filepath, events)
}
}
class SyncedBufferMap extends ResourceMap<SyncedBuffer> {
public getForPath(filePath: string): SyncedBuffer | undefined {
return this.get(Uri.file(filePath).toString())
}
public get allBuffers(): Iterable<SyncedBuffer> {
return this.values
}
}
class PendingDiagnostics extends ResourceMap<number> {
public getOrderedFileSet(): ResourceMap<void> {
const orderedResources = Array.from(this.entries)
.sort((a, b) => a.value - b.value)
.map(entry => entry.resource)
const map = new ResourceMap<void>(this._normalizePath)
for (const resource of orderedResources) {
map.set(resource, undefined)
}
return map
}
return undefined
}
/**
@ -39,52 +152,64 @@ function mode2ScriptKind(
*/
class BufferSynchronizer {
private _pending: Proto.UpdateOpenRequestArgs = {}
private _pendingFiles = new Set<string>()
private readonly _pending: ResourceMap<BufferOperation>
constructor(
private readonly client: ITypeScriptServiceClient
) { }
public open(args: Proto.OpenRequestArgs): void {
this.client.executeWithoutWaitingForResponse('open', args)
private readonly client: ITypeScriptServiceClient,
pathNormalizer: (path: string) => string | undefined
) {
this._pending = new ResourceMap<BufferOperation>(pathNormalizer)
}
public close(filepath: string): void {
const args: Proto.FileRequestArgs = { file: filepath }
this.client.executeWithoutWaitingForResponse('close', args)
public open(resource: string, args: Proto.OpenRequestArgs) {
if (this.supportsBatching) {
this.updatePending(resource, new OpenOperation(args))
} else {
this.client.executeWithoutWaitingForResponse('open', args)
}
}
public change(filepath: string, events: TextDocumentContentChangeEvent[]): void {
/**
* @return Was the buffer open?
*/
public close(resource: string, filepath: string): boolean {
if (this.supportsBatching) {
return this.updatePending(resource, new CloseOperation(filepath))
} else {
const args: Proto.FileRequestArgs = { file: filepath }
this.client.executeWithoutWaitingForResponse('close', args)
return true
}
}
public change(resource: string, filepath: string, events: readonly TextDocumentContentChangeEvent[]) {
if (!events.length) {
return
}
if (this.supportsBatching) {
this.updatePending(filepath, pending => {
if (!pending.changedFiles) {
pending.changedFiles = []
}
pending.changedFiles.push({
fileName: filepath,
textChanges: events.map((change): Proto.CodeEdit => ({
newText: change.text,
start: typeConverters.Position.toLocation((change as any).range.start),
end: typeConverters.Position.toLocation((change as any).range.end),
})).reverse(), // Send the edits end-of-document to start-of-document order
})
})
this.updatePending(resource, new ChangeOperation({
fileName: filepath,
textChanges: events.map((change): Proto.CodeEdit => ({
newText: change.text,
start: typeConverters.Position.toLocation((change as any).range.start),
end: typeConverters.Position.toLocation((change as any).range.end),
})).reverse(), // Send the edits end-of-document to start-of-document order
}))
} else {
for (const event of events) {
for (const { range, text } of events as any) {
const args: Proto.ChangeRequestArgs = {
insertString: event.text,
...typeConverters.Range.toFormattingRequestArgs(filepath, (event as any).range)
insertString: text,
...typeConverters.Range.toFormattingRequestArgs(filepath, range)
}
this.client.executeWithoutWaitingForResponse('change', args)
}
}
}
public reset(): void {
this._pending.clear()
}
public beforeCommand(command: string): void {
if (command === 'updateOpen') {
return
@ -93,265 +218,51 @@ class BufferSynchronizer {
this.flush()
}
private flush(): void {
private flush() {
if (!this.supportsBatching) {
// We've already eagerly synchronized
this._pending.clear()
return
}
if (this._pending.changedFiles) {
this.client.executeWithoutWaitingForResponse('updateOpen', this._pending)
this._pending = {}
this._pendingFiles.clear()
if (this._pending.size > 0) {
const closedFiles: string[] = []
const openFiles: Proto.OpenRequestArgs[] = []
const changedFiles: Proto.FileCodeEdits[] = []
for (const change of this._pending.values) {
switch (change.type) {
case BufferOperationType.Change: changedFiles.push(change.args); break
case BufferOperationType.Open: openFiles.push(change.args); break
case BufferOperationType.Close: closedFiles.push(change.args); break
}
}
this.client.execute('updateOpen', { changedFiles, closedFiles, openFiles }, CancellationToken.None, { nonRecoverable: true })
this._pending.clear()
}
}
private get supportsBatching(): boolean {
return this.client.apiVersion.gte(API.v340) && workspace.getConfiguration('tsserver').get<boolean>('useBatchedBufferSync', true)
return this.client.apiVersion.gte(API.v340)
}
private updatePending(filepath: string, f: (pending: Proto.UpdateOpenRequestArgs) => void): void {
if (this.supportsBatching && this._pendingFiles.has(filepath)) {
this.flush()
this._pendingFiles.clear()
f(this._pending)
this._pendingFiles.add(filepath)
} else {
f(this._pending)
}
}
public reset(): void {
this._pending = {}
this._pendingFiles.clear()
}
}
export default class BufferSyncSupport {
private readonly client: ITypeScriptServiceClient
private readonly modeIds: Set<string>
private readonly uris: Set<string> = new Set()
private readonly disposables: Disposable[] = []
private readonly pendingDiagnostics = new Map<string, number>()
private readonly diagnosticDelayer: Delayer<any>
private pendingGetErr: GetErrRequest | undefined
private readonly synchronizer: BufferSynchronizer
private _validateJavaScript = true
private _validateTypeScript = true
private listening = false
private readonly _onDelete = new Emitter<string>()
public readonly onDelete: Event<string> = this._onDelete.event
constructor(
client: ITypeScriptServiceClient,
) {
this.client = client
this.synchronizer = new BufferSynchronizer(client)
this.modeIds = new Set<string>(languageModeIds.languageIds)
this.diagnosticDelayer = new Delayer<any>(300)
}
public listen(): void {
if (this.listening) {
return
}
this.listening = true
workspace.onDidOpenTextDocument(
this.onDidOpenTextDocument,
this,
this.disposables
)
workspace.onDidCloseTextDocument(
this.onDidCloseTextDocument,
this,
this.disposables
)
workspace.onDidChangeTextDocument(
this.onDidChangeTextDocument,
this,
this.disposables
)
workspace.textDocuments.forEach(this.onDidOpenTextDocument, this)
this.updateConfiguration()
workspace.onDidChangeConfiguration(this.updateConfiguration, this, this.disposables)
}
public dispose(): void {
this.pendingDiagnostics.clear()
disposeAll(this.disposables)
}
public onDidOpenTextDocument(document: TextDocument): void {
if (!this.modeIds.has(document.languageId)) return
let { uri } = document
let filepath = this.client.toPath(uri)
this.uris.add(uri)
const args: Proto.OpenRequestArgs = {
file: filepath,
fileContent: document.getText()
}
if (this.client.apiVersion.gte(API.v203)) {
const scriptKind = mode2ScriptKind(document.languageId)
if (scriptKind) {
args.scriptKindName = scriptKind
}
}
if (this.client.apiVersion.gte(API.v230)) {
let root = this.client.getProjectRootPath(document.uri)
if (root) args.projectRootPath = root
}
this.synchronizer.open(args)
// this.client.executeWithoutWaitingForResponse('open', args)
this.requestDiagnostic(uri)
}
private onDidCloseTextDocument(document: TextDocument): void {
let { uri } = document
if (!this.uris.has(uri)) return
let filepath = this.client.toPath(uri)
this.uris.delete(uri)
this.pendingDiagnostics.delete(uri)
this.synchronizer.close(filepath)
this._onDelete.fire(uri)
this.requestAllDiagnostics()
// this.client.executeWithoutWaitingForResponse('close', args)
}
private onDidChangeTextDocument(e: DidChangeTextDocumentParams): void {
let { textDocument, contentChanges } = e
let { uri } = textDocument
if (!this.uris.has(uri)) return
let filepath = this.client.toPath(uri)
this.synchronizer.change(filepath, contentChanges)
const didTrigger = this.requestDiagnostic(uri)
if (!didTrigger && this.pendingGetErr) {
// In this case we always want to re-trigger all diagnostics
this.pendingGetErr.cancel()
this.pendingGetErr = undefined
this.triggerDiagnostics()
}
}
public beforeCommand(command: string): void {
this.synchronizer.beforeCommand(command)
}
public interuptGetErr<R>(f: () => R): R {
if (!this.pendingGetErr) {
return f()
}
this.pendingGetErr.cancel()
this.pendingGetErr = undefined
const result = f()
this.triggerDiagnostics()
return result
}
public getErr(resources: Uri[]): any {
const handledResources = resources.filter(resource => this.uris.has(resource.toString()))
if (!handledResources.length) {
return
}
for (const resource of handledResources) {
let uri = resource.toString()
if (this.shouldValidate(uri)) {
this.pendingDiagnostics.set(uri, Date.now())
}
}
this.triggerDiagnostics()
}
public has(uri: string): boolean {
return this.uris.has(uri)
}
private triggerDiagnostics(delay = 200): void {
this.diagnosticDelayer.trigger(() => {
this.sendPendingDiagnostics()
}, delay)
}
public requestAllDiagnostics(): void {
for (const uri of this.uris) {
if (this.shouldValidate(uri)) {
this.pendingDiagnostics.set(uri, Date.now())
}
}
this.diagnosticDelayer.trigger(() => { // tslint:disable-line
this.sendPendingDiagnostics()
}, 200)
}
public requestDiagnostic(uri: string): boolean {
let document = workspace.getDocument(uri)
if (!document || !this.shouldValidate(uri)) return false
this.pendingDiagnostics.set(uri, Date.now())
const lineCount = document.lineCount
const delay = Math.min(Math.max(Math.ceil(lineCount / 20), 300), 800)
this.triggerDiagnostics(delay)
return true
}
public hasPendingDiagnostics(uri: string): boolean {
return this.pendingDiagnostics.has(uri)
}
private sendPendingDiagnostics(): void {
const uris = Array.from(this.pendingDiagnostics.entries())
.sort((a, b) => a[1] - b[1])
.map(entry => entry[0])
// Add all open TS buffers to the geterr request. They might be visible
for (const uri of this.uris) {
if (uris.indexOf(uri) == -1) {
uris.push(uri)
}
}
let files = uris.map(uri => this.client.toPath(uri))
if (files.length) {
if (this.pendingGetErr) this.pendingGetErr.cancel()
const getErr = this.pendingGetErr = GetErrRequest.executeGetErrRequest(this.client, files, () => {
if (this.pendingGetErr === getErr) {
this.pendingGetErr = undefined
private updatePending(resource: string, op: BufferOperation): boolean {
switch (op.type) {
case BufferOperationType.Close:
const existing = this._pending.get(resource)
switch (existing?.type) {
case BufferOperationType.Open:
this._pending.delete(resource)
return false // Open then close. No need to do anything
}
})
break
}
this.pendingDiagnostics.clear()
}
private updateConfiguration(): void {
const jsConfig = workspace.getConfiguration('javascript', null)
const tsConfig = workspace.getConfiguration('typescript', null)
this._validateJavaScript = jsConfig.get<boolean>('validate.enable', true)
this._validateTypeScript = tsConfig.get<boolean>('validate.enable', true)
}
public shouldValidate(uri: string): boolean {
let doc = workspace.getDocument(uri)
if (!doc) return false
if (languageModeIds.languageIds.indexOf(doc.filetype) == -1) {
return false
}
if (doc.filetype.startsWith('javascript')) {
return this._validateJavaScript
}
return this._validateTypeScript
}
public reinitialize(): void {
this.pendingDiagnostics.clear()
this.pendingGetErr?.cancel()
this.synchronizer.reset()
for (let doc of workspace.documents) {
this.onDidOpenTextDocument(doc.textDocument)
if (this._pending.has(resource)) {
// we saw this file before, make sure we flush before working with it again
this.flush()
}
this._pending.set(resource, op)
return true
}
}
@ -397,3 +308,259 @@ class GetErrRequest {
this._token.dispose()
}
}
export default class BufferSyncSupport {
private disposables: Disposable[] = []
private readonly client: ITypeScriptServiceClient
private _validateJavaScript: boolean = true;
private _validateTypeScript: boolean = true;
private readonly modeIds: Set<string>
private readonly syncedBuffers: SyncedBufferMap
private readonly pendingDiagnostics: PendingDiagnostics
private readonly diagnosticDelayer: Delayer<any>
private pendingGetErr: GetErrRequest | undefined
private listening: boolean = false;
private readonly synchronizer: BufferSynchronizer
private readonly _onDelete = new Emitter<string>()
public readonly onDelete: Event<string> = this._onDelete.event
readonly _onWillChange = new Emitter<string>()
public readonly onWillChange: Event<string> = this._onWillChange.event
constructor(
client: ITypeScriptServiceClient,
modeIds: readonly string[]
) {
this.client = client
this.modeIds = new Set<string>(modeIds)
this.diagnosticDelayer = new Delayer<any>(300)
const pathNormalizer = (path: string) => this.client.toPath(path)
this.syncedBuffers = new SyncedBufferMap(pathNormalizer)
this.pendingDiagnostics = new PendingDiagnostics(pathNormalizer)
this.synchronizer = new BufferSynchronizer(client, pathNormalizer)
this.updateConfiguration()
workspace.onDidChangeConfiguration(this.updateConfiguration, this, this.disposables)
}
public listen(): void {
if (this.listening) {
return
}
this.listening = true
workspace.onDidOpenTextDocument(
this.openTextDocument,
this,
this.disposables
)
workspace.onDidCloseTextDocument(
this.onDidCloseTextDocument,
this,
this.disposables
)
workspace.onDidChangeTextDocument(
this.onDidChangeTextDocument,
this,
this.disposables
)
workspace.textDocuments.forEach(this.openTextDocument, this)
}
public handles(resource: string): boolean {
return this.syncedBuffers.has(resource)
}
public dispose(): void {
this.pendingDiagnostics.clear()
disposeAll(this.disposables)
this._onWillChange.dispose()
this._onDelete.dispose()
}
public ensureHasBuffer(resource: string): boolean {
if (this.syncedBuffers.has(resource)) {
return true
}
const existingDocument = workspace.textDocuments.find(doc => doc.uri.toString() === resource)
if (existingDocument) {
return this.openTextDocument(existingDocument)
}
return false
}
public toResource(filePath: string): string {
const buffer = this.syncedBuffers.getForPath(filePath)
if (buffer) return buffer.resource
return Uri.file(filePath).toString()
}
public reset(): void {
this.pendingGetErr?.cancel()
this.pendingDiagnostics.clear()
this.synchronizer.reset()
}
public reinitialize(): void {
this.reset()
for (const buffer of this.syncedBuffers.allBuffers) {
buffer.open()
}
}
public openTextDocument(document: TextDocument): boolean {
if (!this.modeIds.has(document.languageId)) {
// can't handle
return false
}
const resource = document.uri
const filepath = this.client.normalizePath(Uri.parse(resource))
if (!filepath) {
return false
}
if (this.syncedBuffers.has(resource)) {
return true
}
const syncedBuffer = new SyncedBuffer(document, filepath, this.client, this.synchronizer)
this.syncedBuffers.set(resource, syncedBuffer)
syncedBuffer.open()
this.requestDiagnostic(syncedBuffer)
return true
}
public closeResource(resource: string): void {
const syncedBuffer = this.syncedBuffers.get(resource)
if (!syncedBuffer) {
return
}
this.pendingDiagnostics.delete(resource)
this.syncedBuffers.delete(resource)
const wasBufferOpen = syncedBuffer.close()
this._onDelete.fire(resource)
if (wasBufferOpen) {
this.requestAllDiagnostics()
}
}
private onDidCloseTextDocument(document: TextDocument): void {
this.closeResource(document.uri)
}
private onDidChangeTextDocument(e: DidChangeTextDocumentParams): void {
const syncedBuffer = this.syncedBuffers.get(e.textDocument.uri)
if (!syncedBuffer) {
return
}
this._onWillChange.fire(syncedBuffer.resource)
syncedBuffer.onContentChanged(e.contentChanges)
const didTrigger = this.requestDiagnostic(syncedBuffer)
if (!didTrigger && this.pendingGetErr) {
// In this case we always want to re-trigger all diagnostics
this.pendingGetErr.cancel()
this.pendingGetErr = undefined
this.triggerDiagnostics()
}
}
public beforeCommand(command: string): void {
this.synchronizer.beforeCommand(command)
}
public interuptGetErr<R>(f: () => R): R {
if (!this.pendingGetErr) {
return f()
}
this.pendingGetErr.cancel()
this.pendingGetErr = undefined
const result = f()
this.triggerDiagnostics()
return result
}
public getErr(resources: Uri[]): any {
const handledResources = resources.filter(resource => this.handles(resource.toString()))
if (!handledResources.length) {
return
}
for (const resource of handledResources) {
this.pendingDiagnostics.set(resource.toString(), Date.now())
}
this.triggerDiagnostics()
}
private triggerDiagnostics(delay: number = 200) {
this.diagnosticDelayer.trigger(() => {
this.sendPendingDiagnostics()
}, delay)
}
public requestAllDiagnostics(): void {
for (const buffer of this.syncedBuffers.allBuffers) {
if (this.shouldValidate(buffer)) {
this.pendingDiagnostics.set(buffer.resource, Date.now())
}
}
this.triggerDiagnostics()
}
private requestDiagnostic(buffer: SyncedBuffer): boolean {
if (!this.shouldValidate(buffer)) {
return false
}
this.pendingDiagnostics.set(buffer.resource, Date.now())
const delay = Math.min(Math.max(Math.ceil(buffer.lineCount / 20), 300), 800)
this.triggerDiagnostics(delay)
return true
}
public hasPendingDiagnostics(uri: string): boolean {
return this.pendingDiagnostics.has(uri)
}
private sendPendingDiagnostics(): void {
const orderedFileSet = this.pendingDiagnostics.getOrderedFileSet()
if (this.pendingGetErr) {
this.pendingGetErr.cancel()
for (const file of this.pendingGetErr.files) {
let resource = Uri.file(file).toString()
if (this.syncedBuffers.get(resource)) {
orderedFileSet.set(resource, undefined)
}
}
this.pendingGetErr = undefined
}
// Add all open TS buffers to the geterr request. They might be visible
for (const buffer of this.syncedBuffers.values) {
orderedFileSet.set(buffer.resource, undefined)
}
if (orderedFileSet.size) {
let files = Array.from(orderedFileSet.keys).map(uri => this.client.normalizePath(Uri.parse(uri)))
const getErr = this.pendingGetErr = GetErrRequest.executeGetErrRequest(this.client, files, () => {
if (this.pendingGetErr === getErr) {
this.pendingGetErr = undefined
}
})
}
this.pendingDiagnostics.clear()
}
private updateConfiguration(): void {
const jsConfig = workspace.getConfiguration('javascript', null)
const tsConfig = workspace.getConfiguration('typescript', null)
this._validateJavaScript = jsConfig.get<boolean>('validate.enable', true)
this._validateTypeScript = tsConfig.get<boolean>('validate.enable', true)
}
private shouldValidate(buffer: SyncedBuffer) {
switch (buffer.kind) {
case BufferKind.JavaScript:
return this._validateJavaScript
case BufferKind.TypeScript:
default:
return this._validateTypeScript
}
}
}

View file

@ -110,6 +110,7 @@ export default class TypeScriptCompletionItemProvider implements CompletionItemP
return null
}
await this.client.interruptGetErr(() => this.fileConfigurationManager.ensureConfigurationForDocument(document, token))
const { completeOption } = this
const args: Proto.CompletionsRequestArgs & { includeAutomaticOptionalChainCompletions?: boolean } = {
...typeConverters.Position.toFileLocationRequestArgs(file, position),

View file

@ -84,6 +84,8 @@ export class DiagnosticsManager {
): void {
const collection = this._diagnostics.get(kind)
if (!collection) return
let doc = workspace.getDocument(uri)
if (doc) uri = doc.uri
if (diagnostics.length === 0) {
const existing = collection.get(uri)

View file

@ -2,8 +2,8 @@
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { workspace, WorkspaceConfiguration } from 'coc.nvim'
import { CancellationToken } from 'vscode-languageserver-protocol'
import { workspace, WorkspaceConfiguration, disposeAll } from 'coc.nvim'
import { CancellationToken, Disposable } from 'vscode-languageserver-protocol'
import { TextDocument } from 'vscode-languageserver-textdocument'
import Proto from '../protocol'
import { ITypeScriptServiceClient } from '../typescriptService'
@ -41,11 +41,20 @@ export interface SuggestOptions {
export default class FileConfigurationManager {
private cachedMap: Map<string, FileConfiguration> = new Map()
private disposables: Disposable[] = []
public constructor(private readonly client: ITypeScriptServiceClient) {
workspace.onDidCloseTextDocument(textDocument => {
// When a document gets closed delete the cached formatting options.
// This is necessary since the tsserver now closed a project when its
// last file in it closes which drops the stored formatting options
// as well.
this.cachedMap.delete(textDocument.uri)
}, undefined, this.disposables)
}
public async ensureConfigurationOptions(document: TextDocument, insertSpaces: boolean, tabSize: number): Promise<void> {
public async ensureConfigurationOptions(document: TextDocument, insertSpaces: boolean, tabSize: number, token: CancellationToken): Promise<void> {
const file = this.client.toPath(document.uri)
let options: FormatOptions = {
tabSize,
@ -62,11 +71,19 @@ export default class FileConfigurationManager {
...currentOptions
}
await this.client.execute('configure', args, CancellationToken.None)
try {
const response = await this.client.execute('configure', args, token)
if (response.type !== 'response') {
this.cachedMap.delete(document.uri)
}
} catch (_e) {
this.cachedMap.delete(document.uri)
}
}
public async ensureConfigurationForDocument(document: TextDocument): Promise<void> {
public async ensureConfigurationForDocument(document: TextDocument, token: CancellationToken): Promise<void> {
let opts = await workspace.getFormatOptions(document.uri)
return this.ensureConfigurationOptions(document, opts.insertSpaces, opts.tabSize)
return this.ensureConfigurationOptions(document, opts.insertSpaces, opts.tabSize, token)
}
public reset(): void {
@ -164,6 +181,10 @@ export default class FileConfigurationManager {
if (this.client.apiVersion.gte(API.v333) || quoteStyle != 'auto') return quoteStyle
return 'single'
}
public dispose(): void {
disposeAll(this.disposables)
}
}
type ModuleImportType = 'relative' | 'non-relative' | 'auto'

View file

@ -35,7 +35,8 @@ export default class TypeScriptFormattingProvider
await this.formattingOptionsManager.ensureConfigurationOptions(
document,
options.insertSpaces,
options.tabSize
options.tabSize,
token
)
try {
const response = await this.client.execute('format', args, token)
@ -101,7 +102,8 @@ export default class TypeScriptFormattingProvider
await this.formattingOptionsManager.ensureConfigurationOptions(
document,
options.insertSpaces,
options.tabSize
options.tabSize,
token
)
const doc = workspace.getDocument(document.uri)

View file

@ -22,7 +22,7 @@ export default class TypeScriptImplementationsCodeLensProvider extends TypeScrip
codeLens.range.start
)
try {
const response = await this.client.execute('implementation', args, token, true)
const response = await this.client.execute('implementation', args, token, { lowPriority: true })
if (response && response.type == 'response' && response.body) {
const locations = response.body
.map(reference => {

View file

@ -1,50 +0,0 @@
import { disposeAll, workspace } from 'coc.nvim'
import { Command, CommandManager } from 'coc.nvim/lib/commands'
import { Disposable, CancellationToken } from 'vscode-languageserver-protocol'
import * as Proto from '../protocol'
import { ITypeScriptServiceClient } from '../typescriptService'
import * as languageIds from '../utils/languageModeIds'
class ProjectErrorCommand implements Command {
public readonly id: string = 'tsserver.project_error'
constructor(
private readonly client: ITypeScriptServiceClient
) {
}
public async execute(): Promise<void> {
let document = await workspace.document
if (languageIds[document.filetype] == null) return
let file = this.client.toPath(document.uri)
const args: Proto.GeterrForProjectRequestArgs = {
file,
delay: 20
}
const response = null
// await this.client.execute('geterrForProject', args, CancellationToken.None)
if (!response || !response.success) {
return
}
return
}
}
export default class ProjectErrors {
private disposables: Disposable[] = []
public constructor(
client: ITypeScriptServiceClient,
commandManager: CommandManager
) {
let cmd = new ProjectErrorCommand(client)
commandManager.register(cmd)
this.disposables.push(Disposable.create(() => {
commandManager.unregister(cmd.id)
}))
}
public dispose(): void {
disposeAll(this.disposables)
}
}

View file

@ -12,6 +12,7 @@ import { ITypeScriptServiceClient } from '../typescriptService'
import API from '../utils/api'
import { applyCodeActionCommands, getEditForCodeAction } from '../utils/codeAction'
import * as typeConverters from '../utils/typeConverters'
import FileConfigurationManager from './fileConfigurationManager'
class ApplyCodeActionCommand implements Command {
public static readonly ID = '_typescript.applyCodeActionCommand'
@ -19,6 +20,7 @@ class ApplyCodeActionCommand implements Command {
constructor(
private readonly client: ITypeScriptServiceClient,
private readonly formattingConfigurationManager: FileConfigurationManager
) { }
public async execute(action: Proto.CodeFixAction): Promise<boolean> {
@ -32,15 +34,18 @@ class ApplyFixAllCodeAction implements Command {
constructor(
private readonly client: ITypeScriptServiceClient,
private readonly formattingConfigurationManager: FileConfigurationManager
) { }
public async execute(
document: TextDocument,
file: string,
tsAction: Proto.CodeFixAction
): Promise<void> {
if (!tsAction.fixId) {
return
}
await this.formattingConfigurationManager.ensureConfigurationForDocument(document, CancellationToken.None)
const args: Proto.GetCombinedCodeFixRequestArgs = {
scope: {
@ -141,12 +146,13 @@ export default class TypeScriptQuickFixProvider implements CodeActionProvider {
constructor(
private readonly client: ITypeScriptServiceClient,
private readonly formattingConfigurationManager: FileConfigurationManager
) {
commands.register(
new ApplyCodeActionCommand(client)
new ApplyCodeActionCommand(client, formattingConfigurationManager)
)
commands.register(
new ApplyFixAllCodeAction(client)
new ApplyFixAllCodeAction(client, formattingConfigurationManager)
)
this.supportedCodeActionProvider = new SupportedCodeActionProvider(client)
@ -158,14 +164,11 @@ export default class TypeScriptQuickFixProvider implements CodeActionProvider {
context: CodeActionContext,
token: CancellationToken
): Promise<CodeAction[]> {
if (!this.client.apiVersion.gte(API.v213)) {
return []
}
const file = this.client.toPath(document.uri)
if (!file) {
return []
}
await this.formattingConfigurationManager.ensureConfigurationForDocument(document, token)
const fixableDiagnostics = await this.supportedCodeActionProvider.getFixableDiagnosticsForContext(
context
@ -283,7 +286,7 @@ export default class TypeScriptQuickFixProvider implements CodeActionProvider {
action.diagnostics = [diagnostic]
action.command = {
command: ApplyFixAllCodeAction.ID,
arguments: [file, tsAction],
arguments: [document, file, tsAction],
title: ''
}
return action

View file

@ -127,7 +127,7 @@ export default class TypeScriptRefactorProvider implements CodeActionProvider {
}
const file = this.client.toPath(document.uri)
if (!file) return undefined
await this.formattingOptionsManager.ensureConfigurationForDocument(document)
await this.formattingOptionsManager.ensureConfigurationForDocument(document, token)
const args: Proto.GetApplicableRefactorsRequestArgs = typeConverters.Range.toFileRangeRequestArgs(
file,
range

View file

@ -21,7 +21,9 @@ export default class TypeScriptReferencesCodeLensProvider extends TypeScriptBase
codeLens.range.start
)
return this.client
.execute('references', args, token, true)
.execute('references', args, token, {
lowPriority: true
})
.then(response => {
if (!response || response.type != 'response' || !response.body) {
throw codeLens

View file

@ -10,9 +10,13 @@ import * as Proto from '../protocol'
import { ITypeScriptServiceClient, ServerResponse } from '../typescriptService'
import API from '../utils/api'
import * as typeConverters from '../utils/typeConverters'
import FileConfigurationManager from './fileConfigurationManager'
export default class TypeScriptRenameProvider implements RenameProvider {
public constructor(private readonly client: ITypeScriptServiceClient) { }
public constructor(
private readonly client: ITypeScriptServiceClient,
private readonly fileConfigurationManager: FileConfigurationManager
) { }
public async prepareRename(
document: TextDocument,
@ -81,6 +85,7 @@ export default class TypeScriptRenameProvider implements RenameProvider {
findInStrings: false,
findInComments: false
}
await this.fileConfigurationManager.ensureConfigurationForDocument(document, token)
return this.client.interruptGetErr(() => {
return this.client.execute('rename', args, token)

View file

@ -13,9 +13,19 @@ export class ResourceMap<T> {
private readonly _map = new Map<string, T>()
constructor(
private readonly _normalizePath?: (resource: string) => string | null
protected readonly _normalizePath?: (resource: string) => string | null
) { }
public get size() {
return this._map.size
}
public get entries(): { resource: string, value: T }[] {
return Array.from(this._map.keys()).map(key => {
return { resource: key, value: this._map[key] }
})
}
public has(resource: string): boolean {
const file = this.toKey(resource)
return !!file && this._map.has(file)
@ -48,6 +58,10 @@ export class ResourceMap<T> {
return this._map.keys()
}
public clear(): void {
this._map.clear()
}
private toKey(resource: string): string | null {
const key = this._normalizePath
? this._normalizePath(resource)

View file

@ -85,7 +85,7 @@ export default class UpdateImportsOnFileRenameHandler {
}
private async getEditsForFileRename(document: TextDocument, oldFile: string, newFile: string): Promise<WorkspaceEdit> {
await this.fileConfigurationManager.ensureConfigurationForDocument(document)
await this.fileConfigurationManager.ensureConfigurationForDocument(document, CancellationToken.None)
const response = await this.client.interruptGetErr(() => {
const args: Proto.GetEditsForFileRenameRequestArgs = {
oldFilePath: oldFile,

View file

@ -1,9 +1,8 @@
import { Uri, disposeAll, IServiceProvider, ServiceStat, workspace, WorkspaceConfiguration } from 'coc.nvim'
import { disposeAll, IServiceProvider, ServiceStat, workspace, WorkspaceConfiguration } from 'coc.nvim'
import { Disposable, DocumentSelector, Emitter, Event } from 'vscode-languageserver-protocol'
import { PluginManager } from '../utils/plugins'
import TypeScriptServiceClientHost from './typescriptServiceClientHost'
import { LanguageDescription, standardLanguageDescriptions } from './utils/languageDescription'
import { PluginManager } from '../utils/plugins'
import { TextDocument } from 'vscode-languageserver-textdocument'
export default class TsserverService implements IServiceProvider {
public id = 'tsserver'
@ -28,24 +27,12 @@ export default class TsserverService implements IServiceProvider {
this.selector = this.descriptions.reduce((arr, c) => {
return arr.concat(c.modeIds)
}, [])
workspace.onDidOpenTextDocument(doc => {
this.ensureConfigurationForDocument(doc)
}, null, this.disposables)
}
public get config(): WorkspaceConfiguration {
return workspace.getConfiguration('tsserver')
}
public ensureConfigurationForDocument(document: TextDocument): void {
let uri = Uri.parse(document.uri)
let language = this.clientHost.findLanguage(uri)
if (!language) return
language.fileConfigurationManager.ensureConfigurationForDocument(document).catch(_e => {
// noop
})
}
public start(): Promise<void> {
if (this.clientHost) return
this.state = ServiceStat.Starting
@ -61,7 +48,6 @@ export default class TsserverService implements IServiceProvider {
}
})
this._onDidServiceReady.fire(void 0)
this.ensureConfiguration()
if (!started) {
started = true
resolve()
@ -70,13 +56,6 @@ export default class TsserverService implements IServiceProvider {
})
}
private ensureConfiguration(): void {
if (!this.clientHost) return
for (let doc of workspace.documents) {
this.ensureConfigurationForDocument(doc.textDocument)
}
}
public dispose(): void {
disposeAll(this.disposables)
}

View file

@ -3,7 +3,8 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { commands, DiagnosticKind, disposeAll, languages, Uri, workspace } from 'coc.nvim'
import { CodeActionKind, Diagnostic, DiagnosticSeverity, Disposable } from 'vscode-languageserver-protocol'
import path from 'path'
import { CodeActionKind, Diagnostic, DiagnosticSeverity, Disposable, TextDocument } from 'vscode-languageserver-protocol'
import { CachedNavTreeResponse } from './features/baseCodeLensProvider'
import CompletionItemProvider from './features/completionItemProvider'
import DefinitionProvider from './features/definitionProvider'
@ -37,16 +38,14 @@ import TypingsStatus from './utils/typingsStatus'
const suggestionSetting = 'suggestionActions.enabled'
export default class LanguageProvider {
public readonly fileConfigurationManager: FileConfigurationManager // tslint:disable-line
private readonly disposables: Disposable[] = []
constructor(
public client: TypeScriptServiceClient,
private readonly fileConfigurationManager: FileConfigurationManager,
private description: LanguageDescription,
typingsStatus: TypingsStatus
private typingsStatus: TypingsStatus
) {
this.fileConfigurationManager = new FileConfigurationManager(client)
workspace.onDidChangeConfiguration(this.configurationChanged, this, this.disposables)
this.configurationChanged()
let initialized = false
@ -167,7 +166,7 @@ export default class LanguageProvider {
this.disposables.push(
languages.registerRenameProvider(
languageIds,
new RenameProvider(client))
new RenameProvider(client, this.fileConfigurationManager))
)
let formatProvider = new FormattingProvider(client, this.fileConfigurationManager)
this.disposables.push(
@ -224,7 +223,7 @@ export default class LanguageProvider {
this.disposables.push(
languages.registerCodeActionProvider(
languageIds,
new QuickfixProvider(client),
new QuickfixProvider(client, this.fileConfigurationManager),
'tsserver',
[CodeActionKind.QuickFix]))
@ -276,23 +275,12 @@ export default class LanguageProvider {
// }
}
public handles(resource: Uri): boolean {
let { modeIds, configFile } = this.description
if (resource.toString().endsWith(configFile)) {
public handles(resource: string, doc: TextDocument): boolean {
if (doc && this.description.modeIds.indexOf(doc.languageId) >= 0) {
return true
}
let doc = workspace.getDocument(resource.toString())
if (doc && modeIds.indexOf(doc.filetype) !== -1) {
return true
}
let str = resource.toString()
if (this.id === 'typescript' && /\.ts(x)?$/.test(str)) {
return true
}
if (this.id === 'javascript' && /\.js(x)?$/.test(str)) {
return true
}
return false
const base = path.basename(Uri.parse(resource).fsPath)
return !!base && (!!this.description.configFilePattern && this.description.configFilePattern.test(base))
}
private get id(): string { // tslint:disable-line
@ -312,9 +300,6 @@ export default class LanguageProvider {
file: Uri,
diagnostics: (Diagnostic & { reportUnnecessary: any })[]
): void {
if (!this.client.bufferSyncSupport.shouldValidate(file.toString())) {
return
}
const config = workspace.getConfiguration(this.id, file.toString())
const reportUnnecessary = config.get<boolean>('showUnused', true)
this.client.diagnosticsManager.diagnosticsReceived(diagnosticsKind, file.toString(), diagnostics.filter(diag => {

View file

@ -8,7 +8,6 @@ import { TextDocument } from 'vscode-languageserver-textdocument'
import { Command } from './commands'
import Proto from './protocol'
import { standardLanguageDescriptions } from './utils/languageDescription'
import { languageIds } from './utils/languageModeIds'
import * as typeconverts from './utils/typeConverters'
import FileConfigurationManager from './features/fileConfigurationManager'
import TypeScriptServiceClient from './typescriptServiceClient'
@ -49,7 +48,8 @@ export class OrganizeImportsCommand implements Command {
public async execute(document?: TextDocument): Promise<void> {
if (!document) {
let doc = await workspace.document
if (languageIds.indexOf(doc.filetype) == -1) return
if (!doc.attached) return
if (this.client.modeIds.indexOf(doc.textDocument.languageId) == -1) return
document = doc.textDocument
}
let edit = await this.getTextEdits(document)
@ -71,17 +71,18 @@ export class OrganizeImportsCodeActionProvider implements CodeActionProvider {
providedCodeActionKinds: [CodeActionKind.SourceOrganizeImports]
}
public provideCodeActions(
public async provideCodeActions(
document: TextDocument,
_range: Range,
context: CodeActionContext,
_token: CancellationToken
): CodeAction[] {
if (languageIds.indexOf(document.languageId) == -1) return
token: CancellationToken
): Promise<CodeAction[]> {
if (this.client.modeIds.indexOf(document.languageId) == -1) return
if (!context.only || !context.only.includes(CodeActionKind.SourceOrganizeImports)) {
return []
}
await this.fileConfigManager.ensureConfigurationForDocument(document, token)
const action = CodeAction.create('Organize Imports', {
title: '',

View file

@ -10,6 +10,7 @@ import { TypeScriptServiceConfiguration } from './utils/configuration'
import Logger from './utils/logger'
import BufferSyncSupport from './features/bufferSyncSupport'
import { DiagnosticsManager } from './features/diagnostics'
import { PluginManager } from '../utils/plugins'
export namespace ServerResponse {
@ -33,10 +34,23 @@ export interface TypeScriptServerPlugin {
readonly languages: string[]
}
export enum ExectuionTarget {
Semantic,
Syntax
}
export type ExecConfig = {
readonly lowPriority?: boolean
readonly nonRecoverable?: boolean
readonly cancelOnResourceChange?: string
readonly executionTarget?: ExectuionTarget
}
export interface TypeScriptRequestTypes {
'applyCodeActionCommand': [Proto.ApplyCodeActionCommandRequestArgs, Proto.ApplyCodeActionCommandResponse]
'completionEntryDetails': [Proto.CompletionDetailsRequestArgs, Proto.CompletionDetailsResponse]
'completionInfo': [Proto.CompletionsRequestArgs, Proto.CompletionInfoResponse]
'updateOpen': [Proto.UpdateOpenRequestArgs, Proto.Response]
// tslint:disable-next-line: deprecation
'completions': [Proto.CompletionsRequestArgs, Proto.CompletionsResponse]
'configure': [Proto.ConfigureRequestArguments, Proto.ConfigureResponse]
@ -79,6 +93,7 @@ export interface ITypeScriptServiceClient {
readonly logger: Logger
readonly bufferSyncSupport: BufferSyncSupport
readonly diagnosticsManager: DiagnosticsManager
readonly pluginManager: PluginManager
getProjectRootPath(uri: string): string | null
normalizePath(resource: Uri): string | null
@ -90,7 +105,7 @@ export interface ITypeScriptServiceClient {
command: K,
args: TypeScriptRequestTypes[K][0],
token: CancellationToken,
lowPriority?: boolean
config?: ExecConfig
): Promise<ServerResponse.Response<TypeScriptRequestTypes[K][1]>>
executeWithoutWaitingForResponse(command: 'open', args: Proto.OpenRequestArgs): void
@ -107,4 +122,12 @@ export interface ITypeScriptServiceClient {
* Cancel on going geterr requests and re-queue them after `f` has been evaluated.
*/
interruptGetErr<R>(f: () => R): R
/**
* Tries to ensure that a vscode document is open on the TS server.
*
* @return The normalized path or `undefined` if the document is not open on the server.
*/
toOpenedFilePath(uri: string, options?: {
suppressAlertOnFailure?: boolean
}): string | undefined
}

View file

@ -7,7 +7,7 @@ import { disposeAll, ServiceStat, Uri, workspace } from 'coc.nvim'
import fs from 'fs'
import os from 'os'
import path from 'path'
import { CancellationToken, Disposable, Emitter, Event } from 'vscode-languageserver-protocol'
import { CancellationToken, CancellationTokenSource, Disposable, Emitter, Event } from 'vscode-languageserver-protocol'
import { PluginManager } from '../utils/plugins'
import { CallbackMap } from './callbackMap'
import BufferSyncSupport from './features/bufferSyncSupport'
@ -15,7 +15,7 @@ import { DiagnosticKind, DiagnosticsManager } from './features/diagnostics'
import FileConfigurationManager from './features/fileConfigurationManager'
import * as Proto from './protocol'
import { RequestItem, RequestQueue, RequestQueueingType } from './requestQueue'
import { ITypeScriptServiceClient, ServerResponse } from './typescriptService'
import { ExecConfig, ITypeScriptServiceClient, ServerResponse } from './typescriptService'
import API from './utils/api'
import { TsServerLogLevel, TypeScriptServiceConfiguration } from './utils/configuration'
import Logger from './utils/logger'
@ -26,9 +26,16 @@ import { TypeScriptVersion, TypeScriptVersionProvider } from './utils/versionPro
import VersionStatus from './utils/versionStatus'
import { ICallback, Reader } from './utils/wireProtocol'
interface ToCancelOnResourceChanged {
readonly resource: string
cancel(): void
}
class ForkedTsServerProcess {
constructor(private childProcess: cp.ChildProcess) { }
public readonly toCancelOnResourceChange = new Set<ToCancelOnResourceChanged>()
public onError(cb: (err: Error) => void): void {
this.childProcess.on('error', cb)
}
@ -75,6 +82,7 @@ export default class TypeScriptServiceClient implements ITypeScriptServiceClient
private _configuration: TypeScriptServiceConfiguration
private versionProvider: TypeScriptVersionProvider
private tsServerLogFile: string | null = null
private tsServerProcess: ForkedTsServerProcess | undefined
private servicePromise: Thenable<ForkedTsServerProcess> | null
private lastError: Error | null
private lastStart: number
@ -97,7 +105,10 @@ export default class TypeScriptServiceClient implements ITypeScriptServiceClient
private readonly disposables: Disposable[] = []
private isRestarting = false
constructor(private pluginManager: PluginManager) {
constructor(
public readonly pluginManager: PluginManager,
public readonly modeIds: string[]
) {
this.pathSeparator = path.sep
this.lastStart = Date.now()
this.servicePromise = null
@ -117,15 +128,19 @@ export default class TypeScriptServiceClient implements ITypeScriptServiceClient
this.restartTsServer()
}, null, this.disposables)
this.bufferSyncSupport = new BufferSyncSupport(this)
this.bufferSyncSupport = new BufferSyncSupport(this, modeIds)
this.onTsServerStarted(() => {
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>()
@ -313,6 +328,7 @@ export default class TypeScriptServiceClient implements ITypeScriptServiceClient
this.state = ServiceStat.Running
this.info('Started TSServer', JSON.stringify(currentVersion, null, 2))
const handle = new ForkedTsServerProcess(childProcess)
this.tsServerProcess = handle
this.lastStart = Date.now()
handle.onError((err: Error) => {
@ -383,6 +399,7 @@ export default class TypeScriptServiceClient implements ITypeScriptServiceClient
}
private serviceStarted(resendModels: boolean): void {
this.bufferSyncSupport.reset()
const watchOptions = this.apiVersion.gte(API.v380)
? this.configuration.watchOptions
: undefined
@ -398,6 +415,7 @@ export default class TypeScriptServiceClient implements ITypeScriptServiceClient
this.setCompilerOptionsForInferredProjects(this._configuration)
if (resendModels) {
this._onResendModelsRequested.fire(void 0)
this.fileConfigurationManager.reset()
this.diagnosticsManager.reInitialize()
this.bufferSyncSupport.reinitialize()
}
@ -461,6 +479,16 @@ export default class TypeScriptServiceClient implements ITypeScriptServiceClient
return this.normalizePath(Uri.parse(uri))
}
public toOpenedFilePath(uri: string, options: { suppressAlertOnFailure?: boolean } = {}): string | undefined {
if (!this.bufferSyncSupport.ensureHasBuffer(uri)) {
if (!options.suppressAlertOnFailure) {
console.error(`Unexpected resource ${uri}`)
}
return undefined
}
return this.toPath(uri)
}
public toResource(filepath: string): string {
if (this._apiVersion.gte(API.v213)) {
if (filepath.startsWith('untitled:')) {
@ -525,13 +553,52 @@ export default class TypeScriptServiceClient implements ITypeScriptServiceClient
public execute(
command: string, args: any,
token: CancellationToken,
lowPriority?: boolean): Promise<ServerResponse.Response<Proto.Response>> {
return this.executeImpl(command, args, {
isAsync: false,
token,
expectsResult: true,
lowPriority
})
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 {
console.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)
this.service().then(service => {
service.kill()
})
}
}
public executeAsync(
@ -860,21 +927,20 @@ export default class TypeScriptServiceClient implements ITypeScriptServiceClient
return args
}
public getProjectRootPath(uri: string): string | null {
public getProjectRootPath(uri: string): string | undefined {
let root = workspace.cwd
let u = Uri.parse(uri)
if (u.scheme == 'file') {
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 (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 null
if (root == os.homedir()) return undefined
return root
}
@ -891,6 +957,17 @@ export default class TypeScriptServiceClient implements ITypeScriptServiceClient
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 {

View file

@ -7,11 +7,13 @@ import { Range, Diagnostic, DiagnosticSeverity, Disposable, Position, Cancellati
import LanguageProvider from './languageProvider'
import * as Proto from './protocol'
import * as PConst from './protocol.const'
import FileConfigurationManager from './features/fileConfigurationManager'
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'
import { flatten } from '../utils/arrays'
// Style check diagnostics that can be reported as warnings
const styleCheckDiagnostics = [
@ -30,6 +32,7 @@ export default class TypeScriptServiceClientHost implements Disposable {
private readonly languages: LanguageProvider[] = []
private readonly languagePerId = new Map<string, LanguageProvider>()
private readonly disposables: Disposable[] = []
private readonly fileConfigurationManager: FileConfigurationManager
private reportStyleCheckAsWarnings = true
constructor(descriptions: LanguageDescription[], pluginManager: PluginManager) {
@ -50,10 +53,13 @@ export default class TypeScriptServiceClientHost implements Disposable {
packageFileWatcher.onDidCreate(this.reloadProjects, this, this.disposables)
packageFileWatcher.onDidChange(handleProjectChange, this, this.disposables)
this.client = new TypeScriptServiceClient(pluginManager)
const allModeIds = this.getAllModeIds(descriptions, pluginManager)
this.client = new TypeScriptServiceClient(pluginManager, allModeIds)
this.disposables.push(this.client)
this.client.onDiagnosticsReceived(({ kind, resource, diagnostics }) => {
this.diagnosticsReceived(kind, resource, diagnostics)
this.diagnosticsReceived(kind, resource, diagnostics).catch(e => {
console.error(e)
})
}, null, this.disposables)
this.client.onConfigDiagnosticsReceived(diag => {
@ -79,12 +85,13 @@ export default class TypeScriptServiceClientHost implements Disposable {
}
}
}, null, this.disposables)
this.typingsStatus = new TypingsStatus(this.client)
this.ataProgressReporter = new AtaProgressReporter(this.client)
this.fileConfigurationManager = new FileConfigurationManager(this.client)
for (const description of descriptions) { // tslint:disable-line
const manager = new LanguageProvider(
this.client,
this.fileConfigurationManager,
description,
this.typingsStatus
)
@ -103,14 +110,13 @@ export default class TypeScriptServiceClientHost implements Disposable {
public dispose(): void {
disposeAll(this.disposables)
this.fileConfigurationManager.dispose()
this.typingsStatus.dispose()
this.ataProgressReporter.dispose()
}
public reset(): void {
for (let lang of this.languages) {
lang.fileConfigurationManager.reset()
}
this.fileConfigurationManager.reset()
}
public get serviceClient(): TypeScriptServiceClient {
@ -132,16 +138,22 @@ export default class TypeScriptServiceClientHost implements Disposable {
this.reportStyleCheckAsWarnings = config.get('reportStyleChecksAsWarnings', true)
}
public findLanguage(resource: Uri): LanguageProvider | null {
public async findLanguage(uri: string): Promise<LanguageProvider> {
try {
return this.languages.find(language => language.handles(resource))
let doc = await workspace.loadFile(uri)
if (!doc) return undefined
return this.languages.find(language => language.handles(uri, doc.textDocument))
} catch {
return null
return undefined
}
}
public handles(uri: string): boolean {
return this.findLanguage(Uri.parse(uri)) != null
public async handles(uri: string): Promise<boolean> {
const provider = await this.findLanguage(uri)
if (provider) {
return true
}
return this.client.bufferSyncSupport.handles(uri)
}
private triggerAllDiagnostics(): void {
@ -150,12 +162,12 @@ export default class TypeScriptServiceClientHost implements Disposable {
}
}
private diagnosticsReceived(
private async diagnosticsReceived(
kind: DiagnosticKind,
resource: Uri,
diagnostics: Proto.Diagnostic[]
): void {
const language = this.findLanguage(resource)
): Promise<void> {
const language = await this.findLanguage(resource.toString())
if (language) {
language.diagnosticsReceived(
kind,
@ -222,4 +234,12 @@ export default class TypeScriptServiceClientHost implements Disposable {
private isStyleCheckDiagnostic(code: number | undefined): boolean {
return code ? styleCheckDiagnostics.indexOf(code) !== -1 : false
}
private getAllModeIds(descriptions: LanguageDescription[], pluginManager: PluginManager) {
const allModeIds = flatten([
...descriptions.map(x => x.modeIds),
...pluginManager.plugins.map(x => x.languages)
])
return allModeIds
}
}

View file

@ -12,6 +12,7 @@ export interface LanguageDescription {
readonly configFile?: string
readonly isExternal?: boolean
readonly diagnosticOwner: string
readonly configFilePattern?: RegExp
}
export const enum DiagnosticLanguage {
@ -27,7 +28,8 @@ export const standardLanguageDescriptions: LanguageDescription[] = [
modeIds: [languageModeIds.typescript, languageModeIds.typescriptreact,
languageModeIds.typescripttsx, languageModeIds.typescriptjsx],
diagnosticLanguage: DiagnosticLanguage.TypeScript,
configFile: 'tsconfig.json'
configFile: 'tsconfig.json',
configFilePattern: /^tsconfig(\..*)?\.json$/gi
},
{
id: 'javascript',
@ -35,6 +37,7 @@ export const standardLanguageDescriptions: LanguageDescription[] = [
diagnosticOwner: 'typescript',
modeIds: [languageModeIds.javascript, languageModeIds.javascriptreact, languageModeIds.javascriptjsx],
diagnosticLanguage: DiagnosticLanguage.JavaScript,
configFile: 'jsconfig.json'
configFile: 'jsconfig.json',
configFilePattern: /^jsconfig(\..*)?\.json$/gi
}
]

View file

@ -13,3 +13,23 @@ export const javascriptjsx = 'javascript.jsx'
export const jsxTags = 'jsx-tags'
export const languageIds = [typescript, typescriptreact, javascript, javascriptreact, javascriptjsx, typescripttsx, jsxTags]
export function mode2ScriptKind(
mode: string
): 'TS' | 'TSX' | 'JS' | 'JSX' | undefined {
switch (mode) {
case typescript:
return 'TS'
case typescripttsx:
return 'TSX'
case typescriptjsx:
return 'TSX'
case typescriptreact:
return 'TSX'
case javascript:
return 'JS'
case javascriptreact:
return 'JSX'
}
return undefined
}

View file

@ -12,6 +12,7 @@ export interface TypeScriptServerPlugin {
readonly name: string
readonly enableForWorkspaceTypeScriptVersions: boolean
readonly languages: ReadonlyArray<string>
readonly configNamespace?: string
}
namespace TypeScriptServerPlugin {