Path: blob/main/src/vs/workbench/api/test/browser/extHostDocumentData.test.ts
5237 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import assert from 'assert';6import { URI } from '../../../../base/common/uri.js';7import { ExtHostDocumentData } from '../../common/extHostDocumentData.js';8import { Position } from '../../common/extHostTypes.js';9import { Range } from '../../../../editor/common/core/range.js';10import { MainThreadDocumentsShape } from '../../common/extHost.protocol.js';11import { IModelChangedEvent } from '../../../../editor/common/model/mirrorTextModel.js';12import { mock } from '../../../../base/test/common/mock.js';13import * as perfData from './extHostDocumentData.test.perf-data.js';14import { setDefaultGetWordAtTextConfig } from '../../../../editor/common/core/wordHelper.js';15import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js';1617suite('ExtHostDocumentData', () => {1819let data: ExtHostDocumentData;2021function assertPositionAt(offset: number, line: number, character: number) {22const position = data.document.positionAt(offset);23assert.strictEqual(position.line, line);24assert.strictEqual(position.character, character);25}2627function assertOffsetAt(line: number, character: number, offset: number) {28const pos = new Position(line, character);29const actual = data.document.offsetAt(pos);30assert.strictEqual(actual, offset);31}3233setup(function () {34data = new ExtHostDocumentData(undefined!, URI.file(''), [35'This is line one', //1636'and this is line number two', //2737'it is followed by #3', //2038'and finished with the fourth.', //2939], '\n', 1, 'text', false, 'utf8');40});4142ensureNoDisposablesAreLeakedInTestSuite();4344test('readonly-ness', () => {45// eslint-disable-next-line local/code-no-any-casts46assert.throws(() => (data as any).document.uri = null);47// eslint-disable-next-line local/code-no-any-casts48assert.throws(() => (data as any).document.fileName = 'foofile');49// eslint-disable-next-line local/code-no-any-casts50assert.throws(() => (data as any).document.isDirty = false);51// eslint-disable-next-line local/code-no-any-casts52assert.throws(() => (data as any).document.isUntitled = false);53// eslint-disable-next-line local/code-no-any-casts54assert.throws(() => (data as any).document.languageId = 'dddd');55// eslint-disable-next-line local/code-no-any-casts56assert.throws(() => (data as any).document.lineCount = 9);57});5859test('save, when disposed', function () {60let saved: URI;61const data = new ExtHostDocumentData(new class extends mock<MainThreadDocumentsShape>() {62override $trySaveDocument(uri: URI) {63assert.ok(!saved);64saved = uri;65return Promise.resolve(true);66}67}, URI.parse('foo:bar'), [], '\n', 1, 'text', true, 'utf8');6869return data.document.save().then(() => {70assert.strictEqual(saved.toString(), 'foo:bar');7172data.dispose();7374return data.document.save().then(() => {75assert.ok(false, 'expected failure');76}, err => {77assert.ok(err);78});79});80});8182test('read, when disposed', function () {83data.dispose();8485const { document } = data;86assert.strictEqual(document.lineCount, 4);87assert.strictEqual(document.lineAt(0).text, 'This is line one');88});8990test('lines', () => {9192assert.strictEqual(data.document.lineCount, 4);9394assert.throws(() => data.document.lineAt(-1));95assert.throws(() => data.document.lineAt(data.document.lineCount));96assert.throws(() => data.document.lineAt(Number.MAX_VALUE));97assert.throws(() => data.document.lineAt(Number.MIN_VALUE));98assert.throws(() => data.document.lineAt(0.8));99100let line = data.document.lineAt(0);101assert.strictEqual(line.lineNumber, 0);102assert.strictEqual(line.text.length, 16);103assert.strictEqual(line.text, 'This is line one');104assert.strictEqual(line.isEmptyOrWhitespace, false);105assert.strictEqual(line.firstNonWhitespaceCharacterIndex, 0);106107data.onEvents({108changes: [{109range: { startLineNumber: 1, startColumn: 1, endLineNumber: 1, endColumn: 1 },110rangeOffset: undefined!,111rangeLength: undefined!,112text: '\t '113}],114eol: undefined!,115versionId: undefined!,116isRedoing: false,117isUndoing: false,118});119120// line didn't change121assert.strictEqual(line.text, 'This is line one');122assert.strictEqual(line.firstNonWhitespaceCharacterIndex, 0);123124// fetch line again125line = data.document.lineAt(0);126assert.strictEqual(line.text, '\t This is line one');127assert.strictEqual(line.firstNonWhitespaceCharacterIndex, 2);128});129130test('line, issue #5704', function () {131132let line = data.document.lineAt(0);133let { range, rangeIncludingLineBreak } = line;134assert.strictEqual(range.end.line, 0);135assert.strictEqual(range.end.character, 16);136assert.strictEqual(rangeIncludingLineBreak.end.line, 1);137assert.strictEqual(rangeIncludingLineBreak.end.character, 0);138139line = data.document.lineAt(data.document.lineCount - 1);140range = line.range;141rangeIncludingLineBreak = line.rangeIncludingLineBreak;142assert.strictEqual(range.end.line, 3);143assert.strictEqual(range.end.character, 29);144assert.strictEqual(rangeIncludingLineBreak.end.line, 3);145assert.strictEqual(rangeIncludingLineBreak.end.character, 29);146147});148149test('offsetAt', () => {150assertOffsetAt(0, 0, 0);151assertOffsetAt(0, 1, 1);152assertOffsetAt(0, 16, 16);153assertOffsetAt(1, 0, 17);154assertOffsetAt(1, 3, 20);155assertOffsetAt(2, 0, 45);156assertOffsetAt(4, 29, 95);157assertOffsetAt(4, 30, 95);158assertOffsetAt(4, Number.MAX_VALUE, 95);159assertOffsetAt(5, 29, 95);160assertOffsetAt(Number.MAX_VALUE, 29, 95);161assertOffsetAt(Number.MAX_VALUE, Number.MAX_VALUE, 95);162});163164test('offsetAt, after remove', function () {165166data.onEvents({167changes: [{168range: { startLineNumber: 1, startColumn: 3, endLineNumber: 1, endColumn: 6 },169rangeOffset: undefined!,170rangeLength: undefined!,171text: ''172}],173eol: undefined!,174versionId: undefined!,175isRedoing: false,176isUndoing: false,177});178179assertOffsetAt(0, 1, 1);180assertOffsetAt(0, 13, 13);181assertOffsetAt(1, 0, 14);182});183184test('offsetAt, after replace', function () {185186data.onEvents({187changes: [{188range: { startLineNumber: 1, startColumn: 3, endLineNumber: 1, endColumn: 6 },189rangeOffset: undefined!,190rangeLength: undefined!,191text: 'is could be'192}],193eol: undefined!,194versionId: undefined!,195isRedoing: false,196isUndoing: false,197});198199assertOffsetAt(0, 1, 1);200assertOffsetAt(0, 24, 24);201assertOffsetAt(1, 0, 25);202});203204test('offsetAt, after insert line', function () {205206data.onEvents({207changes: [{208range: { startLineNumber: 1, startColumn: 3, endLineNumber: 1, endColumn: 6 },209rangeOffset: undefined!,210rangeLength: undefined!,211text: 'is could be\na line with number'212}],213eol: undefined!,214versionId: undefined!,215isRedoing: false,216isUndoing: false,217});218219assertOffsetAt(0, 1, 1);220assertOffsetAt(0, 13, 13);221assertOffsetAt(1, 0, 14);222assertOffsetAt(1, 18, 13 + 1 + 18);223assertOffsetAt(1, 29, 13 + 1 + 29);224assertOffsetAt(2, 0, 13 + 1 + 29 + 1);225});226227test('offsetAt, after remove line', function () {228229data.onEvents({230changes: [{231range: { startLineNumber: 1, startColumn: 3, endLineNumber: 2, endColumn: 6 },232rangeOffset: undefined!,233rangeLength: undefined!,234text: ''235}],236eol: undefined!,237versionId: undefined!,238isRedoing: false,239isUndoing: false,240});241242assertOffsetAt(0, 1, 1);243assertOffsetAt(0, 2, 2);244assertOffsetAt(1, 0, 25);245});246247test('positionAt', () => {248assertPositionAt(0, 0, 0);249assertPositionAt(Number.MIN_VALUE, 0, 0);250assertPositionAt(1, 0, 1);251assertPositionAt(16, 0, 16);252assertPositionAt(17, 1, 0);253assertPositionAt(20, 1, 3);254assertPositionAt(45, 2, 0);255assertPositionAt(95, 3, 29);256assertPositionAt(96, 3, 29);257assertPositionAt(99, 3, 29);258assertPositionAt(Number.MAX_VALUE, 3, 29);259});260261test('getWordRangeAtPosition', () => {262data = new ExtHostDocumentData(undefined!, URI.file(''), [263'aaaa bbbb+cccc abc'264], '\n', 1, 'text', false, 'utf8');265266let range = data.document.getWordRangeAtPosition(new Position(0, 2))!;267assert.strictEqual(range.start.line, 0);268assert.strictEqual(range.start.character, 0);269assert.strictEqual(range.end.line, 0);270assert.strictEqual(range.end.character, 4);271272// ignore bad regular expresson /.*/273assert.throws(() => data.document.getWordRangeAtPosition(new Position(0, 2), /.*/)!);274275range = data.document.getWordRangeAtPosition(new Position(0, 5), /[a-z+]+/)!;276assert.strictEqual(range.start.line, 0);277assert.strictEqual(range.start.character, 5);278assert.strictEqual(range.end.line, 0);279assert.strictEqual(range.end.character, 14);280281range = data.document.getWordRangeAtPosition(new Position(0, 17), /[a-z+]+/)!;282assert.strictEqual(range.start.line, 0);283assert.strictEqual(range.start.character, 15);284assert.strictEqual(range.end.line, 0);285assert.strictEqual(range.end.character, 18);286287range = data.document.getWordRangeAtPosition(new Position(0, 11), /yy/)!;288assert.strictEqual(range, undefined);289});290291test('getWordRangeAtPosition doesn\'t quite use the regex as expected, #29102', function () {292data = new ExtHostDocumentData(undefined!, URI.file(''), [293'some text here',294'/** foo bar */',295'function() {',296' "far boo"',297'}'298], '\n', 1, 'text', false, 'utf8');299300let range = data.document.getWordRangeAtPosition(new Position(0, 0), /\/\*.+\*\//);301assert.strictEqual(range, undefined);302303range = data.document.getWordRangeAtPosition(new Position(1, 0), /\/\*.+\*\//)!;304assert.strictEqual(range.start.line, 1);305assert.strictEqual(range.start.character, 0);306assert.strictEqual(range.end.line, 1);307assert.strictEqual(range.end.character, 14);308309range = data.document.getWordRangeAtPosition(new Position(3, 0), /("|').*\1/);310assert.strictEqual(range, undefined);311312range = data.document.getWordRangeAtPosition(new Position(3, 1), /("|').*\1/)!;313assert.strictEqual(range.start.line, 3);314assert.strictEqual(range.start.character, 1);315assert.strictEqual(range.end.line, 3);316assert.strictEqual(range.end.character, 10);317});318319320test('getWordRangeAtPosition can freeze the extension host #95319', function () {321322const regex = /(https?:\/\/github\.com\/(([^\s]+)\/([^\s]+))\/([^\s]+\/)?(issues|pull)\/([0-9]+))|(([^\s]+)\/([^\s]+))?#([1-9][0-9]*)($|[\s\:\;\-\(\=])/;323324data = new ExtHostDocumentData(undefined!, URI.file(''), [325perfData._$_$_expensive326], '\n', 1, 'text', false, 'utf8');327328// this test only ensures that we eventually give and timeout (when searching "funny" words and long lines)329// for the sake of speedy tests we lower the timeBudget here330const config = setDefaultGetWordAtTextConfig({ maxLen: 1000, windowSize: 15, timeBudget: 30 });331try {332let range = data.document.getWordRangeAtPosition(new Position(0, 1_177_170), regex)!;333assert.strictEqual(range, undefined);334335const pos = new Position(0, 1177170);336range = data.document.getWordRangeAtPosition(pos)!;337assert.ok(range);338assert.ok(range.contains(pos));339assert.strictEqual(data.document.getText(range), 'TaskDefinition');340341} finally {342config.dispose();343}344});345346test('Rename popup sometimes populates with text on the left side omitted #96013', function () {347348const regex = /(-?\d*\.\d\w*)|([^\`\~\!\@\#\$\%\^\&\*\(\)\-\=\+\[\{\]\}\\\|\;\:\'\"\,\.\<\>\/\?\s]+)/g;349const line = 'int abcdefhijklmnopqwvrstxyz;';350351data = new ExtHostDocumentData(undefined!, URI.file(''), [352line353], '\n', 1, 'text', false, 'utf8');354355const range = data.document.getWordRangeAtPosition(new Position(0, 27), regex)!;356assert.strictEqual(range.start.line, 0);357assert.strictEqual(range.end.line, 0);358assert.strictEqual(range.start.character, 4);359assert.strictEqual(range.end.character, 28);360});361362test('Custom snippet $TM_SELECTED_TEXT not show suggestion #108892', function () {363364data = new ExtHostDocumentData(undefined!, URI.file(''), [365` <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>`366], '\n', 1, 'text', false, 'utf8');367368const pos = new Position(0, 55);369const range = data.document.getWordRangeAtPosition(pos)!;370assert.strictEqual(range.start.line, 0);371assert.strictEqual(range.end.line, 0);372assert.strictEqual(range.start.character, 47);373assert.strictEqual(range.end.character, 61);374assert.strictEqual(data.document.getText(range), 'soprannominato');375});376});377378enum AssertDocumentLineMappingDirection {379OffsetToPosition,380PositionToOffset381}382383suite('ExtHostDocumentData updates line mapping', () => {384385function positionToStr(position: { line: number; character: number }): string {386return '(' + position.line + ',' + position.character + ')';387}388389function assertDocumentLineMapping(doc: ExtHostDocumentData, direction: AssertDocumentLineMappingDirection): void {390const allText = doc.getText();391392let line = 0, character = 0, previousIsCarriageReturn = false;393for (let offset = 0; offset <= allText.length; offset++) {394// The position coordinate system cannot express the position between \r and \n395const position: Position = new Position(line, character + (previousIsCarriageReturn ? -1 : 0));396397if (direction === AssertDocumentLineMappingDirection.OffsetToPosition) {398const actualPosition = doc.document.positionAt(offset);399assert.strictEqual(positionToStr(actualPosition), positionToStr(position), 'positionAt mismatch for offset ' + offset);400} else {401// The position coordinate system cannot express the position between \r and \n402const expectedOffset: number = offset + (previousIsCarriageReturn ? -1 : 0);403const actualOffset = doc.document.offsetAt(position);404assert.strictEqual(actualOffset, expectedOffset, 'offsetAt mismatch for position ' + positionToStr(position));405}406407if (allText.charAt(offset) === '\n') {408line++;409character = 0;410} else {411character++;412}413414previousIsCarriageReturn = (allText.charAt(offset) === '\r');415}416}417418function createChangeEvent(range: Range, text: string, eol?: string): IModelChangedEvent {419return {420changes: [{421range: range,422rangeOffset: undefined!,423rangeLength: undefined!,424text: text425}],426eol: eol!,427versionId: undefined!,428isRedoing: false,429isUndoing: false,430};431}432433function testLineMappingDirectionAfterEvents(lines: string[], eol: string, direction: AssertDocumentLineMappingDirection, e: IModelChangedEvent): void {434const myDocument = new ExtHostDocumentData(undefined!, URI.file(''), lines.slice(0), eol, 1, 'text', false, 'utf8');435assertDocumentLineMapping(myDocument, direction);436437myDocument.onEvents(e);438assertDocumentLineMapping(myDocument, direction);439}440441function testLineMappingAfterEvents(lines: string[], e: IModelChangedEvent): void {442testLineMappingDirectionAfterEvents(lines, '\n', AssertDocumentLineMappingDirection.PositionToOffset, e);443testLineMappingDirectionAfterEvents(lines, '\n', AssertDocumentLineMappingDirection.OffsetToPosition, e);444445testLineMappingDirectionAfterEvents(lines, '\r\n', AssertDocumentLineMappingDirection.PositionToOffset, e);446testLineMappingDirectionAfterEvents(lines, '\r\n', AssertDocumentLineMappingDirection.OffsetToPosition, e);447}448449ensureNoDisposablesAreLeakedInTestSuite();450451test('line mapping', () => {452testLineMappingAfterEvents([453'This is line one',454'and this is line number two',455'it is followed by #3',456'and finished with the fourth.',457], { changes: [], eol: undefined!, versionId: 7, isRedoing: false, isUndoing: false });458});459460test('after remove', () => {461testLineMappingAfterEvents([462'This is line one',463'and this is line number two',464'it is followed by #3',465'and finished with the fourth.',466], createChangeEvent(new Range(1, 3, 1, 6), ''));467});468469test('after replace', () => {470testLineMappingAfterEvents([471'This is line one',472'and this is line number two',473'it is followed by #3',474'and finished with the fourth.',475], createChangeEvent(new Range(1, 3, 1, 6), 'is could be'));476});477478test('after insert line', () => {479testLineMappingAfterEvents([480'This is line one',481'and this is line number two',482'it is followed by #3',483'and finished with the fourth.',484], createChangeEvent(new Range(1, 3, 1, 6), 'is could be\na line with number'));485});486487test('after insert two lines', () => {488testLineMappingAfterEvents([489'This is line one',490'and this is line number two',491'it is followed by #3',492'and finished with the fourth.',493], createChangeEvent(new Range(1, 3, 1, 6), 'is could be\na line with number\nyet another line'));494});495496test('after remove line', () => {497testLineMappingAfterEvents([498'This is line one',499'and this is line number two',500'it is followed by #3',501'and finished with the fourth.',502], createChangeEvent(new Range(1, 3, 2, 6), ''));503});504505test('after remove two lines', () => {506testLineMappingAfterEvents([507'This is line one',508'and this is line number two',509'it is followed by #3',510'and finished with the fourth.',511], createChangeEvent(new Range(1, 3, 3, 6), ''));512});513514test('after deleting entire content', () => {515testLineMappingAfterEvents([516'This is line one',517'and this is line number two',518'it is followed by #3',519'and finished with the fourth.',520], createChangeEvent(new Range(1, 3, 4, 30), ''));521});522523test('after replacing entire content', () => {524testLineMappingAfterEvents([525'This is line one',526'and this is line number two',527'it is followed by #3',528'and finished with the fourth.',529], createChangeEvent(new Range(1, 3, 4, 30), 'some new text\nthat\nspans multiple lines'));530});531532test('after changing EOL to CRLF', () => {533testLineMappingAfterEvents([534'This is line one',535'and this is line number two',536'it is followed by #3',537'and finished with the fourth.',538], createChangeEvent(new Range(1, 1, 1, 1), '', '\r\n'));539});540541test('after changing EOL to LF', () => {542testLineMappingAfterEvents([543'This is line one',544'and this is line number two',545'it is followed by #3',546'and finished with the fourth.',547], createChangeEvent(new Range(1, 1, 1, 1), '', '\n'));548});549});550551552