bufferSyncSupport.ts 9.9 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';
M
Matt Bierner 已提交
7
import { CancellationTokenSource, Disposable, 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 { disposeAll } from '../utils/dispose';
M
Matt Bierner 已提交
13
import * as languageModeIds from '../utils/languageModeIds';
14
import * as typeConverters from '../utils/typeConverters';
M
Matt Bierner 已提交
15
import { ResourceMap } from './resourceMap';
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 119 120 121 122
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;
	}

	public get allResources(): Iterable<string> {
		return this.keys;
	}
}

123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144
class PendingDiagnostics {
	private readonly _pendingDiagnostics = new Map<string, number>();

	public set(file: string, time: number): void {
		this._pendingDiagnostics.set(file, time);
	}

	public has(file: string): boolean {
		return this._pendingDiagnostics.has(file);
	}

	public clear(): void {
		this._pendingDiagnostics.clear();
	}

	public getFileList(): Set<string> {
		return new Set(Array.from(this._pendingDiagnostics.entries())
			.sort((a, b) => a[1] - b[1])
			.map(entry => entry[0]));
	}
}

E
Erich Gamma 已提交
145 146
export default class BufferSyncSupport {

147
	private readonly client: ITypeScriptServiceClient;
E
Erich Gamma 已提交
148

149 150
	private _validateJavaScript: boolean = true;
	private _validateTypeScript: boolean = true;
151 152
	private readonly modeIds: Set<string>;
	private readonly disposables: Disposable[] = [];
153
	private readonly syncedBuffers: SyncedBufferMap;
E
Erich Gamma 已提交
154

155
	private readonly pendingDiagnostics = new PendingDiagnostics();
156
	private readonly diagnosticDelayer: Delayer<any>;
M
Matt Bierner 已提交
157
	private pendingGetErr: { request: Promise<any>, files: string[], token: CancellationTokenSource } | undefined;
M
Matt Bierner 已提交
158
	private listening: boolean = false;
E
Erich Gamma 已提交
159

M
💄  
Matt Bierner 已提交
160
	constructor(
161
		client: ITypeScriptServiceClient,
162
		modeIds: string[]
M
💄  
Matt Bierner 已提交
163
	) {
E
Erich Gamma 已提交
164
		this.client = client;
165
		this.modeIds = new Set<string>(modeIds);
E
Erich Gamma 已提交
166

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

169
		this.syncedBuffers = new SyncedBufferMap(path => this.normalizePath(path));
170 171

		this.updateConfiguration();
172
		workspace.onDidChangeConfiguration(this.updateConfiguration, this, this.disposables);
173 174
	}

M
Matt Bierner 已提交
175 176 177
	private readonly _onDelete = new EventEmitter<Uri>();
	public readonly onDelete = this._onDelete.event;

178
	public listen(): void {
M
Matt Bierner 已提交
179 180 181 182
		if (this.listening) {
			return;
		}
		this.listening = true;
183
		workspace.onDidOpenTextDocument(this.openTextDocument, this, this.disposables);
184 185
		workspace.onDidCloseTextDocument(this.onDidCloseTextDocument, this, this.disposables);
		workspace.onDidChangeTextDocument(this.onDidChangeTextDocument, this, this.disposables);
186
		workspace.textDocuments.forEach(this.openTextDocument, this);
E
Erich Gamma 已提交
187 188
	}

189
	public handles(resource: Uri): boolean {
190
		return this.syncedBuffers.has(resource);
191 192
	}

193 194 195 196 197 198 199 200
	public toResource(filePath: string): Uri {
		const buffer = this.syncedBuffers.getForPath(filePath);
		if (buffer) {
			return buffer.resource;
		}
		return Uri.file(filePath);
	}

E
Erich Gamma 已提交
201
	public reOpenDocuments(): void {
202
		for (const buffer of this.syncedBuffers.allBuffers) {
M
Matt Bierner 已提交
203 204
			buffer.open();
		}
E
Erich Gamma 已提交
205 206 207
	}

	public dispose(): void {
208
		disposeAll(this.disposables);
M
Matt Bierner 已提交
209
		this._onDelete.dispose();
E
Erich Gamma 已提交
210 211
	}

212
	public openTextDocument(document: TextDocument): void {
213
		if (!this.modeIds.has(document.languageId)) {
E
Erich Gamma 已提交
214 215
			return;
		}
M
Matt Bierner 已提交
216
		const resource = document.uri;
M
Matt Bierner 已提交
217
		const filepath = this.client.normalizedPath(resource);
E
Erich Gamma 已提交
218 219 220
		if (!filepath) {
			return;
		}
221 222 223 224 225

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

226
		const syncedBuffer = new SyncedBuffer(document, filepath, this.client);
227
		this.syncedBuffers.set(resource, syncedBuffer);
E
Erich Gamma 已提交
228
		syncedBuffer.open();
229
		this.requestDiagnostic(syncedBuffer);
E
Erich Gamma 已提交
230 231
	}

232
	public closeResource(resource: Uri): void {
233
		const syncedBuffer = this.syncedBuffers.get(resource);
E
Erich Gamma 已提交
234 235 236
		if (!syncedBuffer) {
			return;
		}
237
		this.syncedBuffers.delete(resource);
E
Erich Gamma 已提交
238
		syncedBuffer.close();
239
		if (!fs.existsSync(resource.fsPath)) {
M
Matt Bierner 已提交
240
			this._onDelete.fire(resource);
D
Dirk Baeumer 已提交
241 242
			this.requestAllDiagnostics();
		}
E
Erich Gamma 已提交
243 244
	}

245 246 247 248
	private onDidCloseTextDocument(document: TextDocument): void {
		this.closeResource(document.uri);
	}

249
	private onDidChangeTextDocument(e: TextDocumentChangeEvent): void {
250
		const syncedBuffer = this.syncedBuffers.get(e.document.uri);
M
Matt Bierner 已提交
251 252 253 254 255
		if (!syncedBuffer) {
			return;
		}

		syncedBuffer.onContentChanged(e.contentChanges);
256
		this.requestDiagnostic(syncedBuffer);
257

M
Matt Bierner 已提交
258 259 260
		if (this.pendingGetErr) {
			this.pendingGetErr.token.cancel();
			this.pendingGetErr = undefined;
261

262
			this.triggerDiagnostics();
E
Erich Gamma 已提交
263 264 265 266
		}
	}

	public requestAllDiagnostics() {
267 268 269 270
		for (const buffer of this.syncedBuffers.allBuffers) {
			if (this.shouldValidate(buffer)) {
				this.pendingDiagnostics.set(buffer.filepath, Date.now());
			}
M
Matt Bierner 已提交
271
		}
272
		this.triggerDiagnostics();
E
Erich Gamma 已提交
273 274
	}

275 276 277 278 279 280 281
	public getErr(resources: Uri[]): any {
		const handledResources = resources.filter(resource => this.handles(resource));
		if (!handledResources.length) {
			return;
		}

		for (const resource of handledResources) {
M
Matt Bierner 已提交
282
			const file = this.client.normalizedPath(resource);
283 284 285 286 287
			if (file) {
				this.pendingDiagnostics.set(file, Date.now());
			}
		}

288 289 290 291
		this.triggerDiagnostics();
	}

	private triggerDiagnostics(delay: number = 200) {
292 293
		this.diagnosticDelayer.trigger(() => {
			this.sendPendingDiagnostics();
294
		}, delay);
295 296
	}

297 298
	private requestDiagnostic(buffer: SyncedBuffer): void {
		if (!this.shouldValidate(buffer)) {
299 300 301
			return;
		}

302
		this.pendingDiagnostics.set(buffer.filepath, Date.now());
303 304

		const lineCount = buffer.lineCount;
305 306
		const delay = Math.min(Math.max(Math.ceil(lineCount / 20), 300), 800);
		this.triggerDiagnostics(delay);
E
Erich Gamma 已提交
307 308
	}

309
	public hasPendingDiagnostics(resource: Uri): boolean {
M
Matt Bierner 已提交
310
		const file = this.client.normalizedPath(resource);
311 312 313
		return !file || this.pendingDiagnostics.has(file);
	}

E
Erich Gamma 已提交
314
	private sendPendingDiagnostics(): void {
315
		const fileList = this.pendingDiagnostics.getFileList();
E
Erich Gamma 已提交
316 317

		// Add all open TS buffers to the geterr request. They might be visible
318
		for (const file of this.syncedBuffers.allResources) {
319 320
			if (!this.pendingDiagnostics.has(file)) {
				fileList.add(file);
E
Erich Gamma 已提交
321
			}
M
Matt Bierner 已提交
322
		}
E
Erich Gamma 已提交
323

M
Matt Bierner 已提交
324 325
		if (this.pendingGetErr) {
			for (const file of this.pendingGetErr.files) {
326
				fileList.add(file);
M
Matt Bierner 已提交
327 328 329
			}
		}

330 331
		if (fileList.size) {
			const files = Array.from(fileList);
332 333
			const args: Proto.GeterrRequestArgs = {
				delay: 0,
334
				files
M
Matt Bierner 已提交
335 336 337 338
			};
			const token = new CancellationTokenSource();

			const getErr = this.pendingGetErr = {
M
Matt Bierner 已提交
339
				request: this.client.executeAsync('geterr', args, token.token)
M
Matt Bierner 已提交
340 341 342 343 344 345
					.then(undefined, () => { })
					.then(() => {
						if (this.pendingGetErr === getErr) {
							this.pendingGetErr = undefined;
						}
					}),
346
				files,
M
Matt Bierner 已提交
347
				token
348 349
			};
		}
350
		this.pendingDiagnostics.clear();
E
Erich Gamma 已提交
351
	}
352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370

	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;
		}
	}
371 372

	private normalizePath(path: Uri): string | null {
373
		return this.client.normalizedPath(path);
374 375
	}
}