/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ 'use strict'; import { workspace, TextDocument, TextDocumentChangeEvent, TextDocumentContentChangeEvent, Disposable } from 'vscode'; import * as Proto from '../protocol'; import { ITypescriptServiceClient } from '../typescriptService'; import { Delayer } from '../utils/async'; interface IDiagnosticRequestor { requestDiagnostic(filepath: string): void; } class SyncedBuffer { private document: TextDocument; private filepath: string; private diagnosticRequestor: IDiagnosticRequestor; private client: ITypescriptServiceClient; constructor(model: TextDocument, filepath: string, diagnosticRequestor: IDiagnosticRequestor, client: ITypescriptServiceClient) { this.document = model; this.filepath = filepath; this.diagnosticRequestor = diagnosticRequestor; this.client = client; } public open(): void { let args: Proto.OpenRequestArgs = { file: this.filepath }; this.client.execute('open', args, false); // The last line never has a new line character at the end. So we use range. // Sending a replace doesn't work if the buffer is newer then on disk and // if changes are on the last line. In this case the tsserver has less characters // which makes the tsserver bail since the range is invalid /* let lastLineRange = this.document.lineAt(this.document.lineCount - 1).range; let text = this.document.getText(); let changeArgs: Proto.ChangeRequestArgs = { file: this.filepath, line: 1, offset: 1, endLine: lastLineRange.end.line + 1, endOffset: lastLineRange.end.character + 1, insertString: text } this.client.execute('change', changeArgs, false); */ } public close(): void { let args: Proto.FileRequestArgs = { file: this.filepath }; this.client.execute('close', args, false); } onContentChanged(events: TextDocumentContentChangeEvent[]): void { let filePath = this.client.asAbsolutePath(this.document.uri); if (!filePath) { return; } for (let i = 0; i < events.length; i++) { let event = events[i]; let range = event.range; let text = event.text; let args: Proto.ChangeRequestArgs = { file: filePath, line: range.start.line + 1, offset: range.start.character + 1, endLine: range.end.line + 1, endOffset: range.end.character + 1, insertString: text }; this.client.execute('change', args, false); } this.diagnosticRequestor.requestDiagnostic(filePath); } } export default class BufferSyncSupport { private client: ITypescriptServiceClient; private modeId: string; private disposables: Disposable[] = []; private syncedBuffers: { [key: string]: SyncedBuffer }; private pendingDiagnostics: { [key: string]: number; }; private diagnosticDelayer: Delayer; constructor(client: ITypescriptServiceClient, modeId: string) { this.client = client; this.modeId = modeId; this.pendingDiagnostics = Object.create(null); this.diagnosticDelayer = new Delayer(100); this.syncedBuffers = Object.create(null); workspace.onDidOpenTextDocument(this.onDidAddDocument, this, this.disposables); workspace.onDidCloseTextDocument(this.onDidRemoveDocument, this, this.disposables); workspace.onDidChangeTextDocument(this.onDidChangeDocument, this, this.disposables); workspace.textDocuments.forEach(this.onDidAddDocument, this); } public reOpenDocuments(): void { Object.keys(this.syncedBuffers).forEach(key => { this.syncedBuffers[key].open(); }) } public dispose(): void { while (this.disposables.length) { this.disposables.pop().dispose(); } } private onDidAddDocument(document: TextDocument): void { if (document.languageId !== this.modeId) { return; } if (document.isUntitled) { return; } let resource = document.uri; let filepath = this.client.asAbsolutePath(resource); if (!filepath) { return; } let syncedBuffer = new SyncedBuffer(document, filepath, this, this.client); this.syncedBuffers[filepath] = syncedBuffer; syncedBuffer.open(); this.requestDiagnostic(filepath); } private onDidRemoveDocument(document: TextDocument): void { let filepath: string = this.client.asAbsolutePath(document.uri); if (!filepath) { return; } let syncedBuffer = this.syncedBuffers[filepath]; if (!syncedBuffer) { return; } delete this.syncedBuffers[filepath]; syncedBuffer.close(); } private onDidChangeDocument(e: TextDocumentChangeEvent): void { let filepath: string = this.client.asAbsolutePath(e.document.uri); if (!filepath) { return; } let syncedBuffer = this.syncedBuffers[filepath]; if (!syncedBuffer) { return; } syncedBuffer.onContentChanged(e.contentChanges); } public requestAllDiagnostics() { Object.keys(this.syncedBuffers).forEach(filePath => this.pendingDiagnostics[filePath] = Date.now()); this.diagnosticDelayer.trigger(() => { this.sendPendingDiagnostics(); }); } public requestDiagnostic(file: string): void { this.pendingDiagnostics[file] = Date.now(); this.diagnosticDelayer.trigger(() => { this.sendPendingDiagnostics(); }); } private sendPendingDiagnostics(): void { let files = Object.keys(this.pendingDiagnostics).map((key) => { return { file: key, time: this.pendingDiagnostics[key] }; }).sort((a, b) => { return a.time - b.time; }).map((value) => { return value.file; }); // Add all open TS buffers to the geterr request. They might be visible Object.keys(this.syncedBuffers).forEach((file) => { if (!this.pendingDiagnostics[file]) { files.push(file); } }); let args: Proto.GeterrRequestArgs = { delay: 0, files: files }; this.client.execute('geterr', args, false); this.pendingDiagnostics = Object.create(null); } }