bufferSyncSupport.ts 10.5 KB
Newer Older
I
isidor 已提交
1 2 3 4
/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/
E
Erich Gamma 已提交
5

D
Dirk Baeumer 已提交
6
import * as fs from 'fs';
7
import * as vscode from 'vscode';
E
Erich Gamma 已提交
8
import * as Proto from '../protocol';
9
import { ITypeScriptServiceClient } from '../typescriptService';
M
Matt Bierner 已提交
10
import API from '../utils/api';
E
Erich Gamma 已提交
11
import { Delayer } from '../utils/async';
12
import { Disposable } from '../utils/dispose';
M
Matt Bierner 已提交
13
import * as languageModeIds from '../utils/languageModeIds';
M
Matt Bierner 已提交
14
import { ResourceMap } from '../utils/resourceMap';
15
import * as typeConverters from '../utils/typeConverters';
M
Matt Bierner 已提交
16

17 18 19 20
enum BufferKind {
	TypeScript = 1,
	JavaScript = 2,
}
E
Erich Gamma 已提交
21

22 23
function mode2ScriptKind(mode: string): 'TS' | 'TSX' | 'JS' | 'JSX' | undefined {
	switch (mode) {
24 25 26 27
		case languageModeIds.typescript: return 'TS';
		case languageModeIds.typescriptreact: return 'TSX';
		case languageModeIds.javascript: return 'JS';
		case languageModeIds.javascriptreact: return 'JSX';
28 29 30
	}
	return undefined;
}
31

E
Erich Gamma 已提交
32 33
class SyncedBuffer {

M
Matt Bierner 已提交
34
	constructor(
35
		private readonly document: vscode.TextDocument,
36
		public readonly filepath: string,
37
		private readonly client: ITypeScriptServiceClient
M
Matt Bierner 已提交
38
	) { }
E
Erich Gamma 已提交
39 40

	public open(): void {
41
		const args: Proto.OpenRequestArgs = {
D
Dirk Baeumer 已提交
42
			file: this.filepath,
43
			fileContent: this.document.getText(),
E
Erich Gamma 已提交
44
		};
45

M
Matt Bierner 已提交
46
		if (this.client.apiVersion.gte(API.v203)) {
47
			const scriptKind = mode2ScriptKind(this.document.languageId);
48 49
			if (scriptKind) {
				args.scriptKindName = scriptKind;
50
			}
51
		}
52

M
Matt Bierner 已提交
53
		if (this.client.apiVersion.gte(API.v230)) {
54
			args.projectRootPath = this.client.getWorkspaceRootForResource(this.document.uri);
55
		}
56

M
Matt Bierner 已提交
57
		if (this.client.apiVersion.gte(API.v240)) {
58 59 60 61 62
			const tsPluginsForDocument = this.client.plugins
				.filter(x => x.languages.indexOf(this.document.languageId) >= 0);

			if (tsPluginsForDocument.length) {
				(args as any).plugins = tsPluginsForDocument.map(plugin => plugin.name);
63 64 65
			}
		}

66
		this.client.executeWithoutWaitingForResponse('open', args);
E
Erich Gamma 已提交
67 68
	}

69
	public get resource(): vscode.Uri {
70 71 72
		return this.document.uri;
	}

73 74 75 76
	public get lineCount(): number {
		return this.document.lineCount;
	}

77 78 79 80 81 82 83 84 85 86 87 88 89
	public get kind(): BufferKind {
		switch (this.document.languageId) {
			case languageModeIds.javascript:
			case languageModeIds.javascriptreact:
				return BufferKind.JavaScript;

			case languageModeIds.typescript:
			case languageModeIds.typescriptreact:
			default:
				return BufferKind.TypeScript;
		}
	}

E
Erich Gamma 已提交
90
	public close(): void {
M
Matt Bierner 已提交
91
		const args: Proto.FileRequestArgs = {
E
Erich Gamma 已提交
92 93
			file: this.filepath
		};
94
		this.client.executeWithoutWaitingForResponse('close', args);
E
Erich Gamma 已提交
95 96
	}

97
	public onContentChanged(events: vscode.TextDocumentContentChangeEvent[]): void {
M
Matt Bierner 已提交
98
		for (const { range, text } of events) {
M
Matt Bierner 已提交
99
			const args: Proto.ChangeRequestArgs = {
100 101
				insertString: text,
				...typeConverters.Range.toFormattingRequestArgs(this.filepath, range)
E
Erich Gamma 已提交
102
			};
103
			this.client.executeWithoutWaitingForResponse('change', args);
E
Erich Gamma 已提交
104 105 106 107
		}
	}
}

M
Matt Bierner 已提交
108 109 110
class SyncedBufferMap extends ResourceMap<SyncedBuffer> {

	public getForPath(filePath: string): SyncedBuffer | undefined {
111
		return this.get(vscode.Uri.file(filePath));
M
Matt Bierner 已提交
112 113 114 115 116 117 118
	}

	public get allBuffers(): Iterable<SyncedBuffer> {
		return this.values;
	}
}

119
class PendingDiagnostics extends ResourceMap<number> {
120
	public getFileList(): Set<string> {
121
		return new Set(Array.from(this.entries)
122 123 124 125 126
			.sort((a, b) => a[1] - b[1])
			.map(entry => entry[0]));
	}
}

M
Matt Bierner 已提交
127
class GetErrRequest {
M
Matt Bierner 已提交
128 129 130 131 132 133

	public static executeGetErrRequest(
		client: ITypeScriptServiceClient,
		files: string[],
		onDone: () => void
	) {
134
		const token = new vscode.CancellationTokenSource();
M
Matt Bierner 已提交
135 136 137 138 139 140 141
		return new GetErrRequest(client, files, token, onDone);
	}

	private _done: boolean = false;

	private constructor(
		client: ITypeScriptServiceClient,
M
Matt Bierner 已提交
142
		public readonly files: string[],
143
		private readonly _token: vscode.CancellationTokenSource,
M
Matt Bierner 已提交
144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168
		onDone: () => void
	) {
		const args: Proto.GeterrRequestArgs = {
			delay: 0,
			files
		};

		client.executeAsync('geterr', args, _token.token)
			.then(undefined, () => { })
			.then(() => {
				if (this._done) {
					return;
				}
				this._done = true;
				onDone();
			});
	}

	public cancel(): any {
		if (!this._done) {
			this._token.cancel();
		}

		this._token.dispose();
	}
M
Matt Bierner 已提交
169 170
}

171
export default class BufferSyncSupport extends Disposable {
E
Erich Gamma 已提交
172

173
	private readonly client: ITypeScriptServiceClient;
E
Erich Gamma 已提交
174

175 176
	private _validateJavaScript: boolean = true;
	private _validateTypeScript: boolean = true;
177
	private readonly modeIds: Set<string>;
178
	private readonly syncedBuffers: SyncedBufferMap;
179
	private readonly pendingDiagnostics: PendingDiagnostics;
180
	private readonly diagnosticDelayer: Delayer<any>;
M
Matt Bierner 已提交
181
	private pendingGetErr: GetErrRequest | undefined;
M
Matt Bierner 已提交
182
	private listening: boolean = false;
E
Erich Gamma 已提交
183

M
💄  
Matt Bierner 已提交
184
	constructor(
185
		client: ITypeScriptServiceClient,
186
		modeIds: string[]
M
💄  
Matt Bierner 已提交
187
	) {
188
		super();
E
Erich Gamma 已提交
189
		this.client = client;
190
		this.modeIds = new Set<string>(modeIds);
E
Erich Gamma 已提交
191

192
		this.diagnosticDelayer = new Delayer<any>(300);
E
Erich Gamma 已提交
193

194
		const pathNormalizer = (path: vscode.Uri) => this.client.normalizedPath(path);
195 196
		this.syncedBuffers = new SyncedBufferMap(pathNormalizer);
		this.pendingDiagnostics = new PendingDiagnostics(pathNormalizer);
197 198

		this.updateConfiguration();
199
		vscode.workspace.onDidChangeConfiguration(this.updateConfiguration, this, this._disposables);
200 201
	}

202
	private readonly _onDelete = this._register(new vscode.EventEmitter<vscode.Uri>());
M
Matt Bierner 已提交
203 204
	public readonly onDelete = this._onDelete.event;

205
	public listen(): void {
M
Matt Bierner 已提交
206 207 208 209
		if (this.listening) {
			return;
		}
		this.listening = true;
210 211 212 213
		vscode.workspace.onDidOpenTextDocument(this.openTextDocument, this, this._disposables);
		vscode.workspace.onDidCloseTextDocument(this.onDidCloseTextDocument, this, this._disposables);
		vscode.workspace.onDidChangeTextDocument(this.onDidChangeTextDocument, this, this._disposables);
		vscode.workspace.textDocuments.forEach(this.openTextDocument, this);
E
Erich Gamma 已提交
214 215
	}

216
	public handles(resource: vscode.Uri): boolean {
217
		return this.syncedBuffers.has(resource);
218 219
	}

220
	public toResource(filePath: string): vscode.Uri {
221 222 223 224
		const buffer = this.syncedBuffers.getForPath(filePath);
		if (buffer) {
			return buffer.resource;
		}
225
		return vscode.Uri.file(filePath);
226 227
	}

E
Erich Gamma 已提交
228
	public reOpenDocuments(): void {
229
		for (const buffer of this.syncedBuffers.allBuffers) {
M
Matt Bierner 已提交
230 231
			buffer.open();
		}
E
Erich Gamma 已提交
232 233
	}

234
	public openTextDocument(document: vscode.TextDocument): void {
235
		if (!this.modeIds.has(document.languageId)) {
E
Erich Gamma 已提交
236 237
			return;
		}
M
Matt Bierner 已提交
238
		const resource = document.uri;
M
Matt Bierner 已提交
239
		const filepath = this.client.normalizedPath(resource);
E
Erich Gamma 已提交
240 241 242
		if (!filepath) {
			return;
		}
243 244 245 246 247

		if (this.syncedBuffers.has(resource)) {
			return;
		}

248
		const syncedBuffer = new SyncedBuffer(document, filepath, this.client);
249
		this.syncedBuffers.set(resource, syncedBuffer);
E
Erich Gamma 已提交
250
		syncedBuffer.open();
251
		this.requestDiagnostic(syncedBuffer);
E
Erich Gamma 已提交
252 253
	}

254
	public closeResource(resource: vscode.Uri): void {
255
		const syncedBuffer = this.syncedBuffers.get(resource);
E
Erich Gamma 已提交
256 257 258
		if (!syncedBuffer) {
			return;
		}
259
		this.pendingDiagnostics.delete(resource);
260
		this.syncedBuffers.delete(resource);
E
Erich Gamma 已提交
261
		syncedBuffer.close();
262
		if (!fs.existsSync(resource.fsPath)) {
M
Matt Bierner 已提交
263
			this._onDelete.fire(resource);
D
Dirk Baeumer 已提交
264 265
			this.requestAllDiagnostics();
		}
E
Erich Gamma 已提交
266 267
	}

268
	public interuptGetErr<R>(f: () => R): R {
269 270 271 272 273 274 275 276 277 278 279
		// TODO: re-enable for 1.27 insiders
		return f();
		// if (!this.pendingGetErr) {
		// 	return f();
		// }

		// this.pendingGetErr.cancel();
		// this.pendingGetErr = undefined;
		// const result = f();
		// this.triggerDiagnostics();
		// return result;
280 281
	}

282
	private onDidCloseTextDocument(document: vscode.TextDocument): void {
283 284 285
		this.closeResource(document.uri);
	}

286
	private onDidChangeTextDocument(e: vscode.TextDocumentChangeEvent): void {
287
		const syncedBuffer = this.syncedBuffers.get(e.document.uri);
M
Matt Bierner 已提交
288 289 290 291 292
		if (!syncedBuffer) {
			return;
		}

		syncedBuffer.onContentChanged(e.contentChanges);
M
Matt Bierner 已提交
293
		const didTrigger = this.requestDiagnostic(syncedBuffer);
294

M
Matt Bierner 已提交
295
		if (!didTrigger && this.pendingGetErr) {
296
			// In this case we always want to re-trigger all diagnostics
M
Matt Bierner 已提交
297 298
			this.pendingGetErr.cancel();
			this.pendingGetErr = undefined;
299
			this.triggerDiagnostics();
E
Erich Gamma 已提交
300 301 302 303
		}
	}

	public requestAllDiagnostics() {
304 305
		for (const buffer of this.syncedBuffers.allBuffers) {
			if (this.shouldValidate(buffer)) {
306
				this.pendingDiagnostics.set(buffer.resource, Date.now());
307
			}
M
Matt Bierner 已提交
308
		}
309
		this.triggerDiagnostics();
E
Erich Gamma 已提交
310 311
	}

312
	public getErr(resources: vscode.Uri[]): any {
313 314 315 316 317 318
		const handledResources = resources.filter(resource => this.handles(resource));
		if (!handledResources.length) {
			return;
		}

		for (const resource of handledResources) {
319
			this.pendingDiagnostics.set(resource, Date.now());
320 321
		}

322 323 324 325
		this.triggerDiagnostics();
	}

	private triggerDiagnostics(delay: number = 200) {
326 327
		this.diagnosticDelayer.trigger(() => {
			this.sendPendingDiagnostics();
328
		}, delay);
329 330
	}

M
Matt Bierner 已提交
331
	private requestDiagnostic(buffer: SyncedBuffer): boolean {
332
		if (!this.shouldValidate(buffer)) {
M
Matt Bierner 已提交
333
			return false;
334 335
		}

336
		this.pendingDiagnostics.set(buffer.resource, Date.now());
337

M
Matt Bierner 已提交
338
		const delay = Math.min(Math.max(Math.ceil(buffer.lineCount / 20), 300), 800);
339
		this.triggerDiagnostics(delay);
M
Matt Bierner 已提交
340
		return true;
E
Erich Gamma 已提交
341 342
	}

343
	public hasPendingDiagnostics(resource: vscode.Uri): boolean {
344
		return this.pendingDiagnostics.has(resource);
345 346
	}

E
Erich Gamma 已提交
347
	private sendPendingDiagnostics(): void {
348
		const fileList = this.pendingDiagnostics.getFileList();
E
Erich Gamma 已提交
349 350

		// Add all open TS buffers to the geterr request. They might be visible
351 352 353
		for (const buffer of this.syncedBuffers.values) {
			if (!this.pendingDiagnostics.has(buffer.resource)) {
				fileList.add(buffer.filepath);
E
Erich Gamma 已提交
354
			}
M
Matt Bierner 已提交
355
		}
E
Erich Gamma 已提交
356

M
Matt Bierner 已提交
357 358
		if (this.pendingGetErr) {
			for (const file of this.pendingGetErr.files) {
359
				fileList.add(file);
M
Matt Bierner 已提交
360 361 362
			}
		}

363
		if (fileList.size) {
M
Matt Bierner 已提交
364 365 366 367 368 369 370 371 372
			if (this.pendingGetErr) {
				this.pendingGetErr.cancel();
			}

			const getErr = this.pendingGetErr = GetErrRequest.executeGetErrRequest(this.client, Array.from(fileList), () => {
				if (this.pendingGetErr === getErr) {
					this.pendingGetErr = undefined;
				}
			});
373
		}
M
Matt Bierner 已提交
374

375
		this.pendingDiagnostics.clear();
E
Erich Gamma 已提交
376
	}
377 378

	private updateConfiguration() {
379 380
		const jsConfig = vscode.workspace.getConfiguration('javascript', null);
		const tsConfig = vscode.workspace.getConfiguration('typescript', null);
381 382 383 384 385 386 387 388 389 390 391 392 393 394 395

		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;
		}
	}
396
}