Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/copilot/src/util/common/diff.ts
13397 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
/**
7
* Represents information about a specific difference between two sequences.
8
*/
9
export class DiffChange {
10
/**
11
* The position of the first element in the original sequence which
12
* this change affects.
13
*/
14
public originalStart: number;
15
16
/**
17
* The number of elements from the original sequence which were
18
* affected.
19
*/
20
public originalLength: number;
21
22
/**
23
* The position of the first element in the modified sequence which
24
* this change affects.
25
*/
26
public modifiedStart: number;
27
28
/**
29
* The number of elements from the modified sequence which were
30
* affected (added).
31
*/
32
public modifiedLength: number;
33
34
/**
35
* Constructs a new DiffChange with the given sequence information
36
* and content.
37
*/
38
constructor(originalStart: number, originalLength: number, modifiedStart: number, modifiedLength: number) {
39
//Debug.Assert(originalLength > 0 || modifiedLength > 0, "originalLength and modifiedLength cannot both be <= 0");
40
this.originalStart = originalStart;
41
this.originalLength = originalLength;
42
this.modifiedStart = modifiedStart;
43
this.modifiedLength = modifiedLength;
44
}
45
46
/**
47
* The end point (exclusive) of the change in the original sequence.
48
*/
49
public getOriginalEnd() {
50
return this.originalStart + this.originalLength;
51
}
52
53
/**
54
* The end point (exclusive) of the change in the modified sequence.
55
*/
56
public getModifiedEnd() {
57
return this.modifiedStart + this.modifiedLength;
58
}
59
}
60
61
export interface ISequence {
62
getElements(): string[];
63
}
64
65
export class LineSequence implements ISequence {
66
67
constructor(
68
public readonly lines: readonly string[],
69
public readonly trimWhitespace: boolean = true
70
) { }
71
72
public getElements(): string[] {
73
const elements: string[] = [];
74
for (let i = 0, len = this.lines.length; i < len; i++) {
75
elements[i] = this.trimWhitespace ? this.lines[i].trim() : this.lines[i];
76
}
77
return elements;
78
}
79
80
public getCharCount(): number {
81
let cnt = 0;
82
for (const line of this.lines) {
83
cnt += line.length;
84
}
85
return cnt;
86
}
87
}
88
89
export class CharSequence implements ISequence {
90
91
private readonly _chars: string[];
92
93
constructor(str: string) {
94
this._chars = str.split('');
95
}
96
97
public getElements(): string[] {
98
return this._chars;
99
}
100
}
101
102
//
103
// The code below has been ported from a C# implementation in VS
104
//
105
106
class Debug {
107
public static Assert(condition: boolean, message: string): void {
108
if (!condition) {
109
throw new Error(message);
110
}
111
}
112
}
113
114
class MyArray {
115
/**
116
* Copies a range of elements from an Array starting at the specified source index and pastes
117
* them to another Array starting at the specified destination index. The length and the indexes
118
* are specified as 64-bit integers.
119
* sourceArray:
120
* The Array that contains the data to copy.
121
* sourceIndex:
122
* A 64-bit integer that represents the index in the sourceArray at which copying begins.
123
* destinationArray:
124
* The Array that receives the data.
125
* destinationIndex:
126
* A 64-bit integer that represents the index in the destinationArray at which storing begins.
127
* length:
128
* A 64-bit integer that represents the number of elements to copy.
129
*/
130
public static Copy(
131
sourceArray: any[],
132
sourceIndex: number,
133
destinationArray: any[],
134
destinationIndex: number,
135
length: number
136
) {
137
for (let i = 0; i < length; i++) {
138
destinationArray[destinationIndex + i] = sourceArray[sourceIndex + i];
139
}
140
}
141
public static Copy2(
142
sourceArray: Int32Array,
143
sourceIndex: number,
144
destinationArray: Int32Array,
145
destinationIndex: number,
146
length: number
147
) {
148
for (let i = 0; i < length; i++) {
149
destinationArray[destinationIndex + i] = sourceArray[sourceIndex + i];
150
}
151
}
152
}
153
154
//*****************************************************************************
155
// LcsDiff.cs
156
//
157
// An implementation of the difference algorithm described in
158
// "An O(ND) Difference Algorithm and its variations" by Eugene W. Myers
159
//
160
// Copyright (C) 2008 Microsoft Corporation @minifier_do_not_preserve
161
//*****************************************************************************
162
163
// Our total memory usage for storing history is (worst-case):
164
// 2 * [(MaxDifferencesHistory + 1) * (MaxDifferencesHistory + 1) - 1] * sizeof(int)
165
// 2 * [1448*1448 - 1] * 4 = 16773624 = 16MB
166
const enum LocalConstants {
167
MaxDifferencesHistory = 1447,
168
169
/**
170
* MAX SMI (SMall Integer) as defined in v8.
171
* one bit is lost for boxing/unboxing flag.
172
* one bit is lost for sign flag.
173
* See https://thibaultlaurens.github.io/javascript/2013/04/29/how-the-v8-engine-works/#tagged-values
174
*/
175
MAX_SAFE_SMALL_INTEGER = 1 << 30,
176
177
/**
178
* MIN SMI (SMall Integer) as defined in v8.
179
* one bit is lost for boxing/unboxing flag.
180
* one bit is lost for sign flag.
181
* See https://thibaultlaurens.github.io/javascript/2013/04/29/how-the-v8-engine-works/#tagged-values
182
*/
183
MIN_SAFE_SMALL_INTEGER = -(1 << 30),
184
}
185
186
/**
187
* A utility class which helps to create the set of DiffChanges from
188
* a difference operation. This class accepts original DiffElements and
189
* modified DiffElements that are involved in a particular change. The
190
* MarkNextChange() method can be called to mark the separation between
191
* distinct changes. At the end, the Changes property can be called to retrieve
192
* the constructed changes.
193
*/
194
class DiffChangeHelper {
195
private m_changes: DiffChange[];
196
private m_originalStart: number;
197
private m_modifiedStart: number;
198
private m_originalCount: number;
199
private m_modifiedCount: number;
200
201
/**
202
* Constructs a new DiffChangeHelper for the given DiffSequences.
203
*/
204
constructor() {
205
this.m_changes = [];
206
this.m_originalStart = LocalConstants.MAX_SAFE_SMALL_INTEGER;
207
this.m_modifiedStart = LocalConstants.MAX_SAFE_SMALL_INTEGER;
208
this.m_originalCount = 0;
209
this.m_modifiedCount = 0;
210
}
211
212
/**
213
* Marks the beginning of the next change in the set of differences.
214
*/
215
public MarkNextChange(): void {
216
// Only add to the list if there is something to add
217
if (this.m_originalCount > 0 || this.m_modifiedCount > 0) {
218
// Add the new change to our list
219
this.m_changes.push(
220
new DiffChange(this.m_originalStart, this.m_originalCount, this.m_modifiedStart, this.m_modifiedCount)
221
);
222
}
223
224
// Reset for the next change
225
this.m_originalCount = 0;
226
this.m_modifiedCount = 0;
227
this.m_originalStart = LocalConstants.MAX_SAFE_SMALL_INTEGER;
228
this.m_modifiedStart = LocalConstants.MAX_SAFE_SMALL_INTEGER;
229
}
230
231
/**
232
* Adds the original element at the given position to the elements
233
* affected by the current change. The modified index gives context
234
* to the change position with respect to the original sequence.
235
* @param originalIndex The index of the original element to add.
236
* @param modifiedIndex The index of the modified element that provides corresponding position in the modified sequence.
237
*/
238
public AddOriginalElement(originalIndex: number, modifiedIndex: number) {
239
// The 'true' start index is the smallest of the ones we've seen
240
this.m_originalStart = Math.min(this.m_originalStart, originalIndex);
241
this.m_modifiedStart = Math.min(this.m_modifiedStart, modifiedIndex);
242
243
this.m_originalCount++;
244
}
245
246
/**
247
* Adds the modified element at the given position to the elements
248
* affected by the current change. The original index gives context
249
* to the change position with respect to the modified sequence.
250
* @param originalIndex The index of the original element that provides corresponding position in the original sequence.
251
* @param modifiedIndex The index of the modified element to add.
252
*/
253
public AddModifiedElement(originalIndex: number, modifiedIndex: number): void {
254
// The 'true' start index is the smallest of the ones we've seen
255
this.m_originalStart = Math.min(this.m_originalStart, originalIndex);
256
this.m_modifiedStart = Math.min(this.m_modifiedStart, modifiedIndex);
257
258
this.m_modifiedCount++;
259
}
260
261
/**
262
* Retrieves all of the changes marked by the class.
263
*/
264
public getChanges(): DiffChange[] {
265
if (this.m_originalCount > 0 || this.m_modifiedCount > 0) {
266
// Finish up on whatever is left
267
this.MarkNextChange();
268
}
269
270
return this.m_changes;
271
}
272
273
/**
274
* Retrieves all of the changes marked by the class in the reverse order
275
*/
276
public getReverseChanges(): DiffChange[] {
277
if (this.m_originalCount > 0 || this.m_modifiedCount > 0) {
278
// Finish up on whatever is left
279
this.MarkNextChange();
280
}
281
282
this.m_changes.reverse();
283
return this.m_changes;
284
}
285
}
286
287
/**
288
* An implementation of the difference algorithm described in
289
* "An O(ND) Difference Algorithm and its variations" by Eugene W. Myers
290
*/
291
export class LcsDiff {
292
private readonly _originalStringElements: string[];
293
private readonly _originalElementsOrHash: Int32Array;
294
private readonly _modifiedStringElements: string[];
295
private readonly _modifiedElementsOrHash: Int32Array;
296
297
private m_forwardHistory: Int32Array[];
298
private m_reverseHistory: Int32Array[];
299
300
/**
301
* Constructs the DiffFinder
302
*/
303
constructor(originalSequence: ISequence, modifiedSequence: ISequence) {
304
const [originalStringElements, originalElementsOrHash] = LcsDiff._getElements(originalSequence);
305
const [modifiedStringElements, modifiedElementsOrHash] = LcsDiff._getElements(modifiedSequence);
306
307
this._originalStringElements = originalStringElements;
308
this._originalElementsOrHash = originalElementsOrHash;
309
this._modifiedStringElements = modifiedStringElements;
310
this._modifiedElementsOrHash = modifiedElementsOrHash;
311
312
this.m_forwardHistory = [];
313
this.m_reverseHistory = [];
314
}
315
316
private static _getElements(sequence: ISequence): [string[], Int32Array] {
317
const elements = sequence.getElements();
318
const hashes = new Int32Array(elements.length);
319
for (let i = 0, len = elements.length; i < len; i++) {
320
hashes[i] = this._stringHash(elements[i], 0);
321
}
322
return [elements, hashes];
323
}
324
325
/**
326
* Computes a int32 hash code for the given number.
327
*/
328
private static _numberHash(val: number, initialHashVal: number): number {
329
return ((initialHashVal << 5) - initialHashVal + val) | 0; // hashVal * 31 + ch, keep as int32
330
}
331
332
/**
333
* Computes a int32 hash code for the given string.
334
*/
335
private static _stringHash(s: string, hashVal: number) {
336
hashVal = this._numberHash(149417, hashVal);
337
for (let i = 0, length = s.length; i < length; i++) {
338
hashVal = this._numberHash(s.charCodeAt(i), hashVal);
339
}
340
return hashVal;
341
}
342
343
private ElementsAreEqual(originalIndex: number, newIndex: number): boolean {
344
if (this._originalElementsOrHash[originalIndex] !== this._modifiedElementsOrHash[newIndex]) {
345
return false;
346
}
347
return this._originalStringElements[originalIndex] === this._modifiedStringElements[newIndex];
348
}
349
350
public ComputeDiff(): DiffChange[] {
351
return this._ComputeDiff(
352
0,
353
this._originalElementsOrHash.length - 1,
354
0,
355
this._modifiedElementsOrHash.length - 1
356
);
357
}
358
359
/**
360
* Computes the differences between the original and modified input
361
* sequences on the bounded range.
362
* @returns An array of the differences between the two input sequences.
363
*/
364
private _ComputeDiff(
365
originalStart: number,
366
originalEnd: number,
367
modifiedStart: number,
368
modifiedEnd: number
369
): DiffChange[] {
370
return this.ComputeDiffRecursive(originalStart, originalEnd, modifiedStart, modifiedEnd);
371
}
372
373
/**
374
* Private helper method which computes the differences on the bounded range
375
* recursively.
376
* @returns An array of the differences between the two input sequences.
377
*/
378
private ComputeDiffRecursive(
379
originalStart: number,
380
originalEnd: number,
381
modifiedStart: number,
382
modifiedEnd: number
383
): DiffChange[] {
384
// Find the start of the differences
385
while (
386
originalStart <= originalEnd &&
387
modifiedStart <= modifiedEnd &&
388
this.ElementsAreEqual(originalStart, modifiedStart)
389
) {
390
originalStart++;
391
modifiedStart++;
392
}
393
394
// Find the end of the differences
395
while (
396
originalEnd >= originalStart &&
397
modifiedEnd >= modifiedStart &&
398
this.ElementsAreEqual(originalEnd, modifiedEnd)
399
) {
400
originalEnd--;
401
modifiedEnd--;
402
}
403
404
// In the special case where we either have all insertions or all deletions or the sequences are identical
405
if (originalStart > originalEnd || modifiedStart > modifiedEnd) {
406
let changes: DiffChange[];
407
408
if (modifiedStart <= modifiedEnd) {
409
Debug.Assert(
410
originalStart === originalEnd + 1,
411
'originalStart should only be one more than originalEnd'
412
);
413
414
// All insertions
415
changes = [new DiffChange(originalStart, 0, modifiedStart, modifiedEnd - modifiedStart + 1)];
416
} else if (originalStart <= originalEnd) {
417
Debug.Assert(
418
modifiedStart === modifiedEnd + 1,
419
'modifiedStart should only be one more than modifiedEnd'
420
);
421
422
// All deletions
423
changes = [new DiffChange(originalStart, originalEnd - originalStart + 1, modifiedStart, 0)];
424
} else {
425
Debug.Assert(
426
originalStart === originalEnd + 1,
427
'originalStart should only be one more than originalEnd'
428
);
429
Debug.Assert(
430
modifiedStart === modifiedEnd + 1,
431
'modifiedStart should only be one more than modifiedEnd'
432
);
433
434
// Identical sequences - No differences
435
changes = [];
436
}
437
438
return changes;
439
}
440
441
// This problem can be solved using the Divide-And-Conquer technique.
442
const midOriginalArr = [0];
443
const midModifiedArr = [0];
444
const result = this.ComputeRecursionPoint(
445
originalStart,
446
originalEnd,
447
modifiedStart,
448
modifiedEnd,
449
midOriginalArr,
450
midModifiedArr
451
);
452
453
const midOriginal = midOriginalArr[0];
454
const midModified = midModifiedArr[0];
455
456
if (result !== null) {
457
// Result is not-null when there was enough memory to compute the changes while
458
// searching for the recursion point
459
return result;
460
} else {
461
// We can break the problem down recursively by finding the changes in the
462
// First Half: (originalStart, modifiedStart) to (midOriginal, midModified)
463
// Second Half: (midOriginal + 1, minModified + 1) to (originalEnd, modifiedEnd)
464
// NOTE: ComputeDiff() is inclusive, therefore the second range starts on the next point
465
466
const leftChanges = this.ComputeDiffRecursive(originalStart, midOriginal, modifiedStart, midModified);
467
const rightChanges = this.ComputeDiffRecursive(midOriginal + 1, originalEnd, midModified + 1, modifiedEnd);
468
469
return this.ConcatenateChanges(leftChanges, rightChanges);
470
}
471
}
472
473
private WALKTRACE(
474
diagonalForwardBase: number,
475
diagonalForwardStart: number,
476
diagonalForwardEnd: number,
477
diagonalForwardOffset: number,
478
diagonalReverseBase: number,
479
diagonalReverseStart: number,
480
diagonalReverseEnd: number,
481
diagonalReverseOffset: number,
482
forwardPoints: Int32Array,
483
reversePoints: Int32Array,
484
originalIndex: number,
485
originalEnd: number,
486
midOriginalArr: number[],
487
modifiedIndex: number,
488
modifiedEnd: number,
489
midModifiedArr: number[],
490
deltaIsEven: boolean
491
): DiffChange[] {
492
let forwardChanges: DiffChange[] | null = null;
493
let reverseChanges: DiffChange[] | null = null;
494
495
// First, walk backward through the forward diagonals history
496
let changeHelper = new DiffChangeHelper();
497
let diagonalMin = diagonalForwardStart;
498
let diagonalMax = diagonalForwardEnd;
499
let diagonalRelative = midOriginalArr[0] - midModifiedArr[0] - diagonalForwardOffset;
500
let lastOriginalIndex = LocalConstants.MIN_SAFE_SMALL_INTEGER;
501
let historyIndex = this.m_forwardHistory.length - 1;
502
503
do {
504
// Get the diagonal index from the relative diagonal number
505
const diagonal = diagonalRelative + diagonalForwardBase;
506
507
// Figure out where we came from
508
if (
509
diagonal === diagonalMin ||
510
(diagonal < diagonalMax && forwardPoints[diagonal - 1] < forwardPoints[diagonal + 1])
511
) {
512
// Vertical line (the element is an insert)
513
originalIndex = forwardPoints[diagonal + 1];
514
modifiedIndex = originalIndex - diagonalRelative - diagonalForwardOffset;
515
if (originalIndex < lastOriginalIndex) {
516
changeHelper.MarkNextChange();
517
}
518
lastOriginalIndex = originalIndex;
519
changeHelper.AddModifiedElement(originalIndex + 1, modifiedIndex);
520
diagonalRelative = diagonal + 1 - diagonalForwardBase; //Setup for the next iteration
521
} else {
522
// Horizontal line (the element is a deletion)
523
originalIndex = forwardPoints[diagonal - 1] + 1;
524
modifiedIndex = originalIndex - diagonalRelative - diagonalForwardOffset;
525
if (originalIndex < lastOriginalIndex) {
526
changeHelper.MarkNextChange();
527
}
528
lastOriginalIndex = originalIndex - 1;
529
changeHelper.AddOriginalElement(originalIndex, modifiedIndex + 1);
530
diagonalRelative = diagonal - 1 - diagonalForwardBase; //Setup for the next iteration
531
}
532
533
if (historyIndex >= 0) {
534
forwardPoints = this.m_forwardHistory[historyIndex];
535
diagonalForwardBase = forwardPoints[0]; //We stored this in the first spot
536
diagonalMin = 1;
537
diagonalMax = forwardPoints.length - 1;
538
}
539
} while (--historyIndex >= -1);
540
541
// Ironically, we get the forward changes as the reverse of the
542
// order we added them since we technically added them backwards
543
forwardChanges = changeHelper.getReverseChanges();
544
545
// Now walk backward through the reverse diagonals history
546
changeHelper = new DiffChangeHelper();
547
diagonalMin = diagonalReverseStart;
548
diagonalMax = diagonalReverseEnd;
549
diagonalRelative = midOriginalArr[0] - midModifiedArr[0] - diagonalReverseOffset;
550
lastOriginalIndex = LocalConstants.MAX_SAFE_SMALL_INTEGER;
551
historyIndex = deltaIsEven ? this.m_reverseHistory.length - 1 : this.m_reverseHistory.length - 2;
552
553
do {
554
// Get the diagonal index from the relative diagonal number
555
const diagonal = diagonalRelative + diagonalReverseBase;
556
557
// Figure out where we came from
558
if (
559
diagonal === diagonalMin ||
560
(diagonal < diagonalMax && reversePoints[diagonal - 1] >= reversePoints[diagonal + 1])
561
) {
562
// Horizontal line (the element is a deletion))
563
originalIndex = reversePoints[diagonal + 1] - 1;
564
modifiedIndex = originalIndex - diagonalRelative - diagonalReverseOffset;
565
if (originalIndex > lastOriginalIndex) {
566
changeHelper.MarkNextChange();
567
}
568
lastOriginalIndex = originalIndex + 1;
569
changeHelper.AddOriginalElement(originalIndex + 1, modifiedIndex + 1);
570
diagonalRelative = diagonal + 1 - diagonalReverseBase; //Setup for the next iteration
571
} else {
572
// Vertical line (the element is an insertion)
573
originalIndex = reversePoints[diagonal - 1];
574
modifiedIndex = originalIndex - diagonalRelative - diagonalReverseOffset;
575
if (originalIndex > lastOriginalIndex) {
576
changeHelper.MarkNextChange();
577
}
578
lastOriginalIndex = originalIndex;
579
changeHelper.AddModifiedElement(originalIndex + 1, modifiedIndex + 1);
580
diagonalRelative = diagonal - 1 - diagonalReverseBase; //Setup for the next iteration
581
}
582
583
if (historyIndex >= 0) {
584
reversePoints = this.m_reverseHistory[historyIndex];
585
diagonalReverseBase = reversePoints[0]; //We stored this in the first spot
586
diagonalMin = 1;
587
diagonalMax = reversePoints.length - 1;
588
}
589
} while (--historyIndex >= -1);
590
591
// There are cases where the reverse history will find diffs that
592
// are correct, but not intuitive, so we need shift them.
593
reverseChanges = changeHelper.getChanges();
594
595
return this.ConcatenateChanges(forwardChanges, reverseChanges);
596
}
597
598
/**
599
* Given the range to compute the diff on, this method finds the point:
600
* (midOriginal, midModified)
601
* that exists in the middle of the LCS of the two sequences and
602
* is the point at which the LCS problem may be broken down recursively.
603
* This method will try to keep the LCS trace in memory. If the LCS recursion
604
* point is calculated and the full trace is available in memory, then this method
605
* will return the change list.
606
* @param originalStart The start bound of the original sequence range
607
* @param originalEnd The end bound of the original sequence range
608
* @param modifiedStart The start bound of the modified sequence range
609
* @param modifiedEnd The end bound of the modified sequence range
610
* @param midOriginal The middle point of the original sequence range
611
* @param midModified The middle point of the modified sequence range
612
* @returns The diff changes, if available, otherwise null
613
*/
614
private ComputeRecursionPoint(
615
originalStart: number,
616
originalEnd: number,
617
modifiedStart: number,
618
modifiedEnd: number,
619
midOriginalArr: number[],
620
midModifiedArr: number[]
621
) {
622
let originalIndex = 0,
623
modifiedIndex = 0;
624
let diagonalForwardStart = 0,
625
diagonalForwardEnd = 0;
626
let diagonalReverseStart = 0,
627
diagonalReverseEnd = 0;
628
629
// To traverse the edit graph and produce the proper LCS, our actual
630
// start position is just outside the given boundary
631
originalStart--;
632
modifiedStart--;
633
634
// We set these up to make the compiler happy, but they will
635
// be replaced before we return with the actual recursion point
636
midOriginalArr[0] = 0;
637
midModifiedArr[0] = 0;
638
639
// Clear out the history
640
this.m_forwardHistory = [];
641
this.m_reverseHistory = [];
642
643
// Each cell in the two arrays corresponds to a diagonal in the edit graph.
644
// The integer value in the cell represents the originalIndex of the furthest
645
// reaching point found so far that ends in that diagonal.
646
// The modifiedIndex can be computed mathematically from the originalIndex and the diagonal number.
647
const maxDifferences = originalEnd - originalStart + (modifiedEnd - modifiedStart);
648
const numDiagonals = maxDifferences + 1;
649
const forwardPoints = new Int32Array(numDiagonals);
650
const reversePoints = new Int32Array(numDiagonals);
651
// diagonalForwardBase: Index into forwardPoints of the diagonal which passes through (originalStart, modifiedStart)
652
// diagonalReverseBase: Index into reversePoints of the diagonal which passes through (originalEnd, modifiedEnd)
653
const diagonalForwardBase = modifiedEnd - modifiedStart;
654
const diagonalReverseBase = originalEnd - originalStart;
655
// diagonalForwardOffset: Geometric offset which allows modifiedIndex to be computed from originalIndex and the
656
// diagonal number (relative to diagonalForwardBase)
657
// diagonalReverseOffset: Geometric offset which allows modifiedIndex to be computed from originalIndex and the
658
// diagonal number (relative to diagonalReverseBase)
659
const diagonalForwardOffset = originalStart - modifiedStart;
660
const diagonalReverseOffset = originalEnd - modifiedEnd;
661
662
// delta: The difference between the end diagonal and the start diagonal. This is used to relate diagonal numbers
663
// relative to the start diagonal with diagonal numbers relative to the end diagonal.
664
// The Even/Oddn-ness of this delta is important for determining when we should check for overlap
665
const delta = diagonalReverseBase - diagonalForwardBase;
666
const deltaIsEven = delta % 2 === 0;
667
668
// Here we set up the start and end points as the furthest points found so far
669
// in both the forward and reverse directions, respectively
670
forwardPoints[diagonalForwardBase] = originalStart;
671
reversePoints[diagonalReverseBase] = originalEnd;
672
673
// A couple of points:
674
// --With this method, we iterate on the number of differences between the two sequences.
675
// The more differences there actually are, the longer this will take.
676
// --Also, as the number of differences increases, we have to search on diagonals further
677
// away from the reference diagonal (which is diagonalForwardBase for forward, diagonalReverseBase for reverse).
678
// --We extend on even diagonals (relative to the reference diagonal) only when numDifferences
679
// is even and odd diagonals only when numDifferences is odd.
680
for (let numDifferences = 1; numDifferences <= maxDifferences / 2 + 1; numDifferences++) {
681
let furthestOriginalIndex = 0;
682
let furthestModifiedIndex = 0;
683
684
// Run the algorithm in the forward direction
685
diagonalForwardStart = this.ClipDiagonalBound(
686
diagonalForwardBase - numDifferences,
687
numDifferences,
688
diagonalForwardBase,
689
numDiagonals
690
);
691
diagonalForwardEnd = this.ClipDiagonalBound(
692
diagonalForwardBase + numDifferences,
693
numDifferences,
694
diagonalForwardBase,
695
numDiagonals
696
);
697
for (let diagonal = diagonalForwardStart; diagonal <= diagonalForwardEnd; diagonal += 2) {
698
// STEP 1: We extend the furthest reaching point in the present diagonal
699
// by looking at the diagonals above and below and picking the one whose point
700
// is further away from the start point (originalStart, modifiedStart)
701
if (
702
diagonal === diagonalForwardStart ||
703
(diagonal < diagonalForwardEnd && forwardPoints[diagonal - 1] < forwardPoints[diagonal + 1])
704
) {
705
originalIndex = forwardPoints[diagonal + 1];
706
} else {
707
originalIndex = forwardPoints[diagonal - 1] + 1;
708
}
709
modifiedIndex = originalIndex - (diagonal - diagonalForwardBase) - diagonalForwardOffset;
710
711
// Save the current originalIndex so we can test for false overlap in step 3
712
const tempOriginalIndex = originalIndex;
713
714
// STEP 2: We can continue to extend the furthest reaching point in the present diagonal
715
// so long as the elements are equal.
716
while (
717
originalIndex < originalEnd &&
718
modifiedIndex < modifiedEnd &&
719
this.ElementsAreEqual(originalIndex + 1, modifiedIndex + 1)
720
) {
721
originalIndex++;
722
modifiedIndex++;
723
}
724
forwardPoints[diagonal] = originalIndex;
725
726
if (originalIndex + modifiedIndex > furthestOriginalIndex + furthestModifiedIndex) {
727
furthestOriginalIndex = originalIndex;
728
furthestModifiedIndex = modifiedIndex;
729
}
730
731
// STEP 3: If delta is odd (overlap first happens on forward when delta is odd)
732
// and diagonal is in the range of reverse diagonals computed for numDifferences-1
733
// (the previous iteration; we haven't computed reverse diagonals for numDifferences yet)
734
// then check for overlap.
735
if (!deltaIsEven && Math.abs(diagonal - diagonalReverseBase) <= numDifferences - 1) {
736
if (originalIndex >= reversePoints[diagonal]) {
737
midOriginalArr[0] = originalIndex;
738
midModifiedArr[0] = modifiedIndex;
739
740
if (
741
tempOriginalIndex <= reversePoints[diagonal] &&
742
LocalConstants.MaxDifferencesHistory > 0 &&
743
numDifferences <= LocalConstants.MaxDifferencesHistory + 1
744
) {
745
// BINGO! We overlapped, and we have the full trace in memory!
746
return this.WALKTRACE(
747
diagonalForwardBase,
748
diagonalForwardStart,
749
diagonalForwardEnd,
750
diagonalForwardOffset,
751
diagonalReverseBase,
752
diagonalReverseStart,
753
diagonalReverseEnd,
754
diagonalReverseOffset,
755
forwardPoints,
756
reversePoints,
757
originalIndex,
758
originalEnd,
759
midOriginalArr,
760
modifiedIndex,
761
modifiedEnd,
762
midModifiedArr,
763
deltaIsEven
764
);
765
} else {
766
// Either false overlap, or we didn't have enough memory for the full trace
767
// Just return the recursion point
768
return null;
769
}
770
}
771
}
772
}
773
774
// Run the algorithm in the reverse direction
775
diagonalReverseStart = this.ClipDiagonalBound(
776
diagonalReverseBase - numDifferences,
777
numDifferences,
778
diagonalReverseBase,
779
numDiagonals
780
);
781
diagonalReverseEnd = this.ClipDiagonalBound(
782
diagonalReverseBase + numDifferences,
783
numDifferences,
784
diagonalReverseBase,
785
numDiagonals
786
);
787
for (let diagonal = diagonalReverseStart; diagonal <= diagonalReverseEnd; diagonal += 2) {
788
// STEP 1: We extend the furthest reaching point in the present diagonal
789
// by looking at the diagonals above and below and picking the one whose point
790
// is further away from the start point (originalEnd, modifiedEnd)
791
if (
792
diagonal === diagonalReverseStart ||
793
(diagonal < diagonalReverseEnd && reversePoints[diagonal - 1] >= reversePoints[diagonal + 1])
794
) {
795
originalIndex = reversePoints[diagonal + 1] - 1;
796
} else {
797
originalIndex = reversePoints[diagonal - 1];
798
}
799
modifiedIndex = originalIndex - (diagonal - diagonalReverseBase) - diagonalReverseOffset;
800
801
// Save the current originalIndex so we can test for false overlap
802
const tempOriginalIndex = originalIndex;
803
804
// STEP 2: We can continue to extend the furthest reaching point in the present diagonal
805
// as long as the elements are equal.
806
while (
807
originalIndex > originalStart &&
808
modifiedIndex > modifiedStart &&
809
this.ElementsAreEqual(originalIndex, modifiedIndex)
810
) {
811
originalIndex--;
812
modifiedIndex--;
813
}
814
reversePoints[diagonal] = originalIndex;
815
816
// STEP 4: If delta is even (overlap first happens on reverse when delta is even)
817
// and diagonal is in the range of forward diagonals computed for numDifferences
818
// then check for overlap.
819
if (deltaIsEven && Math.abs(diagonal - diagonalForwardBase) <= numDifferences) {
820
if (originalIndex <= forwardPoints[diagonal]) {
821
midOriginalArr[0] = originalIndex;
822
midModifiedArr[0] = modifiedIndex;
823
824
if (
825
tempOriginalIndex >= forwardPoints[diagonal] &&
826
LocalConstants.MaxDifferencesHistory > 0 &&
827
numDifferences <= LocalConstants.MaxDifferencesHistory + 1
828
) {
829
// BINGO! We overlapped, and we have the full trace in memory!
830
return this.WALKTRACE(
831
diagonalForwardBase,
832
diagonalForwardStart,
833
diagonalForwardEnd,
834
diagonalForwardOffset,
835
diagonalReverseBase,
836
diagonalReverseStart,
837
diagonalReverseEnd,
838
diagonalReverseOffset,
839
forwardPoints,
840
reversePoints,
841
originalIndex,
842
originalEnd,
843
midOriginalArr,
844
modifiedIndex,
845
modifiedEnd,
846
midModifiedArr,
847
deltaIsEven
848
);
849
} else {
850
// Either false overlap, or we didn't have enough memory for the full trace
851
// Just return the recursion point
852
return null;
853
}
854
}
855
}
856
}
857
858
// Save current vectors to history before the next iteration
859
if (numDifferences <= LocalConstants.MaxDifferencesHistory) {
860
// We are allocating space for one extra int, which we fill with
861
// the index of the diagonal base index
862
let temp = new Int32Array(diagonalForwardEnd - diagonalForwardStart + 2);
863
temp[0] = diagonalForwardBase - diagonalForwardStart + 1;
864
MyArray.Copy2(
865
forwardPoints,
866
diagonalForwardStart,
867
temp,
868
1,
869
diagonalForwardEnd - diagonalForwardStart + 1
870
);
871
this.m_forwardHistory.push(temp);
872
873
temp = new Int32Array(diagonalReverseEnd - diagonalReverseStart + 2);
874
temp[0] = diagonalReverseBase - diagonalReverseStart + 1;
875
MyArray.Copy2(
876
reversePoints,
877
diagonalReverseStart,
878
temp,
879
1,
880
diagonalReverseEnd - diagonalReverseStart + 1
881
);
882
this.m_reverseHistory.push(temp);
883
}
884
}
885
886
// If we got here, then we have the full trace in history. We just have to convert it to a change list
887
// NOTE: This part is a bit messy
888
return this.WALKTRACE(
889
diagonalForwardBase,
890
diagonalForwardStart,
891
diagonalForwardEnd,
892
diagonalForwardOffset,
893
diagonalReverseBase,
894
diagonalReverseStart,
895
diagonalReverseEnd,
896
diagonalReverseOffset,
897
forwardPoints,
898
reversePoints,
899
originalIndex,
900
originalEnd,
901
midOriginalArr,
902
modifiedIndex,
903
modifiedEnd,
904
midModifiedArr,
905
deltaIsEven
906
);
907
}
908
909
/**
910
* Concatenates the two input DiffChange lists and returns the resulting
911
* list.
912
* @param The left changes
913
* @param The right changes
914
* @returns The concatenated list
915
*/
916
private ConcatenateChanges(left: DiffChange[], right: DiffChange[]): DiffChange[] {
917
const mergedChangeArr: DiffChange[] = [];
918
919
if (left.length === 0 || right.length === 0) {
920
return right.length > 0 ? right : left;
921
} else if (this.ChangesOverlap(left[left.length - 1], right[0], mergedChangeArr)) {
922
// Since we break the problem down recursively, it is possible that we
923
// might recurse in the middle of a change thereby splitting it into
924
// two changes. Here in the combining stage, we detect and fuse those
925
// changes back together
926
const result = new Array<DiffChange>(left.length + right.length - 1);
927
MyArray.Copy(left, 0, result, 0, left.length - 1);
928
result[left.length - 1] = mergedChangeArr[0];
929
MyArray.Copy(right, 1, result, left.length, right.length - 1);
930
931
return result;
932
} else {
933
const result = new Array<DiffChange>(left.length + right.length);
934
MyArray.Copy(left, 0, result, 0, left.length);
935
MyArray.Copy(right, 0, result, left.length, right.length);
936
937
return result;
938
}
939
}
940
941
/**
942
* Returns true if the two changes overlap and can be merged into a single
943
* change
944
* @param left The left change
945
* @param right The right change
946
* @param mergedChange The merged change if the two overlap, null otherwise
947
* @returns True if the two changes overlap
948
*/
949
private ChangesOverlap(left: DiffChange, right: DiffChange, mergedChangeArr: Array<DiffChange | null>): boolean {
950
Debug.Assert(
951
left.originalStart <= right.originalStart,
952
'Left change is not less than or equal to right change'
953
);
954
Debug.Assert(
955
left.modifiedStart <= right.modifiedStart,
956
'Left change is not less than or equal to right change'
957
);
958
959
if (
960
left.originalStart + left.originalLength >= right.originalStart ||
961
left.modifiedStart + left.modifiedLength >= right.modifiedStart
962
) {
963
const originalStart = left.originalStart;
964
let originalLength = left.originalLength;
965
const modifiedStart = left.modifiedStart;
966
let modifiedLength = left.modifiedLength;
967
968
if (left.originalStart + left.originalLength >= right.originalStart) {
969
originalLength = right.originalStart + right.originalLength - left.originalStart;
970
}
971
if (left.modifiedStart + left.modifiedLength >= right.modifiedStart) {
972
modifiedLength = right.modifiedStart + right.modifiedLength - left.modifiedStart;
973
}
974
975
mergedChangeArr[0] = new DiffChange(originalStart, originalLength, modifiedStart, modifiedLength);
976
return true;
977
} else {
978
mergedChangeArr[0] = null;
979
return false;
980
}
981
}
982
983
/**
984
* Helper method used to clip a diagonal index to the range of valid
985
* diagonals. This also decides whether or not the diagonal index,
986
* if it exceeds the boundary, should be clipped to the boundary or clipped
987
* one inside the boundary depending on the Even/Odd status of the boundary
988
* and numDifferences.
989
* @param diagonal The index of the diagonal to clip.
990
* @param numDifferences The current number of differences being iterated upon.
991
* @param diagonalBaseIndex The base reference diagonal.
992
* @param numDiagonals The total number of diagonals.
993
* @returns The clipped diagonal index.
994
*/
995
private ClipDiagonalBound(
996
diagonal: number,
997
numDifferences: number,
998
diagonalBaseIndex: number,
999
numDiagonals: number
1000
): number {
1001
if (diagonal >= 0 && diagonal < numDiagonals) {
1002
// Nothing to clip, its in range
1003
return diagonal;
1004
}
1005
1006
// diagonalsBelow: The number of diagonals below the reference diagonal
1007
// diagonalsAbove: The number of diagonals above the reference diagonal
1008
const diagonalsBelow = diagonalBaseIndex;
1009
const diagonalsAbove = numDiagonals - diagonalBaseIndex - 1;
1010
const diffEven = numDifferences % 2 === 0;
1011
1012
if (diagonal < 0) {
1013
const lowerBoundEven = diagonalsBelow % 2 === 0;
1014
return diffEven === lowerBoundEven ? 0 : 1;
1015
} else {
1016
const upperBoundEven = diagonalsAbove % 2 === 0;
1017
return diffEven === upperBoundEven ? numDiagonals - 1 : numDiagonals - 2;
1018
}
1019
}
1020
}
1021
1022