src/vs/editor/common/diff/defaultLinesDiffComputer/heuristicSequenceOptimizations.ts
473 LOC · 469 covered · 4 uncovered · 117 ranges · 286 concepts · 46 introducers · 133 tests
File neighbourhood
The centred file is linked to every concept that introduces one of its ranges, every test that runs code from the file, and the gray connector concepts standing between those tests and the file's own introducer concepts. Undirected links join concepts to every file where they introduce source and concepts to the tests they introduce; arrows show specialization between the displayed concepts and bridge only concepts omitted from this view. Concept colors match the source ranges below; connector concepts have no source color and are shown in gray.
Focused file, its introducer and connector concepts, their introduced files, and tests that run code from the file
In the embedded map, ordinary wheel input scrolls the page; use the visible controls to zoom and drag to pan. Open the full-screen map for canvas navigation: wheel pans, Ctrl/Command plus wheel zooms, and arrow keys pan when this region is focused. On touch screens, open the full-screen map to pan or pinch. If JavaScript or WebGL is unavailable, use the related-file, concept, and source links on this page.
Graph controls are ready.
Interactive rendering requires JavaScript and WebGL. Use the related-file, concept, and source links on this page while the interactive map is unavailable.
/*---------------------------------------------------------------------------------------------
heuristicSequenceOptimizations.ts ×9
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { forEachWithNeighbors } from '../../../../base/common/arrays.js';
import { OffsetRange } from '../../core/ranges/offsetRange.js';
import { ISequence, OffsetPair, SequenceDiff } from './algorithms/diffAlgorithm.js';
import { LineSequence } from './lineSequence.js';
import { LinesSliceCharSequence } from './linesSliceCharSequence.js';
export function optimizeSequenceDiffs(sequence1: ISequence, sequence2: ISequence, sequenceDiffs: SequenceDiff[]): SequenceDiff[] {
result = joinSequenceDiffsByShifting(sequence1, sequence2, result);
// Sometimes, calling this function twice improves the result.
// Uncomment the second invocation and run the tests to see the difference.
result = joinSequenceDiffsByShifting(sequence1, sequence2, result);
result = shiftSequenceDiffs(sequence1, sequence2, result);
return result;
}
/**
* This function fixes issues like this:
* ```
* import { Baz, Bar } from "foo";
* ```
* <->
* ```
* import { Baz, Bar, Foo } from "foo";
* ```
* Computed diff: [ {Add "," after Bar}, {Add "Foo " after space} }
* Improved diff: [{Add ", Foo" after Bar}]
*/
function joinSequenceDiffsByShifting(sequence1: ISequence, sequence2: ISequence, sequenceDiffs: SequenceDiff[]): SequenceDiff[] {
defaultLinesDiffComputer.ts ×11
if (sequenceDiffs.length === 0) {
}
const result: SequenceDiff[] = [];
result.push(sequenceDiffs[0]);
// First move them all to the left as much as possible and join them if possible
for (let i = 1; i < sequenceDiffs.length; i++) {
let cur = sequenceDiffs[i];
if (cur.seq1Range.isEmpty || cur.seq2Range.isEmpty) {
const length = cur.seq1Range.start - prevResult.seq1Range.endExclusive;
heuristicSequenceOptimizations.ts ×2
let d;
for (d = 1; d <= length; d++) {
if (
sequence1.getElement(cur.seq1Range.start - d) !== sequence1.getElement(cur.seq1Range.endExclusive - d) ||
sequence2.getElement(cur.seq2Range.start - d) !== sequence2.getElement(cur.seq2Range.endExclusive - d)) {
}
d--;
if (d === length) {
result[result.length - 1] = new SequenceDiff(
new OffsetRange(prevResult.seq1Range.start, cur.seq1Range.endExclusive - length),
new OffsetRange(prevResult.seq2Range.start, cur.seq2Range.endExclusive - length),
);
continue;
}
cur = cur.delta(-d);
}
result.push(cur);
}
const result2: SequenceDiff[] = [];
// Then move them all to the right and join them again if possible
for (let i = 0; i < result.length - 1; i++) {
let cur = result[i];
if (cur.seq1Range.isEmpty || cur.seq2Range.isEmpty) {
const length = nextResult.seq1Range.start - cur.seq1Range.endExclusive;
heuristicSequenceOptimizations.ts ×4
let d;
for (d = 0; d < length; d++) {
if (
!sequence1.isStronglyEqual(cur.seq1Range.start + d, cur.seq1Range.endExclusive + d) ||
!sequence2.isStronglyEqual(cur.seq2Range.start + d, cur.seq2Range.endExclusive + d)
heuristicSequenceOptimizations.ts ×1
}
if (d === length) {
result[i + 1] = new SequenceDiff(
new OffsetRange(cur.seq1Range.start + length, nextResult.seq1Range.endExclusive),
new OffsetRange(cur.seq2Range.start + length, nextResult.seq2Range.endExclusive),
);
continue;
}
if (d > 0) {
}
result2.push(cur);
}
if (result.length > 0) {
result2.push(result[result.length - 1]);
}
return result2;
}
// align character level diffs at whitespace characters
// import { IBar } from "foo";
// import { I[Arr, I]Bar } from "foo";
// ->
// import { [IArr, ]IBar } from "foo";
// import { ITransaction, observableValue, transaction } from 'vs/base/common/observable';
// import { ITransaction, observable[FromEvent, observable]Value, transaction } from 'vs/base/common/observable';
// ->
// import { ITransaction, [observableFromEvent, ]observableValue, transaction } from 'vs/base/common/observable';
// collectBrackets(level + 1, levelPerBracketType);
// collectBrackets(level + 1, levelPerBracket[ + 1, levelPerBracket]Type);
// ->
// collectBrackets(level + 1, [levelPerBracket + 1, ]levelPerBracketType);
function shiftSequenceDiffs(sequence1: ISequence, sequence2: ISequence, sequenceDiffs: SequenceDiff[]): SequenceDiff[] {
defaultLinesDiffComputer.ts ×11
if (!sequence1.getBoundaryScore || !sequence2.getBoundaryScore) {
return sequenceDiffs;
}
for (let i = 0; i < sequenceDiffs.length; i++) {
const prevDiff = (i > 0 ? sequenceDiffs[i - 1] : undefined);
heuristicSequenceOptimizations.ts ×26
const diff = sequenceDiffs[i];
const nextDiff = (i + 1 < sequenceDiffs.length ? sequenceDiffs[i + 1] : undefined);
const seq1ValidRange = new OffsetRange(prevDiff ? prevDiff.seq1Range.endExclusive + 1 : 0, nextDiff ? nextDiff.seq1Range.start - 1 : sequence1.length);
const seq2ValidRange = new OffsetRange(prevDiff ? prevDiff.seq2Range.endExclusive + 1 : 0, nextDiff ? nextDiff.seq2Range.start - 1 : sequence2.length);
if (diff.seq1Range.isEmpty) {
sequenceDiffs[i] = shiftDiffToBetterPosition(diff, sequence1, sequence2, seq1ValidRange, seq2ValidRange);
heuristicSequenceOptimizations.ts ×1
sequenceDiffs[i] = shiftDiffToBetterPosition(diff.swap(), sequence2, sequence1, seq2ValidRange, seq1ValidRange).swap();
diffAlgorithm.ts ×1
}
return sequenceDiffs;
}
function shiftDiffToBetterPosition(diff: SequenceDiff, sequence1: ISequence, sequence2: ISequence, seq1ValidRange: OffsetRange, seq2ValidRange: OffsetRange,) {
heuristicSequenceOptimizations.ts ×26
const maxShiftLimit = 100; // To prevent performance issues
// don't touch previous or next!
let deltaBefore = 1;
while (
diff.seq1Range.start - deltaBefore >= seq1ValidRange.start &&
sequence2.isStronglyEqual(diff.seq2Range.start - deltaBefore, diff.seq2Range.endExclusive - deltaBefore) && deltaBefore < maxShiftLimit
heuristicSequenceOptimizations.ts ×26
) {
}
let deltaAfter = 0;
while (
diff.seq1Range.start + deltaAfter < seq1ValidRange.endExclusive &&
diff.seq2Range.endExclusive + deltaAfter < seq2ValidRange.endExclusive &&
heuristicSequenceOptimizations.ts ×3
sequence2.isStronglyEqual(diff.seq2Range.start + deltaAfter, diff.seq2Range.endExclusive + deltaAfter) && deltaAfter < maxShiftLimit
heuristicSequenceOptimizations.ts ×26
) {
}
if (deltaBefore === 0 && deltaAfter === 0) {
}
// Visualize `[sequence1.text, diff.seq1Range.start + deltaAfter]`
// and `[sequence2.text, diff.seq2Range.start + deltaAfter, diff.seq2Range.endExclusive + deltaAfter]`
let bestDelta = 0;
let bestScore = -1;
// find best scored delta
for (let delta = -deltaBefore; delta <= deltaAfter; delta++) {
const seq2OffsetStart = diff.seq2Range.start + delta;
const seq2OffsetEndExclusive = diff.seq2Range.endExclusive + delta;
const seq1Offset = diff.seq1Range.start + delta;
const score = sequence1.getBoundaryScore!(seq1Offset) + sequence2.getBoundaryScore!(seq2OffsetStart) + sequence2.getBoundaryScore!(seq2OffsetEndExclusive);
if (score > bestScore) {
bestScore = score;
bestDelta = delta;
}
}
return diff.delta(bestDelta);
}
export function removeShortMatches(sequence1: ISequence, sequence2: ISequence, sequenceDiffs: SequenceDiff[]): SequenceDiff[] {
for (const s of sequenceDiffs) {
const last = result[result.length - 1];
if (!last) {
result.push(s);
continue;
}
if (s.seq1Range.start - last.seq1Range.endExclusive <= 2 || s.seq2Range.start - last.seq2Range.endExclusive <= 2) {
heuristicSequenceOptimizations.ts ×26
result[result.length - 1] = new SequenceDiff(last.seq1Range.join(s.seq1Range), last.seq2Range.join(s.seq2Range));
heuristicSequenceOptimizations.ts ×1
}
return result;
}
export function extendDiffsToEntireWordIfAppropriate(
sequence2: LinesSliceCharSequence,
sequenceDiffs: SequenceDiff[],
findParent: (seq: LinesSliceCharSequence, idx: number) => OffsetRange | undefined,
force: boolean = false,
): SequenceDiff[] {
const equalMappings = SequenceDiff.invert(sequenceDiffs, sequence1.length);
const additional: SequenceDiff[] = [];
let lastPoint = new OffsetPair(0, 0);
function scanWord(pair: OffsetPair, equalMapping: SequenceDiff) {
if (pair.offset1 < lastPoint.offset1 || pair.offset2 < lastPoint.offset2) {
linesSliceCharSequence.ts ×7
}
const w1 = findParent(sequence1, pair.offset1);
const w2 = findParent(sequence2, pair.offset2);
if (!w1 || !w2) {
return;
}
const equalPart = w.intersect(equalMapping)!;
let equalChars1 = equalPart.seq1Range.length;
let equalChars2 = equalPart.seq2Range.length;
// The words do not touch previous equals mappings, as we would have processed them already.
// But they might touch the next ones.
while (equalMappings.length > 0) {
const intersects = next.seq1Range.intersects(w.seq1Range) || next.seq2Range.intersects(w.seq2Range);
if (!intersects) {
}
const v1 = findParent(sequence1, next.seq1Range.start);
const v2 = findParent(sequence2, next.seq2Range.start);
// Because there is an intersection, we know that the words are not empty.
const v = new SequenceDiff(v1!, v2!);
const equalPart = v.intersect(next)!;
equalChars1 += equalPart.seq1Range.length;
equalChars2 += equalPart.seq2Range.length;
w = w.join(v);
if (w.seq1Range.endExclusive >= next.seq1Range.endExclusive) {
equalMappings.shift();
}
if ((force && equalChars1 + equalChars2 < w.seq1Range.length + w.seq2Range.length) || equalChars1 + equalChars2 < (w.seq1Range.length + w.seq2Range.length) * 2 / 3) {
linesSliceCharSequence.ts ×7
}
lastPoint = w.getEndExclusives();
while (equalMappings.length > 0) {
const next = equalMappings.shift()!;
if (next.seq1Range.isEmpty) {
}
// The equal parts are not empty, so -1 gives us a character that is equal in both parts.
scanWord(next.getEndExclusives().delta(-1), next);
}
const merged = mergeSequenceDiffs(sequenceDiffs, additional);
return merged;
}
function mergeSequenceDiffs(sequenceDiffs1: SequenceDiff[], sequenceDiffs2: SequenceDiff[]): SequenceDiff[] {
heuristicSequenceOptimizations.ts ×26
const result: SequenceDiff[] = [];
while (sequenceDiffs1.length > 0 || sequenceDiffs2.length > 0) {
const sd1 = sequenceDiffs1[0];
const sd2 = sequenceDiffs2[0];
let next: SequenceDiff;
if (sd1 && (!sd2 || sd1.seq1Range.start < sd2.seq1Range.start)) {
next = sequenceDiffs1.shift()!;
} else {
}
if (result.length > 0 && result[result.length - 1].seq1Range.endExclusive >= next.seq1Range.start) {
result[result.length - 1] = result[result.length - 1].join(next);
heuristicSequenceOptimizations.ts ×3
result.push(next);
}
}
return result;
}
export function removeVeryShortMatchingLinesBetweenDiffs(sequence1: LineSequence, _sequence2: LineSequence, sequenceDiffs: SequenceDiff[]): SequenceDiff[] {
if (diffs.length === 0) {
}
let counter = 0;
let shouldRepeat: boolean;
do {
shouldRepeat = false;
const result: SequenceDiff[] = [
diffs[0]
];
for (let i = 1; i < diffs.length; i++) {
const lastResult = result[result.length - 1];
function shouldJoinDiffs(before: SequenceDiff, after: SequenceDiff): boolean {
const unchangedRange = new OffsetRange(lastResult.seq1Range.endExclusive, cur.seq1Range.start);
const unchangedText = sequence1.getText(unchangedRange);
const unchangedTextWithoutWs = unchangedText.replace(/\s/g, '');
if (unchangedTextWithoutWs.length <= 4
&& (before.seq1Range.length + before.seq2Range.length > 5 || after.seq1Range.length + after.seq2Range.length > 5)) {
}
return false;
const shouldJoin = shouldJoinDiffs(lastResult, cur);
if (shouldJoin) {
result[result.length - 1] = result[result.length - 1].join(cur);
}
diffs = result;
return diffs;
}
export function removeVeryShortMatchingTextBetweenLongDiffs(sequence1: LinesSliceCharSequence, sequence2: LinesSliceCharSequence, sequenceDiffs: SequenceDiff[]): SequenceDiff[] {
if (diffs.length === 0) {
return diffs;
}
let counter = 0;
let shouldRepeat: boolean;
do {
shouldRepeat = false;
const result: SequenceDiff[] = [
diffs[0]
];
for (let i = 1; i < diffs.length; i++) {
const lastResult = result[result.length - 1];
function shouldJoinDiffs(before: SequenceDiff, after: SequenceDiff): boolean {
const unchangedRange = new OffsetRange(lastResult.seq1Range.endExclusive, cur.seq1Range.start);
const unchangedLineCount = sequence1.countLinesIn(unchangedRange);
if (unchangedLineCount > 5 || unchangedRange.length > 500) {
}
const unchangedText = sequence1.getText(unchangedRange).trim();
if (unchangedText.length > 20 || unchangedText.split(/\r\n|\r|\n/).length > 1) {
}
const beforeLineCount1 = sequence1.countLinesIn(before.seq1Range);
const beforeSeq1Length = before.seq1Range.length;
const beforeLineCount2 = sequence2.countLinesIn(before.seq2Range);
const beforeSeq2Length = before.seq2Range.length;
const afterLineCount1 = sequence1.countLinesIn(after.seq1Range);
const afterSeq1Length = after.seq1Range.length;
const afterLineCount2 = sequence2.countLinesIn(after.seq2Range);
const afterSeq2Length = after.seq2Range.length;
// TODO: Maybe a neural net can be used to derive the result from these numbers
const max = 2 * 40 + 50;
function cap(v: number): number {
return Math.min(v, max);
}
if (Math.pow(Math.pow(cap(beforeLineCount1 * 40 + beforeSeq1Length), 1.5) + Math.pow(cap(beforeLineCount2 * 40 + beforeSeq2Length), 1.5), 1.5)
+ Math.pow(Math.pow(cap(afterLineCount1 * 40 + afterSeq1Length), 1.5) + Math.pow(cap(afterLineCount2 * 40 + afterSeq2Length), 1.5), 1.5) > ((max ** 1.5) ** 1.5) * 1.3) {
}
const shouldJoin = shouldJoinDiffs(lastResult, cur);
if (shouldJoin) {
result[result.length - 1] = result[result.length - 1].join(cur);
result.push(cur);
}
}
diffs = result;
const newDiffs: SequenceDiff[] = [];
// Remove short suffixes/prefixes
forEachWithNeighbors(diffs, (prev, cur, next) => {
let newDiff = cur;
function shouldMarkAsChanged(text: string): boolean {
return text.length > 0 && text.trim().length <= 3 && cur.seq1Range.length + cur.seq2Range.length > 100;
}
const fullRange1 = sequence1.extendToFullLines(cur.seq1Range);
const prefix = sequence1.getText(new OffsetRange(fullRange1.start, cur.seq1Range.start));
if (shouldMarkAsChanged(prefix)) {
}
const suffix = sequence1.getText(new OffsetRange(cur.seq1Range.endExclusive, fullRange1.endExclusive));
heuristicSequenceOptimizations.ts ×26
if (shouldMarkAsChanged(suffix)) {
}
const availableSpace = SequenceDiff.fromOffsetPairs(
prev ? prev.getEndExclusives() : OffsetPair.zero,
next ? next.getStarts() : OffsetPair.max,
);
const result = newDiff.intersect(availableSpace)!;
if (newDiffs.length > 0 && result.getStarts().equals(newDiffs[newDiffs.length - 1].getEndExclusives())) {
newDiffs[newDiffs.length - 1] = newDiffs[newDiffs.length - 1].join(result);
heuristicSequenceOptimizations.ts ×1
newDiffs.push(result);
}
});
return newDiffs;
}