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
3296 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
assert.throws(() => { (event.document as any) = null!; });
85
});
86
});
87
88
test('event delivery, bad listener', () => {
89
const participant = new ExtHostDocumentSaveParticipant(nullLogService, documents, mainThreadBulkEdits);
90
91
const sub = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (e) {
92
throw new Error('💀');
93
});
94
95
return participant.$participateInSave(resource, SaveReason.EXPLICIT).then(values => {
96
sub.dispose();
97
98
const [first] = values;
99
assert.strictEqual(first, false);
100
});
101
});
102
103
test('event delivery, bad listener doesn\'t prevent more events', () => {
104
const participant = new ExtHostDocumentSaveParticipant(nullLogService, documents, mainThreadBulkEdits);
105
106
const sub1 = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (e) {
107
throw new Error('💀');
108
});
109
let event: vscode.TextDocumentWillSaveEvent;
110
const sub2 = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (e) {
111
event = e;
112
});
113
114
return participant.$participateInSave(resource, SaveReason.EXPLICIT).then(() => {
115
sub1.dispose();
116
sub2.dispose();
117
118
assert.ok(event);
119
});
120
});
121
122
test('event delivery, in subscriber order', () => {
123
const participant = new ExtHostDocumentSaveParticipant(nullLogService, documents, mainThreadBulkEdits);
124
125
let counter = 0;
126
const sub1 = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (event) {
127
assert.strictEqual(counter++, 0);
128
});
129
130
const sub2 = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (event) {
131
assert.strictEqual(counter++, 1);
132
});
133
134
return participant.$participateInSave(resource, SaveReason.EXPLICIT).then(() => {
135
sub1.dispose();
136
sub2.dispose();
137
});
138
});
139
140
test('event delivery, ignore bad listeners', async () => {
141
const participant = new ExtHostDocumentSaveParticipant(nullLogService, documents, mainThreadBulkEdits, { timeout: 5, errors: 1 });
142
143
let callCount = 0;
144
const sub = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (event) {
145
callCount += 1;
146
throw new Error('boom');
147
});
148
149
await participant.$participateInSave(resource, SaveReason.EXPLICIT);
150
await participant.$participateInSave(resource, SaveReason.EXPLICIT);
151
await participant.$participateInSave(resource, SaveReason.EXPLICIT);
152
await participant.$participateInSave(resource, SaveReason.EXPLICIT);
153
154
sub.dispose();
155
assert.strictEqual(callCount, 2);
156
});
157
158
test('event delivery, overall timeout', async function () {
159
const participant = new ExtHostDocumentSaveParticipant(nullLogService, documents, mainThreadBulkEdits, { timeout: 20, errors: 5 });
160
161
// let callCount = 0;
162
const calls: number[] = [];
163
const sub1 = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (event) {
164
calls.push(1);
165
});
166
167
const sub2 = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (event) {
168
calls.push(2);
169
event.waitUntil(timeout(100));
170
});
171
172
const sub3 = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (event) {
173
calls.push(3);
174
});
175
176
const values = await participant.$participateInSave(resource, SaveReason.EXPLICIT);
177
sub1.dispose();
178
sub2.dispose();
179
sub3.dispose();
180
assert.deepStrictEqual(calls, [1, 2]);
181
assert.strictEqual(values.length, 2);
182
});
183
184
test('event delivery, waitUntil', () => {
185
const participant = new ExtHostDocumentSaveParticipant(nullLogService, documents, mainThreadBulkEdits);
186
187
const sub = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (event) {
188
189
event.waitUntil(timeout(10));
190
event.waitUntil(timeout(10));
191
event.waitUntil(timeout(10));
192
});
193
194
return participant.$participateInSave(resource, SaveReason.EXPLICIT).then(() => {
195
sub.dispose();
196
});
197
198
});
199
200
test('event delivery, waitUntil must be called sync', () => {
201
const participant = new ExtHostDocumentSaveParticipant(nullLogService, documents, mainThreadBulkEdits);
202
203
const sub = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (event) {
204
205
event.waitUntil(new Promise<undefined>((resolve, reject) => {
206
setTimeout(() => {
207
try {
208
assert.throws(() => event.waitUntil(timeout(10)));
209
resolve(undefined);
210
} catch (e) {
211
reject(e);
212
}
213
214
}, 10);
215
}));
216
});
217
218
return participant.$participateInSave(resource, SaveReason.EXPLICIT).then(() => {
219
sub.dispose();
220
});
221
});
222
223
test('event delivery, waitUntil will timeout', function () {
224
225
const participant = new ExtHostDocumentSaveParticipant(nullLogService, documents, mainThreadBulkEdits, { timeout: 5, errors: 3 });
226
227
const sub = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (event) {
228
event.waitUntil(timeout(100));
229
});
230
231
return participant.$participateInSave(resource, SaveReason.EXPLICIT).then(values => {
232
sub.dispose();
233
234
const [first] = values;
235
assert.strictEqual(first, false);
236
});
237
});
238
239
test('event delivery, waitUntil failure handling', () => {
240
const participant = new ExtHostDocumentSaveParticipant(nullLogService, documents, mainThreadBulkEdits);
241
242
const sub1 = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (e) {
243
e.waitUntil(Promise.reject(new Error('dddd')));
244
});
245
246
let event: vscode.TextDocumentWillSaveEvent;
247
const sub2 = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (e) {
248
event = e;
249
});
250
251
return participant.$participateInSave(resource, SaveReason.EXPLICIT).then(() => {
252
assert.ok(event);
253
sub1.dispose();
254
sub2.dispose();
255
});
256
});
257
258
test('event delivery, pushEdits sync', () => {
259
260
let dto: IWorkspaceEditDto;
261
const participant = new ExtHostDocumentSaveParticipant(nullLogService, documents, new class extends mock<MainThreadTextEditorsShape>() {
262
$tryApplyWorkspaceEdit(_edits: SerializableObjectWithBuffers<IWorkspaceEditDto>) {
263
dto = _edits.value;
264
return Promise.resolve(true);
265
}
266
});
267
268
const sub = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (e) {
269
e.waitUntil(Promise.resolve([TextEdit.insert(new Position(0, 0), 'bar')]));
270
e.waitUntil(Promise.resolve([TextEdit.setEndOfLine(EndOfLine.CRLF)]));
271
});
272
273
return participant.$participateInSave(resource, SaveReason.EXPLICIT).then(() => {
274
sub.dispose();
275
276
assert.strictEqual(dto.edits.length, 2);
277
assert.ok((<IWorkspaceTextEditDto>dto.edits[0]).textEdit);
278
assert.ok((<IWorkspaceTextEditDto>dto.edits[1]).textEdit);
279
});
280
});
281
282
test('event delivery, concurrent change', () => {
283
284
let edits: IWorkspaceEditDto;
285
const participant = new ExtHostDocumentSaveParticipant(nullLogService, documents, new class extends mock<MainThreadTextEditorsShape>() {
286
$tryApplyWorkspaceEdit(_edits: SerializableObjectWithBuffers<IWorkspaceEditDto>) {
287
edits = _edits.value;
288
return Promise.resolve(true);
289
}
290
});
291
292
const sub = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (e) {
293
294
// concurrent change from somewhere
295
documents.$acceptModelChanged(resource, {
296
changes: [{
297
range: { startLineNumber: 1, startColumn: 1, endLineNumber: 1, endColumn: 1 },
298
rangeOffset: undefined!,
299
rangeLength: undefined!,
300
text: 'bar'
301
}],
302
eol: undefined!,
303
versionId: 2,
304
isRedoing: false,
305
isUndoing: false,
306
detailedReason: undefined,
307
isFlush: false,
308
isEolChange: false,
309
}, 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
sub.dispose();
316
317
assert.strictEqual(edits, undefined);
318
assert.strictEqual(values[0], false);
319
});
320
321
});
322
323
test('event delivery, two listeners -> two document states', () => {
324
325
const participant = new ExtHostDocumentSaveParticipant(nullLogService, documents, new class extends mock<MainThreadTextEditorsShape>() {
326
$tryApplyWorkspaceEdit(dto: SerializableObjectWithBuffers<IWorkspaceEditDto>) {
327
328
for (const edit of dto.value.edits) {
329
330
const uri = URI.revive((<IWorkspaceTextEditDto>edit).resource);
331
const { text, range } = (<IWorkspaceTextEditDto>edit).textEdit;
332
documents.$acceptModelChanged(uri, {
333
changes: [{
334
range,
335
text,
336
rangeOffset: undefined!,
337
rangeLength: undefined!,
338
}],
339
eol: undefined!,
340
versionId: documents.getDocumentData(uri)!.version + 1,
341
isRedoing: false,
342
isUndoing: false,
343
detailedReason: undefined,
344
isFlush: false,
345
isEolChange: false,
346
}, true);
347
// }
348
}
349
350
return Promise.resolve(true);
351
}
352
});
353
354
const document = documents.getDocument(resource);
355
356
const sub1 = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (e) {
357
// the document state we started with
358
assert.strictEqual(document.version, 1);
359
assert.strictEqual(document.getText(), 'foo');
360
361
e.waitUntil(Promise.resolve([TextEdit.insert(new Position(0, 0), 'bar')]));
362
});
363
364
const sub2 = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (e) {
365
// the document state AFTER the first listener kicked in
366
assert.strictEqual(document.version, 2);
367
assert.strictEqual(document.getText(), 'barfoo');
368
369
e.waitUntil(Promise.resolve([TextEdit.insert(new Position(0, 0), 'bar')]));
370
});
371
372
return participant.$participateInSave(resource, SaveReason.EXPLICIT).then(values => {
373
sub1.dispose();
374
sub2.dispose();
375
376
// the document state AFTER eventing is done
377
assert.strictEqual(document.version, 3);
378
assert.strictEqual(document.getText(), 'barbarfoo');
379
});
380
381
});
382
383
test('Log failing listener', function () {
384
let didLogSomething = false;
385
const participant = new ExtHostDocumentSaveParticipant(new class extends NullLogService {
386
override error(message: string | Error, ...args: any[]): void {
387
didLogSomething = true;
388
}
389
}, documents, mainThreadBulkEdits);
390
391
392
const sub = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (e) {
393
throw new Error('boom');
394
});
395
396
return participant.$participateInSave(resource, SaveReason.EXPLICIT).then(() => {
397
sub.dispose();
398
assert.strictEqual(didLogSomething, true);
399
});
400
});
401
});
402
403