Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/editor/common/diff/defaultLinesDiffComputer/defaultLinesDiffComputer.ts
3296 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import { equals } from '../../../../base/common/arrays.js';
7
import { assertFn } from '../../../../base/common/assert.js';
8
import { LineRange } from '../../core/ranges/lineRange.js';
9
import { OffsetRange } from '../../core/ranges/offsetRange.js';
10
import { Position } from '../../core/position.js';
11
import { Range } from '../../core/range.js';
12
import { ArrayText } from '../../core/text/abstractText.js';
13
import { ILinesDiffComputer, ILinesDiffComputerOptions, LinesDiff, MovedText } from '../linesDiffComputer.js';
14
import { DetailedLineRangeMapping, LineRangeMapping, lineRangeMappingFromRangeMappings, RangeMapping } from '../rangeMapping.js';
15
import { DateTimeout, InfiniteTimeout, ITimeout, SequenceDiff } from './algorithms/diffAlgorithm.js';
16
import { DynamicProgrammingDiffing } from './algorithms/dynamicProgrammingDiffing.js';
17
import { MyersDiffAlgorithm } from './algorithms/myersDiffAlgorithm.js';
18
import { computeMovedLines } from './computeMovedLines.js';
19
import { extendDiffsToEntireWordIfAppropriate, optimizeSequenceDiffs, removeShortMatches, removeVeryShortMatchingLinesBetweenDiffs, removeVeryShortMatchingTextBetweenLongDiffs } from './heuristicSequenceOptimizations.js';
20
import { LineSequence } from './lineSequence.js';
21
import { LinesSliceCharSequence } from './linesSliceCharSequence.js';
22
23
export class DefaultLinesDiffComputer implements ILinesDiffComputer {
24
private readonly dynamicProgrammingDiffing = new DynamicProgrammingDiffing();
25
private readonly myersDiffingAlgorithm = new MyersDiffAlgorithm();
26
27
computeDiff(originalLines: string[], modifiedLines: string[], options: ILinesDiffComputerOptions): LinesDiff {
28
if (originalLines.length <= 1 && equals(originalLines, modifiedLines, (a, b) => a === b)) {
29
return new LinesDiff([], [], false);
30
}
31
32
if (originalLines.length === 1 && originalLines[0].length === 0 || modifiedLines.length === 1 && modifiedLines[0].length === 0) {
33
return new LinesDiff([
34
new DetailedLineRangeMapping(
35
new LineRange(1, originalLines.length + 1),
36
new LineRange(1, modifiedLines.length + 1),
37
[
38
new RangeMapping(
39
new Range(1, 1, originalLines.length, originalLines[originalLines.length - 1].length + 1),
40
new Range(1, 1, modifiedLines.length, modifiedLines[modifiedLines.length - 1].length + 1),
41
)
42
]
43
)
44
], [], false);
45
}
46
47
const timeout = options.maxComputationTimeMs === 0 ? InfiniteTimeout.instance : new DateTimeout(options.maxComputationTimeMs);
48
const considerWhitespaceChanges = !options.ignoreTrimWhitespace;
49
50
const perfectHashes = new Map<string, number>();
51
function getOrCreateHash(text: string): number {
52
let hash = perfectHashes.get(text);
53
if (hash === undefined) {
54
hash = perfectHashes.size;
55
perfectHashes.set(text, hash);
56
}
57
return hash;
58
}
59
60
const originalLinesHashes = originalLines.map((l) => getOrCreateHash(l.trim()));
61
const modifiedLinesHashes = modifiedLines.map((l) => getOrCreateHash(l.trim()));
62
63
const sequence1 = new LineSequence(originalLinesHashes, originalLines);
64
const sequence2 = new LineSequence(modifiedLinesHashes, modifiedLines);
65
66
const lineAlignmentResult = (() => {
67
if (sequence1.length + sequence2.length < 1700) {
68
// Use the improved algorithm for small files
69
return this.dynamicProgrammingDiffing.compute(
70
sequence1,
71
sequence2,
72
timeout,
73
(offset1, offset2) =>
74
originalLines[offset1] === modifiedLines[offset2]
75
? modifiedLines[offset2].length === 0
76
? 0.1
77
: 1 + Math.log(1 + modifiedLines[offset2].length)
78
: 0.99
79
);
80
}
81
82
return this.myersDiffingAlgorithm.compute(
83
sequence1,
84
sequence2,
85
timeout
86
);
87
})();
88
89
let lineAlignments = lineAlignmentResult.diffs;
90
let hitTimeout = lineAlignmentResult.hitTimeout;
91
lineAlignments = optimizeSequenceDiffs(sequence1, sequence2, lineAlignments);
92
lineAlignments = removeVeryShortMatchingLinesBetweenDiffs(sequence1, sequence2, lineAlignments);
93
94
const alignments: RangeMapping[] = [];
95
96
const scanForWhitespaceChanges = (equalLinesCount: number) => {
97
if (!considerWhitespaceChanges) {
98
return;
99
}
100
101
for (let i = 0; i < equalLinesCount; i++) {
102
const seq1Offset = seq1LastStart + i;
103
const seq2Offset = seq2LastStart + i;
104
if (originalLines[seq1Offset] !== modifiedLines[seq2Offset]) {
105
// This is because of whitespace changes, diff these lines
106
const characterDiffs = this.refineDiff(originalLines, modifiedLines, new SequenceDiff(
107
new OffsetRange(seq1Offset, seq1Offset + 1),
108
new OffsetRange(seq2Offset, seq2Offset + 1),
109
), timeout, considerWhitespaceChanges, options);
110
for (const a of characterDiffs.mappings) {
111
alignments.push(a);
112
}
113
if (characterDiffs.hitTimeout) {
114
hitTimeout = true;
115
}
116
}
117
}
118
};
119
120
let seq1LastStart = 0;
121
let seq2LastStart = 0;
122
123
for (const diff of lineAlignments) {
124
assertFn(() => diff.seq1Range.start - seq1LastStart === diff.seq2Range.start - seq2LastStart);
125
126
const equalLinesCount = diff.seq1Range.start - seq1LastStart;
127
128
scanForWhitespaceChanges(equalLinesCount);
129
130
seq1LastStart = diff.seq1Range.endExclusive;
131
seq2LastStart = diff.seq2Range.endExclusive;
132
133
const characterDiffs = this.refineDiff(originalLines, modifiedLines, diff, timeout, considerWhitespaceChanges, options);
134
if (characterDiffs.hitTimeout) {
135
hitTimeout = true;
136
}
137
for (const a of characterDiffs.mappings) {
138
alignments.push(a);
139
}
140
}
141
142
scanForWhitespaceChanges(originalLines.length - seq1LastStart);
143
144
const original = new ArrayText(originalLines);
145
const modified = new ArrayText(modifiedLines);
146
147
const changes = lineRangeMappingFromRangeMappings(alignments, original, modified);
148
149
let moves: MovedText[] = [];
150
if (options.computeMoves) {
151
moves = this.computeMoves(changes, originalLines, modifiedLines, originalLinesHashes, modifiedLinesHashes, timeout, considerWhitespaceChanges, options);
152
}
153
154
// Make sure all ranges are valid
155
assertFn(() => {
156
function validatePosition(pos: Position, lines: string[]): boolean {
157
if (pos.lineNumber < 1 || pos.lineNumber > lines.length) { return false; }
158
const line = lines[pos.lineNumber - 1];
159
if (pos.column < 1 || pos.column > line.length + 1) { return false; }
160
return true;
161
}
162
163
function validateRange(range: LineRange, lines: string[]): boolean {
164
if (range.startLineNumber < 1 || range.startLineNumber > lines.length + 1) { return false; }
165
if (range.endLineNumberExclusive < 1 || range.endLineNumberExclusive > lines.length + 1) { return false; }
166
return true;
167
}
168
169
for (const c of changes) {
170
if (!c.innerChanges) { return false; }
171
for (const ic of c.innerChanges) {
172
const valid = validatePosition(ic.modifiedRange.getStartPosition(), modifiedLines) && validatePosition(ic.modifiedRange.getEndPosition(), modifiedLines) &&
173
validatePosition(ic.originalRange.getStartPosition(), originalLines) && validatePosition(ic.originalRange.getEndPosition(), originalLines);
174
if (!valid) {
175
return false;
176
}
177
}
178
if (!validateRange(c.modified, modifiedLines) || !validateRange(c.original, originalLines)) {
179
return false;
180
}
181
}
182
return true;
183
});
184
185
return new LinesDiff(changes, moves, hitTimeout);
186
}
187
188
private computeMoves(
189
changes: DetailedLineRangeMapping[],
190
originalLines: string[],
191
modifiedLines: string[],
192
hashedOriginalLines: number[],
193
hashedModifiedLines: number[],
194
timeout: ITimeout,
195
considerWhitespaceChanges: boolean,
196
options: ILinesDiffComputerOptions,
197
): MovedText[] {
198
const moves = computeMovedLines(
199
changes,
200
originalLines,
201
modifiedLines,
202
hashedOriginalLines,
203
hashedModifiedLines,
204
timeout,
205
);
206
const movesWithDiffs = moves.map(m => {
207
const moveChanges = this.refineDiff(originalLines, modifiedLines, new SequenceDiff(
208
m.original.toOffsetRange(),
209
m.modified.toOffsetRange(),
210
), timeout, considerWhitespaceChanges, options);
211
const mappings = lineRangeMappingFromRangeMappings(moveChanges.mappings, new ArrayText(originalLines), new ArrayText(modifiedLines), true);
212
return new MovedText(m, mappings);
213
});
214
return movesWithDiffs;
215
}
216
217
private refineDiff(originalLines: string[], modifiedLines: string[], diff: SequenceDiff, timeout: ITimeout, considerWhitespaceChanges: boolean, options: ILinesDiffComputerOptions): { mappings: RangeMapping[]; hitTimeout: boolean } {
218
const lineRangeMapping = toLineRangeMapping(diff);
219
const rangeMapping = lineRangeMapping.toRangeMapping2(originalLines, modifiedLines);
220
221
const slice1 = new LinesSliceCharSequence(originalLines, rangeMapping.originalRange, considerWhitespaceChanges);
222
const slice2 = new LinesSliceCharSequence(modifiedLines, rangeMapping.modifiedRange, considerWhitespaceChanges);
223
224
const diffResult = slice1.length + slice2.length < 500
225
? this.dynamicProgrammingDiffing.compute(slice1, slice2, timeout)
226
: this.myersDiffingAlgorithm.compute(slice1, slice2, timeout);
227
228
const check = false;
229
230
let diffs = diffResult.diffs;
231
if (check) { SequenceDiff.assertSorted(diffs); }
232
diffs = optimizeSequenceDiffs(slice1, slice2, diffs);
233
if (check) { SequenceDiff.assertSorted(diffs); }
234
diffs = extendDiffsToEntireWordIfAppropriate(slice1, slice2, diffs, (seq, idx) => seq.findWordContaining(idx));
235
if (check) { SequenceDiff.assertSorted(diffs); }
236
237
if (options.extendToSubwords) {
238
diffs = extendDiffsToEntireWordIfAppropriate(slice1, slice2, diffs, (seq, idx) => seq.findSubWordContaining(idx), true);
239
if (check) { SequenceDiff.assertSorted(diffs); }
240
}
241
242
diffs = removeShortMatches(slice1, slice2, diffs);
243
if (check) { SequenceDiff.assertSorted(diffs); }
244
diffs = removeVeryShortMatchingTextBetweenLongDiffs(slice1, slice2, diffs);
245
if (check) { SequenceDiff.assertSorted(diffs); }
246
247
const result = diffs.map(
248
(d) =>
249
new RangeMapping(
250
slice1.translateRange(d.seq1Range),
251
slice2.translateRange(d.seq2Range)
252
)
253
);
254
255
if (check) { RangeMapping.assertSorted(result); }
256
257
// Assert: result applied on original should be the same as diff applied to original
258
259
return {
260
mappings: result,
261
hitTimeout: diffResult.hitTimeout,
262
};
263
}
264
}
265
266
function toLineRangeMapping(sequenceDiff: SequenceDiff) {
267
return new LineRangeMapping(
268
new LineRange(sequenceDiff.seq1Range.start + 1, sequenceDiff.seq1Range.endExclusive + 1),
269
new LineRange(sequenceDiff.seq2Range.start + 1, sequenceDiff.seq2Range.endExclusive + 1),
270
);
271
}
272
273