rework bufferSyncSupport
This commit is contained in:
parent
6fc53cd80e
commit
c4ae2c2fbf
23 changed files with 735 additions and 460 deletions
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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 => {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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 => {
|
||||
|
|
|
@ -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: '',
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
]
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -12,6 +12,7 @@ export interface TypeScriptServerPlugin {
|
|||
readonly name: string
|
||||
readonly enableForWorkspaceTypeScriptVersions: boolean
|
||||
readonly languages: ReadonlyArray<string>
|
||||
readonly configNamespace?: string
|
||||
}
|
||||
|
||||
namespace TypeScriptServerPlugin {
|
||||
|
|
Loading…
Reference in a new issue