Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/api/test/browser/mainThreadEditors.test.ts
5240 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
6
import assert from 'assert';
7
import { Event } from '../../../../base/common/event.js';
8
import { DisposableStore, IReference, ImmortalReference } from '../../../../base/common/lifecycle.js';
9
import { URI } from '../../../../base/common/uri.js';
10
import { mock } from '../../../../base/test/common/mock.js';
11
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js';
12
import { IBulkEditService } from '../../../../editor/browser/services/bulkEditService.js';
13
import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js';
14
import { EditOperation, ISingleEditOperation } from '../../../../editor/common/core/editOperation.js';
15
import { Position } from '../../../../editor/common/core/position.js';
16
import { Range } from '../../../../editor/common/core/range.js';
17
import { ILanguageService } from '../../../../editor/common/languages/language.js';
18
import { ILanguageConfigurationService } from '../../../../editor/common/languages/languageConfigurationRegistry.js';
19
import { EndOfLineSequence, ITextSnapshot } from '../../../../editor/common/model.js';
20
import { IEditorWorkerService } from '../../../../editor/common/services/editorWorker.js';
21
import { LanguageService } from '../../../../editor/common/services/languageService.js';
22
import { IModelService } from '../../../../editor/common/services/model.js';
23
import { ModelService } from '../../../../editor/common/services/modelService.js';
24
import { IResolvedTextEditorModel, ITextModelService } from '../../../../editor/common/services/resolverService.js';
25
import { ITreeSitterLibraryService } from '../../../../editor/common/services/treeSitter/treeSitterLibraryService.js';
26
import { TestCodeEditorService } from '../../../../editor/test/browser/editorTestServices.js';
27
import { TestLanguageConfigurationService } from '../../../../editor/test/common/modes/testLanguageConfigurationService.js';
28
import { TestTreeSitterLibraryService } from '../../../../editor/test/common/services/testTreeSitterLibraryService.js';
29
import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
30
import { TestConfigurationService } from '../../../../platform/configuration/test/common/testConfigurationService.js';
31
import { IDialogService } from '../../../../platform/dialogs/common/dialogs.js';
32
import { TestDialogService } from '../../../../platform/dialogs/test/common/testDialogService.js';
33
import { IEnvironmentService } from '../../../../platform/environment/common/environment.js';
34
import { IFileService } from '../../../../platform/files/common/files.js';
35
import { SyncDescriptor } from '../../../../platform/instantiation/common/descriptors.js';
36
import { InstantiationService } from '../../../../platform/instantiation/common/instantiationService.js';
37
import { ServiceCollection } from '../../../../platform/instantiation/common/serviceCollection.js';
38
import { ILabelService } from '../../../../platform/label/common/label.js';
39
import { ILogService, NullLogService } from '../../../../platform/log/common/log.js';
40
import { INotificationService } from '../../../../platform/notification/common/notification.js';
41
import { TestNotificationService } from '../../../../platform/notification/test/common/testNotificationService.js';
42
import { TestThemeService } from '../../../../platform/theme/test/common/testThemeService.js';
43
import { IUndoRedoService } from '../../../../platform/undoRedo/common/undoRedo.js';
44
import { UndoRedoService } from '../../../../platform/undoRedo/common/undoRedoService.js';
45
import { IUriIdentityService } from '../../../../platform/uriIdentity/common/uriIdentity.js';
46
import { UriIdentityService } from '../../../../platform/uriIdentity/common/uriIdentityService.js';
47
import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js';
48
import { BulkEditService } from '../../../contrib/bulkEdit/browser/bulkEditService.js';
49
import { IEditorGroupsService } from '../../../services/editor/common/editorGroupsService.js';
50
import { IEditorService } from '../../../services/editor/common/editorService.js';
51
import { IWorkbenchEnvironmentService } from '../../../services/environment/common/environmentService.js';
52
import { SerializableObjectWithBuffers } from '../../../services/extensions/common/proxyIdentifier.js';
53
import { LabelService } from '../../../services/label/common/labelService.js';
54
import { ILifecycleService } from '../../../services/lifecycle/common/lifecycle.js';
55
import { IPaneCompositePartService } from '../../../services/panecomposite/browser/panecomposite.js';
56
import { ITextFileService } from '../../../services/textfile/common/textfiles.js';
57
import { ICopyOperation, ICreateFileOperation, ICreateOperation, IDeleteOperation, IMoveOperation, IWorkingCopyFileService } from '../../../services/workingCopy/common/workingCopyFileService.js';
58
import { IWorkingCopyService } from '../../../services/workingCopy/common/workingCopyService.js';
59
import { TestEditorGroupsService, TestEditorService, TestEnvironmentService, TestLifecycleService, TestWorkingCopyService } from '../../../test/browser/workbenchTestServices.js';
60
import { TestContextService, TestFileService, TestTextResourcePropertiesService } from '../../../test/common/workbenchTestServices.js';
61
import { MainThreadBulkEdits } from '../../browser/mainThreadBulkEdits.js';
62
import { MainThreadTextEditors, IMainThreadEditorLocator } from '../../browser/mainThreadEditors.js';
63
import { MainThreadTextEditor } from '../../browser/mainThreadEditor.js';
64
import { MainThreadDocuments } from '../../browser/mainThreadDocuments.js';
65
import { IWorkspaceTextEditDto } from '../../common/extHost.protocol.js';
66
import { SingleProxyRPCProtocol } from '../common/testRPCProtocol.js';
67
import { ITextResourcePropertiesService } from '../../../../editor/common/services/textResourceConfiguration.js';
68
import { IClipboardService } from '../../../../platform/clipboard/common/clipboardService.js';
69
import { TestClipboardService } from '../../../../platform/clipboard/test/common/testClipboardService.js';
70
import { createTestCodeEditor } from '../../../../editor/test/browser/testCodeEditor.js';
71
72
suite('MainThreadEditors', () => {
73
74
let disposables: DisposableStore;
75
const existingResource = URI.parse('foo:existing');
76
const resource = URI.parse('foo:bar');
77
78
let modelService: IModelService;
79
80
let bulkEdits: MainThreadBulkEdits;
81
let editors: MainThreadTextEditors;
82
let editorLocator: IMainThreadEditorLocator;
83
let testEditor: MainThreadTextEditor;
84
85
const movedResources = new Map<URI, URI>();
86
const copiedResources = new Map<URI, URI>();
87
const createdResources = new Set<URI>();
88
const deletedResources = new Set<URI>();
89
90
const editorId = 'testEditorId';
91
92
setup(() => {
93
disposables = new DisposableStore();
94
95
movedResources.clear();
96
copiedResources.clear();
97
createdResources.clear();
98
deletedResources.clear();
99
100
const configService = new TestConfigurationService();
101
const dialogService = new TestDialogService();
102
const notificationService = new TestNotificationService();
103
const undoRedoService = new UndoRedoService(dialogService, notificationService);
104
const themeService = new TestThemeService();
105
106
const services = new ServiceCollection();
107
services.set(IBulkEditService, new SyncDescriptor(BulkEditService));
108
services.set(ILabelService, new SyncDescriptor(LabelService));
109
services.set(ILogService, new NullLogService());
110
services.set(IWorkspaceContextService, new TestContextService());
111
services.set(IEnvironmentService, TestEnvironmentService);
112
services.set(IWorkbenchEnvironmentService, TestEnvironmentService);
113
services.set(IConfigurationService, configService);
114
services.set(IDialogService, dialogService);
115
services.set(INotificationService, notificationService);
116
services.set(IUndoRedoService, undoRedoService);
117
services.set(ITextResourcePropertiesService, new SyncDescriptor(TestTextResourcePropertiesService));
118
services.set(IModelService, new SyncDescriptor(ModelService));
119
services.set(ICodeEditorService, new TestCodeEditorService(themeService));
120
services.set(IFileService, new TestFileService());
121
services.set(IUriIdentityService, new SyncDescriptor(UriIdentityService));
122
services.set(ITreeSitterLibraryService, new TestTreeSitterLibraryService());
123
services.set(IEditorService, disposables.add(new TestEditorService()));
124
services.set(ILifecycleService, new TestLifecycleService());
125
services.set(IWorkingCopyService, new TestWorkingCopyService());
126
services.set(IEditorGroupsService, new TestEditorGroupsService());
127
services.set(IClipboardService, new TestClipboardService());
128
services.set(ITextFileService, new class extends mock<ITextFileService>() {
129
override isDirty() { return false; }
130
// eslint-disable-next-line local/code-no-any-casts
131
override files = <any>{
132
onDidSave: Event.None,
133
onDidRevert: Event.None,
134
onDidChangeDirty: Event.None,
135
onDidChangeEncoding: Event.None
136
};
137
// eslint-disable-next-line local/code-no-any-casts
138
override untitled = <any>{
139
onDidChangeEncoding: Event.None
140
};
141
override create(operations: { resource: URI }[]) {
142
for (const o of operations) {
143
createdResources.add(o.resource);
144
}
145
return Promise.resolve(Object.create(null));
146
}
147
override async getEncodedReadable(resource: URI, value?: string | ITextSnapshot): Promise<any> {
148
return undefined;
149
}
150
});
151
services.set(IWorkingCopyFileService, new class extends mock<IWorkingCopyFileService>() {
152
override onDidRunWorkingCopyFileOperation = Event.None;
153
override createFolder(operations: ICreateOperation[]): any {
154
this.create(operations);
155
}
156
override create(operations: ICreateFileOperation[]) {
157
for (const operation of operations) {
158
createdResources.add(operation.resource);
159
}
160
return Promise.resolve(Object.create(null));
161
}
162
override move(operations: IMoveOperation[]) {
163
const { source, target } = operations[0].file;
164
movedResources.set(source, target);
165
return Promise.resolve(Object.create(null));
166
}
167
override copy(operations: ICopyOperation[]) {
168
const { source, target } = operations[0].file;
169
copiedResources.set(source, target);
170
return Promise.resolve(Object.create(null));
171
}
172
override delete(operations: IDeleteOperation[]) {
173
for (const operation of operations) {
174
deletedResources.add(operation.resource);
175
}
176
return Promise.resolve(undefined);
177
}
178
});
179
services.set(ITextModelService, new class extends mock<ITextModelService>() {
180
override createModelReference(resource: URI): Promise<IReference<IResolvedTextEditorModel>> {
181
const textEditorModel = new class extends mock<IResolvedTextEditorModel>() {
182
override textEditorModel = modelService.getModel(resource)!;
183
};
184
textEditorModel.isReadonly = () => false;
185
return Promise.resolve(new ImmortalReference(textEditorModel));
186
}
187
});
188
services.set(IEditorWorkerService, new class extends mock<IEditorWorkerService>() {
189
190
});
191
services.set(IPaneCompositePartService, new class extends mock<IPaneCompositePartService>() implements IPaneCompositePartService {
192
override onDidPaneCompositeOpen = Event.None;
193
override onDidPaneCompositeClose = Event.None;
194
override getActivePaneComposite() {
195
return undefined;
196
}
197
});
198
199
services.set(ILanguageService, disposables.add(new LanguageService()));
200
services.set(ILanguageConfigurationService, new TestLanguageConfigurationService());
201
202
const instaService = new InstantiationService(services);
203
204
bulkEdits = instaService.createInstance(MainThreadBulkEdits, SingleProxyRPCProtocol(null));
205
const documents = instaService.createInstance(MainThreadDocuments, SingleProxyRPCProtocol(null));
206
207
// Create editor locator
208
editorLocator = {
209
getEditor(id: string): MainThreadTextEditor | undefined {
210
return id === editorId ? testEditor : undefined;
211
},
212
findTextEditorIdFor() { return undefined; },
213
getIdOfCodeEditor() { return undefined; }
214
};
215
216
editors = instaService.createInstance(MainThreadTextEditors, editorLocator, SingleProxyRPCProtocol(null));
217
modelService = instaService.invokeFunction(accessor => accessor.get(IModelService));
218
219
// Create a test code editor using the helper
220
const model = modelService.createModel('Hello world!', null, existingResource);
221
const testCodeEditor = disposables.add(createTestCodeEditor(model));
222
223
testEditor = disposables.add(instaService.createInstance(
224
MainThreadTextEditor,
225
editorId,
226
model,
227
testCodeEditor,
228
{ onGainedFocus() { }, onLostFocus() { } },
229
documents
230
));
231
});
232
233
teardown(() => {
234
disposables.dispose();
235
});
236
237
ensureNoDisposablesAreLeakedInTestSuite();
238
239
test(`applyWorkspaceEdit returns false if model is changed by user`, () => {
240
241
const model = disposables.add(modelService.createModel('something', null, resource));
242
243
const workspaceResourceEdit: IWorkspaceTextEditDto = {
244
resource: resource,
245
versionId: model.getVersionId(),
246
textEdit: {
247
text: 'asdfg',
248
range: new Range(1, 1, 1, 1)
249
}
250
};
251
252
// Act as if the user edited the model
253
model.applyEdits([EditOperation.insert(new Position(0, 0), 'something')]);
254
255
return bulkEdits.$tryApplyWorkspaceEdit(new SerializableObjectWithBuffers({ edits: [workspaceResourceEdit] })).then((result) => {
256
assert.strictEqual(result, false);
257
});
258
});
259
260
test(`issue #54773: applyWorkspaceEdit checks model version in race situation`, () => {
261
262
const model = disposables.add(modelService.createModel('something', null, resource));
263
264
const workspaceResourceEdit1: IWorkspaceTextEditDto = {
265
resource: resource,
266
versionId: model.getVersionId(),
267
textEdit: {
268
text: 'asdfg',
269
range: new Range(1, 1, 1, 1)
270
}
271
};
272
const workspaceResourceEdit2: IWorkspaceTextEditDto = {
273
resource: resource,
274
versionId: model.getVersionId(),
275
textEdit: {
276
text: 'asdfg',
277
range: new Range(1, 1, 1, 1)
278
}
279
};
280
281
const p1 = bulkEdits.$tryApplyWorkspaceEdit(new SerializableObjectWithBuffers({ edits: [workspaceResourceEdit1] })).then((result) => {
282
// first edit request succeeds
283
assert.strictEqual(result, true);
284
});
285
const p2 = bulkEdits.$tryApplyWorkspaceEdit(new SerializableObjectWithBuffers({ edits: [workspaceResourceEdit2] })).then((result) => {
286
// second edit request fails
287
assert.strictEqual(result, false);
288
});
289
return Promise.all([p1, p2]);
290
});
291
292
test('applyWorkspaceEdit: noop eol edit keeps undo stack clean', async () => {
293
294
const initialText = 'hello\nworld';
295
const model = disposables.add(modelService.createModel(initialText, null, resource));
296
const initialAlternativeVersionId = model.getAlternativeVersionId();
297
298
const insertEdit: IWorkspaceTextEditDto = {
299
resource: resource,
300
versionId: model.getVersionId(),
301
textEdit: {
302
range: new Range(1, 6, 1, 6),
303
text: '2'
304
}
305
};
306
307
const insertResult = await bulkEdits.$tryApplyWorkspaceEdit(new SerializableObjectWithBuffers({ edits: [insertEdit] }));
308
assert.strictEqual(insertResult, true);
309
assert.strictEqual(model.getValue(), 'hello2\nworld');
310
assert.notStrictEqual(model.getAlternativeVersionId(), initialAlternativeVersionId);
311
312
const eolEdit: IWorkspaceTextEditDto = {
313
resource: resource,
314
versionId: model.getVersionId(),
315
textEdit: {
316
range: new Range(1, 1, 1, 1),
317
text: '',
318
eol: EndOfLineSequence.LF
319
}
320
};
321
322
const eolResult = await bulkEdits.$tryApplyWorkspaceEdit(new SerializableObjectWithBuffers({ edits: [eolEdit] }));
323
assert.strictEqual(eolResult, true);
324
assert.strictEqual(model.getValue(), 'hello2\nworld');
325
326
const undoResult = model.undo();
327
if (undoResult) {
328
await undoResult;
329
}
330
assert.strictEqual(model.getValue(), initialText);
331
assert.strictEqual(model.getAlternativeVersionId(), initialAlternativeVersionId);
332
});
333
334
test(`applyWorkspaceEdit with only resource edit`, () => {
335
return bulkEdits.$tryApplyWorkspaceEdit(new SerializableObjectWithBuffers({
336
edits: [
337
{ oldResource: resource, newResource: resource, options: undefined },
338
{ oldResource: undefined, newResource: resource, options: undefined },
339
{ oldResource: resource, newResource: undefined, options: undefined }
340
]
341
})).then((result) => {
342
assert.strictEqual(result, true);
343
assert.strictEqual(movedResources.get(resource), resource);
344
assert.strictEqual(createdResources.has(resource), true);
345
assert.strictEqual(deletedResources.has(resource), true);
346
});
347
});
348
349
test('applyWorkspaceEdit can control undo/redo stack 1', async () => {
350
const model = modelService.getModel(existingResource)!;
351
352
const edit1: ISingleEditOperation = {
353
range: new Range(1, 1, 1, 2),
354
text: 'h',
355
forceMoveMarkers: false
356
};
357
358
const applied1 = await editors.$tryApplyEdits(editorId, model.getVersionId(), [edit1], { undoStopBefore: false, undoStopAfter: false });
359
assert.strictEqual(applied1, true);
360
assert.strictEqual(model.getValue(), 'hello world!');
361
362
const edit2: ISingleEditOperation = {
363
range: new Range(1, 2, 1, 6),
364
text: 'ELLO',
365
forceMoveMarkers: false
366
};
367
368
const applied2 = await editors.$tryApplyEdits(editorId, model.getVersionId(), [edit2], { undoStopBefore: false, undoStopAfter: false });
369
assert.strictEqual(applied2, true);
370
assert.strictEqual(model.getValue(), 'hELLO world!');
371
372
await model.undo();
373
assert.strictEqual(model.getValue(), 'Hello world!');
374
});
375
376
test('applyWorkspaceEdit can control undo/redo stack 2', async () => {
377
const model = modelService.getModel(existingResource)!;
378
379
const edit1: ISingleEditOperation = {
380
range: new Range(1, 1, 1, 2),
381
text: 'h',
382
forceMoveMarkers: false
383
};
384
385
const applied1 = await editors.$tryApplyEdits(editorId, model.getVersionId(), [edit1], { undoStopBefore: false, undoStopAfter: false });
386
assert.strictEqual(applied1, true);
387
assert.strictEqual(model.getValue(), 'hello world!');
388
389
const edit2: ISingleEditOperation = {
390
range: new Range(1, 2, 1, 6),
391
text: 'ELLO',
392
forceMoveMarkers: false
393
};
394
395
const applied2 = await editors.$tryApplyEdits(editorId, model.getVersionId(), [edit2], { undoStopBefore: true, undoStopAfter: false });
396
assert.strictEqual(applied2, true);
397
assert.strictEqual(model.getValue(), 'hELLO world!');
398
399
await model.undo();
400
assert.strictEqual(model.getValue(), 'hello world!');
401
});
402
});
403
404