Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/api/test/browser/extHostDocumentData.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 { URI } from '../../../../base/common/uri.js';
8
import { ExtHostDocumentData } from '../../common/extHostDocumentData.js';
9
import { Position } from '../../common/extHostTypes.js';
10
import { Range } from '../../../../editor/common/core/range.js';
11
import { MainThreadDocumentsShape } from '../../common/extHost.protocol.js';
12
import { IModelChangedEvent } from '../../../../editor/common/model/mirrorTextModel.js';
13
import { mock } from '../../../../base/test/common/mock.js';
14
import * as perfData from './extHostDocumentData.test.perf-data.js';
15
import { setDefaultGetWordAtTextConfig } from '../../../../editor/common/core/wordHelper.js';
16
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js';
17
18
suite('ExtHostDocumentData', () => {
19
20
let data: ExtHostDocumentData;
21
22
function assertPositionAt(offset: number, line: number, character: number) {
23
const position = data.document.positionAt(offset);
24
assert.strictEqual(position.line, line);
25
assert.strictEqual(position.character, character);
26
}
27
28
function assertOffsetAt(line: number, character: number, offset: number) {
29
const pos = new Position(line, character);
30
const actual = data.document.offsetAt(pos);
31
assert.strictEqual(actual, offset);
32
}
33
34
setup(function () {
35
data = new ExtHostDocumentData(undefined!, URI.file(''), [
36
'This is line one', //16
37
'and this is line number two', //27
38
'it is followed by #3', //20
39
'and finished with the fourth.', //29
40
], '\n', 1, 'text', false, 'utf8');
41
});
42
43
ensureNoDisposablesAreLeakedInTestSuite();
44
45
test('readonly-ness', () => {
46
assert.throws(() => (data as any).document.uri = null);
47
assert.throws(() => (data as any).document.fileName = 'foofile');
48
assert.throws(() => (data as any).document.isDirty = false);
49
assert.throws(() => (data as any).document.isUntitled = false);
50
assert.throws(() => (data as any).document.languageId = 'dddd');
51
assert.throws(() => (data as any).document.lineCount = 9);
52
});
53
54
test('save, when disposed', function () {
55
let saved: URI;
56
const data = new ExtHostDocumentData(new class extends mock<MainThreadDocumentsShape>() {
57
override $trySaveDocument(uri: URI) {
58
assert.ok(!saved);
59
saved = uri;
60
return Promise.resolve(true);
61
}
62
}, URI.parse('foo:bar'), [], '\n', 1, 'text', true, 'utf8');
63
64
return data.document.save().then(() => {
65
assert.strictEqual(saved.toString(), 'foo:bar');
66
67
data.dispose();
68
69
return data.document.save().then(() => {
70
assert.ok(false, 'expected failure');
71
}, err => {
72
assert.ok(err);
73
});
74
});
75
});
76
77
test('read, when disposed', function () {
78
data.dispose();
79
80
const { document } = data;
81
assert.strictEqual(document.lineCount, 4);
82
assert.strictEqual(document.lineAt(0).text, 'This is line one');
83
});
84
85
test('lines', () => {
86
87
assert.strictEqual(data.document.lineCount, 4);
88
89
assert.throws(() => data.document.lineAt(-1));
90
assert.throws(() => data.document.lineAt(data.document.lineCount));
91
assert.throws(() => data.document.lineAt(Number.MAX_VALUE));
92
assert.throws(() => data.document.lineAt(Number.MIN_VALUE));
93
assert.throws(() => data.document.lineAt(0.8));
94
95
let line = data.document.lineAt(0);
96
assert.strictEqual(line.lineNumber, 0);
97
assert.strictEqual(line.text.length, 16);
98
assert.strictEqual(line.text, 'This is line one');
99
assert.strictEqual(line.isEmptyOrWhitespace, false);
100
assert.strictEqual(line.firstNonWhitespaceCharacterIndex, 0);
101
102
data.onEvents({
103
changes: [{
104
range: { startLineNumber: 1, startColumn: 1, endLineNumber: 1, endColumn: 1 },
105
rangeOffset: undefined!,
106
rangeLength: undefined!,
107
text: '\t '
108
}],
109
eol: undefined!,
110
versionId: undefined!,
111
isRedoing: false,
112
isUndoing: false,
113
});
114
115
// line didn't change
116
assert.strictEqual(line.text, 'This is line one');
117
assert.strictEqual(line.firstNonWhitespaceCharacterIndex, 0);
118
119
// fetch line again
120
line = data.document.lineAt(0);
121
assert.strictEqual(line.text, '\t This is line one');
122
assert.strictEqual(line.firstNonWhitespaceCharacterIndex, 2);
123
});
124
125
test('line, issue #5704', function () {
126
127
let line = data.document.lineAt(0);
128
let { range, rangeIncludingLineBreak } = line;
129
assert.strictEqual(range.end.line, 0);
130
assert.strictEqual(range.end.character, 16);
131
assert.strictEqual(rangeIncludingLineBreak.end.line, 1);
132
assert.strictEqual(rangeIncludingLineBreak.end.character, 0);
133
134
line = data.document.lineAt(data.document.lineCount - 1);
135
range = line.range;
136
rangeIncludingLineBreak = line.rangeIncludingLineBreak;
137
assert.strictEqual(range.end.line, 3);
138
assert.strictEqual(range.end.character, 29);
139
assert.strictEqual(rangeIncludingLineBreak.end.line, 3);
140
assert.strictEqual(rangeIncludingLineBreak.end.character, 29);
141
142
});
143
144
test('offsetAt', () => {
145
assertOffsetAt(0, 0, 0);
146
assertOffsetAt(0, 1, 1);
147
assertOffsetAt(0, 16, 16);
148
assertOffsetAt(1, 0, 17);
149
assertOffsetAt(1, 3, 20);
150
assertOffsetAt(2, 0, 45);
151
assertOffsetAt(4, 29, 95);
152
assertOffsetAt(4, 30, 95);
153
assertOffsetAt(4, Number.MAX_VALUE, 95);
154
assertOffsetAt(5, 29, 95);
155
assertOffsetAt(Number.MAX_VALUE, 29, 95);
156
assertOffsetAt(Number.MAX_VALUE, Number.MAX_VALUE, 95);
157
});
158
159
test('offsetAt, after remove', function () {
160
161
data.onEvents({
162
changes: [{
163
range: { startLineNumber: 1, startColumn: 3, endLineNumber: 1, endColumn: 6 },
164
rangeOffset: undefined!,
165
rangeLength: undefined!,
166
text: ''
167
}],
168
eol: undefined!,
169
versionId: undefined!,
170
isRedoing: false,
171
isUndoing: false,
172
});
173
174
assertOffsetAt(0, 1, 1);
175
assertOffsetAt(0, 13, 13);
176
assertOffsetAt(1, 0, 14);
177
});
178
179
test('offsetAt, after replace', function () {
180
181
data.onEvents({
182
changes: [{
183
range: { startLineNumber: 1, startColumn: 3, endLineNumber: 1, endColumn: 6 },
184
rangeOffset: undefined!,
185
rangeLength: undefined!,
186
text: 'is could be'
187
}],
188
eol: undefined!,
189
versionId: undefined!,
190
isRedoing: false,
191
isUndoing: false,
192
});
193
194
assertOffsetAt(0, 1, 1);
195
assertOffsetAt(0, 24, 24);
196
assertOffsetAt(1, 0, 25);
197
});
198
199
test('offsetAt, after insert line', function () {
200
201
data.onEvents({
202
changes: [{
203
range: { startLineNumber: 1, startColumn: 3, endLineNumber: 1, endColumn: 6 },
204
rangeOffset: undefined!,
205
rangeLength: undefined!,
206
text: 'is could be\na line with number'
207
}],
208
eol: undefined!,
209
versionId: undefined!,
210
isRedoing: false,
211
isUndoing: false,
212
});
213
214
assertOffsetAt(0, 1, 1);
215
assertOffsetAt(0, 13, 13);
216
assertOffsetAt(1, 0, 14);
217
assertOffsetAt(1, 18, 13 + 1 + 18);
218
assertOffsetAt(1, 29, 13 + 1 + 29);
219
assertOffsetAt(2, 0, 13 + 1 + 29 + 1);
220
});
221
222
test('offsetAt, after remove line', function () {
223
224
data.onEvents({
225
changes: [{
226
range: { startLineNumber: 1, startColumn: 3, endLineNumber: 2, endColumn: 6 },
227
rangeOffset: undefined!,
228
rangeLength: undefined!,
229
text: ''
230
}],
231
eol: undefined!,
232
versionId: undefined!,
233
isRedoing: false,
234
isUndoing: false,
235
});
236
237
assertOffsetAt(0, 1, 1);
238
assertOffsetAt(0, 2, 2);
239
assertOffsetAt(1, 0, 25);
240
});
241
242
test('positionAt', () => {
243
assertPositionAt(0, 0, 0);
244
assertPositionAt(Number.MIN_VALUE, 0, 0);
245
assertPositionAt(1, 0, 1);
246
assertPositionAt(16, 0, 16);
247
assertPositionAt(17, 1, 0);
248
assertPositionAt(20, 1, 3);
249
assertPositionAt(45, 2, 0);
250
assertPositionAt(95, 3, 29);
251
assertPositionAt(96, 3, 29);
252
assertPositionAt(99, 3, 29);
253
assertPositionAt(Number.MAX_VALUE, 3, 29);
254
});
255
256
test('getWordRangeAtPosition', () => {
257
data = new ExtHostDocumentData(undefined!, URI.file(''), [
258
'aaaa bbbb+cccc abc'
259
], '\n', 1, 'text', false, 'utf8');
260
261
let range = data.document.getWordRangeAtPosition(new Position(0, 2))!;
262
assert.strictEqual(range.start.line, 0);
263
assert.strictEqual(range.start.character, 0);
264
assert.strictEqual(range.end.line, 0);
265
assert.strictEqual(range.end.character, 4);
266
267
// ignore bad regular expresson /.*/
268
assert.throws(() => data.document.getWordRangeAtPosition(new Position(0, 2), /.*/)!);
269
270
range = data.document.getWordRangeAtPosition(new Position(0, 5), /[a-z+]+/)!;
271
assert.strictEqual(range.start.line, 0);
272
assert.strictEqual(range.start.character, 5);
273
assert.strictEqual(range.end.line, 0);
274
assert.strictEqual(range.end.character, 14);
275
276
range = data.document.getWordRangeAtPosition(new Position(0, 17), /[a-z+]+/)!;
277
assert.strictEqual(range.start.line, 0);
278
assert.strictEqual(range.start.character, 15);
279
assert.strictEqual(range.end.line, 0);
280
assert.strictEqual(range.end.character, 18);
281
282
range = data.document.getWordRangeAtPosition(new Position(0, 11), /yy/)!;
283
assert.strictEqual(range, undefined);
284
});
285
286
test('getWordRangeAtPosition doesn\'t quite use the regex as expected, #29102', function () {
287
data = new ExtHostDocumentData(undefined!, URI.file(''), [
288
'some text here',
289
'/** foo bar */',
290
'function() {',
291
' "far boo"',
292
'}'
293
], '\n', 1, 'text', false, 'utf8');
294
295
let range = data.document.getWordRangeAtPosition(new Position(0, 0), /\/\*.+\*\//);
296
assert.strictEqual(range, undefined);
297
298
range = data.document.getWordRangeAtPosition(new Position(1, 0), /\/\*.+\*\//)!;
299
assert.strictEqual(range.start.line, 1);
300
assert.strictEqual(range.start.character, 0);
301
assert.strictEqual(range.end.line, 1);
302
assert.strictEqual(range.end.character, 14);
303
304
range = data.document.getWordRangeAtPosition(new Position(3, 0), /("|').*\1/);
305
assert.strictEqual(range, undefined);
306
307
range = data.document.getWordRangeAtPosition(new Position(3, 1), /("|').*\1/)!;
308
assert.strictEqual(range.start.line, 3);
309
assert.strictEqual(range.start.character, 1);
310
assert.strictEqual(range.end.line, 3);
311
assert.strictEqual(range.end.character, 10);
312
});
313
314
315
test('getWordRangeAtPosition can freeze the extension host #95319', function () {
316
317
const regex = /(https?:\/\/github\.com\/(([^\s]+)\/([^\s]+))\/([^\s]+\/)?(issues|pull)\/([0-9]+))|(([^\s]+)\/([^\s]+))?#([1-9][0-9]*)($|[\s\:\;\-\(\=])/;
318
319
data = new ExtHostDocumentData(undefined!, URI.file(''), [
320
perfData._$_$_expensive
321
], '\n', 1, 'text', false, 'utf8');
322
323
// this test only ensures that we eventually give and timeout (when searching "funny" words and long lines)
324
// for the sake of speedy tests we lower the timeBudget here
325
const config = setDefaultGetWordAtTextConfig({ maxLen: 1000, windowSize: 15, timeBudget: 30 });
326
try {
327
let range = data.document.getWordRangeAtPosition(new Position(0, 1_177_170), regex)!;
328
assert.strictEqual(range, undefined);
329
330
const pos = new Position(0, 1177170);
331
range = data.document.getWordRangeAtPosition(pos)!;
332
assert.ok(range);
333
assert.ok(range.contains(pos));
334
assert.strictEqual(data.document.getText(range), 'TaskDefinition');
335
336
} finally {
337
config.dispose();
338
}
339
});
340
341
test('Rename popup sometimes populates with text on the left side omitted #96013', function () {
342
343
const regex = /(-?\d*\.\d\w*)|([^\`\~\!\@\#\$\%\^\&\*\(\)\-\=\+\[\{\]\}\\\|\;\:\'\"\,\.\<\>\/\?\s]+)/g;
344
const line = 'int abcdefhijklmnopqwvrstxyz;';
345
346
data = new ExtHostDocumentData(undefined!, URI.file(''), [
347
line
348
], '\n', 1, 'text', false, 'utf8');
349
350
const range = data.document.getWordRangeAtPosition(new Position(0, 27), regex)!;
351
assert.strictEqual(range.start.line, 0);
352
assert.strictEqual(range.end.line, 0);
353
assert.strictEqual(range.start.character, 4);
354
assert.strictEqual(range.end.character, 28);
355
});
356
357
test('Custom snippet $TM_SELECTED_TEXT not show suggestion #108892', function () {
358
359
data = new ExtHostDocumentData(undefined!, URI.file(''), [
360
` <p><span xml:lang="en">Sheldon</span>, soprannominato "<span xml:lang="en">Shelly</span> dalla madre e dalla sorella, è nato a <span xml:lang="en">Galveston</span>, in <span xml:lang="en">Texas</span>, il 26 febbraio 1980 in un supermercato. È stato un bambino prodigio, come testimoniato dal suo quoziente d'intelligenza (187, di molto superiore alla norma) e dalla sua rapida carriera scolastica: si è diplomato all'eta di 11 anni approdando alla stessa età alla formazione universitaria e all'età di 16 anni ha ottenuto il suo primo dottorato di ricerca. All'inizio della serie e per gran parte di essa vive con il coinquilino Leonard nell'appartamento 4A al 2311 <span xml:lang="en">North Los Robles Avenue</span> di <span xml:lang="en">Pasadena</span>, per poi trasferirsi nell'appartamento di <span xml:lang="en">Penny</span> con <span xml:lang="en">Amy</span> nella decima stagione. Come più volte afferma lui stesso possiede una memoria eidetica e un orecchio assoluto. È stato educato da una madre estremamente religiosa e, in più occasioni, questo aspetto contrasta con il rigore scientifico di <span xml:lang="en">Sheldon</span>; tuttavia la donna sembra essere l'unica persona in grado di comandarlo a bacchetta.</p>`
361
], '\n', 1, 'text', false, 'utf8');
362
363
const pos = new Position(0, 55);
364
const range = data.document.getWordRangeAtPosition(pos)!;
365
assert.strictEqual(range.start.line, 0);
366
assert.strictEqual(range.end.line, 0);
367
assert.strictEqual(range.start.character, 47);
368
assert.strictEqual(range.end.character, 61);
369
assert.strictEqual(data.document.getText(range), 'soprannominato');
370
});
371
});
372
373
enum AssertDocumentLineMappingDirection {
374
OffsetToPosition,
375
PositionToOffset
376
}
377
378
suite('ExtHostDocumentData updates line mapping', () => {
379
380
function positionToStr(position: { line: number; character: number }): string {
381
return '(' + position.line + ',' + position.character + ')';
382
}
383
384
function assertDocumentLineMapping(doc: ExtHostDocumentData, direction: AssertDocumentLineMappingDirection): void {
385
const allText = doc.getText();
386
387
let line = 0, character = 0, previousIsCarriageReturn = false;
388
for (let offset = 0; offset <= allText.length; offset++) {
389
// The position coordinate system cannot express the position between \r and \n
390
const position: Position = new Position(line, character + (previousIsCarriageReturn ? -1 : 0));
391
392
if (direction === AssertDocumentLineMappingDirection.OffsetToPosition) {
393
const actualPosition = doc.document.positionAt(offset);
394
assert.strictEqual(positionToStr(actualPosition), positionToStr(position), 'positionAt mismatch for offset ' + offset);
395
} else {
396
// The position coordinate system cannot express the position between \r and \n
397
const expectedOffset: number = offset + (previousIsCarriageReturn ? -1 : 0);
398
const actualOffset = doc.document.offsetAt(position);
399
assert.strictEqual(actualOffset, expectedOffset, 'offsetAt mismatch for position ' + positionToStr(position));
400
}
401
402
if (allText.charAt(offset) === '\n') {
403
line++;
404
character = 0;
405
} else {
406
character++;
407
}
408
409
previousIsCarriageReturn = (allText.charAt(offset) === '\r');
410
}
411
}
412
413
function createChangeEvent(range: Range, text: string, eol?: string): IModelChangedEvent {
414
return {
415
changes: [{
416
range: range,
417
rangeOffset: undefined!,
418
rangeLength: undefined!,
419
text: text
420
}],
421
eol: eol!,
422
versionId: undefined!,
423
isRedoing: false,
424
isUndoing: false,
425
};
426
}
427
428
function testLineMappingDirectionAfterEvents(lines: string[], eol: string, direction: AssertDocumentLineMappingDirection, e: IModelChangedEvent): void {
429
const myDocument = new ExtHostDocumentData(undefined!, URI.file(''), lines.slice(0), eol, 1, 'text', false, 'utf8');
430
assertDocumentLineMapping(myDocument, direction);
431
432
myDocument.onEvents(e);
433
assertDocumentLineMapping(myDocument, direction);
434
}
435
436
function testLineMappingAfterEvents(lines: string[], e: IModelChangedEvent): void {
437
testLineMappingDirectionAfterEvents(lines, '\n', AssertDocumentLineMappingDirection.PositionToOffset, e);
438
testLineMappingDirectionAfterEvents(lines, '\n', AssertDocumentLineMappingDirection.OffsetToPosition, e);
439
440
testLineMappingDirectionAfterEvents(lines, '\r\n', AssertDocumentLineMappingDirection.PositionToOffset, e);
441
testLineMappingDirectionAfterEvents(lines, '\r\n', AssertDocumentLineMappingDirection.OffsetToPosition, e);
442
}
443
444
ensureNoDisposablesAreLeakedInTestSuite();
445
446
test('line mapping', () => {
447
testLineMappingAfterEvents([
448
'This is line one',
449
'and this is line number two',
450
'it is followed by #3',
451
'and finished with the fourth.',
452
], { changes: [], eol: undefined!, versionId: 7, isRedoing: false, isUndoing: false });
453
});
454
455
test('after remove', () => {
456
testLineMappingAfterEvents([
457
'This is line one',
458
'and this is line number two',
459
'it is followed by #3',
460
'and finished with the fourth.',
461
], createChangeEvent(new Range(1, 3, 1, 6), ''));
462
});
463
464
test('after replace', () => {
465
testLineMappingAfterEvents([
466
'This is line one',
467
'and this is line number two',
468
'it is followed by #3',
469
'and finished with the fourth.',
470
], createChangeEvent(new Range(1, 3, 1, 6), 'is could be'));
471
});
472
473
test('after insert line', () => {
474
testLineMappingAfterEvents([
475
'This is line one',
476
'and this is line number two',
477
'it is followed by #3',
478
'and finished with the fourth.',
479
], createChangeEvent(new Range(1, 3, 1, 6), 'is could be\na line with number'));
480
});
481
482
test('after insert two lines', () => {
483
testLineMappingAfterEvents([
484
'This is line one',
485
'and this is line number two',
486
'it is followed by #3',
487
'and finished with the fourth.',
488
], createChangeEvent(new Range(1, 3, 1, 6), 'is could be\na line with number\nyet another line'));
489
});
490
491
test('after remove line', () => {
492
testLineMappingAfterEvents([
493
'This is line one',
494
'and this is line number two',
495
'it is followed by #3',
496
'and finished with the fourth.',
497
], createChangeEvent(new Range(1, 3, 2, 6), ''));
498
});
499
500
test('after remove two lines', () => {
501
testLineMappingAfterEvents([
502
'This is line one',
503
'and this is line number two',
504
'it is followed by #3',
505
'and finished with the fourth.',
506
], createChangeEvent(new Range(1, 3, 3, 6), ''));
507
});
508
509
test('after deleting entire content', () => {
510
testLineMappingAfterEvents([
511
'This is line one',
512
'and this is line number two',
513
'it is followed by #3',
514
'and finished with the fourth.',
515
], createChangeEvent(new Range(1, 3, 4, 30), ''));
516
});
517
518
test('after replacing entire content', () => {
519
testLineMappingAfterEvents([
520
'This is line one',
521
'and this is line number two',
522
'it is followed by #3',
523
'and finished with the fourth.',
524
], createChangeEvent(new Range(1, 3, 4, 30), 'some new text\nthat\nspans multiple lines'));
525
});
526
527
test('after changing EOL to CRLF', () => {
528
testLineMappingAfterEvents([
529
'This is line one',
530
'and this is line number two',
531
'it is followed by #3',
532
'and finished with the fourth.',
533
], createChangeEvent(new Range(1, 1, 1, 1), '', '\r\n'));
534
});
535
536
test('after changing EOL to LF', () => {
537
testLineMappingAfterEvents([
538
'This is line one',
539
'and this is line number two',
540
'it is followed by #3',
541
'and finished with the fourth.',
542
], createChangeEvent(new Range(1, 1, 1, 1), '', '\n'));
543
});
544
});
545
546