Path: blob/main/src/vs/editor/test/common/model/model.test.ts
5242 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, ModelInjectedTextChangedEvent, ModelRawContentChangedEvent, ModelRawFlush, ModelRawLineChanged, ModelRawLinesDeleted, ModelRawLinesInserted } from '../../../common/textModelEvents.js';18import { createModelServices, createTextModel, instantiateTextModel } from '../testTextModel.js';19import { mock } from '../../../../base/test/common/mock.js';20import { IViewModel } from '../../../common/viewModel.js';2122// --------- utils2324const LINE1 = 'My First Line';25const LINE2 = '\t\tMy Second Line';26const LINE3 = ' Third Line';27const LINE4 = '';28const LINE5 = '1';2930suite('Editor Model - Model', () => {3132let thisModel: TextModel;3334setup(() => {35const text =36LINE1 + '\r\n' +37LINE2 + '\n' +38LINE3 + '\n' +39LINE4 + '\r\n' +40LINE5;41thisModel = createTextModel(text);42});4344teardown(() => {45thisModel.dispose();46});4748ensureNoDisposablesAreLeakedInTestSuite();4950// --------- insert text5152test('model getValue', () => {53assert.strictEqual(thisModel.getValue(), 'My First Line\n\t\tMy Second Line\n Third Line\n\n1');54});5556test('model insert empty text', () => {57thisModel.applyEdits([EditOperation.insert(new Position(1, 1), '')]);58assert.strictEqual(thisModel.getLineCount(), 5);59assert.strictEqual(thisModel.getLineContent(1), 'My First Line');60});6162test('model insert text without newline 1', () => {63thisModel.applyEdits([EditOperation.insert(new Position(1, 1), 'foo ')]);64assert.strictEqual(thisModel.getLineCount(), 5);65assert.strictEqual(thisModel.getLineContent(1), 'foo My First Line');66});6768test('model insert text without newline 2', () => {69thisModel.applyEdits([EditOperation.insert(new Position(1, 3), ' foo')]);70assert.strictEqual(thisModel.getLineCount(), 5);71assert.strictEqual(thisModel.getLineContent(1), 'My foo First Line');72});7374test('model insert text with one newline', () => {75thisModel.applyEdits([EditOperation.insert(new Position(1, 3), ' new line\nNo longer')]);76assert.strictEqual(thisModel.getLineCount(), 6);77assert.strictEqual(thisModel.getLineContent(1), 'My new line');78assert.strictEqual(thisModel.getLineContent(2), 'No longer First Line');79});8081test('model insert text with two newlines', () => {82thisModel.applyEdits([EditOperation.insert(new Position(1, 3), ' new line\nOne more line in the middle\nNo longer')]);83assert.strictEqual(thisModel.getLineCount(), 7);84assert.strictEqual(thisModel.getLineContent(1), 'My new line');85assert.strictEqual(thisModel.getLineContent(2), 'One more line in the middle');86assert.strictEqual(thisModel.getLineContent(3), 'No longer First Line');87});8889test('model insert text with many newlines', () => {90thisModel.applyEdits([EditOperation.insert(new Position(1, 3), '\n\n\n\n')]);91assert.strictEqual(thisModel.getLineCount(), 9);92assert.strictEqual(thisModel.getLineContent(1), 'My');93assert.strictEqual(thisModel.getLineContent(2), '');94assert.strictEqual(thisModel.getLineContent(3), '');95assert.strictEqual(thisModel.getLineContent(4), '');96assert.strictEqual(thisModel.getLineContent(5), ' First Line');97});9899100// --------- insert text eventing101102function withEventCapturing(callback: () => void): ModelRawContentChangedEvent | null {103let e: ModelRawContentChangedEvent | null = null;104const spyViewModel = new class extends mock<IViewModel>() {105override onDidChangeContentOrInjectedText(_e: InternalModelContentChangeEvent | ModelInjectedTextChangedEvent) {106if (e !== null || !(_e instanceof InternalModelContentChangeEvent)) {107assert.fail('Unexpected assertion error');108}109e = _e.rawContentChangedEvent;110}111override emitContentChangeEvent(e: InternalModelContentChangeEvent | ModelInjectedTextChangedEvent): void { }112};113thisModel.registerViewModel(spyViewModel);114callback();115thisModel.unregisterViewModel(spyViewModel);116return e;117}118119test('model insert empty text does not trigger eventing', () => {120const e = withEventCapturing(() => {121thisModel.applyEdits([EditOperation.insert(new Position(1, 1), '')]);122});123assert.deepStrictEqual(e, null, 'was not expecting event');124});125126test('model insert text without newline eventing', () => {127const e = withEventCapturing(() => {128thisModel.applyEdits([EditOperation.insert(new Position(1, 1), 'foo ')]);129});130assert.deepStrictEqual(e, new ModelRawContentChangedEvent(131[132new ModelRawLineChanged(1, 'foo My First Line', null)133],1342,135false,136false137));138});139140test('model insert text with one newline eventing', () => {141const e = withEventCapturing(() => {142thisModel.applyEdits([EditOperation.insert(new Position(1, 3), ' new line\nNo longer')]);143});144assert.deepStrictEqual(e, new ModelRawContentChangedEvent(145[146new ModelRawLineChanged(1, 'My new line', null),147new ModelRawLinesInserted(2, 2, ['No longer First Line'], [null]),148],1492,150false,151false152));153});154155156// --------- delete text157158test('model delete empty text', () => {159thisModel.applyEdits([EditOperation.delete(new Range(1, 1, 1, 1))]);160assert.strictEqual(thisModel.getLineCount(), 5);161assert.strictEqual(thisModel.getLineContent(1), 'My First Line');162});163164test('model delete text from one line', () => {165thisModel.applyEdits([EditOperation.delete(new Range(1, 1, 1, 2))]);166assert.strictEqual(thisModel.getLineCount(), 5);167assert.strictEqual(thisModel.getLineContent(1), 'y First Line');168});169170test('model delete text from one line 2', () => {171thisModel.applyEdits([EditOperation.insert(new Position(1, 1), 'a')]);172assert.strictEqual(thisModel.getLineContent(1), 'aMy First Line');173174thisModel.applyEdits([EditOperation.delete(new Range(1, 2, 1, 4))]);175assert.strictEqual(thisModel.getLineCount(), 5);176assert.strictEqual(thisModel.getLineContent(1), 'a First Line');177});178179test('model delete all text from a line', () => {180thisModel.applyEdits([EditOperation.delete(new Range(1, 1, 1, 14))]);181assert.strictEqual(thisModel.getLineCount(), 5);182assert.strictEqual(thisModel.getLineContent(1), '');183});184185test('model delete text from two lines', () => {186thisModel.applyEdits([EditOperation.delete(new Range(1, 4, 2, 6))]);187assert.strictEqual(thisModel.getLineCount(), 4);188assert.strictEqual(thisModel.getLineContent(1), 'My Second Line');189});190191test('model delete text from many lines', () => {192thisModel.applyEdits([EditOperation.delete(new Range(1, 4, 3, 5))]);193assert.strictEqual(thisModel.getLineCount(), 3);194assert.strictEqual(thisModel.getLineContent(1), 'My Third Line');195});196197test('model delete everything', () => {198thisModel.applyEdits([EditOperation.delete(new Range(1, 1, 5, 2))]);199assert.strictEqual(thisModel.getLineCount(), 1);200assert.strictEqual(thisModel.getLineContent(1), '');201});202203// --------- delete text eventing204205test('model delete empty text does not trigger eventing', () => {206const e = withEventCapturing(() => {207thisModel.applyEdits([EditOperation.delete(new Range(1, 1, 1, 1))]);208});209assert.deepStrictEqual(e, null, 'was not expecting event');210});211212test('model delete text from one line eventing', () => {213const e = withEventCapturing(() => {214thisModel.applyEdits([EditOperation.delete(new Range(1, 1, 1, 2))]);215});216assert.deepStrictEqual(e, new ModelRawContentChangedEvent(217[218new ModelRawLineChanged(1, 'y First Line', null),219],2202,221false,222false223));224});225226test('model delete all text from a line eventing', () => {227const e = withEventCapturing(() => {228thisModel.applyEdits([EditOperation.delete(new Range(1, 1, 1, 14))]);229});230assert.deepStrictEqual(e, new ModelRawContentChangedEvent(231[232new ModelRawLineChanged(1, '', null),233],2342,235false,236false237));238});239240test('model delete text from two lines eventing', () => {241const e = withEventCapturing(() => {242thisModel.applyEdits([EditOperation.delete(new Range(1, 4, 2, 6))]);243});244assert.deepStrictEqual(e, new ModelRawContentChangedEvent(245[246new ModelRawLineChanged(1, 'My Second Line', null),247new ModelRawLinesDeleted(2, 2),248],2492,250false,251false252));253});254255test('model delete text from many lines eventing', () => {256const e = withEventCapturing(() => {257thisModel.applyEdits([EditOperation.delete(new Range(1, 4, 3, 5))]);258});259assert.deepStrictEqual(e, new ModelRawContentChangedEvent(260[261new ModelRawLineChanged(1, 'My Third Line', null),262new ModelRawLinesDeleted(2, 3),263],2642,265false,266false267));268});269270// --------- getValueInRange271272test('getValueInRange', () => {273assert.strictEqual(thisModel.getValueInRange(new Range(1, 1, 1, 1)), '');274assert.strictEqual(thisModel.getValueInRange(new Range(1, 1, 1, 2)), 'M');275assert.strictEqual(thisModel.getValueInRange(new Range(1, 2, 1, 3)), 'y');276assert.strictEqual(thisModel.getValueInRange(new Range(1, 1, 1, 14)), 'My First Line');277assert.strictEqual(thisModel.getValueInRange(new Range(1, 1, 2, 1)), 'My First Line\n');278assert.strictEqual(thisModel.getValueInRange(new Range(1, 1, 2, 2)), 'My First Line\n\t');279assert.strictEqual(thisModel.getValueInRange(new Range(1, 1, 2, 3)), 'My First Line\n\t\t');280assert.strictEqual(thisModel.getValueInRange(new Range(1, 1, 2, 17)), 'My First Line\n\t\tMy Second Line');281assert.strictEqual(thisModel.getValueInRange(new Range(1, 1, 3, 1)), 'My First Line\n\t\tMy Second Line\n');282assert.strictEqual(thisModel.getValueInRange(new Range(1, 1, 4, 1)), 'My First Line\n\t\tMy Second Line\n Third Line\n');283});284285// --------- getValueLengthInRange286287test('getValueLengthInRange', () => {288assert.strictEqual(thisModel.getValueLengthInRange(new Range(1, 1, 1, 1)), ''.length);289assert.strictEqual(thisModel.getValueLengthInRange(new Range(1, 1, 1, 2)), 'M'.length);290assert.strictEqual(thisModel.getValueLengthInRange(new Range(1, 2, 1, 3)), 'y'.length);291assert.strictEqual(thisModel.getValueLengthInRange(new Range(1, 1, 1, 14)), 'My First Line'.length);292assert.strictEqual(thisModel.getValueLengthInRange(new Range(1, 1, 2, 1)), 'My First Line\n'.length);293assert.strictEqual(thisModel.getValueLengthInRange(new Range(1, 1, 2, 2)), 'My First Line\n\t'.length);294assert.strictEqual(thisModel.getValueLengthInRange(new Range(1, 1, 2, 3)), 'My First Line\n\t\t'.length);295assert.strictEqual(thisModel.getValueLengthInRange(new Range(1, 1, 2, 17)), 'My First Line\n\t\tMy Second Line'.length);296assert.strictEqual(thisModel.getValueLengthInRange(new Range(1, 1, 3, 1)), 'My First Line\n\t\tMy Second Line\n'.length);297assert.strictEqual(thisModel.getValueLengthInRange(new Range(1, 1, 4, 1)), 'My First Line\n\t\tMy Second Line\n Third Line\n'.length);298});299300// --------- setValue301test('setValue eventing', () => {302const e = withEventCapturing(() => {303thisModel.setValue('new value');304});305assert.deepStrictEqual(e, new ModelRawContentChangedEvent(306[307new ModelRawFlush()308],3092,310false,311false312));313});314315test('issue #46342: Maintain edit operation order in applyEdits', () => {316const res = thisModel.applyEdits([317{ range: new Range(2, 1, 2, 1), text: 'a' },318{ range: new Range(1, 1, 1, 1), text: 'b' },319], true);320321assert.deepStrictEqual(res[0].range, new Range(2, 1, 2, 2));322assert.deepStrictEqual(res[1].range, new Range(1, 1, 1, 2));323});324});325326327// --------- Special Unicode LINE SEPARATOR character328suite('Editor Model - Model Line Separators', () => {329330let thisModel: TextModel;331332setup(() => {333const text =334LINE1 + '\u2028' +335LINE2 + '\n' +336LINE3 + '\u2028' +337LINE4 + '\r\n' +338LINE5;339thisModel = createTextModel(text);340});341342teardown(() => {343thisModel.dispose();344});345346ensureNoDisposablesAreLeakedInTestSuite();347348test('model getValue', () => {349assert.strictEqual(thisModel.getValue(), 'My First Line\u2028\t\tMy Second Line\n Third Line\u2028\n1');350});351352test('model lines', () => {353assert.strictEqual(thisModel.getLineCount(), 3);354});355356test('Bug 13333:Model should line break on lonely CR too', () => {357const model = createTextModel('Hello\rWorld!\r\nAnother line');358assert.strictEqual(model.getLineCount(), 3);359assert.strictEqual(model.getValue(), 'Hello\r\nWorld!\r\nAnother line');360model.dispose();361});362});363364365// --------- Words366367suite('Editor Model - Words', () => {368369const OUTER_LANGUAGE_ID = 'outerMode';370const INNER_LANGUAGE_ID = 'innerMode';371372class OuterMode extends Disposable {373374public readonly languageId = OUTER_LANGUAGE_ID;375376constructor(377@ILanguageService languageService: ILanguageService,378@ILanguageConfigurationService languageConfigurationService: ILanguageConfigurationService379) {380super();381this._register(languageService.registerLanguage({ id: this.languageId }));382this._register(languageConfigurationService.register(this.languageId, {}));383384const languageIdCodec = languageService.languageIdCodec;385this._register(TokenizationRegistry.register(this.languageId, {386getInitialState: (): IState => NullState,387tokenize: undefined!,388tokenizeEncoded: (line: string, hasEOL: boolean, state: IState): EncodedTokenizationResult => {389const tokensArr: number[] = [];390let prevLanguageId: string | undefined = undefined;391for (let i = 0; i < line.length; i++) {392const languageId = (line.charAt(i) === 'x' ? INNER_LANGUAGE_ID : OUTER_LANGUAGE_ID);393const encodedLanguageId = languageIdCodec.encodeLanguageId(languageId);394if (prevLanguageId !== languageId) {395tokensArr.push(i);396tokensArr.push((encodedLanguageId << MetadataConsts.LANGUAGEID_OFFSET));397}398prevLanguageId = languageId;399}400401const tokens = new Uint32Array(tokensArr.length);402for (let i = 0; i < tokens.length; i++) {403tokens[i] = tokensArr[i];404}405return new EncodedTokenizationResult(tokens, [], state);406}407}));408}409}410411class InnerMode extends Disposable {412413public readonly languageId = INNER_LANGUAGE_ID;414415constructor(416@ILanguageService languageService: ILanguageService,417@ILanguageConfigurationService languageConfigurationService: ILanguageConfigurationService418) {419super();420this._register(languageService.registerLanguage({ id: this.languageId }));421this._register(languageConfigurationService.register(this.languageId, {}));422}423}424425let disposables: Disposable[] = [];426427setup(() => {428disposables = [];429});430431teardown(() => {432dispose(disposables);433disposables = [];434});435436ensureNoDisposablesAreLeakedInTestSuite();437438test('Get word at position', () => {439const text = ['This text has some words. '];440const thisModel = createTextModel(text.join('\n'));441disposables.push(thisModel);442443assert.deepStrictEqual(thisModel.getWordAtPosition(new Position(1, 1)), { word: 'This', startColumn: 1, endColumn: 5 });444assert.deepStrictEqual(thisModel.getWordAtPosition(new Position(1, 2)), { word: 'This', startColumn: 1, endColumn: 5 });445assert.deepStrictEqual(thisModel.getWordAtPosition(new Position(1, 4)), { word: 'This', startColumn: 1, endColumn: 5 });446assert.deepStrictEqual(thisModel.getWordAtPosition(new Position(1, 5)), { word: 'This', startColumn: 1, endColumn: 5 });447assert.deepStrictEqual(thisModel.getWordAtPosition(new Position(1, 6)), { word: 'text', startColumn: 6, endColumn: 10 });448assert.deepStrictEqual(thisModel.getWordAtPosition(new Position(1, 19)), { word: 'some', startColumn: 15, endColumn: 19 });449assert.deepStrictEqual(thisModel.getWordAtPosition(new Position(1, 20)), null);450assert.deepStrictEqual(thisModel.getWordAtPosition(new Position(1, 21)), { word: 'words', startColumn: 21, endColumn: 26 });451assert.deepStrictEqual(thisModel.getWordAtPosition(new Position(1, 26)), { word: 'words', startColumn: 21, endColumn: 26 });452assert.deepStrictEqual(thisModel.getWordAtPosition(new Position(1, 27)), null);453assert.deepStrictEqual(thisModel.getWordAtPosition(new Position(1, 28)), null);454});455456test('getWordAtPosition at embedded language boundaries', () => {457const disposables = new DisposableStore();458const instantiationService = createModelServices(disposables);459const outerMode = disposables.add(instantiationService.createInstance(OuterMode));460disposables.add(instantiationService.createInstance(InnerMode));461462const model = disposables.add(instantiateTextModel(instantiationService, 'ab<xx>ab<x>', outerMode.languageId));463464assert.deepStrictEqual(model.getWordAtPosition(new Position(1, 1)), { word: 'ab', startColumn: 1, endColumn: 3 });465assert.deepStrictEqual(model.getWordAtPosition(new Position(1, 2)), { word: 'ab', startColumn: 1, endColumn: 3 });466assert.deepStrictEqual(model.getWordAtPosition(new Position(1, 3)), { word: 'ab', startColumn: 1, endColumn: 3 });467assert.deepStrictEqual(model.getWordAtPosition(new Position(1, 4)), { word: 'xx', startColumn: 4, endColumn: 6 });468assert.deepStrictEqual(model.getWordAtPosition(new Position(1, 5)), { word: 'xx', startColumn: 4, endColumn: 6 });469assert.deepStrictEqual(model.getWordAtPosition(new Position(1, 6)), { word: 'xx', startColumn: 4, endColumn: 6 });470assert.deepStrictEqual(model.getWordAtPosition(new Position(1, 7)), { word: 'ab', startColumn: 7, endColumn: 9 });471472disposables.dispose();473});474475test('issue #61296: VS code freezes when editing CSS file with emoji', () => {476const MODE_ID = 'testMode';477const disposables = new DisposableStore();478const instantiationService = createModelServices(disposables);479const languageConfigurationService = instantiationService.get(ILanguageConfigurationService);480const languageService = instantiationService.get(ILanguageService);481482disposables.add(languageService.registerLanguage({ id: MODE_ID }));483disposables.add(languageConfigurationService.register(MODE_ID, {484wordPattern: /(#?-?\d*\.\d\w*%?)|(::?[\w-]*(?=[^,{;]*[,{]))|(([@#.!])?[\w-?]+%?|[@#!.])/g485}));486487const thisModel = disposables.add(instantiateTextModel(instantiationService, '.🐷-a-b', MODE_ID));488489assert.deepStrictEqual(thisModel.getWordAtPosition(new Position(1, 1)), { word: '.', startColumn: 1, endColumn: 2 });490assert.deepStrictEqual(thisModel.getWordAtPosition(new Position(1, 2)), { word: '.', startColumn: 1, endColumn: 2 });491assert.deepStrictEqual(thisModel.getWordAtPosition(new Position(1, 3)), null);492assert.deepStrictEqual(thisModel.getWordAtPosition(new Position(1, 4)), { word: '-a-b', startColumn: 4, endColumn: 8 });493assert.deepStrictEqual(thisModel.getWordAtPosition(new Position(1, 5)), { word: '-a-b', startColumn: 4, endColumn: 8 });494assert.deepStrictEqual(thisModel.getWordAtPosition(new Position(1, 6)), { word: '-a-b', startColumn: 4, endColumn: 8 });495assert.deepStrictEqual(thisModel.getWordAtPosition(new Position(1, 7)), { word: '-a-b', startColumn: 4, endColumn: 8 });496assert.deepStrictEqual(thisModel.getWordAtPosition(new Position(1, 8)), { word: '-a-b', startColumn: 4, endColumn: 8 });497498disposables.dispose();499});500});501502503