UNPKG

monaco-editor-core

Version:

A browser based code editor

372 lines (371 loc) • 17 kB
/*--------------------------------------------------------------------------------------------- * 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/offsetRange.js'; import { OffsetPair, SequenceDiff } from './algorithms/diffAlgorithm.js'; export function optimizeSequenceDiffs(sequence1, sequence2, sequenceDiffs) { let result = sequenceDiffs; 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, sequence2, sequenceDiffs) { if (sequenceDiffs.length === 0) { return sequenceDiffs; } const result = []; 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++) { const prevResult = result[result.length - 1]; let cur = sequenceDiffs[i]; if (cur.seq1Range.isEmpty || cur.seq2Range.isEmpty) { const length = cur.seq1Range.start - prevResult.seq1Range.endExclusive; 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)) { break; } } d--; if (d === length) { // Merge previous and current diff 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 = []; // Then move them all to the right and join them again if possible for (let i = 0; i < result.length - 1; i++) { const nextResult = result[i + 1]; let cur = result[i]; if (cur.seq1Range.isEmpty || cur.seq2Range.isEmpty) { const length = nextResult.seq1Range.start - cur.seq1Range.endExclusive; 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)) { break; } } if (d === length) { // Merge previous and current diff, write to result! 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) { cur = cur.delta(d); } } 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, sequence2, sequenceDiffs) { if (!sequence1.getBoundaryScore || !sequence2.getBoundaryScore) { return sequenceDiffs; } for (let i = 0; i < sequenceDiffs.length; i++) { const prevDiff = (i > 0 ? sequenceDiffs[i - 1] : undefined); 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); } else if (diff.seq2Range.isEmpty) { sequenceDiffs[i] = shiftDiffToBetterPosition(diff.swap(), sequence2, sequence1, seq2ValidRange, seq1ValidRange).swap(); } } return sequenceDiffs; } function shiftDiffToBetterPosition(diff, sequence1, sequence2, seq1ValidRange, seq2ValidRange) { const maxShiftLimit = 100; // To prevent performance issues // don't touch previous or next! let deltaBefore = 1; while (diff.seq1Range.start - deltaBefore >= seq1ValidRange.start && diff.seq2Range.start - deltaBefore >= seq2ValidRange.start && sequence2.isStronglyEqual(diff.seq2Range.start - deltaBefore, diff.seq2Range.endExclusive - deltaBefore) && deltaBefore < maxShiftLimit) { deltaBefore++; } deltaBefore--; let deltaAfter = 0; while (diff.seq1Range.start + deltaAfter < seq1ValidRange.endExclusive && diff.seq2Range.endExclusive + deltaAfter < seq2ValidRange.endExclusive && sequence2.isStronglyEqual(diff.seq2Range.start + deltaAfter, diff.seq2Range.endExclusive + deltaAfter) && deltaAfter < maxShiftLimit) { deltaAfter++; } if (deltaBefore === 0 && deltaAfter === 0) { return diff; } // 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, sequence2, sequenceDiffs) { const result = []; 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) { result[result.length - 1] = new SequenceDiff(last.seq1Range.join(s.seq1Range), last.seq2Range.join(s.seq2Range)); } else { result.push(s); } } return result; } export function extendDiffsToEntireWordIfAppropriate(sequence1, sequence2, sequenceDiffs) { const equalMappings = SequenceDiff.invert(sequenceDiffs, sequence1.length); const additional = []; let lastPoint = new OffsetPair(0, 0); function scanWord(pair, equalMapping) { if (pair.offset1 < lastPoint.offset1 || pair.offset2 < lastPoint.offset2) { return; } const w1 = sequence1.findWordContaining(pair.offset1); const w2 = sequence2.findWordContaining(pair.offset2); if (!w1 || !w2) { return; } let w = new SequenceDiff(w1, w2); 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 next = equalMappings[0]; const intersects = next.seq1Range.intersects(w.seq1Range) || next.seq2Range.intersects(w.seq2Range); if (!intersects) { break; } const v1 = sequence1.findWordContaining(next.seq1Range.start); const v2 = sequence2.findWordContaining(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) { // The word extends beyond the next equal mapping. equalMappings.shift(); } else { break; } } if (equalChars1 + equalChars2 < (w.seq1Range.length + w.seq2Range.length) * 2 / 3) { additional.push(w); } lastPoint = w.getEndExclusives(); } while (equalMappings.length > 0) { const next = equalMappings.shift(); if (next.seq1Range.isEmpty) { continue; } scanWord(next.getStarts(), next); // 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, sequenceDiffs2) { const result = []; while (sequenceDiffs1.length > 0 || sequenceDiffs2.length > 0) { const sd1 = sequenceDiffs1[0]; const sd2 = sequenceDiffs2[0]; let next; if (sd1 && (!sd2 || sd1.seq1Range.start < sd2.seq1Range.start)) { next = sequenceDiffs1.shift(); } else { next = sequenceDiffs2.shift(); } if (result.length > 0 && result[result.length - 1].seq1Range.endExclusive >= next.seq1Range.start) { result[result.length - 1] = result[result.length - 1].join(next); } else { result.push(next); } } return result; } export function removeVeryShortMatchingLinesBetweenDiffs(sequence1, _sequence2, sequenceDiffs) { let diffs = sequenceDiffs; if (diffs.length === 0) { return diffs; } let counter = 0; let shouldRepeat; do { shouldRepeat = false; const result = [ diffs[0] ]; for (let i = 1; i < diffs.length; i++) { const cur = diffs[i]; const lastResult = result[result.length - 1]; function shouldJoinDiffs(before, after) { 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 true; } return false; } const shouldJoin = shouldJoinDiffs(lastResult, cur); if (shouldJoin) { shouldRepeat = true; result[result.length - 1] = result[result.length - 1].join(cur); } else { result.push(cur); } } diffs = result; } while (counter++ < 10 && shouldRepeat); return diffs; } export function removeVeryShortMatchingTextBetweenLongDiffs(sequence1, sequence2, sequenceDiffs) { let diffs = sequenceDiffs; if (diffs.length === 0) { return diffs; } let counter = 0; let shouldRepeat; do { shouldRepeat = false; const result = [ diffs[0] ]; for (let i = 1; i < diffs.length; i++) { const cur = diffs[i]; const lastResult = result[result.length - 1]; function shouldJoinDiffs(before, after) { const unchangedRange = new OffsetRange(lastResult.seq1Range.endExclusive, cur.seq1Range.start); const unchangedLineCount = sequence1.countLinesIn(unchangedRange); if (unchangedLineCount > 5 || unchangedRange.length > 500) { return false; } const unchangedText = sequence1.getText(unchangedRange).trim(); if (unchangedText.length > 20 || unchangedText.split(/\r\n|\r|\n/).length > 1) { return false; } 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) { 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) { return true; } return false; } const shouldJoin = shouldJoinDiffs(lastResult, cur); if (shouldJoin) { shouldRepeat = true; result[result.length - 1] = result[result.length - 1].join(cur); } else { result.push(cur); } } diffs = result; } while (counter++ < 10 && shouldRepeat); const newDiffs = []; // Remove short suffixes/prefixes forEachWithNeighbors(diffs, (prev, cur, next) => { let newDiff = cur; function shouldMarkAsChanged(text) { 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)) { newDiff = newDiff.deltaStart(-prefix.length); } const suffix = sequence1.getText(new OffsetRange(cur.seq1Range.endExclusive, fullRange1.endExclusive)); if (shouldMarkAsChanged(suffix)) { newDiff = newDiff.deltaEnd(suffix.length); } 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); } else { newDiffs.push(result); } }); return newDiffs; }