All files / src/utils diff-file.ts

100% Statements 77/77
97.3% Branches 36/37
100% Functions 10/10
100% Lines 73/73

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 13415x 15x 15x     15x 108x 108x   108x 108x   108x       15x 5x 5x 5x   5x 23x   23x   108x 108x 108x 108x     23x 3x 3x     23x         15x 11x 5x 5x   5x 19x 12x 11x                 15x           5x 5x 5x 5x 5x 5x   5x 5x     5x 19x   19x   19x 6x   6x 6x 13x 5x   5x 5x   8x 8x   8x 8x               19x 4x 2x 2x 2x 2x 1x 1x 1x   1x 1x 1x 1x         5x   5x         5x   5x    
import { diffLines, Change } from 'diff'
import chalk from 'chalk'
import { isNil } from 'ramda'
 
 
const createLineNumberFormater = (maxOldLineNumberLength: number, maxNewLineNumberLength: number) => (oldNumber: number | null, newNumber: number | null) => {
  const oldNumberStr = isNil(oldNumber) ? ' ' : String(oldNumber)
  const newNumberStr = isNil(newNumber) ? ' ' : String(newNumber)
 
  const oldN = oldNumberStr.padEnd(maxOldLineNumberLength, ' ')
  const newN = newNumberStr.padEnd(maxNewLineNumberLength, ' ')
 
  return `${oldN}|${newN} `
}
 
type LineFormater = (oldNumber: number | null, newNumber: number | null, tag: string, str: string, fold?: boolean) => string
const createLineFormater = (maxOldLineNumber: number, maxNewLineNumber: number): LineFormater => {
  const maxOldLineNumberLength = String(maxOldLineNumber).length
  const maxNewLineNumberLength = String(maxNewLineNumber).length
  const formatLineNumber = createLineNumberFormater(maxOldLineNumberLength, maxNewLineNumberLength)
 
  return (oldNumber, newNumber, tag, str, fold) => {
    let lines = str.match(/((.*\n)|(.+$))/g) || []
 
    lines = lines
      .map((line, i) => {
        const oldNumberWithOffset = oldNumber && oldNumber + i
        const newNumberWithOffset = newNumber && newNumber + i
        const lineNumber = formatLineNumber(oldNumberWithOffset, newNumberWithOffset)
        return `${lineNumber} ${tag} ${line.replace(/(\n$)/, '')}\n`
      })
 
    if (fold && lines.length > 2) {
      const dot = '...\n'.padStart(maxOldLineNumberLength + 3, ' ')
      lines.splice(1, lines.length - 2, dot)
    }
 
    return lines.join('')
  }
}
 
type EndLineValidater = (i: number) => boolean
const createEndLineValider = (diffPairs: Change[]): EndLineValidater => {
  const index = [...diffPairs].reverse().findIndex(item => !item.added && !item.removed)
  const count = diffPairs.length - 1
  const lastSamePairIndex = index >= 0 ? count - index : index
 
  return i => {
    if (lastSamePairIndex < i) return true
    else if (lastSamePairIndex === diffPairs.length - 1 && lastSamePairIndex === i) return true
    return false
  }
}
 
 
interface ShowDiffOptions {
  fold?: boolean
}
 
export default function showDiff(
  filename: string,
  oldContent: string,
  newContent: string,
  options: ShowDiffOptions = {}
): string {
  let str = ''
  let oldLineNumber = 1
  let newLineNumber = 1
  const maxOldLineNumber = oldContent.split('\n').length
  const maxNewLineNumber = oldContent.split('\n').length
  const formatLine = createLineFormater(maxOldLineNumber, maxNewLineNumber)
 
  const diffPairs = diffLines(oldContent, newContent)
  const isEndLine = createEndLineValider(diffPairs)
 
 
  diffPairs.forEach(({ added, removed, value }, i) => {
    const needFillEndLine = isEndLine(i)
 
    const inc = value.split('\n').length - 1
 
    if (added) {
      const strWithoutColor = formatLine(null, newLineNumber, '+', value)
 
      str += chalk.green(strWithoutColor)
      newLineNumber += inc
    } else if (removed) {
      const strWithoutColor = formatLine(oldLineNumber, null, '-', value)
 
      str += chalk.red(strWithoutColor)
      oldLineNumber += inc
    } else {
      const strWithoutColor = formatLine(oldLineNumber, newLineNumber, ' ', value, options.fold)
      str += chalk.grey(strWithoutColor)
 
      newLineNumber += inc
      oldLineNumber += inc
    }
 
    /**
     * Add an empty line,
     * if '\n' at the end of file.
     * So, It's easy to tell if the last line end with '\n'
     */
    if (needFillEndLine && /\n$/.test(value)) {
      if (added) {
        const strWithoutColor = formatLine(null, newLineNumber, '+', '\n')
        str += chalk.green(strWithoutColor)
        newLineNumber += 1
      } else if (removed) {
        const strWithoutColor = formatLine(oldLineNumber, null, '-', '\n')
        str += chalk.red(strWithoutColor)
        oldLineNumber += 1
      } else {
        const strWithoutColor = formatLine(oldLineNumber, newLineNumber, ' ', '\n')
        str += chalk.grey(strWithoutColor)
        newLineNumber += 1
        oldLineNumber += 1
      }
    }
  })
 
  const headerLength = filename.length + 4
 
  const header = chalk.yellow([
    Array(headerLength).fill('=').join(''),
    `  ${filename}  `,
    Array(headerLength).fill('-').join(''),
  ].join('\n'))
  const footer = chalk.yellow(Array(headerLength).fill('=').join(''))
 
  return ['\n', header, str, footer].join('\n')
}