Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/editor/test/common/services/modelService.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
6
import assert from 'assert';
7
import { CharCode } from '../../../../base/common/charCode.js';
8
import * as platform from '../../../../base/common/platform.js';
9
import { URI } from '../../../../base/common/uri.js';
10
import { EditOperation } from '../../../common/core/editOperation.js';
11
import { Range } from '../../../common/core/range.js';
12
import { Selection } from '../../../common/core/selection.js';
13
import { StringBuilder } from '../../../common/core/stringBuilder.js';
14
import { DefaultEndOfLine, ITextBuffer, ITextBufferFactory, ITextSnapshot } from '../../../common/model.js';
15
import { createTextBuffer } from '../../../common/model/textModel.js';
16
import { ModelService } from '../../../common/services/modelService.js';
17
import { TestConfigurationService } from '../../../../platform/configuration/test/common/testConfigurationService.js';
18
import { createModelServices, createTextModel } from '../testTextModel.js';
19
import { DisposableStore } from '../../../../base/common/lifecycle.js';
20
import { IModelService } from '../../../common/services/model.js';
21
import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
22
import { TestInstantiationService } from '../../../../platform/instantiation/test/common/instantiationServiceMock.js';
23
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js';
24
25
const GENERATE_TESTS = false;
26
27
suite('ModelService', () => {
28
let disposables: DisposableStore;
29
let modelService: IModelService;
30
let instantiationService: TestInstantiationService;
31
32
setup(() => {
33
disposables = new DisposableStore();
34
35
const configService = new TestConfigurationService();
36
configService.setUserConfiguration('files', { 'eol': '\n' });
37
configService.setUserConfiguration('files', { 'eol': '\r\n' }, URI.file(platform.isWindows ? 'c:\\myroot' : '/myroot'));
38
39
instantiationService = createModelServices(disposables, [
40
[IConfigurationService, configService]
41
]);
42
modelService = instantiationService.get(IModelService);
43
});
44
45
teardown(() => {
46
disposables.dispose();
47
});
48
49
ensureNoDisposablesAreLeakedInTestSuite();
50
51
test('EOL setting respected depending on root', () => {
52
const model1 = modelService.createModel('farboo', null);
53
const model2 = modelService.createModel('farboo', null, URI.file(platform.isWindows ? 'c:\\myroot\\myfile.txt' : '/myroot/myfile.txt'));
54
const model3 = modelService.createModel('farboo', null, URI.file(platform.isWindows ? 'c:\\other\\myfile.txt' : '/other/myfile.txt'));
55
56
assert.strictEqual(model1.getOptions().defaultEOL, DefaultEndOfLine.LF);
57
assert.strictEqual(model2.getOptions().defaultEOL, DefaultEndOfLine.CRLF);
58
assert.strictEqual(model3.getOptions().defaultEOL, DefaultEndOfLine.LF);
59
60
model1.dispose();
61
model2.dispose();
62
model3.dispose();
63
});
64
65
test('_computeEdits no change', function () {
66
67
const model = disposables.add(createTextModel(
68
[
69
'This is line one', //16
70
'and this is line number two', //27
71
'it is followed by #3', //20
72
'and finished with the fourth.', //29
73
].join('\n')
74
));
75
76
const textBuffer = createAndRegisterTextBuffer(
77
disposables,
78
[
79
'This is line one', //16
80
'and this is line number two', //27
81
'it is followed by #3', //20
82
'and finished with the fourth.', //29
83
].join('\n'),
84
DefaultEndOfLine.LF
85
);
86
87
const actual = ModelService._computeEdits(model, textBuffer);
88
89
assert.deepStrictEqual(actual, []);
90
});
91
92
test('_computeEdits first line changed', function () {
93
94
const model = disposables.add(createTextModel(
95
[
96
'This is line one', //16
97
'and this is line number two', //27
98
'it is followed by #3', //20
99
'and finished with the fourth.', //29
100
].join('\n')
101
));
102
103
const textBuffer = createAndRegisterTextBuffer(
104
disposables,
105
[
106
'This is line One', //16
107
'and this is line number two', //27
108
'it is followed by #3', //20
109
'and finished with the fourth.', //29
110
].join('\n'),
111
DefaultEndOfLine.LF
112
);
113
114
const actual = ModelService._computeEdits(model, textBuffer);
115
116
assert.deepStrictEqual(actual, [
117
EditOperation.replaceMove(new Range(1, 1, 2, 1), 'This is line One\n')
118
]);
119
});
120
121
test('_computeEdits EOL changed', function () {
122
123
const model = disposables.add(createTextModel(
124
[
125
'This is line one', //16
126
'and this is line number two', //27
127
'it is followed by #3', //20
128
'and finished with the fourth.', //29
129
].join('\n')
130
));
131
132
const textBuffer = createAndRegisterTextBuffer(
133
disposables,
134
[
135
'This is line one', //16
136
'and this is line number two', //27
137
'it is followed by #3', //20
138
'and finished with the fourth.', //29
139
].join('\r\n'),
140
DefaultEndOfLine.LF
141
);
142
143
const actual = ModelService._computeEdits(model, textBuffer);
144
145
assert.deepStrictEqual(actual, []);
146
});
147
148
test('_computeEdits EOL and other change 1', function () {
149
150
const model = disposables.add(createTextModel(
151
[
152
'This is line one', //16
153
'and this is line number two', //27
154
'it is followed by #3', //20
155
'and finished with the fourth.', //29
156
].join('\n')
157
));
158
159
const textBuffer = createAndRegisterTextBuffer(
160
disposables,
161
[
162
'This is line One', //16
163
'and this is line number two', //27
164
'It is followed by #3', //20
165
'and finished with the fourth.', //29
166
].join('\r\n'),
167
DefaultEndOfLine.LF
168
);
169
170
const actual = ModelService._computeEdits(model, textBuffer);
171
172
assert.deepStrictEqual(actual, [
173
EditOperation.replaceMove(
174
new Range(1, 1, 4, 1),
175
[
176
'This is line One',
177
'and this is line number two',
178
'It is followed by #3',
179
''
180
].join('\r\n')
181
)
182
]);
183
});
184
185
test('_computeEdits EOL and other change 2', function () {
186
187
const model = disposables.add(createTextModel(
188
[
189
'package main', // 1
190
'func foo() {', // 2
191
'}' // 3
192
].join('\n')
193
));
194
195
const textBuffer = createAndRegisterTextBuffer(
196
disposables,
197
[
198
'package main', // 1
199
'func foo() {', // 2
200
'}', // 3
201
''
202
].join('\r\n'),
203
DefaultEndOfLine.LF
204
);
205
206
const actual = ModelService._computeEdits(model, textBuffer);
207
208
assert.deepStrictEqual(actual, [
209
EditOperation.replaceMove(new Range(3, 2, 3, 2), '\r\n')
210
]);
211
});
212
213
test('generated1', () => {
214
const file1 = ['pram', 'okctibad', 'pjuwtemued', 'knnnm', 'u', ''];
215
const file2 = ['tcnr', 'rxwlicro', 'vnzy', '', '', 'pjzcogzur', 'ptmxyp', 'dfyshia', 'pee', 'ygg'];
216
assertComputeEdits(file1, file2);
217
});
218
219
test('generated2', () => {
220
const file1 = ['', 'itls', 'hrilyhesv', ''];
221
const file2 = ['vdl', '', 'tchgz', 'bhx', 'nyl'];
222
assertComputeEdits(file1, file2);
223
});
224
225
test('generated3', () => {
226
const file1 = ['ubrbrcv', 'wv', 'xodspybszt', 's', 'wednjxm', 'fklajt', 'fyfc', 'lvejgge', 'rtpjlodmmk', 'arivtgmjdm'];
227
const file2 = ['s', 'qj', 'tu', 'ur', 'qerhjjhyvx', 't'];
228
assertComputeEdits(file1, file2);
229
});
230
231
test('generated4', () => {
232
const file1 = ['ig', 'kh', 'hxegci', 'smvker', 'pkdmjjdqnv', 'vgkkqqx', '', 'jrzeb'];
233
const file2 = ['yk', ''];
234
assertComputeEdits(file1, file2);
235
});
236
237
test('does insertions in the middle of the document', () => {
238
const file1 = [
239
'line 1',
240
'line 2',
241
'line 3'
242
];
243
const file2 = [
244
'line 1',
245
'line 2',
246
'line 5',
247
'line 3'
248
];
249
assertComputeEdits(file1, file2);
250
});
251
252
test('does insertions at the end of the document', () => {
253
const file1 = [
254
'line 1',
255
'line 2',
256
'line 3'
257
];
258
const file2 = [
259
'line 1',
260
'line 2',
261
'line 3',
262
'line 4'
263
];
264
assertComputeEdits(file1, file2);
265
});
266
267
test('does insertions at the beginning of the document', () => {
268
const file1 = [
269
'line 1',
270
'line 2',
271
'line 3'
272
];
273
const file2 = [
274
'line 0',
275
'line 1',
276
'line 2',
277
'line 3'
278
];
279
assertComputeEdits(file1, file2);
280
});
281
282
test('does replacements', () => {
283
const file1 = [
284
'line 1',
285
'line 2',
286
'line 3'
287
];
288
const file2 = [
289
'line 1',
290
'line 7',
291
'line 3'
292
];
293
assertComputeEdits(file1, file2);
294
});
295
296
test('does deletions', () => {
297
const file1 = [
298
'line 1',
299
'line 2',
300
'line 3'
301
];
302
const file2 = [
303
'line 1',
304
'line 3'
305
];
306
assertComputeEdits(file1, file2);
307
});
308
309
test('does insert, replace, and delete', () => {
310
const file1 = [
311
'line 1',
312
'line 2',
313
'line 3',
314
'line 4',
315
'line 5',
316
];
317
const file2 = [
318
'line 0', // insert line 0
319
'line 1',
320
'replace line 2', // replace line 2
321
'line 3',
322
// delete line 4
323
'line 5',
324
];
325
assertComputeEdits(file1, file2);
326
});
327
328
test('maintains undo for same resource and same content', () => {
329
const resource = URI.parse('file://test.txt');
330
331
// create a model
332
const model1 = modelService.createModel('text', null, resource);
333
// make an edit
334
model1.pushEditOperations(null, [{ range: new Range(1, 5, 1, 5), text: '1' }], () => [new Selection(1, 5, 1, 5)]);
335
assert.strictEqual(model1.getValue(), 'text1');
336
// dispose it
337
modelService.destroyModel(resource);
338
339
// create a new model with the same content
340
const model2 = modelService.createModel('text1', null, resource);
341
// undo
342
model2.undo();
343
assert.strictEqual(model2.getValue(), 'text');
344
// dispose it
345
modelService.destroyModel(resource);
346
});
347
348
test('maintains version id and alternative version id for same resource and same content', () => {
349
const resource = URI.parse('file://test.txt');
350
351
// create a model
352
const model1 = modelService.createModel('text', null, resource);
353
// make an edit
354
model1.pushEditOperations(null, [{ range: new Range(1, 5, 1, 5), text: '1' }], () => [new Selection(1, 5, 1, 5)]);
355
assert.strictEqual(model1.getValue(), 'text1');
356
const versionId = model1.getVersionId();
357
const alternativeVersionId = model1.getAlternativeVersionId();
358
// dispose it
359
modelService.destroyModel(resource);
360
361
// create a new model with the same content
362
const model2 = modelService.createModel('text1', null, resource);
363
assert.strictEqual(model2.getVersionId(), versionId);
364
assert.strictEqual(model2.getAlternativeVersionId(), alternativeVersionId);
365
// dispose it
366
modelService.destroyModel(resource);
367
});
368
369
test('does not maintain undo for same resource and different content', () => {
370
const resource = URI.parse('file://test.txt');
371
372
// create a model
373
const model1 = modelService.createModel('text', null, resource);
374
// make an edit
375
model1.pushEditOperations(null, [{ range: new Range(1, 5, 1, 5), text: '1' }], () => [new Selection(1, 5, 1, 5)]);
376
assert.strictEqual(model1.getValue(), 'text1');
377
// dispose it
378
modelService.destroyModel(resource);
379
380
// create a new model with the same content
381
const model2 = modelService.createModel('text2', null, resource);
382
// undo
383
model2.undo();
384
assert.strictEqual(model2.getValue(), 'text2');
385
// dispose it
386
modelService.destroyModel(resource);
387
});
388
389
test('setValue should clear undo stack', () => {
390
const resource = URI.parse('file://test.txt');
391
392
const model = modelService.createModel('text', null, resource);
393
model.pushEditOperations(null, [{ range: new Range(1, 5, 1, 5), text: '1' }], () => [new Selection(1, 5, 1, 5)]);
394
assert.strictEqual(model.getValue(), 'text1');
395
396
model.setValue('text2');
397
model.undo();
398
assert.strictEqual(model.getValue(), 'text2');
399
// dispose it
400
modelService.destroyModel(resource);
401
});
402
});
403
404
function assertComputeEdits(lines1: string[], lines2: string[]): void {
405
const model = createTextModel(lines1.join('\n'));
406
const { disposable, textBuffer } = createTextBuffer(lines2.join('\n'), DefaultEndOfLine.LF);
407
408
// compute required edits
409
// let start = Date.now();
410
const edits = ModelService._computeEdits(model, textBuffer);
411
// console.log(`took ${Date.now() - start} ms.`);
412
413
// apply edits
414
model.pushEditOperations([], edits, null);
415
416
assert.strictEqual(model.getValue(), lines2.join('\n'));
417
disposable.dispose();
418
model.dispose();
419
}
420
421
function getRandomInt(min: number, max: number): number {
422
return Math.floor(Math.random() * (max - min + 1)) + min;
423
}
424
425
function getRandomString(minLength: number, maxLength: number): string {
426
const length = getRandomInt(minLength, maxLength);
427
const t = new StringBuilder(length);
428
for (let i = 0; i < length; i++) {
429
t.appendASCIICharCode(getRandomInt(CharCode.a, CharCode.z));
430
}
431
return t.build();
432
}
433
434
function generateFile(small: boolean): string[] {
435
const lineCount = getRandomInt(1, small ? 3 : 10000);
436
const lines: string[] = [];
437
for (let i = 0; i < lineCount; i++) {
438
lines.push(getRandomString(0, small ? 3 : 10000));
439
}
440
return lines;
441
}
442
443
if (GENERATE_TESTS) {
444
let number = 1;
445
while (true) {
446
447
console.log('------TEST: ' + number++);
448
449
const file1 = generateFile(true);
450
const file2 = generateFile(true);
451
452
console.log('------TEST GENERATED');
453
454
try {
455
assertComputeEdits(file1, file2);
456
} catch (err) {
457
console.log(err);
458
console.log(`
459
const file1 = ${JSON.stringify(file1).replace(/"/g, '\'')};
460
const file2 = ${JSON.stringify(file2).replace(/"/g, '\'')};
461
assertComputeEdits(file1, file2);
462
`);
463
break;
464
}
465
}
466
}
467
468
function createAndRegisterTextBuffer(store: DisposableStore, value: string | ITextBufferFactory | ITextSnapshot, defaultEOL: DefaultEndOfLine): ITextBuffer {
469
const { disposable, textBuffer } = createTextBuffer(value, defaultEOL);
470
store.add(disposable);
471
return textBuffer;
472
}
473
474