Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/copilot/src/util/vs/base/common/diff/diff.ts
13406 views
1
//!!! DO NOT modify, this file was COPIED from 'microsoft/vscode'
2
3
/*---------------------------------------------------------------------------------------------
4
* Copyright (c) Microsoft Corporation. All rights reserved.
5
* Licensed under the MIT License. See License.txt in the project root for license information.
6
*--------------------------------------------------------------------------------------------*/
7
8
import { DiffChange } from './diffChange';
9
import { stringHash } from '../hash';
10
import { Constants } from '../uint';
11
12
export class StringDiffSequence implements ISequence {
13
14
constructor(private source: string) { }
15
16
getElements(): Int32Array | number[] | string[] {
17
const source = this.source;
18
const characters = new Int32Array(source.length);
19
for (let i = 0, len = source.length; i < len; i++) {
20
characters[i] = source.charCodeAt(i);
21
}
22
return characters;
23
}
24
}
25
26
export function stringDiff(original: string, modified: string, pretty: boolean): IDiffChange[] {
27
return new LcsDiff(new StringDiffSequence(original), new StringDiffSequence(modified)).ComputeDiff(pretty).changes;
28
}
29
30
export interface ISequence {
31
getElements(): Int32Array | number[] | string[];
32
getStrictElement?(index: number): string;
33
}
34
35
export interface IDiffChange {
36
/**
37
* The position of the first element in the original sequence which
38
* this change affects.
39
*/
40
originalStart: number;
41
42
/**
43
* The number of elements from the original sequence which were
44
* affected.
45
*/
46
originalLength: number;
47
48
/**
49
* The position of the first element in the modified sequence which
50
* this change affects.
51
*/
52
modifiedStart: number;
53
54
/**
55
* The number of elements from the modified sequence which were
56
* affected (added).
57
*/
58
modifiedLength: number;
59
}
60
61
export interface IContinueProcessingPredicate {
62
(furthestOriginalIndex: number, matchLengthOfLongest: number): boolean;
63
}
64
65
export interface IDiffResult {
66
quitEarly: boolean;
67
changes: IDiffChange[];
68
}
69
70
//
71
// The code below has been ported from a C# implementation in VS
72
//
73
74
class Debug {
75
76
public static Assert(condition: boolean, message: string): void {
77
if (!condition) {
78
throw new Error(message);
79
}
80
}
81
}
82
83
class MyArray {
84
/**
85
* Copies a range of elements from an Array starting at the specified source index and pastes
86
* them to another Array starting at the specified destination index. The length and the indexes
87
* are specified as 64-bit integers.
88
* sourceArray:
89
* The Array that contains the data to copy.
90
* sourceIndex:
91
* A 64-bit integer that represents the index in the sourceArray at which copying begins.
92
* destinationArray:
93
* The Array that receives the data.
94
* destinationIndex:
95
* A 64-bit integer that represents the index in the destinationArray at which storing begins.
96
* length:
97
* A 64-bit integer that represents the number of elements to copy.
98
*/
99
public static Copy(sourceArray: unknown[], sourceIndex: number, destinationArray: unknown[], destinationIndex: number, length: number) {
100
for (let i = 0; i < length; i++) {
101
destinationArray[destinationIndex + i] = sourceArray[sourceIndex + i];
102
}
103
}
104
public static Copy2(sourceArray: Int32Array, sourceIndex: number, destinationArray: Int32Array, destinationIndex: number, length: number) {
105
for (let i = 0; i < length; i++) {
106
destinationArray[destinationIndex + i] = sourceArray[sourceIndex + i];
107
}
108
}
109
}
110
111
//*****************************************************************************
112
// LcsDiff.cs
113
//
114
// An implementation of the difference algorithm described in
115
// "An O(ND) Difference Algorithm and its variations" by Eugene W. Myers
116
//
117
// Copyright (C) 2008 Microsoft Corporation @minifier_do_not_preserve
118
//*****************************************************************************
119
120
// Our total memory usage for storing history is (worst-case):
121
// 2 * [(MaxDifferencesHistory + 1) * (MaxDifferencesHistory + 1) - 1] * sizeof(int)
122
// 2 * [1448*1448 - 1] * 4 = 16773624 = 16MB
123
const enum LocalConstants {
124
MaxDifferencesHistory = 1447
125
}
126
127
/**
128
* A utility class which helps to create the set of DiffChanges from
129
* a difference operation. This class accepts original DiffElements and
130
* modified DiffElements that are involved in a particular change. The
131
* MarkNextChange() method can be called to mark the separation between
132
* distinct changes. At the end, the Changes property can be called to retrieve
133
* the constructed changes.
134
*/
135
class DiffChangeHelper {
136
137
private m_changes: DiffChange[];
138
private m_originalStart: number;
139
private m_modifiedStart: number;
140
private m_originalCount: number;
141
private m_modifiedCount: number;
142
143
/**
144
* Constructs a new DiffChangeHelper for the given DiffSequences.
145
*/
146
constructor() {
147
this.m_changes = [];
148
this.m_originalStart = Constants.MAX_SAFE_SMALL_INTEGER;
149
this.m_modifiedStart = Constants.MAX_SAFE_SMALL_INTEGER;
150
this.m_originalCount = 0;
151
this.m_modifiedCount = 0;
152
}
153
154
/**
155
* Marks the beginning of the next change in the set of differences.
156
*/
157
public MarkNextChange(): void {
158
// Only add to the list if there is something to add
159
if (this.m_originalCount > 0 || this.m_modifiedCount > 0) {
160
// Add the new change to our list
161
this.m_changes.push(new DiffChange(this.m_originalStart, this.m_originalCount,
162
this.m_modifiedStart, this.m_modifiedCount));
163
}
164
165
// Reset for the next change
166
this.m_originalCount = 0;
167
this.m_modifiedCount = 0;
168
this.m_originalStart = Constants.MAX_SAFE_SMALL_INTEGER;
169
this.m_modifiedStart = Constants.MAX_SAFE_SMALL_INTEGER;
170
}
171
172
/**
173
* Adds the original element at the given position to the elements
174
* affected by the current change. The modified index gives context
175
* to the change position with respect to the original sequence.
176
* @param originalIndex The index of the original element to add.
177
* @param modifiedIndex The index of the modified element that provides corresponding position in the modified sequence.
178
*/
179
public AddOriginalElement(originalIndex: number, modifiedIndex: number) {
180
// The 'true' start index is the smallest of the ones we've seen
181
this.m_originalStart = Math.min(this.m_originalStart, originalIndex);
182
this.m_modifiedStart = Math.min(this.m_modifiedStart, modifiedIndex);
183
184
this.m_originalCount++;
185
}
186
187
/**
188
* Adds the modified element at the given position to the elements
189
* affected by the current change. The original index gives context
190
* to the change position with respect to the modified sequence.
191
* @param originalIndex The index of the original element that provides corresponding position in the original sequence.
192
* @param modifiedIndex The index of the modified element to add.
193
*/
194
public AddModifiedElement(originalIndex: number, modifiedIndex: number): void {
195
// The 'true' start index is the smallest of the ones we've seen
196
this.m_originalStart = Math.min(this.m_originalStart, originalIndex);
197
this.m_modifiedStart = Math.min(this.m_modifiedStart, modifiedIndex);
198
199
this.m_modifiedCount++;
200
}
201
202
/**
203
* Retrieves all of the changes marked by the class.
204
*/
205
public getChanges(): DiffChange[] {
206
if (this.m_originalCount > 0 || this.m_modifiedCount > 0) {
207
// Finish up on whatever is left
208
this.MarkNextChange();
209
}
210
211
return this.m_changes;
212
}
213
214
/**
215
* Retrieves all of the changes marked by the class in the reverse order
216
*/
217
public getReverseChanges(): DiffChange[] {
218
if (this.m_originalCount > 0 || this.m_modifiedCount > 0) {
219
// Finish up on whatever is left
220
this.MarkNextChange();
221
}
222
223
this.m_changes.reverse();
224
return this.m_changes;
225
}
226
227
}
228
229
/**
230
* An implementation of the difference algorithm described in
231
* "An O(ND) Difference Algorithm and its variations" by Eugene W. Myers
232
*/
233
export class LcsDiff {
234
235
private readonly ContinueProcessingPredicate: IContinueProcessingPredicate | null;
236
237
private readonly _originalSequence: ISequence;
238
private readonly _modifiedSequence: ISequence;
239
private readonly _hasStrings: boolean;
240
private readonly _originalStringElements: string[];
241
private readonly _originalElementsOrHash: Int32Array;
242
private readonly _modifiedStringElements: string[];
243
private readonly _modifiedElementsOrHash: Int32Array;
244
245
private m_forwardHistory: Int32Array[];
246
private m_reverseHistory: Int32Array[];
247
248
/**
249
* Constructs the DiffFinder
250
*/
251
constructor(originalSequence: ISequence, modifiedSequence: ISequence, continueProcessingPredicate: IContinueProcessingPredicate | null = null) {
252
this.ContinueProcessingPredicate = continueProcessingPredicate;
253
254
this._originalSequence = originalSequence;
255
this._modifiedSequence = modifiedSequence;
256
257
const [originalStringElements, originalElementsOrHash, originalHasStrings] = LcsDiff._getElements(originalSequence);
258
const [modifiedStringElements, modifiedElementsOrHash, modifiedHasStrings] = LcsDiff._getElements(modifiedSequence);
259
260
this._hasStrings = (originalHasStrings && modifiedHasStrings);
261
this._originalStringElements = originalStringElements;
262
this._originalElementsOrHash = originalElementsOrHash;
263
this._modifiedStringElements = modifiedStringElements;
264
this._modifiedElementsOrHash = modifiedElementsOrHash;
265
266
this.m_forwardHistory = [];
267
this.m_reverseHistory = [];
268
}
269
270
private static _isStringArray(arr: Int32Array | number[] | string[]): arr is string[] {
271
return (arr.length > 0 && typeof arr[0] === 'string');
272
}
273
274
private static _getElements(sequence: ISequence): [string[], Int32Array, boolean] {
275
const elements = sequence.getElements();
276
277
if (LcsDiff._isStringArray(elements)) {
278
const hashes = new Int32Array(elements.length);
279
for (let i = 0, len = elements.length; i < len; i++) {
280
hashes[i] = stringHash(elements[i], 0);
281
}
282
return [elements, hashes, true];
283
}
284
285
if (elements instanceof Int32Array) {
286
return [[], elements, false];
287
}
288
289
return [[], new Int32Array(elements), false];
290
}
291
292
private ElementsAreEqual(originalIndex: number, newIndex: number): boolean {
293
if (this._originalElementsOrHash[originalIndex] !== this._modifiedElementsOrHash[newIndex]) {
294
return false;
295
}
296
return (this._hasStrings ? this._originalStringElements[originalIndex] === this._modifiedStringElements[newIndex] : true);
297
}
298
299
private ElementsAreStrictEqual(originalIndex: number, newIndex: number): boolean {
300
if (!this.ElementsAreEqual(originalIndex, newIndex)) {
301
return false;
302
}
303
const originalElement = LcsDiff._getStrictElement(this._originalSequence, originalIndex);
304
const modifiedElement = LcsDiff._getStrictElement(this._modifiedSequence, newIndex);
305
return (originalElement === modifiedElement);
306
}
307
308
private static _getStrictElement(sequence: ISequence, index: number): string | null {
309
if (typeof sequence.getStrictElement === 'function') {
310
return sequence.getStrictElement(index);
311
}
312
return null;
313
}
314
315
private OriginalElementsAreEqual(index1: number, index2: number): boolean {
316
if (this._originalElementsOrHash[index1] !== this._originalElementsOrHash[index2]) {
317
return false;
318
}
319
return (this._hasStrings ? this._originalStringElements[index1] === this._originalStringElements[index2] : true);
320
}
321
322
private ModifiedElementsAreEqual(index1: number, index2: number): boolean {
323
if (this._modifiedElementsOrHash[index1] !== this._modifiedElementsOrHash[index2]) {
324
return false;
325
}
326
return (this._hasStrings ? this._modifiedStringElements[index1] === this._modifiedStringElements[index2] : true);
327
}
328
329
public ComputeDiff(pretty: boolean): IDiffResult {
330
return this._ComputeDiff(0, this._originalElementsOrHash.length - 1, 0, this._modifiedElementsOrHash.length - 1, pretty);
331
}
332
333
/**
334
* Computes the differences between the original and modified input
335
* sequences on the bounded range.
336
* @returns An array of the differences between the two input sequences.
337
*/
338
private _ComputeDiff(originalStart: number, originalEnd: number, modifiedStart: number, modifiedEnd: number, pretty: boolean): IDiffResult {
339
const quitEarlyArr = [false];
340
let changes = this.ComputeDiffRecursive(originalStart, originalEnd, modifiedStart, modifiedEnd, quitEarlyArr);
341
342
if (pretty) {
343
// We have to clean up the computed diff to be more intuitive
344
// but it turns out this cannot be done correctly until the entire set
345
// of diffs have been computed
346
changes = this.PrettifyChanges(changes);
347
}
348
349
return {
350
quitEarly: quitEarlyArr[0],
351
changes: changes
352
};
353
}
354
355
/**
356
* Private helper method which computes the differences on the bounded range
357
* recursively.
358
* @returns An array of the differences between the two input sequences.
359
*/
360
private ComputeDiffRecursive(originalStart: number, originalEnd: number, modifiedStart: number, modifiedEnd: number, quitEarlyArr: boolean[]): DiffChange[] {
361
quitEarlyArr[0] = false;
362
363
// Find the start of the differences
364
while (originalStart <= originalEnd && modifiedStart <= modifiedEnd && this.ElementsAreEqual(originalStart, modifiedStart)) {
365
originalStart++;
366
modifiedStart++;
367
}
368
369
// Find the end of the differences
370
while (originalEnd >= originalStart && modifiedEnd >= modifiedStart && this.ElementsAreEqual(originalEnd, modifiedEnd)) {
371
originalEnd--;
372
modifiedEnd--;
373
}
374
375
// In the special case where we either have all insertions or all deletions or the sequences are identical
376
if (originalStart > originalEnd || modifiedStart > modifiedEnd) {
377
let changes: DiffChange[];
378
379
if (modifiedStart <= modifiedEnd) {
380
Debug.Assert(originalStart === originalEnd + 1, 'originalStart should only be one more than originalEnd');
381
382
// All insertions
383
changes = [
384
new DiffChange(originalStart, 0, modifiedStart, modifiedEnd - modifiedStart + 1)
385
];
386
} else if (originalStart <= originalEnd) {
387
Debug.Assert(modifiedStart === modifiedEnd + 1, 'modifiedStart should only be one more than modifiedEnd');
388
389
// All deletions
390
changes = [
391
new DiffChange(originalStart, originalEnd - originalStart + 1, modifiedStart, 0)
392
];
393
} else {
394
Debug.Assert(originalStart === originalEnd + 1, 'originalStart should only be one more than originalEnd');
395
Debug.Assert(modifiedStart === modifiedEnd + 1, 'modifiedStart should only be one more than modifiedEnd');
396
397
// Identical sequences - No differences
398
changes = [];
399
}
400
401
return changes;
402
}
403
404
// This problem can be solved using the Divide-And-Conquer technique.
405
const midOriginalArr = [0];
406
const midModifiedArr = [0];
407
const result = this.ComputeRecursionPoint(originalStart, originalEnd, modifiedStart, modifiedEnd, midOriginalArr, midModifiedArr, quitEarlyArr);
408
409
const midOriginal = midOriginalArr[0];
410
const midModified = midModifiedArr[0];
411
412
if (result !== null) {
413
// Result is not-null when there was enough memory to compute the changes while
414
// searching for the recursion point
415
return result;
416
} else if (!quitEarlyArr[0]) {
417
// We can break the problem down recursively by finding the changes in the
418
// First Half: (originalStart, modifiedStart) to (midOriginal, midModified)
419
// Second Half: (midOriginal + 1, minModified + 1) to (originalEnd, modifiedEnd)
420
// NOTE: ComputeDiff() is inclusive, therefore the second range starts on the next point
421
422
const leftChanges = this.ComputeDiffRecursive(originalStart, midOriginal, modifiedStart, midModified, quitEarlyArr);
423
let rightChanges: DiffChange[] = [];
424
425
if (!quitEarlyArr[0]) {
426
rightChanges = this.ComputeDiffRecursive(midOriginal + 1, originalEnd, midModified + 1, modifiedEnd, quitEarlyArr);
427
} else {
428
// We didn't have time to finish the first half, so we don't have time to compute this half.
429
// Consider the entire rest of the sequence different.
430
rightChanges = [
431
new DiffChange(midOriginal + 1, originalEnd - (midOriginal + 1) + 1, midModified + 1, modifiedEnd - (midModified + 1) + 1)
432
];
433
}
434
435
return this.ConcatenateChanges(leftChanges, rightChanges);
436
}
437
438
// If we hit here, we quit early, and so can't return anything meaningful
439
return [
440
new DiffChange(originalStart, originalEnd - originalStart + 1, modifiedStart, modifiedEnd - modifiedStart + 1)
441
];
442
}
443
444
private WALKTRACE(diagonalForwardBase: number, diagonalForwardStart: number, diagonalForwardEnd: number, diagonalForwardOffset: number,
445
diagonalReverseBase: number, diagonalReverseStart: number, diagonalReverseEnd: number, diagonalReverseOffset: number,
446
forwardPoints: Int32Array, reversePoints: Int32Array,
447
originalIndex: number, originalEnd: number, midOriginalArr: number[],
448
modifiedIndex: number, modifiedEnd: number, midModifiedArr: number[],
449
deltaIsEven: boolean, quitEarlyArr: boolean[]
450
): DiffChange[] {
451
let forwardChanges: DiffChange[] | null = null;
452
let reverseChanges: DiffChange[] | null = null;
453
454
// First, walk backward through the forward diagonals history
455
let changeHelper = new DiffChangeHelper();
456
let diagonalMin = diagonalForwardStart;
457
let diagonalMax = diagonalForwardEnd;
458
let diagonalRelative = (midOriginalArr[0] - midModifiedArr[0]) - diagonalForwardOffset;
459
let lastOriginalIndex = Constants.MIN_SAFE_SMALL_INTEGER;
460
let historyIndex = this.m_forwardHistory.length - 1;
461
462
do {
463
// Get the diagonal index from the relative diagonal number
464
const diagonal = diagonalRelative + diagonalForwardBase;
465
466
// Figure out where we came from
467
if (diagonal === diagonalMin || (diagonal < diagonalMax && forwardPoints[diagonal - 1] < forwardPoints[diagonal + 1])) {
468
// Vertical line (the element is an insert)
469
originalIndex = forwardPoints[diagonal + 1];
470
modifiedIndex = originalIndex - diagonalRelative - diagonalForwardOffset;
471
if (originalIndex < lastOriginalIndex) {
472
changeHelper.MarkNextChange();
473
}
474
lastOriginalIndex = originalIndex;
475
changeHelper.AddModifiedElement(originalIndex + 1, modifiedIndex);
476
diagonalRelative = (diagonal + 1) - diagonalForwardBase; //Setup for the next iteration
477
} else {
478
// Horizontal line (the element is a deletion)
479
originalIndex = forwardPoints[diagonal - 1] + 1;
480
modifiedIndex = originalIndex - diagonalRelative - diagonalForwardOffset;
481
if (originalIndex < lastOriginalIndex) {
482
changeHelper.MarkNextChange();
483
}
484
lastOriginalIndex = originalIndex - 1;
485
changeHelper.AddOriginalElement(originalIndex, modifiedIndex + 1);
486
diagonalRelative = (diagonal - 1) - diagonalForwardBase; //Setup for the next iteration
487
}
488
489
if (historyIndex >= 0) {
490
forwardPoints = this.m_forwardHistory[historyIndex];
491
diagonalForwardBase = forwardPoints[0]; //We stored this in the first spot
492
diagonalMin = 1;
493
diagonalMax = forwardPoints.length - 1;
494
}
495
} while (--historyIndex >= -1);
496
497
// Ironically, we get the forward changes as the reverse of the
498
// order we added them since we technically added them backwards
499
forwardChanges = changeHelper.getReverseChanges();
500
501
if (quitEarlyArr[0]) {
502
// TODO: Calculate a partial from the reverse diagonals.
503
// For now, just assume everything after the midOriginal/midModified point is a diff
504
505
let originalStartPoint = midOriginalArr[0] + 1;
506
let modifiedStartPoint = midModifiedArr[0] + 1;
507
508
if (forwardChanges !== null && forwardChanges.length > 0) {
509
const lastForwardChange = forwardChanges[forwardChanges.length - 1];
510
originalStartPoint = Math.max(originalStartPoint, lastForwardChange.getOriginalEnd());
511
modifiedStartPoint = Math.max(modifiedStartPoint, lastForwardChange.getModifiedEnd());
512
}
513
514
reverseChanges = [
515
new DiffChange(originalStartPoint, originalEnd - originalStartPoint + 1,
516
modifiedStartPoint, modifiedEnd - modifiedStartPoint + 1)
517
];
518
} else {
519
// Now walk backward through the reverse diagonals history
520
changeHelper = new DiffChangeHelper();
521
diagonalMin = diagonalReverseStart;
522
diagonalMax = diagonalReverseEnd;
523
diagonalRelative = (midOriginalArr[0] - midModifiedArr[0]) - diagonalReverseOffset;
524
lastOriginalIndex = Constants.MAX_SAFE_SMALL_INTEGER;
525
historyIndex = (deltaIsEven) ? this.m_reverseHistory.length - 1 : this.m_reverseHistory.length - 2;
526
527
do {
528
// Get the diagonal index from the relative diagonal number
529
const diagonal = diagonalRelative + diagonalReverseBase;
530
531
// Figure out where we came from
532
if (diagonal === diagonalMin || (diagonal < diagonalMax && reversePoints[diagonal - 1] >= reversePoints[diagonal + 1])) {
533
// Horizontal line (the element is a deletion))
534
originalIndex = reversePoints[diagonal + 1] - 1;
535
modifiedIndex = originalIndex - diagonalRelative - diagonalReverseOffset;
536
if (originalIndex > lastOriginalIndex) {
537
changeHelper.MarkNextChange();
538
}
539
lastOriginalIndex = originalIndex + 1;
540
changeHelper.AddOriginalElement(originalIndex + 1, modifiedIndex + 1);
541
diagonalRelative = (diagonal + 1) - diagonalReverseBase; //Setup for the next iteration
542
} else {
543
// Vertical line (the element is an insertion)
544
originalIndex = reversePoints[diagonal - 1];
545
modifiedIndex = originalIndex - diagonalRelative - diagonalReverseOffset;
546
if (originalIndex > lastOriginalIndex) {
547
changeHelper.MarkNextChange();
548
}
549
lastOriginalIndex = originalIndex;
550
changeHelper.AddModifiedElement(originalIndex + 1, modifiedIndex + 1);
551
diagonalRelative = (diagonal - 1) - diagonalReverseBase; //Setup for the next iteration
552
}
553
554
if (historyIndex >= 0) {
555
reversePoints = this.m_reverseHistory[historyIndex];
556
diagonalReverseBase = reversePoints[0]; //We stored this in the first spot
557
diagonalMin = 1;
558
diagonalMax = reversePoints.length - 1;
559
}
560
} while (--historyIndex >= -1);
561
562
// There are cases where the reverse history will find diffs that
563
// are correct, but not intuitive, so we need shift them.
564
reverseChanges = changeHelper.getChanges();
565
}
566
567
return this.ConcatenateChanges(forwardChanges, reverseChanges);
568
}
569
570
/**
571
* Given the range to compute the diff on, this method finds the point:
572
* (midOriginal, midModified)
573
* that exists in the middle of the LCS of the two sequences and
574
* is the point at which the LCS problem may be broken down recursively.
575
* This method will try to keep the LCS trace in memory. If the LCS recursion
576
* point is calculated and the full trace is available in memory, then this method
577
* will return the change list.
578
* @param originalStart The start bound of the original sequence range
579
* @param originalEnd The end bound of the original sequence range
580
* @param modifiedStart The start bound of the modified sequence range
581
* @param modifiedEnd The end bound of the modified sequence range
582
* @param midOriginal The middle point of the original sequence range
583
* @param midModified The middle point of the modified sequence range
584
* @returns The diff changes, if available, otherwise null
585
*/
586
private ComputeRecursionPoint(originalStart: number, originalEnd: number, modifiedStart: number, modifiedEnd: number, midOriginalArr: number[], midModifiedArr: number[], quitEarlyArr: boolean[]) {
587
let originalIndex = 0, modifiedIndex = 0;
588
let diagonalForwardStart = 0, diagonalForwardEnd = 0;
589
let diagonalReverseStart = 0, diagonalReverseEnd = 0;
590
591
// To traverse the edit graph and produce the proper LCS, our actual
592
// start position is just outside the given boundary
593
originalStart--;
594
modifiedStart--;
595
596
// We set these up to make the compiler happy, but they will
597
// be replaced before we return with the actual recursion point
598
midOriginalArr[0] = 0;
599
midModifiedArr[0] = 0;
600
601
// Clear out the history
602
this.m_forwardHistory = [];
603
this.m_reverseHistory = [];
604
605
// Each cell in the two arrays corresponds to a diagonal in the edit graph.
606
// The integer value in the cell represents the originalIndex of the furthest
607
// reaching point found so far that ends in that diagonal.
608
// The modifiedIndex can be computed mathematically from the originalIndex and the diagonal number.
609
const maxDifferences = (originalEnd - originalStart) + (modifiedEnd - modifiedStart);
610
const numDiagonals = maxDifferences + 1;
611
const forwardPoints = new Int32Array(numDiagonals);
612
const reversePoints = new Int32Array(numDiagonals);
613
// diagonalForwardBase: Index into forwardPoints of the diagonal which passes through (originalStart, modifiedStart)
614
// diagonalReverseBase: Index into reversePoints of the diagonal which passes through (originalEnd, modifiedEnd)
615
const diagonalForwardBase = (modifiedEnd - modifiedStart);
616
const diagonalReverseBase = (originalEnd - originalStart);
617
// diagonalForwardOffset: Geometric offset which allows modifiedIndex to be computed from originalIndex and the
618
// diagonal number (relative to diagonalForwardBase)
619
// diagonalReverseOffset: Geometric offset which allows modifiedIndex to be computed from originalIndex and the
620
// diagonal number (relative to diagonalReverseBase)
621
const diagonalForwardOffset = (originalStart - modifiedStart);
622
const diagonalReverseOffset = (originalEnd - modifiedEnd);
623
624
// delta: The difference between the end diagonal and the start diagonal. This is used to relate diagonal numbers
625
// relative to the start diagonal with diagonal numbers relative to the end diagonal.
626
// The Even/Oddn-ness of this delta is important for determining when we should check for overlap
627
const delta = diagonalReverseBase - diagonalForwardBase;
628
const deltaIsEven = (delta % 2 === 0);
629
630
// Here we set up the start and end points as the furthest points found so far
631
// in both the forward and reverse directions, respectively
632
forwardPoints[diagonalForwardBase] = originalStart;
633
reversePoints[diagonalReverseBase] = originalEnd;
634
635
// Remember if we quit early, and thus need to do a best-effort result instead of a real result.
636
quitEarlyArr[0] = false;
637
638
639
640
// A couple of points:
641
// --With this method, we iterate on the number of differences between the two sequences.
642
// The more differences there actually are, the longer this will take.
643
// --Also, as the number of differences increases, we have to search on diagonals further
644
// away from the reference diagonal (which is diagonalForwardBase for forward, diagonalReverseBase for reverse).
645
// --We extend on even diagonals (relative to the reference diagonal) only when numDifferences
646
// is even and odd diagonals only when numDifferences is odd.
647
for (let numDifferences = 1; numDifferences <= (maxDifferences / 2) + 1; numDifferences++) {
648
let furthestOriginalIndex = 0;
649
let furthestModifiedIndex = 0;
650
651
// Run the algorithm in the forward direction
652
diagonalForwardStart = this.ClipDiagonalBound(diagonalForwardBase - numDifferences, numDifferences, diagonalForwardBase, numDiagonals);
653
diagonalForwardEnd = this.ClipDiagonalBound(diagonalForwardBase + numDifferences, numDifferences, diagonalForwardBase, numDiagonals);
654
for (let diagonal = diagonalForwardStart; diagonal <= diagonalForwardEnd; diagonal += 2) {
655
// STEP 1: We extend the furthest reaching point in the present diagonal
656
// by looking at the diagonals above and below and picking the one whose point
657
// is further away from the start point (originalStart, modifiedStart)
658
if (diagonal === diagonalForwardStart || (diagonal < diagonalForwardEnd && forwardPoints[diagonal - 1] < forwardPoints[diagonal + 1])) {
659
originalIndex = forwardPoints[diagonal + 1];
660
} else {
661
originalIndex = forwardPoints[diagonal - 1] + 1;
662
}
663
modifiedIndex = originalIndex - (diagonal - diagonalForwardBase) - diagonalForwardOffset;
664
665
// Save the current originalIndex so we can test for false overlap in step 3
666
const tempOriginalIndex = originalIndex;
667
668
// STEP 2: We can continue to extend the furthest reaching point in the present diagonal
669
// so long as the elements are equal.
670
while (originalIndex < originalEnd && modifiedIndex < modifiedEnd && this.ElementsAreEqual(originalIndex + 1, modifiedIndex + 1)) {
671
originalIndex++;
672
modifiedIndex++;
673
}
674
forwardPoints[diagonal] = originalIndex;
675
676
if (originalIndex + modifiedIndex > furthestOriginalIndex + furthestModifiedIndex) {
677
furthestOriginalIndex = originalIndex;
678
furthestModifiedIndex = modifiedIndex;
679
}
680
681
// STEP 3: If delta is odd (overlap first happens on forward when delta is odd)
682
// and diagonal is in the range of reverse diagonals computed for numDifferences-1
683
// (the previous iteration; we haven't computed reverse diagonals for numDifferences yet)
684
// then check for overlap.
685
if (!deltaIsEven && Math.abs(diagonal - diagonalReverseBase) <= (numDifferences - 1)) {
686
if (originalIndex >= reversePoints[diagonal]) {
687
midOriginalArr[0] = originalIndex;
688
midModifiedArr[0] = modifiedIndex;
689
690
if (tempOriginalIndex <= reversePoints[diagonal] && LocalConstants.MaxDifferencesHistory > 0 && numDifferences <= (LocalConstants.MaxDifferencesHistory + 1)) {
691
// BINGO! We overlapped, and we have the full trace in memory!
692
return this.WALKTRACE(diagonalForwardBase, diagonalForwardStart, diagonalForwardEnd, diagonalForwardOffset,
693
diagonalReverseBase, diagonalReverseStart, diagonalReverseEnd, diagonalReverseOffset,
694
forwardPoints, reversePoints,
695
originalIndex, originalEnd, midOriginalArr,
696
modifiedIndex, modifiedEnd, midModifiedArr,
697
deltaIsEven, quitEarlyArr
698
);
699
} else {
700
// Either false overlap, or we didn't have enough memory for the full trace
701
// Just return the recursion point
702
return null;
703
}
704
}
705
}
706
}
707
708
// Check to see if we should be quitting early, before moving on to the next iteration.
709
const matchLengthOfLongest = ((furthestOriginalIndex - originalStart) + (furthestModifiedIndex - modifiedStart) - numDifferences) / 2;
710
711
if (this.ContinueProcessingPredicate !== null && !this.ContinueProcessingPredicate(furthestOriginalIndex, matchLengthOfLongest)) {
712
// We can't finish, so skip ahead to generating a result from what we have.
713
quitEarlyArr[0] = true;
714
715
// Use the furthest distance we got in the forward direction.
716
midOriginalArr[0] = furthestOriginalIndex;
717
midModifiedArr[0] = furthestModifiedIndex;
718
719
if (matchLengthOfLongest > 0 && LocalConstants.MaxDifferencesHistory > 0 && numDifferences <= (LocalConstants.MaxDifferencesHistory + 1)) {
720
// Enough of the history is in memory to walk it backwards
721
return this.WALKTRACE(diagonalForwardBase, diagonalForwardStart, diagonalForwardEnd, diagonalForwardOffset,
722
diagonalReverseBase, diagonalReverseStart, diagonalReverseEnd, diagonalReverseOffset,
723
forwardPoints, reversePoints,
724
originalIndex, originalEnd, midOriginalArr,
725
modifiedIndex, modifiedEnd, midModifiedArr,
726
deltaIsEven, quitEarlyArr
727
);
728
} else {
729
// We didn't actually remember enough of the history.
730
731
//Since we are quitting the diff early, we need to shift back the originalStart and modified start
732
//back into the boundary limits since we decremented their value above beyond the boundary limit.
733
originalStart++;
734
modifiedStart++;
735
736
return [
737
new DiffChange(originalStart, originalEnd - originalStart + 1,
738
modifiedStart, modifiedEnd - modifiedStart + 1)
739
];
740
}
741
}
742
743
// Run the algorithm in the reverse direction
744
diagonalReverseStart = this.ClipDiagonalBound(diagonalReverseBase - numDifferences, numDifferences, diagonalReverseBase, numDiagonals);
745
diagonalReverseEnd = this.ClipDiagonalBound(diagonalReverseBase + numDifferences, numDifferences, diagonalReverseBase, numDiagonals);
746
for (let diagonal = diagonalReverseStart; diagonal <= diagonalReverseEnd; diagonal += 2) {
747
// STEP 1: We extend the furthest reaching point in the present diagonal
748
// by looking at the diagonals above and below and picking the one whose point
749
// is further away from the start point (originalEnd, modifiedEnd)
750
if (diagonal === diagonalReverseStart || (diagonal < diagonalReverseEnd && reversePoints[diagonal - 1] >= reversePoints[diagonal + 1])) {
751
originalIndex = reversePoints[diagonal + 1] - 1;
752
} else {
753
originalIndex = reversePoints[diagonal - 1];
754
}
755
modifiedIndex = originalIndex - (diagonal - diagonalReverseBase) - diagonalReverseOffset;
756
757
// Save the current originalIndex so we can test for false overlap
758
const tempOriginalIndex = originalIndex;
759
760
// STEP 2: We can continue to extend the furthest reaching point in the present diagonal
761
// as long as the elements are equal.
762
while (originalIndex > originalStart && modifiedIndex > modifiedStart && this.ElementsAreEqual(originalIndex, modifiedIndex)) {
763
originalIndex--;
764
modifiedIndex--;
765
}
766
reversePoints[diagonal] = originalIndex;
767
768
// STEP 4: If delta is even (overlap first happens on reverse when delta is even)
769
// and diagonal is in the range of forward diagonals computed for numDifferences
770
// then check for overlap.
771
if (deltaIsEven && Math.abs(diagonal - diagonalForwardBase) <= numDifferences) {
772
if (originalIndex <= forwardPoints[diagonal]) {
773
midOriginalArr[0] = originalIndex;
774
midModifiedArr[0] = modifiedIndex;
775
776
if (tempOriginalIndex >= forwardPoints[diagonal] && LocalConstants.MaxDifferencesHistory > 0 && numDifferences <= (LocalConstants.MaxDifferencesHistory + 1)) {
777
// BINGO! We overlapped, and we have the full trace in memory!
778
return this.WALKTRACE(diagonalForwardBase, diagonalForwardStart, diagonalForwardEnd, diagonalForwardOffset,
779
diagonalReverseBase, diagonalReverseStart, diagonalReverseEnd, diagonalReverseOffset,
780
forwardPoints, reversePoints,
781
originalIndex, originalEnd, midOriginalArr,
782
modifiedIndex, modifiedEnd, midModifiedArr,
783
deltaIsEven, quitEarlyArr
784
);
785
} else {
786
// Either false overlap, or we didn't have enough memory for the full trace
787
// Just return the recursion point
788
return null;
789
}
790
}
791
}
792
}
793
794
// Save current vectors to history before the next iteration
795
if (numDifferences <= LocalConstants.MaxDifferencesHistory) {
796
// We are allocating space for one extra int, which we fill with
797
// the index of the diagonal base index
798
let temp = new Int32Array(diagonalForwardEnd - diagonalForwardStart + 2);
799
temp[0] = diagonalForwardBase - diagonalForwardStart + 1;
800
MyArray.Copy2(forwardPoints, diagonalForwardStart, temp, 1, diagonalForwardEnd - diagonalForwardStart + 1);
801
this.m_forwardHistory.push(temp);
802
803
temp = new Int32Array(diagonalReverseEnd - diagonalReverseStart + 2);
804
temp[0] = diagonalReverseBase - diagonalReverseStart + 1;
805
MyArray.Copy2(reversePoints, diagonalReverseStart, temp, 1, diagonalReverseEnd - diagonalReverseStart + 1);
806
this.m_reverseHistory.push(temp);
807
}
808
809
}
810
811
// If we got here, then we have the full trace in history. We just have to convert it to a change list
812
// NOTE: This part is a bit messy
813
return this.WALKTRACE(diagonalForwardBase, diagonalForwardStart, diagonalForwardEnd, diagonalForwardOffset,
814
diagonalReverseBase, diagonalReverseStart, diagonalReverseEnd, diagonalReverseOffset,
815
forwardPoints, reversePoints,
816
originalIndex, originalEnd, midOriginalArr,
817
modifiedIndex, modifiedEnd, midModifiedArr,
818
deltaIsEven, quitEarlyArr
819
);
820
}
821
822
/**
823
* Shifts the given changes to provide a more intuitive diff.
824
* While the first element in a diff matches the first element after the diff,
825
* we shift the diff down.
826
*
827
* @param changes The list of changes to shift
828
* @returns The shifted changes
829
*/
830
private PrettifyChanges(changes: DiffChange[]): DiffChange[] {
831
832
// Shift all the changes down first
833
for (let i = 0; i < changes.length; i++) {
834
const change = changes[i];
835
const originalStop = (i < changes.length - 1) ? changes[i + 1].originalStart : this._originalElementsOrHash.length;
836
const modifiedStop = (i < changes.length - 1) ? changes[i + 1].modifiedStart : this._modifiedElementsOrHash.length;
837
const checkOriginal = change.originalLength > 0;
838
const checkModified = change.modifiedLength > 0;
839
840
while (
841
change.originalStart + change.originalLength < originalStop
842
&& change.modifiedStart + change.modifiedLength < modifiedStop
843
&& (!checkOriginal || this.OriginalElementsAreEqual(change.originalStart, change.originalStart + change.originalLength))
844
&& (!checkModified || this.ModifiedElementsAreEqual(change.modifiedStart, change.modifiedStart + change.modifiedLength))
845
) {
846
const startStrictEqual = this.ElementsAreStrictEqual(change.originalStart, change.modifiedStart);
847
const endStrictEqual = this.ElementsAreStrictEqual(change.originalStart + change.originalLength, change.modifiedStart + change.modifiedLength);
848
if (endStrictEqual && !startStrictEqual) {
849
// moving the change down would create an equal change, but the elements are not strict equal
850
break;
851
}
852
change.originalStart++;
853
change.modifiedStart++;
854
}
855
856
const mergedChangeArr: Array<DiffChange | null> = [null];
857
if (i < changes.length - 1 && this.ChangesOverlap(changes[i], changes[i + 1], mergedChangeArr)) {
858
changes[i] = mergedChangeArr[0]!;
859
changes.splice(i + 1, 1);
860
i--;
861
continue;
862
}
863
}
864
865
// Shift changes back up until we hit empty or whitespace-only lines
866
for (let i = changes.length - 1; i >= 0; i--) {
867
const change = changes[i];
868
869
let originalStop = 0;
870
let modifiedStop = 0;
871
if (i > 0) {
872
const prevChange = changes[i - 1];
873
originalStop = prevChange.originalStart + prevChange.originalLength;
874
modifiedStop = prevChange.modifiedStart + prevChange.modifiedLength;
875
}
876
877
const checkOriginal = change.originalLength > 0;
878
const checkModified = change.modifiedLength > 0;
879
880
let bestDelta = 0;
881
let bestScore = this._boundaryScore(change.originalStart, change.originalLength, change.modifiedStart, change.modifiedLength);
882
883
for (let delta = 1; ; delta++) {
884
const originalStart = change.originalStart - delta;
885
const modifiedStart = change.modifiedStart - delta;
886
887
if (originalStart < originalStop || modifiedStart < modifiedStop) {
888
break;
889
}
890
891
if (checkOriginal && !this.OriginalElementsAreEqual(originalStart, originalStart + change.originalLength)) {
892
break;
893
}
894
895
if (checkModified && !this.ModifiedElementsAreEqual(modifiedStart, modifiedStart + change.modifiedLength)) {
896
break;
897
}
898
899
const touchingPreviousChange = (originalStart === originalStop && modifiedStart === modifiedStop);
900
const score = (
901
(touchingPreviousChange ? 5 : 0)
902
+ this._boundaryScore(originalStart, change.originalLength, modifiedStart, change.modifiedLength)
903
);
904
905
if (score > bestScore) {
906
bestScore = score;
907
bestDelta = delta;
908
}
909
}
910
911
change.originalStart -= bestDelta;
912
change.modifiedStart -= bestDelta;
913
914
const mergedChangeArr: Array<DiffChange | null> = [null];
915
if (i > 0 && this.ChangesOverlap(changes[i - 1], changes[i], mergedChangeArr)) {
916
changes[i - 1] = mergedChangeArr[0]!;
917
changes.splice(i, 1);
918
i++;
919
continue;
920
}
921
}
922
923
// There could be multiple longest common substrings.
924
// Give preference to the ones containing longer lines
925
if (this._hasStrings) {
926
for (let i = 1, len = changes.length; i < len; i++) {
927
const aChange = changes[i - 1];
928
const bChange = changes[i];
929
const matchedLength = bChange.originalStart - aChange.originalStart - aChange.originalLength;
930
const aOriginalStart = aChange.originalStart;
931
const bOriginalEnd = bChange.originalStart + bChange.originalLength;
932
const abOriginalLength = bOriginalEnd - aOriginalStart;
933
const aModifiedStart = aChange.modifiedStart;
934
const bModifiedEnd = bChange.modifiedStart + bChange.modifiedLength;
935
const abModifiedLength = bModifiedEnd - aModifiedStart;
936
// Avoid wasting a lot of time with these searches
937
if (matchedLength < 5 && abOriginalLength < 20 && abModifiedLength < 20) {
938
const t = this._findBetterContiguousSequence(
939
aOriginalStart, abOriginalLength,
940
aModifiedStart, abModifiedLength,
941
matchedLength
942
);
943
if (t) {
944
const [originalMatchStart, modifiedMatchStart] = t;
945
if (originalMatchStart !== aChange.originalStart + aChange.originalLength || modifiedMatchStart !== aChange.modifiedStart + aChange.modifiedLength) {
946
// switch to another sequence that has a better score
947
aChange.originalLength = originalMatchStart - aChange.originalStart;
948
aChange.modifiedLength = modifiedMatchStart - aChange.modifiedStart;
949
bChange.originalStart = originalMatchStart + matchedLength;
950
bChange.modifiedStart = modifiedMatchStart + matchedLength;
951
bChange.originalLength = bOriginalEnd - bChange.originalStart;
952
bChange.modifiedLength = bModifiedEnd - bChange.modifiedStart;
953
}
954
}
955
}
956
}
957
}
958
959
return changes;
960
}
961
962
private _findBetterContiguousSequence(originalStart: number, originalLength: number, modifiedStart: number, modifiedLength: number, desiredLength: number): [number, number] | null {
963
if (originalLength < desiredLength || modifiedLength < desiredLength) {
964
return null;
965
}
966
const originalMax = originalStart + originalLength - desiredLength + 1;
967
const modifiedMax = modifiedStart + modifiedLength - desiredLength + 1;
968
let bestScore = 0;
969
let bestOriginalStart = 0;
970
let bestModifiedStart = 0;
971
for (let i = originalStart; i < originalMax; i++) {
972
for (let j = modifiedStart; j < modifiedMax; j++) {
973
const score = this._contiguousSequenceScore(i, j, desiredLength);
974
if (score > 0 && score > bestScore) {
975
bestScore = score;
976
bestOriginalStart = i;
977
bestModifiedStart = j;
978
}
979
}
980
}
981
if (bestScore > 0) {
982
return [bestOriginalStart, bestModifiedStart];
983
}
984
return null;
985
}
986
987
private _contiguousSequenceScore(originalStart: number, modifiedStart: number, length: number): number {
988
let score = 0;
989
for (let l = 0; l < length; l++) {
990
if (!this.ElementsAreEqual(originalStart + l, modifiedStart + l)) {
991
return 0;
992
}
993
score += this._originalStringElements[originalStart + l].length;
994
}
995
return score;
996
}
997
998
private _OriginalIsBoundary(index: number): boolean {
999
if (index <= 0 || index >= this._originalElementsOrHash.length - 1) {
1000
return true;
1001
}
1002
return (this._hasStrings && /^\s*$/.test(this._originalStringElements[index]));
1003
}
1004
1005
private _OriginalRegionIsBoundary(originalStart: number, originalLength: number): boolean {
1006
if (this._OriginalIsBoundary(originalStart) || this._OriginalIsBoundary(originalStart - 1)) {
1007
return true;
1008
}
1009
if (originalLength > 0) {
1010
const originalEnd = originalStart + originalLength;
1011
if (this._OriginalIsBoundary(originalEnd - 1) || this._OriginalIsBoundary(originalEnd)) {
1012
return true;
1013
}
1014
}
1015
return false;
1016
}
1017
1018
private _ModifiedIsBoundary(index: number): boolean {
1019
if (index <= 0 || index >= this._modifiedElementsOrHash.length - 1) {
1020
return true;
1021
}
1022
return (this._hasStrings && /^\s*$/.test(this._modifiedStringElements[index]));
1023
}
1024
1025
private _ModifiedRegionIsBoundary(modifiedStart: number, modifiedLength: number): boolean {
1026
if (this._ModifiedIsBoundary(modifiedStart) || this._ModifiedIsBoundary(modifiedStart - 1)) {
1027
return true;
1028
}
1029
if (modifiedLength > 0) {
1030
const modifiedEnd = modifiedStart + modifiedLength;
1031
if (this._ModifiedIsBoundary(modifiedEnd - 1) || this._ModifiedIsBoundary(modifiedEnd)) {
1032
return true;
1033
}
1034
}
1035
return false;
1036
}
1037
1038
private _boundaryScore(originalStart: number, originalLength: number, modifiedStart: number, modifiedLength: number): number {
1039
const originalScore = (this._OriginalRegionIsBoundary(originalStart, originalLength) ? 1 : 0);
1040
const modifiedScore = (this._ModifiedRegionIsBoundary(modifiedStart, modifiedLength) ? 1 : 0);
1041
return (originalScore + modifiedScore);
1042
}
1043
1044
/**
1045
* Concatenates the two input DiffChange lists and returns the resulting
1046
* list.
1047
* @param The left changes
1048
* @param The right changes
1049
* @returns The concatenated list
1050
*/
1051
private ConcatenateChanges(left: DiffChange[], right: DiffChange[]): DiffChange[] {
1052
const mergedChangeArr: DiffChange[] = [];
1053
1054
if (left.length === 0 || right.length === 0) {
1055
return (right.length > 0) ? right : left;
1056
} else if (this.ChangesOverlap(left[left.length - 1], right[0], mergedChangeArr)) {
1057
// Since we break the problem down recursively, it is possible that we
1058
// might recurse in the middle of a change thereby splitting it into
1059
// two changes. Here in the combining stage, we detect and fuse those
1060
// changes back together
1061
const result = new Array<DiffChange>(left.length + right.length - 1);
1062
MyArray.Copy(left, 0, result, 0, left.length - 1);
1063
result[left.length - 1] = mergedChangeArr[0];
1064
MyArray.Copy(right, 1, result, left.length, right.length - 1);
1065
1066
return result;
1067
} else {
1068
const result = new Array<DiffChange>(left.length + right.length);
1069
MyArray.Copy(left, 0, result, 0, left.length);
1070
MyArray.Copy(right, 0, result, left.length, right.length);
1071
1072
return result;
1073
}
1074
}
1075
1076
/**
1077
* Returns true if the two changes overlap and can be merged into a single
1078
* change
1079
* @param left The left change
1080
* @param right The right change
1081
* @param mergedChange The merged change if the two overlap, null otherwise
1082
* @returns True if the two changes overlap
1083
*/
1084
private ChangesOverlap(left: DiffChange, right: DiffChange, mergedChangeArr: Array<DiffChange | null>): boolean {
1085
Debug.Assert(left.originalStart <= right.originalStart, 'Left change is not less than or equal to right change');
1086
Debug.Assert(left.modifiedStart <= right.modifiedStart, 'Left change is not less than or equal to right change');
1087
1088
if (left.originalStart + left.originalLength >= right.originalStart || left.modifiedStart + left.modifiedLength >= right.modifiedStart) {
1089
const originalStart = left.originalStart;
1090
let originalLength = left.originalLength;
1091
const modifiedStart = left.modifiedStart;
1092
let modifiedLength = left.modifiedLength;
1093
1094
if (left.originalStart + left.originalLength >= right.originalStart) {
1095
originalLength = right.originalStart + right.originalLength - left.originalStart;
1096
}
1097
if (left.modifiedStart + left.modifiedLength >= right.modifiedStart) {
1098
modifiedLength = right.modifiedStart + right.modifiedLength - left.modifiedStart;
1099
}
1100
1101
mergedChangeArr[0] = new DiffChange(originalStart, originalLength, modifiedStart, modifiedLength);
1102
return true;
1103
} else {
1104
mergedChangeArr[0] = null;
1105
return false;
1106
}
1107
}
1108
1109
/**
1110
* Helper method used to clip a diagonal index to the range of valid
1111
* diagonals. This also decides whether or not the diagonal index,
1112
* if it exceeds the boundary, should be clipped to the boundary or clipped
1113
* one inside the boundary depending on the Even/Odd status of the boundary
1114
* and numDifferences.
1115
* @param diagonal The index of the diagonal to clip.
1116
* @param numDifferences The current number of differences being iterated upon.
1117
* @param diagonalBaseIndex The base reference diagonal.
1118
* @param numDiagonals The total number of diagonals.
1119
* @returns The clipped diagonal index.
1120
*/
1121
private ClipDiagonalBound(diagonal: number, numDifferences: number, diagonalBaseIndex: number, numDiagonals: number): number {
1122
if (diagonal >= 0 && diagonal < numDiagonals) {
1123
// Nothing to clip, its in range
1124
return diagonal;
1125
}
1126
1127
// diagonalsBelow: The number of diagonals below the reference diagonal
1128
// diagonalsAbove: The number of diagonals above the reference diagonal
1129
const diagonalsBelow = diagonalBaseIndex;
1130
const diagonalsAbove = numDiagonals - diagonalBaseIndex - 1;
1131
const diffEven = (numDifferences % 2 === 0);
1132
1133
if (diagonal < 0) {
1134
const lowerBoundEven = (diagonalsBelow % 2 === 0);
1135
return (diffEven === lowerBoundEven) ? 0 : 1;
1136
} else {
1137
const upperBoundEven = (diagonalsAbove % 2 === 0);
1138
return (diffEven === upperBoundEven) ? numDiagonals - 1 : numDiagonals - 2;
1139
}
1140
}
1141
}
1142
1143
1144
/**
1145
* Precomputed equality array for character codes.
1146
*/
1147
const precomputedEqualityArray = new Uint32Array(0x10000);
1148
1149
/**
1150
* Computes the Levenshtein distance for strings of length <= 32.
1151
* @param firstString - The first string.
1152
* @param secondString - The second string.
1153
* @returns The Levenshtein distance.
1154
*/
1155
const computeLevenshteinDistanceForShortStrings = (firstString: string, secondString: string): number => {
1156
const firstStringLength = firstString.length;
1157
const secondStringLength = secondString.length;
1158
const lastBitMask = 1 << (firstStringLength - 1);
1159
let positiveVector = -1;
1160
let negativeVector = 0;
1161
let distance = firstStringLength;
1162
let index = firstStringLength;
1163
1164
// Initialize precomputedEqualityArray for firstString
1165
while (index--) {
1166
precomputedEqualityArray[firstString.charCodeAt(index)] |= 1 << index;
1167
}
1168
1169
// Process each character of secondString
1170
for (index = 0; index < secondStringLength; index++) {
1171
let equalityMask = precomputedEqualityArray[secondString.charCodeAt(index)];
1172
const combinedVector = equalityMask | negativeVector;
1173
equalityMask |= ((equalityMask & positiveVector) + positiveVector) ^ positiveVector;
1174
negativeVector |= ~(equalityMask | positiveVector);
1175
positiveVector &= equalityMask;
1176
if (negativeVector & lastBitMask) {
1177
distance++;
1178
}
1179
if (positiveVector & lastBitMask) {
1180
distance--;
1181
}
1182
negativeVector = (negativeVector << 1) | 1;
1183
positiveVector = (positiveVector << 1) | ~(combinedVector | negativeVector);
1184
negativeVector &= combinedVector;
1185
}
1186
1187
// Reset precomputedEqualityArray
1188
index = firstStringLength;
1189
while (index--) {
1190
precomputedEqualityArray[firstString.charCodeAt(index)] = 0;
1191
}
1192
1193
return distance;
1194
};
1195
1196
/**
1197
* Computes the Levenshtein distance for strings of length > 32.
1198
* @param firstString - The first string.
1199
* @param secondString - The second string.
1200
* @returns The Levenshtein distance.
1201
*/
1202
function computeLevenshteinDistanceForLongStrings(firstString: string, secondString: string): number {
1203
const firstStringLength = firstString.length;
1204
const secondStringLength = secondString.length;
1205
const horizontalBitArray = [];
1206
const verticalBitArray = [];
1207
const horizontalSize = Math.ceil(firstStringLength / 32);
1208
const verticalSize = Math.ceil(secondStringLength / 32);
1209
1210
// Initialize horizontal and vertical bit arrays
1211
for (let i = 0; i < horizontalSize; i++) {
1212
horizontalBitArray[i] = -1;
1213
verticalBitArray[i] = 0;
1214
}
1215
1216
let verticalIndex = 0;
1217
for (; verticalIndex < verticalSize - 1; verticalIndex++) {
1218
let negativeVector = 0;
1219
let positiveVector = -1;
1220
const start = verticalIndex * 32;
1221
const verticalLength = Math.min(32, secondStringLength) + start;
1222
1223
// Initialize precomputedEqualityArray for secondString
1224
for (let k = start; k < verticalLength; k++) {
1225
precomputedEqualityArray[secondString.charCodeAt(k)] |= 1 << k;
1226
}
1227
1228
// Process each character of firstString
1229
for (let i = 0; i < firstStringLength; i++) {
1230
const equalityMask = precomputedEqualityArray[firstString.charCodeAt(i)];
1231
const previousBit = (horizontalBitArray[(i / 32) | 0] >>> i) & 1;
1232
const matchBit = (verticalBitArray[(i / 32) | 0] >>> i) & 1;
1233
const combinedVector = equalityMask | negativeVector;
1234
const combinedHorizontalVector = ((((equalityMask | matchBit) & positiveVector) + positiveVector) ^ positiveVector) | equalityMask | matchBit;
1235
let positiveHorizontalVector = negativeVector | ~(combinedHorizontalVector | positiveVector);
1236
let negativeHorizontalVector = positiveVector & combinedHorizontalVector;
1237
if ((positiveHorizontalVector >>> 31) ^ previousBit) {
1238
horizontalBitArray[(i / 32) | 0] ^= 1 << i;
1239
}
1240
if ((negativeHorizontalVector >>> 31) ^ matchBit) {
1241
verticalBitArray[(i / 32) | 0] ^= 1 << i;
1242
}
1243
positiveHorizontalVector = (positiveHorizontalVector << 1) | previousBit;
1244
negativeHorizontalVector = (negativeHorizontalVector << 1) | matchBit;
1245
positiveVector = negativeHorizontalVector | ~(combinedVector | positiveHorizontalVector);
1246
negativeVector = positiveHorizontalVector & combinedVector;
1247
}
1248
1249
// Reset precomputedEqualityArray
1250
for (let k = start; k < verticalLength; k++) {
1251
precomputedEqualityArray[secondString.charCodeAt(k)] = 0;
1252
}
1253
}
1254
1255
let negativeVector = 0;
1256
let positiveVector = -1;
1257
const start = verticalIndex * 32;
1258
const verticalLength = Math.min(32, secondStringLength - start) + start;
1259
1260
// Initialize precomputedEqualityArray for secondString
1261
for (let k = start; k < verticalLength; k++) {
1262
precomputedEqualityArray[secondString.charCodeAt(k)] |= 1 << k;
1263
}
1264
1265
let distance = secondStringLength;
1266
1267
// Process each character of firstString
1268
for (let i = 0; i < firstStringLength; i++) {
1269
const equalityMask = precomputedEqualityArray[firstString.charCodeAt(i)];
1270
const previousBit = (horizontalBitArray[(i / 32) | 0] >>> i) & 1;
1271
const matchBit = (verticalBitArray[(i / 32) | 0] >>> i) & 1;
1272
const combinedVector = equalityMask | negativeVector;
1273
const combinedHorizontalVector = ((((equalityMask | matchBit) & positiveVector) + positiveVector) ^ positiveVector) | equalityMask | matchBit;
1274
let positiveHorizontalVector = negativeVector | ~(combinedHorizontalVector | positiveVector);
1275
let negativeHorizontalVector = positiveVector & combinedHorizontalVector;
1276
distance += (positiveHorizontalVector >>> (secondStringLength - 1)) & 1;
1277
distance -= (negativeHorizontalVector >>> (secondStringLength - 1)) & 1;
1278
if ((positiveHorizontalVector >>> 31) ^ previousBit) {
1279
horizontalBitArray[(i / 32) | 0] ^= 1 << i;
1280
}
1281
if ((negativeHorizontalVector >>> 31) ^ matchBit) {
1282
verticalBitArray[(i / 32) | 0] ^= 1 << i;
1283
}
1284
positiveHorizontalVector = (positiveHorizontalVector << 1) | previousBit;
1285
negativeHorizontalVector = (negativeHorizontalVector << 1) | matchBit;
1286
positiveVector = negativeHorizontalVector | ~(combinedVector | positiveHorizontalVector);
1287
negativeVector = positiveHorizontalVector & combinedVector;
1288
}
1289
1290
// Reset precomputedEqualityArray
1291
for (let k = start; k < verticalLength; k++) {
1292
precomputedEqualityArray[secondString.charCodeAt(k)] = 0;
1293
}
1294
1295
return distance;
1296
}
1297
1298
/**
1299
* Computes the Levenshtein distance between two strings.
1300
* @param firstString - The first string.
1301
* @param secondString - The second string.
1302
* @returns The Levenshtein distance.
1303
*/
1304
export function computeLevenshteinDistance(firstString: string, secondString: string): number {
1305
if (firstString.length < secondString.length) {
1306
const temp = secondString;
1307
secondString = firstString;
1308
firstString = temp;
1309
}
1310
if (secondString.length === 0) {
1311
return firstString.length;
1312
}
1313
if (firstString.length <= 32) {
1314
return computeLevenshteinDistanceForShortStrings(firstString, secondString);
1315
}
1316
return computeLevenshteinDistanceForLongStrings(firstString, secondString);
1317
}
1318
1319