bufferSyncSupport.ts 10.0 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 { CancellationTokenSource, EventEmitter, TextDocument, TextDocumentChangeEvent, TextDocumentContentChangeEvent, Uri, workspace } 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 35
	constructor(
		private readonly document: 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
			}
		}

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

69 70 71 72
	public get resource(): Uri {
		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 94 95 96
			file: this.filepath
		};
		this.client.execute('close', args, false);
	}

M
Matt Bierner 已提交
97
	public onContentChanged(events: 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 104 105 106 107
			};
			this.client.execute('change', args, false);
		}
	}
}

M
Matt Bierner 已提交
108 109 110 111 112 113 114 115 116 117 118
class SyncedBufferMap extends ResourceMap<SyncedBuffer> {

	public getForPath(filePath: string): SyncedBuffer | undefined {
		return this.get(Uri.file(filePath));
	}

	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 134 135 136 137 138 139 140 141

	public static executeGetErrRequest(
		client: ITypeScriptServiceClient,
		files: string[],
		onDone: () => void
	) {
		const token = new CancellationTokenSource();
		return new GetErrRequest(client, files, token, onDone);
	}

	private _done: boolean = false;

	private constructor(
		client: ITypeScriptServiceClient,
M
Matt Bierner 已提交
142
		public readonly files: string[],
M
Matt Bierner 已提交
143 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
		private readonly _token: CancellationTokenSource,
		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: Uri) => this.client.normalizedPath(path);
195 196
		this.syncedBuffers = new SyncedBufferMap(pathNormalizer);
		this.pendingDiagnostics = new PendingDiagnostics(pathNormalizer);
197 198

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

202
	private readonly _onDelete = this._register(new EventEmitter<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
		workspace.onDidOpenTextDocument(this.openTextDocument, this, this._disposables);
		workspace.onDidCloseTextDocument(this.onDidCloseTextDocument, this, this._disposables);
		workspace.onDidChangeTextDocument(this.onDidChangeTextDocument, this, this._disposables);
213
		workspace.textDocuments.forEach(this.openTextDocument, this);
E
Erich Gamma 已提交
214 215
	}

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

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

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: 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: Uri): void {
255
		const syncedBuffer = this.syncedBuffers.get(resource);
E
Erich Gamma 已提交
256 257 258
		if (!syncedBuffer) {
			return;
		}
259
		this.syncedBuffers.delete(resource);
E
Erich Gamma 已提交
260
		syncedBuffer.close();
261
		if (!fs.existsSync(resource.fsPath)) {
M
Matt Bierner 已提交
262
			this._onDelete.fire(resource);
D
Dirk Baeumer 已提交
263 264
			this.requestAllDiagnostics();
		}
E
Erich Gamma 已提交
265 266
	}

267 268 269 270
	private onDidCloseTextDocument(document: TextDocument): void {
		this.closeResource(document.uri);
	}

271
	private onDidChangeTextDocument(e: TextDocumentChangeEvent): void {
272
		const syncedBuffer = this.syncedBuffers.get(e.document.uri);
M
Matt Bierner 已提交
273 274 275 276 277
		if (!syncedBuffer) {
			return;
		}

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

M
Matt Bierner 已提交
280
		if (!didTrigger && this.pendingGetErr) {
281
			// In this case we always want to re-trigger all diagnostics
M
Matt Bierner 已提交
282 283
			this.pendingGetErr.cancel();
			this.pendingGetErr = undefined;
284
			this.triggerDiagnostics();
E
Erich Gamma 已提交
285 286 287 288
		}
	}

	public requestAllDiagnostics() {
289 290
		for (const buffer of this.syncedBuffers.allBuffers) {
			if (this.shouldValidate(buffer)) {
291
				this.pendingDiagnostics.set(buffer.resource, Date.now());
292
			}
M
Matt Bierner 已提交
293
		}
294
		this.triggerDiagnostics();
E
Erich Gamma 已提交
295 296
	}

297 298 299 300 301 302 303
	public getErr(resources: Uri[]): any {
		const handledResources = resources.filter(resource => this.handles(resource));
		if (!handledResources.length) {
			return;
		}

		for (const resource of handledResources) {
304
			this.pendingDiagnostics.set(resource, Date.now());
305 306
		}

307 308 309 310
		this.triggerDiagnostics();
	}

	private triggerDiagnostics(delay: number = 200) {
311 312
		this.diagnosticDelayer.trigger(() => {
			this.sendPendingDiagnostics();
313
		}, delay);
314 315
	}

M
Matt Bierner 已提交
316
	private requestDiagnostic(buffer: SyncedBuffer): boolean {
317
		if (!this.shouldValidate(buffer)) {
M
Matt Bierner 已提交
318
			return false;
319 320
		}

321
		this.pendingDiagnostics.set(buffer.resource, Date.now());
322

M
Matt Bierner 已提交
323
		const delay = Math.min(Math.max(Math.ceil(buffer.lineCount / 20), 300), 800);
324
		this.triggerDiagnostics(delay);
M
Matt Bierner 已提交
325
		return true;
E
Erich Gamma 已提交
326 327
	}

328
	public hasPendingDiagnostics(resource: Uri): boolean {
329
		return this.pendingDiagnostics.has(resource);
330 331
	}

E
Erich Gamma 已提交
332
	private sendPendingDiagnostics(): void {
333
		const fileList = this.pendingDiagnostics.getFileList();
E
Erich Gamma 已提交
334 335

		// Add all open TS buffers to the geterr request. They might be visible
336 337 338
		for (const buffer of this.syncedBuffers.values) {
			if (!this.pendingDiagnostics.has(buffer.resource)) {
				fileList.add(buffer.filepath);
E
Erich Gamma 已提交
339
			}
M
Matt Bierner 已提交
340
		}
E
Erich Gamma 已提交
341

M
Matt Bierner 已提交
342 343
		if (this.pendingGetErr) {
			for (const file of this.pendingGetErr.files) {
344
				fileList.add(file);
M
Matt Bierner 已提交
345 346 347
			}
		}

348
		if (fileList.size) {
M
Matt Bierner 已提交
349 350 351 352 353 354 355 356 357
			if (this.pendingGetErr) {
				this.pendingGetErr.cancel();
			}

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

360
		this.pendingDiagnostics.clear();
E
Erich Gamma 已提交
361
	}
362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380

	private updateConfiguration() {
		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;
		}
	}
381
}