/** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ import type { DiffOptions, DiffOptionsNormalized } from './types' import diffSequences from 'diff-sequences' import { Diff, DIFF_DELETE, DIFF_EQUAL, DIFF_INSERT } from './cleanupSemantic' import { joinAlignedDiffsExpand, joinAlignedDiffsNoExpand, } from './joinAlignedDiffs' import { normalizeDiffOptions } from './normalizeDiffOptions' function isEmptyString(lines: Array) { return lines.length === 1 && lines[0].length === 0 } interface ChangeCounts { a: number b: number } function countChanges(diffs: Array): ChangeCounts { let a = 0 let b = 0 diffs.forEach((diff) => { switch (diff[0]) { case DIFF_DELETE: a += 1 break case DIFF_INSERT: b += 1 break } }) return { a, b } } function printAnnotation( { aAnnotation, aColor, aIndicator, bAnnotation, bColor, bIndicator, includeChangeCounts, omitAnnotationLines, }: DiffOptionsNormalized, changeCounts: ChangeCounts, ): string { if (omitAnnotationLines) { return '' } let aRest = '' let bRest = '' if (includeChangeCounts) { const aCount = String(changeCounts.a) const bCount = String(changeCounts.b) // Padding right aligns the ends of the annotations. const baAnnotationLengthDiff = bAnnotation.length - aAnnotation.length const aAnnotationPadding = ' '.repeat(Math.max(0, baAnnotationLengthDiff)) const bAnnotationPadding = ' '.repeat(Math.max(0, -baAnnotationLengthDiff)) // Padding left aligns the ends of the counts. const baCountLengthDiff = bCount.length - aCount.length const aCountPadding = ' '.repeat(Math.max(0, baCountLengthDiff)) const bCountPadding = ' '.repeat(Math.max(0, -baCountLengthDiff)) aRest = `${aAnnotationPadding} ${aIndicator} ${aCountPadding}${aCount}` bRest = `${bAnnotationPadding} ${bIndicator} ${bCountPadding}${bCount}` } const a = `${aIndicator} ${aAnnotation}${aRest}` const b = `${bIndicator} ${bAnnotation}${bRest}` return `${aColor(a)}\n${bColor(b)}\n\n` } export function printDiffLines( diffs: Array, truncated: boolean, options: DiffOptionsNormalized, ): string { return ( printAnnotation(options, countChanges(diffs)) + (options.expand ? joinAlignedDiffsExpand(diffs, options) : joinAlignedDiffsNoExpand(diffs, options)) + (truncated ? options.truncateAnnotationColor(`\n${options.truncateAnnotation}`) : '') ) } // Compare two arrays of strings line-by-line. Format as comparison lines. export function diffLinesUnified( aLines: Array, bLines: Array, options?: DiffOptions, ): string { const normalizedOptions = normalizeDiffOptions(options) const [diffs, truncated] = diffLinesRaw( isEmptyString(aLines) ? [] : aLines, isEmptyString(bLines) ? [] : bLines, normalizedOptions, ) return printDiffLines(diffs, truncated, normalizedOptions) } // Given two pairs of arrays of strings: // Compare the pair of comparison arrays line-by-line. // Format the corresponding lines in the pair of displayable arrays. export function diffLinesUnified2( aLinesDisplay: Array, bLinesDisplay: Array, aLinesCompare: Array, bLinesCompare: Array, options?: DiffOptions, ): string { if (isEmptyString(aLinesDisplay) && isEmptyString(aLinesCompare)) { aLinesDisplay = [] aLinesCompare = [] } if (isEmptyString(bLinesDisplay) && isEmptyString(bLinesCompare)) { bLinesDisplay = [] bLinesCompare = [] } if ( aLinesDisplay.length !== aLinesCompare.length || bLinesDisplay.length !== bLinesCompare.length ) { // Fall back to diff of display lines. return diffLinesUnified(aLinesDisplay, bLinesDisplay, options) } const [diffs, truncated] = diffLinesRaw( aLinesCompare, bLinesCompare, options, ) // Replace comparison lines with displayable lines. let aIndex = 0 let bIndex = 0 diffs.forEach((diff: Diff) => { switch (diff[0]) { case DIFF_DELETE: diff[1] = aLinesDisplay[aIndex] aIndex += 1 break case DIFF_INSERT: diff[1] = bLinesDisplay[bIndex] bIndex += 1 break default: diff[1] = bLinesDisplay[bIndex] aIndex += 1 bIndex += 1 } }) return printDiffLines(diffs, truncated, normalizeDiffOptions(options)) } // Compare two arrays of strings line-by-line. export function diffLinesRaw( aLines: Array, bLines: Array, options?: DiffOptions, ): [Array, boolean] { const truncate = options?.truncateThreshold ?? false const truncateThreshold = Math.max( Math.floor(options?.truncateThreshold ?? 0), 0, ) const aLength = truncate ? Math.min(aLines.length, truncateThreshold) : aLines.length const bLength = truncate ? Math.min(bLines.length, truncateThreshold) : bLines.length const truncated = aLength !== aLines.length || bLength !== bLines.length const isCommon = (aIndex: number, bIndex: number) => aLines[aIndex] === bLines[bIndex] const diffs: Array = [] let aIndex = 0 let bIndex = 0 const foundSubsequence = ( nCommon: number, aCommon: number, bCommon: number, ) => { for (; aIndex !== aCommon; aIndex += 1) { diffs.push(new Diff(DIFF_DELETE, aLines[aIndex])) } for (; bIndex !== bCommon; bIndex += 1) { diffs.push(new Diff(DIFF_INSERT, bLines[bIndex])) } for (; nCommon !== 0; nCommon -= 1, aIndex += 1, bIndex += 1) { diffs.push(new Diff(DIFF_EQUAL, bLines[bIndex])) } } diffSequences(aLength, bLength, isCommon, foundSubsequence) // After the last common subsequence, push remaining change items. for (; aIndex !== aLength; aIndex += 1) { diffs.push(new Diff(DIFF_DELETE, aLines[aIndex])) } for (; bIndex !== bLength; bIndex += 1) { diffs.push(new Diff(DIFF_INSERT, bLines[bIndex])) } return [diffs, truncated] }