UNPKG

monaco-editor-core

Version:

A browser based code editor

248 lines (247 loc) • 14.5 kB
/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { equals, groupAdjacentBy } from '../../../../base/common/arrays.js'; import { assertFn, checkAdjacentItems } from '../../../../base/common/assert.js'; import { LineRange } from '../../core/lineRange.js'; import { OffsetRange } from '../../core/offsetRange.js'; import { Range } from '../../core/range.js'; import { DateTimeout, InfiniteTimeout, SequenceDiff } from './algorithms/diffAlgorithm.js'; import { DynamicProgrammingDiffing } from './algorithms/dynamicProgrammingDiffing.js'; import { MyersDiffAlgorithm } from './algorithms/myersDiffAlgorithm.js'; import { computeMovedLines } from './computeMovedLines.js'; import { extendDiffsToEntireWordIfAppropriate, optimizeSequenceDiffs, removeShortMatches, removeVeryShortMatchingLinesBetweenDiffs, removeVeryShortMatchingTextBetweenLongDiffs } from './heuristicSequenceOptimizations.js'; import { LineSequence } from './lineSequence.js'; import { LinesSliceCharSequence } from './linesSliceCharSequence.js'; import { LinesDiff, MovedText } from '../linesDiffComputer.js'; import { DetailedLineRangeMapping, LineRangeMapping, RangeMapping } from '../rangeMapping.js'; export class DefaultLinesDiffComputer { constructor() { this.dynamicProgrammingDiffing = new DynamicProgrammingDiffing(); this.myersDiffingAlgorithm = new MyersDiffAlgorithm(); } computeDiff(originalLines, modifiedLines, options) { if (originalLines.length <= 1 && equals(originalLines, modifiedLines, (a, b) => a === b)) { return new LinesDiff([], [], false); } if (originalLines.length === 1 && originalLines[0].length === 0 || modifiedLines.length === 1 && modifiedLines[0].length === 0) { return new LinesDiff([ new DetailedLineRangeMapping(new LineRange(1, originalLines.length + 1), new LineRange(1, modifiedLines.length + 1), [ new RangeMapping(new Range(1, 1, originalLines.length, originalLines[originalLines.length - 1].length + 1), new Range(1, 1, modifiedLines.length, modifiedLines[modifiedLines.length - 1].length + 1)) ]) ], [], false); } const timeout = options.maxComputationTimeMs === 0 ? InfiniteTimeout.instance : new DateTimeout(options.maxComputationTimeMs); const considerWhitespaceChanges = !options.ignoreTrimWhitespace; const perfectHashes = new Map(); function getOrCreateHash(text) { let hash = perfectHashes.get(text); if (hash === undefined) { hash = perfectHashes.size; perfectHashes.set(text, hash); } return hash; } const originalLinesHashes = originalLines.map((l) => getOrCreateHash(l.trim())); const modifiedLinesHashes = modifiedLines.map((l) => getOrCreateHash(l.trim())); const sequence1 = new LineSequence(originalLinesHashes, originalLines); const sequence2 = new LineSequence(modifiedLinesHashes, modifiedLines); const lineAlignmentResult = (() => { if (sequence1.length + sequence2.length < 1700) { // Use the improved algorithm for small files return this.dynamicProgrammingDiffing.compute(sequence1, sequence2, timeout, (offset1, offset2) => originalLines[offset1] === modifiedLines[offset2] ? modifiedLines[offset2].length === 0 ? 0.1 : 1 + Math.log(1 + modifiedLines[offset2].length) : 0.99); } return this.myersDiffingAlgorithm.compute(sequence1, sequence2, timeout); })(); let lineAlignments = lineAlignmentResult.diffs; let hitTimeout = lineAlignmentResult.hitTimeout; lineAlignments = optimizeSequenceDiffs(sequence1, sequence2, lineAlignments); lineAlignments = removeVeryShortMatchingLinesBetweenDiffs(sequence1, sequence2, lineAlignments); const alignments = []; const scanForWhitespaceChanges = (equalLinesCount) => { if (!considerWhitespaceChanges) { return; } for (let i = 0; i < equalLinesCount; i++) { const seq1Offset = seq1LastStart + i; const seq2Offset = seq2LastStart + i; if (originalLines[seq1Offset] !== modifiedLines[seq2Offset]) { // This is because of whitespace changes, diff these lines const characterDiffs = this.refineDiff(originalLines, modifiedLines, new SequenceDiff(new OffsetRange(seq1Offset, seq1Offset + 1), new OffsetRange(seq2Offset, seq2Offset + 1)), timeout, considerWhitespaceChanges); for (const a of characterDiffs.mappings) { alignments.push(a); } if (characterDiffs.hitTimeout) { hitTimeout = true; } } } }; let seq1LastStart = 0; let seq2LastStart = 0; for (const diff of lineAlignments) { assertFn(() => diff.seq1Range.start - seq1LastStart === diff.seq2Range.start - seq2LastStart); const equalLinesCount = diff.seq1Range.start - seq1LastStart; scanForWhitespaceChanges(equalLinesCount); seq1LastStart = diff.seq1Range.endExclusive; seq2LastStart = diff.seq2Range.endExclusive; const characterDiffs = this.refineDiff(originalLines, modifiedLines, diff, timeout, considerWhitespaceChanges); if (characterDiffs.hitTimeout) { hitTimeout = true; } for (const a of characterDiffs.mappings) { alignments.push(a); } } scanForWhitespaceChanges(originalLines.length - seq1LastStart); const changes = lineRangeMappingFromRangeMappings(alignments, originalLines, modifiedLines); let moves = []; if (options.computeMoves) { moves = this.computeMoves(changes, originalLines, modifiedLines, originalLinesHashes, modifiedLinesHashes, timeout, considerWhitespaceChanges); } // Make sure all ranges are valid assertFn(() => { function validatePosition(pos, lines) { if (pos.lineNumber < 1 || pos.lineNumber > lines.length) { return false; } const line = lines[pos.lineNumber - 1]; if (pos.column < 1 || pos.column > line.length + 1) { return false; } return true; } function validateRange(range, lines) { if (range.startLineNumber < 1 || range.startLineNumber > lines.length + 1) { return false; } if (range.endLineNumberExclusive < 1 || range.endLineNumberExclusive > lines.length + 1) { return false; } return true; } for (const c of changes) { if (!c.innerChanges) { return false; } for (const ic of c.innerChanges) { const valid = validatePosition(ic.modifiedRange.getStartPosition(), modifiedLines) && validatePosition(ic.modifiedRange.getEndPosition(), modifiedLines) && validatePosition(ic.originalRange.getStartPosition(), originalLines) && validatePosition(ic.originalRange.getEndPosition(), originalLines); if (!valid) { return false; } } if (!validateRange(c.modified, modifiedLines) || !validateRange(c.original, originalLines)) { return false; } } return true; }); return new LinesDiff(changes, moves, hitTimeout); } computeMoves(changes, originalLines, modifiedLines, hashedOriginalLines, hashedModifiedLines, timeout, considerWhitespaceChanges) { const moves = computeMovedLines(changes, originalLines, modifiedLines, hashedOriginalLines, hashedModifiedLines, timeout); const movesWithDiffs = moves.map(m => { const moveChanges = this.refineDiff(originalLines, modifiedLines, new SequenceDiff(m.original.toOffsetRange(), m.modified.toOffsetRange()), timeout, considerWhitespaceChanges); const mappings = lineRangeMappingFromRangeMappings(moveChanges.mappings, originalLines, modifiedLines, true); return new MovedText(m, mappings); }); return movesWithDiffs; } refineDiff(originalLines, modifiedLines, diff, timeout, considerWhitespaceChanges) { const lineRangeMapping = toLineRangeMapping(diff); const rangeMapping = lineRangeMapping.toRangeMapping2(originalLines, modifiedLines); const slice1 = new LinesSliceCharSequence(originalLines, rangeMapping.originalRange, considerWhitespaceChanges); const slice2 = new LinesSliceCharSequence(modifiedLines, rangeMapping.modifiedRange, considerWhitespaceChanges); const diffResult = slice1.length + slice2.length < 500 ? this.dynamicProgrammingDiffing.compute(slice1, slice2, timeout) : this.myersDiffingAlgorithm.compute(slice1, slice2, timeout); const check = false; let diffs = diffResult.diffs; if (check) { SequenceDiff.assertSorted(diffs); } diffs = optimizeSequenceDiffs(slice1, slice2, diffs); if (check) { SequenceDiff.assertSorted(diffs); } diffs = extendDiffsToEntireWordIfAppropriate(slice1, slice2, diffs); if (check) { SequenceDiff.assertSorted(diffs); } diffs = removeShortMatches(slice1, slice2, diffs); if (check) { SequenceDiff.assertSorted(diffs); } diffs = removeVeryShortMatchingTextBetweenLongDiffs(slice1, slice2, diffs); if (check) { SequenceDiff.assertSorted(diffs); } const result = diffs.map((d) => new RangeMapping(slice1.translateRange(d.seq1Range), slice2.translateRange(d.seq2Range))); if (check) { RangeMapping.assertSorted(result); } // Assert: result applied on original should be the same as diff applied to original return { mappings: result, hitTimeout: diffResult.hitTimeout, }; } } export function lineRangeMappingFromRangeMappings(alignments, originalLines, modifiedLines, dontAssertStartLine = false) { const changes = []; for (const g of groupAdjacentBy(alignments.map(a => getLineRangeMapping(a, originalLines, modifiedLines)), (a1, a2) => a1.original.overlapOrTouch(a2.original) || a1.modified.overlapOrTouch(a2.modified))) { const first = g[0]; const last = g[g.length - 1]; changes.push(new DetailedLineRangeMapping(first.original.join(last.original), first.modified.join(last.modified), g.map(a => a.innerChanges[0]))); } assertFn(() => { if (!dontAssertStartLine && changes.length > 0) { if (changes[0].modified.startLineNumber !== changes[0].original.startLineNumber) { return false; } if (modifiedLines.length - changes[changes.length - 1].modified.endLineNumberExclusive !== originalLines.length - changes[changes.length - 1].original.endLineNumberExclusive) { return false; } } return checkAdjacentItems(changes, (m1, m2) => m2.original.startLineNumber - m1.original.endLineNumberExclusive === m2.modified.startLineNumber - m1.modified.endLineNumberExclusive && // There has to be an unchanged line in between (otherwise both diffs should have been joined) m1.original.endLineNumberExclusive < m2.original.startLineNumber && m1.modified.endLineNumberExclusive < m2.modified.startLineNumber); }); return changes; } export function getLineRangeMapping(rangeMapping, originalLines, modifiedLines) { let lineStartDelta = 0; let lineEndDelta = 0; // rangeMapping describes the edit that replaces `rangeMapping.originalRange` with `newText := getText(modifiedLines, rangeMapping.modifiedRange)`. // original: ]xxx \n <- this line is not modified // modified: ]xx \n if (rangeMapping.modifiedRange.endColumn === 1 && rangeMapping.originalRange.endColumn === 1 && rangeMapping.originalRange.startLineNumber + lineStartDelta <= rangeMapping.originalRange.endLineNumber && rangeMapping.modifiedRange.startLineNumber + lineStartDelta <= rangeMapping.modifiedRange.endLineNumber) { // We can only do this if the range is not empty yet lineEndDelta = -1; } // original: xxx[ \n <- this line is not modified // modified: xxx[ \n if (rangeMapping.modifiedRange.startColumn - 1 >= modifiedLines[rangeMapping.modifiedRange.startLineNumber - 1].length && rangeMapping.originalRange.startColumn - 1 >= originalLines[rangeMapping.originalRange.startLineNumber - 1].length && rangeMapping.originalRange.startLineNumber <= rangeMapping.originalRange.endLineNumber + lineEndDelta && rangeMapping.modifiedRange.startLineNumber <= rangeMapping.modifiedRange.endLineNumber + lineEndDelta) { // We can only do this if the range is not empty yet lineStartDelta = 1; } const originalLineRange = new LineRange(rangeMapping.originalRange.startLineNumber + lineStartDelta, rangeMapping.originalRange.endLineNumber + 1 + lineEndDelta); const modifiedLineRange = new LineRange(rangeMapping.modifiedRange.startLineNumber + lineStartDelta, rangeMapping.modifiedRange.endLineNumber + 1 + lineEndDelta); return new DetailedLineRangeMapping(originalLineRange, modifiedLineRange, [rangeMapping]); } function toLineRangeMapping(sequenceDiff) { return new LineRangeMapping(new LineRange(sequenceDiff.seq1Range.start + 1, sequenceDiff.seq1Range.endExclusive + 1), new LineRange(sequenceDiff.seq2Range.start + 1, sequenceDiff.seq2Range.endExclusive + 1)); }