Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/editor/test/common/model/model.test.ts
5242 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 { Disposable, DisposableStore, dispose } from '../../../../base/common/lifecycle.js';
8
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js';
9
import { EditOperation } from '../../../common/core/editOperation.js';
10
import { Position } from '../../../common/core/position.js';
11
import { Range } from '../../../common/core/range.js';
12
import { MetadataConsts } from '../../../common/encodedTokenAttributes.js';
13
import { EncodedTokenizationResult, IState, TokenizationRegistry } from '../../../common/languages.js';
14
import { ILanguageService } from '../../../common/languages/language.js';
15
import { ILanguageConfigurationService } from '../../../common/languages/languageConfigurationRegistry.js';
16
import { NullState } from '../../../common/languages/nullTokenize.js';
17
import { TextModel } from '../../../common/model/textModel.js';
18
import { InternalModelContentChangeEvent, ModelInjectedTextChangedEvent, ModelRawContentChangedEvent, ModelRawFlush, ModelRawLineChanged, ModelRawLinesDeleted, ModelRawLinesInserted } from '../../../common/textModelEvents.js';
19
import { createModelServices, createTextModel, instantiateTextModel } from '../testTextModel.js';
20
import { mock } from '../../../../base/test/common/mock.js';
21
import { IViewModel } from '../../../common/viewModel.js';
22
23
// --------- utils
24
25
const LINE1 = 'My First Line';
26
const LINE2 = '\t\tMy Second Line';
27
const LINE3 = ' Third Line';
28
const LINE4 = '';
29
const LINE5 = '1';
30
31
suite('Editor Model - Model', () => {
32
33
let thisModel: TextModel;
34
35
setup(() => {
36
const text =
37
LINE1 + '\r\n' +
38
LINE2 + '\n' +
39
LINE3 + '\n' +
40
LINE4 + '\r\n' +
41
LINE5;
42
thisModel = createTextModel(text);
43
});
44
45
teardown(() => {
46
thisModel.dispose();
47
});
48
49
ensureNoDisposablesAreLeakedInTestSuite();
50
51
// --------- insert text
52
53
test('model getValue', () => {
54
assert.strictEqual(thisModel.getValue(), 'My First Line\n\t\tMy Second Line\n Third Line\n\n1');
55
});
56
57
test('model insert empty text', () => {
58
thisModel.applyEdits([EditOperation.insert(new Position(1, 1), '')]);
59
assert.strictEqual(thisModel.getLineCount(), 5);
60
assert.strictEqual(thisModel.getLineContent(1), 'My First Line');
61
});
62
63
test('model insert text without newline 1', () => {
64
thisModel.applyEdits([EditOperation.insert(new Position(1, 1), 'foo ')]);
65
assert.strictEqual(thisModel.getLineCount(), 5);
66
assert.strictEqual(thisModel.getLineContent(1), 'foo My First Line');
67
});
68
69
test('model insert text without newline 2', () => {
70
thisModel.applyEdits([EditOperation.insert(new Position(1, 3), ' foo')]);
71
assert.strictEqual(thisModel.getLineCount(), 5);
72
assert.strictEqual(thisModel.getLineContent(1), 'My foo First Line');
73
});
74
75
test('model insert text with one newline', () => {
76
thisModel.applyEdits([EditOperation.insert(new Position(1, 3), ' new line\nNo longer')]);
77
assert.strictEqual(thisModel.getLineCount(), 6);
78
assert.strictEqual(thisModel.getLineContent(1), 'My new line');
79
assert.strictEqual(thisModel.getLineContent(2), 'No longer First Line');
80
});
81
82
test('model insert text with two newlines', () => {
83
thisModel.applyEdits([EditOperation.insert(new Position(1, 3), ' new line\nOne more line in the middle\nNo longer')]);
84
assert.strictEqual(thisModel.getLineCount(), 7);
85
assert.strictEqual(thisModel.getLineContent(1), 'My new line');
86
assert.strictEqual(thisModel.getLineContent(2), 'One more line in the middle');
87
assert.strictEqual(thisModel.getLineContent(3), 'No longer First Line');
88
});
89
90
test('model insert text with many newlines', () => {
91
thisModel.applyEdits([EditOperation.insert(new Position(1, 3), '\n\n\n\n')]);
92
assert.strictEqual(thisModel.getLineCount(), 9);
93
assert.strictEqual(thisModel.getLineContent(1), 'My');
94
assert.strictEqual(thisModel.getLineContent(2), '');
95
assert.strictEqual(thisModel.getLineContent(3), '');
96
assert.strictEqual(thisModel.getLineContent(4), '');
97
assert.strictEqual(thisModel.getLineContent(5), ' First Line');
98
});
99
100
101
// --------- insert text eventing
102
103
function withEventCapturing(callback: () => void): ModelRawContentChangedEvent | null {
104
let e: ModelRawContentChangedEvent | null = null;
105
const spyViewModel = new class extends mock<IViewModel>() {
106
override onDidChangeContentOrInjectedText(_e: InternalModelContentChangeEvent | ModelInjectedTextChangedEvent) {
107
if (e !== null || !(_e instanceof InternalModelContentChangeEvent)) {
108
assert.fail('Unexpected assertion error');
109
}
110
e = _e.rawContentChangedEvent;
111
}
112
override emitContentChangeEvent(e: InternalModelContentChangeEvent | ModelInjectedTextChangedEvent): void { }
113
};
114
thisModel.registerViewModel(spyViewModel);
115
callback();
116
thisModel.unregisterViewModel(spyViewModel);
117
return e;
118
}
119
120
test('model insert empty text does not trigger eventing', () => {
121
const e = withEventCapturing(() => {
122
thisModel.applyEdits([EditOperation.insert(new Position(1, 1), '')]);
123
});
124
assert.deepStrictEqual(e, null, 'was not expecting event');
125
});
126
127
test('model insert text without newline eventing', () => {
128
const e = withEventCapturing(() => {
129
thisModel.applyEdits([EditOperation.insert(new Position(1, 1), 'foo ')]);
130
});
131
assert.deepStrictEqual(e, new ModelRawContentChangedEvent(
132
[
133
new ModelRawLineChanged(1, 'foo My First Line', null)
134
],
135
2,
136
false,
137
false
138
));
139
});
140
141
test('model insert text with one newline eventing', () => {
142
const e = withEventCapturing(() => {
143
thisModel.applyEdits([EditOperation.insert(new Position(1, 3), ' new line\nNo longer')]);
144
});
145
assert.deepStrictEqual(e, new ModelRawContentChangedEvent(
146
[
147
new ModelRawLineChanged(1, 'My new line', null),
148
new ModelRawLinesInserted(2, 2, ['No longer First Line'], [null]),
149
],
150
2,
151
false,
152
false
153
));
154
});
155
156
157
// --------- delete text
158
159
test('model delete empty text', () => {
160
thisModel.applyEdits([EditOperation.delete(new Range(1, 1, 1, 1))]);
161
assert.strictEqual(thisModel.getLineCount(), 5);
162
assert.strictEqual(thisModel.getLineContent(1), 'My First Line');
163
});
164
165
test('model delete text from one line', () => {
166
thisModel.applyEdits([EditOperation.delete(new Range(1, 1, 1, 2))]);
167
assert.strictEqual(thisModel.getLineCount(), 5);
168
assert.strictEqual(thisModel.getLineContent(1), 'y First Line');
169
});
170
171
test('model delete text from one line 2', () => {
172
thisModel.applyEdits([EditOperation.insert(new Position(1, 1), 'a')]);
173
assert.strictEqual(thisModel.getLineContent(1), 'aMy First Line');
174
175
thisModel.applyEdits([EditOperation.delete(new Range(1, 2, 1, 4))]);
176
assert.strictEqual(thisModel.getLineCount(), 5);
177
assert.strictEqual(thisModel.getLineContent(1), 'a First Line');
178
});
179
180
test('model delete all text from a line', () => {
181
thisModel.applyEdits([EditOperation.delete(new Range(1, 1, 1, 14))]);
182
assert.strictEqual(thisModel.getLineCount(), 5);
183
assert.strictEqual(thisModel.getLineContent(1), '');
184
});
185
186
test('model delete text from two lines', () => {
187
thisModel.applyEdits([EditOperation.delete(new Range(1, 4, 2, 6))]);
188
assert.strictEqual(thisModel.getLineCount(), 4);
189
assert.strictEqual(thisModel.getLineContent(1), 'My Second Line');
190
});
191
192
test('model delete text from many lines', () => {
193
thisModel.applyEdits([EditOperation.delete(new Range(1, 4, 3, 5))]);
194
assert.strictEqual(thisModel.getLineCount(), 3);
195
assert.strictEqual(thisModel.getLineContent(1), 'My Third Line');
196
});
197
198
test('model delete everything', () => {
199
thisModel.applyEdits([EditOperation.delete(new Range(1, 1, 5, 2))]);
200
assert.strictEqual(thisModel.getLineCount(), 1);
201
assert.strictEqual(thisModel.getLineContent(1), '');
202
});
203
204
// --------- delete text eventing
205
206
test('model delete empty text does not trigger eventing', () => {
207
const e = withEventCapturing(() => {
208
thisModel.applyEdits([EditOperation.delete(new Range(1, 1, 1, 1))]);
209
});
210
assert.deepStrictEqual(e, null, 'was not expecting event');
211
});
212
213
test('model delete text from one line eventing', () => {
214
const e = withEventCapturing(() => {
215
thisModel.applyEdits([EditOperation.delete(new Range(1, 1, 1, 2))]);
216
});
217
assert.deepStrictEqual(e, new ModelRawContentChangedEvent(
218
[
219
new ModelRawLineChanged(1, 'y First Line', null),
220
],
221
2,
222
false,
223
false
224
));
225
});
226
227
test('model delete all text from a line eventing', () => {
228
const e = withEventCapturing(() => {
229
thisModel.applyEdits([EditOperation.delete(new Range(1, 1, 1, 14))]);
230
});
231
assert.deepStrictEqual(e, new ModelRawContentChangedEvent(
232
[
233
new ModelRawLineChanged(1, '', null),
234
],
235
2,
236
false,
237
false
238
));
239
});
240
241
test('model delete text from two lines eventing', () => {
242
const e = withEventCapturing(() => {
243
thisModel.applyEdits([EditOperation.delete(new Range(1, 4, 2, 6))]);
244
});
245
assert.deepStrictEqual(e, new ModelRawContentChangedEvent(
246
[
247
new ModelRawLineChanged(1, 'My Second Line', null),
248
new ModelRawLinesDeleted(2, 2),
249
],
250
2,
251
false,
252
false
253
));
254
});
255
256
test('model delete text from many lines eventing', () => {
257
const e = withEventCapturing(() => {
258
thisModel.applyEdits([EditOperation.delete(new Range(1, 4, 3, 5))]);
259
});
260
assert.deepStrictEqual(e, new ModelRawContentChangedEvent(
261
[
262
new ModelRawLineChanged(1, 'My Third Line', null),
263
new ModelRawLinesDeleted(2, 3),
264
],
265
2,
266
false,
267
false
268
));
269
});
270
271
// --------- getValueInRange
272
273
test('getValueInRange', () => {
274
assert.strictEqual(thisModel.getValueInRange(new Range(1, 1, 1, 1)), '');
275
assert.strictEqual(thisModel.getValueInRange(new Range(1, 1, 1, 2)), 'M');
276
assert.strictEqual(thisModel.getValueInRange(new Range(1, 2, 1, 3)), 'y');
277
assert.strictEqual(thisModel.getValueInRange(new Range(1, 1, 1, 14)), 'My First Line');
278
assert.strictEqual(thisModel.getValueInRange(new Range(1, 1, 2, 1)), 'My First Line\n');
279
assert.strictEqual(thisModel.getValueInRange(new Range(1, 1, 2, 2)), 'My First Line\n\t');
280
assert.strictEqual(thisModel.getValueInRange(new Range(1, 1, 2, 3)), 'My First Line\n\t\t');
281
assert.strictEqual(thisModel.getValueInRange(new Range(1, 1, 2, 17)), 'My First Line\n\t\tMy Second Line');
282
assert.strictEqual(thisModel.getValueInRange(new Range(1, 1, 3, 1)), 'My First Line\n\t\tMy Second Line\n');
283
assert.strictEqual(thisModel.getValueInRange(new Range(1, 1, 4, 1)), 'My First Line\n\t\tMy Second Line\n Third Line\n');
284
});
285
286
// --------- getValueLengthInRange
287
288
test('getValueLengthInRange', () => {
289
assert.strictEqual(thisModel.getValueLengthInRange(new Range(1, 1, 1, 1)), ''.length);
290
assert.strictEqual(thisModel.getValueLengthInRange(new Range(1, 1, 1, 2)), 'M'.length);
291
assert.strictEqual(thisModel.getValueLengthInRange(new Range(1, 2, 1, 3)), 'y'.length);
292
assert.strictEqual(thisModel.getValueLengthInRange(new Range(1, 1, 1, 14)), 'My First Line'.length);
293
assert.strictEqual(thisModel.getValueLengthInRange(new Range(1, 1, 2, 1)), 'My First Line\n'.length);
294
assert.strictEqual(thisModel.getValueLengthInRange(new Range(1, 1, 2, 2)), 'My First Line\n\t'.length);
295
assert.strictEqual(thisModel.getValueLengthInRange(new Range(1, 1, 2, 3)), 'My First Line\n\t\t'.length);
296
assert.strictEqual(thisModel.getValueLengthInRange(new Range(1, 1, 2, 17)), 'My First Line\n\t\tMy Second Line'.length);
297
assert.strictEqual(thisModel.getValueLengthInRange(new Range(1, 1, 3, 1)), 'My First Line\n\t\tMy Second Line\n'.length);
298
assert.strictEqual(thisModel.getValueLengthInRange(new Range(1, 1, 4, 1)), 'My First Line\n\t\tMy Second Line\n Third Line\n'.length);
299
});
300
301
// --------- setValue
302
test('setValue eventing', () => {
303
const e = withEventCapturing(() => {
304
thisModel.setValue('new value');
305
});
306
assert.deepStrictEqual(e, new ModelRawContentChangedEvent(
307
[
308
new ModelRawFlush()
309
],
310
2,
311
false,
312
false
313
));
314
});
315
316
test('issue #46342: Maintain edit operation order in applyEdits', () => {
317
const res = thisModel.applyEdits([
318
{ range: new Range(2, 1, 2, 1), text: 'a' },
319
{ range: new Range(1, 1, 1, 1), text: 'b' },
320
], true);
321
322
assert.deepStrictEqual(res[0].range, new Range(2, 1, 2, 2));
323
assert.deepStrictEqual(res[1].range, new Range(1, 1, 1, 2));
324
});
325
});
326
327
328
// --------- Special Unicode LINE SEPARATOR character
329
suite('Editor Model - Model Line Separators', () => {
330
331
let thisModel: TextModel;
332
333
setup(() => {
334
const text =
335
LINE1 + '\u2028' +
336
LINE2 + '\n' +
337
LINE3 + '\u2028' +
338
LINE4 + '\r\n' +
339
LINE5;
340
thisModel = createTextModel(text);
341
});
342
343
teardown(() => {
344
thisModel.dispose();
345
});
346
347
ensureNoDisposablesAreLeakedInTestSuite();
348
349
test('model getValue', () => {
350
assert.strictEqual(thisModel.getValue(), 'My First Line\u2028\t\tMy Second Line\n Third Line\u2028\n1');
351
});
352
353
test('model lines', () => {
354
assert.strictEqual(thisModel.getLineCount(), 3);
355
});
356
357
test('Bug 13333:Model should line break on lonely CR too', () => {
358
const model = createTextModel('Hello\rWorld!\r\nAnother line');
359
assert.strictEqual(model.getLineCount(), 3);
360
assert.strictEqual(model.getValue(), 'Hello\r\nWorld!\r\nAnother line');
361
model.dispose();
362
});
363
});
364
365
366
// --------- Words
367
368
suite('Editor Model - Words', () => {
369
370
const OUTER_LANGUAGE_ID = 'outerMode';
371
const INNER_LANGUAGE_ID = 'innerMode';
372
373
class OuterMode extends Disposable {
374
375
public readonly languageId = OUTER_LANGUAGE_ID;
376
377
constructor(
378
@ILanguageService languageService: ILanguageService,
379
@ILanguageConfigurationService languageConfigurationService: ILanguageConfigurationService
380
) {
381
super();
382
this._register(languageService.registerLanguage({ id: this.languageId }));
383
this._register(languageConfigurationService.register(this.languageId, {}));
384
385
const languageIdCodec = languageService.languageIdCodec;
386
this._register(TokenizationRegistry.register(this.languageId, {
387
getInitialState: (): IState => NullState,
388
tokenize: undefined!,
389
tokenizeEncoded: (line: string, hasEOL: boolean, state: IState): EncodedTokenizationResult => {
390
const tokensArr: number[] = [];
391
let prevLanguageId: string | undefined = undefined;
392
for (let i = 0; i < line.length; i++) {
393
const languageId = (line.charAt(i) === 'x' ? INNER_LANGUAGE_ID : OUTER_LANGUAGE_ID);
394
const encodedLanguageId = languageIdCodec.encodeLanguageId(languageId);
395
if (prevLanguageId !== languageId) {
396
tokensArr.push(i);
397
tokensArr.push((encodedLanguageId << MetadataConsts.LANGUAGEID_OFFSET));
398
}
399
prevLanguageId = languageId;
400
}
401
402
const tokens = new Uint32Array(tokensArr.length);
403
for (let i = 0; i < tokens.length; i++) {
404
tokens[i] = tokensArr[i];
405
}
406
return new EncodedTokenizationResult(tokens, [], state);
407
}
408
}));
409
}
410
}
411
412
class InnerMode extends Disposable {
413
414
public readonly languageId = INNER_LANGUAGE_ID;
415
416
constructor(
417
@ILanguageService languageService: ILanguageService,
418
@ILanguageConfigurationService languageConfigurationService: ILanguageConfigurationService
419
) {
420
super();
421
this._register(languageService.registerLanguage({ id: this.languageId }));
422
this._register(languageConfigurationService.register(this.languageId, {}));
423
}
424
}
425
426
let disposables: Disposable[] = [];
427
428
setup(() => {
429
disposables = [];
430
});
431
432
teardown(() => {
433
dispose(disposables);
434
disposables = [];
435
});
436
437
ensureNoDisposablesAreLeakedInTestSuite();
438
439
test('Get word at position', () => {
440
const text = ['This text has some words. '];
441
const thisModel = createTextModel(text.join('\n'));
442
disposables.push(thisModel);
443
444
assert.deepStrictEqual(thisModel.getWordAtPosition(new Position(1, 1)), { word: 'This', startColumn: 1, endColumn: 5 });
445
assert.deepStrictEqual(thisModel.getWordAtPosition(new Position(1, 2)), { word: 'This', startColumn: 1, endColumn: 5 });
446
assert.deepStrictEqual(thisModel.getWordAtPosition(new Position(1, 4)), { word: 'This', startColumn: 1, endColumn: 5 });
447
assert.deepStrictEqual(thisModel.getWordAtPosition(new Position(1, 5)), { word: 'This', startColumn: 1, endColumn: 5 });
448
assert.deepStrictEqual(thisModel.getWordAtPosition(new Position(1, 6)), { word: 'text', startColumn: 6, endColumn: 10 });
449
assert.deepStrictEqual(thisModel.getWordAtPosition(new Position(1, 19)), { word: 'some', startColumn: 15, endColumn: 19 });
450
assert.deepStrictEqual(thisModel.getWordAtPosition(new Position(1, 20)), null);
451
assert.deepStrictEqual(thisModel.getWordAtPosition(new Position(1, 21)), { word: 'words', startColumn: 21, endColumn: 26 });
452
assert.deepStrictEqual(thisModel.getWordAtPosition(new Position(1, 26)), { word: 'words', startColumn: 21, endColumn: 26 });
453
assert.deepStrictEqual(thisModel.getWordAtPosition(new Position(1, 27)), null);
454
assert.deepStrictEqual(thisModel.getWordAtPosition(new Position(1, 28)), null);
455
});
456
457
test('getWordAtPosition at embedded language boundaries', () => {
458
const disposables = new DisposableStore();
459
const instantiationService = createModelServices(disposables);
460
const outerMode = disposables.add(instantiationService.createInstance(OuterMode));
461
disposables.add(instantiationService.createInstance(InnerMode));
462
463
const model = disposables.add(instantiateTextModel(instantiationService, 'ab<xx>ab<x>', outerMode.languageId));
464
465
assert.deepStrictEqual(model.getWordAtPosition(new Position(1, 1)), { word: 'ab', startColumn: 1, endColumn: 3 });
466
assert.deepStrictEqual(model.getWordAtPosition(new Position(1, 2)), { word: 'ab', startColumn: 1, endColumn: 3 });
467
assert.deepStrictEqual(model.getWordAtPosition(new Position(1, 3)), { word: 'ab', startColumn: 1, endColumn: 3 });
468
assert.deepStrictEqual(model.getWordAtPosition(new Position(1, 4)), { word: 'xx', startColumn: 4, endColumn: 6 });
469
assert.deepStrictEqual(model.getWordAtPosition(new Position(1, 5)), { word: 'xx', startColumn: 4, endColumn: 6 });
470
assert.deepStrictEqual(model.getWordAtPosition(new Position(1, 6)), { word: 'xx', startColumn: 4, endColumn: 6 });
471
assert.deepStrictEqual(model.getWordAtPosition(new Position(1, 7)), { word: 'ab', startColumn: 7, endColumn: 9 });
472
473
disposables.dispose();
474
});
475
476
test('issue #61296: VS code freezes when editing CSS file with emoji', () => {
477
const MODE_ID = 'testMode';
478
const disposables = new DisposableStore();
479
const instantiationService = createModelServices(disposables);
480
const languageConfigurationService = instantiationService.get(ILanguageConfigurationService);
481
const languageService = instantiationService.get(ILanguageService);
482
483
disposables.add(languageService.registerLanguage({ id: MODE_ID }));
484
disposables.add(languageConfigurationService.register(MODE_ID, {
485
wordPattern: /(#?-?\d*\.\d\w*%?)|(::?[\w-]*(?=[^,{;]*[,{]))|(([@#.!])?[\w-?]+%?|[@#!.])/g
486
}));
487
488
const thisModel = disposables.add(instantiateTextModel(instantiationService, '.🐷-a-b', MODE_ID));
489
490
assert.deepStrictEqual(thisModel.getWordAtPosition(new Position(1, 1)), { word: '.', startColumn: 1, endColumn: 2 });
491
assert.deepStrictEqual(thisModel.getWordAtPosition(new Position(1, 2)), { word: '.', startColumn: 1, endColumn: 2 });
492
assert.deepStrictEqual(thisModel.getWordAtPosition(new Position(1, 3)), null);
493
assert.deepStrictEqual(thisModel.getWordAtPosition(new Position(1, 4)), { word: '-a-b', startColumn: 4, endColumn: 8 });
494
assert.deepStrictEqual(thisModel.getWordAtPosition(new Position(1, 5)), { word: '-a-b', startColumn: 4, endColumn: 8 });
495
assert.deepStrictEqual(thisModel.getWordAtPosition(new Position(1, 6)), { word: '-a-b', startColumn: 4, endColumn: 8 });
496
assert.deepStrictEqual(thisModel.getWordAtPosition(new Position(1, 7)), { word: '-a-b', startColumn: 4, endColumn: 8 });
497
assert.deepStrictEqual(thisModel.getWordAtPosition(new Position(1, 8)), { word: '-a-b', startColumn: 4, endColumn: 8 });
498
499
disposables.dispose();
500
});
501
});
502
503