extHostDocumentSaveParticipant.test.ts 13.1 KB
Newer Older
1 2 3 4 5 6 7 8
/*---------------------------------------------------------------------------------------------
 *  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 * as assert from 'assert';
import URI from 'vs/base/common/uri';
9 10
import { TPromise } from 'vs/base/common/winjs.base';
import { ExtHostDocuments } from 'vs/workbench/api/node/extHostDocuments';
J
Johannes Rieken 已提交
11
import { ExtHostDocumentsAndEditors } from 'vs/workbench/api/node/extHostDocumentsAndEditors';
12
import { TextDocumentSaveReason, TextEdit, Position, EndOfLine } from 'vs/workbench/api/node/extHostTypes';
13
import { MainThreadEditorsShape, WorkspaceEditDto } from 'vs/workbench/api/node/extHost.protocol';
14
import { ExtHostDocumentSaveParticipant } from 'vs/workbench/api/node/extHostDocumentSaveParticipant';
A
Alex Dima 已提交
15
import { SingleProxyRPCProtocol } from './testRPCProtocol';
16
import { SaveReason } from 'vs/workbench/services/textfile/common/textfiles';
17
import * as vscode from 'vscode';
18
import { mock } from 'vs/workbench/test/electron-browser/api/mock';
19
import { IExtensionDescription } from 'vs/platform/extensions/common/extensions';
20
import { NullLogService } from 'vs/platform/log/common/log';
21
import { isResourceTextEdit, ResourceTextEdit } from 'vs/editor/common/modes';
22 23 24 25

suite('ExtHostDocumentSaveParticipant', () => {

	let resource = URI.parse('foo:bar');
26
	let mainThreadEditors = new class extends mock<MainThreadEditorsShape>() { };
27
	let documents: ExtHostDocuments;
28
	let nullLogService = new NullLogService();
29 30 31 32 33 34 35 36 37 38
	let nullExtensionDescription: IExtensionDescription = {
		id: 'nullExtensionDescription',
		name: 'Null Extension Description',
		publisher: 'vscode',
		enableProposedApi: false,
		engines: undefined,
		extensionFolderPath: undefined,
		isBuiltin: false,
		version: undefined
	};
39 40

	setup(() => {
A
Alex Dima 已提交
41
		const documentsAndEditors = new ExtHostDocumentsAndEditors(SingleProxyRPCProtocol(null));
J
Johannes Rieken 已提交
42 43 44 45
		documentsAndEditors.$acceptDocumentsAndEditorsDelta({
			addedDocuments: [{
				isDirty: false,
				modeId: 'foo',
46
				uri: resource,
J
Johannes Rieken 已提交
47 48 49 50 51
				versionId: 1,
				lines: ['foo'],
				EOL: '\n',
			}]
		});
A
Alex Dima 已提交
52
		documents = new ExtHostDocuments(SingleProxyRPCProtocol(null), documentsAndEditors);
53 54 55
	});

	test('no listeners, no problem', () => {
56
		const participant = new ExtHostDocumentSaveParticipant(nullLogService, documents, mainThreadEditors);
57
		return participant.$participateInSave(resource, SaveReason.EXPLICIT).then(() => assert.ok(true));
58 59 60
	});

	test('event delivery', () => {
61
		const participant = new ExtHostDocumentSaveParticipant(nullLogService, documents, mainThreadEditors);
62

63
		let event: vscode.TextDocumentWillSaveEvent;
64
		let sub = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (e) {
65 66 67
			event = e;
		});

68
		return participant.$participateInSave(resource, SaveReason.EXPLICIT).then(() => {
69 70 71
			sub.dispose();

			assert.ok(event);
72
			assert.equal(event.reason, TextDocumentSaveReason.Manual);
73 74 75 76
			assert.equal(typeof event.waitUntil, 'function');
		});
	});

J
Johannes Rieken 已提交
77
	test('event delivery, immutable', () => {
78
		const participant = new ExtHostDocumentSaveParticipant(nullLogService, documents, mainThreadEditors);
J
Johannes Rieken 已提交
79

80
		let event: vscode.TextDocumentWillSaveEvent;
81
		let sub = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (e) {
J
Johannes Rieken 已提交
82 83 84
			event = e;
		});

85
		return participant.$participateInSave(resource, SaveReason.EXPLICIT).then(() => {
J
Johannes Rieken 已提交
86 87 88 89 90 91 92
			sub.dispose();

			assert.ok(event);
			assert.throws(() => event.document = null);
		});
	});

93
	test('event delivery, bad listener', () => {
94
		const participant = new ExtHostDocumentSaveParticipant(nullLogService, documents, mainThreadEditors);
95

96
		let sub = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (e) {
97 98 99
			throw new Error('💀');
		});

100
		return participant.$participateInSave(resource, SaveReason.EXPLICIT).then(values => {
101 102 103
			sub.dispose();

			const [first] = values;
104
			assert.equal(first, false);
105 106 107 108
		});
	});

	test('event delivery, bad listener doesn\'t prevent more events', () => {
109
		const participant = new ExtHostDocumentSaveParticipant(nullLogService, documents, mainThreadEditors);
110

111
		let sub1 = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (e) {
112 113 114
			throw new Error('💀');
		});
		let event: vscode.TextDocumentWillSaveEvent;
115
		let sub2 = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (e) {
116 117 118
			event = e;
		});

119
		return participant.$participateInSave(resource, SaveReason.EXPLICIT).then(() => {
120 121 122 123 124 125 126
			sub1.dispose();
			sub2.dispose();

			assert.ok(event);
		});
	});

J
Johannes Rieken 已提交
127
	test('event delivery, in subscriber order', () => {
128
		const participant = new ExtHostDocumentSaveParticipant(nullLogService, documents, mainThreadEditors);
129 130

		let counter = 0;
131
		let sub1 = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (event) {
132 133 134
			assert.equal(counter++, 0);
		});

135
		let sub2 = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (event) {
136 137 138
			assert.equal(counter++, 1);
		});

139
		return participant.$participateInSave(resource, SaveReason.EXPLICIT).then(() => {
140 141 142 143 144
			sub1.dispose();
			sub2.dispose();
		});
	});

145
	test('event delivery, ignore bad listeners', async () => {
146
		const participant = new ExtHostDocumentSaveParticipant(nullLogService, documents, mainThreadEditors, { timeout: 5, errors: 1 });
147 148

		let callCount = 0;
149
		let sub = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (event) {
150 151 152 153
			callCount += 1;
			throw new Error('boom');
		});

154 155 156 157
		await participant.$participateInSave(resource, SaveReason.EXPLICIT);
		await participant.$participateInSave(resource, SaveReason.EXPLICIT);
		await participant.$participateInSave(resource, SaveReason.EXPLICIT);
		await participant.$participateInSave(resource, SaveReason.EXPLICIT);
158

159 160
		sub.dispose();
		assert.equal(callCount, 2);
161 162
	});

163
	test('event delivery, overall timeout', () => {
164
		const participant = new ExtHostDocumentSaveParticipant(nullLogService, documents, mainThreadEditors, { timeout: 20, errors: 5 });
165 166

		let callCount = 0;
167
		let sub1 = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (event) {
168
			callCount += 1;
J
Johannes Rieken 已提交
169
			event.waitUntil(TPromise.timeout(17));
170 171
		});

172
		let sub2 = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (event) {
173
			callCount += 1;
J
Johannes Rieken 已提交
174
			event.waitUntil(TPromise.timeout(17));
175 176
		});

177
		let sub3 = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (event) {
178 179 180
			callCount += 1;
		});

181
		return participant.$participateInSave(resource, SaveReason.EXPLICIT).then(values => {
182 183 184 185 186 187 188 189 190
			sub1.dispose();
			sub2.dispose();
			sub3.dispose();

			assert.equal(callCount, 2);
			assert.equal(values.length, 2);
		});
	});

191
	test('event delivery, waitUntil', () => {
192
		const participant = new ExtHostDocumentSaveParticipant(nullLogService, documents, mainThreadEditors);
193

194
		let sub = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (event) {
195 196 197 198 199 200

			event.waitUntil(TPromise.timeout(10));
			event.waitUntil(TPromise.timeout(10));
			event.waitUntil(TPromise.timeout(10));
		});

201
		return participant.$participateInSave(resource, SaveReason.EXPLICIT).then(() => {
202 203 204 205 206 207
			sub.dispose();
		});

	});

	test('event delivery, waitUntil must be called sync', () => {
208
		const participant = new ExtHostDocumentSaveParticipant(nullLogService, documents, mainThreadEditors);
209

210
		let sub = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (event) {
211 212 213 214 215 216 217 218 219 220 221 222 223 224

			event.waitUntil(new TPromise((resolve, reject) => {
				setTimeout(() => {
					try {
						assert.throws(() => event.waitUntil(TPromise.timeout(10)));
						resolve(void 0);
					} catch (e) {
						reject(e);
					}

				}, 10);
			}));
		});

225
		return participant.$participateInSave(resource, SaveReason.EXPLICIT).then(() => {
226 227 228 229
			sub.dispose();
		});
	});

230
	test('event delivery, waitUntil will timeout', () => {
231
		const participant = new ExtHostDocumentSaveParticipant(nullLogService, documents, mainThreadEditors, { timeout: 5, errors: 3 });
232

233
		let sub = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (event) {
234 235 236
			event.waitUntil(TPromise.timeout(15));
		});

237
		return participant.$participateInSave(resource, SaveReason.EXPLICIT).then(values => {
238 239 240
			sub.dispose();

			const [first] = values;
241
			assert.equal(first, false);
242 243 244
		});
	});

245
	test('event delivery, waitUntil failure handling', () => {
246
		const participant = new ExtHostDocumentSaveParticipant(nullLogService, documents, mainThreadEditors);
247

248
		let sub1 = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (e) {
249
			e.waitUntil(TPromise.wrapError(new Error('dddd')));
250 251
		});

252
		let event: vscode.TextDocumentWillSaveEvent;
253
		let sub2 = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (e) {
254 255 256
			event = e;
		});

257
		return participant.$participateInSave(resource, SaveReason.EXPLICIT).then(() => {
258 259 260 261 262 263
			assert.ok(event);
			sub1.dispose();
			sub2.dispose();
		});
	});

264 265
	test('event delivery, pushEdits sync', () => {

266
		let dto: WorkspaceEditDto;
267
		const participant = new ExtHostDocumentSaveParticipant(nullLogService, documents, new class extends mock<MainThreadEditorsShape>() {
268 269
			$tryApplyWorkspaceEdit(_edits: WorkspaceEditDto) {
				dto = _edits;
270 271 272 273
				return TPromise.as(true);
			}
		});

274
		let sub = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (e) {
J
Johannes Rieken 已提交
275
			e.waitUntil(TPromise.as([TextEdit.insert(new Position(0, 0), 'bar')]));
276
			e.waitUntil(TPromise.as([TextEdit.setEndOfLine(EndOfLine.CRLF)]));
277 278
		});

279
		return participant.$participateInSave(resource, SaveReason.EXPLICIT).then(() => {
280 281
			sub.dispose();

282 283 284
			assert.equal(dto.edits.length, 1);
			assert.ok(isResourceTextEdit(dto.edits[0]));
			assert.equal((<ResourceTextEdit>dto.edits[0]).edits.length, 2);
285 286
		});
	});
287 288 289

	test('event delivery, concurrent change', () => {

290
		let edits: WorkspaceEditDto;
291
		const participant = new ExtHostDocumentSaveParticipant(nullLogService, documents, new class extends mock<MainThreadEditorsShape>() {
292
			$tryApplyWorkspaceEdit(_edits: WorkspaceEditDto) {
293 294 295 296 297
				edits = _edits;
				return TPromise.as(true);
			}
		});

298
		let sub = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (e) {
299 300

			// concurrent change from somewhere
301 302 303 304 305 306 307 308 309
			documents.$acceptModelChanged(resource.toString(), {
				changes: [{
					range: { startLineNumber: 1, startColumn: 1, endLineNumber: 1, endColumn: 1 },
					rangeLength: undefined,
					text: 'bar'
				}],
				eol: undefined,
				versionId: 2
			}, true);
310 311 312 313

			e.waitUntil(TPromise.as([TextEdit.insert(new Position(0, 0), 'bar')]));
		});

314
		return participant.$participateInSave(resource, SaveReason.EXPLICIT).then(values => {
315 316 317
			sub.dispose();

			assert.equal(edits, undefined);
318
			assert.equal(values[0], false);
319 320 321 322 323 324
		});

	});

	test('event delivery, two listeners -> two document states', () => {

325
		const participant = new ExtHostDocumentSaveParticipant(nullLogService, documents, new class extends mock<MainThreadEditorsShape>() {
326
			$tryApplyWorkspaceEdit(dto: WorkspaceEditDto) {
327

328 329 330 331 332
				for (const edit of dto.edits) {
					if (!isResourceTextEdit(edit)) {
						continue;
					}
					const { resource, edits } = edit;
333
					const uri = URI.revive(resource);
334
					for (const { text, range } of edits) {
335
						documents.$acceptModelChanged(uri.toString(), {
336 337
							changes: [{
								range,
338
								text,
339 340 341
								rangeLength: undefined,
							}],
							eol: undefined,
342
							versionId: documents.getDocumentData(uri).version + 1
343 344
						}, true);
					}
345
				}
346

347 348 349 350 351 352
				return TPromise.as(true);
			}
		});

		const document = documents.getDocumentData(resource).document;

353
		let sub1 = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (e) {
354 355 356 357 358 359 360
			// the document state we started with
			assert.equal(document.version, 1);
			assert.equal(document.getText(), 'foo');

			e.waitUntil(TPromise.as([TextEdit.insert(new Position(0, 0), 'bar')]));
		});

361
		let sub2 = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (e) {
362 363 364 365 366 367 368
			// the document state AFTER the first listener kicked in
			assert.equal(document.version, 2);
			assert.equal(document.getText(), 'barfoo');

			e.waitUntil(TPromise.as([TextEdit.insert(new Position(0, 0), 'bar')]));
		});

369
		return participant.$participateInSave(resource, SaveReason.EXPLICIT).then(values => {
370 371 372 373 374 375 376 377 378
			sub1.dispose();
			sub2.dispose();

			// the document state AFTER eventing is done
			assert.equal(document.version, 3);
			assert.equal(document.getText(), 'barbarfoo');
		});

	});
379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397

	test('Log failing listener', function () {
		let didLogSomething = false;
		let participant = new ExtHostDocumentSaveParticipant(new class extends NullLogService {
			error(message: string | Error, ...args: any[]): void {
				didLogSomething = true;
			}
		}, documents, mainThreadEditors);


		let sub = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (e) {
			throw new Error('boom');
		});

		return participant.$participateInSave(resource, SaveReason.EXPLICIT).then(() => {
			sub.dispose();
			assert.equal(didLogSomething, true);
		});
	});
398
});