Change:
return new LinesDiff(changes, moves, hitTimeout); } <ins> private computeMoves(changes: LineRangeMapping[], originalLines: string[], modifiedLines: string[], hashedOriginalLines: number[], hashedModifiedLines: number[], timeout: ITimeout, considerWhitespaceChanges: boolean): MovedText[] { const moves: MovedText[] = []; const deletions = changes .filter(c => c.modifiedRange.isEmpty && c.originalRange.length >= 3) .map(d => new LineRangeFragment(d.originalRange, originalLines, d)); const insertions = new Set(changes .filter(c => c.originalRange.isEmpty && c.modifiedRange.length >= 3) .map(d => new LineRangeFragment(d.modifiedRange, modifiedLines, d))); const excludedChanges = new Set<LineRangeMapping>(); for (const deletion of deletions) { let highestSimilarity = -1; let best: LineRangeFragment | undefined; for (const insertion of insertions) { const similarity = deletion.computeSimilarity(insertion); if (similarity > highestSimilarity) { highestSimilarity = similarity; best = insertion; } } if (highestSimilarity > 0.90 && best) { const moveChanges = this.refineDiff(originalLines, modifiedLines, new SequenceDiff( new OffsetRange(deletion.range.startLineNumber - 1, deletion.range.endLineNumberExclusive - 1), new OffsetRange(best.range.startLineNumber - 1, best.range.endLineNumberExclusive - 1) ), timeout, considerWhitespaceChanges); const mappings = lineRangeMappingFromRangeMappings(moveChanges.mappings, originalLines, modifiedLines, true); insertions.delete(best); moves.push(new MovedText(new SimpleLineRangeMapping(deletion.range, best.range), mappings)); excludedChanges.add(deletion.source); excludedChanges.add(best.source); } } const original3LineHashes = new SetMap<string, { range: LineRange }>(); for (const change of changes) { if (excludedChanges.has(change)) { continue; } for (let i = change.originalRange.startLineNumber; i < change.originalRange.endLineNumberExclusive - 2; i++) { const key = `${hashedOriginalLines[i - 1]}:${hashedOriginalLines[i + 1 - 1]}:${hashedOriginalLines[i + 2 - 1]}`; original3LineHashes.add(key, { range: new LineRange(i, i + 3) }); } } interface PossibleMapping { modifiedLineRange: LineRange; originalLineRange: LineRange; } const possibleMappings: PossibleMapping[] = []; changes.sort(compareBy(c => c.modifiedRange.startLineNumber, numberComparator)); for (const change of changes) { if (excludedChanges.has(change)) { continue; } let lastMappings: PossibleMapping[] = []; for (let i = change.modifiedRange.startLineNumber; i < change.modifiedRange.endLineNumberExclusive - 2; i++) { const key = `${hashedModifiedLines[i - 1]}:${hashedModifiedLines[i + 1 - 1]}:${hashedModifiedLines[i + 2 - 1]}`; //const isWeakKey = (originalLines[i].trim().length + originalLines[i + 1].trim().length + originalLines[i + 2].trim().length < 20); const currentModifiedRange = new LineRange(i, i + 3); const nextMappings: PossibleMapping[] = []; original3LineHashes.forEach(key, ({ range }) => { for (const lastMapping of lastMappings) { // does this match extend some last match? if (lastMapping.originalLineRange.endLineNumberExclusive + 1 === range.endLineNumberExclusive && lastMapping.modifiedLineRange.endLineNumberExclusive + 1 === currentModifiedRange.endLineNumberExclusive) { lastMapping.originalLineRange = new LineRange(lastMapping.originalLineRange.startLineNumber, range.endLineNumberExclusive); lastMapping.modifiedLineRange = new LineRange(lastMapping.modifiedLineRange.startLineNumber, currentModifiedRange.endLineNumberExclusive); nextMappings.push(lastMapping); return; } } const mapping: PossibleMapping = { modifiedLineRange: currentModifiedRange, originalLineRange: range, }; possibleMappings.push(mapping); nextMappings.push(mapping); }); lastMappings = nextMappings; } } possibleMappings.sort(reverseOrder(compareBy(m => m.modifiedLineRange.length, numberComparator))); const modifiedSet = new LineRangeSet(); const originalSet = new LineRangeSet(); for (const mapping of possibleMappings) { const diffOrigToMod = mapping.modifiedLineRange.startLineNumber - mapping.originalLineRange.startLineNumber; const modifiedSections = modifiedSet.subtractFrom(mapping.modifiedLineRange); const originalTranslatedSections = originalSet.subtractFrom(mapping.originalLineRange).map(r => r.delta(diffOrigToMod)); const modifiedIntersectedSections = intersectRanges(modifiedSections, originalTranslatedSections); for (const s of modifiedIntersectedSections) { if (s.length < 3) { continue; } const modifiedLineRange = s; const originalLineRange = s.delta(-diffOrigToMod); const moveChanges = this.refineDiff(originalLines, modifiedLines, new SequenceDiff( new OffsetRange(originalLineRange.startLineNumber - 1, originalLineRange.endLineNumberExclusive - 1), new OffsetRange(modifiedLineRange.startLineNumber - 1, modifiedLineRange.endLineNumberExclusive - 1) ), timeout, considerWhitespaceChanges); const mappings = lineRangeMappingFromRangeMappings(moveChanges.mappings, originalLines, modifiedLines, true); moves.push(new MovedText(new SimpleLineRangeMapping(originalLineRange, modifiedLineRange), mappings)); modifiedSet.addRange(modifiedLineRange); originalSet.addRange(originalLineRange); } } return moves; } </ins> private refineDiff(originalLines: string[], modifiedLines: string[], diff: SequenceDiff, timeout: ITimeout, considerWhitespaceChanges: boolean): { mappings: RangeMapping[]; hitTimeout: boolean } { const slice1 = new LinesSliceCharSequence(originalLines, diff.seq1Range, considerWhitespaceChanges); const slice2 = new LinesSliceCharSequence(modifiedLines, diff.seq2Range, considerWhitespaceChanges);

Discussion:
When a code fragment of at least 3 lines of code is deleted somewhere in inserted somewhere else without modifications, this move should be detected. . (select latest -dev version to verify)