Path: blob/main/src/vs/editor/common/diff/defaultLinesDiffComputer/defaultLinesDiffComputer.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 { equals } from '../../../../base/common/arrays.js';6import { assertFn } from '../../../../base/common/assert.js';7import { LineRange } from '../../core/ranges/lineRange.js';8import { OffsetRange } from '../../core/ranges/offsetRange.js';9import { Position } from '../../core/position.js';10import { Range } from '../../core/range.js';11import { ArrayText } from '../../core/text/abstractText.js';12import { ILinesDiffComputer, ILinesDiffComputerOptions, LinesDiff, MovedText } from '../linesDiffComputer.js';13import { DetailedLineRangeMapping, LineRangeMapping, lineRangeMappingFromRangeMappings, RangeMapping } from '../rangeMapping.js';14import { DateTimeout, InfiniteTimeout, ITimeout, SequenceDiff } from './algorithms/diffAlgorithm.js';15import { DynamicProgrammingDiffing } from './algorithms/dynamicProgrammingDiffing.js';16import { MyersDiffAlgorithm } from './algorithms/myersDiffAlgorithm.js';17import { computeMovedLines } from './computeMovedLines.js';18import { extendDiffsToEntireWordIfAppropriate, optimizeSequenceDiffs, removeShortMatches, removeVeryShortMatchingLinesBetweenDiffs, removeVeryShortMatchingTextBetweenLongDiffs } from './heuristicSequenceOptimizations.js';19import { LineSequence } from './lineSequence.js';20import { LinesSliceCharSequence } from './linesSliceCharSequence.js';2122export class DefaultLinesDiffComputer implements ILinesDiffComputer {23private readonly dynamicProgrammingDiffing = new DynamicProgrammingDiffing();24private readonly myersDiffingAlgorithm = new MyersDiffAlgorithm();2526computeDiff(originalLines: string[], modifiedLines: string[], options: ILinesDiffComputerOptions): LinesDiff {27if (originalLines.length <= 1 && equals(originalLines, modifiedLines, (a, b) => a === b)) {28return new LinesDiff([], [], false);29}3031if (originalLines.length === 1 && originalLines[0].length === 0 || modifiedLines.length === 1 && modifiedLines[0].length === 0) {32return new LinesDiff([33new DetailedLineRangeMapping(34new LineRange(1, originalLines.length + 1),35new LineRange(1, modifiedLines.length + 1),36[37new RangeMapping(38new Range(1, 1, originalLines.length, originalLines[originalLines.length - 1].length + 1),39new Range(1, 1, modifiedLines.length, modifiedLines[modifiedLines.length - 1].length + 1),40)41]42)43], [], false);44}4546const timeout = options.maxComputationTimeMs === 0 ? InfiniteTimeout.instance : new DateTimeout(options.maxComputationTimeMs);47const considerWhitespaceChanges = !options.ignoreTrimWhitespace;4849const perfectHashes = new Map<string, number>();50function getOrCreateHash(text: string): number {51let hash = perfectHashes.get(text);52if (hash === undefined) {53hash = perfectHashes.size;54perfectHashes.set(text, hash);55}56return hash;57}5859const originalLinesHashes = originalLines.map((l) => getOrCreateHash(l.trim()));60const modifiedLinesHashes = modifiedLines.map((l) => getOrCreateHash(l.trim()));6162const sequence1 = new LineSequence(originalLinesHashes, originalLines);63const sequence2 = new LineSequence(modifiedLinesHashes, modifiedLines);6465const lineAlignmentResult = (() => {66if (sequence1.length + sequence2.length < 1700) {67// Use the improved algorithm for small files68return this.dynamicProgrammingDiffing.compute(69sequence1,70sequence2,71timeout,72(offset1, offset2) =>73originalLines[offset1] === modifiedLines[offset2]74? modifiedLines[offset2].length === 075? 0.176: 1 + Math.log(1 + modifiedLines[offset2].length)77: 0.9978);79}8081return this.myersDiffingAlgorithm.compute(82sequence1,83sequence2,84timeout85);86})();8788let lineAlignments = lineAlignmentResult.diffs;89let hitTimeout = lineAlignmentResult.hitTimeout;90lineAlignments = optimizeSequenceDiffs(sequence1, sequence2, lineAlignments);91lineAlignments = removeVeryShortMatchingLinesBetweenDiffs(sequence1, sequence2, lineAlignments);9293const alignments: RangeMapping[] = [];9495const scanForWhitespaceChanges = (equalLinesCount: number) => {96if (!considerWhitespaceChanges) {97return;98}99100for (let i = 0; i < equalLinesCount; i++) {101const seq1Offset = seq1LastStart + i;102const seq2Offset = seq2LastStart + i;103if (originalLines[seq1Offset] !== modifiedLines[seq2Offset]) {104// This is because of whitespace changes, diff these lines105const characterDiffs = this.refineDiff(originalLines, modifiedLines, new SequenceDiff(106new OffsetRange(seq1Offset, seq1Offset + 1),107new OffsetRange(seq2Offset, seq2Offset + 1),108), timeout, considerWhitespaceChanges, options);109for (const a of characterDiffs.mappings) {110alignments.push(a);111}112if (characterDiffs.hitTimeout) {113hitTimeout = true;114}115}116}117};118119let seq1LastStart = 0;120let seq2LastStart = 0;121122for (const diff of lineAlignments) {123assertFn(() => diff.seq1Range.start - seq1LastStart === diff.seq2Range.start - seq2LastStart);124125const equalLinesCount = diff.seq1Range.start - seq1LastStart;126127scanForWhitespaceChanges(equalLinesCount);128129seq1LastStart = diff.seq1Range.endExclusive;130seq2LastStart = diff.seq2Range.endExclusive;131132const characterDiffs = this.refineDiff(originalLines, modifiedLines, diff, timeout, considerWhitespaceChanges, options);133if (characterDiffs.hitTimeout) {134hitTimeout = true;135}136for (const a of characterDiffs.mappings) {137alignments.push(a);138}139}140141scanForWhitespaceChanges(originalLines.length - seq1LastStart);142143const original = new ArrayText(originalLines);144const modified = new ArrayText(modifiedLines);145146const changes = lineRangeMappingFromRangeMappings(alignments, original, modified);147148let moves: MovedText[] = [];149if (options.computeMoves) {150moves = this.computeMoves(changes, originalLines, modifiedLines, originalLinesHashes, modifiedLinesHashes, timeout, considerWhitespaceChanges, options);151}152153// Make sure all ranges are valid154assertFn(() => {155function validatePosition(pos: Position, lines: string[]): boolean {156if (pos.lineNumber < 1 || pos.lineNumber > lines.length) { return false; }157const line = lines[pos.lineNumber - 1];158if (pos.column < 1 || pos.column > line.length + 1) { return false; }159return true;160}161162function validateRange(range: LineRange, lines: string[]): boolean {163if (range.startLineNumber < 1 || range.startLineNumber > lines.length + 1) { return false; }164if (range.endLineNumberExclusive < 1 || range.endLineNumberExclusive > lines.length + 1) { return false; }165return true;166}167168for (const c of changes) {169if (!c.innerChanges) { return false; }170for (const ic of c.innerChanges) {171const valid = validatePosition(ic.modifiedRange.getStartPosition(), modifiedLines) && validatePosition(ic.modifiedRange.getEndPosition(), modifiedLines) &&172validatePosition(ic.originalRange.getStartPosition(), originalLines) && validatePosition(ic.originalRange.getEndPosition(), originalLines);173if (!valid) {174return false;175}176}177if (!validateRange(c.modified, modifiedLines) || !validateRange(c.original, originalLines)) {178return false;179}180}181return true;182});183184return new LinesDiff(changes, moves, hitTimeout);185}186187private computeMoves(188changes: DetailedLineRangeMapping[],189originalLines: string[],190modifiedLines: string[],191hashedOriginalLines: number[],192hashedModifiedLines: number[],193timeout: ITimeout,194considerWhitespaceChanges: boolean,195options: ILinesDiffComputerOptions,196): MovedText[] {197const moves = computeMovedLines(198changes,199originalLines,200modifiedLines,201hashedOriginalLines,202hashedModifiedLines,203timeout,204);205const movesWithDiffs = moves.map(m => {206const moveChanges = this.refineDiff(originalLines, modifiedLines, new SequenceDiff(207m.original.toOffsetRange(),208m.modified.toOffsetRange(),209), timeout, considerWhitespaceChanges, options);210const mappings = lineRangeMappingFromRangeMappings(moveChanges.mappings, new ArrayText(originalLines), new ArrayText(modifiedLines), true);211return new MovedText(m, mappings);212});213return movesWithDiffs;214}215216private refineDiff(originalLines: string[], modifiedLines: string[], diff: SequenceDiff, timeout: ITimeout, considerWhitespaceChanges: boolean, options: ILinesDiffComputerOptions): { mappings: RangeMapping[]; hitTimeout: boolean } {217const lineRangeMapping = toLineRangeMapping(diff);218const rangeMapping = lineRangeMapping.toRangeMapping2(originalLines, modifiedLines);219220const slice1 = new LinesSliceCharSequence(originalLines, rangeMapping.originalRange, considerWhitespaceChanges);221const slice2 = new LinesSliceCharSequence(modifiedLines, rangeMapping.modifiedRange, considerWhitespaceChanges);222223const diffResult = slice1.length + slice2.length < 500224? this.dynamicProgrammingDiffing.compute(slice1, slice2, timeout)225: this.myersDiffingAlgorithm.compute(slice1, slice2, timeout);226227const check = false;228229let diffs = diffResult.diffs;230if (check) { SequenceDiff.assertSorted(diffs); }231diffs = optimizeSequenceDiffs(slice1, slice2, diffs);232if (check) { SequenceDiff.assertSorted(diffs); }233diffs = extendDiffsToEntireWordIfAppropriate(slice1, slice2, diffs, (seq, idx) => seq.findWordContaining(idx));234if (check) { SequenceDiff.assertSorted(diffs); }235236if (options.extendToSubwords) {237diffs = extendDiffsToEntireWordIfAppropriate(slice1, slice2, diffs, (seq, idx) => seq.findSubWordContaining(idx), true);238if (check) { SequenceDiff.assertSorted(diffs); }239}240241diffs = removeShortMatches(slice1, slice2, diffs);242if (check) { SequenceDiff.assertSorted(diffs); }243diffs = removeVeryShortMatchingTextBetweenLongDiffs(slice1, slice2, diffs);244if (check) { SequenceDiff.assertSorted(diffs); }245246const result = diffs.map(247(d) =>248new RangeMapping(249slice1.translateRange(d.seq1Range),250slice2.translateRange(d.seq2Range)251)252);253254if (check) { RangeMapping.assertSorted(result); }255256// Assert: result applied on original should be the same as diff applied to original257258return {259mappings: result,260hitTimeout: diffResult.hitTimeout,261};262}263}264265function toLineRangeMapping(sequenceDiff: SequenceDiff) {266return new LineRangeMapping(267new LineRange(sequenceDiff.seq1Range.start + 1, sequenceDiff.seq1Range.endExclusive + 1),268new LineRange(sequenceDiff.seq2Range.start + 1, sequenceDiff.seq2Range.endExclusive + 1),269);270}271272273