Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/api/test/browser/extHostDocumentSaveParticipant.test.ts
5251 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
import assert from 'assert';
6
import { URI } from '../../../../base/common/uri.js';
7
import { ExtHostDocuments } from '../../common/extHostDocuments.js';
8
import { ExtHostDocumentsAndEditors } from '../../common/extHostDocumentsAndEditors.js';
9
import { TextDocumentSaveReason, TextEdit, Position, EndOfLine } from '../../common/extHostTypes.js';
10
import { MainThreadTextEditorsShape, IWorkspaceEditDto, IWorkspaceTextEditDto, MainThreadBulkEditsShape } from '../../common/extHost.protocol.js';
11
import { ExtHostDocumentSaveParticipant } from '../../common/extHostDocumentSaveParticipant.js';
12
import { SingleProxyRPCProtocol } from '../common/testRPCProtocol.js';
13
import { SaveReason } from '../../../common/editor.js';
14
import type * as vscode from 'vscode';
15
import { mock } from '../../../../base/test/common/mock.js';
16
import { NullLogService } from '../../../../platform/log/common/log.js';
17
import { nullExtensionDescription } from '../../../services/extensions/common/extensions.js';
18
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js';
19
import { SerializableObjectWithBuffers } from '../../../services/extensions/common/proxyIdentifier.js';
20
21
function timeout(n: number) {
22
return new Promise(resolve => setTimeout(resolve, n));
23
}
24
25
suite('ExtHostDocumentSaveParticipant', () => {
26
27
const resource = URI.parse('foo:bar');
28
const mainThreadBulkEdits = new class extends mock<MainThreadBulkEditsShape>() { };
29
let documents: ExtHostDocuments;
30
const nullLogService = new NullLogService();
31
32
setup(() => {
33
const documentsAndEditors = new ExtHostDocumentsAndEditors(SingleProxyRPCProtocol(null), new NullLogService());
34
documentsAndEditors.$acceptDocumentsAndEditorsDelta({
35
addedDocuments: [{
36
isDirty: false,
37
languageId: 'foo',
38
uri: resource,
39
versionId: 1,
40
lines: ['foo'],
41
EOL: '\n',
42
encoding: 'utf8'
43
}]
44
});
45
documents = new ExtHostDocuments(SingleProxyRPCProtocol(null), documentsAndEditors);
46
});
47
48
ensureNoDisposablesAreLeakedInTestSuite();
49
50
test('no listeners, no problem', () => {
51
const participant = new ExtHostDocumentSaveParticipant(nullLogService, documents, mainThreadBulkEdits);
52
return participant.$participateInSave(resource, SaveReason.EXPLICIT).then(() => assert.ok(true));
53
});
54
55
test('event delivery', () => {
56
const participant = new ExtHostDocumentSaveParticipant(nullLogService, documents, mainThreadBulkEdits);
57
58
let event: vscode.TextDocumentWillSaveEvent;
59
const sub = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (e) {
60
event = e;
61
});
62
63
return participant.$participateInSave(resource, SaveReason.EXPLICIT).then(() => {
64
sub.dispose();
65
66
assert.ok(event);
67
assert.strictEqual(event.reason, TextDocumentSaveReason.Manual);
68
assert.strictEqual(typeof event.waitUntil, 'function');
69
});
70
});
71
72
test('event delivery, immutable', () => {
73
const participant = new ExtHostDocumentSaveParticipant(nullLogService, documents, mainThreadBulkEdits);
74
75
let event: vscode.TextDocumentWillSaveEvent;
76
const sub = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (e) {
77
event = e;
78
});
79
80
return participant.$participateInSave(resource, SaveReason.EXPLICIT).then(() => {
81
sub.dispose();
82
83
assert.ok(event);
84
// eslint-disable-next-line local/code-no-any-casts
85
assert.throws(() => { (event.document as any) = null!; });
86
});
87
});
88
89
test('event delivery, bad listener', () => {
90
const participant = new ExtHostDocumentSaveParticipant(nullLogService, documents, mainThreadBulkEdits);
91
92
const sub = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (e) {
93
throw new Error('💀');
94
});
95
96
return participant.$participateInSave(resource, SaveReason.EXPLICIT).then(values => {
97
sub.dispose();
98
99
const [first] = values;
100
assert.strictEqual(first, false);
101
});
102
});
103
104
test('event delivery, bad listener doesn\'t prevent more events', () => {
105
const participant = new ExtHostDocumentSaveParticipant(nullLogService, documents, mainThreadBulkEdits);
106
107
const sub1 = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (e) {
108
throw new Error('💀');
109
});
110
let event: vscode.TextDocumentWillSaveEvent;
111
const sub2 = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (e) {
112
event = e;
113
});
114
115
return participant.$participateInSave(resource, SaveReason.EXPLICIT).then(() => {
116
sub1.dispose();
117
sub2.dispose();
118
119
assert.ok(event);
120
});
121
});
122
123
test('event delivery, in subscriber order', () => {
124
const participant = new ExtHostDocumentSaveParticipant(nullLogService, documents, mainThreadBulkEdits);
125
126
let counter = 0;
127
const sub1 = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (event) {
128
assert.strictEqual(counter++, 0);
129
});
130
131
const sub2 = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (event) {
132
assert.strictEqual(counter++, 1);
133
});
134
135
return participant.$participateInSave(resource, SaveReason.EXPLICIT).then(() => {
136
sub1.dispose();
137
sub2.dispose();
138
});
139
});
140
141
test('event delivery, ignore bad listeners', async () => {
142
const participant = new ExtHostDocumentSaveParticipant(nullLogService, documents, mainThreadBulkEdits, { timeout: 5, errors: 1 });
143
144
let callCount = 0;
145
const sub = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (event) {
146
callCount += 1;
147
throw new Error('boom');
148
});
149
150
await participant.$participateInSave(resource, SaveReason.EXPLICIT);
151
await participant.$participateInSave(resource, SaveReason.EXPLICIT);
152
await participant.$participateInSave(resource, SaveReason.EXPLICIT);
153
await participant.$participateInSave(resource, SaveReason.EXPLICIT);
154
155
sub.dispose();
156
assert.strictEqual(callCount, 2);
157
});
158
159
test('event delivery, overall timeout', async function () {
160
const participant = new ExtHostDocumentSaveParticipant(nullLogService, documents, mainThreadBulkEdits, { timeout: 20, errors: 5 });
161
162
// let callCount = 0;
163
const calls: number[] = [];
164
const sub1 = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (event) {
165
calls.push(1);
166
});
167
168
const sub2 = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (event) {
169
calls.push(2);
170
event.waitUntil(timeout(100));
171
});
172
173
const sub3 = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (event) {
174
calls.push(3);
175
});
176
177
const values = await participant.$participateInSave(resource, SaveReason.EXPLICIT);
178
sub1.dispose();
179
sub2.dispose();
180
sub3.dispose();
181
assert.deepStrictEqual(calls, [1, 2]);
182
assert.strictEqual(values.length, 2);
183
});
184
185
test('event delivery, waitUntil', () => {
186
const participant = new ExtHostDocumentSaveParticipant(nullLogService, documents, mainThreadBulkEdits);
187
188
const sub = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (event) {
189
190
event.waitUntil(timeout(10));
191
event.waitUntil(timeout(10));
192
event.waitUntil(timeout(10));
193
});
194
195
return participant.$participateInSave(resource, SaveReason.EXPLICIT).then(() => {
196
sub.dispose();
197
});
198
199
});
200
201
test('event delivery, waitUntil must be called sync', () => {
202
const participant = new ExtHostDocumentSaveParticipant(nullLogService, documents, mainThreadBulkEdits);
203
204
const sub = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (event) {
205
206
event.waitUntil(new Promise<undefined>((resolve, reject) => {
207
setTimeout(() => {
208
try {
209
assert.throws(() => event.waitUntil(timeout(10)));
210
resolve(undefined);
211
} catch (e) {
212
reject(e);
213
}
214
215
}, 10);
216
}));
217
});
218
219
return participant.$participateInSave(resource, SaveReason.EXPLICIT).then(() => {
220
sub.dispose();
221
});
222
});
223
224
test('event delivery, waitUntil will timeout', function () {
225
226
const participant = new ExtHostDocumentSaveParticipant(nullLogService, documents, mainThreadBulkEdits, { timeout: 5, errors: 3 });
227
228
const sub = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (event) {
229
event.waitUntil(timeout(100));
230
});
231
232
return participant.$participateInSave(resource, SaveReason.EXPLICIT).then(values => {
233
sub.dispose();
234
235
const [first] = values;
236
assert.strictEqual(first, false);
237
});
238
});
239
240
test('event delivery, waitUntil failure handling', () => {
241
const participant = new ExtHostDocumentSaveParticipant(nullLogService, documents, mainThreadBulkEdits);
242
243
const sub1 = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (e) {
244
e.waitUntil(Promise.reject(new Error('dddd')));
245
});
246
247
let event: vscode.TextDocumentWillSaveEvent;
248
const sub2 = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (e) {
249
event = e;
250
});
251
252
return participant.$participateInSave(resource, SaveReason.EXPLICIT).then(() => {
253
assert.ok(event);
254
sub1.dispose();
255
sub2.dispose();
256
});
257
});
258
259
test('event delivery, pushEdits sync', () => {
260
261
let dto: IWorkspaceEditDto;
262
const participant = new ExtHostDocumentSaveParticipant(nullLogService, documents, new class extends mock<MainThreadTextEditorsShape>() {
263
$tryApplyWorkspaceEdit(_edits: SerializableObjectWithBuffers<IWorkspaceEditDto>) {
264
dto = _edits.value;
265
return Promise.resolve(true);
266
}
267
});
268
269
const sub = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (e) {
270
e.waitUntil(Promise.resolve([TextEdit.insert(new Position(0, 0), 'bar')]));
271
e.waitUntil(Promise.resolve([TextEdit.setEndOfLine(EndOfLine.CRLF)]));
272
});
273
274
return participant.$participateInSave(resource, SaveReason.EXPLICIT).then(() => {
275
sub.dispose();
276
277
assert.strictEqual(dto.edits.length, 2);
278
assert.ok((<IWorkspaceTextEditDto>dto.edits[0]).textEdit);
279
assert.ok((<IWorkspaceTextEditDto>dto.edits[1]).textEdit);
280
});
281
});
282
283
test('event delivery, concurrent change', () => {
284
285
let edits: IWorkspaceEditDto;
286
const participant = new ExtHostDocumentSaveParticipant(nullLogService, documents, new class extends mock<MainThreadTextEditorsShape>() {
287
$tryApplyWorkspaceEdit(_edits: SerializableObjectWithBuffers<IWorkspaceEditDto>) {
288
edits = _edits.value;
289
return Promise.resolve(true);
290
}
291
});
292
293
const sub = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (e) {
294
295
// concurrent change from somewhere
296
documents.$acceptModelChanged(resource, {
297
changes: [{
298
range: { startLineNumber: 1, startColumn: 1, endLineNumber: 1, endColumn: 1 },
299
rangeOffset: undefined!,
300
rangeLength: undefined!,
301
text: 'bar'
302
}],
303
eol: undefined!,
304
versionId: 2,
305
isRedoing: false,
306
isUndoing: false,
307
detailedReason: undefined,
308
isFlush: false,
309
isEolChange: false,
310
}, true);
311
312
e.waitUntil(Promise.resolve([TextEdit.insert(new Position(0, 0), 'bar')]));
313
});
314
315
return participant.$participateInSave(resource, SaveReason.EXPLICIT).then(values => {
316
sub.dispose();
317
318
assert.strictEqual(edits, undefined);
319
assert.strictEqual(values[0], false);
320
});
321
322
});
323
324
test('event delivery, two listeners -> two document states', () => {
325
326
const participant = new ExtHostDocumentSaveParticipant(nullLogService, documents, new class extends mock<MainThreadTextEditorsShape>() {
327
$tryApplyWorkspaceEdit(dto: SerializableObjectWithBuffers<IWorkspaceEditDto>) {
328
329
for (const edit of dto.value.edits) {
330
331
const uri = URI.revive((<IWorkspaceTextEditDto>edit).resource);
332
const { text, range } = (<IWorkspaceTextEditDto>edit).textEdit;
333
documents.$acceptModelChanged(uri, {
334
changes: [{
335
range,
336
text,
337
rangeOffset: undefined!,
338
rangeLength: undefined!,
339
}],
340
eol: undefined!,
341
versionId: documents.getDocumentData(uri)!.version + 1,
342
isRedoing: false,
343
isUndoing: false,
344
detailedReason: undefined,
345
isFlush: false,
346
isEolChange: false,
347
}, true);
348
// }
349
}
350
351
return Promise.resolve(true);
352
}
353
});
354
355
const document = documents.getDocument(resource);
356
357
const sub1 = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (e) {
358
// the document state we started with
359
assert.strictEqual(document.version, 1);
360
assert.strictEqual(document.getText(), 'foo');
361
362
e.waitUntil(Promise.resolve([TextEdit.insert(new Position(0, 0), 'bar')]));
363
});
364
365
const sub2 = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (e) {
366
// the document state AFTER the first listener kicked in
367
assert.strictEqual(document.version, 2);
368
assert.strictEqual(document.getText(), 'barfoo');
369
370
e.waitUntil(Promise.resolve([TextEdit.insert(new Position(0, 0), 'bar')]));
371
});
372
373
return participant.$participateInSave(resource, SaveReason.EXPLICIT).then(values => {
374
sub1.dispose();
375
sub2.dispose();
376
377
// the document state AFTER eventing is done
378
assert.strictEqual(document.version, 3);
379
assert.strictEqual(document.getText(), 'barbarfoo');
380
});
381
382
});
383
384
test('Log failing listener', function () {
385
let didLogSomething = false;
386
const participant = new ExtHostDocumentSaveParticipant(new class extends NullLogService {
387
override error(message: string | Error, ...args: any[]): void {
388
didLogSomething = true;
389
}
390
}, documents, mainThreadBulkEdits);
391
392
393
const sub = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (e) {
394
throw new Error('boom');
395
});
396
397
return participant.$participateInSave(resource, SaveReason.EXPLICIT).then(() => {
398
sub.dispose();
399
assert.strictEqual(didLogSomething, true);
400
});
401
});
402
});
403
404