Path: blob/main/src/vs/editor/test/common/model/model.test.ts
3296 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 { Disposable, DisposableStore, dispose } from '../../../../base/common/lifecycle.js';7import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js';8import { EditOperation } from '../../../common/core/editOperation.js';9import { Position } from '../../../common/core/position.js';10import { Range } from '../../../common/core/range.js';11import { MetadataConsts } from '../../../common/encodedTokenAttributes.js';12import { EncodedTokenizationResult, IState, TokenizationRegistry } from '../../../common/languages.js';13import { ILanguageService } from '../../../common/languages/language.js';14import { ILanguageConfigurationService } from '../../../common/languages/languageConfigurationRegistry.js';15import { NullState } from '../../../common/languages/nullTokenize.js';16import { TextModel } from '../../../common/model/textModel.js';17import { InternalModelContentChangeEvent, ModelRawContentChangedEvent, ModelRawFlush, ModelRawLineChanged, ModelRawLinesDeleted, ModelRawLinesInserted } from '../../../common/textModelEvents.js';18import { createModelServices, createTextModel, instantiateTextModel } from '../testTextModel.js';1920// --------- utils2122const LINE1 = 'My First Line';23const LINE2 = '\t\tMy Second Line';24const LINE3 = ' Third Line';25const LINE4 = '';26const LINE5 = '1';2728suite('Editor Model - Model', () => {2930let thisModel: TextModel;3132setup(() => {33const text =34LINE1 + '\r\n' +35LINE2 + '\n' +36LINE3 + '\n' +37LINE4 + '\r\n' +38LINE5;39thisModel = createTextModel(text);40});4142teardown(() => {43thisModel.dispose();44});4546ensureNoDisposablesAreLeakedInTestSuite();4748// --------- insert text4950test('model getValue', () => {51assert.strictEqual(thisModel.getValue(), 'My First Line\n\t\tMy Second Line\n Third Line\n\n1');52});5354test('model insert empty text', () => {55thisModel.applyEdits([EditOperation.insert(new Position(1, 1), '')]);56assert.strictEqual(thisModel.getLineCount(), 5);57assert.strictEqual(thisModel.getLineContent(1), 'My First Line');58});5960test('model insert text without newline 1', () => {61thisModel.applyEdits([EditOperation.insert(new Position(1, 1), 'foo ')]);62assert.strictEqual(thisModel.getLineCount(), 5);63assert.strictEqual(thisModel.getLineContent(1), 'foo My First Line');64});6566test('model insert text without newline 2', () => {67thisModel.applyEdits([EditOperation.insert(new Position(1, 3), ' foo')]);68assert.strictEqual(thisModel.getLineCount(), 5);69assert.strictEqual(thisModel.getLineContent(1), 'My foo First Line');70});7172test('model insert text with one newline', () => {73thisModel.applyEdits([EditOperation.insert(new Position(1, 3), ' new line\nNo longer')]);74assert.strictEqual(thisModel.getLineCount(), 6);75assert.strictEqual(thisModel.getLineContent(1), 'My new line');76assert.strictEqual(thisModel.getLineContent(2), 'No longer First Line');77});7879test('model insert text with two newlines', () => {80thisModel.applyEdits([EditOperation.insert(new Position(1, 3), ' new line\nOne more line in the middle\nNo longer')]);81assert.strictEqual(thisModel.getLineCount(), 7);82assert.strictEqual(thisModel.getLineContent(1), 'My new line');83assert.strictEqual(thisModel.getLineContent(2), 'One more line in the middle');84assert.strictEqual(thisModel.getLineContent(3), 'No longer First Line');85});8687test('model insert text with many newlines', () => {88thisModel.applyEdits([EditOperation.insert(new Position(1, 3), '\n\n\n\n')]);89assert.strictEqual(thisModel.getLineCount(), 9);90assert.strictEqual(thisModel.getLineContent(1), 'My');91assert.strictEqual(thisModel.getLineContent(2), '');92assert.strictEqual(thisModel.getLineContent(3), '');93assert.strictEqual(thisModel.getLineContent(4), '');94assert.strictEqual(thisModel.getLineContent(5), ' First Line');95});969798// --------- insert text eventing99100test('model insert empty text does not trigger eventing', () => {101const disposable = thisModel.onDidChangeContentOrInjectedText((e) => {102assert.ok(false, 'was not expecting event');103});104thisModel.applyEdits([EditOperation.insert(new Position(1, 1), '')]);105disposable.dispose();106});107108test('model insert text without newline eventing', () => {109let e: ModelRawContentChangedEvent | null = null;110const disposable = thisModel.onDidChangeContentOrInjectedText((_e) => {111if (e !== null || !(_e instanceof InternalModelContentChangeEvent)) {112assert.fail('Unexpected assertion error');113}114e = _e.rawContentChangedEvent;115});116thisModel.applyEdits([EditOperation.insert(new Position(1, 1), 'foo ')]);117assert.deepStrictEqual(e, new ModelRawContentChangedEvent(118[119new ModelRawLineChanged(1, 'foo My First Line', null)120],1212,122false,123false124));125disposable.dispose();126});127128test('model insert text with one newline eventing', () => {129let e: ModelRawContentChangedEvent | null = null;130const disposable = thisModel.onDidChangeContentOrInjectedText((_e) => {131if (e !== null || !(_e instanceof InternalModelContentChangeEvent)) {132assert.fail('Unexpected assertion error');133}134e = _e.rawContentChangedEvent;135});136thisModel.applyEdits([EditOperation.insert(new Position(1, 3), ' new line\nNo longer')]);137assert.deepStrictEqual(e, new ModelRawContentChangedEvent(138[139new ModelRawLineChanged(1, 'My new line', null),140new ModelRawLinesInserted(2, 2, ['No longer First Line'], [null]),141],1422,143false,144false145));146disposable.dispose();147});148149150// --------- delete text151152test('model delete empty text', () => {153thisModel.applyEdits([EditOperation.delete(new Range(1, 1, 1, 1))]);154assert.strictEqual(thisModel.getLineCount(), 5);155assert.strictEqual(thisModel.getLineContent(1), 'My First Line');156});157158test('model delete text from one line', () => {159thisModel.applyEdits([EditOperation.delete(new Range(1, 1, 1, 2))]);160assert.strictEqual(thisModel.getLineCount(), 5);161assert.strictEqual(thisModel.getLineContent(1), 'y First Line');162});163164test('model delete text from one line 2', () => {165thisModel.applyEdits([EditOperation.insert(new Position(1, 1), 'a')]);166assert.strictEqual(thisModel.getLineContent(1), 'aMy First Line');167168thisModel.applyEdits([EditOperation.delete(new Range(1, 2, 1, 4))]);169assert.strictEqual(thisModel.getLineCount(), 5);170assert.strictEqual(thisModel.getLineContent(1), 'a First Line');171});172173test('model delete all text from a line', () => {174thisModel.applyEdits([EditOperation.delete(new Range(1, 1, 1, 14))]);175assert.strictEqual(thisModel.getLineCount(), 5);176assert.strictEqual(thisModel.getLineContent(1), '');177});178179test('model delete text from two lines', () => {180thisModel.applyEdits([EditOperation.delete(new Range(1, 4, 2, 6))]);181assert.strictEqual(thisModel.getLineCount(), 4);182assert.strictEqual(thisModel.getLineContent(1), 'My Second Line');183});184185test('model delete text from many lines', () => {186thisModel.applyEdits([EditOperation.delete(new Range(1, 4, 3, 5))]);187assert.strictEqual(thisModel.getLineCount(), 3);188assert.strictEqual(thisModel.getLineContent(1), 'My Third Line');189});190191test('model delete everything', () => {192thisModel.applyEdits([EditOperation.delete(new Range(1, 1, 5, 2))]);193assert.strictEqual(thisModel.getLineCount(), 1);194assert.strictEqual(thisModel.getLineContent(1), '');195});196197// --------- delete text eventing198199test('model delete empty text does not trigger eventing', () => {200const disposable = thisModel.onDidChangeContentOrInjectedText((e) => {201assert.ok(false, 'was not expecting event');202});203thisModel.applyEdits([EditOperation.delete(new Range(1, 1, 1, 1))]);204disposable.dispose();205});206207test('model delete text from one line eventing', () => {208let e: ModelRawContentChangedEvent | null = null;209const disposable = thisModel.onDidChangeContentOrInjectedText((_e) => {210if (e !== null || !(_e instanceof InternalModelContentChangeEvent)) {211assert.fail('Unexpected assertion error');212}213e = _e.rawContentChangedEvent;214});215thisModel.applyEdits([EditOperation.delete(new Range(1, 1, 1, 2))]);216assert.deepStrictEqual(e, new ModelRawContentChangedEvent(217[218new ModelRawLineChanged(1, 'y First Line', null),219],2202,221false,222false223));224disposable.dispose();225});226227test('model delete all text from a line eventing', () => {228let e: ModelRawContentChangedEvent | null = null;229const disposable = thisModel.onDidChangeContentOrInjectedText((_e) => {230if (e !== null || !(_e instanceof InternalModelContentChangeEvent)) {231assert.fail('Unexpected assertion error');232}233e = _e.rawContentChangedEvent;234});235thisModel.applyEdits([EditOperation.delete(new Range(1, 1, 1, 14))]);236assert.deepStrictEqual(e, new ModelRawContentChangedEvent(237[238new ModelRawLineChanged(1, '', null),239],2402,241false,242false243));244disposable.dispose();245});246247test('model delete text from two lines eventing', () => {248let e: ModelRawContentChangedEvent | null = null;249const disposable = thisModel.onDidChangeContentOrInjectedText((_e) => {250if (e !== null || !(_e instanceof InternalModelContentChangeEvent)) {251assert.fail('Unexpected assertion error');252}253e = _e.rawContentChangedEvent;254});255thisModel.applyEdits([EditOperation.delete(new Range(1, 4, 2, 6))]);256assert.deepStrictEqual(e, new ModelRawContentChangedEvent(257[258new ModelRawLineChanged(1, 'My Second Line', null),259new ModelRawLinesDeleted(2, 2),260],2612,262false,263false264));265disposable.dispose();266});267268test('model delete text from many lines eventing', () => {269let e: ModelRawContentChangedEvent | null = null;270const disposable = thisModel.onDidChangeContentOrInjectedText((_e) => {271if (e !== null || !(_e instanceof InternalModelContentChangeEvent)) {272assert.fail('Unexpected assertion error');273}274e = _e.rawContentChangedEvent;275});276thisModel.applyEdits([EditOperation.delete(new Range(1, 4, 3, 5))]);277assert.deepStrictEqual(e, new ModelRawContentChangedEvent(278[279new ModelRawLineChanged(1, 'My Third Line', null),280new ModelRawLinesDeleted(2, 3),281],2822,283false,284false285));286disposable.dispose();287});288289// --------- getValueInRange290291test('getValueInRange', () => {292assert.strictEqual(thisModel.getValueInRange(new Range(1, 1, 1, 1)), '');293assert.strictEqual(thisModel.getValueInRange(new Range(1, 1, 1, 2)), 'M');294assert.strictEqual(thisModel.getValueInRange(new Range(1, 2, 1, 3)), 'y');295assert.strictEqual(thisModel.getValueInRange(new Range(1, 1, 1, 14)), 'My First Line');296assert.strictEqual(thisModel.getValueInRange(new Range(1, 1, 2, 1)), 'My First Line\n');297assert.strictEqual(thisModel.getValueInRange(new Range(1, 1, 2, 2)), 'My First Line\n\t');298assert.strictEqual(thisModel.getValueInRange(new Range(1, 1, 2, 3)), 'My First Line\n\t\t');299assert.strictEqual(thisModel.getValueInRange(new Range(1, 1, 2, 17)), 'My First Line\n\t\tMy Second Line');300assert.strictEqual(thisModel.getValueInRange(new Range(1, 1, 3, 1)), 'My First Line\n\t\tMy Second Line\n');301assert.strictEqual(thisModel.getValueInRange(new Range(1, 1, 4, 1)), 'My First Line\n\t\tMy Second Line\n Third Line\n');302});303304// --------- getValueLengthInRange305306test('getValueLengthInRange', () => {307assert.strictEqual(thisModel.getValueLengthInRange(new Range(1, 1, 1, 1)), ''.length);308assert.strictEqual(thisModel.getValueLengthInRange(new Range(1, 1, 1, 2)), 'M'.length);309assert.strictEqual(thisModel.getValueLengthInRange(new Range(1, 2, 1, 3)), 'y'.length);310assert.strictEqual(thisModel.getValueLengthInRange(new Range(1, 1, 1, 14)), 'My First Line'.length);311assert.strictEqual(thisModel.getValueLengthInRange(new Range(1, 1, 2, 1)), 'My First Line\n'.length);312assert.strictEqual(thisModel.getValueLengthInRange(new Range(1, 1, 2, 2)), 'My First Line\n\t'.length);313assert.strictEqual(thisModel.getValueLengthInRange(new Range(1, 1, 2, 3)), 'My First Line\n\t\t'.length);314assert.strictEqual(thisModel.getValueLengthInRange(new Range(1, 1, 2, 17)), 'My First Line\n\t\tMy Second Line'.length);315assert.strictEqual(thisModel.getValueLengthInRange(new Range(1, 1, 3, 1)), 'My First Line\n\t\tMy Second Line\n'.length);316assert.strictEqual(thisModel.getValueLengthInRange(new Range(1, 1, 4, 1)), 'My First Line\n\t\tMy Second Line\n Third Line\n'.length);317});318319// --------- setValue320test('setValue eventing', () => {321let e: ModelRawContentChangedEvent | null = null;322const disposable = thisModel.onDidChangeContentOrInjectedText((_e) => {323if (e !== null || !(_e instanceof InternalModelContentChangeEvent)) {324assert.fail('Unexpected assertion error');325}326e = _e.rawContentChangedEvent;327});328thisModel.setValue('new value');329assert.deepStrictEqual(e, new ModelRawContentChangedEvent(330[331new ModelRawFlush()332],3332,334false,335false336));337disposable.dispose();338});339340test('issue #46342: Maintain edit operation order in applyEdits', () => {341const res = thisModel.applyEdits([342{ range: new Range(2, 1, 2, 1), text: 'a' },343{ range: new Range(1, 1, 1, 1), text: 'b' },344], true);345346assert.deepStrictEqual(res[0].range, new Range(2, 1, 2, 2));347assert.deepStrictEqual(res[1].range, new Range(1, 1, 1, 2));348});349});350351352// --------- Special Unicode LINE SEPARATOR character353suite('Editor Model - Model Line Separators', () => {354355let thisModel: TextModel;356357setup(() => {358const text =359LINE1 + '\u2028' +360LINE2 + '\n' +361LINE3 + '\u2028' +362LINE4 + '\r\n' +363LINE5;364thisModel = createTextModel(text);365});366367teardown(() => {368thisModel.dispose();369});370371ensureNoDisposablesAreLeakedInTestSuite();372373test('model getValue', () => {374assert.strictEqual(thisModel.getValue(), 'My First Line\u2028\t\tMy Second Line\n Third Line\u2028\n1');375});376377test('model lines', () => {378assert.strictEqual(thisModel.getLineCount(), 3);379});380381test('Bug 13333:Model should line break on lonely CR too', () => {382const model = createTextModel('Hello\rWorld!\r\nAnother line');383assert.strictEqual(model.getLineCount(), 3);384assert.strictEqual(model.getValue(), 'Hello\r\nWorld!\r\nAnother line');385model.dispose();386});387});388389390// --------- Words391392suite('Editor Model - Words', () => {393394const OUTER_LANGUAGE_ID = 'outerMode';395const INNER_LANGUAGE_ID = 'innerMode';396397class OuterMode extends Disposable {398399public readonly languageId = OUTER_LANGUAGE_ID;400401constructor(402@ILanguageService languageService: ILanguageService,403@ILanguageConfigurationService languageConfigurationService: ILanguageConfigurationService404) {405super();406this._register(languageService.registerLanguage({ id: this.languageId }));407this._register(languageConfigurationService.register(this.languageId, {}));408409const languageIdCodec = languageService.languageIdCodec;410this._register(TokenizationRegistry.register(this.languageId, {411getInitialState: (): IState => NullState,412tokenize: undefined!,413tokenizeEncoded: (line: string, hasEOL: boolean, state: IState): EncodedTokenizationResult => {414const tokensArr: number[] = [];415let prevLanguageId: string | undefined = undefined;416for (let i = 0; i < line.length; i++) {417const languageId = (line.charAt(i) === 'x' ? INNER_LANGUAGE_ID : OUTER_LANGUAGE_ID);418const encodedLanguageId = languageIdCodec.encodeLanguageId(languageId);419if (prevLanguageId !== languageId) {420tokensArr.push(i);421tokensArr.push((encodedLanguageId << MetadataConsts.LANGUAGEID_OFFSET));422}423prevLanguageId = languageId;424}425426const tokens = new Uint32Array(tokensArr.length);427for (let i = 0; i < tokens.length; i++) {428tokens[i] = tokensArr[i];429}430return new EncodedTokenizationResult(tokens, state);431}432}));433}434}435436class InnerMode extends Disposable {437438public readonly languageId = INNER_LANGUAGE_ID;439440constructor(441@ILanguageService languageService: ILanguageService,442@ILanguageConfigurationService languageConfigurationService: ILanguageConfigurationService443) {444super();445this._register(languageService.registerLanguage({ id: this.languageId }));446this._register(languageConfigurationService.register(this.languageId, {}));447}448}449450let disposables: Disposable[] = [];451452setup(() => {453disposables = [];454});455456teardown(() => {457dispose(disposables);458disposables = [];459});460461ensureNoDisposablesAreLeakedInTestSuite();462463test('Get word at position', () => {464const text = ['This text has some words. '];465const thisModel = createTextModel(text.join('\n'));466disposables.push(thisModel);467468assert.deepStrictEqual(thisModel.getWordAtPosition(new Position(1, 1)), { word: 'This', startColumn: 1, endColumn: 5 });469assert.deepStrictEqual(thisModel.getWordAtPosition(new Position(1, 2)), { word: 'This', startColumn: 1, endColumn: 5 });470assert.deepStrictEqual(thisModel.getWordAtPosition(new Position(1, 4)), { word: 'This', startColumn: 1, endColumn: 5 });471assert.deepStrictEqual(thisModel.getWordAtPosition(new Position(1, 5)), { word: 'This', startColumn: 1, endColumn: 5 });472assert.deepStrictEqual(thisModel.getWordAtPosition(new Position(1, 6)), { word: 'text', startColumn: 6, endColumn: 10 });473assert.deepStrictEqual(thisModel.getWordAtPosition(new Position(1, 19)), { word: 'some', startColumn: 15, endColumn: 19 });474assert.deepStrictEqual(thisModel.getWordAtPosition(new Position(1, 20)), null);475assert.deepStrictEqual(thisModel.getWordAtPosition(new Position(1, 21)), { word: 'words', startColumn: 21, endColumn: 26 });476assert.deepStrictEqual(thisModel.getWordAtPosition(new Position(1, 26)), { word: 'words', startColumn: 21, endColumn: 26 });477assert.deepStrictEqual(thisModel.getWordAtPosition(new Position(1, 27)), null);478assert.deepStrictEqual(thisModel.getWordAtPosition(new Position(1, 28)), null);479});480481test('getWordAtPosition at embedded language boundaries', () => {482const disposables = new DisposableStore();483const instantiationService = createModelServices(disposables);484const outerMode = disposables.add(instantiationService.createInstance(OuterMode));485disposables.add(instantiationService.createInstance(InnerMode));486487const model = disposables.add(instantiateTextModel(instantiationService, 'ab<xx>ab<x>', outerMode.languageId));488489assert.deepStrictEqual(model.getWordAtPosition(new Position(1, 1)), { word: 'ab', startColumn: 1, endColumn: 3 });490assert.deepStrictEqual(model.getWordAtPosition(new Position(1, 2)), { word: 'ab', startColumn: 1, endColumn: 3 });491assert.deepStrictEqual(model.getWordAtPosition(new Position(1, 3)), { word: 'ab', startColumn: 1, endColumn: 3 });492assert.deepStrictEqual(model.getWordAtPosition(new Position(1, 4)), { word: 'xx', startColumn: 4, endColumn: 6 });493assert.deepStrictEqual(model.getWordAtPosition(new Position(1, 5)), { word: 'xx', startColumn: 4, endColumn: 6 });494assert.deepStrictEqual(model.getWordAtPosition(new Position(1, 6)), { word: 'xx', startColumn: 4, endColumn: 6 });495assert.deepStrictEqual(model.getWordAtPosition(new Position(1, 7)), { word: 'ab', startColumn: 7, endColumn: 9 });496497disposables.dispose();498});499500test('issue #61296: VS code freezes when editing CSS file with emoji', () => {501const MODE_ID = 'testMode';502const disposables = new DisposableStore();503const instantiationService = createModelServices(disposables);504const languageConfigurationService = instantiationService.get(ILanguageConfigurationService);505const languageService = instantiationService.get(ILanguageService);506507disposables.add(languageService.registerLanguage({ id: MODE_ID }));508disposables.add(languageConfigurationService.register(MODE_ID, {509wordPattern: /(#?-?\d*\.\d\w*%?)|(::?[\w-]*(?=[^,{;]*[,{]))|(([@#.!])?[\w-?]+%?|[@#!.])/g510}));511512const thisModel = disposables.add(instantiateTextModel(instantiationService, '.🐷-a-b', MODE_ID));513514assert.deepStrictEqual(thisModel.getWordAtPosition(new Position(1, 1)), { word: '.', startColumn: 1, endColumn: 2 });515assert.deepStrictEqual(thisModel.getWordAtPosition(new Position(1, 2)), { word: '.', startColumn: 1, endColumn: 2 });516assert.deepStrictEqual(thisModel.getWordAtPosition(new Position(1, 3)), null);517assert.deepStrictEqual(thisModel.getWordAtPosition(new Position(1, 4)), { word: '-a-b', startColumn: 4, endColumn: 8 });518assert.deepStrictEqual(thisModel.getWordAtPosition(new Position(1, 5)), { word: '-a-b', startColumn: 4, endColumn: 8 });519assert.deepStrictEqual(thisModel.getWordAtPosition(new Position(1, 6)), { word: '-a-b', startColumn: 4, endColumn: 8 });520assert.deepStrictEqual(thisModel.getWordAtPosition(new Position(1, 7)), { word: '-a-b', startColumn: 4, endColumn: 8 });521assert.deepStrictEqual(thisModel.getWordAtPosition(new Position(1, 8)), { word: '-a-b', startColumn: 4, endColumn: 8 });522523disposables.dispose();524});525});526527528