Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/editor/contrib/linkedEditing/test/browser/linkedEditing.test.ts
3296 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 { DisposableStore } from '../../../../../base/common/lifecycle.js';
8
import { URI } from '../../../../../base/common/uri.js';
9
import { runWithFakedTimers } from '../../../../../base/test/common/timeTravelScheduler.js';
10
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js';
11
import { CoreEditingCommands } from '../../../../browser/coreCommands.js';
12
import { IPosition, Position } from '../../../../common/core/position.js';
13
import { IRange, Range } from '../../../../common/core/range.js';
14
import { USUAL_WORD_SEPARATORS } from '../../../../common/core/wordHelper.js';
15
import { Handler } from '../../../../common/editorCommon.js';
16
import { ILanguageConfigurationService } from '../../../../common/languages/languageConfigurationRegistry.js';
17
import { ITextModel } from '../../../../common/model.js';
18
import { ILanguageFeaturesService } from '../../../../common/services/languageFeatures.js';
19
import { DeleteAllLeftAction } from '../../../linesOperations/browser/linesOperations.js';
20
import { LinkedEditingContribution } from '../../browser/linkedEditing.js';
21
import { DeleteWordLeft } from '../../../wordOperations/browser/wordOperations.js';
22
import { ITestCodeEditor, createCodeEditorServices, instantiateTestCodeEditor } from '../../../../test/browser/testCodeEditor.js';
23
import { instantiateTextModel } from '../../../../test/common/testTextModel.js';
24
import { TestInstantiationService } from '../../../../../platform/instantiation/test/common/instantiationServiceMock.js';
25
26
const mockFile = URI.parse('test:somefile.ttt');
27
const mockFileSelector = { scheme: 'test' };
28
const timeout = 30;
29
30
interface TestEditor {
31
setPosition(pos: Position): Promise<any>;
32
setSelection(sel: IRange): Promise<any>;
33
trigger(source: string | null | undefined, handlerId: string, payload: any): Promise<any>;
34
undo(): void;
35
redo(): void;
36
}
37
38
const languageId = 'linkedEditingTestLangage';
39
40
suite('linked editing', () => {
41
let disposables: DisposableStore;
42
let instantiationService: TestInstantiationService;
43
let languageFeaturesService: ILanguageFeaturesService;
44
let languageConfigurationService: ILanguageConfigurationService;
45
46
setup(() => {
47
disposables = new DisposableStore();
48
instantiationService = createCodeEditorServices(disposables);
49
languageFeaturesService = instantiationService.get(ILanguageFeaturesService);
50
languageConfigurationService = instantiationService.get(ILanguageConfigurationService);
51
52
disposables.add(languageConfigurationService.register(languageId, {
53
wordPattern: /[a-zA-Z]+/
54
}));
55
});
56
57
teardown(() => {
58
disposables.dispose();
59
});
60
61
ensureNoDisposablesAreLeakedInTestSuite();
62
63
function createMockEditor(text: string | string[]): ITestCodeEditor {
64
const model = disposables.add(instantiateTextModel(instantiationService, typeof text === 'string' ? text : text.join('\n'), languageId, undefined, mockFile));
65
const editor = disposables.add(instantiateTestCodeEditor(instantiationService, model));
66
return editor;
67
}
68
69
function testCase(
70
name: string,
71
initialState: { text: string | string[]; responseWordPattern?: RegExp },
72
operations: (editor: TestEditor) => Promise<void>,
73
expectedEndText: string | string[]
74
) {
75
test(name, async () => {
76
await runWithFakedTimers({}, async () => {
77
78
disposables.add(languageFeaturesService.linkedEditingRangeProvider.register(mockFileSelector, {
79
provideLinkedEditingRanges(model: ITextModel, pos: IPosition) {
80
const wordAtPos = model.getWordAtPosition(pos);
81
if (wordAtPos) {
82
const matches = model.findMatches(wordAtPos.word, false, false, true, USUAL_WORD_SEPARATORS, false);
83
return { ranges: matches.map(m => m.range), wordPattern: initialState.responseWordPattern };
84
}
85
return { ranges: [], wordPattern: initialState.responseWordPattern };
86
}
87
}));
88
89
const editor = createMockEditor(initialState.text);
90
editor.updateOptions({ linkedEditing: true });
91
const linkedEditingContribution = disposables.add(editor.registerAndInstantiateContribution(
92
LinkedEditingContribution.ID,
93
LinkedEditingContribution,
94
));
95
linkedEditingContribution.setDebounceDuration(0);
96
97
const testEditor: TestEditor = {
98
setPosition(pos: Position) {
99
editor.setPosition(pos);
100
return linkedEditingContribution.currentUpdateTriggerPromise;
101
},
102
setSelection(sel: IRange) {
103
editor.setSelection(sel);
104
return linkedEditingContribution.currentUpdateTriggerPromise;
105
},
106
trigger(source: string | null | undefined, handlerId: string, payload: any) {
107
if (handlerId === Handler.Type || handlerId === Handler.Paste) {
108
editor.trigger(source, handlerId, payload);
109
} else if (handlerId === 'deleteLeft') {
110
editor.runCommand(CoreEditingCommands.DeleteLeft, payload);
111
} else if (handlerId === 'deleteWordLeft') {
112
instantiationService.invokeFunction((accessor) => (new DeleteWordLeft()).runEditorCommand(accessor, editor, payload));
113
} else if (handlerId === 'deleteAllLeft') {
114
instantiationService.invokeFunction((accessor) => (new DeleteAllLeftAction()).runEditorCommand(accessor, editor, payload));
115
} else {
116
throw new Error(`Unknown handler ${handlerId}!`);
117
}
118
return linkedEditingContribution.currentSyncTriggerPromise;
119
},
120
undo() {
121
editor.runCommand(CoreEditingCommands.Undo, null);
122
},
123
redo() {
124
editor.runCommand(CoreEditingCommands.Redo, null);
125
}
126
};
127
128
await operations(testEditor);
129
130
return new Promise<void>((resolve) => {
131
setTimeout(() => {
132
if (typeof expectedEndText === 'string') {
133
assert.strictEqual(editor.getModel()!.getValue(), expectedEndText);
134
} else {
135
assert.strictEqual(editor.getModel()!.getValue(), expectedEndText.join('\n'));
136
}
137
resolve();
138
}, timeout);
139
});
140
});
141
});
142
}
143
144
const state = {
145
text: '<ooo></ooo>'
146
};
147
148
/**
149
* Simple insertion
150
*/
151
testCase('Simple insert - initial', state, async (editor) => {
152
const pos = new Position(1, 2);
153
await editor.setPosition(pos);
154
await editor.trigger('keyboard', Handler.Type, { text: 'i' });
155
}, '<iooo></iooo>');
156
157
testCase('Simple insert - middle', state, async (editor) => {
158
const pos = new Position(1, 3);
159
await editor.setPosition(pos);
160
await editor.trigger('keyboard', Handler.Type, { text: 'i' });
161
}, '<oioo></oioo>');
162
163
testCase('Simple insert - end', state, async (editor) => {
164
const pos = new Position(1, 5);
165
await editor.setPosition(pos);
166
await editor.trigger('keyboard', Handler.Type, { text: 'i' });
167
}, '<oooi></oooi>');
168
169
/**
170
* Simple insertion - end
171
*/
172
testCase('Simple insert end - initial', state, async (editor) => {
173
const pos = new Position(1, 8);
174
await editor.setPosition(pos);
175
await editor.trigger('keyboard', Handler.Type, { text: 'i' });
176
}, '<iooo></iooo>');
177
178
testCase('Simple insert end - middle', state, async (editor) => {
179
const pos = new Position(1, 9);
180
await editor.setPosition(pos);
181
await editor.trigger('keyboard', Handler.Type, { text: 'i' });
182
}, '<oioo></oioo>');
183
184
testCase('Simple insert end - end', state, async (editor) => {
185
const pos = new Position(1, 11);
186
await editor.setPosition(pos);
187
await editor.trigger('keyboard', Handler.Type, { text: 'i' });
188
}, '<oooi></oooi>');
189
190
/**
191
* Boundary insertion
192
*/
193
testCase('Simple insert - out of boundary', state, async (editor) => {
194
const pos = new Position(1, 1);
195
await editor.setPosition(pos);
196
await editor.trigger('keyboard', Handler.Type, { text: 'i' });
197
}, 'i<ooo></ooo>');
198
199
testCase('Simple insert - out of boundary 2', state, async (editor) => {
200
const pos = new Position(1, 6);
201
await editor.setPosition(pos);
202
await editor.trigger('keyboard', Handler.Type, { text: 'i' });
203
}, '<ooo>i</ooo>');
204
205
testCase('Simple insert - out of boundary 3', state, async (editor) => {
206
const pos = new Position(1, 7);
207
await editor.setPosition(pos);
208
await editor.trigger('keyboard', Handler.Type, { text: 'i' });
209
}, '<ooo><i/ooo>');
210
211
testCase('Simple insert - out of boundary 4', state, async (editor) => {
212
const pos = new Position(1, 12);
213
await editor.setPosition(pos);
214
await editor.trigger('keyboard', Handler.Type, { text: 'i' });
215
}, '<ooo></ooo>i');
216
217
/**
218
* Insert + Move
219
*/
220
testCase('Continuous insert', state, async (editor) => {
221
const pos = new Position(1, 2);
222
await editor.setPosition(pos);
223
await editor.trigger('keyboard', Handler.Type, { text: 'i' });
224
await editor.trigger('keyboard', Handler.Type, { text: 'i' });
225
}, '<iiooo></iiooo>');
226
227
testCase('Insert - move - insert', state, async (editor) => {
228
const pos = new Position(1, 2);
229
await editor.setPosition(pos);
230
await editor.trigger('keyboard', Handler.Type, { text: 'i' });
231
await editor.setPosition(new Position(1, 4));
232
await editor.trigger('keyboard', Handler.Type, { text: 'i' });
233
}, '<ioioo></ioioo>');
234
235
testCase('Insert - move - insert outside region', state, async (editor) => {
236
const pos = new Position(1, 2);
237
await editor.setPosition(pos);
238
await editor.trigger('keyboard', Handler.Type, { text: 'i' });
239
await editor.setPosition(new Position(1, 7));
240
await editor.trigger('keyboard', Handler.Type, { text: 'i' });
241
}, '<iooo>i</iooo>');
242
243
/**
244
* Selection insert
245
*/
246
testCase('Selection insert - simple', state, async (editor) => {
247
const pos = new Position(1, 2);
248
await editor.setPosition(pos);
249
await editor.setSelection(new Range(1, 2, 1, 3));
250
await editor.trigger('keyboard', Handler.Type, { text: 'i' });
251
}, '<ioo></ioo>');
252
253
testCase('Selection insert - whole', state, async (editor) => {
254
const pos = new Position(1, 2);
255
await editor.setPosition(pos);
256
await editor.setSelection(new Range(1, 2, 1, 5));
257
await editor.trigger('keyboard', Handler.Type, { text: 'i' });
258
}, '<i></i>');
259
260
testCase('Selection insert - across boundary', state, async (editor) => {
261
const pos = new Position(1, 2);
262
await editor.setPosition(pos);
263
await editor.setSelection(new Range(1, 1, 1, 3));
264
await editor.trigger('keyboard', Handler.Type, { text: 'i' });
265
}, 'ioo></oo>');
266
267
/**
268
* @todo
269
* Undefined behavior
270
*/
271
// testCase('Selection insert - across two boundary', state, async (editor) => {
272
// const pos = new Position(1, 2);
273
// await editor.setPosition(pos);
274
// await linkedEditingContribution.updateLinkedUI(pos);
275
// await editor.setSelection(new Range(1, 4, 1, 9));
276
// await editor.trigger('keyboard', Handler.Type, { text: 'i' });
277
// }, '<ooioo>');
278
279
/**
280
* Break out behavior
281
*/
282
testCase('Breakout - type space', state, async (editor) => {
283
const pos = new Position(1, 5);
284
await editor.setPosition(pos);
285
await editor.trigger('keyboard', Handler.Type, { text: ' ' });
286
}, '<ooo ></ooo>');
287
288
testCase('Breakout - type space then undo', state, async (editor) => {
289
const pos = new Position(1, 5);
290
await editor.setPosition(pos);
291
await editor.trigger('keyboard', Handler.Type, { text: ' ' });
292
editor.undo();
293
}, '<ooo></ooo>');
294
295
testCase('Breakout - type space in middle', state, async (editor) => {
296
const pos = new Position(1, 4);
297
await editor.setPosition(pos);
298
await editor.trigger('keyboard', Handler.Type, { text: ' ' });
299
}, '<oo o></ooo>');
300
301
testCase('Breakout - paste content starting with space', state, async (editor) => {
302
const pos = new Position(1, 5);
303
await editor.setPosition(pos);
304
await editor.trigger('keyboard', Handler.Paste, { text: ' i="i"' });
305
}, '<ooo i="i"></ooo>');
306
307
testCase('Breakout - paste content starting with space then undo', state, async (editor) => {
308
const pos = new Position(1, 5);
309
await editor.setPosition(pos);
310
await editor.trigger('keyboard', Handler.Paste, { text: ' i="i"' });
311
editor.undo();
312
}, '<ooo></ooo>');
313
314
testCase('Breakout - paste content starting with space in middle', state, async (editor) => {
315
const pos = new Position(1, 4);
316
await editor.setPosition(pos);
317
await editor.trigger('keyboard', Handler.Paste, { text: ' i' });
318
}, '<oo io></ooo>');
319
320
/**
321
* Break out with custom provider wordPattern
322
*/
323
324
const state3 = {
325
...state,
326
responseWordPattern: /[a-yA-Y]+/
327
};
328
329
testCase('Breakout with stop pattern - insert', state3, async (editor) => {
330
const pos = new Position(1, 2);
331
await editor.setPosition(pos);
332
await editor.trigger('keyboard', Handler.Type, { text: 'i' });
333
}, '<iooo></iooo>');
334
335
testCase('Breakout with stop pattern - insert stop char', state3, async (editor) => {
336
const pos = new Position(1, 2);
337
await editor.setPosition(pos);
338
await editor.trigger('keyboard', Handler.Type, { text: 'z' });
339
}, '<zooo></ooo>');
340
341
testCase('Breakout with stop pattern - paste char', state3, async (editor) => {
342
const pos = new Position(1, 2);
343
await editor.setPosition(pos);
344
await editor.trigger('keyboard', Handler.Paste, { text: 'z' });
345
}, '<zooo></ooo>');
346
347
testCase('Breakout with stop pattern - paste string', state3, async (editor) => {
348
const pos = new Position(1, 2);
349
await editor.setPosition(pos);
350
await editor.trigger('keyboard', Handler.Paste, { text: 'zo' });
351
}, '<zoooo></ooo>');
352
353
testCase('Breakout with stop pattern - insert at end', state3, async (editor) => {
354
const pos = new Position(1, 5);
355
await editor.setPosition(pos);
356
await editor.trigger('keyboard', Handler.Type, { text: 'z' });
357
}, '<oooz></ooo>');
358
359
const state4 = {
360
...state,
361
responseWordPattern: /[a-eA-E]+/
362
};
363
364
testCase('Breakout with stop pattern - insert stop char, respos', state4, async (editor) => {
365
const pos = new Position(1, 2);
366
await editor.setPosition(pos);
367
await editor.trigger('keyboard', Handler.Type, { text: 'i' });
368
}, '<iooo></ooo>');
369
370
/**
371
* Delete
372
*/
373
testCase('Delete - left char', state, async (editor) => {
374
const pos = new Position(1, 5);
375
await editor.setPosition(pos);
376
await editor.trigger('keyboard', 'deleteLeft', {});
377
}, '<oo></oo>');
378
379
testCase('Delete - left char then undo', state, async (editor) => {
380
const pos = new Position(1, 5);
381
await editor.setPosition(pos);
382
await editor.trigger('keyboard', 'deleteLeft', {});
383
editor.undo();
384
}, '<ooo></ooo>');
385
386
testCase('Delete - left word', state, async (editor) => {
387
const pos = new Position(1, 5);
388
await editor.setPosition(pos);
389
await editor.trigger('keyboard', 'deleteWordLeft', {});
390
}, '<></>');
391
392
testCase('Delete - left word then undo', state, async (editor) => {
393
const pos = new Position(1, 5);
394
await editor.setPosition(pos);
395
await editor.trigger('keyboard', 'deleteWordLeft', {});
396
editor.undo();
397
editor.undo();
398
}, '<ooo></ooo>');
399
400
/**
401
* Todo: Fix test
402
*/
403
// testCase('Delete - left all', state, async (editor) => {
404
// const pos = new Position(1, 3);
405
// await editor.setPosition(pos);
406
// await linkedEditingContribution.updateLinkedUI(pos);
407
// await editor.trigger('keyboard', 'deleteAllLeft', {});
408
// }, '></>');
409
410
/**
411
* Todo: Fix test
412
*/
413
// testCase('Delete - left all then undo', state, async (editor) => {
414
// const pos = new Position(1, 5);
415
// await editor.setPosition(pos);
416
// await linkedEditingContribution.updateLinkedUI(pos);
417
// await editor.trigger('keyboard', 'deleteAllLeft', {});
418
// editor.undo();
419
// }, '></ooo>');
420
421
testCase('Delete - left all then undo twice', state, async (editor) => {
422
const pos = new Position(1, 5);
423
await editor.setPosition(pos);
424
await editor.trigger('keyboard', 'deleteAllLeft', {});
425
editor.undo();
426
editor.undo();
427
}, '<ooo></ooo>');
428
429
testCase('Delete - selection', state, async (editor) => {
430
const pos = new Position(1, 5);
431
await editor.setPosition(pos);
432
await editor.setSelection(new Range(1, 2, 1, 3));
433
await editor.trigger('keyboard', 'deleteLeft', {});
434
}, '<oo></oo>');
435
436
testCase('Delete - selection across boundary', state, async (editor) => {
437
const pos = new Position(1, 3);
438
await editor.setPosition(pos);
439
await editor.setSelection(new Range(1, 1, 1, 3));
440
await editor.trigger('keyboard', 'deleteLeft', {});
441
}, 'oo></oo>');
442
443
/**
444
* Undo / redo
445
*/
446
testCase('Undo/redo - simple undo', state, async (editor) => {
447
const pos = new Position(1, 2);
448
await editor.setPosition(pos);
449
await editor.trigger('keyboard', Handler.Type, { text: 'i' });
450
editor.undo();
451
editor.undo();
452
}, '<ooo></ooo>');
453
454
testCase('Undo/redo - simple undo/redo', state, async (editor) => {
455
const pos = new Position(1, 2);
456
await editor.setPosition(pos);
457
await editor.trigger('keyboard', Handler.Type, { text: 'i' });
458
editor.undo();
459
editor.redo();
460
}, '<iooo></iooo>');
461
462
/**
463
* Multi line
464
*/
465
const state2 = {
466
text: [
467
'<ooo>',
468
'</ooo>'
469
]
470
};
471
472
testCase('Multiline insert', state2, async (editor) => {
473
const pos = new Position(1, 2);
474
await editor.setPosition(pos);
475
await editor.trigger('keyboard', Handler.Type, { text: 'i' });
476
}, [
477
'<iooo>',
478
'</iooo>'
479
]);
480
});
481
482