Path: blob/main/src/vs/editor/contrib/linkedEditing/test/browser/linkedEditing.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 { DisposableStore } from '../../../../../base/common/lifecycle.js';7import { URI } from '../../../../../base/common/uri.js';8import { runWithFakedTimers } from '../../../../../base/test/common/timeTravelScheduler.js';9import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js';10import { CoreEditingCommands } from '../../../../browser/coreCommands.js';11import { IPosition, Position } from '../../../../common/core/position.js';12import { IRange, Range } from '../../../../common/core/range.js';13import { USUAL_WORD_SEPARATORS } from '../../../../common/core/wordHelper.js';14import { Handler } from '../../../../common/editorCommon.js';15import { ILanguageConfigurationService } from '../../../../common/languages/languageConfigurationRegistry.js';16import { ITextModel } from '../../../../common/model.js';17import { ILanguageFeaturesService } from '../../../../common/services/languageFeatures.js';18import { DeleteAllLeftAction } from '../../../linesOperations/browser/linesOperations.js';19import { LinkedEditingContribution } from '../../browser/linkedEditing.js';20import { DeleteWordLeft } from '../../../wordOperations/browser/wordOperations.js';21import { ITestCodeEditor, createCodeEditorServices, instantiateTestCodeEditor } from '../../../../test/browser/testCodeEditor.js';22import { instantiateTextModel } from '../../../../test/common/testTextModel.js';23import { TestInstantiationService } from '../../../../../platform/instantiation/test/common/instantiationServiceMock.js';2425const mockFile = URI.parse('test:somefile.ttt');26const mockFileSelector = { scheme: 'test' };27const timeout = 30;2829interface TestEditor {30setPosition(pos: Position): Promise<any>;31setSelection(sel: IRange): Promise<any>;32trigger(source: string | null | undefined, handlerId: string, payload: any): Promise<any>;33undo(): void;34redo(): void;35}3637const languageId = 'linkedEditingTestLangage';3839suite('linked editing', () => {40let disposables: DisposableStore;41let instantiationService: TestInstantiationService;42let languageFeaturesService: ILanguageFeaturesService;43let languageConfigurationService: ILanguageConfigurationService;4445setup(() => {46disposables = new DisposableStore();47instantiationService = createCodeEditorServices(disposables);48languageFeaturesService = instantiationService.get(ILanguageFeaturesService);49languageConfigurationService = instantiationService.get(ILanguageConfigurationService);5051disposables.add(languageConfigurationService.register(languageId, {52wordPattern: /[a-zA-Z]+/53}));54});5556teardown(() => {57disposables.dispose();58});5960ensureNoDisposablesAreLeakedInTestSuite();6162function createMockEditor(text: string | string[]): ITestCodeEditor {63const model = disposables.add(instantiateTextModel(instantiationService, typeof text === 'string' ? text : text.join('\n'), languageId, undefined, mockFile));64const editor = disposables.add(instantiateTestCodeEditor(instantiationService, model));65return editor;66}6768function testCase(69name: string,70initialState: { text: string | string[]; responseWordPattern?: RegExp },71operations: (editor: TestEditor) => Promise<void>,72expectedEndText: string | string[]73) {74test(name, async () => {75await runWithFakedTimers({}, async () => {7677disposables.add(languageFeaturesService.linkedEditingRangeProvider.register(mockFileSelector, {78provideLinkedEditingRanges(model: ITextModel, pos: IPosition) {79const wordAtPos = model.getWordAtPosition(pos);80if (wordAtPos) {81const matches = model.findMatches(wordAtPos.word, false, false, true, USUAL_WORD_SEPARATORS, false);82return { ranges: matches.map(m => m.range), wordPattern: initialState.responseWordPattern };83}84return { ranges: [], wordPattern: initialState.responseWordPattern };85}86}));8788const editor = createMockEditor(initialState.text);89editor.updateOptions({ linkedEditing: true });90const linkedEditingContribution = disposables.add(editor.registerAndInstantiateContribution(91LinkedEditingContribution.ID,92LinkedEditingContribution,93));94linkedEditingContribution.setDebounceDuration(0);9596const testEditor: TestEditor = {97setPosition(pos: Position) {98editor.setPosition(pos);99return linkedEditingContribution.currentUpdateTriggerPromise;100},101setSelection(sel: IRange) {102editor.setSelection(sel);103return linkedEditingContribution.currentUpdateTriggerPromise;104},105trigger(source: string | null | undefined, handlerId: string, payload: any) {106if (handlerId === Handler.Type || handlerId === Handler.Paste) {107editor.trigger(source, handlerId, payload);108} else if (handlerId === 'deleteLeft') {109editor.runCommand(CoreEditingCommands.DeleteLeft, payload);110} else if (handlerId === 'deleteWordLeft') {111instantiationService.invokeFunction((accessor) => (new DeleteWordLeft()).runEditorCommand(accessor, editor, payload));112} else if (handlerId === 'deleteAllLeft') {113instantiationService.invokeFunction((accessor) => (new DeleteAllLeftAction()).runEditorCommand(accessor, editor, payload));114} else {115throw new Error(`Unknown handler ${handlerId}!`);116}117return linkedEditingContribution.currentSyncTriggerPromise;118},119undo() {120editor.runCommand(CoreEditingCommands.Undo, null);121},122redo() {123editor.runCommand(CoreEditingCommands.Redo, null);124}125};126127await operations(testEditor);128129return new Promise<void>((resolve) => {130setTimeout(() => {131if (typeof expectedEndText === 'string') {132assert.strictEqual(editor.getModel()!.getValue(), expectedEndText);133} else {134assert.strictEqual(editor.getModel()!.getValue(), expectedEndText.join('\n'));135}136resolve();137}, timeout);138});139});140});141}142143const state = {144text: '<ooo></ooo>'145};146147/**148* Simple insertion149*/150testCase('Simple insert - initial', state, async (editor) => {151const pos = new Position(1, 2);152await editor.setPosition(pos);153await editor.trigger('keyboard', Handler.Type, { text: 'i' });154}, '<iooo></iooo>');155156testCase('Simple insert - middle', state, async (editor) => {157const pos = new Position(1, 3);158await editor.setPosition(pos);159await editor.trigger('keyboard', Handler.Type, { text: 'i' });160}, '<oioo></oioo>');161162testCase('Simple insert - end', state, async (editor) => {163const pos = new Position(1, 5);164await editor.setPosition(pos);165await editor.trigger('keyboard', Handler.Type, { text: 'i' });166}, '<oooi></oooi>');167168/**169* Simple insertion - end170*/171testCase('Simple insert end - initial', state, async (editor) => {172const pos = new Position(1, 8);173await editor.setPosition(pos);174await editor.trigger('keyboard', Handler.Type, { text: 'i' });175}, '<iooo></iooo>');176177testCase('Simple insert end - middle', state, async (editor) => {178const pos = new Position(1, 9);179await editor.setPosition(pos);180await editor.trigger('keyboard', Handler.Type, { text: 'i' });181}, '<oioo></oioo>');182183testCase('Simple insert end - end', state, async (editor) => {184const pos = new Position(1, 11);185await editor.setPosition(pos);186await editor.trigger('keyboard', Handler.Type, { text: 'i' });187}, '<oooi></oooi>');188189/**190* Boundary insertion191*/192testCase('Simple insert - out of boundary', state, async (editor) => {193const pos = new Position(1, 1);194await editor.setPosition(pos);195await editor.trigger('keyboard', Handler.Type, { text: 'i' });196}, 'i<ooo></ooo>');197198testCase('Simple insert - out of boundary 2', state, async (editor) => {199const pos = new Position(1, 6);200await editor.setPosition(pos);201await editor.trigger('keyboard', Handler.Type, { text: 'i' });202}, '<ooo>i</ooo>');203204testCase('Simple insert - out of boundary 3', state, async (editor) => {205const pos = new Position(1, 7);206await editor.setPosition(pos);207await editor.trigger('keyboard', Handler.Type, { text: 'i' });208}, '<ooo><i/ooo>');209210testCase('Simple insert - out of boundary 4', state, async (editor) => {211const pos = new Position(1, 12);212await editor.setPosition(pos);213await editor.trigger('keyboard', Handler.Type, { text: 'i' });214}, '<ooo></ooo>i');215216/**217* Insert + Move218*/219testCase('Continuous insert', state, async (editor) => {220const pos = new Position(1, 2);221await editor.setPosition(pos);222await editor.trigger('keyboard', Handler.Type, { text: 'i' });223await editor.trigger('keyboard', Handler.Type, { text: 'i' });224}, '<iiooo></iiooo>');225226testCase('Insert - move - insert', state, async (editor) => {227const pos = new Position(1, 2);228await editor.setPosition(pos);229await editor.trigger('keyboard', Handler.Type, { text: 'i' });230await editor.setPosition(new Position(1, 4));231await editor.trigger('keyboard', Handler.Type, { text: 'i' });232}, '<ioioo></ioioo>');233234testCase('Insert - move - insert outside region', state, async (editor) => {235const pos = new Position(1, 2);236await editor.setPosition(pos);237await editor.trigger('keyboard', Handler.Type, { text: 'i' });238await editor.setPosition(new Position(1, 7));239await editor.trigger('keyboard', Handler.Type, { text: 'i' });240}, '<iooo>i</iooo>');241242/**243* Selection insert244*/245testCase('Selection insert - simple', state, async (editor) => {246const pos = new Position(1, 2);247await editor.setPosition(pos);248await editor.setSelection(new Range(1, 2, 1, 3));249await editor.trigger('keyboard', Handler.Type, { text: 'i' });250}, '<ioo></ioo>');251252testCase('Selection insert - whole', state, async (editor) => {253const pos = new Position(1, 2);254await editor.setPosition(pos);255await editor.setSelection(new Range(1, 2, 1, 5));256await editor.trigger('keyboard', Handler.Type, { text: 'i' });257}, '<i></i>');258259testCase('Selection insert - across boundary', state, async (editor) => {260const pos = new Position(1, 2);261await editor.setPosition(pos);262await editor.setSelection(new Range(1, 1, 1, 3));263await editor.trigger('keyboard', Handler.Type, { text: 'i' });264}, 'ioo></oo>');265266/**267* @todo268* Undefined behavior269*/270// testCase('Selection insert - across two boundary', state, async (editor) => {271// const pos = new Position(1, 2);272// await editor.setPosition(pos);273// await linkedEditingContribution.updateLinkedUI(pos);274// await editor.setSelection(new Range(1, 4, 1, 9));275// await editor.trigger('keyboard', Handler.Type, { text: 'i' });276// }, '<ooioo>');277278/**279* Break out behavior280*/281testCase('Breakout - type space', state, async (editor) => {282const pos = new Position(1, 5);283await editor.setPosition(pos);284await editor.trigger('keyboard', Handler.Type, { text: ' ' });285}, '<ooo ></ooo>');286287testCase('Breakout - type space then undo', state, async (editor) => {288const pos = new Position(1, 5);289await editor.setPosition(pos);290await editor.trigger('keyboard', Handler.Type, { text: ' ' });291editor.undo();292}, '<ooo></ooo>');293294testCase('Breakout - type space in middle', state, async (editor) => {295const pos = new Position(1, 4);296await editor.setPosition(pos);297await editor.trigger('keyboard', Handler.Type, { text: ' ' });298}, '<oo o></ooo>');299300testCase('Breakout - paste content starting with space', state, async (editor) => {301const pos = new Position(1, 5);302await editor.setPosition(pos);303await editor.trigger('keyboard', Handler.Paste, { text: ' i="i"' });304}, '<ooo i="i"></ooo>');305306testCase('Breakout - paste content starting with space then undo', state, async (editor) => {307const pos = new Position(1, 5);308await editor.setPosition(pos);309await editor.trigger('keyboard', Handler.Paste, { text: ' i="i"' });310editor.undo();311}, '<ooo></ooo>');312313testCase('Breakout - paste content starting with space in middle', state, async (editor) => {314const pos = new Position(1, 4);315await editor.setPosition(pos);316await editor.trigger('keyboard', Handler.Paste, { text: ' i' });317}, '<oo io></ooo>');318319/**320* Break out with custom provider wordPattern321*/322323const state3 = {324...state,325responseWordPattern: /[a-yA-Y]+/326};327328testCase('Breakout with stop pattern - insert', state3, async (editor) => {329const pos = new Position(1, 2);330await editor.setPosition(pos);331await editor.trigger('keyboard', Handler.Type, { text: 'i' });332}, '<iooo></iooo>');333334testCase('Breakout with stop pattern - insert stop char', state3, async (editor) => {335const pos = new Position(1, 2);336await editor.setPosition(pos);337await editor.trigger('keyboard', Handler.Type, { text: 'z' });338}, '<zooo></ooo>');339340testCase('Breakout with stop pattern - paste char', state3, async (editor) => {341const pos = new Position(1, 2);342await editor.setPosition(pos);343await editor.trigger('keyboard', Handler.Paste, { text: 'z' });344}, '<zooo></ooo>');345346testCase('Breakout with stop pattern - paste string', state3, async (editor) => {347const pos = new Position(1, 2);348await editor.setPosition(pos);349await editor.trigger('keyboard', Handler.Paste, { text: 'zo' });350}, '<zoooo></ooo>');351352testCase('Breakout with stop pattern - insert at end', state3, async (editor) => {353const pos = new Position(1, 5);354await editor.setPosition(pos);355await editor.trigger('keyboard', Handler.Type, { text: 'z' });356}, '<oooz></ooo>');357358const state4 = {359...state,360responseWordPattern: /[a-eA-E]+/361};362363testCase('Breakout with stop pattern - insert stop char, respos', state4, async (editor) => {364const pos = new Position(1, 2);365await editor.setPosition(pos);366await editor.trigger('keyboard', Handler.Type, { text: 'i' });367}, '<iooo></ooo>');368369/**370* Delete371*/372testCase('Delete - left char', state, async (editor) => {373const pos = new Position(1, 5);374await editor.setPosition(pos);375await editor.trigger('keyboard', 'deleteLeft', {});376}, '<oo></oo>');377378testCase('Delete - left char then undo', state, async (editor) => {379const pos = new Position(1, 5);380await editor.setPosition(pos);381await editor.trigger('keyboard', 'deleteLeft', {});382editor.undo();383}, '<ooo></ooo>');384385testCase('Delete - left word', state, async (editor) => {386const pos = new Position(1, 5);387await editor.setPosition(pos);388await editor.trigger('keyboard', 'deleteWordLeft', {});389}, '<></>');390391testCase('Delete - left word then undo', state, async (editor) => {392const pos = new Position(1, 5);393await editor.setPosition(pos);394await editor.trigger('keyboard', 'deleteWordLeft', {});395editor.undo();396editor.undo();397}, '<ooo></ooo>');398399/**400* Todo: Fix test401*/402// testCase('Delete - left all', state, async (editor) => {403// const pos = new Position(1, 3);404// await editor.setPosition(pos);405// await linkedEditingContribution.updateLinkedUI(pos);406// await editor.trigger('keyboard', 'deleteAllLeft', {});407// }, '></>');408409/**410* Todo: Fix test411*/412// testCase('Delete - left all then undo', state, async (editor) => {413// const pos = new Position(1, 5);414// await editor.setPosition(pos);415// await linkedEditingContribution.updateLinkedUI(pos);416// await editor.trigger('keyboard', 'deleteAllLeft', {});417// editor.undo();418// }, '></ooo>');419420testCase('Delete - left all then undo twice', state, async (editor) => {421const pos = new Position(1, 5);422await editor.setPosition(pos);423await editor.trigger('keyboard', 'deleteAllLeft', {});424editor.undo();425editor.undo();426}, '<ooo></ooo>');427428testCase('Delete - selection', state, async (editor) => {429const pos = new Position(1, 5);430await editor.setPosition(pos);431await editor.setSelection(new Range(1, 2, 1, 3));432await editor.trigger('keyboard', 'deleteLeft', {});433}, '<oo></oo>');434435testCase('Delete - selection across boundary', state, async (editor) => {436const pos = new Position(1, 3);437await editor.setPosition(pos);438await editor.setSelection(new Range(1, 1, 1, 3));439await editor.trigger('keyboard', 'deleteLeft', {});440}, 'oo></oo>');441442/**443* Undo / redo444*/445testCase('Undo/redo - simple undo', state, async (editor) => {446const pos = new Position(1, 2);447await editor.setPosition(pos);448await editor.trigger('keyboard', Handler.Type, { text: 'i' });449editor.undo();450editor.undo();451}, '<ooo></ooo>');452453testCase('Undo/redo - simple undo/redo', state, async (editor) => {454const pos = new Position(1, 2);455await editor.setPosition(pos);456await editor.trigger('keyboard', Handler.Type, { text: 'i' });457editor.undo();458editor.redo();459}, '<iooo></iooo>');460461/**462* Multi line463*/464const state2 = {465text: [466'<ooo>',467'</ooo>'468]469};470471testCase('Multiline insert', state2, async (editor) => {472const pos = new Position(1, 2);473await editor.setPosition(pos);474await editor.trigger('keyboard', Handler.Type, { text: 'i' });475}, [476'<iooo>',477'</iooo>'478]);479});480481482