Path: blob/main/src/vs/editor/common/diff/defaultLinesDiffComputer/computeMovedLines.ts
3296 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { ITimeout, SequenceDiff } from './algorithms/diffAlgorithm.js';6import { DetailedLineRangeMapping, LineRangeMapping } from '../rangeMapping.js';7import { pushMany, compareBy, numberComparator, reverseOrder } from '../../../../base/common/arrays.js';8import { MonotonousArray, findLastMonotonous } from '../../../../base/common/arraysFind.js';9import { SetMap } from '../../../../base/common/map.js';10import { LineRange, LineRangeSet } from '../../core/ranges/lineRange.js';11import { LinesSliceCharSequence } from './linesSliceCharSequence.js';12import { LineRangeFragment, isSpace } from './utils.js';13import { MyersDiffAlgorithm } from './algorithms/myersDiffAlgorithm.js';14import { Range } from '../../core/range.js';1516export function computeMovedLines(17changes: DetailedLineRangeMapping[],18originalLines: string[],19modifiedLines: string[],20hashedOriginalLines: number[],21hashedModifiedLines: number[],22timeout: ITimeout23): LineRangeMapping[] {24let { moves, excludedChanges } = computeMovesFromSimpleDeletionsToSimpleInsertions(changes, originalLines, modifiedLines, timeout);2526if (!timeout.isValid()) { return []; }2728const filteredChanges = changes.filter(c => !excludedChanges.has(c));29const unchangedMoves = computeUnchangedMoves(filteredChanges, hashedOriginalLines, hashedModifiedLines, originalLines, modifiedLines, timeout);30pushMany(moves, unchangedMoves);3132moves = joinCloseConsecutiveMoves(moves);33// Ignore too short moves34moves = moves.filter(current => {35const lines = current.original.toOffsetRange().slice(originalLines).map(l => l.trim());36const originalText = lines.join('\n');37return originalText.length >= 15 && countWhere(lines, l => l.length >= 2) >= 2;38});39moves = removeMovesInSameDiff(changes, moves);4041return moves;42}4344function countWhere<T>(arr: T[], predicate: (t: T) => boolean): number {45let count = 0;46for (const t of arr) {47if (predicate(t)) {48count++;49}50}51return count;52}5354function computeMovesFromSimpleDeletionsToSimpleInsertions(55changes: DetailedLineRangeMapping[],56originalLines: string[],57modifiedLines: string[],58timeout: ITimeout,59) {60const moves: LineRangeMapping[] = [];6162const deletions = changes63.filter(c => c.modified.isEmpty && c.original.length >= 3)64.map(d => new LineRangeFragment(d.original, originalLines, d));65const insertions = new Set(changes66.filter(c => c.original.isEmpty && c.modified.length >= 3)67.map(d => new LineRangeFragment(d.modified, modifiedLines, d)));6869const excludedChanges = new Set<DetailedLineRangeMapping>();7071for (const deletion of deletions) {72let highestSimilarity = -1;73let best: LineRangeFragment | undefined;74for (const insertion of insertions) {75const similarity = deletion.computeSimilarity(insertion);76if (similarity > highestSimilarity) {77highestSimilarity = similarity;78best = insertion;79}80}8182if (highestSimilarity > 0.90 && best) {83insertions.delete(best);84moves.push(new LineRangeMapping(deletion.range, best.range));85excludedChanges.add(deletion.source);86excludedChanges.add(best.source);87}8889if (!timeout.isValid()) {90return { moves, excludedChanges };91}92}9394return { moves, excludedChanges };95}9697function computeUnchangedMoves(98changes: DetailedLineRangeMapping[],99hashedOriginalLines: number[],100hashedModifiedLines: number[],101originalLines: string[],102modifiedLines: string[],103timeout: ITimeout,104) {105const moves: LineRangeMapping[] = [];106107const original3LineHashes = new SetMap<string, { range: LineRange }>();108109for (const change of changes) {110for (let i = change.original.startLineNumber; i < change.original.endLineNumberExclusive - 2; i++) {111const key = `${hashedOriginalLines[i - 1]}:${hashedOriginalLines[i + 1 - 1]}:${hashedOriginalLines[i + 2 - 1]}`;112original3LineHashes.add(key, { range: new LineRange(i, i + 3) });113}114}115116interface PossibleMapping {117modifiedLineRange: LineRange;118originalLineRange: LineRange;119}120121const possibleMappings: PossibleMapping[] = [];122123changes.sort(compareBy(c => c.modified.startLineNumber, numberComparator));124125for (const change of changes) {126let lastMappings: PossibleMapping[] = [];127for (let i = change.modified.startLineNumber; i < change.modified.endLineNumberExclusive - 2; i++) {128const key = `${hashedModifiedLines[i - 1]}:${hashedModifiedLines[i + 1 - 1]}:${hashedModifiedLines[i + 2 - 1]}`;129const currentModifiedRange = new LineRange(i, i + 3);130131const nextMappings: PossibleMapping[] = [];132original3LineHashes.forEach(key, ({ range }) => {133for (const lastMapping of lastMappings) {134// does this match extend some last match?135if (lastMapping.originalLineRange.endLineNumberExclusive + 1 === range.endLineNumberExclusive &&136lastMapping.modifiedLineRange.endLineNumberExclusive + 1 === currentModifiedRange.endLineNumberExclusive) {137lastMapping.originalLineRange = new LineRange(lastMapping.originalLineRange.startLineNumber, range.endLineNumberExclusive);138lastMapping.modifiedLineRange = new LineRange(lastMapping.modifiedLineRange.startLineNumber, currentModifiedRange.endLineNumberExclusive);139nextMappings.push(lastMapping);140return;141}142}143144const mapping: PossibleMapping = {145modifiedLineRange: currentModifiedRange,146originalLineRange: range,147};148possibleMappings.push(mapping);149nextMappings.push(mapping);150});151lastMappings = nextMappings;152}153154if (!timeout.isValid()) {155return [];156}157}158159possibleMappings.sort(reverseOrder(compareBy(m => m.modifiedLineRange.length, numberComparator)));160161const modifiedSet = new LineRangeSet();162const originalSet = new LineRangeSet();163164for (const mapping of possibleMappings) {165166const diffOrigToMod = mapping.modifiedLineRange.startLineNumber - mapping.originalLineRange.startLineNumber;167const modifiedSections = modifiedSet.subtractFrom(mapping.modifiedLineRange);168const originalTranslatedSections = originalSet.subtractFrom(mapping.originalLineRange).getWithDelta(diffOrigToMod);169170const modifiedIntersectedSections = modifiedSections.getIntersection(originalTranslatedSections);171172for (const s of modifiedIntersectedSections.ranges) {173if (s.length < 3) {174continue;175}176const modifiedLineRange = s;177const originalLineRange = s.delta(-diffOrigToMod);178179moves.push(new LineRangeMapping(originalLineRange, modifiedLineRange));180181modifiedSet.addRange(modifiedLineRange);182originalSet.addRange(originalLineRange);183}184}185186moves.sort(compareBy(m => m.original.startLineNumber, numberComparator));187188const monotonousChanges = new MonotonousArray(changes);189for (let i = 0; i < moves.length; i++) {190const move = moves[i];191const firstTouchingChangeOrig = monotonousChanges.findLastMonotonous(c => c.original.startLineNumber <= move.original.startLineNumber)!;192const firstTouchingChangeMod = findLastMonotonous(changes, c => c.modified.startLineNumber <= move.modified.startLineNumber)!;193const linesAbove = Math.max(194move.original.startLineNumber - firstTouchingChangeOrig.original.startLineNumber,195move.modified.startLineNumber - firstTouchingChangeMod.modified.startLineNumber196);197198const lastTouchingChangeOrig = monotonousChanges.findLastMonotonous(c => c.original.startLineNumber < move.original.endLineNumberExclusive)!;199const lastTouchingChangeMod = findLastMonotonous(changes, c => c.modified.startLineNumber < move.modified.endLineNumberExclusive)!;200const linesBelow = Math.max(201lastTouchingChangeOrig.original.endLineNumberExclusive - move.original.endLineNumberExclusive,202lastTouchingChangeMod.modified.endLineNumberExclusive - move.modified.endLineNumberExclusive203);204205let extendToTop: number;206for (extendToTop = 0; extendToTop < linesAbove; extendToTop++) {207const origLine = move.original.startLineNumber - extendToTop - 1;208const modLine = move.modified.startLineNumber - extendToTop - 1;209if (origLine > originalLines.length || modLine > modifiedLines.length) {210break;211}212if (modifiedSet.contains(modLine) || originalSet.contains(origLine)) {213break;214}215if (!areLinesSimilar(originalLines[origLine - 1], modifiedLines[modLine - 1], timeout)) {216break;217}218}219220if (extendToTop > 0) {221originalSet.addRange(new LineRange(move.original.startLineNumber - extendToTop, move.original.startLineNumber));222modifiedSet.addRange(new LineRange(move.modified.startLineNumber - extendToTop, move.modified.startLineNumber));223}224225let extendToBottom: number;226for (extendToBottom = 0; extendToBottom < linesBelow; extendToBottom++) {227const origLine = move.original.endLineNumberExclusive + extendToBottom;228const modLine = move.modified.endLineNumberExclusive + extendToBottom;229if (origLine > originalLines.length || modLine > modifiedLines.length) {230break;231}232if (modifiedSet.contains(modLine) || originalSet.contains(origLine)) {233break;234}235if (!areLinesSimilar(originalLines[origLine - 1], modifiedLines[modLine - 1], timeout)) {236break;237}238}239240if (extendToBottom > 0) {241originalSet.addRange(new LineRange(move.original.endLineNumberExclusive, move.original.endLineNumberExclusive + extendToBottom));242modifiedSet.addRange(new LineRange(move.modified.endLineNumberExclusive, move.modified.endLineNumberExclusive + extendToBottom));243}244245if (extendToTop > 0 || extendToBottom > 0) {246moves[i] = new LineRangeMapping(247new LineRange(move.original.startLineNumber - extendToTop, move.original.endLineNumberExclusive + extendToBottom),248new LineRange(move.modified.startLineNumber - extendToTop, move.modified.endLineNumberExclusive + extendToBottom),249);250}251}252253return moves;254}255256function areLinesSimilar(line1: string, line2: string, timeout: ITimeout): boolean {257if (line1.trim() === line2.trim()) { return true; }258if (line1.length > 300 && line2.length > 300) { return false; }259260const myersDiffingAlgorithm = new MyersDiffAlgorithm();261const result = myersDiffingAlgorithm.compute(262new LinesSliceCharSequence([line1], new Range(1, 1, 1, line1.length), false),263new LinesSliceCharSequence([line2], new Range(1, 1, 1, line2.length), false),264timeout265);266let commonNonSpaceCharCount = 0;267const inverted = SequenceDiff.invert(result.diffs, line1.length);268for (const seq of inverted) {269seq.seq1Range.forEach(idx => {270if (!isSpace(line1.charCodeAt(idx))) {271commonNonSpaceCharCount++;272}273});274}275276function countNonWsChars(str: string): number {277let count = 0;278for (let i = 0; i < line1.length; i++) {279if (!isSpace(str.charCodeAt(i))) {280count++;281}282}283return count;284}285286const longerLineLength = countNonWsChars(line1.length > line2.length ? line1 : line2);287const r = commonNonSpaceCharCount / longerLineLength > 0.6 && longerLineLength > 10;288return r;289}290291function joinCloseConsecutiveMoves(moves: LineRangeMapping[]): LineRangeMapping[] {292if (moves.length === 0) {293return moves;294}295296moves.sort(compareBy(m => m.original.startLineNumber, numberComparator));297298const result = [moves[0]];299for (let i = 1; i < moves.length; i++) {300const last = result[result.length - 1];301const current = moves[i];302303const originalDist = current.original.startLineNumber - last.original.endLineNumberExclusive;304const modifiedDist = current.modified.startLineNumber - last.modified.endLineNumberExclusive;305const currentMoveAfterLast = originalDist >= 0 && modifiedDist >= 0;306307if (currentMoveAfterLast && originalDist + modifiedDist <= 2) {308result[result.length - 1] = last.join(current);309continue;310}311312result.push(current);313}314return result;315}316317function removeMovesInSameDiff(changes: DetailedLineRangeMapping[], moves: LineRangeMapping[]) {318const changesMonotonous = new MonotonousArray(changes);319moves = moves.filter(m => {320const diffBeforeEndOfMoveOriginal = changesMonotonous.findLastMonotonous(c => c.original.startLineNumber < m.original.endLineNumberExclusive)321|| new LineRangeMapping(new LineRange(1, 1), new LineRange(1, 1));322const diffBeforeEndOfMoveModified = findLastMonotonous(changes, c => c.modified.startLineNumber < m.modified.endLineNumberExclusive);323324const differentDiffs = diffBeforeEndOfMoveOriginal !== diffBeforeEndOfMoveModified;325return differentDiffs;326});327return moves;328}329330331