extHostDocumentSaveParticipant.test.ts 13.2 KB
Newer Older
1 2 3 4 5
/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
6
import { URI } from 'vs/base/common/uri';
J
Johannes Rieken 已提交
7 8 9
import { ExtHostDocuments } from 'vs/workbench/api/common/extHostDocuments';
import { ExtHostDocumentsAndEditors } from 'vs/workbench/api/common/extHostDocumentsAndEditors';
import { TextDocumentSaveReason, TextEdit, Position, EndOfLine } from 'vs/workbench/api/common/extHostTypes';
10
import { MainThreadTextEditorsShape, WorkspaceEditDto } from 'vs/workbench/api/common/extHost.protocol';
J
Johannes Rieken 已提交
11
import { ExtHostDocumentSaveParticipant } from 'vs/workbench/api/common/extHostDocumentSaveParticipant';
A
Alex Dima 已提交
12
import { SingleProxyRPCProtocol } from './testRPCProtocol';
13
import { SaveReason } from 'vs/workbench/services/textfile/common/textfiles';
14
import * as vscode from 'vscode';
15
import { mock } from 'vs/workbench/test/electron-browser/api/mock';
16
import { NullLogService } from 'vs/platform/log/common/log';
17
import { isResourceTextEdit, ResourceTextEdit } from 'vs/editor/common/modes';
18
import { timeout } from 'vs/base/common/async';
19
import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions';
20 21 22 23

suite('ExtHostDocumentSaveParticipant', () => {

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

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

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

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

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

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

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

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

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

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

			assert.ok(event);
88
			assert.throws(() => { event.document = null!; });
J
Johannes Rieken 已提交
89 90 91
		});
	});

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

193
		let sub = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (event) {
194

195 196 197
			event.waitUntil(timeout(10));
			event.waitUntil(timeout(10));
			event.waitUntil(timeout(10));
198 199
		});

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

	});

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

209
		let sub = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (event) {
210

J
Johannes Rieken 已提交
211
			event.waitUntil(new Promise((resolve, reject) => {
212 213
				setTimeout(() => {
					try {
214
						assert.throws(() => event.waitUntil(timeout(10)));
R
Rob Lourens 已提交
215
						resolve(undefined);
216 217 218 219 220 221 222 223
					} catch (e) {
						reject(e);
					}

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

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

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

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

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

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

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

247
		let sub1 = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (e) {
248
			e.waitUntil(Promise.reject(new Error('dddd')));
249 250
		});

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

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

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

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

273
		let sub = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (e) {
274 275
			e.waitUntil(Promise.resolve([TextEdit.insert(new Position(0, 0), 'bar')]));
			e.waitUntil(Promise.resolve([TextEdit.setEndOfLine(EndOfLine.CRLF)]));
276 277
		});

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

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

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

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

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

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

311
			e.waitUntil(Promise.resolve([TextEdit.insert(new Position(0, 0), 'bar')]));
312 313
		});

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<MainThreadTextEditorsShape>() {
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, {
336 337
							changes: [{
								range,
338
								text,
339 340
								rangeOffset: undefined!,
								rangeLength: undefined!,
341
							}],
342
							eol: undefined!,
343
							versionId: documents.getDocumentData(uri)!.version + 1
344 345
						}, true);
					}
346
				}
347

348
				return Promise.resolve(true);
349 350 351
			}
		});

352
		const document = documents.getDocument(resource);
353

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

359
			e.waitUntil(Promise.resolve([TextEdit.insert(new Position(0, 0), 'bar')]));
360 361
		});

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

367
			e.waitUntil(Promise.resolve([TextEdit.insert(new Position(0, 0), 'bar')]));
368 369
		});

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

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

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

	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);
		});
	});
399
});