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