UNPKG

monaco-editor-core

Version:

A browser based code editor

466 lines (465 loc) • 24.9 kB
/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { LcsDiff } from '../../../base/common/diff/diff.js'; import { LinesDiff } from './linesDiffComputer.js'; import { RangeMapping, DetailedLineRangeMapping } from './rangeMapping.js'; import * as strings from '../../../base/common/strings.js'; import { Range } from '../core/range.js'; import { assertFn, checkAdjacentItems } from '../../../base/common/assert.js'; import { LineRange } from '../core/lineRange.js'; const MINIMUM_MATCHING_CHARACTER_LENGTH = 3; export class LegacyLinesDiffComputer { computeDiff(originalLines, modifiedLines, options) { const diffComputer = new DiffComputer(originalLines, modifiedLines, { maxComputationTime: options.maxComputationTimeMs, shouldIgnoreTrimWhitespace: options.ignoreTrimWhitespace, shouldComputeCharChanges: true, shouldMakePrettyDiff: true, shouldPostProcessCharChanges: true, }); const result = diffComputer.computeDiff(); const changes = []; let lastChange = null; for (const c of result.changes) { let originalRange; if (c.originalEndLineNumber === 0) { // Insertion originalRange = new LineRange(c.originalStartLineNumber + 1, c.originalStartLineNumber + 1); } else { originalRange = new LineRange(c.originalStartLineNumber, c.originalEndLineNumber + 1); } let modifiedRange; if (c.modifiedEndLineNumber === 0) { // Deletion modifiedRange = new LineRange(c.modifiedStartLineNumber + 1, c.modifiedStartLineNumber + 1); } else { modifiedRange = new LineRange(c.modifiedStartLineNumber, c.modifiedEndLineNumber + 1); } let change = new DetailedLineRangeMapping(originalRange, modifiedRange, c.charChanges?.map(c => new RangeMapping(new Range(c.originalStartLineNumber, c.originalStartColumn, c.originalEndLineNumber, c.originalEndColumn), new Range(c.modifiedStartLineNumber, c.modifiedStartColumn, c.modifiedEndLineNumber, c.modifiedEndColumn)))); if (lastChange) { if (lastChange.modified.endLineNumberExclusive === change.modified.startLineNumber || lastChange.original.endLineNumberExclusive === change.original.startLineNumber) { // join touching diffs. Probably moving diffs up/down in the algorithm causes touching diffs. change = new DetailedLineRangeMapping(lastChange.original.join(change.original), lastChange.modified.join(change.modified), lastChange.innerChanges && change.innerChanges ? lastChange.innerChanges.concat(change.innerChanges) : undefined); changes.pop(); } } changes.push(change); lastChange = change; } assertFn(() => { 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 new LinesDiff(changes, [], result.quitEarly); } } function computeDiff(originalSequence, modifiedSequence, continueProcessingPredicate, pretty) { const diffAlgo = new LcsDiff(originalSequence, modifiedSequence, continueProcessingPredicate); return diffAlgo.ComputeDiff(pretty); } class LineSequence { constructor(lines) { const startColumns = []; const endColumns = []; for (let i = 0, length = lines.length; i < length; i++) { startColumns[i] = getFirstNonBlankColumn(lines[i], 1); endColumns[i] = getLastNonBlankColumn(lines[i], 1); } this.lines = lines; this._startColumns = startColumns; this._endColumns = endColumns; } getElements() { const elements = []; for (let i = 0, len = this.lines.length; i < len; i++) { elements[i] = this.lines[i].substring(this._startColumns[i] - 1, this._endColumns[i] - 1); } return elements; } getStrictElement(index) { return this.lines[index]; } getStartLineNumber(i) { return i + 1; } getEndLineNumber(i) { return i + 1; } createCharSequence(shouldIgnoreTrimWhitespace, startIndex, endIndex) { const charCodes = []; const lineNumbers = []; const columns = []; let len = 0; for (let index = startIndex; index <= endIndex; index++) { const lineContent = this.lines[index]; const startColumn = (shouldIgnoreTrimWhitespace ? this._startColumns[index] : 1); const endColumn = (shouldIgnoreTrimWhitespace ? this._endColumns[index] : lineContent.length + 1); for (let col = startColumn; col < endColumn; col++) { charCodes[len] = lineContent.charCodeAt(col - 1); lineNumbers[len] = index + 1; columns[len] = col; len++; } if (!shouldIgnoreTrimWhitespace && index < endIndex) { // Add \n if trim whitespace is not ignored charCodes[len] = 10 /* CharCode.LineFeed */; lineNumbers[len] = index + 1; columns[len] = lineContent.length + 1; len++; } } return new CharSequence(charCodes, lineNumbers, columns); } } class CharSequence { constructor(charCodes, lineNumbers, columns) { this._charCodes = charCodes; this._lineNumbers = lineNumbers; this._columns = columns; } toString() { return ('[' + this._charCodes.map((s, idx) => (s === 10 /* CharCode.LineFeed */ ? '\\n' : String.fromCharCode(s)) + `-(${this._lineNumbers[idx]},${this._columns[idx]})`).join(', ') + ']'); } _assertIndex(index, arr) { if (index < 0 || index >= arr.length) { throw new Error(`Illegal index`); } } getElements() { return this._charCodes; } getStartLineNumber(i) { if (i > 0 && i === this._lineNumbers.length) { // the start line number of the element after the last element // is the end line number of the last element return this.getEndLineNumber(i - 1); } this._assertIndex(i, this._lineNumbers); return this._lineNumbers[i]; } getEndLineNumber(i) { if (i === -1) { // the end line number of the element before the first element // is the start line number of the first element return this.getStartLineNumber(i + 1); } this._assertIndex(i, this._lineNumbers); if (this._charCodes[i] === 10 /* CharCode.LineFeed */) { return this._lineNumbers[i] + 1; } return this._lineNumbers[i]; } getStartColumn(i) { if (i > 0 && i === this._columns.length) { // the start column of the element after the last element // is the end column of the last element return this.getEndColumn(i - 1); } this._assertIndex(i, this._columns); return this._columns[i]; } getEndColumn(i) { if (i === -1) { // the end column of the element before the first element // is the start column of the first element return this.getStartColumn(i + 1); } this._assertIndex(i, this._columns); if (this._charCodes[i] === 10 /* CharCode.LineFeed */) { return 1; } return this._columns[i] + 1; } } class CharChange { constructor(originalStartLineNumber, originalStartColumn, originalEndLineNumber, originalEndColumn, modifiedStartLineNumber, modifiedStartColumn, modifiedEndLineNumber, modifiedEndColumn) { this.originalStartLineNumber = originalStartLineNumber; this.originalStartColumn = originalStartColumn; this.originalEndLineNumber = originalEndLineNumber; this.originalEndColumn = originalEndColumn; this.modifiedStartLineNumber = modifiedStartLineNumber; this.modifiedStartColumn = modifiedStartColumn; this.modifiedEndLineNumber = modifiedEndLineNumber; this.modifiedEndColumn = modifiedEndColumn; } static createFromDiffChange(diffChange, originalCharSequence, modifiedCharSequence) { const originalStartLineNumber = originalCharSequence.getStartLineNumber(diffChange.originalStart); const originalStartColumn = originalCharSequence.getStartColumn(diffChange.originalStart); const originalEndLineNumber = originalCharSequence.getEndLineNumber(diffChange.originalStart + diffChange.originalLength - 1); const originalEndColumn = originalCharSequence.getEndColumn(diffChange.originalStart + diffChange.originalLength - 1); const modifiedStartLineNumber = modifiedCharSequence.getStartLineNumber(diffChange.modifiedStart); const modifiedStartColumn = modifiedCharSequence.getStartColumn(diffChange.modifiedStart); const modifiedEndLineNumber = modifiedCharSequence.getEndLineNumber(diffChange.modifiedStart + diffChange.modifiedLength - 1); const modifiedEndColumn = modifiedCharSequence.getEndColumn(diffChange.modifiedStart + diffChange.modifiedLength - 1); return new CharChange(originalStartLineNumber, originalStartColumn, originalEndLineNumber, originalEndColumn, modifiedStartLineNumber, modifiedStartColumn, modifiedEndLineNumber, modifiedEndColumn); } } function postProcessCharChanges(rawChanges) { if (rawChanges.length <= 1) { return rawChanges; } const result = [rawChanges[0]]; let prevChange = result[0]; for (let i = 1, len = rawChanges.length; i < len; i++) { const currChange = rawChanges[i]; const originalMatchingLength = currChange.originalStart - (prevChange.originalStart + prevChange.originalLength); const modifiedMatchingLength = currChange.modifiedStart - (prevChange.modifiedStart + prevChange.modifiedLength); // Both of the above should be equal, but the continueProcessingPredicate may prevent this from being true const matchingLength = Math.min(originalMatchingLength, modifiedMatchingLength); if (matchingLength < MINIMUM_MATCHING_CHARACTER_LENGTH) { // Merge the current change into the previous one prevChange.originalLength = (currChange.originalStart + currChange.originalLength) - prevChange.originalStart; prevChange.modifiedLength = (currChange.modifiedStart + currChange.modifiedLength) - prevChange.modifiedStart; } else { // Add the current change result.push(currChange); prevChange = currChange; } } return result; } class LineChange { constructor(originalStartLineNumber, originalEndLineNumber, modifiedStartLineNumber, modifiedEndLineNumber, charChanges) { this.originalStartLineNumber = originalStartLineNumber; this.originalEndLineNumber = originalEndLineNumber; this.modifiedStartLineNumber = modifiedStartLineNumber; this.modifiedEndLineNumber = modifiedEndLineNumber; this.charChanges = charChanges; } static createFromDiffResult(shouldIgnoreTrimWhitespace, diffChange, originalLineSequence, modifiedLineSequence, continueCharDiff, shouldComputeCharChanges, shouldPostProcessCharChanges) { let originalStartLineNumber; let originalEndLineNumber; let modifiedStartLineNumber; let modifiedEndLineNumber; let charChanges = undefined; if (diffChange.originalLength === 0) { originalStartLineNumber = originalLineSequence.getStartLineNumber(diffChange.originalStart) - 1; originalEndLineNumber = 0; } else { originalStartLineNumber = originalLineSequence.getStartLineNumber(diffChange.originalStart); originalEndLineNumber = originalLineSequence.getEndLineNumber(diffChange.originalStart + diffChange.originalLength - 1); } if (diffChange.modifiedLength === 0) { modifiedStartLineNumber = modifiedLineSequence.getStartLineNumber(diffChange.modifiedStart) - 1; modifiedEndLineNumber = 0; } else { modifiedStartLineNumber = modifiedLineSequence.getStartLineNumber(diffChange.modifiedStart); modifiedEndLineNumber = modifiedLineSequence.getEndLineNumber(diffChange.modifiedStart + diffChange.modifiedLength - 1); } if (shouldComputeCharChanges && diffChange.originalLength > 0 && diffChange.originalLength < 20 && diffChange.modifiedLength > 0 && diffChange.modifiedLength < 20 && continueCharDiff()) { // Compute character changes for diff chunks of at most 20 lines... const originalCharSequence = originalLineSequence.createCharSequence(shouldIgnoreTrimWhitespace, diffChange.originalStart, diffChange.originalStart + diffChange.originalLength - 1); const modifiedCharSequence = modifiedLineSequence.createCharSequence(shouldIgnoreTrimWhitespace, diffChange.modifiedStart, diffChange.modifiedStart + diffChange.modifiedLength - 1); if (originalCharSequence.getElements().length > 0 && modifiedCharSequence.getElements().length > 0) { let rawChanges = computeDiff(originalCharSequence, modifiedCharSequence, continueCharDiff, true).changes; if (shouldPostProcessCharChanges) { rawChanges = postProcessCharChanges(rawChanges); } charChanges = []; for (let i = 0, length = rawChanges.length; i < length; i++) { charChanges.push(CharChange.createFromDiffChange(rawChanges[i], originalCharSequence, modifiedCharSequence)); } } } return new LineChange(originalStartLineNumber, originalEndLineNumber, modifiedStartLineNumber, modifiedEndLineNumber, charChanges); } } export class DiffComputer { constructor(originalLines, modifiedLines, opts) { this.shouldComputeCharChanges = opts.shouldComputeCharChanges; this.shouldPostProcessCharChanges = opts.shouldPostProcessCharChanges; this.shouldIgnoreTrimWhitespace = opts.shouldIgnoreTrimWhitespace; this.shouldMakePrettyDiff = opts.shouldMakePrettyDiff; this.originalLines = originalLines; this.modifiedLines = modifiedLines; this.original = new LineSequence(originalLines); this.modified = new LineSequence(modifiedLines); this.continueLineDiff = createContinueProcessingPredicate(opts.maxComputationTime); this.continueCharDiff = createContinueProcessingPredicate(opts.maxComputationTime === 0 ? 0 : Math.min(opts.maxComputationTime, 5000)); // never run after 5s for character changes... } computeDiff() { if (this.original.lines.length === 1 && this.original.lines[0].length === 0) { // empty original => fast path if (this.modified.lines.length === 1 && this.modified.lines[0].length === 0) { return { quitEarly: false, changes: [] }; } return { quitEarly: false, changes: [{ originalStartLineNumber: 1, originalEndLineNumber: 1, modifiedStartLineNumber: 1, modifiedEndLineNumber: this.modified.lines.length, charChanges: undefined }] }; } if (this.modified.lines.length === 1 && this.modified.lines[0].length === 0) { // empty modified => fast path return { quitEarly: false, changes: [{ originalStartLineNumber: 1, originalEndLineNumber: this.original.lines.length, modifiedStartLineNumber: 1, modifiedEndLineNumber: 1, charChanges: undefined }] }; } const diffResult = computeDiff(this.original, this.modified, this.continueLineDiff, this.shouldMakePrettyDiff); const rawChanges = diffResult.changes; const quitEarly = diffResult.quitEarly; // The diff is always computed with ignoring trim whitespace // This ensures we get the prettiest diff if (this.shouldIgnoreTrimWhitespace) { const lineChanges = []; for (let i = 0, length = rawChanges.length; i < length; i++) { lineChanges.push(LineChange.createFromDiffResult(this.shouldIgnoreTrimWhitespace, rawChanges[i], this.original, this.modified, this.continueCharDiff, this.shouldComputeCharChanges, this.shouldPostProcessCharChanges)); } return { quitEarly: quitEarly, changes: lineChanges }; } // Need to post-process and introduce changes where the trim whitespace is different // Note that we are looping starting at -1 to also cover the lines before the first change const result = []; let originalLineIndex = 0; let modifiedLineIndex = 0; for (let i = -1 /* !!!! */, len = rawChanges.length; i < len; i++) { const nextChange = (i + 1 < len ? rawChanges[i + 1] : null); const originalStop = (nextChange ? nextChange.originalStart : this.originalLines.length); const modifiedStop = (nextChange ? nextChange.modifiedStart : this.modifiedLines.length); while (originalLineIndex < originalStop && modifiedLineIndex < modifiedStop) { const originalLine = this.originalLines[originalLineIndex]; const modifiedLine = this.modifiedLines[modifiedLineIndex]; if (originalLine !== modifiedLine) { // These lines differ only in trim whitespace // Check the leading whitespace { let originalStartColumn = getFirstNonBlankColumn(originalLine, 1); let modifiedStartColumn = getFirstNonBlankColumn(modifiedLine, 1); while (originalStartColumn > 1 && modifiedStartColumn > 1) { const originalChar = originalLine.charCodeAt(originalStartColumn - 2); const modifiedChar = modifiedLine.charCodeAt(modifiedStartColumn - 2); if (originalChar !== modifiedChar) { break; } originalStartColumn--; modifiedStartColumn--; } if (originalStartColumn > 1 || modifiedStartColumn > 1) { this._pushTrimWhitespaceCharChange(result, originalLineIndex + 1, 1, originalStartColumn, modifiedLineIndex + 1, 1, modifiedStartColumn); } } // Check the trailing whitespace { let originalEndColumn = getLastNonBlankColumn(originalLine, 1); let modifiedEndColumn = getLastNonBlankColumn(modifiedLine, 1); const originalMaxColumn = originalLine.length + 1; const modifiedMaxColumn = modifiedLine.length + 1; while (originalEndColumn < originalMaxColumn && modifiedEndColumn < modifiedMaxColumn) { const originalChar = originalLine.charCodeAt(originalEndColumn - 1); const modifiedChar = originalLine.charCodeAt(modifiedEndColumn - 1); if (originalChar !== modifiedChar) { break; } originalEndColumn++; modifiedEndColumn++; } if (originalEndColumn < originalMaxColumn || modifiedEndColumn < modifiedMaxColumn) { this._pushTrimWhitespaceCharChange(result, originalLineIndex + 1, originalEndColumn, originalMaxColumn, modifiedLineIndex + 1, modifiedEndColumn, modifiedMaxColumn); } } } originalLineIndex++; modifiedLineIndex++; } if (nextChange) { // Emit the actual change result.push(LineChange.createFromDiffResult(this.shouldIgnoreTrimWhitespace, nextChange, this.original, this.modified, this.continueCharDiff, this.shouldComputeCharChanges, this.shouldPostProcessCharChanges)); originalLineIndex += nextChange.originalLength; modifiedLineIndex += nextChange.modifiedLength; } } return { quitEarly: quitEarly, changes: result }; } _pushTrimWhitespaceCharChange(result, originalLineNumber, originalStartColumn, originalEndColumn, modifiedLineNumber, modifiedStartColumn, modifiedEndColumn) { if (this._mergeTrimWhitespaceCharChange(result, originalLineNumber, originalStartColumn, originalEndColumn, modifiedLineNumber, modifiedStartColumn, modifiedEndColumn)) { // Merged into previous return; } let charChanges = undefined; if (this.shouldComputeCharChanges) { charChanges = [new CharChange(originalLineNumber, originalStartColumn, originalLineNumber, originalEndColumn, modifiedLineNumber, modifiedStartColumn, modifiedLineNumber, modifiedEndColumn)]; } result.push(new LineChange(originalLineNumber, originalLineNumber, modifiedLineNumber, modifiedLineNumber, charChanges)); } _mergeTrimWhitespaceCharChange(result, originalLineNumber, originalStartColumn, originalEndColumn, modifiedLineNumber, modifiedStartColumn, modifiedEndColumn) { const len = result.length; if (len === 0) { return false; } const prevChange = result[len - 1]; if (prevChange.originalEndLineNumber === 0 || prevChange.modifiedEndLineNumber === 0) { // Don't merge with inserts/deletes return false; } if (prevChange.originalEndLineNumber === originalLineNumber && prevChange.modifiedEndLineNumber === modifiedLineNumber) { if (this.shouldComputeCharChanges && prevChange.charChanges) { prevChange.charChanges.push(new CharChange(originalLineNumber, originalStartColumn, originalLineNumber, originalEndColumn, modifiedLineNumber, modifiedStartColumn, modifiedLineNumber, modifiedEndColumn)); } return true; } if (prevChange.originalEndLineNumber + 1 === originalLineNumber && prevChange.modifiedEndLineNumber + 1 === modifiedLineNumber) { prevChange.originalEndLineNumber = originalLineNumber; prevChange.modifiedEndLineNumber = modifiedLineNumber; if (this.shouldComputeCharChanges && prevChange.charChanges) { prevChange.charChanges.push(new CharChange(originalLineNumber, originalStartColumn, originalLineNumber, originalEndColumn, modifiedLineNumber, modifiedStartColumn, modifiedLineNumber, modifiedEndColumn)); } return true; } return false; } } function getFirstNonBlankColumn(txt, defaultValue) { const r = strings.firstNonWhitespaceIndex(txt); if (r === -1) { return defaultValue; } return r + 1; } function getLastNonBlankColumn(txt, defaultValue) { const r = strings.lastNonWhitespaceIndex(txt); if (r === -1) { return defaultValue; } return r + 2; } function createContinueProcessingPredicate(maximumRuntime) { if (maximumRuntime === 0) { return () => true; } const startTime = Date.now(); return () => { return Date.now() - startTime < maximumRuntime; }; }