Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/copilot/src/extension/inlineEdits/test/node/nesXtabHistoryTracker.spec.ts
13405 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 * as fs from 'fs/promises';
7
import { afterEach, describe, expect, it } from 'vitest';
8
import { DefaultsOnlyConfigurationService } from '../../../../platform/configuration/common/defaultsOnlyConfigurationService';
9
import { overrideNowValue } from '../../../../platform/inlineEdits/common/utils/utils';
10
import { NesXtabHistoryTracker, XtabEditMergeStrategy } from '../../../../platform/inlineEdits/common/workspaceEditTracker/nesXtabHistoryTracker';
11
import { NullExperimentationService } from '../../../../platform/telemetry/common/nullExperimentationService';
12
import { assert } from '../../../../util/vs/base/common/assert';
13
import { observableValue } from '../../../../util/vs/base/common/observable';
14
import * as path from '../../../../util/vs/base/common/path';
15
import { IRecordingInformation, ObservableWorkspaceRecordingReplayer } from '../../common/observableWorkspaceRecordingReplayer';
16
17
18
describe('NesXtabHistoryTracker', () => {
19
20
afterEach(() => {
21
overrideNowValue(-1);
22
});
23
24
function createTracker(replayerWorkspace: any, maxHistorySize?: number | undefined, mergeStrategy = XtabEditMergeStrategy.sameStartLine) {
25
return new (class extends NesXtabHistoryTracker {
26
protected override readonly mergeStrategy = observableValue(this, mergeStrategy);
27
})(replayerWorkspace, maxHistorySize, new DefaultsOnlyConfigurationService(), new NullExperimentationService());
28
}
29
30
function historyToString(tracker: NesXtabHistoryTracker): string {
31
const history = tracker.getHistory();
32
assert(history.every(e => e.kind === 'edit'));
33
return stripTrailingWhitespace(history.map(h => h.edit.toString()).join('\n---\n'));
34
}
35
36
/** Strip trailing whitespace from each line to avoid fragile snapshots. */
37
function stripTrailingWhitespace(s: string): string {
38
return s.replace(/[^\S\n]+$/gm, '');
39
}
40
41
it('1 line, 1 edit', () => {
42
const recording: IRecordingInformation = {
43
log: [
44
{ documentType: '[email protected]', kind: 'header', repoRootUri: 'file:///Users/john/myProject', time: 0, uuid: '' },
45
{ time: 10, id: 0, kind: 'documentEncountered', relativePath: 'src/a.ts' },
46
{ time: 11, id: 0, v: 1, kind: 'setContent', content: 'hemmo world\ngoodbye' },
47
{ time: 11, id: 0, v: 1, kind: 'changed', edit: [[2, 4, 'll']] },
48
]
49
};
50
const replayer = new ObservableWorkspaceRecordingReplayer(recording);
51
const tracker = createTracker(replayer.workspace);
52
replayer.replay();
53
expect(historyToString(tracker)).toMatchInlineSnapshot(`
54
"- 1 hemmo world
55
+ 1 hello world
56
2 2 goodbye"
57
`);
58
});
59
60
it('1 line, 2 edits', () => {
61
const recording: IRecordingInformation = {
62
log: [
63
{ documentType: '[email protected]', kind: 'header', repoRootUri: 'file:///Users/john/myProject', time: 0, uuid: '' },
64
{ time: 10, id: 0, kind: 'documentEncountered', relativePath: 'src/a.ts' },
65
{ time: 11, id: 0, v: 1, kind: 'setContent', content: 'hemmo world\ngoodbye' },
66
{ time: 11, id: 0, v: 1, kind: 'changed', edit: [[2, 4, 'll']] },
67
{ time: 11, id: 0, v: 1, kind: 'changed', edit: [[8, 8, 'ooooo']] },
68
]
69
};
70
const replayer = new ObservableWorkspaceRecordingReplayer(recording);
71
const tracker = createTracker(replayer.workspace);
72
replayer.replay();
73
expect(historyToString(tracker)).toMatchInlineSnapshot(`
74
"- 1 hemmo world
75
+ 1 hello woooooorld
76
2 2 goodbye"
77
`);
78
});
79
80
it('handles simple history', () => {
81
const recording: IRecordingInformation = {
82
log: [
83
{ documentType: '[email protected]', kind: 'header', repoRootUri: 'file:///Users/john/myProject', time: 0, uuid: '' },
84
{ time: 10, id: 0, kind: 'documentEncountered', relativePath: 'src/a.ts' },
85
{ time: 11, id: 0, v: 1, kind: 'setContent', content: 'hemmo' },
86
{ time: 11, id: 0, v: 1, kind: 'changed', edit: [[5, 5, '\n']] },
87
{ time: 11, id: 0, v: 1, kind: 'changed', edit: [[2, 4, 'll']] },
88
{ time: 11, id: 0, v: 1, kind: 'changed', edit: [[6, 6, 'world']] },
89
]
90
};
91
const replayer = new ObservableWorkspaceRecordingReplayer(recording);
92
const tracker = createTracker(replayer.workspace);
93
replayer.replay();
94
expect(historyToString(tracker)).toMatchInlineSnapshot(`
95
" 1 1 hemmo
96
+ 2
97
---
98
- 1 hemmo
99
+ 1 hello
100
2 2
101
---
102
1 1 hello
103
- 2
104
+ 2 world"
105
`);
106
});
107
108
it('handles simple history with small maxHistorySize', () => {
109
const recording: IRecordingInformation = {
110
log: [
111
{ documentType: '[email protected]', kind: 'header', repoRootUri: 'file:///Users/john/myProject', time: 0, uuid: '' },
112
{ time: 10, id: 0, kind: 'documentEncountered', relativePath: 'src/a.ts' },
113
{ time: 11, id: 0, v: 1, kind: 'setContent', content: 'hemmo' },
114
{ time: 12, id: 0, v: 2, kind: 'changed', edit: [[5, 5, '\n']] },
115
{ time: 13, id: 0, v: 3, kind: 'changed', edit: [[2, 4, 'll']] },
116
{ time: 14, id: 0, v: 4, kind: 'changed', edit: [[6, 6, 'world']] },
117
{ time: 15, id: 0, v: 5, kind: 'changed', edit: [[0, 5, 'goodbye']] },
118
]
119
};
120
const replayer = new ObservableWorkspaceRecordingReplayer(recording);
121
const tracker = createTracker(replayer.workspace, 2);
122
replayer.replay();
123
expect(historyToString(tracker)).toMatchInlineSnapshot(`
124
" 1 1 hello
125
- 2
126
+ 2 world
127
---
128
- 1 hello
129
+ 1 goodbye
130
2 2 world"
131
`);
132
});
133
134
it('add new lines and edit one of them', async () => {
135
const recording: IRecordingInformation = await fs.readFile(path.join(__dirname, 'recordings/ArrayToObject.recording.w.json'), 'utf8').then(JSON.parse);
136
const replayer = new ObservableWorkspaceRecordingReplayer(recording);
137
const tracker = createTracker(replayer.workspace);
138
replayer.replay();
139
expect(historyToString(tracker)).toMatchInlineSnapshot(`
140
" 147 147 commandsWithArgs.set(commandId, argumentsSchema);
141
148 148 }
142
+ 149
143
+ 150
144
149 151
145
150 152 const searchableCommands: Searchables<Command>[] = [];
146
151 153
147
---
148
148 148 }
149
149 149
150
- 150
151
+ 150 function findVscodeDiff(schema: any, path: string[] = []): void {
152
151 151
153
152 152 const searchableCommands: Searchables<Command>[] = [];
154
153 153
155
---
156
149 149
157
150 150 function findVscodeDiff(schema: any, path: string[] = []): void {
158
+ 151 if (typeof schema === 'object' && schema !== null) {
159
+ 152 for (const key in schema) {
160
+ 153 if (schema[key] === 'vscode.diff') {
161
+ 154 console.log(\`Found "vscode.diff" at path: \${path.concat(key).join('.')}\`);
162
+ 155 } else {
163
+ 156 findVscodeDiff(schema[key], path.concat(key));
164
+ 157 }
165
+ 158 }
166
+ 159 }
167
+ 160 }
168
+ 161
169
+ 162 findVscodeDiff(keybindingsSchema);
170
151 163
171
152 164 const searchableCommands: Searchables<Command>[] = [];
172
153 165
173
---
174
147 147 commandsWithArgs.set(commandId, argumentsSchema);
175
148 148 }
176
- 149
177
- 150 function findVscodeDiff(schema: any, path: string[] = []): void {
178
- 151 if (typeof schema === 'object' && schema !== null) {
179
- 152 for (const key in schema) {
180
- 153 if (schema[key] === 'vscode.diff') {
181
- 154 console.log(\`Found "vscode.diff" at path: \${path.concat(key).join('.')}\`);
182
- 155 } else {
183
- 156 findVscodeDiff(schema[key], path.concat(key));
184
- 157 }
185
- 158 }
186
- 159 }
187
- 160 }
188
- 161
189
- 162 findVscodeDiff(keybindingsSchema);
190
163 149
191
164 150 const searchableCommands: Searchables<Command>[] = [];
192
165 151
193
---
194
25 25 }
195
26 26
196
+ 27 export interface Searchables
197
+ 28
198
27 29 export class Configurations implements vscode.Disposable {
199
28 30
200
29 31 private readonly miniSearch: MiniSearch<Searchables<Setting | Command>>;
201
---
202
24 24 when?: string;
203
25 25 }
204
- 26
205
- 27 export interface Searchables
206
28 26
207
29 27 export class Configurations implements vscode.Disposable {
208
30 28
209
---
210
18 18 }
211
19 19
212
- 20 private validateSettings(settings: IStringDictionary<any>): [string, any][] {
213
+ 20 private validateSettings(settings: IStringDictionary<any>): {key: string, value:any}[] {
214
21 21 const result: [string, any][] = [];
215
22 22 for (const [key, value] of Object.entries(settings)) {
216
23 23 result.push([key, value]);"
217
`);
218
});
219
220
it('doesnt throw with empty line edit', async () => {
221
const recording: IRecordingInformation = await fs.readFile(path.join(__dirname, 'recordings/DeclaringConstructorArgument.recording.w.json'), 'utf8').then(JSON.parse);
222
const replayer = new ObservableWorkspaceRecordingReplayer(recording);
223
const tracker = createTracker(replayer.workspace);
224
replayer.replay();
225
const history = tracker.getHistory();
226
assert(history.every(e => e.kind === 'edit'));
227
expect(stripTrailingWhitespace(history.map(h => `${h.docId.path}\n---\n${h.edit.toString()}`).join('\n--------------\n'))).toMatchInlineSnapshot(`
228
"/c:/code/src/platform/inlineEdits/common/workspaceEditTracker/nesWorkspaceEditTracker.ts
229
---
230
36 36 }
231
37 37
232
+ 38 class FifoQueue<T> {
233
+ 39
234
+ 40 }
235
+ 41
236
38 42 class DocumentState {
237
39 43 private baseValue: StringValue;
238
40 44 private currentValue: StringValue;
239
--------------
240
/c:/code/src/platform/inlineEdits/common/workspaceEditTracker/nesWorkspaceEditTracker.ts
241
---
242
37 37
243
38 38 class FifoQueue<T> {
244
- 39
245
+ 39 constructor(
246
+ 40 public readonly size: number
247
+ 41 )
248
40 42 }
249
41 43
250
42 44 class DocumentState {
251
--------------
252
/c:/code/src/platform/inlineEdits/common/workspaceEditTracker/nesWorkspaceEditTracker.ts
253
---
254
39 39 constructor(
255
40 40 public readonly size: number
256
- 41 )
257
+ 41 ) {
258
+ 42
259
+ 43 }
260
42 44 }
261
43 45
262
44 46 class DocumentState {
263
--------------
264
/c:/code/src/platform/inlineEdits/common/workspaceEditTracker/nesWorkspaceEditTracker.ts
265
---
266
40 40 public readonly size: number
267
41 41 ) {
268
- 42
269
43 42 }
270
44 43 }
271
45 44
272
--------------
273
/c:/code/src/platform/inlineEdits/common/workspaceEditTracker/nesWorkspaceEditTracker.ts
274
---
275
41 41 ) {
276
42 42 }
277
+ 43
278
+ 44
279
43 45 }
280
44 46
281
45 47 class DocumentState {
282
--------------
283
/c:/code/src/platform/inlineEdits/common/workspaceEditTracker/nesWorkspaceEditTracker.ts
284
---
285
38 38 class FifoQueue<T> {
286
39 39 constructor(
287
- 40 public readonly size: number
288
+ 40 public readonly maxSize: number
289
41 41 ) {
290
42 42 }
291
43 43
292
- 44
293
+ 44
294
45 45 }
295
46 46
296
47 47 class DocumentState {
297
--------------
298
/c:/code/src/platform/inlineEdits/common/workspaceEditTracker/nesWorkspaceEditTracker.ts
299
---
300
41 41 ) {
301
42 42 }
302
- 43
303
+ 43
304
44 44
305
45 45 }
306
46 46
307
--------------
308
/c:/code/src/platform/inlineEdits/common/workspaceEditTracker/nesWorkspaceEditTracker.ts
309
---
310
37 37
311
38 38 class FifoQueue<T> {
312
+ 39 private _arr: T[] = [];
313
39 40 constructor(
314
40 41 public readonly maxSize: number
315
41 42 ) {
316
42 43 }
317
- 43
318
+ 44
319
44 45
320
45 46 }
321
46 47
322
--------------
323
/c:/code/src/platform/inlineEdits/common/workspaceEditTracker/nesWorkspaceEditTracker.ts
324
---
325
38 38 class FifoQueue<T> {
326
39 39 private _arr: T[] = [];
327
+ 40
328
40 41 constructor(
329
41 42 public readonly maxSize: number
330
42 43 ) {
331
43 44 }
332
44 45
333
- 45
334
+ 46
335
46 47 }
336
47 48
337
48 49 class DocumentState {
338
--------------
339
/c:/code/src/platform/inlineEdits/common/workspaceEditTracker/nesWorkspaceEditTracker.ts
340
---
341
44 44 }
342
45 45
343
- 46
344
+ 46 push(e: T): void {
345
+ 47 this._arr.push(e);
346
+ 48 if (this._arr.length > this.maxSize) {
347
+ 49 this._arr.shift();
348
+ 50 }
349
+ 51 }
350
47 52 }
351
48 53
352
49 54 class DocumentState {
353
--------------
354
/c:/code/src/platform/inlineEdits/common/workspaceEditTracker/nesWorkspaceEditTracker.ts
355
---
356
15 15 export class NesWorkspaceEditTracker implements IWorkspaceEditTracker {
357
16 16 private readonly _documentState = new Map<DocumentUri, DocumentState>();
358
+ 17 private readonly _lastDocuments = new FifoQueue<DocumentUri>(5);
359
17 18
360
18 19 public handleDocumentOpened(docUri: DocumentUri, state: StringValue): void {
361
19 20 this._documentState.set(docUri, new DocumentState(state.value));
362
--------------
363
/c:/code/src/platform/inlineEdits/common/workspaceEditTracker/nesWorkspaceEditTracker.ts
364
---
365
23 23 public handleEdit(docUri: DocumentUri, edit: Edit): void {
366
24 24 const state = this._documentState.get(docUri)!;
367
+ 25 this._lastDocuments.push()
368
25 26 state.handleEdit(edit);
369
26 27 }
370
27 28
371
--------------
372
/c:/code/src/platform/inlineEdits/common/workspaceEditTracker/nesWorkspaceEditTracker.ts
373
---
374
15 15 export class NesWorkspaceEditTracker implements IWorkspaceEditTracker {
375
16 16 private readonly _documentState = new Map<DocumentUri, DocumentState>();
376
- 17 private readonly _lastDocuments = new FifoQueue<DocumentUri>(5);
377
+ 17 private readonly _lastDocuments = new FifoSet<DocumentState>(5);
378
18 18
379
19 19 public handleDocumentOpened(docUri: DocumentUri, state: StringValue): void {
380
20 20 this._documentState.set(docUri, new DocumentState(state.value));
381
---
382
38 38 }
383
39 39
384
- 40 class FifoQueue<T> {
385
+ 40 class FifoSet<T> {
386
41 41 private _arr: T[] = [];
387
42 42
388
43 43 constructor(
389
--------------
390
/c:/code/src/platform/inlineEdits/common/workspaceEditTracker/nesWorkspaceEditTracker.ts
391
---
392
47 47
393
48 48 push(e: T): void {
394
- 49 this._arr.push(e);
395
- 50 if (this._arr.length > this.maxSize) {
396
- 51 this._arr.shift();
397
- 52 }
398
+ 49
399
53 50 }
400
54 51 }
401
55 52
402
--------------
403
/c:/code/src/platform/inlineEdits/common/workspaceEditTracker/nesWorkspaceEditTracker.ts
404
---
405
406
--------------
407
/c:/code/src/platform/inlineEdits/common/workspaceEditTracker/nesWorkspaceEditTracker.ts
408
---
409
47 47
410
48 48 push(e: T): void {
411
- 49
412
+ 49 const existing = this._arr.indexOf(e);
413
+ 50
414
50 51 }
415
51 52 }
416
52 53
417
--------------
418
/c:/code/src/platform/inlineEdits/common/workspaceEditTracker/nesWorkspaceEditTracker.ts
419
---
420
48 48 push(e: T): void {
421
49 49 const existing = this._arr.indexOf(e);
422
- 50
423
+ 50 if (existing !== -1) {
424
+ 51 this._arr.splice(existing, 1);
425
+ 52 } else if (this._arr.length >= this.maxSize) {
426
+ 53 this._arr.shift();
427
+ 54 }
428
51 55 }
429
52 56 }
430
53 57
431
--------------
432
/c:/code/src/platform/inlineEdits/common/workspaceEditTracker/nesWorkspaceEditTracker.ts
433
---
434
50 50 if (existing !== -1) {
435
51 51 this._arr.splice(existing, 1);
436
+ 52
437
52 53 } else if (this._arr.length >= this.maxSize) {
438
53 54 this._arr.shift();
439
54 55 }
440
--------------
441
/c:/code/src/platform/inlineEdits/common/workspaceEditTracker/nesWorkspaceEditTracker.ts
442
---
443
54 54 this._arr.shift();
444
55 55 }
445
+ 56 this._arr.push(e);
446
56 57 }
447
57 58 }
448
58 59
449
--------------
450
/c:/code/src/platform/inlineEdits/common/workspaceEditTracker/nesWorkspaceEditTracker.ts
451
---
452
50 50 if (existing !== -1) {
453
51 51 this._arr.splice(existing, 1);
454
- 52
455
53 52 } else if (this._arr.length >= this.maxSize) {
456
54 53 this._arr.shift();
457
55 54 }
458
--------------
459
/c:/code/src/platform/inlineEdits/common/workspaceEditTracker/nesWorkspaceEditTracker.ts
460
---
461
23 23 public handleEdit(docUri: DocumentUri, edit: Edit): void {
462
24 24 const state = this._documentState.get(docUri)!;
463
- 25 this._lastDocuments.push()
464
+ 25 this._lastDocuments.push(state);
465
26 26 state.handleEdit(edit);
466
27 27 }
467
28 28
468
--------------
469
/c:/code/src/platform/inlineEdits/common/workspaceEditTracker/nesWorkspaceEditTracker.ts
470
---
471
86 86 }
472
87 87
473
- 88 getRecentEdit(): RecentWorkspaceEdits | undefined {
474
+ 88 getRecentEdit(editCount: number): RecentWorkspaceEdits | undefined {
475
89 89 this._applyStaleEdits();
476
90 90
477
91 91 if (this.edits.length === 0) { return undefined; }
478
--------------
479
/c:/code/src/platform/inlineEdits/common/workspaceEditTracker/nesWorkspaceEditTracker.ts
480
---
481
87 87
482
88 88 getRecentEdit(editCount: number): RecentWorkspaceEdits | undefined {
483
- 89 this._applyStaleEdits();
484
+ 89 this._applyStaleEdits(editCount);
485
90 90
486
91 91 if (this.edits.length === 0) { return undefined; }
487
92 92
488
--------------
489
/c:/code/src/platform/inlineEdits/common/workspaceEditTracker/nesWorkspaceEditTracker.ts
490
---
491
86 86 }
492
87 87
493
- 88 getRecentEdit(editCount: number): RecentWorkspaceEdits | undefined {
494
+ 88 getRecentEdit(editCount: number): { edits: RecentWorkspaceEdits; editCount: number } | undefined {
495
89 89 this._applyStaleEdits(editCount);
496
90 90
497
91 91 if (this.edits.length === 0) { return undefined; }
498
--------------
499
/c:/code/src/platform/inlineEdits/common/workspaceEditTracker/nesWorkspaceEditTracker.ts
500
---
501
99 99 }
502
100 100
503
- 101 private _applyStaleEdits(): void {
504
+ 101 private _applyStaleEdits(editCount: number): void {
505
102 102 let recentEdit = Edit.empty;
506
103 103 let i: number;
507
104 104 let count = 0;
508
--------------
509
/c:/code/src/platform/inlineEdits/common/workspaceEditTracker/nesWorkspaceEditTracker.ts
510
---
511
103 103 let i: number;
512
104 104 let count = 0;
513
- 105 for (i = this.edits.length - 1; i >= 0 && count < 5; i--, count++) {
514
+ 105 for (i = this.edits.length - 1; i >= 0 && count < editCount; i--, count++) {
515
106 106 const e = this.edits[i];
516
107 107
517
108 108 if (now() - e.instant > 10 * 60 * 1000) { break; }
518
--------------
519
/c:/code/src/platform/inlineEdits/common/workspaceEditTracker/nesWorkspaceEditTracker.ts
520
---
521
96 96
522
97 97 const result = new RootedEdit(this.baseValue, composedEdits);
523
- 98 return new RecentWorkspaceEdits(result, recentEditRange!);
524
+ 98 return {
525
+ 99 edits: new RecentWorkspaceEdits(result, recentEditRange!) };
526
99 100 }
527
100 101
528
101 102 private _applyStaleEdits(editCount: number): void {
529
--------------
530
/c:/code/src/platform/inlineEdits/common/workspaceEditTracker/nesWorkspaceEditTracker.ts
531
---
532
97 97 const result = new RootedEdit(this.baseValue, composedEdits);
533
98 98 return {
534
- 99 edits: new RecentWorkspaceEdits(result, recentEditRange!) };
535
+ 99 edits: new RecentDocumentEdit(result, recentEditRange!),
536
+ 100 editCount: this.edits.length,
537
+ 101 };
538
100 102 }
539
101 103
540
102 104 private _applyStaleEdits(editCount: number): void {
541
--------------
542
/c:/code/src/platform/inlineEdits/common/workspaceEditTracker/nesWorkspaceEditTracker.ts
543
---
544
11 11 import { TextLengthEdit } from '../dataTypes/textEditLength';
545
12 12 import { Instant, now } from '../utils/utils';
546
- 13 import { IWorkspaceEditTracker, RecentWorkspaceEdits } from './workspaceEditTracker';
547
+ 13 import { IWorkspaceEditTracker, RecentDocumentEdit, RecentWorkspaceEdits } from './workspaceEditTracker';
548
14 14
549
15 15 export class NesWorkspaceEditTracker implements IWorkspaceEditTracker {
550
16 16 private readonly _documentState = new Map<DocumentUri, DocumentState>();
551
--------------
552
/c:/code/src/platform/inlineEdits/common/workspaceEditTracker/nesWorkspaceEditTracker.ts
553
---
554
97 97 const result = new RootedEdit(this.baseValue, composedEdits);
555
98 98 return {
556
- 99 edits: new RecentDocumentEdit(result, recentEditRange!),
557
+ 99 edits: new RecentDocumentEdit(this.docUri, result, recentEditRange!),
558
100 100 editCount: this.edits.length,
559
101 101 };
560
102 102 }"
561
`);
562
});
563
564
describe('proximity strategy', () => {
565
566
/**
567
* Content layout (5 lines):
568
* line 1: "aaa"
569
* line 2: "bbb"
570
* line 3: "ccc"
571
* line 4: "ddd"
572
* line 5: "eee"
573
*
574
* Edit on line 1, then edit on line 2 — 0 lines apart → should merge with lineGap=1
575
*/
576
it('merges edits within lineGap', () => {
577
const recording: IRecordingInformation = {
578
log: [
579
{ documentType: '[email protected]', kind: 'header', repoRootUri: 'file:///Users/john/myProject', time: 0, uuid: '' },
580
{ time: 10, id: 0, kind: 'documentEncountered', relativePath: 'src/a.ts' },
581
{ time: 11, id: 0, v: 1, kind: 'setContent', content: 'aaa\nbbb\nccc\nddd\neee' },
582
// Replace line 1: "aaa" → "AAA" (offset 0-3)
583
{ time: 12, id: 0, v: 2, kind: 'changed', edit: [[0, 3, 'AAA']] },
584
// Replace line 2: "bbb" → "BBB" (offset 4-7, after "AAA\n")
585
{ time: 13, id: 0, v: 3, kind: 'changed', edit: [[4, 7, 'BBB']] },
586
]
587
};
588
const replayer = new ObservableWorkspaceRecordingReplayer(recording);
589
const tracker = createTracker(replayer.workspace, undefined, XtabEditMergeStrategy.proximity(1));
590
replayer.replay();
591
592
// Should produce 1 merged entry (adjacent lines, gap=0 ≤ 1)
593
expect(tracker.getHistory().length).toBe(1);
594
expect(historyToString(tracker)).toMatchInlineSnapshot(`
595
"- 1 aaa
596
+ 1 AAA
597
- 2 bbb
598
+ 2 BBB
599
3 3 ccc
600
4 4 ddd
601
5 5 eee"
602
`);
603
});
604
605
/**
606
* Edit on line 1, then edit on line 5 — 3 lines apart → should NOT merge with lineGap=1
607
*/
608
it('does not merge edits beyond lineGap', () => {
609
const recording: IRecordingInformation = {
610
log: [
611
{ documentType: '[email protected]', kind: 'header', repoRootUri: 'file:///Users/john/myProject', time: 0, uuid: '' },
612
{ time: 10, id: 0, kind: 'documentEncountered', relativePath: 'src/a.ts' },
613
{ time: 11, id: 0, v: 1, kind: 'setContent', content: 'aaa\nbbb\nccc\nddd\neee' },
614
// Replace line 1: "aaa" → "AAA" (offset 0-3)
615
{ time: 12, id: 0, v: 2, kind: 'changed', edit: [[0, 3, 'AAA']] },
616
// Replace line 5: "eee" → "EEE" (offset 16-19, after "AAA\nbbb\nccc\nddd\n")
617
{ time: 13, id: 0, v: 3, kind: 'changed', edit: [[16, 19, 'EEE']] },
618
]
619
};
620
const replayer = new ObservableWorkspaceRecordingReplayer(recording);
621
const tracker = createTracker(replayer.workspace, undefined, XtabEditMergeStrategy.proximity(1));
622
replayer.replay();
623
624
// Should produce 2 separate entries (distance = 3 > 1)
625
expect(historyToString(tracker)).toMatchInlineSnapshot(`
626
"- 1 aaa
627
+ 1 AAA
628
2 2 bbb
629
3 3 ccc
630
4 4 ddd
631
---
632
3 3 ccc
633
4 4 ddd
634
- 5 eee
635
+ 5 EEE"
636
`);
637
});
638
639
/**
640
* Edit on line 1, then edit on line 3 with lineGap=2 → distance=1, should merge
641
*/
642
it('merges edits exactly at lineGap boundary', () => {
643
const recording: IRecordingInformation = {
644
log: [
645
{ documentType: '[email protected]', kind: 'header', repoRootUri: 'file:///Users/john/myProject', time: 0, uuid: '' },
646
{ time: 10, id: 0, kind: 'documentEncountered', relativePath: 'src/a.ts' },
647
{ time: 11, id: 0, v: 1, kind: 'setContent', content: 'aaa\nbbb\nccc\nddd\neee' },
648
// Replace line 1
649
{ time: 12, id: 0, v: 2, kind: 'changed', edit: [[0, 3, 'AAA']] },
650
// Replace line 3 (offset 8-11, after "AAA\nbbb\n")
651
{ time: 13, id: 0, v: 3, kind: 'changed', edit: [[8, 11, 'CCC']] },
652
]
653
};
654
const replayer = new ObservableWorkspaceRecordingReplayer(recording);
655
const tracker = createTracker(replayer.workspace, undefined, XtabEditMergeStrategy.proximity(2));
656
replayer.replay();
657
658
// distance between line 1 and line 3 is 1 (one line apart), which is ≤ 2
659
expect(historyToString(tracker)).toMatchInlineSnapshot(`
660
"- 1 aaa
661
+ 1 AAA
662
2 2 bbb
663
- 3 ccc
664
+ 3 CCC
665
4 4 ddd
666
5 5 eee"
667
`);
668
});
669
670
/** lineGap=0 merges only when edits are on the same line (or touching lines) */
671
it('lineGap=0 does not merge edits on non-adjacent lines', () => {
672
const recording: IRecordingInformation = {
673
log: [
674
{ documentType: '[email protected]', kind: 'header', repoRootUri: 'file:///Users/john/myProject', time: 0, uuid: '' },
675
{ time: 10, id: 0, kind: 'documentEncountered', relativePath: 'src/a.ts' },
676
{ time: 11, id: 0, v: 1, kind: 'setContent', content: 'aaa\nbbb\nccc' },
677
// Replace on line 1
678
{ time: 12, id: 0, v: 2, kind: 'changed', edit: [[0, 3, 'AAA']] },
679
// Replace on line 3 (offset 8-11)
680
{ time: 13, id: 0, v: 3, kind: 'changed', edit: [[8, 11, 'CCC']] },
681
]
682
};
683
const replayer = new ObservableWorkspaceRecordingReplayer(recording);
684
const tracker = createTracker(replayer.workspace, undefined, XtabEditMergeStrategy.proximity(0));
685
replayer.replay();
686
687
// distance=1 > 0 → should NOT merge
688
expect(historyToString(tracker)).toMatchInlineSnapshot(`
689
"- 1 aaa
690
+ 1 AAA
691
2 2 bbb
692
3 3 ccc
693
---
694
1 1 AAA
695
2 2 bbb
696
- 3 ccc
697
+ 3 CCC"
698
`);
699
});
700
});
701
702
describe('hybrid strategy', () => {
703
704
/**
705
* Two rapid edits on adjacent lines → should merge
706
*/
707
it('merges rapid edits in same region', () => {
708
const recording: IRecordingInformation = {
709
log: [
710
{ documentType: '[email protected]', kind: 'header', repoRootUri: 'file:///Users/john/myProject', time: 0, uuid: '' },
711
{ time: 10, id: 0, kind: 'documentEncountered', relativePath: 'src/a.ts' },
712
{ time: 11, id: 0, v: 1, kind: 'setContent', content: 'aaa\nbbb\nccc\nddd\neee' },
713
{ time: 12, id: 0, v: 2, kind: 'changed', edit: [[0, 3, 'AAA']] },
714
{ time: 13, id: 0, v: 3, kind: 'changed', edit: [[4, 7, 'BBB']] },
715
]
716
};
717
718
overrideNowValue(1000);
719
const replayer = new ObservableWorkspaceRecordingReplayer(recording);
720
const tracker = createTracker(replayer.workspace, undefined, XtabEditMergeStrategy.hybrid(1, 2000));
721
replayer.replay();
722
723
// Both edits arrive at the same overridden time, within splitAfterMs and within lineGap → merge
724
expect(tracker.getHistory().length).toBe(1);
725
expect(historyToString(tracker)).toMatchInlineSnapshot(`
726
"- 1 aaa
727
+ 1 AAA
728
- 2 bbb
729
+ 2 BBB
730
3 3 ccc
731
4 4 ddd
732
5 5 eee"
733
`);
734
});
735
736
/**
737
* Same region but long pause between edits → should split
738
*/
739
it('splits edits separated by long pause', () => {
740
const recording: IRecordingInformation = {
741
log: [
742
{ documentType: '[email protected]', kind: 'header', repoRootUri: 'file:///Users/john/myProject', time: 0, uuid: '' },
743
{ time: 10, id: 0, kind: 'documentEncountered', relativePath: 'src/a.ts' },
744
{ time: 11, id: 0, v: 1, kind: 'setContent', content: 'aaa\nbbb\nccc\nddd\neee' },
745
{ time: 12, id: 0, v: 2, kind: 'changed', edit: [[0, 3, 'AAA']] },
746
{ time: 13, id: 0, v: 3, kind: 'changed', edit: [[4, 7, 'BBB']] },
747
]
748
};
749
750
overrideNowValue(1000);
751
const replayer = new ObservableWorkspaceRecordingReplayer(recording);
752
const tracker = createTracker(replayer.workspace, undefined, XtabEditMergeStrategy.hybrid(1, 500));
753
754
// Replay header + document + setContent
755
replayer.step(); // header
756
replayer.step(); // documentEncountered
757
replayer.step(); // setContent
758
759
// First edit at time 1000
760
overrideNowValue(1000);
761
replayer.step(); // changed: AAA
762
763
// Second edit at time 2000, 1000ms later > 500ms splitAfterMs
764
overrideNowValue(2000);
765
replayer.step(); // changed: BBB
766
767
// Should produce 2 separate entries
768
expect(historyToString(tracker)).toMatchInlineSnapshot(`
769
"- 1 aaa
770
+ 1 AAA
771
2 2 bbb
772
3 3 ccc
773
4 4 ddd
774
---
775
1 1 AAA
776
- 2 bbb
777
+ 2 BBB
778
3 3 ccc
779
4 4 ddd
780
5 5 eee"
781
`);
782
});
783
784
/**
785
* Rapid edits but far apart → should split due to distance despite being rapid
786
*/
787
it('splits rapid edits that are far apart', () => {
788
overrideNowValue(1000);
789
790
const recording: IRecordingInformation = {
791
log: [
792
{ documentType: '[email protected]', kind: 'header', repoRootUri: 'file:///Users/john/myProject', time: 0, uuid: '' },
793
{ time: 10, id: 0, kind: 'documentEncountered', relativePath: 'src/a.ts' },
794
{ time: 11, id: 0, v: 1, kind: 'setContent', content: 'aaa\nbbb\nccc\nddd\neee' },
795
// Line 1 edit
796
{ time: 12, id: 0, v: 2, kind: 'changed', edit: [[0, 3, 'AAA']] },
797
// Line 5 edit (offset 16-19, distance=3 > lineGap=1)
798
{ time: 13, id: 0, v: 3, kind: 'changed', edit: [[16, 19, 'EEE']] },
799
]
800
};
801
const replayer = new ObservableWorkspaceRecordingReplayer(recording);
802
const tracker = createTracker(replayer.workspace, undefined, XtabEditMergeStrategy.hybrid(1, 5000));
803
replayer.replay();
804
805
// Even though both are rapid (same overrideNowValue), distance=3 > lineGap=1 → split
806
expect(historyToString(tracker)).toMatchInlineSnapshot(`
807
"- 1 aaa
808
+ 1 AAA
809
2 2 bbb
810
3 3 ccc
811
4 4 ddd
812
---
813
3 3 ccc
814
4 4 ddd
815
- 5 eee
816
+ 5 EEE"
817
`);
818
});
819
820
/**
821
* Three edit bursts: rapid-on-same-line, pause, rapid-on-same-line → 2 entries
822
*/
823
it('creates separate entries per logical burst', () => {
824
const recording: IRecordingInformation = {
825
log: [
826
{ documentType: '[email protected]', kind: 'header', repoRootUri: 'file:///Users/john/myProject', time: 0, uuid: '' },
827
{ time: 10, id: 0, kind: 'documentEncountered', relativePath: 'src/a.ts' },
828
{ time: 11, id: 0, v: 1, kind: 'setContent', content: 'aaa\nbbb\nccc' },
829
// Burst 1: two rapid edits on line 1
830
{ time: 12, id: 0, v: 2, kind: 'changed', edit: [[0, 1, 'A']] },
831
{ time: 13, id: 0, v: 3, kind: 'changed', edit: [[1, 2, 'A']] },
832
// Burst 2: edit on line 1 again, after pause
833
{ time: 14, id: 0, v: 4, kind: 'changed', edit: [[2, 3, 'A']] },
834
]
835
};
836
837
overrideNowValue(1000);
838
const replayer = new ObservableWorkspaceRecordingReplayer(recording);
839
const tracker = createTracker(replayer.workspace, undefined, XtabEditMergeStrategy.hybrid(1, 500));
840
841
// Replay header + document + setContent
842
replayer.step(); // header
843
replayer.step(); // documentEncountered
844
replayer.step(); // setContent
845
846
// Burst 1
847
overrideNowValue(1000);
848
replayer.step(); // edit 1
849
overrideNowValue(1100);
850
replayer.step(); // edit 2
851
852
// Pause...
853
// Burst 2
854
overrideNowValue(5000);
855
replayer.step(); // edit 3
856
857
// Burst 1 merged into 1 entry, burst 2 is separate → 2 entries
858
expect(historyToString(tracker)).toMatchInlineSnapshot(`
859
"- 1 aaa
860
+ 1 AAa
861
2 2 bbb
862
3 3 ccc
863
---
864
- 1 AAa
865
+ 1 AAA
866
2 2 bbb
867
3 3 ccc"
868
`);
869
});
870
});
871
});
872
873